Skip to content

feat(core): let a workflow agent pin a directory and outlive the default bounds - #8972

Open
qqqys wants to merge 16 commits into
QwenLM:mainfrom
qqqys:workflow/agent-working-dir
Open

feat(core): let a workflow agent pin a directory and outlive the default bounds#8972
qqqys wants to merge 16 commits into
QwenLM:mainfrom
qqqys:workflow/agent-working-dir

Conversation

@qqqys

@qqqys qqqys commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Three changes that together let a workflow subagent do work that is neither short nor in-place.

A workflow script can pin an agent to a directory. agent({workingDir}) runs that agent inside an existing git worktree the caller already owns — nothing is created, nothing is removed, and the child's cwd surfaces are rebound so its file, shell and search tools resolve inside it. This is the same contract the Agent tool already exposes as working_dir, and it reuses the same validation: the path must resolve inside the repository and must be a worktree git actually knows about.

Two details make the difference between working and silently not working, and both are handled. The fast dispatch path hands the run's Config to the agent untouched and has no way to honour a directory rebind, so workingDir forces the override path — left on the fast path it would be dropped without a word and the agent would run in the parent working tree, which is the exact failure the option exists to prevent. And the resume key projection now includes workingDir, because the same prompt run against two worktrees is two different questions; without it, a resume that changed only the directory would replay the previous tree's answers as this one's.

The validation itself moves out of the Agent tool into a small shared module. It is the load-bearing half and not worth two copies: the path comes from a model either way — a tool call's argument, or a line in a workflow script — and pinning replaces the child's workspace boundary wholesale. The caller passes the name of its own parameter, so a script reads workingDir "…" in the error and a tool call reads working_dir "…".

The per-subagent resource bounds become operator-tunable. A workflow subagent was capped at 50 turns and 10 minutes, hard-coded at both dispatch sites with no override, while the three other workflow bounds all have one. Both now honour an env override with a hard ceiling, on the same contract as the agent cap: a non-integer or sub-1 value is rejected with a warning and the default is used, and a value above the ceiling is clamped. The doc comment also states how the three time bounds differ from one another, since raising one without the others just moves which limit kills the run.

A regression test pins the headless foreground contract. A foreground workflow call must complete with no interactive session and no completion channel.

Why it's needed

isolation: 'worktree' is not a substitute for pinning. It creates a worktree from the current tree and refuses to run when the parent tree is dirty. That is the opposite of what a caller needs when the directory already exists and its uncommitted state is the whole point — a review worktree, a scratch checkout a previous step provisioned, anything whose lifetime the caller owns. Today a script simply cannot express that, so any workflow whose agents must work somewhere other than the session's own directory has no way to say so.

The turn and time bounds matter for the same class of work. A build-and-test agent, or an analysis of a two-thousand-line file that has to page through large reads, exceeds 50 turns or 10 minutes routinely. Under the terminal-state contract, being cut off does not surface as a visible failure: the dispatch throws, parallel() turns it into a null element, and the caller sees an agent that silently went missing. An operator who hits this has no knob at all today — the two ceilings are the only workflow bounds without one, and at the override site they beat the agent type's own configuration.

The headless test guards a path with no signal today. qwen --prompt — CI, cron, any unattended run — has no TUI, no approval bridge and a closed stdin. The workflow tool's default permission is ask, which the scheduler resolves against the run's approval mode; that is fine, but it means nothing inside the tool or the runner may reach for interactivity, or the foreground call would hang forever on a prompt nobody can answer. The background half is already refused explicitly with a clear error; the foreground half had nothing pinning it.

Reviewer Test Plan

How to verify

cd packages/core
npx vitest run src/agents/ src/tools/

Expected: all pass. On this branch, 4250 passed | 6 skipped (4256), 127 files passed | 1 skipped.

New tests, and what each is for:

src/agents/worktree-pin.test.ts covers the shared resolver against a mocked git worktree service — a registered worktree inside the repo resolves; a path outside the repository, an unregistered directory, missing git tooling, and a non-repository parent each refuse with the actual cause named rather than a generic message; a detached-HEAD worktree with no branch is accepted, since the branch is a label and never a gate; and the caller's parameter name appears in the error text.

src/agents/runtime/workflow-orchestrator.test.ts treats that resolver as a seam and asserts what the orchestrator does with its verdict: a workingDir dispatch leaves the fast path (the subagent is created through the manager, and the fast-path constructor is never called), the subagent's Config answers with the pinned directory rather than the parent's, a refusal aborts the dispatch without creating an agent at all, and the resolver is told the workflow opt's own name. The same file covers the two env-tunable bounds — defaults, valid overrides, clamping above the ceiling, and rejection of 0 / abc / 2.5 / 0x10 / 1e3.

src/agents/runtime/workflow-sandbox.test.ts covers the script-facing surface: workingDir reaches dispatch, a non-string is refused, and workingDir together with isolation is refused as a contradiction rather than resolved by precedence — a script that got a silent winner would believe it was isolated when it was pinned, or the reverse.

src/agents/runtime/workflow-journal.test.ts asserts two dispatches identical but for workingDir derive different resume keys.

src/tools/workflow/workflow.test.ts runs a foreground workflow to completion against a config with no interactive session and no completion channel.

To exercise the pin by hand: create a worktree with git worktree add ../wt-demo, then run a workflow with QWEN_CODE_ENABLE_WORKFLOWS=1 whose script calls agent('run pwd and report it', { workingDir: '../wt-demo' }). The agent reports the worktree path; without this change the same script is rejected as an unknown option.

Evidence (Before & After)

N/A — no user-visible or TUI change. The user-facing surface is a new agent() option and two new env variables, both documented in the tool description and the code.

Tested on

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

Environment (optional)

Unit tests only (vitest, Node 22, Linux).

Risk & Scope

  • Main risk or tradeoff: workingDir rebinds the subagent's workspace boundary, so the validation is the security-relevant part of this PR — it is shared with the Agent tool rather than reimplemented precisely so the two cannot drift apart, but a reviewer should read agents/worktree-pin.ts as the load-bearing file. Raising the per-subagent bounds lets a single agent burn more tokens and wall clock than before; the defaults are unchanged, both overrides are clamped, and the run-level agent cap and wall clock still bound the whole run.
  • Not validated / out of scope: no live workflow run was executed against a real pinned worktree — the pin is covered by unit tests over the rebind and the resolver, not by an end-to-end run. The headless test asserts the tool and runner complete without interactivity; it does not exercise a real CI invocation, and the approval behaviour of tools called inside a headless workflow subagent is unchanged and untested here. No default is changed by this PR.
  • Breaking changes / migration notes: none. workingDir is additive and rejected in combination with isolation; both env variables are opt-in; moving the resolver into a shared module changes no behaviour for the Agent tool, whose error text is byte-identical because the default parameter name is working_dir.

Linked Issues

Part of #8769.

中文说明

这个 PR 做了什么

三处改动,合起来让 workflow 子 agent 能做既不短、也不在原地的工作。

workflow 脚本可以把一个 agent 钉在某个目录上。 agent({workingDir}) 让该 agent 在调用方已经拥有的、既有的 git worktree 里运行——不创建、不删除,并且子 agent 的「我在哪」相关面被重新绑定,使其文件、shell 与搜索工具都落在该目录内。这与 Agent 工具已经暴露的 working_dir 是同一套契约,并复用同一套校验:路径必须落在仓库内部,且必须是 git 真正登记过的 worktree。

有两个细节决定了它是「能用」还是「悄悄不生效」,两者都处理了。快速派发路径把运行时 Config 原样交给 agent,没有任何办法执行目录重绑定,所以 workingDir 会强制走 override 路径——如果留在快速路径上,它会被一声不吭地丢弃,agent 转而在父工作树里运行,而这正是这个选项要防止的失败。另外,resume key 的投影现在包含 workingDir,因为同一个 prompt 跑在两个 worktree 上是两个不同的问题;否则一次只改了目录的 resume 会把上一棵树的答案当成这一棵树的答案重放。

校验逻辑本身从 Agent 工具中移入一个小的共享模块。它是承重的那一半,不值得存在两份:无论哪条路径,路径都来自模型——工具调用的参数,或 workflow 脚本里的一行——而钉住会整体替换子 agent 的工作区边界。调用方传入自己那一侧的参数名,因此脚本在错误里读到的是 workingDir "…",工具调用读到的是 working_dir "…"

单个子 agent 的资源上限变为运维可调。 workflow 子 agent 此前被限制在 50 轮与 10 分钟,在两个派发点硬编码且没有任何覆盖手段,而其余三个 workflow 上限都有。现在两者都支持带硬上限的环境变量覆盖,契约与 agent 数量上限一致:非整数或小于 1 的值会被拒绝并给出警告、回退到默认值,高于硬上限的值会被夹紧。文档注释同时说明了三个时间上限彼此的分工,因为只抬高其中一个而不管其余,只是换成另一个上限来杀掉这次运行。

一个回归测试钉住 headless 前台契约。 前台的 workflow 调用必须在没有交互式会话、也没有完成通道的情况下跑完。

为什么需要

isolation: 'worktree' 不能替代「钉住」。它是从当前树新建一个 worktree,并且在父树有未提交改动时拒绝运行。当目录已经存在、而且其未提交状态正是重点时——一个 review worktree、上一步准备好的临时检出、任何生命周期由调用方掌握的目录——这恰恰是相反的语义。今天脚本根本无法表达这一点,因此任何需要让 agent 在会话自身目录之外工作的 workflow,都没有办法说出这个需求。

轮数与时间上限影响的是同一类工作。一个构建与测试的 agent,或者对一个两千行文件的分析(需要分页读完大段内容),例行地会超过 50 轮或 10 分钟。在终态契约下,被切断不会表现为可见的失败:派发抛错,parallel() 把它变成一个 null 元素,调用方看到的是一个悄悄消失的 agent。今天撞上这一点的运维方没有任何旋钮——这两个上限是唯一没有覆盖手段的 workflow 上限,而且在 override 站点它们会盖过 agent 类型自身的配置。

headless 测试守护的是一条今天没有任何信号的路径。qwen --prompt——CI、cron、任何无人值守的运行——没有 TUI、没有审批桥接、stdin 是关闭的。workflow 工具的默认权限是 ask,由调度器结合运行的审批模式解析;这没有问题,但它意味着工具与 runner 内部不得有任何地方去索取交互,否则前台调用会永远挂在一个没人能回答的确认框上。后台那一半已经用明确的错误拒绝掉了;前台这一半此前没有任何东西钉住。

审阅者验证方案

如何验证

cd packages/core
npx vitest run src/agents/ src/tools/

预期全部通过。本分支上为 4250 passed | 6 skipped (4256)127 files passed | 1 skipped

新增测试,以及各自的用途:

src/agents/worktree-pin.test.ts 针对被 mock 的 git worktree 服务覆盖共享校验器——仓库内已登记的 worktree 可以解析通过;仓库之外的路径、未登记的目录、缺失的 git 工具、非仓库的父目录,各自以真实原因而非笼统消息拒绝;处于 detached HEAD、没有分支的 worktree 会被接受,因为分支只是标签、从来不是关卡;并且调用方的参数名会出现在错误文本里。

src/agents/runtime/workflow-orchestrator.test.ts 把该校验器当作接缝,断言编排器拿到裁决后的行为:带 workingDir 的派发会离开快速路径(子 agent 经由 manager 创建,快速路径的构造函数完全没有被调用)、子 agent 的 Config 回答的是被钉住的目录而不是父目录、被拒绝时派发中止且根本不创建 agent、以及校验器被告知的是 workflow 侧选项自己的名字。同一文件覆盖两个可调上限——默认值、有效覆盖、超上限夹紧,以及对 0 / abc / 2.5 / 0x10 / 1e3 的拒绝。

src/agents/runtime/workflow-sandbox.test.ts 覆盖面向脚本的接口:workingDir 能到达派发层、非字符串被拒绝、workingDirisolation 同时出现时作为矛盾被拒绝而不是按优先级择一——如果脚本拿到一个无声的胜者,它会以为自己被隔离了而实际是被钉住,或者相反。

src/agents/runtime/workflow-journal.test.ts 断言两次仅 workingDir 不同的派发会派生出不同的 resume key。

src/tools/workflow/workflow.test.ts 针对一个没有交互式会话、也没有完成通道的 config,把前台 workflow 跑到结束。

若要手动验证钉住效果:用 git worktree add ../wt-demo 创建一个 worktree,然后用 QWEN_CODE_ENABLE_WORKFLOWS=1 跑一个脚本调用 agent('run pwd and report it', { workingDir: '../wt-demo' }) 的 workflow。agent 会报告该 worktree 路径;没有本改动时,同一脚本会因未知选项被拒绝。

证据(前后对比)

N/A —— 没有用户可见或 TUI 变化。面向用户的接口是一个新的 agent() 选项和两个新的环境变量,均已写入工具描述与代码注释。

测试环境

操作系统 状态
🍏 macOS ⚠️
🪟 Windows ⚠️
🐧 Linux

环境(可选)

仅单元测试(vitest,Node 22,Linux)。

风险与范围

  • 主要风险或权衡:workingDir 会重新绑定子 agent 的工作区边界,所以校验是本 PR 中与安全相关的部分——它与 Agent 工具共享而非重新实现,正是为了两者不会漂移,但审阅者应把 agents/worktree-pin.ts 当作承重文件来读。抬高单个子 agent 的上限意味着一个 agent 可以比以前烧掉更多 token 与墙钟时间;默认值未变,两个覆盖都会被夹紧,运行级的 agent 数量上限与墙钟仍然约束整次运行。
  • 未验证 / 范围之外:没有针对真实的被钉住 worktree 执行过实际 workflow 运行——钉住由覆盖重绑定与校验器的单元测试保证,而非端到端运行。headless 测试断言工具与 runner 在无交互下跑完;它没有真正跑一次 CI 调用,headless workflow 子 agent 内部所调用工具的审批行为未做改动,本 PR 也未对其测试。本 PR 不改变任何默认值。
  • 破坏性变更 / 迁移说明:无。workingDir 是增量的,且与 isolation 同时出现时会被拒绝;两个环境变量都是选择性启用;把校验器移入共享模块对 Agent 工具的行为没有任何改变,其错误文本逐字节一致,因为默认参数名就是 working_dir

关联 Issue

Part of #8769.

…ult bounds

Three gaps that together keep workflow subagents to short, in-place work.

**`agent({workingDir})`.** A script had no way to run an agent inside a
directory. `isolation: 'worktree'` is not a substitute: it CREATES a
worktree from the current tree and refuses to run when the parent tree is
dirty — the opposite of pinning an agent to a directory whose uncommitted
state is the point (a review worktree, a checkout a previous step
provisioned). `workingDir` is the same contract `AgentTool` already
exposes as `working_dir`: an existing, caller-owned worktree that the
harness neither creates nor removes.

Two details are easy to get wrong and both are covered:

- The fast path hands `config` to `AgentHeadless` untouched and cannot
  honour a rebind, so `workingDir` forces the override path. Left on the
  fast path it would be dropped in silence and the agent would run in the
  parent tree — the failure the option exists to prevent.
- `canonicalizeAgentOpts` now projects `workingDir`. The same prompt run
  against two worktrees is two different questions; without the
  projection a resume that changed only the directory would replay the
  previous tree's answers as this one's.

The validation moves to `agents/worktree-pin.ts`, shared with `AgentTool`
rather than duplicated: the path comes from a model either way, and
pinning replaces the child's `WorkspaceContext` wholesale, so it must
resolve inside the repository and be a registered linked worktree. The
caller passes the parameter name so errors say `workingDir` to a script
and `working_dir` to a tool call.

**Tunable per-subagent bounds.** `max_turns: 50` / `max_time_minutes: 10`
were hard-pinned at both dispatch sites with no override, while the three
other workflow bounds all have one. A build-and-test agent, or an
analysis of a 2 000-line file, exceeds them routinely — and under the
GOAL-terminal contract being cut off surfaces as a `null` element, an
agent that silently went missing rather than one that visibly failed.
Both are now env-tunable and clamped, and the doc comment states how
`stallMs`, `max_time_minutes` and the run wall clock differ, since
raising one without the others just moves which limit kills the run.

**Headless regression test.** A foreground `Workflow` call must complete
with no interactive session and no completion channel: `qwen --prompt`
has no TUI, no approval bridge and a closed stdin, so anything reaching
for interactivity inside the tool or runner would hang on a prompt nobody
can answer. The background half was already refused explicitly; this
pins the foreground half.

Part of QwenLM#8769.
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 12, 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 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.

Thanks for the PR, @qqqys — the write-up itself is genuinely detailed, but it doesn't follow the repository's PR template, so the gate has to stop here before code review. This is a formatting gate, not a code concern.

The body is missing all of the required sections from pull_request_template.md (it currently uses a custom agent({workingDir}) / Tunable per-subagent bounds / Headless regression test / Tests structure):

  • What this PR does — prose description of the change
  • Why it's needed — the motivation is already in your description; it just needs this section
  • Reviewer Test Plan, with its three subsections:
    • How to verify — the behaviors a reviewer should confirm and what to expect: e.g. a Workflow call with workingDir pins the subagent to that worktree and stays off the fast path, an invalid path aborts the dispatch with the cause named, the env-tunable bounds clamp/reject as documented, and which suites pin that
    • Evidence (Before & After) — this is internal workflow-engine behavior rather than a TUI surface change, so N/A is fine here per the template, with commands and output under How to verify
    • Tested on — the OS matrix (🍏/🪟/🐧 with ✅/⚠️/N/A); right now it's unclear where your verification ran
  • Risk & Scope — the three bullets: main risk or tradeoff (e.g. forcing the override path when workingDir is set) / not validated / breaking changes
  • Linked Issues — reference #8769 without a closing keyword, since this is part of that proposal
  • The <details> Chinese translation of the body

Could you restructure the body to follow the template? The content you already wrote is good — most of it can be moved into the right sections as-is. Please keep each paragraph or list item as one long line (the template notes that GitHub renders single newlines as <br>, so hard-wrapped text displays as a narrow column).

Once the body is updated, a maintainer can re-run triage with @qwen-code /triage to continue.

中文说明

感谢提交 PR,@qqqys——描述本身写得很详细,但没有遵循仓库的 PR 模板,所以 gate 在代码审查之前先停在这里。这是一次格式上的拦截,而不是对代码的质疑。

正文缺少 pull_request_template.md 要求的所有章节(目前使用了自定义的 agent({workingDir}) / Tunable per-subagent bounds / Headless regression test / Tests 结构):

  • What this PR does——用散文描述改动
  • Why it's needed——动机在你的描述里已经写了,只需要放到这个章节
  • Reviewer Test Plan,包含三个子章节:
    • How to verify——评审者应确认的行为和预期结果:例如带 workingDirWorkflow 调用会把子代理固定到该 worktree 并绕开 fast path、非法路径会中止分发并说明原因、环境变量可调上限按文档所述钳制/拒绝,以及哪些测试套件固定了这些行为
    • Evidence (Before & After)——这是 workflow 引擎内部行为而非 TUI 界面改动,按模板写 N/A 即可,命令与输出放在 How to verify 下
    • Tested on——操作系统矩阵(🍏/🪟/🐧 加 ✅/⚠️/N/A);目前无法判断你的验证是在哪个平台上进行的
  • Risk & Scope——三个要点:主要风险或权衡(例如设置 workingDir 时强制走 override 路径)/ 未验证项 / 破坏性变更
  • Linked Issues——引用 #8769(不使用关闭关键字,因为这是该提案的一部分)
  • 正文的 <details> 中文翻译

能否按模板重构正文?你已经写好的内容是好的——多数可以直接挪到对应章节。请保持每个段落或列表项为一长行(模板注明 GitHub 会把单个换行渲染成 <br>,硬换行的文字会显示成窄列)。

正文更新后,维护者可以用 @qwen-code /triage 重新触发 triage 继续流程。

Qwen Code · qwen3.8-max

@qqqys
qqqys dismissed qwen-code-ci-bot’s stale review August 12, 2026 04:08

已被后续 commit 取代,当前 head 需重新 review

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

Reviewed. Suggestions are inline.

Not explored to full depth (tool budget reached): You are review agent reverse-audit — Reverse audit agen...: none — all checks above completed within budget.; You are review agent reverse-audit — Reverse audit agen...: none — all checks above completed within budget..

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

Test Plan (not a blocker): src/agents/worktree-pin.test.tsno such file or directory; src/agents/runtime/workflow-orchestrator.test.tsno such file or directory; src/agents/runtime/workflow-sandbox.test.tsno such file or directory; src/agents/runtime/workflow-journal.test.tsno such file or directory; src/tools/workflow/workflow.test.tsno such file or directory; and 1 more.

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

Comment on lines +795 to +796
} else if (typeof opts.workingDir === 'string' && opts.workingDir) {
// Caller-owned worktree: same rebind, no provisioning and no cleanup.

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.

[Suggestion] Guard asymmetry: a defined-but-falsy workingDir skips the rebind in silence.

The fast-path gate above treats opts.workingDir !== undefined as "must take the override path" (the fast-path condition requires opts.workingDir === undefined), but this branch only pins when typeof opts.workingDir === 'string' && opts.workingDir is truthy. A defined-but-falsy workingDir ('' or a non-string) therefore bypasses the fast path, fails this truthiness check, and the subagent runs in the parent working tree with no error — the exact silent misdirection this option exists to prevent.

Failure scenario: a caller invoking the exported WorkflowAgentDispatch seam with { workingDir: '' } gets an agent silently running in the parent tree — probe-reproduced at this commit: dispatch went through the override path with the parent Config untouched, resolver never consulted, no error. Latent today because the sandbox validates non-empty strings upstream.

Make the guards symmetric — branch on opts.workingDir !== undefined and throw for an invalid value:

} else if (opts.workingDir !== undefined) {
  if (typeof opts.workingDir !== 'string' || !opts.workingDir) {
    throw new Error(
      'agent({workingDir}): must be a non-empty string naming an existing git worktree of this repository.',
    );
  }

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

已修复:主机侧 dispatch 现在也会拒绝空 workingDir,避免绕过 sandbox 时静默落回父工作区。验证证据:orchestrator 141/141、sandbox 151/151 通过;Core typecheck、ESLint、Prettier、diff check 通过。

Comment on lines +1678 to +1680
await expect(
sandbox.run(`return agent("x", { workingDir: 7 });`),
).rejects.toThrow(/workingDir.*non-empty string/);

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.

[Suggestion] The empty-string half of the sandbox guard has no test — only the non-string half (workingDir: 7) is exercised here, and no test in packages/core exercises workingDir: ''.

The clause is load-bearing: if || agentOpts.workingDir.length === 0 were dropped from workflow-sandbox.ts, agent("x", { workingDir: "" }) would pass sandbox validation and reach the orchestrator — where the fast-path gate is false for '' but the rebind branch is truthy-gated — so no rebind, no error, and the agent silently runs in the parent working tree. No test would fail.

Suggested change
await expect(
sandbox.run(`return agent("x", { workingDir: 7 });`),
).rejects.toThrow(/workingDir.*non-empty string/);
await expect(
sandbox.run(`return agent("x", { workingDir: 7 });`),
).rejects.toThrow(/workingDir.*non-empty string/);
await expect(
sandbox.run(`return agent("x", { workingDir: "" });`),
).rejects.toThrow(/workingDir.*non-empty string/);

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

已修复:sandbox 回归现在同时覆盖非字符串和空字符串 workingDir。验证证据:workflow-sandbox 151/151 通过。

Comment on lines +2473 to +2475
expect(
resolveSubagentMaxTurns({ QWEN_CODE_WORKFLOW_AGENT_MAX_TURNS: '120' }),
).toBe(120);

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.

[Suggestion] The env-tunable bounds are only tested at resolver level — the dispatch wiring survives a revert mutation.

Mutation check at this commit: replacing resolveSubagentMaxTurns() / resolveSubagentMaxTimeMinutes() with the DEFAULT_* constants at both call sites (fast path ~547-548, override path ~885-886) keeps all 139 tests in this file green — the pre-existing wiring test asserts exactly { max_turns: 50, max_time_minutes: 10 } (identical for constants and resolvers in a clean env), and the override path's captured runConfigOverrides is recorded but never asserted.

Failure scenario: an operator sets QWEN_CODE_WORKFLOW_AGENT_MAX_TURNS=120 to stop long agents being cut to null in parallel(); a future refactor silently ignores the env var at the dispatch sites, and the suite blesses it.

Add one test per path that stubs the env and asserts the dispatched runConfig reflects the override — this shape was probe-verified to fail against the mutated code and pass against the original:

vi.stubEnv('QWEN_CODE_WORKFLOW_AGENT_MAX_TURNS', '120');
// assert created[0].runConfig.max_turns === 120 (fast path)
// and the captured runConfigOverrides.max_turns === 120 (override path)

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

Comment on lines +361 to +363
it('foreground execute() completes with no interactive session or completion channel', async () => {
const registry = new WorkflowRunRegistry();
const config = {

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.

[Suggestion] This test pins only the tool layer — the end-to-end contract issue #8769 P0 #2 names is not exercised anywhere.

P0 #2 requires a foreground Workflow call to work end-to-end non-interactively under the appropriate approval mode: getDefaultPermission() is 'ask', resolved by the scheduler against the run's approval mode. This test constructs the tool directly with isInteractive: () => false and calls execute() — it never exercises approval-mode resolution of the 'ask' default permission, and no integration test covers a headless Workflow run. The comment above calls this "the regression test for that contract".

Failure scenario: if a future change introduces an interactive prompt into the scheduler/permission path for ask-default tools in headless runs, the qwen --prompt path (CI, cron — closed stdin) hangs on a prompt nobody can answer while this unit test stays green.

Consider adding — or tracking as an explicit follow-up before Phase 1 — an end-to-end headless check, e.g. an integration test running a Workflow call via qwen --prompt under yolo/auto approval with stdin closed, since the unit layer cannot represent approval-mode resolution.

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

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.

Deferred as an explicit follow-up before Phase 1, rather than implemented in this round. The unit-layer contract this PR owns — a foreground Workflow call completing with no interactive session and no completion channel — is pinned by the headless regression test added here. What the finding correctly notes is that approval-mode resolution of the 'ask' default permission happens in the scheduler against the run's approval mode, which the unit layer cannot represent; a genuine end-to-end check needs new integration-harness scaffolding (bundled CLI running a Workflow call via --prompt under yolo/auto approval with stdin closed, against a mock model endpoint — no Workflow integration harness exists today). Adding that harness would grow this PR well past its original intent, so it is tracked as a follow-up instead of being silently dropped.

中文说明

作为 Phase 1 之前的明确后续事项推迟,本轮不实现。本 PR 所保证的单元层契约——前台 Workflow 调用在没有交互式会话、没有 completion channel 的情况下完成——已经由此处新增的 headless 回归测试固定下来。该发现正确指出:'ask' 默认权限的 approval-mode 解析发生在调度器中、针对本次运行的 approval mode 进行,而单元层无法表达这一点;真正的端到端检查需要新的集成测试脚手架(通过 --promptyolo/auto 审批模式下、stdin 关闭、对着 mock 模型端点运行一次 Workflow 调用的打包 CLI——目前并不存在 Workflow 集成脚手架)。加入该脚手架会使本 PR 远超其原始意图,因此改为作为后续事项跟踪,而不是被静默丢弃。

Comment on lines +81 to +87
for (const k of [
'schema',
'model',
'isolation',
'agentType',
'workingDir',
] as const) {

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.

[Suggestion] The fileoverview contract description contradicts this projection change.

This diff adds workingDir to the projection (here), and the function-level doc argues it is load-bearing for cache correctness — but the file's @fileoverview (lines 23-26) still asserts the projection "keeps only the dispatch-affecting opts (schema, model, isolation, agentType)" — the exact opposite of the implemented behavior, for the one opt where cache-correctness is a safety property. The fileoverview is the module's authoritative description of resume-key derivation ("Key derivation (matches upstream v2)"), and it is what a reader meets first.

Failure scenario: a maintainer diagnosing resume-cache behavior — "why did changing only workingDir force a re-run?" — reads the fileoverview, concludes workingDir is cosmetic and projected away, and "fixes" the key by removing it — restoring the cross-directory replay hole this PR closes.

Update the fileoverview sentence to the new set: "keeps only the dispatch-affecting opts (schema, model, isolation, agentType, workingDir)".

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

已修复:journal fileoverview 已把 workingDir 纳入 dispatch-affecting canonical options。验证证据:Prettier、ESLint、diff check 通过。

);
if ('error' in resolved) {
throw new Error(
`agent({workingDir: ${JSON.stringify(opts.workingDir)}}): ${resolved.error}`,

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.

[Suggestion] This refusal message escapes opts.workingDir via JSON.stringify but interpolates resolved.error raw — and every refusal variant in worktree-pin.ts embeds the script-supplied resolvedPath unescaped, so control characters reach the error text anyway.

Probe-reproduced: workingDir: 'foo\r\n[audit] injected' passes the sandbox's only validation (non-empty string) and the thrown error carries raw CRLF — surfaced to logs/display/OTLP — even though the adjacent agentType branch carries an explicit SECURITY comment applying sanitizeForErrorMessage for exactly this fragmentation class, and the first half of this very message is escaped.

Suggested change
`agent({workingDir: ${JSON.stringify(opts.workingDir)}}): ${resolved.error}`,
`agent({workingDir: ${JSON.stringify(opts.workingDir)}}): ${sanitizeForErrorMessage(resolved.error)}`,

Ideally also sanitize resolvedPath where the worktree-pin error strings are built, so both surfaces are covered (the AgentTool surface shares this module).

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

已修复:resolver 返回的模型可控错误文本在拼接前会移除控制字符,并增加 CR/LF/NUL 回归。验证证据:workflow-orchestrator 141/141 通过;ESLint 通过。

* methods would otherwise still resolve through the prototype to the parent.
*/
function createWorktreeConfigOverride(base: Config, wtPath: string): Config {
function createDirScopedConfigOverride(base: Config, wtPath: string): Config {

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.

[Suggestion] The shared dir-scoped rebind drops user-configured customIgnoreFiles — a mirror gap vs the AgentTool block the doc comment claims to mirror.

The body below (~1199) builds new FileDiscoveryService(wtPath) without customIgnoreFiles, while AgentTool's inline rebind (agent.ts ~2864-2867) passes this.config.getFileFilteringOptions().customIgnoreFiles. Probe-demonstrated with a temp-dir fixture (.cursorignore listing secret.txt): this helper's construction shape yields shouldQwenIgnoreFile('secret.txt') = false — the secret surfaces in ls/read-file/grep inside the pinned worktree — while the AgentTool shape yields true. The identical one-liner pre-existed for isolation: 'worktree', but this PR extracts it into this helper, wires the new workingDir surface through it, and asserts surface parity in the doc comment. AgentTool has a regression test for the carry-through (agent.test.ts ~2083-2127); the workflow surface has none.

const wtFileService = new FileDiscoveryService(
  wtPath,
  base.getFileFilteringOptions().customIgnoreFiles,
);

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

已修复:目录重绑定会继承 base Config 的 customIgnoreFiles,并增加 .cursorignore 传递断言。验证证据:workflow-orchestrator 141/141 通过;Core typecheck 通过。

Comment on lines +63 to +64
it('refuses a path outside the repository', async () => {
const result = await resolveExternalWorktreeDir(config, '/elsewhere/tree');

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.

[Suggestion] The symlink/realpath half of the containment guard has zero coverage in this new test file — every test stubs GitWorktreeService with plain strings; there is no node:fs/promises mock or symlink fixture. /repo does not exist during the tests, so both fs.realpath calls reject and the .catch() fallbacks degrade containment to a string comparison — the canonical-path logic never executes.

Probe-demonstrated: deleting both fs.realpath calls from worktree-pin.ts keeps all 7 tests in this file green.

Failure scenario: a future "simplification" removes the canonical-path comparison, and a model-supplied in-repo path that is a symlink to a registered worktree outside the repo then passes both string containment and isRegisteredLinkedWorktree (which realpaths its own input and matches the target's registry entry) — re-binding the child's WorkspaceContext outside the repository, the exact escape the guard exists to stop.

Add a case (temp-dir fixture or an fs.realpath mock) where the in-repo path canonicalizes outside the repo, asserting the resolves outside this repository error.

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

Comment on lines +163 to +165
* operator-tunable via env, on the same pattern as the three other workflow
* bounds (`QWEN_CODE_MAX_WORKFLOW_AGENTS`, `QWEN_CODE_WORKFLOW_STALL_SECONDS`,
* `QWEN_CODE_MAX_WORKFLOW_SECONDS`), each clamped to a hard ceiling.

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.

[Suggestion] This clause is factually wrong about two of the three named bounds.

Probe at this commit ran all four resolvers with oversized input: only QWEN_CODE_MAX_WORKFLOW_AGENTS clamps (→ ceiling 10000). QWEN_CODE_WORKFLOW_STALL_SECONDS is applied verbatim by resolveStallMs (999999999s → 999999999000ms; no ceiling branch in the function) and QWEN_CODE_MAX_WORKFLOW_SECONDS is applied verbatim by resolveMaxWallClockMs (the repo's own test shows even '0.1' honored).

Failure scenario: a maintainer bounding worst-case workflow wall time from this rationale comment concludes a misconfigured env cannot exceed a ceiling, when QWEN_CODE_MAX_WORKFLOW_SECONDS=999999999 is honored unclamped (~31.7 years); the claim also makes the new HARD_* clamps look like universal house style, inviting a wrong "consistency" change in either direction.

Suggested change
* operator-tunable via env, on the same pattern as the three other workflow
* bounds (`QWEN_CODE_MAX_WORKFLOW_AGENTS`, `QWEN_CODE_WORKFLOW_STALL_SECONDS`,
* `QWEN_CODE_MAX_WORKFLOW_SECONDS`), each clamped to a hard ceiling.
* operator-tunable via env, on the same env-override pattern as the other
* workflow bounds; like `QWEN_CODE_MAX_WORKFLOW_AGENTS` (and unlike
* `QWEN_CODE_WORKFLOW_STALL_SECONDS` / `QWEN_CODE_MAX_WORKFLOW_SECONDS`,
* which apply valid overrides verbatim), clamped to a hard ceiling.

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

Comment on lines +101 to +102
const relToRepo = path.relative(realRepoRoot, realResolved);
if (relToRepo.startsWith('..') || path.isAbsolute(relToRepo)) {

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.

[Suggestion] startsWith('..') also matches legitimate names beginning with .. — spuriously refusing registered worktrees like ..hidden-wt.

Probe-reproduced end-to-end against real git: git worktree add ..hidden-wt succeeds and registers; driving this module's exact logic refuses the pin with resolves outside this repository, because path.relative(repoRoot, '<repo>/..hidden-wt') is '..hidden-wt' and startsWith('..') is true. Fails closed (false refusal + misleading error), not a bypass. Moved verbatim from agent.ts, but newly reachable from workflow scripts.

Failure scenario: a user who keeps a worktree under a dot-dot-prefixed name (legal on POSIX; only . and .. exactly are reserved) cannot pin it, and the error sends debugging in the wrong direction.

Test traversal segments, not the string prefix (flip-verified: accepts ..hidden-wt, still refuses genuine ../ traversal):

Suggested change
const relToRepo = path.relative(realRepoRoot, realResolved);
if (relToRepo.startsWith('..') || path.isAbsolute(relToRepo)) {
const relToRepo = path.relative(realRepoRoot, realResolved);
if (
relToRepo === '..' ||
relToRepo.startsWith(`..${path.sep}`) ||
path.isAbsolute(relToRepo)
) {

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

已修复:containment 只拒绝精确 .. 或 ../ 路径段,不再误拒 ..hidden-wt,并增加回归。验证证据:worktree-pin 8/8 通过;Core typecheck 通过。

@qqqys

qqqys commented Aug 12, 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 12, 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-dev-bot

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

Copy link
Copy Markdown
Collaborator

AutoFix round 8 finishedview run. See this round's report below.

中文说明

AutoFix 第 8 轮已完成 —— 查看运行。本轮报告见下方。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

qwen-code-dev-bot and others added 2 commits August 12, 2026 09:51
Address the remaining review findings on the agent workingDir pin.

Containment anchored at `--show-toplevel`, which from inside a linked
worktree answers with the worktree's own root — spuriously refusing
registered sibling worktrees, the documented review-pipeline setup.
Resolve the repository's main working tree via the first entry of
`git worktree list --porcelain` (new GitWorktreeService helper) and
anchor there, keeping the toplevel answer as fallback.

Add dispatch-site wiring tests for the env-tunable subagent bounds at
both the fast and the override path: with a clean env the DEFAULT_*
constants and the resolvers are indistinguishable, so a revert mutation
at either call site kept every existing test green.

Cover the fs.realpath half of the containment guard with a real symlink
fixture (plain-string stubs made both realpath calls reject, so the
canonical-path logic never executed), and the sibling-anchor fix with a
unit case.

Correct the bounds doc comment: only QWEN_CODE_MAX_WORKFLOW_AGENTS and
the subagent bounds clamp to a ceiling; the stall and wall-clock env
overrides apply valid values verbatim.
@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 feedback round — PR #8972

Commit: b78fc69e1f (on workflow/agent-working-dir). No base merge performed (--conflict false); the branch already carries the earlier merge of main.

Findings and dispositions

Fixed this round

  • [Suggestion] Containment anchored at --show-toplevel (worktree-pin.ts) — Verified against live git: from inside a linked worktree git rev-parse --show-toplevel returns the worktree's own root, so pinning a registered sibling worktree (the documented review-pipeline setup) was spuriously refused with a misleading "resolves outside this repository" error. Fixed by adding GitWorktreeService.getMainWorktreePath() (first entry of git worktree list --porcelain, which always lists the primary working tree first) and anchoring containment at the main working tree, with the toplevel answer kept as fallback. The authoritative registration gate (isRegisteredLinkedWorktree) is unchanged; genuine outside paths are still refused. Covered by a new unit test plus a live-git end-to-end probe.
  • [Suggestion] Env-tunable bounds only tested at resolver level (workflow-orchestrator.test.ts) — Confirmed the reviewer's mutation check: replacing the resolvers with the DEFAULT_* constants at both dispatch sites kept all existing tests green. Added one wiring test per dispatch site (fast path asserts created[0].runConfig, override path asserts the captured runConfigOverrides) that stubs the env; both were mutation-verified to fail against the reverted wiring.
  • [Suggestion] Symlink/realpath half of the containment guard had zero coverage (worktree-pin.test.ts) — Confirmed: with plain-string stubs both fs.realpath calls reject and the .catch() fallbacks degrade containment to string comparison. Added a real temp-dir fixture (in-repo symlink pointing outside the repo) that forces the canonical-path logic to run and asserts the refusal; mutation-verified (removing the realpath calls now fails the suite).
  • [Suggestion] Factually wrong bounds comment (workflow-orchestrator.ts:165) — Verified by reading resolveStallMs and resolveMaxWallClockMs: only QWEN_CODE_MAX_WORKFLOW_AGENTS (and the new subagent bounds) clamp to a ceiling; the stall/wall-clock overrides apply valid values verbatim. Corrected the comment accordingly.

Re-verified from the previous round (still holding in HEAD)

  • Defined-but-falsy workingDir rejection on the host dispatch side (guard + empty-string sandbox test) — code and tests present.
  • Journal @fileoverview now lists workingDir among the dispatch-affecting opts.
  • resolved.error sanitized before interpolation (control-character regression test present).
  • customIgnoreFiles carried into the dir-scoped rebind (.cursorignore carry-through assertion present).
  • Containment refuses exact .. / ../ segments only, accepting ..hidden-wt (regression test present).

Deferred (not resolved — recorded reply posted on the thread)

  • [Suggestion] End-to-end headless Workflow contract (workflow.test.ts:363) — Deferred as an explicit follow-up before Phase 1. The unit-layer contract is pinned by the headless regression test this PR adds; approval-mode resolution of the 'ask' default permission happens in the scheduler and cannot be represented at the unit layer. A genuine E2E check requires new integration-harness scaffolding (no Workflow integration harness exists today), which would grow this PR well past its original intent.

Reviewer test-plan note ("no such file or directory" for six test files) — these were reviewer-side paths missing the packages/core/ prefix; all six files exist and pass.

Failed check analysis: Test (ubuntu-latest, Node 22.x)

The failed CI check could not be reproduced locally under CI-equivalent conditions. A full-workspace npm run test:ci run with clean environment variables and a writable HOME passes everywhere: the only failing file locally is packages/cli/src/ui/auth/AuthDialog.test.tsx (one stale provider-ordering test, broken on main since the Grok preset landed), and that test is explicitly skipped on CI by isUnreliableTuiInputEnvironment (process.env['CI'] === 'true'). The branch contributes zero changes outside packages/core (git diff origin/main HEAD -- ':!packages/core' is empty), and the full core suite passes (19,850 tests), so the CI failure is not attributable to any code path this PR owns; the workflow's independent CI re-run is the final gate.

Several local-only failure classes were identified and ruled out as runner artifacts (they do not exist on CI runners): a non-writable $HOME on this self-hosted container (tests that write under ~/.qwen fail locally), QWEN_HOME/SANDBOX/QWEN_CODE_* variables leaking from the agent process into vitest children, and a missing zip/unzip binary tripping the install-script test's CI guard.

Verification

All commands run at commit b78fc69e1f on this checkout:

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed (repo-wide; ESLint + prettier --check also run on the five touched files individually)
  • Focused vitest (packages/core: worktree-pin.test.ts, workflow-orchestrator.test.ts, workflow-sandbox.test.ts, workflow-journal.test.ts, tools/workflow/workflow.test.ts) — 357 passed (worktree-pin 10, orchestrator 143, sandbox 151, journal 15, workflow 42)
  • packages/core full npm run test:ci (CI-equivalent env: qwen env vars unset, writable HOME) — 582 files passed, 19,850 tests passed, 0 failed
  • Full-workspace npm run test:ci (CI-equivalent env) — all packages green except AuthDialog.test.tsx, which fails only locally and is skipped on CI (CI=true confirmed: 18 of 25 tests skip, file passes)
  • npm run test:scripts — 51 files passed; the one failing file is the install-script guard throwing on missing zip/unzip in this container (present on GitHub runners)
  • Mutation checks — reverting either dispatch site to DEFAULT_* constants fails both new wiring tests (2/2); dropping the main-tree anchor fails the sibling-worktree test; dropping the fs.realpath calls fails the symlink test
  • Live-git probe (temp repo with two worktrees under .qwen/tmp/) — from inside a linked worktree: sibling pin resolves with correct branch label; outside path refused by containment; unregistered in-repo dir refused by the registration gate
  • Integration tests after npm run bundle — not run: the touched behavior (worktree pin validation, dispatch bounds wiring) is fully exercised by the core unit tests above, not only through the bundled CLI or integration harness
  • npm run generate:settings-schema — not applicable: no settings source changed
中文说明

评审反馈轮次 — PR #8972

提交:b78fc69e1f(位于 workflow/agent-working-dir 分支)。未执行 base 合并(--conflict false);分支上已包含早前对 main 的合并。

发现与处置

本轮修复

  • [建议] containment 以 --show-toplevel 为锚点(worktree-pin.ts)— 已在真实 git 上验证:在链接 worktree 内部执行 git rev-parse --show-toplevel 返回的是该 worktree 自身的根目录,因此对一个已注册的兄弟 worktree 做 pin(正是评审流水线文档中的使用场景)会被错误拒绝,并给出误导性的 "resolves outside this repository" 错误。修复方式:新增 GitWorktreeService.getMainWorktreePath()(取 git worktree list --porcelain 的第一项,该命令总是先列出主工作树),将 containment 的锚点改为主工作树,并保留 toplevel 结果作为回退。权威门禁(isRegisteredLinkedWorktree)未改动;真正位于仓库外的路径仍会被拒绝。新增一个单元测试,并做了真实 git 的端到端探针验证。
  • [建议] 环境可调上限只在 resolver 层有测试(workflow-orchestrator.test.ts)— 确认了评审者的变异测试结论:在两个 dispatch 调用点把 resolver 换回 DEFAULT_* 常量后,现有测试全部仍然通过。为此每个 dispatch 点各新增一个接线测试(fast path 断言 created[0].runConfig,override path 断言捕获到的 runConfigOverrides),通过 stub 环境变量实现;两个测试均经过变异验证,在接线被还原时会失败。
  • [建议] containment 门禁的 symlink/realpath 一半零覆盖(worktree-pin.test.ts)— 确认属实:使用纯字符串 stub 时两处 fs.realpath 调用都会 reject,.catch() 回退使 containment 退化为字符串比较。新增真实临时目录夹具(仓库内 symlink 指向仓库外目标),强制 canonical-path 逻辑真正执行并断言拒绝;已做变异验证(删掉 realpath 调用后测试套件会失败)。
  • [建议] 关于上限的注释与事实不符(workflow-orchestrator.ts:165)— 通过阅读 resolveStallMsresolveMaxWallClockMs 确认:只有 QWEN_CODE_MAX_WORKFLOW_AGENTS(以及新增的 subagent 上限)会被钳制到天花板;stall/wall-clock 的覆盖值是原样生效的。已按此修正注释。

上一轮修复的复核(在 HEAD 中仍然成立)

  • 主机侧 dispatch 对已定义但为 falsy 的 workingDir 的拒绝(守卫 + 空字符串 sandbox 测试)— 代码与测试均在。
  • journal 的 @fileoverview 已将 workingDir 列入影响 dispatch 的 canonical options。
  • resolved.error 在拼接前经过净化(含控制字符回归测试)。
  • 目录作用域重绑定继承 customIgnoreFiles(含 .cursorignore 传递断言)。
  • containment 只拒绝精确 .. / ../ 路径段,接受 ..hidden-wt(回归测试在)。

推迟(未解决 — 已在对应线程回复并记录原因)

  • [建议] 端到端 headless Workflow 契约(workflow.test.ts:363)— 作为 Phase 1 之前的明确后续事项推迟。本 PR 新增的 headless 回归测试已固定单元层契约;'ask' 默认权限的 approval-mode 解析发生在调度器中,单元层无法表达。真正的端到端检查需要新的集成测试脚手架(目前不存在 Workflow 集成脚手架),会使本 PR 远超原始意图。

评审者测试计划备注(六个测试文件 "no such file or directory") — 这是评审侧路径缺少 packages/core/ 前缀所致;六个文件均存在且测试通过。

失败检查分析:Test (ubuntu-latest, Node 22.x)

该失败的 CI 检查在本地 CI 等价条件下无法复现。在干净环境变量 + 可写 HOME 的条件下运行全仓库 npm run test:ci 全部通过:本地唯一失败的文件是 packages/cli/src/ui/auth/AuthDialog.test.tsx(一个 provider 排序过期的测试,自 Grok preset 合入 main 后即损坏),而该测试在 CI 上被 isUnreliableTuiInputEnvironmentprocess.env['CI'] === 'true')显式跳过。本分支在 packages/core 之外没有任何改动(git diff origin/main HEAD -- ':!packages/core' 为空),且 core 全量测试通过(19,850 个),因此该 CI 失败不能归因于本 PR 拥有的任何代码路径;以工作流自身的独立 CI 重跑为最终门禁。

另识别并排除了若干仅本地出现的失败类别(在 CI runner 上不存在):本自托管容器中 $HOME 不可写(向 ~/.qwen 写入的测试本地失败)、agent 进程的 QWEN_HOME/SANDBOX/QWEN_CODE_* 变量泄漏进 vitest 子进程、以及缺少 zip/unzip 二进制触发 install-script 测试的 CI 守卫。

验证

所有命令均在提交 b78fc69e1f 的当前检出上实际执行:

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过(全仓库;另对五个改动文件单独运行 ESLint + prettier --check
  • 聚焦 vitest(packages/core:worktree-pin.test.tsworkflow-orchestrator.test.tsworkflow-sandbox.test.tsworkflow-journal.test.tstools/workflow/workflow.test.ts)— 357 通过(worktree-pin 10、orchestrator 143、sandbox 151、journal 15、workflow 42)
  • packages/core 全量 npm run test:ci(CI 等价环境:unset qwen 相关变量、可写 HOME)— 582 个文件通过、19,850 个测试通过、0 失败
  • 全仓库 npm run test:ci(CI 等价环境)— 除 AuthDialog.test.tsx 外全部通过;该文件仅本地失败,且在 CI 上被跳过(已确认 CI=true 时 25 个测试中 18 个跳过,文件通过)
  • npm run test:scripts — 51 个文件通过;唯一失败的文件是 install-script 守卫因本容器缺少 zip/unzip 而主动抛出(GitHub runner 上具备这些二进制)
  • 变异检查 — 将任一 dispatch 点还原为 DEFAULT_* 常量会使两个新接线测试失败(2/2);移除主工作树锚点会使兄弟 worktree 测试失败;移除 fs.realpath 调用会使 symlink 测试失败
  • 真实 git 探针(临时仓库,.qwen/tmp/ 下两个 worktree)— 在链接 worktree 内部:兄弟 pin 成功解析且分支标签正确;仓库外路径被 containment 拒绝;未注册的仓库内目录被注册门禁拒绝
  • npm run bundle 之后的集成测试 — 未运行:本次改动的行为(worktree pin 校验、dispatch 上限接线)已完全由上述 core 单元测试覆盖,并非只能通过打包 CLI 或集成脚手架验证
  • npm run generate:settings-schema — 不适用:未改动任何 settings 源

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

⚠️ The branch received new commits while this round ran; they were merged into this push, but this round's verification predates that merge — re-check anything that landed mid-run. · 本轮运行期间分支收到了新的提交;本次推送已将其合并,但本轮验证在合并之前完成——请复查运行期间落地的改动。

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.

Reviewed. Suggestions are inline.

Not reviewed: build-and-test — Test (macos-latest / windows-latest, Node 22.x) was skipped in CI and the unit suite did not run locally on those OSes.

Not explored to full depth (tool budget reached): You are review agent reverse-audit — Reverse audit agen...: none — all checks above completed within budget.; You are review agent reverse-audit — Reverse audit agen...: none — all checks above completed within budget..

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

Test Plan (not a blocker): src/agents/worktree-pin.test.tsno such file or directory; src/agents/runtime/workflow-orchestrator.test.tsno such file or directory; src/agents/runtime/workflow-sandbox.test.tsno such file or directory; src/agents/runtime/workflow-journal.test.tsno such file or directory; src/tools/workflow/workflow.test.tsno such file or directory; and 1 more.

中文说明

已审查。 建议见行内评论。

未审查:build-and-test — Test (macos-latest / windows-latest, Node 22.x) was skipped in CI and the unit suite did not run locally on those OSes。

未探索到全部深度(达到工具调用预算):You are review agent reverse-audit — Reverse audit agen...:none — all checks above completed within budget.;You are review agent reverse-audit — Reverse audit agen...:none — all checks above completed within budget.

未审查:反向审计——在 5 轮的反审轮数上限内未收敛。

Test Plan(非阻断):src/agents/worktree-pin.test.tsno such file or directory; src/agents/runtime/workflow-orchestrator.test.tsno such file or directory; src/agents/runtime/workflow-sandbox.test.tsno such file or directory; src/agents/runtime/workflow-journal.test.tsno such file or directory; src/tools/workflow/workflow.test.tsno such file or directory; and 1 more。

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

Comment on lines +330 to +335
async getMainWorktreePath(): Promise<string | null> {
try {
const out = await (
await this.getGit()
).raw(['worktree', 'list', '--porcelain']);
const firstLine = out.split('\n', 1)[0]?.trim() ?? '';

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.

[Suggestion] The new getMainWorktreePath() has zero direct test coverage — the only test depending on its semantics (the sibling-worktree acceptance test in worktree-pin.test.ts) stubs the method out wholesale, and gitWorktreeService.test.ts never references it. This method is the anchor of the PR's central re-anchoring fix.

Failure scenario: a regression making it always return null (e.g. inverting the startsWith('worktree ') check) silently falls back to getRepoTopLevel() in worktree-pin.ts — from inside a linked worktree that answers the worktree's own root, so sibling pins like ../review-pr-1-base are spuriously refused as outside the repository: exactly the regression this PR fixes. Probe-verified at this commit: under that mutation all 287 tests in the three relevant suites stay green, while the unit tests sketched below flip 2/4.

// packages/core/src/services/gitWorktreeService.test.ts — existing hoistedMockRaw pattern
it('parses the first porcelain entry as the main worktree path', async () => {
  hoistedMockRaw.mockResolvedValueOnce('worktree /repo\nworktree /repo/wt\n');
  expect(await service.getMainWorktreePath()).toBe('/repo');
});
// plus: first line without the `worktree ` prefix -> null; raw() rejecting -> null; empty output -> null
中文说明

新增的 getMainWorktreePath() 没有任何直接测试覆盖——唯一依赖其语义的测试(worktree-pin.test.ts 中的兄弟 worktree 接受测试)把该方法整个 stub 掉了,gitWorktreeService.test.ts 也从未引用它。该方法是本 PR 核心「重新锚定」修复的锚点。

失败场景:某个使其恒返回 null 的回归(例如把 startsWith('worktree ') 检查写反)会静默回退到 worktree-pin.ts 中的 getRepoTopLevel()——在 linked worktree 内部运行时它回答的是该 worktree 自己的根,于是像 ../review-pr-1-base 这样的兄弟钉住会被误判为「在仓库之外」而遭拒绝:这正是本 PR 要修复的回归。已在当前提交上用探针验证:该变异下三个相关套件的全部 287 个测试仍保持全绿,而上面草拟的单元测试会有 2/4 翻红(草图使用 gitWorktreeService.test.ts 现有的 hoistedMockRaw 模式:porcelain 首条目 → 主树路径;首行无 worktree 前缀 → null;raw() 拒绝 → null;空输出 → null)。

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

Comment on lines 23 to +26
* The `canonicalOpts` projection keeps only the dispatch-affecting opts
* (`schema`, `model`, `isolation`, `agentType`) with object keys sorted, so
* cosmetic opt differences (a re-ordered schema, a `label` change) don't
* bust the cache.
* (`schema`, `model`, `isolation`, `agentType`, `workingDir`) with object keys
* sorted, so cosmetic opt differences (a re-ordered schema, a `label` change)
* don't bust the cache.

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.

[Suggestion] Test-efficacy probe (deterministic): reverting this documentation-only hunk on its own leaves every test green — nothing gates this comment against code drift. The companion code hunk (adding 'workingDir' to the canonicalizeAgentOpts projection) WAS killed by the probe and the whole-file revert is gated by workflow-journal.test.ts, so the behaviour itself is covered — only this comment is ungated. Note round 1's R1-5 was exactly this drift class on this same file, so the risk is not hypothetical. Measured coverage observation, not a behavioural defect — no code change is required for this PR.

Failure scenario: a future change dropping workingDir from the projection (or adding another dispatch-affecting opt) leaves this JSDoc stale with nothing flagging the mismatch — a misleading contract document on a cache-correctness module.

中文说明

测试有效性探针(确定性结果):单独回退这个纯文档 hunk 后所有测试仍然全绿——没有任何东西把这条注释与代码钉在一起以防漂移。配套的代码 hunk(把 'workingDir' 加入 canonicalizeAgentOpts 投影)被探针杀死,整文件回退也被 workflow-journal.test.ts 拦截,所以行为本身是有覆盖的——只有这条注释没有被钉住。注意第一轮的 R1-5 正是同一文件上同一类「文档与投影漂移」,因此这里的漂移风险并非假设。这是测量得出的覆盖观察,不是行为缺陷——本 PR 无需改动代码。

失败场景:未来的改动若把 workingDir 从投影中移除(或新增另一个影响派发的选项),这段 JSDoc 会在没有任何东西提示不一致的情况下过期——而这是位于缓存正确性攸关模块上的一份契约文档。

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

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.

Declined — no code change this round. Your own deterministic probe records that the behaviour is already gated: adding 'workingDir' to the canonicalizeAgentOpts projection is mutation-killed, and a whole-file revert is caught by workflow-journal.test.ts. Only the prose comment is ungated, and the finding explicitly states no code change is required for this PR. Gating the prose itself would mean a brittle assertion on comment text, so per the repo's simplicity rule we are not growing the diff for it. The residual drift risk (a future change dropping workingDir from the projection leaving this JSDoc stale) is acknowledged and recorded here; the projection assertion in workflow-journal.test.ts remains the behavioural gate.

中文说明

拒绝——本轮不做代码改动。您自己的确定性探针已确认行为本身有测试拦截:把 'workingDir' 加入 canonicalizeAgentOpts 投影会被变异杀死,整文件回退也被 workflow-journal.test.ts 拦截。只有这段文字注释没有被钉住,且该发现明确写明本 PR 无需改动代码。要钉住文字本身,只能靠对注释文本的脆弱断言,因此按本仓库的简洁性原则不为此扩大 diff。残余的漂移风险(未来改动把 workingDir 从投影中移除、这段 JSDoc 随之过期而无人提示)在此记录在案;workflow-journal.test.ts 中的投影断言仍是行为层面的拦截。

Comment on lines +114 to +115
'cannot serve. Mutually exclusive with `isolation`. The path must live ' +
'inside the repository and appear in `git worktree list`. ' +

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.

[Suggestion] The model-facing workingDir description states an eligibility condition that is not sufficient: "appear in git worktree list". The authoritative gate isRegisteredLinkedWorktree rejects the main working tree even though it is always listed first in git worktree list, and it also rejects stale-but-still-listed registry entries via its liveness probe. The refusal text itself names "it is the main working tree" — the implementation knows the documented condition is insufficient.

Failure scenario: probe-verified against real git at this commit — resolveExternalWorktreeDir(config, '.', 'workingDir') and the absolute main-tree path are both refused while the linked review worktree is accepted. A script author following this description — which earlier invites pinning with "its uncommitted state is the point" — pins the dirty main checkout and gets a runtime refusal; inside parallel() (documented in the next paragraph as errors-as-data) the throwing thunk becomes null at its index, so a documented-contract script silently degrades to a null result instead of an agent result.

Suggested change
'cannot serve. Mutually exclusive with `isolation`. The path must live ' +
'inside the repository and appear in `git worktree list`. ' +
'cannot serve. Mutually exclusive with `isolation`. The path must live ' +
'inside the repository and be a linked worktree registered via ' +
'`git worktree add` — the main checkout is not eligible. ' +
中文说明

面向模型的 workingDir 描述给出了一个不充分的资格条件:「出现在 git worktree list 中」。权威关卡 isRegisteredLinkedWorktree 会拒绝主工作树——尽管它总是排在 git worktree list 的第一条——并且会通过存活探测拒绝那些仍在列表中但已失效的登记条目。拒绝文案自己都写着 "it is the main working tree"——实现明知文档给出的条件不够。

失败场景:已在当前提交上用真实 git 探针验证——resolveExternalWorktreeDir(config, '.', 'workingDir') 与主树绝对路径都被拒绝,而 linked 的 review worktree 被接受。脚本作者按照这段描述(前文还在用「其未提交状态正是重点」邀请钉住)把子 agent 钉在脏的主检出上,得到运行时拒绝;而在 parallel()(下一段明确描述为「错误即数据」)里,抛错的 thunk 会变成其下标上的 null——按文档契约写出的脚本静默退化为一个 null 结果,而不是 agent 结果。建议修复:收紧表述为「在仓库内部、且是通过 git worktree add 登记的 linked worktree——主检出不可作为钉住目标」。

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

Comment on lines +43 to +46
expect(canonicalizeAgentOpts({ workingDir: 'wt' })).toBe(
JSON.stringify({ workingDir: 'wt' }),
);
expect(a).not.toBe(b);

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.

[Suggestion] The new workingDir journal-key test gates only the MISS direction (different dirs ⇒ different keys); the HIT direction (same workingDir ⇒ same key ⇒ resume actually replays from cache) is asserted nowhere — deriveAgentKey's determinism tests use only {}/{ model } opts, and every P6 end-to-end resume test dispatches bare opts.

Failure scenario: probe-verified mutation at this commit — a per-call nonce in the workingDir branch of deriveAgentKey keeps all 15 journal tests green (not.toBe stays green, the projection assertion pins canonicalizeAgentOpts rather than the hash, and the determinism test never passes a workingDir). Result: every resumeFromRunId of a workingDir-using workflow silently misses the journal and re-runs all dispatches live — full token/time re-spend with no error — defeating the resume cache for exactly the workflow shape this PR introduces. The symmetric assertion below fails under the mutation and passes on the correct code.

Suggested change
expect(canonicalizeAgentOpts({ workingDir: 'wt' })).toBe(
JSON.stringify({ workingDir: 'wt' }),
);
expect(a).not.toBe(b);
expect(canonicalizeAgentOpts({ workingDir: 'wt' })).toBe(
JSON.stringify({ workingDir: 'wt' }),
);
expect(a).not.toBe(b);
expect(
deriveAgentKey('', 'review it', {
workingDir: '.qwen/tmp/review-pr-1',
}),
).toBe(
deriveAgentKey('', 'review it', {
workingDir: '.qwen/tmp/review-pr-1',
}),
);
中文说明

这个新的 workingDir 日志键测试只钉住了 MISS 方向(不同目录 ⇒ 不同键);HIT 方向(相同 workingDir ⇒ 相同键 ⇒ resume 确实从缓存重放)没有任何断言——deriveAgentKey 的确定性测试只用 {}/{ model } 选项,P6 的端到端 resume 测试全部以裸选项派发。

失败场景:已在当前提交上做变异探针——在 deriveAgentKey 的 workingDir 分支里加入每次调用不同的 nonce,15 个 journal 测试全部保持绿(not.toBe 仍绿;投影断言钉的是 canonicalizeAgentOpts 而不是哈希;确定性测试从不传 workingDir)。结果是:使用 workingDir 的 workflow 每次 resumeFromRunId 都会静默 miss 日志、全部派发重新实跑——token 与时间被完整重花且没有任何报错——恰好在本 PR 引入的这种 workflow 形态上击穿了 resume 缓存。下面的对称断言在该变异下会失败、在正确代码上通过。

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

Comment on lines +92 to +95
const repoRoot =
(await probe.getMainWorktreePath()) ??
(await probe.getRepoTopLevel()) ??
parentCwd;

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.

[Suggestion] Probe-reproduced containment edge introduced by the new main-tree anchor: getMainWorktreePath() parses newline-delimited porcelain, so a main-tree path containing a newline truncates the anchor to a shorter prefix that may resolve inside a different repository, against whose registry isRegisteredLinkedWorktree then validates. The probe built that layout with real git: a pin to the other repo's live registered worktree was accepted, rebinding the child outside the invoking repository; replacing the anchor with the pre-PR getRepoTopLevel() chain refused the same input (the probe flips) — the escape is introduced by this re-anchor. It also falsifies the new JSDoc's claim that "consumers fail closed on the bad anchor and the authoritative registration checks never consult this value": the consumer accepted, and the registration check consults the anchor via wtService's sourceRepoPath.

Severity is Suggestion rather than Critical: the trigger is pathological and self-inflicted (cloning into a path containing a newline, nested inside another repo that has a registered worktree under the truncated prefix), the pin path remains model-supplied, and a pin is a cwd rebind, not a sandbox. The far more probable outcome of a newline-bearing clone path is the benign direction: truncated prefix inside no repo ⇒ everything fails closed and legitimate pins are spuriously refused.

Failure scenario: repo R1 cloned into /a/<LF>R1 where /a is inside repo R2 — running inside R1, getMainWorktreePath() splits on the newline and returns /a; containment then checks the model-supplied pin against /a, and GitWorktreeService('/a') validates against R2's registry, so a pin to R2's live worktree /a/wtR2 passes both gates — the child's WorkspaceContext rebinds outside the invoking repository, exactly what the containment comment above this block forbids.

Suggested fix (author's choice): cross-check the parsed anchor before trusting it (e.g. require agreement with git rev-parse --show-toplevel when cwd is in the main tree); fall back to getRepoTopLevel() when the porcelain first entry does not consume cleanly; or parse with -z on git ≥ 2.36. Do not simply revert to getRepoTopLevel() — that regresses the linked-worktree sibling-pin case this PR deliberately fixes.

中文说明

探针复现的包含边界问题,由新的「主树锚点」引入:getMainWorktreePath() 解析以换行分隔的 porcelain 输出,因此包含换行的主树路径会把锚点截断成更短的前缀,而该前缀可能落在另一个仓库内部,随后 isRegisteredLinkedWorktree 会对着那个仓库的登记册做校验。探针用真实 git 搭出了这个布局:钉到对方仓库「存活且已登记」的 worktree 被接受了,子 agent 被重绑定到调用仓库之外;把锚点换回 PR 之前的 getRepoTopLevel() 链后,同样的输入被拒绝(探针翻转)——这个逃逸是本次重新锚定引入的。它同时也证伪了新 JSDoc 的说法(「消费者在坏锚点上会失败关闭、且权威登记检查从不使用该值」):消费者接受了,而登记检查经由 wtServicesourceRepoPath 确实使用了该锚点。

严重度定为 Suggestion 而非 Critical:触发条件是病态且自伤的(把仓库克隆进一个含换行的路径,且其嵌套在另一个仓库内、截断前缀之下恰好有对方已登记的 worktree);钉住路径仍然由模型提供;且钉住是 cwd 重绑定,不是沙箱。含换行的克隆路径更常见的结果是良性方向:截断前缀不在任何仓库内 ⇒ 全部失败关闭,合法的钉住被误拒。

失败场景:仓库 R1 克隆到 /a/<LF>R1,而 /a 位于仓库 R2 内部——在 R1 内运行时,getMainWorktreePath() 按换行切分、返回 /a;包含检查随后以 /a 为基准校验模型给出的钉住路径,GitWorktreeService('/a') 对照 R2 的登记册校验,于是钉到 R2 的存活 worktree /a/wtR2 两关全过——子 agent 的 WorkspaceContext 被重绑定到调用仓库之外,正是上方注释明令禁止的情形。

建议修复(由作者选择):在信任解析出的锚点前做交叉校验(例如 cwd 在主树时要求与 git rev-parse --show-toplevel 一致);porcelain 首条目未被完整消费时回退到 getRepoTopLevel();或在 git ≥ 2.36 上用 -z 解析。请不要简单回退为 getRepoTopLevel()——那会让本 PR 刻意修复的「linked worktree 内钉住兄弟 worktree」场景回归。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

🤖 No code changes this round — the failed check was investigated to the limit available in this environment and is not reproducible from anything this PR owns. · 本轮未改动代码 —— 失败的检查已在当前环境能力范围内彻查,无法从本 PR 拥有的任何代码路径复现。

Review feedback round — PR #8972

Commit: dc40e9254b (on workflow/agent-working-dir, identical to the tree the failed CI run tested). No review comments, inline comments, or issue-level feedback were actionable this round; the only feedback item is the failed check Test (ubuntu-latest Node 22.x).

Failed check analysis: Test (ubuntu-latest, Node 22.x)

The failed run is Qwen Code CI #31585543074, job 94078460935 — started 2026-08-12T10:01:33Z, failed 10:20:42Z (19m09s), testing exactly this HEAD (dc40e9254b, committed 10:01:06Z). The same check was also red on the earlier head c46bcdc9f8 (before round 1's fix and the main merge). This environment holds no GitHub credentials, so the CI job log itself is unreadable; everything below comes from local surrogate reproduction and repository history.

Every runnable gate of that job was re-run locally at this exact tree under CI-equivalent conditions (isolated HOME, all QWEN_*/API-key env cleared, CI=true where applicable), and all of them pass:

  • node scripts/lint.js --eslint — passed; node scripts/lint.js --prettier — passed (note: this repo's prettier step is prettier --write ., not a check); --sensitive-keywords — passed (--actionlint/--shellcheck/--yamllint binaries are not installed in this container, but this PR changes no .github/*.yml or shell files for them to lint)
  • npm run typecheck — passed; npm run build — passed; npm run bundle — passed
  • npm run audit:runtime:critical, check:lockfile, check:desktop-isolation, check:voice-guard-sync, check-i18n — all passed
  • Settings schema and VS Code companion notices regenerated and confirmed byte-identical to the committed artifacts (git status --porcelain empty for both)
  • npm run check:serve-fast-path-bundle — passed
  • node --test over all 16 HELPER_TESTS files — 255 passed
  • Focused vitest for the five changed test files (packages/core: worktree-pin, workflow-orchestrator, workflow-sandbox, workflow-journal, tools/workflow/workflow) — 361 passed
  • Full npm run test:ci with CI=true and a clean env — every workspace passed: cli 798 files, core 582 files (includes the whole PR-added suite), web-shell 181, webui 34, sdk-typescript 32, vscode-ide-companion 54, acp-bridge 26, audio-capture, chrome-extension, all nine channels packages, external-context 10, plus test:scripts 51/52 — the single exception is install-script.test.js deliberately throwing its "zip/unzip missing on a CI host" guard because this container has no zip binary (CI installs/has it, so this artifact is local-only)
  • npm run test:integration:no-ak:sandbox:none (the required no-AK gate, after build+bundle) — 12/12 files, 140 tests passed

Why the check is still red on CI — evidence, not a guess: the branch diff against origin/main is confined to twelve packages/core files (git diff origin/main...HEAD --name-only shows nothing else), the full core suite passes, and the workflow already verified this same check green on current main before merging main in at 09:43Z — yet the re-run stayed red. Meanwhile the repository maintainer is actively landing a deflake branch (fix/ci-idle-parse-guard-flake, commits pushed 05:16–10:11Z today, i.e. the same window as both red runs) whose commit messages describe exactly this shared self-hosted fleet: transient ENOSPC bursts failing the Test step mid-suite on actions-runner-test-* machines ("132 of 147 errors were mkdtemp failures — while the hosts look healthy afterwards", which is precisely why local reproduction is green) plus retry: 2 for load-spike timeout flakes. This runner host (actions-runner-test-9, same fleet) is healthy right now (/tmp 17% inodes used, 113G free), consistent with a transient fleet condition rather than a deterministic code defect.

Local-only failure classes were identified and ruled out as artifacts of this agent container (they do not exist on CI runners): QWEN_HOME/QWEN_CODE_* variables leaking from the agent process into vitest children break ~28 packages/cli tests locally (clearing them makes all 798 cli files pass), the missing zip binary trips the install-script guard only when CI=true, and an earlier local "all green" summary was re-verified with full logs after discovering a | tail pipeline had masked an exit code.

Conclusion and next steps: there is no code change this PR can make to address this failure — the PR's own code paths are exhaustively green under CI-equivalent reproduction, and the credible cause (shared-fleet ENOSPC/load transients) is being fixed on the maintainer's deflake branch, which this PR should receive via a future main merge rather than duplicate. The CI failure patrol (which has log access this environment lacks) and its rerun path are the right owners for the red check; if it persists after the deflake lands on main, merging main into this branch is the remedy.

Verification

All commands actually run at commit dc40e9254b on this checkout (results above):

  • git diff origin/main...HEAD --name-only / --stat — 12 files, all under packages/core
  • node scripts/lint.js --eslint — passed
  • node scripts/lint.js --prettier — passed (it runs prettier --write .; the incidental reformat of 40 unrelated files was restored with git restore, tree left identical to HEAD)
  • node scripts/lint.js --sensitive-keywords — passed; --actionlint/--shellcheck/--yamllint — binaries unavailable locally, not applicable (no matching files changed)
  • npm run typecheck — passed
  • npm run build — passed; npm run bundle — passed
  • npm run audit:runtime:critical — passed; npm run check:lockfile — passed; npm run check:desktop-isolation — passed; npm run check:voice-guard-sync — passed
  • npm run check-i18n — passed
  • npm run generate:settings-schema + git status --porcelain on the schema — clean
  • npm run generate:notices --workspace=qwen-code-vscode-ide-companion + porcelain check — clean
  • npm run check:serve-fast-path-bundle — passed
  • node --test HELPER_TESTS (16 files) — 255 passed
  • Focused vitest (packages/core, five changed files) — 361 passed
  • Full npm run test:ci (CI=true, QWEN env cleared, isolated HOME, full log at /tmp/testci-clean.log) — all workspaces passed; sole exception the local-only install-script zip guard
  • npm run test:integration:no-ak:sandbox:none (clean env, full log at /tmp/noak-clean.log) — 12/12 files, 140 tests passed
  • Chrome extension package/scan step — not reproducible here (no zip binary in this container); unaffected by this PR (no dependency or asset changes)
  • CI job log for run 31585543074 — unavailable: this environment holds no GitHub credentials
中文说明

评审反馈轮次 — PR #8972

提交:dc40e9254b(位于 workflow/agent-working-dir 分支,与失败 CI 运行所测试的树完全一致)。本轮没有可处理的评审评论、行内评论或议题级反馈;唯一的反馈项是失败的检查 Test (ubuntu-latest Node 22.x)

失败检查分析:Test (ubuntu-latest, Node 22.x)

失败的运行是 Qwen Code CI #31585543074,任务 94078460935 —— 开始于 2026-08-12T10:01:33Z,失败于 10:20:42Z(历时 19 分 09 秒),测试的正是当前 HEAD(dc40e9254b,提交于 10:01:06Z)。同一检查在较早的 head c46bcdc9f8(第 1 轮修复与 main 合入之前)上也是红色。本环境没有 GitHub 凭据,因此无法读取 CI 任务日志本身;以下所有内容均来自本地替代复现与仓库历史。

该任务的每一个可运行门禁都已在当前这棵树上、以 CI 等价条件(隔离的 HOME、清空所有 QWEN_*/API key 环境变量、适用时设置 CI=true)重新执行,且全部通过:

  • node scripts/lint.js --eslint — 通过;node scripts/lint.js --prettier — 通过(注意:本仓库的 prettier 步骤是 prettier --write .,不是检查);--sensitive-keywords — 通过(--actionlint/--shellcheck/--yamllint 的二进制未安装在本容器中,但本 PR 没有改动任何供其检查的 .github/*.yml 或 shell 文件)
  • npm run typecheck — 通过;npm run build — 通过;npm run bundle — 通过
  • npm run audit:runtime:criticalcheck:lockfilecheck:desktop-isolationcheck:voice-guard-synccheck-i18n — 全部通过
  • 设置 schema 与 VS Code companion notices 重新生成后与已提交产物逐字节一致(两者的 git status --porcelain 均为空)
  • npm run check:serve-fast-path-bundle — 通过
  • 对全部 16 个 HELPER_TESTS 文件运行 node --test — 255 个测试通过
  • 五个改动测试文件的聚焦 vitest(packages/core:worktree-pinworkflow-orchestratorworkflow-sandboxworkflow-journaltools/workflow/workflow)— 361 个测试通过
  • 完整 npm run test:ciCI=true + 干净环境)— 所有工作区通过:cli 798 个文件、core 582 个文件(含本 PR 新增的全部套件)、web-shell 181、webui 34、sdk-typescript 32、vscode-ide-companion 54、acp-bridge 26、audio-capture、chrome-extension、全部九个 channels 包、external-context 10,外加 test:scripts 51/52 —— 唯一的例外是 install-script.test.js 主动抛出其 "CI 主机缺少 zip/unzip" 守卫,因为本容器没有 zip 二进制(CI 上已安装/具备,因此这是仅本地存在的假象)
  • npm run test:integration:no-ak:sandbox:none(必需的 no-AK 门禁,在 build+bundle 之后)— 12/12 个文件、140 个测试通过

为什么 CI 上仍然红 —— 基于证据而非猜测: 分支相对 origin/main 的差异仅限于十二个 packages/core 文件(git diff origin/main...HEAD --name-only 没有其他内容),core 全量测试通过,且工作流在 09:43Z 合入 main 之前已确认同一检查在当前 main 上为绿色 —— 但重跑之后仍然是红色。与此同时,仓库维护者正在积极落地一个 deflake 分支(fix/ci-idle-parse-guard-flake,其提交推送于今天 05:16–10:11Z,恰与两次红检同一时间窗口),其提交信息描述的正是这个共享自托管 runner 集群:ENOSPC 瞬时爆发导致 Test 步骤在套件中途失败,发生在 actions-runner-test-* 机器上("132 of 147 errors were mkdtemp failures —— 而事后主机看起来完全健康",这恰恰解释了为什么本地复现是绿色),外加针对负载尖峰超时抖动的 retry: 2。本 runner 主机(actions-runner-test-9,同一集群)当前状态健康(/tmp inode 使用 17%,空闲 113G),与"集群瞬时状况"而非"确定性代码缺陷"的判断一致。

若干仅本地出现的失败类别已被识别并排除为本 agent 容器的假象(在 CI runner 上不存在):从 agent 进程泄漏进 vitest 子进程的 QWEN_HOME/QWEN_CODE_* 变量会在本地弄坏约 28 个 packages/cli 测试(清空后 cli 全部 798 个文件通过);缺失的 zip 二进制只在 CI=true 时触发 install-script 守卫;并且此前一份本地"全绿"结论已在发现 | tail 管道掩盖了退出码之后用完整日志重新核验。

结论与后续: 本 PR 没有任何可以针对该失败做出的代码改动 —— PR 自身的代码路径在 CI 等价复现下已全面绿色,而可信原因(共享集群的 ENOSPC/负载瞬时问题)正在维护者的 deflake 分支上修复;本 PR 应在未来合入 main 时获得该修复,而不是自行复制。CI 失败巡查(拥有本环境所没有的日志访问权限)及其重跑路径是该红检的正确处理者;若 deflake 落地 main 后仍然失败,把 main 合入本分支即是解决办法。

验证

以下命令均在提交 dc40e9254b 的当前检出上实际执行(结果见上文):

  • git diff origin/main...HEAD --name-only / --stat — 12 个文件,全部位于 packages/core
  • node scripts/lint.js --eslint — 通过
  • node scripts/lint.js --prettier — 通过(其执行 prettier --write .;对 40 个无关文件的 incidental 重格式化已用 git restore 还原,树与 HEAD 保持完全一致)
  • node scripts/lint.js --sensitive-keywords — 通过;--actionlint/--shellcheck/--yamllint — 本地缺少二进制,不适用(未改动任何对应文件)
  • npm run typecheck — 通过
  • npm run build — 通过;npm run bundle — 通过
  • npm run audit:runtime:critical — 通过;npm run check:lockfile — 通过;npm run check:desktop-isolation — 通过;npm run check:voice-guard-sync — 通过
  • npm run check-i18n — 通过
  • npm run generate:settings-schema + 对 schema 的 git status --porcelain — 干净
  • npm run generate:notices --workspace=qwen-code-vscode-ide-companion + porcelain 检查 — 干净
  • npm run check:serve-fast-path-bundle — 通过
  • node --test HELPER_TESTS(16 个文件)— 255 个测试通过
  • 聚焦 vitest(packages/core,五个改动文件)— 361 个测试通过
  • 完整 npm run test:ci(CI=true、清空 QWEN 环境、隔离 HOME,完整日志在 /tmp/testci-clean.log)— 所有工作区通过;唯一例外是仅本地的 install-script zip 守卫
  • npm run test:integration:no-ak:sandbox:none(干净环境,完整日志在 /tmp/noak-clean.log)— 12/12 个文件、140 个测试通过
  • Chrome 扩展打包/扫描步骤 — 本容器无 zip 二进制,无法复现;与本 PR 无关(未改动依赖或资产)
  • 运行 31585543074 的 CI 任务日志 — 不可获得:本环境没有 GitHub 凭据

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


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

QwenLM#8972)

A main working tree whose path contains a newline splits the porcelain
first entry of `git worktree list`, and the truncated prefix could
resolve inside a different repository — re-anchoring the pin's
containment and registration checks against that repo's worktree
registry. Detect the malformed first record (a path remainder where a
record attribute belongs) and fall back to `--show-toplevel`, whose
single-value answer keeps interior newlines intact.

Also pins down round-2 review findings: direct unit and real-git
coverage for `getMainWorktreePath()` (whose semantics were only
exercised through a stub), the symmetric journal-key HIT direction for
`workingDir` resumes, and the model-facing `workingDir` eligibility
description (the main checkout is not a valid pin target even though it
appears in `git worktree list`).
@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 轮)。改动内容与我反驳保留之处如下:

Round summary

Addressed 4 of the 5 inline findings from the automated reviewer's round-2 pass and declined 1 with a recorded reason. All changes are confined to packages/core (5 files, +191/-5), committed as b989dc3da4.

Feedback points and decisions

  1. [Suggestion] getMainWorktreePath() has zero direct test coverage (gitWorktreeService.ts:335, rc:3766333225) — Addressed. Added a getMainWorktreePath describe block to gitWorktreeService.test.ts using the existing hoistedMockRaw pattern: first porcelain entry → main path; first line without the worktree prefix → null; raw() rejecting → null; empty output → null; plus a bare-repository first entry. Also added a real-git integration describe in gitWorktreeService.linked.integ.test.ts proving the method answers the main tree even when called from inside a linked worktree — the anchor this PR re-anchored on, previously exercised only through a stub.
  2. [Suggestion] Documentation-only hunk is ungated against drift (workflow-journal.ts:26, rc:3766333241) — Declined. The finding's own deterministic probe records that the behaviour IS gated (the projection hunk is mutation-killed, the whole-file revert is caught by workflow-journal.test.ts) — only the prose is ungated — and it explicitly states "no code change is required for this PR". Gating prose would require brittle comment-content assertions; not worth the diff growth (see the thread reply).
  3. [Suggestion] workingDir description states an insufficient eligibility condition (workflow.ts:115, rc:3766333250) — Addressed. Adopted the suggested wording: the path must be a linked worktree registered via git worktree add — the main checkout is not eligible, even though it is always listed first in git worktree list.
  4. [Suggestion] Journal-key test gates only the MISS direction (workflow-journal.test.ts:46, rc:3766333276) — Addressed. Added the symmetric HIT-direction assertion (same workingDir ⇒ same derived key), which fails under the per-call-nonce mutation described in the finding and passes on the correct code.
  5. [Suggestion] Newline in the main-tree path truncates the anchor (containment edge) (worktree-pin.ts:95, rc:3766333294) — Addressed. Reproduced the porcelain truncation against real git first (a clone into /…/sub/<LF>R1 lists worktree /…/sub/ followed by the path remainder R1). getMainWorktreePath() now validates that every line of the first record up to the blank separator is a recognized porcelain attribute (HEAD , branch , detached, bare, locked, prunable) and returns null otherwise; the existing fallback chain in worktree-pin.ts then uses getRepoTopLevel(), whose single-value --show-toplevel answer keeps interior newlines intact — the pre-PR anchor chain this finding's probe verified refuses the escape input. The falsified JSDoc claim is rewritten. Regression coverage: a unit test plus a real-git integration test, both pinning truncated anchor → null and --show-toplevel still answering the full newline path. Deliberately did NOT switch to -z parsing (needs Git ≥ 2.36; the codebase supports older git, as the existing isRegisteredLinkedWorktree comment notes) and did NOT revert to getRepoTopLevel() (would regress the linked-worktree sibling-pin case this PR fixes). A path whose newline remainder literally matches an attribute keyword stays theoretically undetectable; that requires a still more pathological layout than the probed one.

Conflict notes

None — --conflict false; no merge performed.

Verification

Commands actually run (results at the pushed commit b989dc3da4):

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npx prettier --check on the 5 changed files — passed
  • Focused Vitest (packages/core), all suites the PR touches plus the new tests: gitWorktreeService.test.ts, gitWorktreeService.linked.integ.test.ts, worktree-pin.test.ts, workflow-journal.test.ts, workflow.test.ts, workflow-orchestrator.test.ts, workflow-sandbox.test.ts, agent.test.ts — 8 files, 655 tests passed
  • Full packages/core suite (582 files) — 19,858 passed, 10 skipped, 0 failed

Environment note on the still-red Test (ubuntu-latest Node 22.x) check: this sandbox initially produced local unit-test failures that were all traced to sandbox environment leakage, not branch code — the autofix home override QWEN_HOME (breaks tests asserting literal ~/.qwen/... paths), the SANDBOX marker env var (breaks the allowEditorTypeInSandbox "not in sandbox" cases), and a non-writable real HOME (EACCES: permission denied, mkdir '/home/github-runner/.qwen' — tests run as user node, home owned by root). First diagnostic run: 82 failures in 12 files; second run with QWEN_HOME unset: 95 failures in a different set of 6 files; with those vars unset and a writable HOME the full suite above is fully green on this branch. The exact CI logs are not accessible from inside this workflow (no GitHub credentials), so the workflow's independent CI remains the final gate for that check.

中文说明

本轮摘要

已处理自动化审查者第二轮行内发现中的 4 项,另有 1 项以记录在案的理由拒绝。全部改动限于 packages/core(5 个文件,+191/-5),提交为 b989dc3da4

各反馈点及处理决定

  1. [Suggestion] getMainWorktreePath() 没有任何直接测试覆盖gitWorktreeService.ts:335,rc:3766333225)——已处理。gitWorktreeService.test.ts 中使用现有的 hoistedMockRaw 模式新增 getMainWorktreePath describe 块:porcelain 首条目 → 主树路径;首行无 worktree 前缀 → null;raw() 拒绝 → null;空输出 → null;另加 bare 仓库首条目用例。同时在 gitWorktreeService.linked.integ.test.ts 新增真实 git 集成 describe,证明即使从 linked worktree 内部调用,该方法也回答主树——这正是本 PR 重新锚定的锚点,此前只通过 stub 被间接使用。
  2. [Suggestion] 纯文档 hunk 没有测试拦截漂移workflow-journal.ts:26,rc:3766333241)——拒绝。 该发现自身的确定性探针已确认行为本身有测试拦截(投影 hunk 会被变异杀死,整文件回退被 workflow-journal.test.ts 拦截)——只有文字注释没被钉住——且发现明确写明「本 PR 无需改动代码」。钉住文字只能靠脆弱的注释内容断言,不值得扩大 diff(见线程序回复)。
  3. [Suggestion] workingDir 描述给出了不充分的资格条件workflow.ts:115,rc:3766333250)——已处理。 采纳建议措辞:路径必须是通过 git worktree add 登记的 linked worktree——主检出不可作为钉住目标,尽管它总是排在 git worktree list 的第一条。
  4. [Suggestion] 日志键测试只钉住了 MISS 方向workflow-journal.test.ts:46,rc:3766333276)——已处理。 补上对称的 HIT 方向断言(相同 workingDir ⇒ 相同派生键),该断言在发现所述的每次调用 nonce 变异下会失败、在正确代码上通过。
  5. [Suggestion] 主树路径含换行会截断锚点(包含边界问题)worktree-pin.ts:95,rc:3766333294)——已处理。 先用真实 git 复现了 porcelain 截断(克隆进 /…/sub/<LF>R1 时输出 worktree /…/sub/ 后跟路径残余 R1)。getMainWorktreePath() 现在会校验首条记录中直到空分隔行为止的每一行都是可识别的 porcelain 属性(HEAD branch detachedbarelockedprunable),否则返回 null;随后 worktree-pin.ts 中既有的回退链使用 getRepoTopLevel()——其单值 --show-toplevel 回答会保留路径内部的换行——也就是本发现的探针已验证会拒绝逃逸输入的 PR 前锚点链。被证伪的 JSDoc 说法已重写。回归覆盖:一个单元测试加一个真实 git 集成测试,两者都钉住「截断锚点 → null」且 --show-toplevel 仍回答完整的含换行路径。刻意没有改用 -z 解析(需要 Git ≥ 2.36;现有 isRegisteredLinkedWorktree 注释已说明代码库支持更旧的 git),也没有回退为 getRepoTopLevel()(那会让本 PR 刻意修复的 linked worktree 兄弟钉住场景回归)。路径换行残余恰好与某个属性关键字字面相同的极端情形在理论上仍不可检测,但那需要比探针场景更病态的布局。

冲突说明

无——--conflict false,未执行任何合并。

验证

实际执行的命令(结果为推送提交 b989dc3da4 上测得):

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • 对 5 个改动文件执行 npx prettier --check — 通过
  • 聚焦 Vitest(packages/core):PR 触及的全部套件加新增测试:gitWorktreeService.test.tsgitWorktreeService.linked.integ.test.tsworktree-pin.test.tsworkflow-journal.test.tsworkflow.test.tsworkflow-orchestrator.test.tsworkflow-sandbox.test.tsagent.test.ts — 8 个文件,655 个测试通过
  • 完整 packages/core 套件(582 个文件)— 19,858 通过,10 跳过,0 失败

关于仍为红色的 Test (ubuntu-latest Node 22.x) 检查的环境说明:本沙箱最初出现的本地单元测试失败全部追溯到沙箱环境泄漏,而非分支代码——autofix 的 home 覆盖变量 QWEN_HOME(使断言字面 ~/.qwen/... 路径的测试失败)、SANDBOX 标记环境变量(使 allowEditorTypeInSandbox 的「非沙箱」用例失败)、以及不可写的真实 HOME(EACCES: permission denied, mkdir '/home/github-runner/.qwen'——测试以 node 用户运行,而 home 目录属 root 所有)。第一次诊断运行:12 个文件 82 个失败;第二次在取消 QWEN_HOME 后运行:另一组 6 个文件 95 个失败;取消这些变量并使用可写 HOME 后,上面的完整套件在本分支上全绿。本工作流内部无法访问确切的 CI 日志(无 GitHub 凭据),因此该检查的最终判定仍以工作流的独立 CI 为准。

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-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

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

Reviewed. Suggestions are inline.

Not reviewed: reverse audit — reached the round cap (5) without two consecutive dry rounds; round 5 still surfaced findings.

Not reviewed: build-and-test — Test (macos-latest / windows-latest, Node 22.x) was skipped in CI and the unit suite did not run locally on those OSes.

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

Not explored to full depth (tool budget reached): You are review agent reverse-audit — Reverse audit agen...: (none — all planned checks completed within budget); chunk 2: none — all checks I intended completed within budget..

Test Plan (not a blocker): src/agents/worktree-pin.test.tsno such file or directory; src/agents/runtime/workflow-orchestrator.test.tsno such file or directory; src/agents/runtime/workflow-sandbox.test.tsno such file or directory; src/agents/runtime/workflow-journal.test.tsno such file or directory; src/tools/workflow/workflow.test.tsno such file or directory; and 1 more.

中文说明

已审查。 建议见行内评论。

未审查:reverse audit — reached the round cap (5) without two consecutive dry rounds; round 5 still surfaced findings。

未审查:build-and-test — Test (macos-latest / windows-latest, Node 22.x) was skipped in CI and the unit suite did not run locally on those OSes。

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

未探索到全部深度(达到工具调用预算):You are review agent reverse-audit — Reverse audit agen...:(none — all planned checks completed within budget);chunk 2:none — all checks I intended completed within budget.

Test Plan(非阻断):src/agents/worktree-pin.test.tsno such file or directory; src/agents/runtime/workflow-orchestrator.test.tsno such file or directory; src/agents/runtime/workflow-sandbox.test.tsno such file or directory; src/agents/runtime/workflow-journal.test.tsno such file or directory; src/tools/workflow/workflow.test.tsno such file or directory; and 1 more。

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

Comment on lines +363 to +366
for (const line of lines.slice(1)) {
const attr = line.trim();
if (attr === '') break; // blank line ends the first record
if (!isWorktreeListPorcelainAttribute(attr)) return null;

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.

[Suggestion] The round-2 newline-truncation detector is bypassable: a truncated first porcelain record is ACCEPTED — getMainWorktreePath() returns the truncated prefix as the main-tree anchor — when the path remainder after an interior newline is attribute-shaped (detached, bare, locked, prunable, HEAD …, branch …), or when the path ends with a newline (the blank remainder hits the break). Probe-reproduced against real git end-to-end: a pin to a neighbouring repo's registered worktree was accepted, rebinding the child outside the invoking repository. Bounded at Suggestion because the trigger requires the invoking repo's own path to contain a newline (self-inflicted, not model-producible) and a pin is documented as a cwd pin, not a sandbox — but it defeats a protection this PR itself added with an explicit test and threat comment.

Failure scenario: repo R1 cloned into /outer/sub/<LF>detached with /outer/sub inside another repo R2 — porcelain emits worktree /outer/sub/ + detached + HEAD …, every continuation line passes isWorktreeListPorcelainAttribute, the anchor becomes /outer/sub/, and GitWorktreeService('/outer/sub') validates a model-supplied pin against R2's registry — the child's workspace rebinds outside the invoking repository.

Suggested fix: round-trip-validate the parsed anchor before trusting it (git -C <anchor> rev-parse --git-common-dir must agree with this repo's common dir), or anchor via rev-parse --git-common-dir, or parse --porcelain -z on Git ≥ 2.36. Anchor validation also closes the whitespace vector in the sibling comment.

中文说明

第二轮加入的换行截断检测器可被绕过:当路径在内部换行之后的残余部分呈属性形状(detachedbarelockedprunableHEAD …branch …),或路径以换行结尾(空残余命中 break)时,被截断的 porcelain 首条目会被接受——getMainWorktreePath() 把截断前缀当作主树锚点返回。已用真实 git 做端到端探针复现:钉到相邻仓库已登记的 worktree 被接受,子 agent 被重绑定到调用仓库之外。触发条件要求调用仓库自身路径含换行(自伤布局,模型无法制造),且钉住按文档只是 cwd 钉住而非沙箱,因此定为 Suggestion;但它击败的是本 PR 自己用显式测试和威胁注释加上的防护。

失败场景:仓库 R1 克隆到 /outer/sub/<LF>detached,而 /outer/sub 位于另一仓库 R2 内——porcelain 输出 worktree /outer/sub/ + detached + HEAD …,每条续行都通过 isWorktreeListPorcelainAttribute,锚点变为 /outer/sub/GitWorktreeService('/outer/sub') 会对照 R2 的登记册校验模型提供的钉住路径——子 agent 的工作区被重绑定到调用仓库之外。

建议修复:在信任解析出的锚点前做往返校验(git -C <锚点> rev-parse --git-common-dir 必须与本仓库的 common dir 一致),或改用 rev-parse --git-common-dir 取锚点,或在 Git ≥ 2.36 上用 --porcelain -z 解析。锚点往返校验同时能堵住相邻评论中的空白字符向量。

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

Comment on lines +796 to +797
} else if (opts.workingDir !== undefined) {
if (typeof opts.workingDir !== 'string' || !opts.workingDir) {

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.

[Suggestion] The two workingDir entrances disagree on workingDir + isolation together: the sandbox entrance throws "incompatible options", but this entrance resolves the combination by precedence — isolation: 'worktree' wins and workingDir is silently dropped, the opposite of AgentTool's documented working_dir-wins semantics. The sandbox gate is additionally probe-verified bypassable from model-authored vm scripts: the gates read the raw agentOpts BEFORE the JSON revival, so an enumerable getter on isolation returning undefined for the two validation reads and 'worktree' at stringify time slips the combination through to dispatch.

Failure scenario: a script passes {workingDir: 'wt'} plus a read-counted isolation getter → the host dispatch receives {workingDir:'wt', isolation:'worktree'} → a fresh isolation worktree is provisioned and workingDir is never read — the agent runs somewhere other than the directory the caller named, and cleanup later removes that worktree while the script believes state accumulated in the pinned tree. The harm is bounded (same-repo cwd deviation, no boundary escape), but this is exactly the silent-winner failure the sandbox gate's own comment says it exists to name.

Suggested fix (probe-flip verified): throw in runOverridePath when both are defined — the host layer sees the revived plain object, so this is TOCTOU-proof:

if (opts.isolation !== undefined && opts.workingDir !== undefined) {
  throw new Error('agent({workingDir, isolation}): incompatible options. ...');
}

Optionally also re-gate safeOpts in the sandbox so the script gets the named error instead of a dispatch-time refusal.

中文说明

workingDir 的两个入口对 workingDirisolation 同时出现的处理不一致:sandbox 入口抛出 "incompatible options",而此入口按优先级裁决——isolation: 'worktree' 胜出、workingDir 被静默丢弃,与 AgentTool 文档声明的 working_dir 优先语义相反。此外,sandbox 关卡已被探针验证可被模型编写的 vm 脚本绕过:关卡在 JSON 复活之前读取原始 agentOpts,因此给 isolation 一个可枚举 getter(前两次验证读取返回 undefined、stringify 时返回 'worktree')即可让组合溜进派发层。

失败场景:脚本传入 {workingDir: 'wt'} 加一个计数读取的 isolation getter → 宿主派发收到 {workingDir:'wt', isolation:'worktree'} → 现场新建隔离 worktree、workingDir 从未被读取——agent 跑在调用方指定目录之外的地方,cleanup 随后删除该 worktree,而脚本以为状态积累在被钉住的树里。危害有界(同仓库内 cwd 偏离、无边界逃逸),但这正是 sandbox 关卡注释自称存在目的就是要点名的「静默胜出」失败。

建议修复(探针已验证翻转):在 runOverridePath 中两者同时定义时抛错——宿主层看到的是复活后的纯对象,因此没有 TOCTOU 问题;可选地同时在 sandbox 里对 safeOpts 复检,让脚本拿到点名错误而非派发期拒绝。

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

Comment on lines +87 to 88
'agent() opts: `{ label?, phase?, schema?, model?, agentType?, isolation?, workingDir?, stallMs? }`. ' +
'`schema` (JSON Schema object): the subagent must deliver its result ' +

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.

[Suggestion] Two model-facing copies of the agent() opts drift in this PR. (1) The opts list advertised here includes stallMs, but it is the only opt with no explanatory paragraph — its no-progress semantics, the 60s default (DEFAULT_STALL_MS), and the 0 kill-switch ("A stallMs of 0 means 'no watchdog'", workflow-stall.ts) are undiscoverable from the schema a script author or script-writing model actually reads. (2) The tool-level copy — WORKFLOW_TOOL_DESCRIPTION's capability enumeration, ~line 552, "Per-call agent({ schema, agentType, model, isolation: 'worktree' }) covers … git-worktree-isolated subagents" — still presents isolation as the entire worktree story and never names workingDir or stallMs, at the moment this PR introduces workingDir with text explicitly documenting that isolation "cannot serve" the pinning case.

Failure scenario: a script needing a legitimately quiet long dispatch cannot learn that stallMs: 0 disables the watchdog — it guesses a huge number (watchdog stays armed) or misreads the opt as a wall-clock cap, yielding spurious aborts on a healthy run; and a model authoring from the tool description passes isolation: 'worktree' for exactly the pre-existing caller-owned-worktree case workingDir exists for — refused on a dirty parent or provisioned as a fresh checkout lacking the uncommitted state that is the point — while workingDir stays undiscoverable. The file's docblock calls this prose load-bearing and interpolates the caps precisely to prevent hand-sync drift; the opts enumeration is hand-synced, and this PR is the drift event.

Suggested fix: add a stallMs paragraph alongside the others here (stall detector, not a wall-clock cap; 0 disables the watchdog), and extend the tool-level enumeration to agent({ schema, agentType, model, isolation: 'worktree', workingDir, stallMs }), naming pinning to a caller-owned worktree and the stall watchdog.

中文说明

本 PR 让面向模型的两份 agent() 选项文案发生了漂移。(1) 此处列出的选项已包含 stallMs,但它是唯一没有解释段落的选项——其无进展语义、60 秒默认值(DEFAULT_STALL_MS)以及 0 关闭开关("A stallMs of 0 means 'no watchdog'",workflow-stall.ts)在脚本作者或写脚本的模型实际阅读的 schema 中均不可发现。(2) 工具级文案——WORKFLOW_TOOL_DESCRIPTION 的能力枚举(约第 552 行,"Per-call agent({ schema, agentType, model, isolation: 'worktree' }) covers … git-worktree-isolated subagents")——仍把 isolation 呈现为 worktree 故事的全部,只字未提 workingDirstallMs;而本 PR 恰恰在引入 workingDir 时明确写着 isolation "cannot serve" 钉住场景。

失败场景:需要合法静默长派发的脚本无法得知 stallMs: 0 可关闭看门狗——要么猜一个巨大数值(看门狗仍然在位),要么把它误读为墙钟上限,健康运行被误杀;从工具描述出发的模型会对「既有、调用方自有 worktree」这一 workingDir 正是为之存在的场景传 isolation: 'worktree'——在脏父树上被拒绝,或拿到一个缺少关键未提交状态的全新检出——而 workingDir 始终不可发现。该文件的 docblock 自称这些文字是承重件,并特意用插值处理各项上限以避免手工同步漂移;选项枚举靠手工同步,而本 PR 正是漂移事件。

建议修复:在此为 stallMs 补一段(停滞检测器而非墙钟上限;0 关闭看门狗),并把工具级枚举扩为 agent({ schema, agentType, model, isolation: 'worktree', workingDir, stallMs }),点名「钉住到调用方自有 worktree」与停滞看门狗。

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

// about who owns the directory's lifetime, so name it here rather than
// silently letting one win.
if (agentOpts.workingDir !== undefined) {
if (typeof agentOpts.workingDir !== 'string' || agentOpts.workingDir.length === 0) {

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.

[Suggestion] Whitespace-only workingDir passes both workflow entrance gates — this length === 0 check and the orchestrator's !opts.workingDir — and is refused only deep in the registration gate with a message blaming the directory ("not a registered linked worktree"), while the equivalent AgentTool input is trimmed and refused up front ("must be a non-empty string"). Probe-confirmed through the real sandbox: dispatch received {"workingDir":" "}. Fail-closed, but the three surfaces now demonstrably disagree on which layer rejects a blank-ish value, and the deep message misdirects.

Failure scenario: agent('x', { workingDir: ' ' }) clears both non-empty checks, resolves to /repo/ , passes containment, and fails isRegisteredLinkedWorktree with a registration-status diagnosis for a blank value — sending the model to fix worktree registration instead of the argument.

Suggested change
if (typeof agentOpts.workingDir !== 'string' || agentOpts.workingDir.length === 0) {
if (typeof agentOpts.workingDir !== 'string' || agentOpts.workingDir.trim().length === 0) {

(Apply the same trim-based check at the orchestrator entrance.)

中文说明

纯空白的 workingDir 能通过两个 workflow 入口关卡——此处的 length === 0 检查与 orchestrator 的 !opts.workingDir——只在登记关卡深处被拒绝,且错误消息怪的是目录("not a registered linked worktree");而等价的 AgentTool 输入会先被 trim 并在上层以 "must be a non-empty string" 拒绝。已通过真实 sandbox 探针确认:派发收到了 {"workingDir":" "}。虽然失败关闭,但三个面现在确凿地在「哪一层拒绝空白值」上互相矛盾,且深层消息具有误导性。

失败场景:agent('x', { workingDir: ' ' }) 通过两处非空检查、解析为 /repo/ 、通过包含检查,然后在 isRegisteredLinkedWorktree 处以「登记状态」诊断拒绝一个空白值——模型会去修 worktree 登记而不是修参数。

建议修复如上(对 orchestrator 入口同样采用 trim 检查)。

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

Comment on lines +368 to +369
const mainPath = firstLine.slice('worktree '.length).trim();
return mainPath.length > 0 ? mainPath : null;

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.

[Suggestion] getMainWorktreePath() silently mutates a main-tree path with leading/trailing whitespace: both lines[0]?.trim() (~line 360) and this payload .trim() strip the space/tab/CR that git's porcelain output preserves verbatim — producing a wrong anchor with NO residue line, so the newline-truncation detector can never fire (the record is well-formed). Probe-verified against real git (a trailing-space repo; porcelain preserves the space) and end-to-end: with a sibling repo at the trimmed path, containment anchored there and its registry validated a pin to a worktree outside the invoking repository; the two-site fix flipped the probe. Same trigger class as the sibling finding (self-inflicted path plus a sibling repo at the trimmed path), hence Suggestion.

Failure scenario: invoking repo at /srv/proj (trailing space) → the method returns /srv/proj; if that sibling exists as another repo with registered worktrees, a model-supplied pin to one of them passes both gates and the child rebinds outside the invoking repository. Without the sibling, pins fail closed with confusing refusals.

Suggested fix: strip only the line terminator, at both sites — const firstLine = (lines[0] ?? '').replace(/\r$/, ''); and take firstLine.slice('worktree '.length) without .trim() (keep the length > 0 guard). The alternative round-trip anchor validation closes this and the detector bypass above; note the getRepoTopLevel() fallback has the same trailing-whitespace trim, so cover the whole fallback chain.

中文说明

getMainWorktreePath() 会静默改变带前导/尾随空白的主树路径:lines[0]?.trim()(约第 360 行)与这里的载荷 .trim() 都会去掉 git porcelain 输出原样保留的空格/制表符/CR——产生一个没有任何残余行的错误锚点,换行截断检测器因此永远不会触发(记录是良构的)。已用真实 git 探针验证(尾随空格仓库;porcelain 保留空格)并端到端复现:当修剪后的路径处存在兄弟仓库时,包含检查以该处为锚、其登记册验证通过了钉到调用仓库之外 worktree 的钉住;修复两处 .trim() 后探针翻转。触发条件与相邻发现同类(自伤路径加修剪路径处的兄弟仓库),故定为 Suggestion。

失败场景:调用仓库位于 /srv/proj (尾随空格)→ 方法返回 /srv/proj;若该兄弟路径是另一个有已登记 worktree 的仓库,模型钉到其中之一可两关全过,子 agent 被重绑定到调用仓库之外;若兄弟不存在,钉住以令人困惑的拒绝失败关闭。

建议修复:两处都只去掉行终止符——const firstLine = (lines[0] ?? '').replace(/\r$/, '');,取 firstLine.slice('worktree '.length) 而不再 .trim()(保留 length > 0 守卫)。锚点往返校验可同时堵住此处与上面的检测器绕过;注意 getRepoTopLevel() 回退也有同样的尾随空白 trim,需覆盖整条回退链。

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

Comment on lines +105 to +108
const realRepoRoot = await fs.realpath(repoRoot).catch(() => repoRoot);
const realResolved = await fs
.realpath(resolvedPath)
.catch(() => resolvedPath);

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.

[Suggestion] Containment compares the two sides in different representations when only one realpath succeeds — canonical realRepoRoot against the verbatim spelling of a resolvedPath whose realpath rejected (absent path). Under a symlinked repo ancestor (macOS /tmp/private/tmp, /var/folders, autofs homes), a nonexistent pin target yields path.relative('/private/tmp/repo', '/tmp/repo/…')'../../…' → refusal "resolves outside this repository (/private/tmp/repo)" — naming a canonical root the caller never typed and hiding the true cause (the path does not exist). Probe-reproduced with real symlinked temp dirs; the fix flipped the probe to the accurate registration-gate message. Fails closed; the reverse direction cannot false-accept.

Failure scenario: session repo under /tmp; the model passes .qwen/tmp/review-pr-2 when only review-pr-1 exists → a misleading containment refusal instead of the registration gate's accurate "absent from git worktree list", so the model retries against a nonexistent containment problem (e.g. /private/… spellings) instead of correcting the path.

Suggested fix: keep both sides in one representation — on realpath(resolvedPath) rejection, run the containment comparison against the un-canonicalized repoRoot (migration-free for the existing stub tests); or reject early with an explicit "does not exist" error (that variant needs the plain-string stub tests moved to real temp dirs).

中文说明

当只有一侧 realpath 成功时,包含检查用不同表示比较两侧——规范化后的 realRepoRoot 对比 realpath 失败(路径不存在)的 resolvedPath 原文拼写。在仓库祖先为符号链接时(macOS /tmp/private/tmp/var/folders、autofs 家目录),不存在的钉住目标会得到 path.relative('/private/tmp/repo', '/tmp/repo/…')'../../…' → 拒绝 "resolves outside this repository (/private/tmp/repo)"——点名了一个调用方从未输入的规范化根,掩盖了真实原因(路径不存在)。已用真实符号链接临时目录探针复现;修复后探针翻转为登记关卡的准确消息。失败关闭;反向不可能误接受。

失败场景:会话仓库位于 /tmp 下;模型在只有 review-pr-1 时传入 .qwen/tmp/review-pr-2 → 得到误导性的包含拒绝而非登记关卡准确的 "absent from git worktree list",模型会针对一个不存在的包含问题反复重试(如改用 /private/… 拼写)而不是纠正路径。

建议修复:让两侧保持同一表示——realpath(resolvedPath) 失败时改用未规范化的 repoRoot 做包含比较(现有 stub 测试无需迁移);或提前以明确的 "does not exist" 错误拒绝(该变体需把纯字符串 stub 测试迁到真实临时目录)。

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

Comment on lines +92 to +94
const repoRoot =
(await probe.getMainWorktreePath()) ??
(await probe.getRepoTopLevel()) ??

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.

[Suggestion] When getMainWorktreePath() correctly returns null (truncation detected — the good path of the round-2 fix — or worktree list unreadable) and the parent runs inside a linked worktree, this fallback chain degrades the anchor to the current worktree's own root (getRepoTopLevel() from inside a linked worktree answers that worktree's root), so every legitimate sibling-worktree pin is over-refused with guidance that cannot be satisfied. Real-git probe: the mislabeled refusal reproduces; the flip arm (clean main-tree path) succeeds on the identical layout; the registration gate would have passed the sibling (it reads gitdir files directly, newline-immune) — the refusal is purely the degraded anchor's. The rationale comment above names exactly this harm for the getMainWorktreePath()-success state but does not answer for the fallback state.

Failure scenario: a repo whose main-tree path contains a newline, two registered sibling worktrees wt1/wt2, cwd inside wt1 (the documented normal state for /review pipelines), the model pins wt2 → "working_dir … resolves outside this repository (…/wt1). Pass a worktree that lives inside the repository." — the target genuinely lives inside the repository and no registered worktree exists inside the current one, so every retry fails and the pipeline stalls. Fails closed — no escape.

Suggested fix: track which arm produced the anchor; when it fell back past getMainWorktreePath(), say so in the refusal — e.g. "the repository's main working tree could not be determined (git worktree list unreadable or its path malformed), so containment was checked against the current worktree root."

中文说明

getMainWorktreePath() 正确地返回 null(检测到截断——第二轮修复的良性路径——或 worktree list 不可读)且父会话运行在 linked worktree 内时,这条回退链把锚点退化为当前 worktree 自己的根(从 linked worktree 内调用 getRepoTopLevel() 回答的是该 worktree 的根),于是每一个合法的兄弟 worktree 钉住都被过度拒绝,且指引无法满足。真实 git 探针:误标拒绝可复现;翻转臂(干净主树路径)在完全相同的布局下成功;登记关卡本可通过该兄弟(它直接读 gitdir 文件,不受换行影响)——拒绝纯粹来自退化的锚点。上方的动机注释恰好为 getMainWorktreePath() 成功的状态点名了此害,却没有为回退状态兜底。

失败场景:主树路径含换行的仓库、两个已登记兄弟 worktree wt1/wt2、cwd 在 wt1(/review 流水线的文档化常态),模型钉住 wt2 → "working_dir … resolves outside this repository (…/wt1). Pass a worktree that lives inside the repository."——目标确实在仓库内,且当前 worktree 内不存在任何已登记 worktree,任何重试都会失败,流水线卡死。失败关闭——无逃逸。

建议修复:记录锚点出自哪一支;当回退到 getMainWorktreePath() 之后时,在拒绝文案中说明——例如 "the repository's main working tree could not be determined (git worktree list unreadable or its path malformed), so containment was checked against the current worktree root."

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

// check rejects the main tree, a plain sub-directory (including a stale
// registry record whose directory was recreated), a worktree belonging to
// another repo, and a hand-crafted directory carrying a copied `.git` file.
if (!(await wtService.isRegisteredLinkedWorktree(resolvedPath))) {

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.

[Suggestion] The validator resolves the pin path twice and returns the lexical spelling: containment computes realResolved (~line 106), then isRegisteredLinkedWorktree re-resolves independently (its first statement is a fresh fs.realpath, gitWorktreeService.ts:1330), and the resolver returns path: resolvedPath (~line 149) — which the rebind block binds as the child's cwd. Probe-verified deterministically (no race needed): with a same-repo-registered worktree living OUTSIDE the repo (git worktree add may place worktrees anywhere; the entry lands in this repo's .git/worktrees/) and a symlink passed as the pin, re-pointing the symlink after validation moved the spawned child's pwd outside the repository while both gates had passed; the resolve-once fix flipped the probe. WorkspaceContext snapshots the canonical root at construction (file tools largely absorbed), so the observable drift is the shell cwd. Bounded at Suggestion: registering the outside worktree requires same-repo write access (which a workflow subagent with shell plausibly has), and pinning is documented as a cwd pin — but the pin itself silently landing outside despite passing validation is not the "explicit absolute path" case the JSDoc carves out.

Failure scenario: a model with write access runs git worktree add /tmp/evil-wt, passes a symlink /repo/link/repo/wt-in as workingDir; both gates pass on wt-in; the symlink is re-pointed to /tmp/evil-wt any time before or during the child run; the child's shell cwd resolves outside the repository while the run believes it is pinned inside.

Suggested fix (probe-verified, all 10 existing worktree-pin tests stay green): resolve once and thread the single resolution through — pass realResolved (not resolvedPath) to isRegisteredLinkedWorktree/getRegisteredWorktreeBranch, and return path: realResolved so the child binds to the exact directory object that was validated.

中文说明

校验器对钉住路径解析了两次并返回词法拼写:包含检查计算 realResolved(约第 106 行),随后 isRegisteredLinkedWorktree 独立地重新解析(其第一条语句就是新的 fs.realpath,gitWorktreeService.ts:1330),而 resolver 返回 path: resolvedPath(约第 149 行)——重绑定块把它绑为子 agent 的 cwd。已用确定性探针验证(无需竞态):把一个登记在本仓库、但实际位于仓库之外的 worktree(git worktree add 可以把 worktree 放在任何地方;条目落在本仓库的 .git/worktrees/)配合一个作为钉住目标的符号链接,在校验通过后重新指向符号链接,即可让派生出的子进程 pwd 落到仓库之外——而两道关卡都曾通过;「只解析一次」的修复使探针翻转。WorkspaceContext 在构造时对规范化根做快照(文件工具基本被吸收),可观察的漂移是 shell 的 cwd。定为 Suggestion:登记仓库外的 worktree 需要同仓库写权限(带 shell 的 workflow 子 agent 很可能有),且钉住按文档只是 cwd 钉住——但「通过校验的钉住却静默落在仓库外」并不是 JSDoc 明确排除的「显式绝对路径」情形。

失败场景:有写权限的模型执行 git worktree add /tmp/evil-wt,把符号链接 /repo/link/repo/wt-in 作为 workingDir 传入;两道关卡都按 wt-in 通过;符号链接在子运行之前或期间被重新指向 /tmp/evil-wt;子 agent 的 shell cwd 解析到仓库之外,而运行以为自己被钉在仓库内。

建议修复(探针已验证,现有 10 个 worktree-pin 测试全部保持绿):只解析一次并把单一结果贯穿到底——把 realResolved(而非 resolvedPath)传给 isRegisteredLinkedWorktree/getRegisteredWorktreeBranch,并返回 path: realResolved,使子 agent 绑定到被校验过的那个目录对象。

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

- Round-trip-validate the porcelain main-tree anchor (git-common-dir must
  agree) so attribute-shaped or trailing-newline truncations cannot aim
  the pin gate at a different repository's worktree registry
- Preserve legitimate path whitespace when parsing the anchor and the
  --show-toplevel fallback (terminator-only strip, untrimmed raw output)
- Thread one canonical realpath through both pin gates and the rebind so
  a re-pointed symlink cannot land the child where neither gate looked
- Canonicalise both containment sides or neither, so an absent target
  reaches the registration gate's accurate message instead of a
  manufactured outside-the-repository refusal
- Name the degraded anchor in the containment refusal when the main
  working tree could not be determined
- Throw on agent({workingDir, isolation}) at the orchestrator entrance
  (revived plain object — not evadable by the sandbox getter trick)
- Trim-based blank check for workingDir at both workflow entrances
- Document stallMs in the workflow schema and extend the tool-level
  capability enumeration to workingDir and stallMs
…wenLM#8972)

The round-3 degraded-anchor test passes null to the getMainWorktreePath
mock, but the vi.hoisted stub inferred Promise<string> from its default
implementation while GitWorktreeService.getMainWorktreePath() returns
Promise<string | null> — tsc --build failed on the clean rebuild of
packages/core. Annotate the stub with the real service signature.
@QwenLM QwenLM deleted a comment from danialzivehdadr Aug 12, 2026
@QwenLM QwenLM deleted a comment from danialzivehdadr Aug 12, 2026
@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 #8972 review feedback (same-run verification repair)

The previous commit 1ef877794b (all 8 round-3 suggestions implemented) was
REJECTED by the deterministic verification gate: a clean rebuild of
packages/core failed with
src/agents/worktree-pin.test.ts(205,47): error TS2345: Argument of type 'null' is not assignable to parameter of type 'string',
and the baseline A/B leg confirmed the failure belongs to that commit. Per the
same-run repair rule, the rejected commit is preserved and this round adds one
verified follow-up commit, 6fd1953fe5, that fixes exactly that rejection.

Rejection repair

Root cause: the round-3 commit added the degraded-anchor test
('names the degraded anchor when the main worktree is undeterminable'),
which calls svc.getMainWorktreePath.mockResolvedValue(null). The
vi.hoisted stub factory declared
getMainWorktreePath: vi.fn(async () => '/repo'), so the mock type was
inferred as () => Promise<string> — but the real
GitWorktreeService.getMainWorktreePath() returns
Promise<string | null>. The prior round's incremental tsc --build state
masked this; the gate's clean rebuild caught it.

Fix (one line): annotate the stub with the real service signature —
getMainWorktreePath: vi.fn(async (): Promise<string | null> => '/repo').
No runtime behavior change; the other stub methods never receive null, so
they are left as-is.

The 8 inline findings — re-verified resolved in the preserved commit

Each finding was re-checked against the code at HEAD this round (not assumed
from the prior round's summary); all 8 are resolved in 1ef877794b:

  • rc:3768316678 (R3-1, newline-truncation detector bypass)getMainWorktreePath() now round-trip-validates the parsed anchor: rev-parse --git-common-dir run at the anchor must equal this repository's common dir (both sides resolved + realpathed with fallback; mismatch or probe failure returns null, failing closed to the --show-toplevel fallback). Both bypass arms (attribute-shaped remainder; trailing-newline path) are covered, with unit tests and real-git integration tests.
  • rc:3768316687 (R3-2, two entrances disagree / sandbox gate bypass)runOverridePath throws agent({workingDir, isolation}): incompatible options. … before any provisioning. It runs on the JSON-revived plain object, so the enumerable-getter trick that evades the pre-revival sandbox gate cannot evade it. The reviewer's explicitly-optional sandbox re-gate remains declined: the host-side throw already surfaces the identical named error to the script at dispatch time, so the re-gate would be a duplicate check with no observable difference.
  • rc:3768316693 (R3-3, model-facing opts drift) — the script parameter description now has a stallMs paragraph (no-progress watchdog, not a wall-clock cap; suspended while a tool is in flight; 0 disables) interpolating DEFAULT_STALL_MS / MAX_STALL_ATTEMPTS from workflow-stall.ts, and WORKFLOW_TOOL_DESCRIPTION's enumeration is now agent({ schema, agentType, model, isolation: 'worktree', workingDir, stallMs }), naming pinning to a caller-owned worktree and the stall watchdog.
  • rc:3768316701 (R3-4, whitespace-only workingDir passes both gates) — both the sandbox gate and the orchestrator entrance now use the trim-based check (typeof … !== 'string' || ….trim().length === 0), matching AgentTool's up-front validation; covered by the extended sandbox test and a new orchestrator-entrance test.
  • rc:3768316724 (R3-5, anchor whitespace mutation) — both parse sites strip only the line terminator (porcelain first line replace(/\r$/, ''); payload untrimmed with the length guard kept), and getRepoTopLevel() now uses .raw(['rev-parse', '--show-toplevel']) with terminator-only stripping because simple-git's revparse() trims. R3-1's round-trip validation backstops residual ambiguity.
  • rc:3768316738 (R3-6, mixed-representation containment) — containment canonicalises both sides or neither: when either fs.realpath fails, the comparison runs on verbatim spellings and the refusal names the representation compared, so an absent target reaches the registration gate's accurate "absent from git worktree list" message. Real-temp-dir test with a symlinked repo ancestor added.
  • rc:3768316742 (R3-7, degraded anchor over-refusal) — the resolver tracks whether getMainWorktreePath() produced the anchor; on fallback the refusal says "The repository's main working tree could not be determined (git worktree list unreadable or its path malformed), so containment was checked against the current worktree root." Unit-tested (this is the test whose null argument exposed the rejected type error).
  • rc:3768316754 (R3-8, double resolution / lexical return) — the pin path is resolved once (pinnedPath = realResolved ?? resolvedPath) and threaded through isRegisteredLinkedWorktree, getRegisteredWorktreeBranch, and the returned path/slug, so a symlink re-pointed after validation cannot move the child's cwd. Real-temp-dir test asserts the gates receive and the result carries the canonical path.

Conflict notes

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

Verification

Commands actually run this round and their results (required checks per the
repository's gate):

  • npm run build — FAILED at the preserved HEAD, reproducing the gate's exact rejection (src/agents/worktree-pin.test.ts(205,47): error TS2345); passed after the one-line fix, and passed again post-commit
  • npm run typecheck — passed
  • npm run lint (eslint . + integration-tests) — passed
  • npx prettier --check on the 10 files changed across this round's two commits — passed
  • vitest run src/agents/worktree-pin.test.ts src/services/gitWorktreeService.test.ts src/services/gitWorktreeService.linked.integ.test.ts (packages/core, incl. real-git arms) — 63 passed, including the previously-uncompilable degraded-anchor test
  • vitest run src/agents/runtime/workflow-sandbox.test.ts src/agents/runtime/workflow-orchestrator.test.ts src/tools/workflow/workflow.test.ts src/tools/agent/agent.test.ts (packages/core) — 588 passed
  • Integration tests after npm run bundle — not run: the changed behavior (pin validation, workflow entrance gates, model-facing descriptions) is exercised by the unit suites and real-git integration tests above, not only through the bundled CLI or integration harness.

No settings source changed, so npm run generate:settings-schema was not
required.

中文说明

轮次总结 — PR #8972 评审反馈(同轮验证修复)

上一个提交 1ef877794b(已实现全部 8 条第 3 轮建议)被确定性验证门禁拒绝:packages/core 的干净重新构建失败,报错 src/agents/worktree-pin.test.ts(205,47): error TS2345: Argument of type 'null' is not assignable to parameter of type 'string',且基线 A/B 对照组确认该失败属于该提交。按照同轮修复规则,被拒绝的提交予以保留,本轮新增一个已验证的后续提交 6fd1953fe5,精确修复该拒绝项。

拒绝项修复

根因: 第 3 轮提交新增了退化锚点测试('names the degraded anchor when the main worktree is undeterminable'),其中调用 svc.getMainWorktreePath.mockResolvedValue(null)。而 vi.hoisted 桩工厂声明的是 getMainWorktreePath: vi.fn(async () => '/repo'),mock 类型被推断为 () => Promise<string>——但真实的 GitWorktreeService.getMainWorktreePath() 返回 Promise<string | null>。上一轮的 tsc --build 增量状态掩盖了该错误,门禁的干净重建将其捕获。

修复(一行): 为桩标注真实服务签名——getMainWorktreePath: vi.fn(async (): Promise<string | null> => '/repo')。无运行时行为变化;其余桩方法从未接收 null,保持原样。

8 条行内发现 — 在保留提交中重新验证为已解决

本轮逐条对照 HEAD 代码重新核查(而非采信上一轮的总结);8 条全部已在 1ef877794b 中解决:

  • rc:3768316678(R3-1,换行截断检测器可被绕过)getMainWorktreePath() 现对解析出的锚点做往返校验:在锚点处运行 rev-parse --git-common-dir 必须等于本仓库的 common dir(两侧均 resolve 并带兜底地 realpath;不一致或探测失败则返回 null,失败关闭回退到 --show-toplevel)。两条绕过臂(属性形状残余、尾随换行路径)均已覆盖,含单元测试与真实 git 集成测试。
  • rc:3768316687(R3-2,两个入口处理不一致 / sandbox 关卡可被绕过)runOverridePath 在任何 provisioning 之前抛出 agent({workingDir, isolation}): incompatible options. …。它作用于 JSON 复活后的纯对象,因此能骗过复活前 sandbox 关卡的可枚举 getter 伎俩无法绕过它。评审者明确标注为可选的 sandbox 复检维持不做:宿主侧抛错已能在派发时向脚本呈现完全相同的点名错误,复检只是没有可观察差异的重复检查。
  • rc:3768316693(R3-3,面向模型的选项文案漂移)script 参数描述现包含 stallMs 段落(无进展看门狗而非墙钟上限;工具在途期间暂停计时;0 关闭),并从 workflow-stall.ts 插值 DEFAULT_STALL_MS / MAX_STALL_ATTEMPTSWORKFLOW_TOOL_DESCRIPTION 的能力枚举已扩为 agent({ schema, agentType, model, isolation: 'worktree', workingDir, stallMs }),点名钉住到调用方自有 worktree 与停滞看门狗。
  • rc:3768316701(R3-4,纯空白 workingDir 通过两道入口关卡) — sandbox 关卡与 orchestrator 入口现均使用 trim 检查(typeof … !== 'string' || ….trim().length === 0),与 AgentTool 的上层校验一致;由扩展的 sandbox 测试与新增的 orchestrator 入口测试覆盖。
  • rc:3768316724(R3-5,锚点空白字符变异) — 两处解析点均只去掉行终止符(porcelain 首行 replace(/\r$/, '');载荷不再 trim、保留长度守卫);getRepoTopLevel() 改用 .raw(['rev-parse', '--show-toplevel']) 并只去行终止符,因为 simple-git 的 revparse() 会 trim。R3-1 的往返校验为残余歧义兜底。
  • rc:3768316738(R3-6,混合表示的包含比较) — 包含检查要么两侧都规范化、要么都不规范化:任一 fs.realpath 失败时比较在原文拼写上进行,拒绝消息点名实际使用的表示,使不存在的路径到达登记关卡准确的 "absent from git worktree list" 消息。新增带符号链接仓库祖先的真实临时目录测试。
  • rc:3768316742(R3-7,退化锚点过度拒绝) — 解析器记录锚点是否来自 getMainWorktreePath();回退时拒绝文案说明 "The repository's main working tree could not be determined (git worktree list unreadable or its path malformed), so containment was checked against the current worktree root."。已有单元测试(正是该测试的 null 实参暴露了被拒绝的类型错误)。
  • rc:3768316754(R3-8,双重解析 / 返回词法拼写) — 钉住路径只解析一次(pinnedPath = realResolved ?? resolvedPath),贯穿 isRegisteredLinkedWorktreegetRegisteredWorktreeBranch 与返回的 path/slug,校验后重新指向的符号链接无法再移动子 agent 的 cwd。真实临时目录测试断言关卡收到且结果携带规范化路径。

冲突说明

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

验证

本轮实际运行的命令及结果(仓库门禁要求的检查):

  • npm run build — 在保留的 HEAD 上失败,精确复现门禁拒绝项(src/agents/worktree-pin.test.ts(205,47): error TS2345);一行修复后通过,提交后再次通过
  • npm run typecheck — 通过
  • npm run lint(eslint . + integration-tests)— 通过
  • npx prettier --check(本轮两个提交共改动的 10 个文件)— 通过
  • vitest run src/agents/worktree-pin.test.ts src/services/gitWorktreeService.test.ts src/services/gitWorktreeService.linked.integ.test.ts(packages/core,含真实 git 臂)— 63 通过,包括此前无法编译的退化锚点测试
  • vitest run src/agents/runtime/workflow-sandbox.test.ts src/agents/runtime/workflow-orchestrator.test.ts src/tools/workflow/workflow.test.ts src/tools/agent/agent.test.ts(packages/core)— 588 通过
  • npm run bundle 后的集成测试 — 未运行:改动的行为(钉住校验、workflow 入口关卡、面向模型的描述)由上述单元测试与真实 git 集成测试覆盖,并非只能通过打包 CLI 或集成测试框架验证。

未改动任何 settings 源文件,因此无需运行 npm run generate:settings-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

…g-dir

# Conflicts:
#	packages/core/src/tools/agent/agent.ts
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round summary — PR #8972 (address-review)

This round had no human or automated reviewer comments to triage — the feedback
consisted of one failing CI check and a base-conflict flag. Both were handled;
the conflict resolution surfaced and fixed one additional deterministic test
failure the merge would otherwise have introduced.

Feedback points and dispositions

1. Base conflict with main (--conflict true) — RESOLVED

Merged origin/main into workflow/agent-working-dir (merge commit
0c16d2d62f). Exactly one conflict existed, in
packages/core/src/tools/agent/agent.ts, plus one semantic reconciliation in
this PR's extracted validator:

  • Conflict shape: this PR moved resolveExternalWorktreeDir out of
    agent.ts into packages/core/src/agents/worktree-pin.ts (shared with the
    workflow runtime); main's feat(cli): add native multi-agent coordination #8804 (native multi-agent coordination) modified
    the same function in place and added teammate pinning. The resolution keeps
    the extraction (both conflict hunks take this branch's side) while merging
    all of main's semantics — read_only teammates, teammate working_dir
    pinning, updated validation rules and descriptions. The merged file is
    byte-identical to main's except for the function move
    (git diff origin/main -- packages/core/src/tools/agent/agent.ts = 1
    insertion, 93 deletions).
  • Semantic reconciliation: main feat(cli): add native multi-agent coordination #8804 deliberately removed the
    repository-containment gate
    ("must resolve inside the repository") from
    the pin validation and locks that in with a new test, accepts a registered sibling worktree of this repo, which pins a worktree located OUTSIDE the
    repository directory. This branch had carried the containment check into
    worktree-pin.ts. Following main's reviewed direction, the containment
    block was removed from worktree-pin.ts; the registration gate
    (isRegisteredLinkedWorktree) is now the single authoritative check, as
    main documents. All of this PR's hardening that is orthogonal to
    containment is preserved: main-worktree anchoring with truncated-anchor
    refusal (getMainWorktreePath round-trip validation), single-resolution
    threading (gate and child bind the same canonical path), and the
    registry+liveness double check in gitWorktreeService.
  • Aligned artifacts: worktree-pin.test.ts rewritten for the merged
    semantics (containment-refusal tests replaced by
    accepts a registered worktree outside the repository directory, pinning
    the new behavior); two stale "live inside the repository" wordings updated
    (one code comment in workflow-orchestrator.ts, one sentence in
    docs/users/features/sub-agents.md).

2. Failed check: Test (ubuntu-latest Node 22.x) — INVESTIGATED, no deterministic defect found on the pre-merge head; one post-merge defect fixed

CI logs are not available in this environment (no gh credentials), so the
check was reproduced locally step by step against the pre-merge head
(6fd1953fe5):

  • Reproduced an initial local failure of npm run test:ci and diagnosed every
    failing group as an artifact of this shell's environment, not a branch
    defect: (a) missing workspace dist/ entries (CI builds them in the
    bundle-closure step before tests), (b) QWEN_HOME/model env vars leaking
    from the agent's own CLI process into settings tests, (c) CI=true unset —
    the AuthDialog TUI-input tests that failed locally are it.skip-gated on
    CI === 'true' and never run on GitHub Actions.
  • With a faithful CI environment (clean HOME, CI=true, cleared API keys,
    full build), every reproducible step of the Test job passed on the
    pre-merge head
    : lockfile, runtime audit (0 critical), ESLint, i18n,
    settings-schema freshness, NOTICES freshness, the full vitest suite across
    all workspaces (≈39k tests), and helper tests. The original 4-minute CI
    failure therefore does not reproduce deterministically here; it was most
    likely transient/runner-specific. The merge below re-triggers CI, and the
    workflow's independent verification gate remains the final arbiter.
  • Post-merge deterministic failure found and fixed: main's newest commit
    (feat(cli): add native multi-agent coordination #8804) added packages/core/src/skills/bundled/coordinate/SKILL.md,
    pushing the review-context manifest's merged relatedPaths expansion from
    exactly 128 to 129 files — over the MAX_ARRAY_ITEMS wire cap pinned by
    manifest-repository-context.committed.test.ts. This is main-side breakage
    (this PR adds zero files under the affected globs), but the merged branch
    would fail the Test job on it. Fixed exactly the way the canary's design
    comment prescribes ("trim the manifest's glob list or narrow the globs'
    reach"): narrowed the skills rule's relatedPaths from
    packages/core/src/skills/** to packages/core/src/skills/* (framework
    code only; union drops to 105 ≤ 128 with headroom), updating
    .qwen/review-context.json and the test's pinned manifest/sentinel in
    lockstep. The suite passes (5/5).

3. Load-flake triage (verification hygiene)

Full-parallel local test:ci runs intermittently time out a handful of
unrelated suites (15s testTimeout) under this shared runner's load — e.g.
update.test.ts, server-default-bridge-wiring.test.ts,
shell-ast-parser-lazy.test.ts (the web-tree-sitter lazy runtime explicitly
named as load-bound in packages/core/vitest.config.ts),
DaemonSessionProvider.test.tsx. Each failing file was re-run in isolation
and passes consistently (results below). No assertion-level failures remain.

Changed files (this round)

  • packages/core/src/tools/agent/agent.ts — merge conflict resolution (kept
    the validator extraction; merged all of main's semantics).
  • packages/core/src/agents/worktree-pin.ts — removed the containment gate
    per main feat(cli): add native multi-agent coordination #8804; docs updated; hardening preserved.
  • packages/core/src/agents/worktree-pin.test.ts — rewritten for the merged
    semantics (containment tests replaced by the outside-repo acceptance test).
  • packages/core/src/agents/runtime/workflow-orchestrator.ts — comment
    wording aligned with the merged semantics.
  • docs/users/features/sub-agents.md — one sentence aligned.
  • .qwen/review-context.json — skills relatedPaths narrowed (cap canary
    fix).
  • packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts
    pinned manifest + sentinel updated in lockstep.
  • Plus the merged content of origin/main (auto-merged, no other conflicts).

Verification

Commands actually run and their results (merged tree unless noted):

  • npm run build — passed (pre-merge reproduction build and post-merge
    rebuild, exit 0 both).
  • npm run typecheck — passed (exit 0, no TS errors).
  • npm run lint — passed (exit 0).
  • npm run check:lockfile — passed. npm run audit:runtime:critical
    passed (0 critical). Settings schema regenerated — unchanged/up-to-date.
  • npx prettier --check on every edited file — passed after formatting.
  • Focused vitest, packages/core (worktree-pin.test.ts,
    gitWorktreeService.test.ts, gitWorktreeService.linked.integ.test.ts,
    agent.test.ts incl. main's new teammate/sibling-worktree tests,
    workflow.test.ts, workflow-orchestrator.test.ts) — 492 passed + 23
    passed in the linked-integ/pin re-run.
  • Focused vitest, packages/cli (the 10 suites that failed under the polluted
    local env, clean CI env) — 357/358 passed; the 1 remainder is an AuthDialog
    TUI test skipped on CI by design (it.skip when CI === 'true').
  • packages/channels/github/src/GithubAdapter.test.ts — 190 passed in 3
    consecutive isolation runs (its one observed failure was a mid-merge
    parallel-run artifact).
  • npm run test:ci (all workspaces, CI-equivalent env), twice on the merged
    tree — run 1: 19284/19285 cli tests passed, sole failure = the manifest cap
    canary (fixed, then 5/5 in isolation); run 2: only the load-timeout flakes
    listed above, each green in isolation: update.test.ts +
    server-default-bridge-wiring.test.ts +
    workspace-registration-store.test.ts + script-lint-isolation.test.ts +
    resolve-voice-config.test.ts (48 passed together),
    shell-ast-parser-lazy.test.ts (5 passed ×2),
    DaemonSessionProvider.test.tsx (266 passed ×3). All other workspaces fully
    green in both runs (core 582 files, etc.).
  • npm run test:scripts — 1036 passed; the only file-level error is
    install-script.test.js's guard that throws when zip is missing on a CI
    host — this local runner has no zip, GitHub runners ship it (the Test job
    also installs it in a dedicated step). Not a code failure.
  • npm run check:serve-fast-path-bundle (clean + cli-only rebuild + bundle +
    closure checks) — passed ("Startup bundle closure checks passed").
  • Not run locally: the no-AK integration gate — the touched behavior
    (worktree pin validation) is exercised by core unit + linked integration
    tests, not through the bundled CLI or integration harness.

Remaining risks / notes: the original pre-merge CI failure could not be
attributed to a deterministic defect locally; if it recurs on the re-triggered
CI, it is likely runner-flaky and the logs of that run are needed to say more.

中文说明

Autofix 本轮总结 — PR #8972(address-review)

本轮没有需要分叉处理的人工或自动评审意见——反馈仅包含一个失败的 CI 检查和一个与基础分支冲突的标记。两者均已处理;冲突解决过程中还发现并修复了合并本身会引入的一个确定性测试失败。

反馈点与处理结果

1. 与 main 的基础分支冲突(--conflict true)— 已解决

已将 origin/main 合并进 workflow/agent-working-dir(合并提交 0c16d2d62f)。仅存在一处冲突,位于 packages/core/src/tools/agent/agent.ts,另有一处语义协调位于本 PR 抽离出的校验器中:

  • 冲突形态:本 PR 将 resolveExternalWorktreeDiragent.ts 移入 packages/core/src/agents/worktree-pin.ts(与 workflow 运行时共享);main 的 feat(cli): add native multi-agent coordination #8804(原生多智能体协调)原地修改了同一函数并新增了 teammate 固定(pinning)能力。解决方案保留了抽离结构(两处冲突块均取本分支一侧),同时完整并入 main 的语义——read_only teammate、teammate 的 working_dir 固定、更新后的校验规则与描述。合并后的文件与 main 版本逐字节一致,仅差函数搬移本身(git diff origin/main -- packages/core/src/tools/agent/agent.ts = 1 行新增、93 行删除)。
  • 语义协调:main feat(cli): add native multi-agent coordination #8804 有意移除了仓库包含性检查("必须解析到仓库内部"),并用新测试 accepts a registered sibling worktree of this repo 锁定该语义——该测试会固定一个位于仓库目录之外的 worktree。本分支此前把包含性检查带进了 worktree-pin.ts。遵循 main 已评审的方向,已从 worktree-pin.ts 中移除包含性检查块;注册表检查(isRegisteredLinkedWorktree)成为唯一权威门槛,与 main 的文档一致。本 PR 中与包含性无关的加固全部保留:主工作树锚定及截断锚点拒绝(getMainWorktreePath 往返校验)、单一解析穿透(门槛与子代理绑定同一规范路径)、以及 gitWorktreeService 中的注册表+存活性双重检查。
  • 同步修正的产物worktree-pin.test.ts 按合并后语义重写(包含性拒绝测试替换为 accepts a registered worktree outside the repository directory,锁定新行为);两处过时的"必须位于仓库内部"措辞已更新(workflow-orchestrator.ts 中的一处代码注释、docs/users/features/sub-agents.md 中的一句话)。

2. 失败检查:Test (ubuntu-latest Node 22.x) — 已排查,合并前的头提交上未发现确定性缺陷;修复了一个合并后缺陷

本环境无法获取 CI 日志(无 gh 凭据),因此对合并前的头提交(6fd1953fe5)逐步本地复现了该检查:

  • 首次本地复现 npm run test:ci 失败,随后将每一组失败都诊断为本 shell 环境的产物,而非分支缺陷:(a) 缺少工作区 dist/ 产物(CI 在测试前的 bundle-closure 步骤会构建它们);(b) QWEN_HOME/模型环境变量从 agent 自身的 CLI 进程泄漏进 settings 测试;(c) 未设置 CI=true——本地失败的 AuthDialog TUI 输入测试在 CI === 'true' 时被 it.skip 跳过,在 GitHub Actions 上根本不会运行。
  • 在忠实的 CI 环境(干净 HOMECI=true、清空 API key、完整构建)下,合并前头提交上 Test 检查的每个可复现步骤均通过:lockfile、运行时漏洞审计(0 个严重)、ESLint、i18n、settings-schema 新鲜度、NOTICES 新鲜度、全部工作区的完整 vitest 套件(约 3.9 万个测试)以及 helper 测试。因此最初那次 4 分钟的 CI 失败在本地无法确定性复现,很可能是瞬时/Runner 相关问题。下述合并会重新触发 CI,工作流的独立验证门槛仍是最终裁决者。
  • 发现并修复了一个合并后的确定性失败:main 的最新提交(feat(cli): add native multi-agent coordination #8804)新增了 packages/core/src/skills/bundled/coordinate/SKILL.md,使 review-context manifest 合并后的 relatedPaths 展开数从恰好 128 增至 129——超出了 manifest-repository-context.committed.test.ts 锁定的 MAX_ARRAY_ITEMS 线上上限。这是 main 侧的破坏(本 PR 在受影响的 glob 下没有新增任何文件),但合并后的分支会因此让 Test 检查失败。修复方式完全遵循该哨兵测试设计注释中规定的处置("裁剪 manifest 的 glob 列表或收窄 glob 的覆盖范围"):将 skills 规则的 relatedPathspackages/core/src/skills/** 收窄为 packages/core/src/skills/*(仅框架代码;并集降至 105 ≤ 128,留有余量),并同步更新 .qwen/review-context.json 与测试中锁定的 manifest/哨兵文件。该套件通过(5/5)。

3. 负载性抖动分叉(验证卫生)

在本共享 Runner 的负载下,全并行本地 test:ci 会间歇性地把少数几个无关套件跑到超时(15 秒 testTimeout)——例如 update.test.tsserver-default-bridge-wiring.test.tsshell-ast-parser-lazy.test.tspackages/core/vitest.config.ts 中明确点名 web-tree-sitter 懒加载运行时属于负载敏感)、DaemonSessionProvider.test.tsx。每个失败文件都已在隔离状态下重跑并稳定通过(结果见下)。不存在断言层面的失败。

本轮变更文件

  • packages/core/src/tools/agent/agent.ts — 合并冲突解决(保留校验器抽离;并入 main 的全部语义)。
  • packages/core/src/agents/worktree-pin.ts — 按 main feat(cli): add native multi-agent coordination #8804 移除包含性门槛;文档更新;加固保留。
  • packages/core/src/agents/worktree-pin.test.ts — 按合并后语义重写(包含性测试替换为仓库外接受测试)。
  • packages/core/src/agents/runtime/workflow-orchestrator.ts — 注释措辞与合并后语义对齐。
  • docs/users/features/sub-agents.md — 一句话对齐。
  • .qwen/review-context.json — 收窄 skills 的 relatedPaths(上限哨兵修复)。
  • packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts — 锁定的 manifest 与哨兵同步更新。
  • 另含 origin/main 的合并内容(自动合并,无其他冲突)。

验证

实际执行的命令及结果(除注明外均为合并后树):

  • npm run build — 通过(合并前复现构建与合并后重新构建均为退出码 0)。
  • npm run typecheck — 通过(退出码 0,无 TS 错误)。
  • npm run lint — 通过(退出码 0)。
  • npm run check:lockfile — 通过。npm run audit:runtime:critical — 通过(0 个严重)。settings schema 重新生成 — 无变化/最新。
  • 对每个编辑过的文件执行 npx prettier --check — 格式化后通过。
  • 定向 vitest,packages/coreworktree-pin.test.tsgitWorktreeService.test.tsgitWorktreeService.linked.integ.test.ts、含 main 新 teammate/兄弟 worktree 测试的 agent.test.tsworkflow.test.tsworkflow-orchestrator.test.ts)— 492 通过 + 链接集成/固定重跑 23 通过。
  • 定向 vitest,packages/cli(在被污染的本地环境中失败的 10 个套件,干净 CI 环境)— 357/358 通过;剩余 1 个是 AuthDialog TUI 测试,按设计在 CI 上跳过(CI === 'true'it.skip)。
  • packages/channels/github/src/GithubAdapter.test.ts — 连续 3 次隔离运行均 190 通过(唯一一次观察到的失败是合并进行中的并行运行产物)。
  • npm run test:ci(全部工作区,CI 等效环境),合并后树上运行两次 — 第一次:cli 测试 19284/19285 通过,唯一失败 = manifest 上限哨兵(已修复,隔离重跑 5/5);第二次:仅剩上文列出的负载超时抖动,且各自隔离运行均为绿色:update.test.ts + server-default-bridge-wiring.test.ts + workspace-registration-store.test.ts + script-lint-isolation.test.ts + resolve-voice-config.test.ts(合计 48 通过)、shell-ast-parser-lazy.test.ts(5 通过 ×2)、DaemonSessionProvider.test.tsx(266 通过 ×3)。两次运行中其余所有工作区全绿(core 582 个文件等)。
  • npm run test:scripts — 1036 通过;唯一的文件级错误是 install-script.test.js 的守卫:CI 主机缺少 zip 时会抛错——本 Runner 没有 zip,而 GitHub Runner 自带(Test 检查还有专门步骤安装它)。非代码失败。
  • npm run check:serve-fast-path-bundle(清理 + cli-only 重建 + bundle + 闭包检查)— 通过("Startup bundle closure checks passed")。
  • 未在本地运行:no-AK 集成门槛 — 所触行为(worktree 固定校验)由 core 单元测试 + 链接集成测试覆盖,不经由打包 CLI 或集成测试框架。

剩余风险/说明:合并前的原始 CI 失败在本地无法归因于确定性缺陷;若重新触发的 CI 上再次出现,大概率是 Runner 抖动,需要那一次运行的日志才能进一步定位。

Base-conflict check · 基分支冲突检查: conflicted with main — resolved in this push. · 与 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-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix round summary — PR #8972 (address-review): no code changes

This round's only feedback was the failed Test (ubuntu-latest, Node 22.x) check; there were no reviewer comments, inline findings, or base conflicts. The failure was traced to an exact CI step via GitHub's public check-run data: the job dies at "Install linters", a runner-tooling download step that runs before any code is checked, and which is currently failing fleet-wide on the self-hosted ECS runner lanes. There is no defect in this PR and no code change this PR could make that would affect that step — so this round makes no changes.

Why the failed check is not caused by this PR

Per-step conclusions for both failed runs of this PR (read from the public check-run API, no credentials):

CI run (commit) Runner Failed step Step duration
run 31632877146 (0c16d2d62f) ecs-qwen-runner-sg-2 19 — Install linters 0 s
run 31634006417 (29b866c77e) ecs-qwen-runner-hk-j6c03lyei7s809zq1s6t-5 19 — Install linters 1 s
  • Every step before it succeeded (checkout, head verification, CI-profile classify, npm ci, runtime audit, lockfile, desktop-isolation, voice-guard sync). ESLint and everything after it (prettier, i18n, schema checks, unit tests, the no-AK integration gate) were skipped — they never ran.
  • "Install linters" (node scripts/lint.js --setup) downloads actionlint/shellcheck release binaries from GitHub and installs yamllint from PyPI onto the runner. It exercises no repository code. The 0–1 s failure on two different regional runners (Singapore, HK) reads as an immediate environment refusal, not a timeout and not a test failure.
  • It is fleet-wide, not specific to this branch: 14 of the last 15 completed ci.yml PR runs failed, including the dependency-only fix/sharp-0.35-bump run (31633973484), which failed at the same step on a third ECS runner (...hk-...-17). The single green run (fix/autofix-hermetic-git-config, 31632369733) also ran on ECS (ecs-qwen-runner-64c-1), so a subset of regional ecs-qwen lanes is broken while others work.
  • scripts/lint.js was last modified 2026-08-05; this PR touches no CI tooling or workflow files.

Local verification — every locally reproducible part of the Test job passes on this branch

Commands actually run on the current head (29b866c77e), in a CI-equivalent environment (clean HOME, no leaked harness env vars):

  • npm run build — passed (exit 0).
  • npm run typecheck — passed (exit 0; the initial local failure was only the missing dist/ that CI builds first).
  • npm run lint (ESLint) — passed (exit 0). node scripts/lint.js --prettier — clean (note: this step writes, so it cannot fail CI). Sensitive-keyword step — exit 0 (it is currently a no-op in scripts/lint.js, same on main).
  • npm run check:lockfile — passed. npm run audit:runtime:critical — passed. npm run check:desktop-isolation — passed. npm run check:voice-guard-sync — passed.
  • npm run check-i18n — passed. npm run generate:settings-schema — regenerated schema unchanged (fresh). npm run generate:notices (vscode companion) — NOTICES unchanged (fresh).
  • .github/scripts helper tests (node --test, all 16 suites from the CI env list) — 255/255 passed.
  • Full vitest suite, packages/core — 583 files, 19921 passed, 0 failed.
  • Full vitest suite, packages/cli — 804 files, 0 failed.
  • Full vitest suites for the remaining test:ci workspaces — acp-bridge (26), audio-capture (1), chrome-extension (7 + 1 skipped), sdk-typescript (32), vscode-ide-companion (54), web-shell (183), webui (34): all passed.
  • npm run test:scripts — 1036 tests passed; the only file-level error is install-script.test.js's guard that throws when zip is missing on a CI host — this local machine has no zip, GitHub runners ship it (the Test job installs it in a dedicated step). Not a code failure.
  • npm run check:serve-fast-path-bundle (clean + cli-only rebuild + bundle + closure checks) — passed ("Startup bundle closure checks passed", exit 0).
  • Not reproducible here: the failing "Install linters" step itself (runner-side binary downloads; fails fleet-wide even for PRs this branch does not share code with), actionlint/shellcheck/yamllint runs (binaries not installable in this sandbox; this PR modifies no shell or workflow files), and the no-AK integration gate (step 36 — the failed job never reached step 20; round 4's unit + linked-integration coverage already exercises the touched behavior).

Disposition and suggested next steps

  • Failed check → investigated with per-step evidence; classified as an environment/runner-fleet issue, declined as unactionable from this PR (there is no code in this branch that the step touches).
  • Re-running CI is the workflow's lever; a maintainer may also want to inspect the affected ecs-qwen sg/hk lanes (instant failures downloading GitHub/PyPI release assets).
  • No files changed this round; no commit.
中文说明

Autofix 本轮总结 — PR #8972(address-review):无代码改动

本轮唯一的反馈是失败的 Test (ubuntu-latest, Node 22.x) 检查;没有评审意见、行内发现或基分支冲突。通过 GitHub 公开的 check-run 数据逐步骤定位,失败原因已经查明:该 job 死在 "Install linters" 这一步——这是一个在检查任何代码之前运行的 runner 工具下载步骤,目前正在自托管 ECS runner 通道上全舰队范围地失败。本 PR 不存在任何缺陷,也没有任何本 PR 可以做出的代码改动能影响该步骤——因此本轮不做任何改动。

为什么该失败检查与本 PR 无关

本 PR 两次失败运行的逐步骤结论(读取自公开的 check-run API,未使用任何凭据):

CI 运行(提交) Runner 失败步骤 步骤耗时
run 31632877146(0c16d2d62f ecs-qwen-runner-sg-2 19 — Install linters 0 秒
run 31634006417(29b866c77e ecs-qwen-runner-hk-j6c03lyei7s809zq1s6t-5 19 — Install linters 1 秒
  • 此前的每一步都已成功(checkout、head 校验、CI profile 分类、npm ci、运行时依赖审计、lockfile、desktop 隔离检查、voice-guard 同步检查)。ESLint 及其后的所有步骤(prettier、i18n、schema 检查、单元测试、no-AK 集成门槛)全部被跳过——根本没有执行
  • "Install linters"(node scripts/lint.js --setup)会从 GitHub 下载 actionlint/shellcheck 的发布二进制,并通过 PyPI 安装 yamllint 到 runner 上。它不执行任何仓库代码。在两个不同区域的 runner(新加坡、中国香港)上 0–1 秒内失败,看起来是环境层面的即时拒绝,而不是超时,也不是测试失败。
  • 这是全舰队范围的问题,并非本分支特有:最近 15 次已完成的 ci.yml PR 运行中有 14 次失败,其中包括仅改依赖的 fix/sharp-0.35-bump 运行(31633973484),它在第三台 ECS runner(...hk-...-17)上失败在同一步骤。唯一一次绿色运行(fix/autofix-hermetic-git-config,31632369733)同样跑在 ECS 上(ecs-qwen-runner-64c-1),说明是部分区域的 ecs-qwen 通道损坏,而其余通道正常。
  • scripts/lint.js 最近一次修改是 2026-08-05;本 PR 没有触碰任何 CI 工具或 workflow 文件。

本地验证 — Test 检查中所有可在本地复现的部分在本分支上全部通过

在当前 head(29b866c77e)上、CI 等效环境(干净的 HOME、无泄漏的宿主环境变量)下实际执行的命令:

  • npm run build — 通过(退出码 0)。
  • npm run typecheck — 通过(退出码 0;最初本地失败仅因缺少 CI 会先构建的 dist/)。
  • npm run lint(ESLint)— 通过(退出码 0)。node scripts/lint.js --prettier — 干净(注意:该步骤是写入模式,因此不会使 CI 失败)。sensitive-keyword 步骤 — 退出码 0(该步骤目前在 scripts/lint.js 中是空操作,main 上同样如此)。
  • npm run check:lockfile — 通过。npm run audit:runtime:critical — 通过。npm run check:desktop-isolation — 通过。npm run check:voice-guard-sync — 通过。
  • npm run check-i18n — 通过。npm run generate:settings-schema — 重新生成的 schema 无变化(新鲜)。npm run generate:notices(vscode companion)— NOTICES 无变化(新鲜)。
  • .github/scripts helper 测试(node --test,CI 环境清单中的全部 16 个套件)— 255/255 通过。
  • 完整 vitest 套件,packages/core — 583 个文件,19921 通过,0 失败。
  • 完整 vitest 套件,packages/cli — 804 个文件,0 失败。
  • 其余 test:ci 工作区的完整 vitest 套件 — acp-bridge(26)、audio-capture(1)、chrome-extension(7 + 1 跳过)、sdk-typescript(32)、vscode-ide-companion(54)、web-shell(183)、webui(34):全部通过。
  • npm run test:scripts — 1036 个测试通过;唯一的文件级错误是 install-script.test.js 的守卫:CI 主机缺少 zip 时会抛错——本机没有 zip,而 GitHub runner 自带(Test 检查还有专门步骤安装它)。非代码失败。
  • npm run check:serve-fast-path-bundle(清理 + cli-only 重建 + bundle + 闭包检查)— 通过("Startup bundle closure checks passed",退出码 0)。
  • 本地无法复现的部分:失败的 "Install linters" 步骤本身(runner 侧的二进制下载;即便与本分支没有共同代码的 PR 也在舰队范围内失败)、actionlint/shellcheck/yamllint 的实际运行(二进制无法在本沙箱中安装;本 PR 未修改任何 shell 或 workflow 文件)、以及 no-AK 集成门槛(第 36 步——失败的 job 连第 20 步都未到达;第 4 轮的单元 + 链接集成测试覆盖已经验证了本 PR 触动的行为)。

处理结论与建议的后续动作

  • 失败检查 → 已凭逐步骤证据排查完毕;定性为环境/runner 舰队问题,以"本 PR 无法采取行动"拒绝处理(该步骤不触碰本分支中的任何代码)。
  • 重新运行 CI 是工作流侧的手段;维护者也可以检查受影响的 ecs-qwen 新加坡/中国香港通道(下载 GitHub/PyPI 发布资源时即时失败)。
  • 本轮未改动任何文件;无提交。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


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

Not reviewed: reverse audit — reached the round cap (5) without two consecutive dry rounds; round 5 still surfaced findings..

Not reviewed: build-and-test — Test (macos-latest / windows-latest, Node 22.x) was skipped in CI (merge_group-only lanes) and the unit suite did not run locally on those OSes; the Windows gap is exactly what R4-5/R4-6 name..

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

Not explored to full depth (tool budget reached): You are review agent reverse-audit — Reverse audit agen...: none — all checks above completed within budget.; You are review agent reverse-audit — Reverse audit agen...: none — all checks above completed within budget.; You are review agent reverse-audit — Reverse audit agen...: none — all checks above completed within budget.; You are review agent reverse-audit — Reverse audit agen...: none — all checks above completed within budget.; You are review agent reverse-audit — Reverse audit agen...: none — all checks above completed within budget., and 1 more.

Test Plan (not a blocker): src/agents/worktree-pin.test.tsno such file or directory; src/agents/runtime/workflow-orchestrator.test.tsno such file or directory; src/agents/runtime/workflow-sandbox.test.tsno such file or directory; src/agents/runtime/workflow-journal.test.tsno such file or directory; src/tools/workflow/workflow.test.tsno such file or directory; and 1 more.

中文说明

未审查:reverse audit — reached the round cap (5) without two consecutive dry rounds; round 5 still surfaced findings.。

未审查:build-and-test — Test (macos-latest / windows-latest, Node 22.x) was skipped in CI (merge_group-only lanes) and the unit suite did not run locally on those OSes; the Windows gap is exactly what R4-5/R4-6 name.。

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

未探索到全部深度(达到工具调用预算):You are review agent reverse-audit — Reverse audit agen...:none — all checks above completed within budget.;You are review agent reverse-audit — Reverse audit agen...:none — all checks above completed within budget.;You are review agent reverse-audit — Reverse audit agen...:none — all checks above completed within budget.;You are review agent reverse-audit — Reverse audit agen...:none — all checks above completed within budget.;You are review agent reverse-audit — Reverse audit agen...:none — all checks above completed within budget.,另有 1 条。

Test Plan(非阻断):src/agents/worktree-pin.test.tsno such file or directory; src/agents/runtime/workflow-orchestrator.test.tsno such file or directory; src/agents/runtime/workflow-sandbox.test.tsno such file or directory; src/agents/runtime/workflow-journal.test.tsno such file or directory; src/tools/workflow/workflow.test.tsno such file or directory; and 1 more。

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

Comment on lines +118 to +120
'cannot serve. Mutually exclusive with `isolation`. The path must live ' +
'inside the repository and be a linked worktree registered via ' +
'`git worktree add` — the main checkout is not eligible. ' +

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.

[Suggestion] R4-1: The model-facing workingDir description states the path "must live inside the repository", but the resolver this PR ships deliberately does not enforce containment — the sole gate is worktree-registry membership (isRegisteredLinkedWorktree), and worktree-pin.ts documents "A registered worktree may live anywhere on disk — the registry entry naming this repository is the boundary, not directory containment." This PR's own worktree-pin.test.ts accepts /elsewhere/wt, and its sub-agents.md hunk removed this same wording from the user doc.

Failure scenario: a model authoring a workflow script reads this schema and refuses to pin a registered worktree located outside the repo directory — a configuration this PR explicitly supports and tests ("leader-owned teammate worktrees rely on that") — or wastes steps relocating the worktree inside the repo. The code behaves as intended; only the description contradicts it.

Suggested change
'cannot serve. Mutually exclusive with `isolation`. The path must live ' +
'inside the repository and be a linked worktree registered via ' +
'`git worktree add` — the main checkout is not eligible. ' +
'cannot serve. Mutually exclusive with `isolation`. The path must be a ' +
'linked worktree of this repository registered via `git worktree add` ' +
'(it may live anywhere on disk) — the main checkout is not eligible. ' +
中文说明

面向模型的 workingDir 描述声称路径「必须位于仓库内部」,但本 PR 引入的解析器刻意不强制目录包含——唯一关卡是 worktree 登记册成员资格(isRegisteredLinkedWorktree),且 worktree-pin.ts 明确写着「已登记的 worktree 可以位于磁盘任何位置——以登记条目指认本仓库为边界,而非目录包含」。本 PR 自己的 worktree-pin.test.ts 接受 /elsewhere/wtsub-agents.md 的改动也从用户文档中删去了同样的表述。

失败场景:模型读到这段 schema 后会拒绝钉住位于仓库目录之外的已登记 worktree(本 PR 明确支持并测试了这种配置——「leader 拥有的 teammate worktree 依赖这一点」),或者浪费步骤把 worktree 搬进仓库。代码行为符合预期,只有描述与之矛盾。

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

'`stallMs` (number, ms): a no-progress watchdog, not a wall-clock cap. ' +
'The dispatch is aborted and retried (up to ' +
`${MAX_STALL_ATTEMPTS} attempts total) after this many milliseconds ` +
'with no observable subagent progress; the timer is suspended while ' +

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.

[Suggestion] R4-4: This sentence claims the dispatch is aborted after stallMs "with no observable subagent progress", but the watchdog only arms on the FIRST progress event — workflow-stall.ts intentionally does not count time-to-first-response as a stall ("Intentionally NOT armed here. The first onActivity … arms it"), and nothing else wraps the dispatch. Verified by running the committed workflow-stall.test.ts (24/24 pass; test #8 advances fake timers 10 s with zero events and asserts no abort).

Failure scenario: a workflow author sets agent({stallMs: 10000}) to bound hangs; a provider/connection hang before the first token emits zero events, the watchdog never arms, and the dispatch runs to the subagent's own max_time_minutes cap — holding a concurrency slot and budget for ~10 minutes instead of the ~10 seconds this wording implies.

Suggested change
'with no observable subagent progress; the timer is suspended while ' +
'with no observable subagent progress once progress has begun (a dispatch that produces no first response is bounded by the subagent time cap, not this watchdog); the timer is suspended while ' +
中文说明

这句话声称派发会在 stallMs 毫秒「没有可观察的子代理进展」后被中止,但看门狗只在第一个进展事件时才启动——workflow-stall.ts 刻意不把首响应前的时间计为停滞(「这里故意不启动。第一个 onActivity……才启动」),也没有其他机制包裹派发。已通过运行本仓库的 workflow-stall.test.ts 验证(24/24 通过;测试 #8 在零事件下推进假定时器 10 秒并断言不中止)。

失败场景:workflow 作者设置 agent({stallMs: 10000}) 来限制挂起;首个 token 之前的 provider/连接挂起不会产生任何事件,看门狗永不启动,派发会一直跑到子代理自身的 max_time_minutes 上限——占用一个并发槽位与预算约 10 分钟,而不是这段表述所暗示的约 10 秒。

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

Comment on lines +118 to +122
`${label} "${resolvedPath}" is not a registered linked worktree of ` +
`this repository (it is the main working tree, is absent from \`git ` +
`worktree list\`, or its git metadata could not be read) — pinning an ` +
`agent there would not isolate it. Pass a worktree created via ` +
`\`git worktree add\`.`,

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.

[Suggestion] R4-2: The PR's Risk & Scope claims "moving the resolver into a shared module changes no behaviour for the Agent tool, whose error text is byte-identical because the default parameter name is working_dir" — but this refusal text changed "pinning a sub-agent there would not isolate it" (the deleted agent.ts copy) to "pinning an agent there", so the Agent tool's user-visible error text is not byte-identical. Nothing asserts the old wording, so nothing breaks today.

Failure scenario: a maintainer verifying the "no behaviour change" claim takes the description at its word and skips reading worktree-pin.ts — the file this PR itself calls load-bearing; the same sentence also papers over two deliberate behaviour changes (realpath-canonicalized gate input and returned path, and the anchor move from --show-toplevel to getMainWorktreePath()).

Suggested change
`${label} "${resolvedPath}" is not a registered linked worktree of ` +
`this repository (it is the main working tree, is absent from \`git ` +
`worktree list\`, or its git metadata could not be read) — pinning an ` +
`agent there would not isolate it. Pass a worktree created via ` +
`\`git worktree add\`.`,
`this repository (it is the main working tree, is absent from \`git ` +
`worktree list\`, or its git metadata could not be read) — pinning a ` +
`sub-agent there would not isolate it. Pass a worktree created via ` +

(Alternative: keep the new wording and correct the Risk & Scope section instead.)

中文说明

PR 的「风险与范围」声称「把校验器移入共享模块对 Agent 工具的行为没有任何改变,其错误文本逐字节一致,因为默认参数名就是 working_dir」——但这条拒绝文案从 "pinning a sub-agent there would not isolate it"(被删除的 agent.ts 副本)变成了 "pinning an agent there",因此 Agent 工具用户可见的错误文本并非逐字节一致。没有任何代码断言旧文案,所以今天不会有东西坏掉。

失败场景:维护者验证「无行为变化」这一声明时,会照单全收描述里的说法而跳过阅读 worktree-pin.ts——本 PR 自己称之为承重文件;同一句话还掩盖了两处有意的行为变化(门关卡输入与返回路径改为 realpath 规范化,以及锚点从 --show-toplevel 移到 getMainWorktreePath())。

(替代方案:保留新文案,改为修正 PR 描述中的「风险与范围」部分。)

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

Comment on lines +277 to +279
const fromWorktree = new GitWorktreeService(wt);
expect(await fromWorktree.getMainWorktreePath()).toBe(repo);
expect(await fromWorktree.getRepoTopLevel()).toBe(wt);

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] R4-5 (1/5) — new tests compare platform-dependent path forms strictly and fail on the Test (windows-latest, Node 22.x) merge-queue lane. This assertion compares git-emitted paths against Node-computed paths with strict .toBe(): getMainWorktreePath() returns the porcelain line verbatim and getRepoTopLevel() strips only the line terminator — Git for Windows emits forward slashes (C:/...) — while repo / wt come from fs.realpathSync(fs.mkdtempSync(...)) / path.join(...) (backslashes).

Failure scenario: expect('C:/...').toBe('C:\\...') fails; the lane runs packages/core vitest unfiltered (no *.integ.test.ts exclusion in vitest.config.ts, no win32 skip in test-setup.ts), turning the merge-queue gate red for this PR and every queued PR after it. The three newline-path siblings in this same describe ARE guarded it.skipIf(process.platform === 'win32'); this one is not. POSIX arm probed on Linux: 13/13 pass, confirming the failure is separator-specific.

Suggested change
const fromWorktree = new GitWorktreeService(wt);
expect(await fromWorktree.getMainWorktreePath()).toBe(repo);
expect(await fromWorktree.getRepoTopLevel()).toBe(wt);
const fromWorktree = new GitWorktreeService(wt);
expect(path.normalize((await fromWorktree.getMainWorktreePath()) ?? '')).toBe(path.normalize(repo));
expect(path.normalize((await fromWorktree.getRepoTopLevel()) ?? '')).toBe(path.normalize(wt));

(Alternative: it.skipIf(process.platform === 'win32') like the siblings, if the intent is POSIX-only coverage.)

中文说明

R4-5(5 处之 1)——新增测试以严格相等比较平台相关的路径形式,会在 Test (windows-latest, Node 22.x) 合并队列流水线上失败。此处断言用严格 .toBe() 比较 git 输出的路径与 Node 计算的路径:getMainWorktreePath() 原样返回 porcelain 行,getRepoTopLevel() 只去掉行终止符——Windows 上的 Git 输出正斜杠(C:/...)——而 repo / wt 来自 fs.realpathSync(fs.mkdtempSync(...)) / path.join(...)(反斜杠)。

失败场景:expect('C:/...').toBe('C:\\...') 失败;该流水线无过滤地运行 packages/core 的 vitest(vitest.config.ts 没有排除 *.integ.test.tstest-setup.ts 没有 win32 跳过),使合并队列门禁在本 PR 及之后每个排队的 PR 上变红。同一 describe 中的三个换行路径兄弟测试都有 it.skipIf(process.platform === 'win32') 保护,唯独这个没有。已在 Linux 上探针验证 POSIX 侧:13/13 通过,确认失败仅由分隔符差异引起。

(替代方案:若只打算覆盖 POSIX,可像兄弟测试一样加 it.skipIf(process.platform === 'win32')。)

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

Comment on lines +56 to +58
expect(result).toEqual({
path: '/repo/.qwen/tmp/review-pr-7',
branch: 'pr-7',

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] R4-5 (2/5) — same pattern as the gitWorktreeService.linked.integ.test.ts comment: strict POSIX path literals compared against path.resolve() output fail on the Windows lane. resolveExternalWorktreeDir computes path.resolve(parentCwd, workingDir) with the platform path module — on win32 that yields \repo\.qwen\tmp\review-pr-7 (probe-verified via path.win32.resolve), never the literal '/repo/.qwen/tmp/review-pr-7' asserted here. A win32-mocked vitest run of the real resolver fails the PR's assertion shape and passes the platform-arithmetic fix; baseline is 10/10 on Linux.

Failure scenario: the merge-queue Test (windows-latest, Node 22.x) lane runs this file unfiltered — four tests go red and the required-status-check blocks the queue.

Suggested change
expect(result).toEqual({
path: '/repo/.qwen/tmp/review-pr-7',
branch: 'pr-7',
expect(result).toEqual({
path: path.resolve('/repo', '.qwen/tmp/review-pr-7'),
branch: 'pr-7',
中文说明

R4-5(5 处之 2)——与 gitWorktreeService.linked.integ.test.ts 上的评论同一模式:严格比较 POSIX 路径字面量与 path.resolve() 输出,在 Windows 流水线上失败。resolveExternalWorktreeDir 用平台 path 模块计算 path.resolve(parentCwd, workingDir)——在 win32 上得到 \repo\.qwen\tmp\review-pr-7(已用 path.win32.resolve 探针验证),永远不会是这里断言的字面量 '/repo/.qwen/tmp/review-pr-7'。用 win32 模拟的 vitest 运行真实解析器时,PR 的断言形式失败、平台算术修复后通过;Linux 基线 10/10。

失败场景:合并队列的 Test (windows-latest, Node 22.x) 流水线无过滤运行本文件——四个测试变红,必需状态检查阻塞队列。

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

Comment on lines +77 to +79
expect(result).toMatchObject({
path: '/repo/.qwen/tmp/review-pr-1-base',
});

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] R4-5 (3/5) — same pattern as (2/5): the literal '/repo/.qwen/tmp/review-pr-1-base' is compared against the platform path.resolve() result (backslash form on win32). Probe-verified: path.win32.resolve('/repo', '../review-pr-1-base') never equals the POSIX literal, so this toMatchObject fails on the Test (windows-latest, Node 22.x) merge-queue lane.

Suggested change
expect(result).toMatchObject({
path: '/repo/.qwen/tmp/review-pr-1-base',
});
expect(result).toMatchObject({
path: path.resolve('/repo/.qwen/tmp/review-pr-1', '../review-pr-1-base'),
});
中文说明

R4-5(5 处之 3)——与(5 处之 2)同一模式:字面量 '/repo/.qwen/tmp/review-pr-1-base' 与平台 path.resolve() 结果比较(win32 上为反斜杠形式)。已探针验证:path.win32.resolve('/repo', '../review-pr-1-base') 永远不等于该 POSIX 字面量,因此这个 toMatchObject 会在 Test (windows-latest, Node 22.x) 合并队列流水线上失败。

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

Comment on lines +86 to +88
const result = await resolveExternalWorktreeDir(config, '/elsewhere/wt');
expect(result).toEqual({
path: '/elsewhere/wt',

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] R4-5 (4/5) — same pattern as (2/5): the literal '/elsewhere/wt' is asserted against the platform-resolved path. On win32 path.resolve('/repo', '/elsewhere/wt') yields \elsewhere\wt, so this toEqual fails on the Test (windows-latest, Node 22.x) merge-queue lane.

Suggested change
const result = await resolveExternalWorktreeDir(config, '/elsewhere/wt');
expect(result).toEqual({
path: '/elsewhere/wt',
const result = await resolveExternalWorktreeDir(config, '/elsewhere/wt');
expect(result).toEqual({
path: path.resolve('/elsewhere/wt'),
中文说明

R4-5(5 处之 4)——与(5 处之 2)同一模式:字面量 '/elsewhere/wt' 与平台解析后的路径断言比较。在 win32 上 path.resolve('/repo', '/elsewhere/wt') 得到 \elsewhere\wt,因此这个 toEqual 会在 Test (windows-latest, Node 22.x) 合并队列流水线上失败。

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

null as unknown as { branch: string },
);
const result = await resolveExternalWorktreeDir(config, 'wt');
expect(result).toMatchObject({ path: '/repo/wt', branch: '' });

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] R4-5 (5/5) — same pattern as (2/5): the literal '/repo/wt' is asserted against the platform-resolved path (backslash form on win32), failing on the Test (windows-latest, Node 22.x) merge-queue lane.

Suggested change
expect(result).toMatchObject({ path: '/repo/wt', branch: '' });
expect(result).toMatchObject({ path: path.resolve('/repo', 'wt'), branch: '' });
中文说明

R4-5(5 处之 5)——与(5 处之 2)同一模式:字面量 '/repo/wt' 与平台解析后的路径断言比较(win32 上为反斜杠形式),会在 Test (windows-latest, Node 22.x) 合并队列流水线上失败。

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

Comment on lines +1405 to +1406
if (agentOpts.workingDir !== undefined) {
if (typeof agentOpts.workingDir !== 'string' || agentOpts.workingDir.trim().length === 0) {

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.

[Suggestion] R4-8: The schema this PR adds advertises stallMs with "0 disables the watchdog", but unlike its sibling new option workingDir (strictly type-checked in this gate) nothing validates stallMs's type — workflow-orchestrator.ts does typeof opts.stallMs === 'number' ? opts.stallMs : undefined, so a non-number is silently dropped and the default 60 s watchdog applies. Probe-verified against the real sandbox + resolver: agent({stallMs: '0'}) reaches dispatch as the string and resolves to 60000; numeric 0 resolves to 0; the gate below flips the string case to a loud refusal.

Failure scenario: a model-authored script calls agent('long quiet task', { stallMs: '0' }) — or forwards caller input whose type it does not control — the intended disable is silently discarded, and the dispatch runs under the 60 s watchdog: aborted and retried up to 3 times (wasted tokens, possible total failure of a dispatch the author meant to leave unwatched).

Suggested change
if (agentOpts.workingDir !== undefined) {
if (typeof agentOpts.workingDir !== 'string' || agentOpts.workingDir.trim().length === 0) {
if (agentOpts.stallMs !== undefined && (typeof agentOpts.stallMs !== 'number' || !Number.isFinite(agentOpts.stallMs))) {
throw new Error("agent({stallMs}): must be a finite number of milliseconds (0 disables the watchdog).");
}
if (agentOpts.workingDir !== undefined) {
if (typeof agentOpts.workingDir !== 'string' || agentOpts.workingDir.trim().length === 0) {
中文说明

R4-8:本 PR 新增的 schema 宣称 stallMs0 关闭看门狗」,但与同为新选项的 workingDir(在此关卡被严格类型检查)不同,stallMs 的类型没有任何校验——workflow-orchestrator.ts 只做 typeof opts.stallMs === 'number' ? opts.stallMs : undefined,非数字会被静默丢弃并回落到默认 60 秒看门狗。已用真实 sandbox + 解析器探针验证:agent({stallMs: '0'}) 以字符串到达派发层并解析为 60000;数字 0 解析为 0;下面的关卡会把字符串情况翻转为显式拒绝。

失败场景:模型写的脚本调用 agent('long quiet task', { stallMs: '0'})——或转发其无法控制类型的调用方输入——预期的关闭被静默丢弃,派发在 60 秒看门狗下运行:被中止并最多重试 3 次(浪费 token,作者本想放任不管的派发甚至可能彻底失败)。

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

…review (QwenLM#8972)

- Reject non-numeric agent({stallMs}) loudly in the sandbox gate instead of
  silently dropping it to the default 60s watchdog, which contradicted the
  advertised "0 disables the watchdog".
- Compare worktree paths via path.resolve/path.normalize in the new tests so
  the windows-latest unit lane stops failing on separator differences.
- Correct the model-facing workingDir/stallMs descriptions to match the
  registry-only gate and the first-progress-event arming semantics.
- Restore the Agent tool's historical "a sub-agent" refusal wording.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Round-4 feedback addressed

All nine inline findings are resolved in code (one commit, fix(workflows): harden stallMs gate and test portability per round-4 review).

Critical — platform-dependent path assertions (R4-5 1/5–5/5)

Accepted. Verified the mechanism first: packages/core/vitest.config.ts has no *.integ.test.ts exclusion, so both files run unfiltered on the Test (windows-latest, Node 22.x) merge-queue lane; the resolver computes paths with the platform path module while the assertions used POSIX literals, and the integ test compared git's forward-slash porcelain output against Node's backslash fs paths with strict .toBe(). Probed path.win32.resolve/normalize/basename semantics on Linux to confirm each fix, then:

  • worktree-pin.test.ts: the four flagged assertions now expect path.resolve(...) expressions mirroring the resolver's own computation (platform-correct by construction); slug/repoRoot/branch fields were checked to be platform-independent and left untouched.
  • gitWorktreeService.linked.integ.test.ts: the getMainWorktreePath() assertions compare path.normalize(...) on both sides (converts git's forward slashes to the platform separator), keeping Windows coverage rather than skipping, with a comment explaining why.

Suggestion — workingDir description contradicts the gate (R4-1)

Accepted. The gate is registry membership only (isRegisteredLinkedWorktree), and worktree-pin.ts documents that a registered worktree may live anywhere on disk — the old "must live inside the repository" wording could make a model refuse or relocate a supported configuration. Rewrote the schema description to name the real gate and add "(it may live anywhere on disk)". Checked agent.ts's working_dir description too — it already says only "registered linked worktree of this repository", so no change needed there.

Suggestion — stallMs description overstates the watchdog (R4-4)

Accepted. Verified against workflow-stall.ts: the watchdog arms on the FIRST progress event ("Intentionally NOT armed here…"), so a dispatch that never produces a first response is bounded by the subagent's max_time_minutes, not by stallMs. The description now says "once progress has begun (a dispatch that produces no first response is bounded by the subagent time cap, not this watchdog)".

Suggestion — refusal wording broke the "byte-identical" claim (R4-2)

Accepted option (a): restored the deleted agent.ts copy's "pinning a sub-agent there would not isolate it" wording in the shared resolver. Both call sites (Agent tool, workflow agent()) dispatch subagents, so the wording is accurate for both and makes the Agent tool's user-visible error text identical to before the move. One disclosure note for the PR description's Risk & Scope section (which only its author can edit): the same sentence should also mention the two deliberate behaviour changes the finding names — realpath-canonicalized gate input/returned path, and the anchor move from --show-toplevel to getMainWorktreePath().

Suggestion — silent drop of non-numeric stallMs (R4-8)

Accepted. Verified: the orchestrator does typeof opts.stallMs === 'number' ? opts.stallMs : undefined, so agent({stallMs: '0'}) was silently discarded and ran under the default 60s watchdog — the opposite of the advertised "0 disables". Added a gate in the sandbox init script (next to the workingDir gate) that throws agent({stallMs}): must be a finite number of milliseconds (0 disables the watchdog). for non-number / non-finite values. Added regression tests: string/NaN/boolean are refused; stallMs: 0 still passes through to dispatch intact.

Review-level items

  • "Test (macos-latest / windows-latest) skipped in CI" — these lanes are merge_group-only; the Windows gap they name is exactly the assertion set fixed above. Windows itself cannot run on this Linux runner; verification is the Linux runs plus the path.win32 probes.
  • "Integration Tests (CLI, No Sandbox) skipped" — the no-AK integration gate that lives inside the Test lane was run locally and passes (140/140); the merge_group-only integration lane runs at queue time.
  • "Test Plan (not a blocker)" — the test-plan paths in the PR body are missing their packages/core/ prefix; the files exist (e.g. packages/core/src/agents/worktree-pin.test.ts). PR-body text is outside this round's write scope, noted here for the maintainer.
  • Still-red Test (ubuntu-latest Node 22.x): reproduced the lane locally step by step. Every runnable step passes on this branch (see Verification). The full unit suite, run with a scrubbed environment and CI=true, still shows two failure classes that are artifacts of this sandbox container, not of the branch: (1) EACCES writing to the root-owned /home/github-runner home (this session runs as uid 1000; the CI job owns its home) — gemini.test.tsx, agent-headless, config, workflow-snapshot, ChannelBase, directoryCommand, cdCommand; (2) the main-branch enter/exit-worktree session sidecar integ tests contend under the 16-thread full-suite pool here but pass standalone and are untouched by this PR. AuthDialog's TUI-input tests are skipped under CI=true and never run in the lane.

Verification

Commands actually run and their results:

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint (ESLint, both invocations) — passed
  • node scripts/lint.js --eslint — passed; node scripts/lint.js --prettier — passed (and the six touched files are Prettier-stable)
  • npm run check-i18n — passed; npm run check:lockfile — passed
  • npm run generate:settings-schema — regenerated, no diff (schema up to date)
  • npm run generate:notices --workspace=qwen-code-vscode-ide-companion — regenerated, no diff
  • npm run check:desktop-isolation / check:voice-guard-sync / check:serve-fast-path-bundle — passed
  • node --test on the 16 CI helper-test files — 255 passed, 0 failed
  • npm -w packages/chrome-extension run package + scan:artifacts — passed
  • Focused vitest, packages/core (worktree-pin, gitWorktreeService + linked integ, workflow-sandbox, workflow, workflow-orchestrator, workflow-journal) — 7 files, 415 passed, 0 failed (includes the two new stallMs gate tests)
  • npm run test:integration:no-ak:sandbox:none (CI-equivalent scrubbed env, fresh HOME) — 12 files, 140 tests passed
  • npm run test:ci (full unit suite, env-scrubbed, CI=true) — 19,300+ tests passed; remaining failures are the two sandbox-artifact classes documented above, none in code touched by this PR
  • Windows path arithmetic probed via node:path.win32 (resolve, normalize, basename) for every changed assertion
  • actionlint / shellcheck / yamllint binaries are not installed in this sandbox; this round's diff contains no workflow YAML, shell, or action files (six .ts files under packages/core/ only)
中文说明

第 4 轮反馈处理情况

全部九条 inline 发现均已在代码中解决(单次提交:fix(workflows): harden stallMs gate and test portability per round-4 review)。

Critical —— 平台相关的路径断言(R4-5 之 1/5 至 5/5)

接受。先验证了机制:packages/core/vitest.config.ts 没有排除 *.integ.test.ts,因此这两个文件会在 Test (windows-latest, Node 22.x) 合并队列流水线上无过滤运行;解析器用平台 path 模块计算路径,而断言使用 POSIX 字面量,integ 测试还用严格 .toBe() 比较 git 输出的正斜杠路径与 Node fs 的反斜杠路径。已在 Linux 上用 path.win32.resolve/normalize/basename 探针确认每个修复的算术,然后:

  • worktree-pin.test.ts:四处被标记的断言改为期望 path.resolve(...) 表达式,与解析器自身的计算一一对应(按构造即平台正确);slug/repoRoot/branch 字段经核对与平台无关,保持不变。
  • gitWorktreeService.linked.integ.test.tsgetMainWorktreePath() 的断言两侧都用 path.normalize(...) 比较(把 git 的正斜杠转换为平台分隔符),保留 Windows 覆盖而非跳过,并加注释说明原因。

Suggestion —— workingDir 描述与门关卡矛盾(R4-1)

接受。门关卡只检查登记册成员资格(isRegisteredLinkedWorktree),且 worktree-pin.ts 明确写着已登记的 worktree 可以位于磁盘任何位置——旧的「必须位于仓库内部」表述会让模型拒绝或迁移一个本 PR 明确支持的配置。已重写 schema 描述,指明真正的关卡并补充「(it may live anywhere on disk)」。也检查了 agent.tsworking_dir 描述——它本来就只写「registered linked worktree of this repository」,无需改动。

Suggestion —— stallMs 描述夸大了看门狗(R4-4)

接受。已对照 workflow-stall.ts 验证:看门狗在第一个进展事件时才启动(「这里故意不启动……」),因此一个始终没有首响应的派发受子代理自身 max_time_minutes 约束,而不是 stallMs。描述现在写明「once progress has begun (a dispatch that produces no first response is bounded by the subagent time cap, not this watchdog)」。

Suggestion —— 拒绝文案破坏了「逐字节一致」的声明(R4-2)

采纳方案 (a):在共享解析器中恢复被删除的 agent.ts 副本的措辞 "pinning a sub-agent there would not isolate it"。两个调用方(Agent 工具、workflow 的 agent())派发的都是子代理,该措辞对两者都准确,并使 Agent 工具用户可见的错误文本与迁移前一致。给 PR 描述的「风险与范围」部分补一条披露说明(该部分只能由其作者编辑):同一句话还应提及该发现指出的两处有意行为变化——门关卡输入与返回路径改为 realpath 规范化,以及锚点从 --show-toplevel 移到 getMainWorktreePath()

Suggestion —— 非数字 stallMs 被静默丢弃(R4-8)

接受。已验证:orchestrator 只做 typeof opts.stallMs === 'number' ? opts.stallMs : undefined,因此 agent({stallMs: '0'}) 会被静默丢弃并按默认 60 秒看门狗运行——与宣称的「0 关闭」恰好相反。已在 sandbox 初始化脚本中(紧邻 workingDir 关卡)新增关卡:对非数字/非有限值抛出 agent({stallMs}): must be a finite number of milliseconds (0 disables the watchdog).。新增回归测试:字符串/NaN/布尔值被拒绝;stallMs: 0 仍原样透传到派发层。

评审级别条目

  • 「Test (macos-latest / windows-latest) 在 CI 中被跳过」——这些流水线仅在 merge_group 触发;它们指名的 Windows 缺口正是上面修复的断言集。Windows 无法在本 Linux runner 上运行;验证依据是 Linux 运行结果加 path.win32 探针。
  • 「Integration Tests (CLI, No Sandbox) 被跳过」——Test 流水线内的 no-AK 集成关卡已在本地运行并通过(140/140);仅 merge_group 触发的集成流水线会在合并队列时运行。
  • 「Test Plan(非阻断)」——PR 正文中测试计划的路径缺少 packages/core/ 前缀;文件实际存在(如 packages/core/src/agents/worktree-pin.test.ts)。PR 正文文本不在本轮的写入范围内,在此记录以供维护者处理。
  • 仍然变红的 Test (ubuntu-latest Node 22.x):已在本地逐步复现该流水线。本分支上所有可运行的步骤都通过(见「验证」)。完整单元测试套件在清洗环境变量并以 CI=true 运行后,仍有两类失败,属于本沙箱容器的环境产物,而非分支代码问题:(1) 向 root 所有的 /home/github-runner 家目录写入时 EACCES(本会话以 uid 1000 运行,而 CI 作业拥有自己的家目录)——涉及 gemini.test.tsx、agent-headless、config、workflow-snapshot、ChannelBase、directoryCommand、cdCommand;(2) main 分支自带的 enter/exit-worktree 会话 sidecar integ 测试在此处 16 线程全套并发下发生争用,但单独运行均通过,且本 PR 未触碰它们。AuthDialog 的 TUI 输入测试在 CI=true 下被跳过,从不在该流水线中运行。

验证

实际运行的命令及结果:

  • npm run build —— 通过
  • npm run typecheck —— 通过
  • npm run lint(ESLint,两种调用方式)—— 通过
  • node scripts/lint.js --eslint —— 通过;node scripts/lint.js --prettier —— 通过(且六个改动文件为 Prettier 稳定格式)
  • npm run check-i18n —— 通过;npm run check:lockfile —— 通过
  • npm run generate:settings-schema —— 重新生成后无差异(schema 为最新)
  • npm run generate:notices --workspace=qwen-code-vscode-ide-companion —— 重新生成后无差异
  • npm run check:desktop-isolation / check:voice-guard-sync / check:serve-fast-path-bundle —— 通过
  • 对 16 个 CI 辅助测试文件运行 node --test —— 255 通过,0 失败
  • npm -w packages/chrome-extension run package + scan:artifacts —— 通过
  • packages/core 定向 vitest(worktree-pin、gitWorktreeService + linked integ、workflow-sandbox、workflow、workflow-orchestrator、workflow-journal)—— 7 个文件、415 通过、0 失败(含两个新增的 stallMs 关卡测试)
  • npm run test:integration:no-ak:sandbox:none(CI 等效的清洗环境、全新 HOME)—— 12 个文件、140 个测试通过
  • npm run test:ci(完整单元测试套件,清洗环境变量,CI=true)—— 19300+ 个测试通过;其余失败为上文记录的两类沙箱环境产物,均不在本 PR 改动的代码中
  • node:path.win32resolvenormalizebasename)对每个改动的断言做了 Windows 路径算术探针
  • actionlint / shellcheck / yamllint 二进制未安装在本沙箱中;本轮 diff 不包含任何 workflow YAML、shell 或 action 文件(仅 packages/core/ 下六个 .ts 文件)

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

…M#8972)

The failing CI annotation named keeps-the-attachment-live (load variant):
the live agent_message_chunk was asserted after a single-macrotask flush,
but under runner contention the batched setTimeout(0) dispatch can land one
tick after that window, so the chunk reads as missing even though it is
delivered. Replace the fixed-depth read with a bound-wait (vi.waitFor) for
the exact same blocks, preserving the assertion. Verified with 10 repeated
full-file runs under load (all failing before the fix, all passing after).
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

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

Autofix round summary — PR #8972 (address-review)

Feedback points and dispositions

This round's only actionable feedback was the failed check Test (ubuntu-latest, Node 22.x) (CI run 31649613702 on head cdcb10ac43). There were no reviewer comments, inline findings, or base conflicts (--conflict false; no merge performed). Critical-only mode is active; the deferred non-Critical section contains no items, so nothing was touched there.

Failed check → root-caused from per-step evidence and fixed with a minimal deflake. Disposition: Addressed.

Diagnosis (evidence, not guesswork)

  • Public check-run API per-step conclusions: steps 1–34 all succeeded (install, lockfile, ESLint, actionlint, shellcheck, yamllint, Prettier, i18n, settings-schema freshness, notices, bundle-closure, helper tests); step 35 Run tests and generate reports failed; the no-AK integration gate never ran. So the failure is a unit-test failure, not a lint/schema/freshness guard.
  • Check-run annotations name the exact test: packages/webui/src/daemon/session/DaemonSessionProvider.test.tsxkeeps the current attachment live while a same-session load fails, with expected [ 'A transcript' ] to deeply equal [ 'A transcript', ' still live' ].
  • This PR does not touch packages/webui at all (git diff origin/main HEAD -- packages/webui is empty; the branch fully contains current main). The failing test was merged into main today by 9ff1519ac9 fix(webui): Close same-session refresh race gaps (#8990) — so this is a freshly-landed, contention-sensitive test on main that now red-lines branches up to date with main (another up-to-date branch's run in the same window failed at the same step).
  • Reproduced locally in a CI-equivalent env (isolated HOME, CI=true): the test failed intermittently in the full-file run, always on the transcript assertion.
  • Ruled out a product race with temporary instrumentation (reverted before committing): a bound-poll showed the live chunk is delivered, just one tick after the single-macrotask flush window the test drains (flushTranscriptDispatch awaits exactly one setTimeout(0)); the provider's batched-dispatch timer can fire right after that window under runner contention. Nothing was dropped.

The fix (deflake policy, allowed fix #2 "stabilize timing/waiting")

Replaced the fixed-depth read with a bound-wait for the exact same condition: the bare expect(...).toEqual(['A transcript', ' still live']) is now inside await vi.waitFor(() => { ... }) (the file's own idiom, used ~20 times). The assertion, the inputs, and the act/flush sequence are preserved byte-for-byte; nothing is skipped, loosened, retried, or wrapped in try/catch. One file, +9/−3 lines, including a 4-line comment stating the non-obvious timing constraint.

Note: two sibling assertions in the same file use the same fixed-depth pattern but have not been observed flaking; left untouched per the minimal-scope rule.

Verification

Commands actually run on this branch (CI-equivalent env: isolated HOME, CI=true, empty API keys where CI does):

  • npm run build — passed (exit 0).
  • npm run typecheck — passed (exit 0).
  • npm run lint — passed (exit 0).
  • npx eslint packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx — passed; npx prettier --check on the same file — passed.
  • Focused Vitest, the named test's file, repeated under load: npx vitest run src/daemon/session/DaemonSessionProvider.test.tsx (packages/webui) × 10 consecutive full-file runs — 10/10 passed (266 tests each). Same loop before the fix: 7/7 failed on the named assertion; the CI run failed it too.
  • Full webui package suite: npx vitest run (packages/webui) — 34 files, 555 tests, all passed.
  • Ruled-out candidates earlier this round (all green, showing the failed step's surface is otherwise healthy): full packages/core suite — 583 files, 19923 passed; full packages/cli suite — 804 files, 0 failures under CI=true (the one local-only failure, an AuthDialog TUI-input test, is it.skip when CI=true and byte-identical to main); no-AK integration gate (all 12 files) — 140/140 passed; npm run generate:settings-schema + freshness check — clean; npm run check:lockfile — passed; npm run check-i18n — passed.

Files changed this round

  • packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx (+9/−3) — bound-wait replacing the fixed-depth transcript read in keeps the current attachment live while a same-session %s fails.

Commit: 5158c81523 test(webui): deflake same-session refresh transcript assertion (#8972).

中文说明

Autofix 本轮总结 — PR #8972(address-review)

反馈点与处置

本轮唯一可执行的反馈是失败检查 Test (ubuntu-latest, Node 22.x)(head cdcb10ac43 上的 CI 运行 31649613702)。没有评审评论、行内发现或基分支冲突(--conflict false,未执行合并)。当前处于仅处理 Critical 的模式;延后的非 Critical 区域没有任何条目,因此未触碰。

失败检查 → 已凭逐步骤证据定位根因,并以最小 deflake 修复。 处置:已处理。

诊断(凭证据,不靠猜测)

  • 通过公开的 check-run API 读取逐步骤结论:第 1–34 步全部成功(安装、lockfile、ESLint、actionlint、shellcheck、yamllint、Prettier、i18n、settings-schema 新鲜度、notices、bundle-closure、helper 测试);第 35 步 Run tests and generate reports 失败;no-AK 集成门槛根本未运行。因此失败是单元测试失败,而非 lint/schema/新鲜度守卫。
  • check-run 注记指出了确切的测试:packages/webui/src/daemon/session/DaemonSessionProvider.test.tsxkeeps the current attachment live while a same-session load fails,报错为 expected [ 'A transcript' ] to deeply equal [ 'A transcript', ' still live' ]
  • 本 PR 完全没有触碰 packages/webuigit diff origin/main HEAD -- packages/webui 为空;分支已完整包含当前 main)。该失败测试是今天由 9ff1519ac9 fix(webui): Close same-session refresh race gaps (#8990) 合入 main 的 —— 因此这是刚落在 main 上、对资源竞争敏感的测试,现在会让所有与 main 同步的分支变红(同一时间窗内另一个已同步分支的运行也在同一步骤失败)。
  • 在 CI 等价环境(隔离 HOMECI=true)下本地复现:该测试在整文件运行中间歇性失败,失败点始终是该 transcript 断言。
  • 用临时插桩(提交前已还原)排除了产品竞态:有界轮询表明该实时 chunk 确实送达了,只是比测试所 drain 的单 macrotask 刷新窗口晚了一拍flushTranscriptDispatch 只等待恰好一个 setTimeout(0));在 runner 竞争下,provider 的批量 dispatch 定时器可能刚好在该窗口之后触发。没有任何事件被丢弃。

修复(deflake 政策,允许的修复方式 #2「稳定计时/等待」)

把固定深度的读取替换为对同一条件的有界等待:裸 expect(...).toEqual(['A transcript', ' still live']) 现在包在 await vi.waitFor(() => { ... }) 中(这是该文件自身的惯用写法,全文约 20 处)。断言、输入、act/flush 序列逐字节保留;没有跳过、放宽、重试或 try/catch 包裹。单文件 +9/−3 行,其中 4 行注释说明这一非显而易见的计时约束。

注:同文件中还有两处姊妹断言使用同样的固定深度模式,但尚未观察到它们抖动;按最小作用域原则未触碰。

验证

本轮在此分支上实际运行的命令(CI 等价环境:隔离 HOMECI=true、按 CI 做法清空 API key):

  • npm run build — 通过(exit 0)。
  • npm run typecheck — 通过(exit 0)。
  • npm run lint — 通过(exit 0)。
  • npx eslint packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx — 通过;对同一文件的 npx prettier --check — 通过。
  • 聚焦 Vitest,在负载下重复运行目标测试文件:npx vitest run src/daemon/session/DaemonSessionProvider.test.tsx(packages/webui)连续整文件运行 × 10 次 — 10/10 通过(每次 266 个测试)。修复前同样的循环:7/7 在该断言上失败;CI 运行也失败了。
  • webui 包全套件:npx vitest run(packages/webui)— 34 个文件、555 个测试全部通过。
  • 本轮早些时候排除的候选项(均为绿色,说明失败步骤的其余面是健康的):packages/core 全套件 — 583 个文件、19923 通过;packages/cli 全套件 — 804 个文件,在 CI=true 下 0 失败(唯一仅在本地失败的是一个 AuthDialog TUI 输入测试,它在 CI=true 时被 it.skip 跳过且与 main 逐字节一致);no-AK 集成门槛(全部 12 个文件)— 140/140 通过;npm run generate:settings-schema + 新鲜度检查 — 无变化;npm run check:lockfile — 通过;npm run check-i18n — 通过。

本轮改动的文件

  • packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx(+9/−3)— 在 keeps the current attachment live while a same-session %s fails 中,用有界等待替换固定深度的 transcript 读取。

提交:5158c81523 test(webui): deflake same-session refresh transcript assertion (#8972)

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @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

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

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

Screenshots · before / after

No screenshot changes against the PR base.

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

Qwen Code · web-shell visuals

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix round: no action on PR #8972

This round's feedback contains no actionable items, so no code changes were made and no commit was created.

Triage summary

  • Reviews / inline comments / issue-level comments: none newer than the last evaluation (2026-08-12T23:20:52Z).
  • Failed checks: none. All completed checks succeeded; the in-progress review-pr check is the automated review workflow itself, not a failure.
  • Still-red checks: none.
  • Critical-only mode: active after 5 change-producing rounds. The single deferred non-Critical item (a PR comment by @qwen-code-ci-bot) was excluded by the workflow and remains open for human follow-up. Per the Critical-only rules it is an audit record only — no code change, thread resolution, or reply was made for it.
  • Base conflict: none reported (--conflict false), so no merge of origin/main was performed.
中文说明

Autofix 轮次:PR #8972 无操作

本轮反馈中没有任何可处理的项目,因此未修改任何代码,也未创建任何提交。

分诊摘要

  • 评审 / 行内评论 / Issue 级评论:没有比上次评估(2026-08-12T23:20:52Z)更新的条目。
  • 失败检查:无。所有已完成的检查均成功;进行中的 review-pr 检查是自动评审工作流本身,并非失败。
  • 持续失败的检查:无。
  • 仅处理 Critical 模式:在完成 5 个产生改动的轮次后已生效。唯一被延后的非 Critical 条目(来自 @qwen-code-ci-bot 的 PR 评论)已被工作流排除,保持开放留待人工跟进。按照 Critical-only 规则,它仅作为审计记录——未针对它做任何代码修改、线程解决或回复。
  • 与 base 分支的冲突:未报告(--conflict false),因此未执行 origin/main 的合并。

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


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

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

⚠️ Downgraded from Approve to Comment: CI failing: review-pr, Test (ubuntu-latest, Node 22.x); PR head advanced during review: reviewed cdcb10a, PR is now at 5158c81 (+1 unreviewed commit touching packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx). Reviewed.

Not explored to full depth (tool budget reached): This PR #8972 adds workingDir support for workflow agents...: None — I completed the full scope.; chunk 5: 无。.

Test Plan (not a blocker): src/agents/worktree-pin.test.tsno such file or directory; src/agents/runtime/workflow-orchestrator.test.tsno such file or directory; src/agents/runtime/workflow-sandbox.test.tsno such file or directory; src/agents/runtime/workflow-journal.test.tsno such file or directory; src/tools/workflow/workflow.test.tsno such file or directory.

中文说明

⚠️ 已从批准降级为评论:CI failing: review-pr, Test (ubuntu-latest, Node 22.x); PR head advanced during review: reviewed cdcb10a, PR is now at 5158c81 (+1 unreviewed commit touching packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx)。 已审查。

未探索到全部深度(达到工具调用预算):This PR #8972 adds workingDir support for workflow agents...:None — I completed the full scope.;chunk 5:无。

Test Plan(非阻断):src/agents/worktree-pin.test.tsno such file or directory; src/agents/runtime/workflow-orchestrator.test.tsno such file or directory; src/agents/runtime/workflow-sandbox.test.tsno such file or directory; src/agents/runtime/workflow-journal.test.tsno such file or directory; src/tools/workflow/workflow.test.tsno such file or directory

— deepseek-v4-flash via Qwen Code /review (v0.21.10)

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix round summary — no action required

No code changes were made this round. The only feedback was an automated review downgrade carrying zero findings, and every concern it cited was checked against evidence and found to require no change.

1. The review downgrade (Approve → Comment) is procedural, not a defect finding.
The review's findings list is empty and it reports "None — I completed the full scope." Both stated downgrade reasons are transient:

  • "CI failing: review-pr, Test (ubuntu-latest, Node 22.x)" — at the review's evaluation time (2026-08-13T00:57:13Z) these checks had not finished yet. Test (ubuntu-latest, Node 22.x) completed SUCCESS at 2026-08-13T01:02:10Z on the current head, and review-pr is the review job itself. All other required checks on head 5158c815 are green.
  • "PR head advanced during review" — the one unreviewed commit (5158c815) is the previous round's test-only deflake of packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx. It is already the pushed head and will be covered by the next automated review pass.

2. The Test Plan note ("no such file or directory") is a false positive.
All five referenced test files exist in this PR under the packages/core/ prefix:

  • packages/core/src/agents/worktree-pin.test.ts
  • packages/core/src/agents/runtime/workflow-orchestrator.test.ts
  • packages/core/src/agents/runtime/workflow-sandbox.test.ts
  • packages/core/src/agents/runtime/workflow-journal.test.ts
  • packages/core/src/tools/workflow/workflow.test.ts

The note's paths are package-relative; running them from the repository root yields "no such file or directory". Run from packages/core, all five files pass: 5 test files, 365/365 tests passed (focused Vitest run, this round).

3. Nothing else is actionable. No inline comments, no issue-level comments, no failed checks, and no still-red checks this round. Critical-only mode is active and no Critical finding exists.

中文说明

Autofix 轮次总结 — 无需处理

本轮未做任何代码改动。本轮唯一的反馈是一条零发现的自动评审降级通知,其中提到的每个疑虑都经过证据核查,均无需改动。

1. 评审降级(Approve → Comment)是程序性的,并非缺陷发现。
该评审的发现列表为空,且其自述"None — I completed the full scope."(无遗漏,已完成全部范围)。其给出的两个降级原因都是暂时性的:

  • "CI failing: review-pr, Test (ubuntu-latest, Node 22.x)"(CI 失败:review-pr、Test (ubuntu-latest, Node 22.x))——在评审评估时点(2026-08-13T00:57:13Z)这些检查尚未完成。Test (ubuntu-latest, Node 22.x) 已于 2026-08-13T01:02:10Z 在当前 head 上以 SUCCESS(成功)结束,而 review-pr 就是评审任务本身。当前 head 5158c815 上的其余必需检查全部为绿色。
  • "PR head advanced during review"(评审期间 PR head 有更新)——未被评审的那一个提交(5158c815)是上一轮对 packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx 的纯测试去抖(deflake)改动。它已经是当前推送的 head,下一轮自动评审会覆盖到它。

2. Test Plan 提示("no such file or directory",即"文件或目录不存在")是误报。
被提到的五个测试文件在本 PR 中均存在,位于 packages/core/ 前缀之下:

  • packages/core/src/agents/worktree-pin.test.ts
  • packages/core/src/agents/runtime/workflow-orchestrator.test.ts
  • packages/core/src/agents/runtime/workflow-sandbox.test.ts
  • packages/core/src/agents/runtime/workflow-journal.test.ts
  • packages/core/src/tools/workflow/workflow.test.ts

该提示中的路径是相对于包目录的;从仓库根目录直接运行才会报"文件或目录不存在"。在 packages/core 目录下运行时,五个文件全部通过:5 个测试文件,365/365 个测试通过(本轮聚焦 Vitest 运行的实测结果)。

3. 没有其他可处理项。 本轮没有行内评论、没有 issue 级评论、没有失败的检查,也没有持续失败的检查。当前处于仅处理 Critical 的模式,且本轮不存在任何 Critical 发现。

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


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

Reviewed. Suggestions are inline.

Not reviewed: build-and-test — Test (macos-latest / windows-latest, Node 22.x) was skipped in CI (merge_group-only lanes) and the unit suite did not run locally on those OSes.

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

Not reviewed: reverse audit — reached the round cap (5) without two consecutive dry rounds; round 5 still surfaced findings.

Not explored to full depth (tool budget reached): You are review agent reverse-audit — Reverse audit agen...: none — all planned checks completed (~15 tool calls).; You are review agent reverse-audit — Reverse audit agen...: none — all checks completed within budget.; You are review agent reverse-audit — Reverse audit agen...: none — all planned checks completed within budget.; You are review agent reverse-audit — Reverse audit agen...: none — all planned checks completed within budget.; You are review agent reverse-audit — Reverse audit agen...: none — all checks above completed within budget..

Test Plan (not a blocker): src/agents/worktree-pin.test.tsno such file or directory; src/agents/runtime/workflow-orchestrator.test.tsno such file or directory; src/agents/runtime/workflow-sandbox.test.tsno such file or directory; src/agents/runtime/workflow-journal.test.tsno such file or directory; src/tools/workflow/workflow.test.tsno such file or directory; and 1 more.

中文说明

已审查。 建议见行内评论。

未审查:build-and-test — Test (macos-latest / windows-latest, Node 22.x) was skipped in CI (merge_group-only lanes) and the unit suite did not run locally on those OSes。

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

未审查:reverse audit — reached the round cap (5) without two consecutive dry rounds; round 5 still surfaced findings。

未探索到全部深度(达到工具调用预算):You are review agent reverse-audit — Reverse audit agen...:none — all planned checks completed (~15 tool calls).;You are review agent reverse-audit — Reverse audit agen...:none — all checks completed within budget.;You are review agent reverse-audit — Reverse audit agen...:none — all planned checks completed within budget.;You are review agent reverse-audit — Reverse audit agen...:none — all planned checks completed within budget.;You are review agent reverse-audit — Reverse audit agen...:none — all checks above completed within budget.

Test Plan(非阻断):src/agents/worktree-pin.test.tsno such file or directory; src/agents/runtime/workflow-orchestrator.test.tsno such file or directory; src/agents/runtime/workflow-sandbox.test.tsno such file or directory; src/agents/runtime/workflow-journal.test.tsno such file or directory; src/tools/workflow/workflow.test.tsno such file or directory; and 1 more。

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

Comment thread .qwen/review-context.json Outdated
{
"paths": ["packages/core/src/skills/**"],
"relatedPaths": ["packages/core/src/skills/**"],
"relatedPaths": ["packages/core/src/skills/*"],

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.

[Suggestion] The core-skills relatedPaths glob was narrowed from packages/core/src/skills/** to packages/core/src/skills/* by a merge-resolution edit that neither parent of merge 0c16d2d62f made — verified: both parents carry ** while the merge result carries *, and the merge message lists only agent.ts as conflicted — and the committed-manifest test was edited in lockstep to bless the narrowing. Probe against the real provider: the wide glob expands to 42 files, the narrow one to 18 — all 24 packages/core/src/skills/bundled/** files (including every bundled SKILL.md) drop out of the related-reading list handed to future core-skills review agents. This is unrelated to the PR's stated scope. — Failure scenario: any future review touching packages/core/src/skills/** silently loses the nested skill files from reviewer context, and the lockstep test edit means CI cannot flag the narrowing.

Suggested change
"relatedPaths": ["packages/core/src/skills/*"],
"relatedPaths": ["packages/core/src/skills/**"],

(If the narrowing is genuinely wanted, land it as its own commit saying so.)

中文说明

core-skills 的 relatedPaths glob 被一次合并解决编辑从 packages/core/src/skills/** 收窄为 packages/core/src/skills/*——已验证:合并 0c16d2d62f 的两个父提交都携带 **,合并结果却携带 *,且合并信息只把 agent.ts 列为冲突文件——同时 committed-manifest 测试被同步修改以认可该收窄。已对真实 provider 探针验证:宽 glob 展开 42 个文件,窄 glob 只有 18 个——全部 24 个 packages/core/src/skills/bundled/** 文件(包括每个捆绑 SKILL.md)都会从交给未来 core-skills 评审 agent 的相关阅读列表中消失。这与本 PR 声明的范围无关。失败场景:未来任何触碰 packages/core/src/skills/** 的评审都会静默失去嵌套技能文件的上下文,而同步修改的测试使 CI 无法发现该收窄。(若确实想要收窄,请作为单独提交并说明理由。)

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

expect(created).toHaveLength(0);
});

it('workingDir rebinds the subagent runtime context to the pinned directory', 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.

[Suggestion] Every workingDir dispatch test in this diff is bare — none combines workingDir with model, agentType, or schema, so the rebind is pinned for only one shape of override dispatch; the workingDir + schema chain (schema override layered over the dir-scoped override via prototype chain) is a real untested chain. — Failure scenario: if a future edit conditions the workingDir branch on the absence of agentType/model (or moves it after an early return), agent('x', { workingDir: 'wt', model: 'qwen3-max' }) would silently run the subagent in the parent working tree — the exact failure this option exists to prevent — while both existing tests stay green. Suggested fix: add one case dispatching workingDir together with model, asserting both calls[0].config.model and the rebound runtime target dir.

await orch.dispatch(prompt, {
  workingDir: '.qwen/tmp/review-pr-7',
  model: 'qwen3-max',
});
// assert calls[0].config.model AND the rebound runtime target dir
中文说明

本 diff 中所有 workingDir 派发测试都是裸的——没有一个把 workingDirmodelagentTypeschema 组合使用,因此重绑定只为一种 override 派发形态钉住;workingDir + schema 链(schema override 经原型链叠加在目录作用域 override 之上)是真实存在却未被测试的链。失败场景:若未来某次编辑把 workingDir 分支改为以「不存在 agentType/model」为条件(或把它移到某个 early return 之后),agent('x', { workingDir: 'wt', model: 'qwen3-max' }) 会静默地在父工作树中运行子 agent——这正是该选项要防止的失败——而现有两个测试都保持绿色。建议补一个 workingDirmodel 组合的派发用例,同时断言 calls[0].config.model 与重绑定后的运行时目标目录。

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

Comment on lines +233 to +234
describe('GitWorktreeService.getMainWorktreePath() (real git)', () => {
vi.setConfig({ testTimeout: 30000, hookTimeout: 30000 });

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.

[Suggestion] This new describe block re-declares vi.setConfig, tmpDirs, the afterEach cleanup, commitInitial, and initRepo — 31 lines measured byte-identical to the isRegisteredLinkedWorktree() describe block immediately above it in the same file. — Failure scenario: the setup logic now exists in two copies; a future fix to repo provisioning (a new git default, a CI credential-helper workaround, a Windows path adjustment) applied to one block but not the other silently leaves the second block broken or flaky, invisible until one of them fails. Suggested fix: hoist tmpDirs, the afterEach, commitInitial, and initRepo to file scope shared by both describe blocks — the copies are byte-identical, so no parameterization is needed.

中文说明

新增的 describe 块重新声明了 vi.setConfigtmpDirsafterEach 清理、commitInitialinitRepo——共 31 行,经实测与同文件上方紧邻的 isRegisteredLinkedWorktree() describe 块逐字节相同。失败场景:仓库准备逻辑现在存在两份副本;未来对仓库准备的修复(新的 git 默认值、CI 凭证助手绕过、Windows 路径调整)若只应用于其中一个块,会静默让另一个块坏掉或变得 flaky,直到其中一个失败才可见。建议把 tmpDirsafterEachcommitInitialinitRepo 提升到文件作用域供两个 describe 块共享——两份副本逐字节相同,无需参数化。

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

// default watchdog applies, contradicting "0 disables the watchdog"
// — refuse it loudly like the other option gates.
if (agentOpts.stallMs !== undefined && (typeof agentOpts.stallMs !== 'number' || !Number.isFinite(agentOpts.stallMs))) {
throw new Error("agent({stallMs}): must be a finite number of milliseconds (0 disables the watchdog).");

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.

[Suggestion] The new gate admits negative finite numbers, which downstream resolveStallMs silently replaces with the default 60 s watchdog — probe-verified: agent('x', { stallMs: -1 }) passes the gate (no throw) and dispatch receives stallMs: -1, resolving to 60000. The gate fails its own stated purpose — its comment says it exists so a non-honored value is "refused loudly like the other option gates" instead of being "silently dropped downstream" — and its message ("must be a finite number of milliseconds") advertises any finite number as honored. — Failure scenario: stallMs: -1 — the "-1 = no timeout" convention, or a computed remainder deadline - Date.now() gone negative — passes the gate and runs under the default 60 s watchdog instead of unwatched: a healthy-but-quiet stream is aborted and retried up to 3 attempts, tripling token spend. Fix: reject negatives in the condition above this throw.

if (agentOpts.stallMs !== undefined && (typeof agentOpts.stallMs !== 'number' || !Number.isFinite(agentOpts.stallMs) || agentOpts.stallMs < 0)) {
中文说明

新关卡接受负的有限数字,而下游 resolveStallMs 会把它静默替换为默认 60 秒看门狗——已探针验证:agent('x', { stallMs: -1 }) 通过关卡(不抛错),派发收到 stallMs: -1,最终解析为 60000。该关卡违背了自身注释声明的目的——注释说它的存在是为了让不被尊重的值「像其他选项关卡一样被大声拒绝」,而不是「在下游被静默丢弃」——且其消息(「必须是有限的毫秒数」)宣称任何有限数字都会被尊重。失败场景:stallMs: -1——「-1 = 无超时」约定,或算成负数的剩余时间 deadline - Date.now()——通过关卡后在默认 60 秒看门狗下运行,而非放任不管:健康但安静的流会被中止并最多重试 3 次,三倍 token 消耗。修复:在此 throw 上方的条件中加入 || agentOpts.stallMs < 0

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

// default watchdog applies, contradicting "0 disables the watchdog"
// — refuse it loudly like the other option gates.
if (agentOpts.stallMs !== undefined && (typeof agentOpts.stallMs !== 'number' || !Number.isFinite(agentOpts.stallMs))) {
throw new Error("agent({stallMs}): must be a finite number of milliseconds (0 disables the watchdog).");

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.

[Suggestion] This gate is the only one of the PR's three new gates without a host-side mirror, and it reads pre-revival opts — probe-verified enumerable-getter evasion: get stallMs() { return ++n > 3 ? '0' : 1000; } passes the gate (each of its reads sees 1000), the JSON.stringify revival surfaces '0', and host dispatch silently drops it (typeof opts.stallMs === 'number' ? … : undefined, workflow-orchestrator.ts), applying the default watchdog. Defense-in-depth / sibling-consistency gap: the bypass cannot produce an unsafe state (the host falls back to the safe default, and 0 is directly admissible), but the gate's "loud refusal" contract is evadable, unlike its workingDir siblings, which got host mirrors for exactly this reason. — Failure scenario: a saved workflow script (project-scoped third-party code) passes a getter-backed stallMs; the dispatch the script spelled as unwatched runs under the 60 s watchdog — the precise failure the gate's own comment describes. Suggested fix: mirror the gate host-side in createProductionDispatch before resolveStallMs, replacing the silent typeof … : undefined drop.

中文说明

该关卡是本 PR 三个新关卡中唯一没有主机侧镜像的,且读取的是 revival 之前的 opts——已探针验证的可枚举 getter 绕过:get stallMs() { return ++n > 3 ? '0' : 1000; } 通过关卡(它的每次读取都看到 1000),JSON.stringify revival 时浮现 '0',主机侧派发静默丢弃(workflow-orchestrator.ts 的 typeof opts.stallMs === 'number' ? … : undefined),套用默认看门狗。这是纵深防御/兄弟一致性缺口:该绕过无法产生不安全状态(主机侧回落到安全默认值,且 0 本身可直接通过),但与正因同样原因获得了主机侧镜像的 workingDir 兄弟关卡不同,此关卡的「大声拒绝」契约可以被绕过。失败场景:保存的 workflow 脚本(项目作用域的第三方代码)传入 getter 承载的 stallMs;脚本明确声明不加看门狗的派发却在 60 秒看门狗下运行——正是该关卡注释所描述的失败。建议在 createProductionDispatchresolveStallMs 之前于主机侧镜像该关卡,替换静默的 typeof … : undefined 丢弃。

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

* would otherwise still resolve through the prototype to the parent.
* Shared by both directory-scoped dispatch modes — `isolation: 'worktree'`,
* which provisions the directory, and `workingDir`, which is handed one the
* caller already owns. Mirrors the inline rebind block at agent.ts:2008-2024.

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.

[Suggestion] The rewritten doc comment re-asserts "Mirrors the inline rebind block at agent.ts:2008-2024", but at the reviewed commit that block lives at agent.ts:2913-2932 — lines 2008-2024 contain unrelated span code (buildSubagentSpanSpec). Verified at this commit: the pointer sits on a + line of this PR's rewrite; the real block at 2913-2932 is the line-for-line mirror; and this comment is the only sync-guard tying the two duplicated rebind implementations together (no cross-reference exists from agent.ts). — Failure scenario: the next maintainer fixing a rebind divergence follows the pointer, lands ~900 lines short in the span-spec helper, and either edits the wrong place or concludes the mirror no longer exists — the twin-implementation drift this comment exists to prevent then happens silently, now that the orchestrator copy serves both isolation:'worktree' and workingDir dispatches. Line-number pointers rot — this file's three sibling agent.ts:NNNN pointers are all stale already; name the landmark instead.

Suggested change
* caller already owns. Mirrors the inline rebind block at agent.ts:2008-2024.
* caller already owns. Mirrors the `worktreeIsolation` rebind block in `AgentTool.executeSubagent` (search `Phase 2: rebind cwd` in tools/agent/agent.ts).
中文说明

重写后的文档注释重新断言「Mirrors the inline rebind block at agent.ts:2008-2024」,但在被审提交上该块位于 agent.ts:2913-2932——2008-2024 行包含无关的 span 代码(buildSubagentSpanSpec)。已在该提交上验证:该指针位于本 PR 重写段落的 + 行;2913-2932 处的真实块是逐行镜像;且该注释是把两份重复重绑定实现绑在一起的唯一同步守卫(agent.ts 中不存在反向引用)。失败场景:下一个修复重绑定分歧的维护者顺着指针走,落在约 900 行之外的 span-spec 辅助函数里,要么改错地方,要么以为镜像已不存在——于是这份注释本要防止的双实现漂移悄悄发生,何况 orchestrator 副本现在同时服务 isolation:'worktree'workingDir 两种派发。行号指针会腐烂——本文件另外三个 agent.ts:NNNN 指针已全部过期;请改为引用地标。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix round summary — no action taken (PR #8972)

No changes were made this round.

Why no action was taken

  • No new actionable feedback. Every actionable section of this round's triage is empty: no new reviews, inline comments, or issue-level comments newer than the last evaluation (2026-08-13T02:07:06Z).
  • Critical-only mode is active (the PR has completed five change-producing rounds). The workflow's deterministic filter excluded the non-Critical feedback below from this round; those items remain open for human follow-up. Per the mode's rules, this round did not modify code, resolve threads, or reply to comments for them:
    • Review by @qwen-code-ci-bot (pullrequestreview-4923065597)
    • Inline comments rc:3772126869, rc:3772126880, rc:3772126882, rc:3772126886, rc:3772126889, rc:3772126894
  • All CI checks pass on the current head. Completed checks: 14 SUCCESS, 39 SKIPPED, 0 failed or pending. The skipped lanes are conditional / merge_group-only workflows, not failures. The only outstanding CHANGES_REQUESTED review is from the automated reviewer at 2026-08-12T22:04:10Z, which predates the last-evaluation cutoff; its actionable items were already processed in earlier rounds.
  • No base conflict (--conflict false) — no merge of origin/main was performed.

No commits were made and the working tree is unchanged.

中文说明

Autofix 轮次总结 — 未采取任何操作(PR #8972

本轮未做任何更改。

未采取操作的原因

  • 没有新的可操作反馈。 本轮分诊的所有可操作区域均为空:自上次评估(2026-08-13T02:07:06Z)之后,没有新的评审、行内评论或 issue 级评论。
  • 当前处于仅处理 Critical 的模式(该 PR 已完成五个产生改动的轮次)。工作流的确定性过滤器已将下方非 Critical 反馈排除在本轮之外;这些条目保持开放,留待人工跟进。按照该模式的规则,本轮未对这些条目修改代码、未解决讨论串、也未回复评论:
    • @qwen-code-ci-bot 的评审(pullrequestreview-4923065597)
    • 行内评论 rc:3772126869、rc:3772126880、rc:3772126882、rc:3772126886、rc:3772126889、rc:3772126894
  • 当前 head 上所有 CI 检查均通过。 已完成的检查:14 个 SUCCESS,39 个 SKIPPED,0 个失败或进行中。被跳过的检查项是条件触发 / 仅 merge_group 的工作流,并非失败。唯一仍然挂起的 CHANGES_REQUESTED 评审来自自动化评审器,提交于 2026-08-12T22:04:10Z,早于上次评估的截止时间;其中的可操作条目已在之前的轮次中处理完毕。
  • 没有与基础分支的冲突--conflict false)——未执行对 origin/main 的合并。

未创建任何提交,工作树保持不变。

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


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

@qqqys
qqqys dismissed qwen-code-ci-bot’s stale review August 13, 2026 07:04

Superseded by a later commit; the current head requires a fresh review.

…g-dir

# Conflicts:
#	.qwen/review-context.json
#	packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round summary — merge origin/main (conflict resolution)

This round had no actionable feedback: Critical-only mode is active, the deferred
non-Critical section contained no items, and there were no failed or still-red
checks. The round's work was the requested base-conflict resolution
(--conflict true).

Changes

Merged origin/main (tip 8e0033d64d) into workflow/agent-working-dir.
Two conflicts, plus one semantic overlap the textual auto-merge stacked:

  • .qwen/review-context.json — resolved to origin/main. The branch had
    narrowed the skills relatedPaths glob (***) to stay within the
    manifest's resolved-file bound; main's 02f1e90a0a (fix(review): drop the web-shell e2e related-paths that breach the resolved-file bound #9028) and 8e0033d64d
    (fix(ci): reduce ENOSPC and load-sensitive test flakes #8982) since removed those relatedPaths entries entirely (same bound fix,
    carried further), superseding the branch's narrowing.
  • packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts
    resolved to origin/main for the same reason: the committed-manifest
    expectation and its sentinel map must mirror the committed JSON exactly.
    Both resolved files are byte-identical to origin/main.
  • packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx — the
    textual auto-merge kept BOTH fixes for the same same-session refresh flake:
    this branch's bound-wait (vi.waitFor, commit 5158c81523) wrapping the
    transcript assertion, and main's deterministic drain (sourceEventProcessed
    deferred, test(webui): drain the batched transcript dispatch with two timer hops #9058). Main's drain is the complete fix, so the file now matches
    origin/main and the redundant bound-wait is dropped. The deflake intent is
    fully preserved by the deterministic version.
  • packages/core/src/agents/runtime/workflow-sandbox.test.ts auto-merged
    cleanly: this branch's added workingDir/stallMs tests and main's
    fake-timer rearm deflake (fix(ci): reduce ENOSPC and load-sensitive test flakes #8982) touch disjoint regions; both verified green.
  • packages/cli/src/ui/hooks/use-effort-command.ts converged on both sides via
    the identical Prettier reformat — no decision needed.

Post-merge, the tree diff against origin/main contains exactly this PR's
feature files (worktree pinning / agent workingDir), nothing else.

Dispositions

  • No feedback points this round (nothing to address, decline, or escalate).

Verification

Commands actually run on the merged tree:

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npx vitest run (packages/core, touched files: workflow-journal, workflow-orchestrator, workflow-sandbox, worktree-pin, gitWorktreeService, tools/workflow, tools/agent) — 7 files, 660 passed
  • npx vitest run src/services/gitWorktreeService.linked.integ.test.ts (packages/core) — 13 passed
  • npx vitest run (packages/cli: manifest-repository-context.committed, manifest-repository-context, use-effort-command) — 3 files, 71 passed
  • npx vitest run src/daemon/session/DaemonSessionProvider.test.tsx (packages/webui) — 267 passed
  • npm run generate:settings-schema — regenerated artifact byte-identical to the merged one (settings sources came from main as a matched pair); no drift
中文说明

Autofix 本轮总结 — 合并 origin/main(冲突解决)

本轮没有可处理的反馈:当前处于 Critical-only 模式,延后的非 Critical 部分不含任何条目,也没有失败或持续失败的检查项。本轮的工作是按要求解决与 base 分支的冲突(--conflict true)。

改动内容

origin/main(顶端提交 8e0033d64d)合并进 workflow/agent-working-dir。共有两处冲突,外加一处文本自动合并叠加出的语义重叠:

  • .qwen/review-context.json — 采用 origin/main 的版本。本分支曾将 skills 的 relatedPaths glob 收窄(***)以保持在 manifest 解析文件数上限之内;而 main 上的 02f1e90a0afix(review): drop the web-shell e2e related-paths that breach the resolved-file bound #9028)和 8e0033d64dfix(ci): reduce ENOSPC and load-sensitive test flakes #8982)之后将这些 relatedPaths 条目整体删除(同一上限修复、做得更彻底),取代了分支上的收窄改动。
  • packages/cli/src/commands/review/lib/manifest-repository-context.committed.test.ts — 出于同样原因采用 origin/main 的版本:已提交 manifest 的期望值和 sentinel 映射必须与已提交的 JSON 完全一致。两个解决后的文件与 origin/main 逐字节一致。
  • packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx — 文本自动合并同时保留了对同一个「同会话刷新」flake 的两处修复:本分支对 transcript 断言的有界等待(vi.waitFor,提交 5158c81523),以及 main 的确定性排空(sourceEventProcessed deferred,test(webui): drain the batched transcript dispatch with two timer hops #9058)。main 的排空是完整修复,因此该文件现在与 origin/main 一致,冗余的有界等待被移除。去 flake 的意图由确定性版本完整保留。
  • packages/core/src/agents/runtime/workflow-sandbox.test.ts 自动合并干净:本分支新增的 workingDir/stallMs 测试与 main 的 fake-timer rearm 去 flake(fix(ci): reduce ENOSPC and load-sensitive test flakes #8982)触及不相交的区域;两者均已验证通过。
  • packages/cli/src/ui/hooks/use-effort-command.ts 双方以完全相同的 Prettier 重排收敛 —— 无需决策。

合并后,相对 origin/main 的树差异恰好只包含本 PR 的特性文件(worktree pinning / agent workingDir),别无其他。

处置

  • 本轮没有反馈条目(无需处理、拒绝或升级的内容)。

验证

在合并后的树上实际运行的命令:

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • npx vitest run(packages/core,触及文件:workflow-journal、workflow-orchestrator、workflow-sandbox、worktree-pin、gitWorktreeService、tools/workflow、tools/agent)— 7 个文件,660 个测试通过
  • npx vitest run src/services/gitWorktreeService.linked.integ.test.ts(packages/core)— 13 个测试通过
  • npx vitest run(packages/cli:manifest-repository-context.committed、manifest-repository-context、use-effort-command)— 3 个文件,71 个测试通过
  • npx vitest run src/daemon/session/DaemonSessionProvider.test.tsx(packages/webui)— 267 个测试通过
  • npm run generate:settings-schema — 重新生成的产物与合并进来的版本逐字节一致(settings 源文件由 main 成对带入);无漂移

Deferred non-Critical feedback

Critical-only mode is active: 5 change-producing rounds are complete. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: conflicted with main — resolved in this push. · 与 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. Suggestions are inline.

Not reviewed: build-and-test — Test (macos-latest / windows-latest, Node 22.x) was skipped in CI (merge_group-only lanes) and the unit suite did not run locally on those OSes.

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

Not explored to full depth (tool budget reached): "PR #8972 adds workflow-agent workingDir pinning,…": none (finished within budget; the only check I did not perform is reading the body of every one of the nine getRepoTopLevel callers line-by-line — I inspected…; "PR #8972 adds workflow-agent workingDir pinning,…": none — all checks I opened were completed (~12 tool calls).; "PR #8972 adds workflow-agent workingDir pinning,…": none — all checks above completed within budget.; "PR #8972 adds workflow-agent workingDir pinning,…": none — all checks above completed within budget; no unfinished check to disclose..

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

Test Plan (not a blocker): src/agents/worktree-pin.test.tsno such file or directory; src/agents/runtime/workflow-orchestrator.test.tsno such file or directory; src/agents/runtime/workflow-sandbox.test.tsno such file or directory; src/agents/runtime/workflow-journal.test.tsno such file or directory; src/tools/workflow/workflow.test.tsno such file or directory; and 1 more.

中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

未审查:build-and-test — Test (macos-latest / windows-latest, Node 22.x) was skipped in CI (merge_group-only lanes) and the unit suite did not run locally on those OSes。

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

未探索到全部深度(达到工具调用预算):"PR #8972 adds workflow-agent workingDir pinning,…"none (finished within budget; the only check I did not perform is reading the body of every one of the nine getRepoTopLevel callers line-by-line — I inspected…"PR #8972 adds workflow-agent workingDir pinning,…"none — all checks I opened were completed (~12 tool calls)."PR #8972 adds workflow-agent workingDir pinning,…"none — all checks above completed within budget."PR #8972 adds workflow-agent workingDir pinning,…"none — all checks above completed within budget; no unfinished check to disclose.

未审查:反向审计——在 5 轮的反审轮数上限内未收敛。

Test Plan(非阻断):src/agents/worktree-pin.test.tsno such file or directory; src/agents/runtime/workflow-orchestrator.test.tsno such file or directory; src/agents/runtime/workflow-sandbox.test.tsno such file or directory; src/agents/runtime/workflow-journal.test.tsno such file or directory; src/tools/workflow/workflow.test.tsno such file or directory; and 1 more。

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

// A non-number stallMs is silently dropped downstream and the
// default watchdog applies, contradicting "0 disables the watchdog"
// — refuse it loudly like the other option gates.
if (agentOpts.stallMs !== undefined && (typeof agentOpts.stallMs !== 'number' || !Number.isFinite(agentOpts.stallMs))) {

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.

[Suggestion] R6-1: This gate admits every finite stallMs, but Node coerces setTimeout delays outside [1, 2^31-1] to 1 ms — so fractional values (0 < v < 1) and values above 2147483647 arm a timer that fires ~1 ms after the first progress event. Probe-verified at this commit: runStallResilient with stallMs: 2**31 and a healthy simulated agent (progress every 5 ms) produced attempts=3 and a terminal "stalled on all 3 attempts" error; the control (stallMs: 60000) succeeded in one attempt; clamping in resolveStallMs flips the probe. Distinct from the negative-value gap already flagged at this gate: negatives are silently rescued to the 60 s default, while these admitted values produce immediate false aborts.

Failure scenario: agent("x", { stallMs: 2**31 }) ("effectively no watchdog") or { stallMs: 0.5 } (unit slip for 500 ms) passes the gate, then the watchdog aborts + retries the healthy dispatch 3× and it terminally fails — wasted tokens, and the error names a stall that never happened.

Suggested change
if (agentOpts.stallMs !== undefined && (typeof agentOpts.stallMs !== 'number' || !Number.isFinite(agentOpts.stallMs))) {
if (agentOpts.stallMs !== undefined && (typeof agentOpts.stallMs !== 'number' || !Number.isFinite(agentOpts.stallMs) || !Number.isInteger(agentOpts.stallMs) || agentOpts.stallMs > 2147483647)) {
中文说明

R6-1:该门禁接受所有有限的 stallMs,但 Node 会把超出 [1, 2^31-1] 范围的 setTimeout 延迟强制为 1 毫秒——因此小数值(0 < v < 1)和大于 2147483647 的值会在首个进度事件后约 1 毫秒触发计时器。已在本提交探针验证:runStallResilientstallMs: 2**31 且模拟 agent 健康(每 5 毫秒一次进度)时得到 attempts=3 与终态错误 "stalled on all 3 attempts";对照组(stallMs: 60000)一次成功;在 resolveStallMs 中夹紧可翻转探针。与该门禁已报告的负数缺口不同:负数被悄悄救回 60 秒默认值,而这些取值会立即造成误报中止。

失败场景:agent("x", { stallMs: 2**31 })(“近似关闭看门狗”)或 { stallMs: 0.5 }(500 毫秒的单位笔误)通过门禁后,看门狗将健康的派发中止并重试 3 次直至终态失败——浪费 tokens,且错误声称发生了从未发生的停滞。

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

Comment on lines +32 to +34
GitWorktreeService: vi
.fn()
.mockImplementation(() => stubs.current as unknown),

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.

[Suggestion] R6-2: Every test here mocks GitWorktreeService with a constructor that ignores its cwd argument, and no test asserts a construction call — so the resolver's repo-root re-anchoring (new GitWorktreeService(repoRoot) when repoRoot !== parentCwd) is entirely unobserved. Probe-verified at this commit: the mutation const wtService = probe; (dropping the second construction) survives all 10 tests, and adding a construction assertion flips the probe.

Failure scenario: a refactor that drops or swaps the second construction makes the registration gate and branch-label lookup run against a service anchored at the parent's cwd (possibly a monorepo subdirectory or a linked worktree) instead of the main working tree, defeating the anchoring the resolver's comment block documents — CI stays green.

Suggested fix: capture the mock constructor and assert the construction cwds where anchoring matters, e.g. in the sibling-worktree test:

expect(GitWorktreeServiceMock).toHaveBeenNthCalledWith(1, '/repo/.qwen/tmp/review-pr-1');
expect(GitWorktreeServiceMock).toHaveBeenNthCalledWith(2, '/repo');
中文说明

R6-2:这里所有测试都用一个忽略 cwd 参数的构造函数 mock 了 GitWorktreeService,且没有任何测试断言构造调用——因此 resolver 的仓库根重锚定(repoRoot !== parentCwd 时的 new GitWorktreeService(repoRoot))完全不被观测。已在本提交探针验证:变异 const wtService = probe;(丢弃第二次构造)在全部 10 个测试下存活;添加构造断言可翻转探针。

失败场景:丢弃或替换第二次构造的重构会让注册门禁与分支标签查询运行在锚定于父 cwd(可能是 monorepo 子目录或某个 linked worktree)的服务上,而不是主工作树——破坏了 resolver 注释块所记录的锚定行为,而 CI 全绿。

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

// the registry gate and labels stay scoped to the repository. A registered
// sibling worktree is the documented review-pipeline setup.
it('accepts a sibling worktree when the parent runs inside a linked worktree', async () => {
svc.getRepoTopLevel.mockResolvedValue('/repo/.qwen/tmp/review-pr-1');

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.

[Suggestion] R6-3: This getRepoTopLevel mock is dead setup — the default getMainWorktreePath → '/repo' stub short-circuits the ?? chain before getRepoTopLevel is consulted (probe-verified: expect(svc.getRepoTopLevel).not.toHaveBeenCalled() passes on unmutated code) — and the assertion below observes only path, never repoRoot. Probe-verified: the mutation repoRoot = parentCwd (dropping the main-tree anchoring this PR introduced) survives all 10 tests here plus agent.test.ts (258 tests); asserting repoRoot flips the probe.

Failure scenario: a regression of the main-tree anchoring ships green; real runs from inside a linked worktree then scope the branch-label lookup and refusal labeling to the worktree instead of the repository — exactly the regression the anchoring change exists to prevent.

Suggested fix: assert the anchor explicitly in the test below:

expect(result).toMatchObject({
  path: path.resolve('/repo/.qwen/tmp/review-pr-1', '../review-pr-1-base'),
  repoRoot: '/repo',
});

optionally with expect(svc.getRepoTopLevel).not.toHaveBeenCalled() so the setup matches what the resolver actually consults.

中文说明

R6-3:这个 getRepoTopLevel mock 是死 setup——默认的 getMainWorktreePath → '/repo' 桩会在 ?? 链触及 getRepoTopLevel 之前短路(探针验证:对未变异代码 expect(svc.getRepoTopLevel).not.toHaveBeenCalled() 通过)——且下方断言只观测 path,从不观测 repoRoot。探针验证:变异 repoRoot = parentCwd(丢弃本 PR 引入的主树锚定)在全部 10 个测试及 agent.test.ts(258 个测试)下存活;断言 repoRoot 可翻转探针。

失败场景:主树锚定回归后 CI 全绿;从 linked worktree 内部运行的真实场景会把分支标签查询与拒绝措辞的作用域错误地限定到该 worktree 而非仓库——这正是锚定改动要防止的回归。

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

// git metadata could not be read" — name both rather than assert one.
return {
error:
`${label} "${resolvedPath}" is not a registered linked worktree of ` +

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.

[Suggestion] R6-4: This refusal message enumerates three causes, but the gate has a fourth failure axis the message never names — the liveness probe. Probe-verified with real git at this commit: git worktree add wt-stale then rm -rf the directory without git worktree remove keeps the registry record (git worktree list still lists the path as prunable), and isRegisteredLinkedWorktree returns false via the in-path rev-parse --git-dir probe. For that input every enumerated cause is false — it is not the main tree, it IS present in git worktree list, and its metadata read fine. Behavior is correct (fails closed); only the diagnosis is wrong.

Failure scenario: a user hitting the stale-record case is told three false causes and debugs in the wrong direction; the actual remedy is git worktree add again or git worktree prune.

Suggested fix:

return {
  error:
    `${label} "${resolvedPath}" is not a registered linked worktree of ` +
    `this repository (it is the main working tree, is not a live linked ` +
    `worktree in this repository's registry \u2014 possibly a stale record whose ` +
    `directory was recreated \u2014 or its git metadata could not be read) \u2014 ` +
    `pinning a sub-agent there would not isolate it. Pass a worktree ` +
    `created via \`git worktree add\`.`,
};
中文说明

R6-4:该拒绝消息列举了三种原因,但门禁还有第四个消息从未提及的失败轴——存活探测。已用真实 git 在本提交探针验证:git worktree add wt-stale 后不经 git worktree remove 直接 rm -rf 目录,注册记录仍然保留(git worktree list 仍把该路径列为 prunable),而 isRegisteredLinkedWorktree 经由路径内的 rev-parse --git-dir 探测返回 false。对该输入,列举的每一种原因都是假的——它不是主树、它确实在 git worktree list 中、其元数据读取正常。行为正确(失败即关闭);只是诊断错了。

失败场景:用户撞上过期记录场景时被告知三个错误原因,从而朝错误方向调试;实际补救是重新 git worktree addgit worktree prune

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

// inside getRegisteredWorktreeBranch against the repository, not a monorepo
// subdirectory the parent launched from.
const mainTreePath = await probe.getMainWorktreePath();
const repoRoot = mainTreePath ?? (await probe.getRepoTopLevel()) ?? parentCwd;

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.

[Suggestion] R6-5: The resolver's fallback arms are never exercised — every test in worktree-pin.test.ts leaves the default stub getMainWorktreePath → '/repo' or overrides it with a non-null value, and no other suite drives the real resolver with a null anchor.

Failure scenario: getMainWorktreePath() returns null (the outcome this PR's own service tests produce for newline-containing main-tree paths or a failing anchor probe) while the parent runs inside a linked worktree; the resolver then degrades to --show-toplevel, which from a linked worktree answers the worktree's OWN root — precisely the mis-anchoring the main-tree anchor exists to avoid. No test drives this branch, so a future edit breaking the fallback is invisible to CI. The registration gate itself still fails closed, which is why this is a Suggestion.

Suggested fix: add one resolver test — svc.getMainWorktreePath.mockResolvedValue(null) with getRepoTopLevel → '/repo', asserting a successful pin still resolves and repoRoot is '/repo' (plus optionally a both-null case asserting the parentCwd arm).

中文说明

R6-5:resolver 的回退分支从未被演练——worktree-pin.test.ts 中所有测试要么保留默认桩 getMainWorktreePath → '/repo',要么用非 null 值覆盖它,也没有其他套件用 null 锚点驱动真实 resolver。

失败场景:当父进程运行在 linked worktree 内部而 getMainWorktreePath() 返回 null(本 PR 自己的 service 测试就会对含换行的主树路径或失败的锚点探测产生该结果)时,resolver 退化到 --show-toplevel——从 linked worktree 内调用它会回答该 worktree 自己的根——恰恰是主树锚点要避免的错误锚定。没有测试驱动这一分支,未来破坏该回退的编辑对 CI 不可见。注册门禁本身仍然失败即关闭,因此这是建议而非阻断。

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

).rejects.toThrow(/stallMs.*finite number/);
});

it('agent({stallMs: 0}) passes through and disables the watchdog', 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.

[Suggestion] R6-6: This test's name claims the end-to-end effect ("disables the watchdog"), but its assertions only pin stallMs: 0 arriving at the injected fake dispatch. The sole production wiring between that boundary and the watchdog — resolveStallMs(typeof opts.stallMs === 'number' ? opts.stallMs : undefined) in workflow-orchestrator.ts — is exercised with 0 by no test anywhere (verified: the dispatch-level test uses stallMs: 5, workflow-stall.test.ts bypasses the wiring, the journal test uses 1234). Probe-verified: the mutation opts.stallMs || undefined ships 322/322 green and hands 60000 to runStallResilient for an explicit stallMs: 0; the PR's real wiring hands 0.

Failure scenario: a future one-token edit of that ternary silently re-applies DEFAULT_STALL_MS (60 s): a dispatch the script author explicitly left unwatched gets aborted and retried up to 3× after 60 s of quiet streaming, wasting tokens, while every existing test stays green. The PR's own env-bound wiring tests exist on precisely this reasoning ("resolver-level tests cannot catch a revert at the dispatch site").

Suggested fix: add a dispatch-site test asserting stallMs: 0 reaches resolveStallMs/runStallResilient as 0 (spy the wiring), or rename the test to "…passes through to dispatch" to match what it pins.

中文说明

R6-6:该测试名称声称端到端效果(“禁用看门狗”),但断言只钉住 stallMs: 0 到达了注入的假 dispatch。从该边界到看门狗之间唯一的生产接线——workflow-orchestrator.ts 中的 resolveStallMs(typeof opts.stallMs === 'number' ? opts.stallMs : undefined)——没有任何测试用 0 演练过(已核实:dispatch 层测试用 stallMs: 5,workflow-stall.test.ts 绕过接线,journal 测试用 1234)。探针验证:变异 opts.stallMs || undefined 在 322/322 全绿下把显式的 stallMs: 0 变成传给 runStallResilient60000;PR 的真实接线传的是 0

失败场景:未来对该三元表达式的一个 token 编辑会悄悄重新启用 DEFAULT_STALL_MS(60 秒):脚本作者明确声明不看管的派发会在 60 秒安静流式后被中止并最多重试 3 次,浪费 tokens,而所有现有测试保持绿色。本 PR 自己的 env 上限接线测试正是基于同样的理由存在(“resolver 层测试抓不到派发站点的回退”)。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix review round: no action taken (PR #8972)

This round has no actionable feedback, so no code changes or commits were made.

  • Reviews / inline comments / issue-level comments: none newer than the last evaluation (2026-08-13T03:15:43Z).
  • Failed checks: none. Still-red checks: none.
  • Base conflict: none reported, so origin/main was not merged.
  • Critical-only mode is active (5 change-producing rounds are complete). The deferred non-Critical items listed in the workflow's audit record (review pullrequestreview-4931538683 and inline findings rc:3779049707, rc:3779049723, rc:3779049729, rc:3779049732, rc:3779049736, rc:3779049744) remain open for human follow-up. Per the critical-only rules, this round made no code changes, resolved no threads, and wrote no comment replies for those deferred items.

The PR head is unchanged by this round.

中文说明

Autofix 审查轮次:未执行任何操作(PR #8972

本轮没有可处理的反馈,因此未做任何代码改动,也未产生任何提交。

  • 审查 / 行内评论 / Issue 级评论: 自上次评估(2026-08-13T03:15:43Z)之后没有新内容。
  • 失败的检查: 无。持续失败的检查: 无。
  • 与基线分支的冲突: 未报告,因此未合并 origin/main
  • 已进入仅处理 Critical 的模式(已完成 5 个产生改动的轮次)。工作流审计记录中列出的被延后非 Critical 条目(审查 pullrequestreview-4931538683 以及行内发现 rc:3779049707、rc:3779049723、rc:3779049729、rc:3779049732、rc:3779049736、rc:3779049744)保持开放,留待人工跟进。按照仅处理 Critical 的规则,本轮未针对这些被延后条目修改代码、未解决任何讨论串,也未撰写任何评论回复。

本轮未改变 PR 的 head 提交。

Deferred non-Critical feedback

Critical-only mode is active: 5 change-producing rounds are complete. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


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

5 participants