feat(core): let a workflow agent pin a directory and outlive the default bounds - #8972
feat(core): let a workflow agent pin a directory and outlive the default bounds#8972qqqys wants to merge 16 commits into
Conversation
…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
left a comment
There was a problem hiding this comment.
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
Workflowcall withworkingDirpins 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/Ais 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
- How to verify — the behaviors a reviewer should confirm and what to expect: e.g. a
- Risk & Scope — the three bullets: main risk or tradeoff (e.g. forcing the override path when
workingDiris 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——评审者应确认的行为和预期结果:例如带
workingDir的Workflow调用会把子代理固定到该 worktree 并绕开 fast path、非法路径会中止分发并说明原因、环境变量可调上限按文档所述钳制/拒绝,以及哪些测试套件固定了这些行为 - Evidence (Before & After)——这是 workflow 引擎内部行为而非 TUI 界面改动,按模板写
N/A即可,命令与输出放在 How to verify 下 - Tested on——操作系统矩阵(🍏/🪟/🐧 加 ✅/
⚠️ /N/A);目前无法判断你的验证是在哪个平台上进行的
- How to verify——评审者应确认的行为和预期结果:例如带
- Risk & Scope——三个要点:主要风险或权衡(例如设置
workingDir时强制走 override 路径)/ 未验证项 / 破坏性变更 - Linked Issues——引用 #8769(不使用关闭关键字,因为这是该提案的一部分)
- 正文的
<details>中文翻译
能否按模板重构正文?你已经写好的内容是好的——多数可以直接挪到对应章节。请保持每个段落或列表项为一长行(模板注明 GitHub 会把单个换行渲染成 <br>,硬换行的文字会显示成窄列)。
正文更新后,维护者可以用 @qwen-code /triage 重新触发 triage 继续流程。
— Qwen Code · qwen3.8-max
已被后续 commit 取代,当前 head 需重新 review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
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.ts — no such file or directory; src/agents/runtime/workflow-orchestrator.test.ts — no such file or directory; src/agents/runtime/workflow-sandbox.test.ts — no such file or directory; src/agents/runtime/workflow-journal.test.ts — no such file or directory; src/tools/workflow/workflow.test.ts — no such file or directory; and 1 more.
— qwen3.8-max via Qwen Code /review (v0.21.10)
| } else if (typeof opts.workingDir === 'string' && opts.workingDir) { | ||
| // Caller-owned worktree: same rebind, no provisioning and no cleanup. |
There was a problem hiding this comment.
[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)
There was a problem hiding this comment.
已修复:主机侧 dispatch 现在也会拒绝空 workingDir,避免绕过 sandbox 时静默落回父工作区。验证证据:orchestrator 141/141、sandbox 151/151 通过;Core typecheck、ESLint、Prettier、diff check 通过。
| await expect( | ||
| sandbox.run(`return agent("x", { workingDir: 7 });`), | ||
| ).rejects.toThrow(/workingDir.*non-empty string/); |
There was a problem hiding this comment.
[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.
| 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)
There was a problem hiding this comment.
已修复:sandbox 回归现在同时覆盖非字符串和空字符串 workingDir。验证证据:workflow-sandbox 151/151 通过。
| expect( | ||
| resolveSubagentMaxTurns({ QWEN_CODE_WORKFLOW_AGENT_MAX_TURNS: '120' }), | ||
| ).toBe(120); |
There was a problem hiding this comment.
[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)
| it('foreground execute() completes with no interactive session or completion channel', async () => { | ||
| const registry = new WorkflowRunRegistry(); | ||
| const config = { |
There was a problem hiding this comment.
[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)
There was a problem hiding this comment.
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 进行,而单元层无法表达这一点;真正的端到端检查需要新的集成测试脚手架(通过 --prompt 在 yolo/auto 审批模式下、stdin 关闭、对着 mock 模型端点运行一次 Workflow 调用的打包 CLI——目前并不存在 Workflow 集成脚手架)。加入该脚手架会使本 PR 远超其原始意图,因此改为作为后续事项跟踪,而不是被静默丢弃。
| for (const k of [ | ||
| 'schema', | ||
| 'model', | ||
| 'isolation', | ||
| 'agentType', | ||
| 'workingDir', | ||
| ] as const) { |
There was a problem hiding this comment.
[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)
There was a problem hiding this comment.
已修复: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}`, |
There was a problem hiding this comment.
[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.
| `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)
There was a problem hiding this comment.
已修复: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 { |
There was a problem hiding this comment.
[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)
There was a problem hiding this comment.
已修复:目录重绑定会继承 base Config 的 customIgnoreFiles,并增加 .cursorignore 传递断言。验证证据:workflow-orchestrator 141/141 通过;Core typecheck 通过。
| it('refuses a path outside the repository', async () => { | ||
| const result = await resolveExternalWorktreeDir(config, '/elsewhere/tree'); |
There was a problem hiding this comment.
[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)
| * 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. |
There was a problem hiding this comment.
[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.
| * 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)
| const relToRepo = path.relative(realRepoRoot, realResolved); | ||
| if (relToRepo.startsWith('..') || path.isAbsolute(relToRepo)) { |
There was a problem hiding this comment.
[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):
| 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)
There was a problem hiding this comment.
已修复:containment 只拒绝精确 .. 或 ../ 路径段,不再误拒 ..hidden-wt,并增加回归。验证证据:worktree-pin 8/8 通过;Core typecheck 通过。
|
@qwen-code /takeover |
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 |
|
🔀 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 将重新运行。 |
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 into workflow/agent-working-dir
|
🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下: Review feedback round — PR #8972Commit: Findings and dispositionsFixed this round
Re-verified from the previous round (still holding in HEAD)
Deferred (not resolved — recorded reply posted on the thread)
Reviewer test-plan note ("no such file or directory" for six test files) — these were reviewer-side paths missing the Failed check analysis:
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
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.ts — no such file or directory; src/agents/runtime/workflow-orchestrator.test.ts — no such file or directory; src/agents/runtime/workflow-sandbox.test.ts — no such file or directory; src/agents/runtime/workflow-journal.test.ts — no such file or directory; src/tools/workflow/workflow.test.ts — no 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.ts — no such file or directory; src/agents/runtime/workflow-orchestrator.test.ts — no such file or directory; src/agents/runtime/workflow-sandbox.test.ts — no such file or directory; src/agents/runtime/workflow-journal.test.ts — no such file or directory; src/tools/workflow/workflow.test.ts — no such file or directory; and 1 more。
— qwen3.8-max via Qwen Code /review (v0.21.10)
| async getMainWorktreePath(): Promise<string | null> { | ||
| try { | ||
| const out = await ( | ||
| await this.getGit() | ||
| ).raw(['worktree', 'list', '--porcelain']); | ||
| const firstLine = out.split('\n', 1)[0]?.trim() ?? ''; |
There was a problem hiding this comment.
[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)
| * 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. |
There was a problem hiding this comment.
[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)
There was a problem hiding this comment.
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 中的投影断言仍是行为层面的拦截。
| 'cannot serve. Mutually exclusive with `isolation`. The path must live ' + | ||
| 'inside the repository and appear in `git worktree list`. ' + |
There was a problem hiding this comment.
[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.
| '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)
| expect(canonicalizeAgentOpts({ workingDir: 'wt' })).toBe( | ||
| JSON.stringify({ workingDir: 'wt' }), | ||
| ); | ||
| expect(a).not.toBe(b); |
There was a problem hiding this comment.
[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.
| 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)
| const repoRoot = | ||
| (await probe.getMainWorktreePath()) ?? | ||
| (await probe.getRepoTopLevel()) ?? | ||
| parentCwd; |
There was a problem hiding this comment.
[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 的说法(「消费者在坏锚点上会失败关闭、且权威登记检查从不使用该值」):消费者接受了,而登记检查经由 wtService 的 sourceRepoPath 确实使用了该锚点。
严重度定为 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)
|
🤖 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 #8972Commit: Failed check analysis:
|
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`).
|
🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下: Round summaryAddressed 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 Feedback points and decisions
Conflict notesNone — VerificationCommands actually run (results at the pushed commit
Environment note on the still-red 中文说明本轮摘要已处理自动化审查者第二轮行内发现中的 4 项,另有 1 项以记录在案的理由拒绝。全部改动限于 各反馈点及处理决定
冲突说明无—— 验证实际执行的命令(结果为推送提交
关于仍为红色的 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/模型 |
|
🔀 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
left a comment
There was a problem hiding this comment.
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.ts — no such file or directory; src/agents/runtime/workflow-orchestrator.test.ts — no such file or directory; src/agents/runtime/workflow-sandbox.test.ts — no such file or directory; src/agents/runtime/workflow-journal.test.ts — no such file or directory; src/tools/workflow/workflow.test.ts — no 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.ts — no such file or directory; src/agents/runtime/workflow-orchestrator.test.ts — no such file or directory; src/agents/runtime/workflow-sandbox.test.ts — no such file or directory; src/agents/runtime/workflow-journal.test.ts — no such file or directory; src/tools/workflow/workflow.test.ts — no such file or directory; and 1 more。
— qwen3.8-max via Qwen Code /review (v0.21.10)
| 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; |
There was a problem hiding this comment.
[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.
中文说明
第二轮加入的换行截断检测器可被绕过:当路径在内部换行之后的残余部分呈属性形状(detached、bare、locked、prunable、HEAD …、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)
| } else if (opts.workingDir !== undefined) { | ||
| if (typeof opts.workingDir !== 'string' || !opts.workingDir) { |
There was a problem hiding this comment.
[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 的两个入口对 workingDir 与 isolation 同时出现的处理不一致: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)
| 'agent() opts: `{ label?, phase?, schema?, model?, agentType?, isolation?, workingDir?, stallMs? }`. ' + | ||
| '`schema` (JSON Schema object): the subagent must deliver its result ' + |
There was a problem hiding this comment.
[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 故事的全部,只字未提 workingDir 与 stallMs;而本 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) { |
There was a problem hiding this comment.
[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.
| 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)
| const mainPath = firstLine.slice('worktree '.length).trim(); | ||
| return mainPath.length > 0 ? mainPath : null; |
There was a problem hiding this comment.
[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)
| const realRepoRoot = await fs.realpath(repoRoot).catch(() => repoRoot); | ||
| const realResolved = await fs | ||
| .realpath(resolvedPath) | ||
| .catch(() => resolvedPath); |
There was a problem hiding this comment.
[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)
| const repoRoot = | ||
| (await probe.getMainWorktreePath()) ?? | ||
| (await probe.getRepoTopLevel()) ?? |
There was a problem hiding this comment.
[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))) { |
There was a problem hiding this comment.
[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.
|
🤖 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 Rejection repairRoot cause: the round-3 commit added the degraded-anchor test Fix (one line): annotate the stub with the real service signature — The 8 inline findings — re-verified resolved in the preserved commitEach finding was re-checked against the code at HEAD this round (not assumed
Conflict notesNone — VerificationCommands actually run this round and their results (required checks per the
No settings source changed, so 中文说明轮次总结 — PR #8972 评审反馈(同轮验证修复)上一个提交 拒绝项修复根因: 第 3 轮提交新增了退化锚点测试( 修复(一行): 为桩标注真实服务签名—— 8 条行内发现 — 在保留提交中重新验证为已解决本轮逐条对照 HEAD 代码重新核查(而非采信上一轮的总结);8 条全部已在
冲突说明无 — 验证本轮实际运行的命令及结果(仓库门禁要求的检查):
未改动任何 settings 源文件,因此无需运行 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/模型 |
…g-dir # Conflicts: # packages/core/src/tools/agent/agent.ts
|
🤖 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 Feedback points and dispositions1. Base conflict with
|
|
🔀 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 将重新运行。 |
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix round summary — PR #8972 (address-review): no code changesThis round's only feedback was the failed Why the failed check is not caused by this PRPer-step conclusions for both failed runs of this PR (read from the public check-run API, no credentials):
Local verification — every locally reproducible part of the Test job passes on this branchCommands actually run on the current head (
Disposition and suggested next steps
中文说明Autofix 本轮总结 — PR #8972(address-review):无代码改动本轮唯一的反馈是失败的 为什么该失败检查与本 PR 无关本 PR 两次失败运行的逐步骤结论(读取自公开的 check-run API,未使用任何凭据):
本地验证 — Test 检查中所有可在本地复现的部分在本分支上全部通过在当前 head(
处理结论与建议的后续动作
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
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.ts — no such file or directory; src/agents/runtime/workflow-orchestrator.test.ts — no such file or directory; src/agents/runtime/workflow-sandbox.test.ts — no such file or directory; src/agents/runtime/workflow-journal.test.ts — no such file or directory; src/tools/workflow/workflow.test.ts — no 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.ts — no such file or directory; src/agents/runtime/workflow-orchestrator.test.ts — no such file or directory; src/agents/runtime/workflow-sandbox.test.ts — no such file or directory; src/agents/runtime/workflow-journal.test.ts — no such file or directory; src/tools/workflow/workflow.test.ts — no such file or directory; and 1 more。
— qwen3.8-max via Qwen Code /review (v0.21.10)
| '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. ' + |
There was a problem hiding this comment.
[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.
| '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/wt,sub-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 ' + |
There was a problem hiding this comment.
[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.
| '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)
| `${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\`.`, |
There was a problem hiding this comment.
[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()).
| `${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)
| const fromWorktree = new GitWorktreeService(wt); | ||
| expect(await fromWorktree.getMainWorktreePath()).toBe(repo); | ||
| expect(await fromWorktree.getRepoTopLevel()).toBe(wt); |
There was a problem hiding this comment.
[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.
| 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.ts,test-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)
| expect(result).toEqual({ | ||
| path: '/repo/.qwen/tmp/review-pr-7', | ||
| branch: 'pr-7', |
There was a problem hiding this comment.
[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.
| 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)
| expect(result).toMatchObject({ | ||
| path: '/repo/.qwen/tmp/review-pr-1-base', | ||
| }); |
There was a problem hiding this comment.
[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.
| 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)
| const result = await resolveExternalWorktreeDir(config, '/elsewhere/wt'); | ||
| expect(result).toEqual({ | ||
| path: '/elsewhere/wt', |
There was a problem hiding this comment.
[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.
| 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: '' }); |
There was a problem hiding this comment.
[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.
| 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)
| if (agentOpts.workingDir !== undefined) { | ||
| if (typeof agentOpts.workingDir !== 'string' || agentOpts.workingDir.trim().length === 0) { |
There was a problem hiding this comment.
[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).
| 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 宣称 stallMs 「0 关闭看门狗」,但与同为新选项的 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.
|
🤖 Addressed the latest review feedback (round 5/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 5/100 轮)。改动内容与我反驳保留之处如下: Round-4 feedback addressedAll nine inline findings are resolved in code (one commit, Critical — platform-dependent path assertions (R4-5 1/5–5/5)Accepted. Verified the mechanism first:
Suggestion —
|
…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).
|
🤖 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 dispositionsThis round's only actionable feedback was the failed check Failed check → root-caused from per-step evidence and fixed with a minimal deflake. Disposition: Addressed. Diagnosis (evidence, not guesswork)
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 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. VerificationCommands actually run on this branch (CI-equivalent env: isolated
Files changed this round
Commit: 中文说明Autofix 本轮总结 — PR #8972(address-review)反馈点与处置本轮唯一可执行的反馈是失败检查 失败检查 → 已凭逐步骤证据定位根因,并以最小 deflake 修复。 处置:已处理。 诊断(凭证据,不靠猜测)
修复(deflake 政策,允许的修复方式 #2「稳定计时/等待」)把固定深度的读取替换为对同一条件的有界等待:裸 注:同文件中还有两处姊妹断言使用同样的固定深度模式,但尚未观察到它们抖动;按最小作用域原则未触碰。 验证本轮在此分支上实际运行的命令(CI 等价环境:隔离
本轮改动的文件
提交: Deferred non-Critical feedbackCritical-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. ( 中文说明完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 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/模型 |
🖼️ web-shell visual previewRendered against a mock daemon (no real backend): the PR base vs this PR head 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 |
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix round: no action on PR #8972This round's feedback contains no actionable items, so no code changes were made and no commit was created. Triage summary
中文说明Autofix 轮次:PR #8972 无操作本轮反馈中没有任何可处理的项目,因此未修改任何代码,也未创建任何提交。 分诊摘要
Deferred non-Critical feedbackCritical-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. (
中文说明完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
doudouOUC
left a comment
There was a problem hiding this comment.
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.ts — no such file or directory; src/agents/runtime/workflow-orchestrator.test.ts — no such file or directory; src/agents/runtime/workflow-sandbox.test.ts — no such file or directory; src/agents/runtime/workflow-journal.test.ts — no such file or directory; src/tools/workflow/workflow.test.ts — no such file or directory.
中文说明
未探索到全部深度(达到工具调用预算):This PR #8972 adds workingDir support for workflow agents...:None — I completed the full scope.;chunk 5:无。。
Test Plan(非阻断):src/agents/worktree-pin.test.ts — no such file or directory; src/agents/runtime/workflow-orchestrator.test.ts — no such file or directory; src/agents/runtime/workflow-sandbox.test.ts — no such file or directory; src/agents/runtime/workflow-journal.test.ts — no such file or directory; src/tools/workflow/workflow.test.ts — no such file or directory。
— deepseek-v4-flash via Qwen Code /review (v0.21.10)
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix round summary — no action requiredNo 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.
2. The Test Plan note ("no such file or directory") is a false positive.
The note's paths are package-relative; running them from the repository root yields "no such file or directory". Run from 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)是程序性的,并非缺陷发现。
2. Test Plan 提示("no such file or directory",即"文件或目录不存在")是误报。
该提示中的路径是相对于包目录的;从仓库根目录直接运行才会报"文件或目录不存在"。在 3. 没有其他可处理项。 本轮没有行内评论、没有 issue 级评论、没有失败的检查,也没有持续失败的检查。当前处于仅处理 Critical 的模式,且本轮不存在任何 Critical 发现。 Deferred non-Critical feedbackCritical-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. ( 中文说明完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
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.ts — no such file or directory; src/agents/runtime/workflow-orchestrator.test.ts — no such file or directory; src/agents/runtime/workflow-sandbox.test.ts — no such file or directory; src/agents/runtime/workflow-journal.test.ts — no such file or directory; src/tools/workflow/workflow.test.ts — no 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.ts — no such file or directory; src/agents/runtime/workflow-orchestrator.test.ts — no such file or directory; src/agents/runtime/workflow-sandbox.test.ts — no such file or directory; src/agents/runtime/workflow-journal.test.ts — no such file or directory; src/tools/workflow/workflow.test.ts — no such file or directory; and 1 more。
— qwen3.8-max via Qwen Code /review (v0.21.10)
| { | ||
| "paths": ["packages/core/src/skills/**"], | ||
| "relatedPaths": ["packages/core/src/skills/**"], | ||
| "relatedPaths": ["packages/core/src/skills/*"], |
There was a problem hiding this comment.
[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.
| "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 () => { |
There was a problem hiding this comment.
[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 派发测试都是裸的——没有一个把 workingDir 与 model、agentType 或 schema 组合使用,因此重绑定只为一种 override 派发形态钉住;workingDir + schema 链(schema override 经原型链叠加在目录作用域 override 之上)是真实存在却未被测试的链。失败场景:若未来某次编辑把 workingDir 分支改为以「不存在 agentType/model」为条件(或把它移到某个 early return 之后),agent('x', { workingDir: 'wt', model: 'qwen3-max' }) 会静默地在父工作树中运行子 agent——这正是该选项要防止的失败——而现有两个测试都保持绿色。建议补一个 workingDir 与 model 组合的派发用例,同时断言 calls[0].config.model 与重绑定后的运行时目标目录。
— qwen3.8-max via Qwen Code /review (v0.21.10)
| describe('GitWorktreeService.getMainWorktreePath() (real git)', () => { | ||
| vi.setConfig({ testTimeout: 30000, hookTimeout: 30000 }); |
There was a problem hiding this comment.
[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.setConfig、tmpDirs、afterEach 清理、commitInitial 和 initRepo——共 31 行,经实测与同文件上方紧邻的 isRegisteredLinkedWorktree() describe 块逐字节相同。失败场景:仓库准备逻辑现在存在两份副本;未来对仓库准备的修复(新的 git 默认值、CI 凭证助手绕过、Windows 路径调整)若只应用于其中一个块,会静默让另一个块坏掉或变得 flaky,直到其中一个失败才可见。建议把 tmpDirs、afterEach、commitInitial、initRepo 提升到文件作用域供两个 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)."); |
There was a problem hiding this comment.
[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)."); |
There was a problem hiding this comment.
[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 秒看门狗下运行——正是该关卡注释所描述的失败。建议在 createProductionDispatch 中 resolveStallMs 之前于主机侧镜像该关卡,替换静默的 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. |
There was a problem hiding this comment.
[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.
| * 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)
|
🤖 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 commits were made and the working tree is unchanged. 中文说明Autofix 轮次总结 — 未采取任何操作(PR #8972)本轮未做任何更改。 未采取操作的原因
未创建任何提交,工作树保持不变。 Deferred non-Critical feedbackCritical-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. (
中文说明完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
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
|
🤖 Addressed the latest review feedback (round 7/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 7/100 轮)。改动内容与我反驳保留之处如下: Autofix round summary — merge
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
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.ts — no such file or directory; src/agents/runtime/workflow-orchestrator.test.ts — no such file or directory; src/agents/runtime/workflow-sandbox.test.ts — no such file or directory; src/agents/runtime/workflow-journal.test.ts — no such file or directory; src/tools/workflow/workflow.test.ts — no 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.ts — no such file or directory; src/agents/runtime/workflow-orchestrator.test.ts — no such file or directory; src/agents/runtime/workflow-sandbox.test.ts — no such file or directory; src/agents/runtime/workflow-journal.test.ts — no such file or directory; src/tools/workflow/workflow.test.ts — no 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))) { |
There was a problem hiding this comment.
[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.
| 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 毫秒触发计时器。已在本提交探针验证:runStallResilient 在 stallMs: 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)
| GitWorktreeService: vi | ||
| .fn() | ||
| .mockImplementation(() => stubs.current as unknown), |
There was a problem hiding this comment.
[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'); |
There was a problem hiding this comment.
[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 ` + |
There was a problem hiding this comment.
[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 add 或 git 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; |
There was a problem hiding this comment.
[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 () => { |
There was a problem hiding this comment.
[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 变成传给 runStallResilient 的 60000;PR 的真实接线传的是 0。
失败场景:未来对该三元表达式的一个 token 编辑会悄悄重新启用 DEFAULT_STALL_MS(60 秒):脚本作者明确声明不看管的派发会在 60 秒安静流式后被中止并最多重试 3 次,浪费 tokens,而所有现有测试保持绿色。本 PR 自己的 env 上限接线测试正是基于同样的理由存在(“resolver 层测试抓不到派发站点的回退”)。
— qwen3.8-max via Qwen Code /review (v0.21.11)
|
🤖 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.
The PR head is unchanged by this round. 中文说明Autofix 审查轮次:未执行任何操作(PR #8972)本轮没有可处理的反馈,因此未做任何代码改动,也未产生任何提交。
本轮未改变 PR 的 head 提交。 Deferred non-Critical feedbackCritical-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. (
中文说明已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
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 asworking_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
workingDirforces 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 includesworkingDir, 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 readsworking_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 anullelement, 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 isask, 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
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.tscovers 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.tstreats that resolver as a seam and asserts what the orchestrator does with its verdict: aworkingDirdispatch 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 of0/abc/2.5/0x10/1e3.src/agents/runtime/workflow-sandbox.test.tscovers the script-facing surface:workingDirreaches dispatch, a non-string is refused, andworkingDirtogether withisolationis 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.tsasserts two dispatches identical but forworkingDirderive different resume keys.src/tools/workflow/workflow.test.tsruns 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 withQWEN_CODE_ENABLE_WORKFLOWS=1whose script callsagent('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
Environment (optional)
Unit tests only (vitest, Node 22, Linux).
Risk & Scope
workingDirrebinds 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 readagents/worktree-pin.tsas 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.workingDiris additive and rejected in combination withisolation; 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 isworking_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 内部不得有任何地方去索取交互,否则前台调用会永远挂在一个没人能回答的确认框上。后台那一半已经用明确的错误拒绝掉了;前台这一半此前没有任何东西钉住。审阅者验证方案
如何验证
预期全部通过。本分支上为
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能到达派发层、非字符串被拒绝、workingDir与isolation同时出现时作为矛盾被拒绝而不是按优先级择一——如果脚本拿到一个无声的胜者,它会以为自己被隔离了而实际是被钉住,或者相反。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()选项和两个新的环境变量,均已写入工具描述与代码注释。测试环境
环境(可选)
仅单元测试(vitest,Node 22,Linux)。
风险与范围
workingDir会重新绑定子 agent 的工作区边界,所以校验是本 PR 中与安全相关的部分——它与 Agent 工具共享而非重新实现,正是为了两者不会漂移,但审阅者应把agents/worktree-pin.ts当作承重文件来读。抬高单个子 agent 的上限意味着一个 agent 可以比以前烧掉更多 token 与墙钟时间;默认值未变,两个覆盖都会被夹紧,运行级的 agent 数量上限与墙钟仍然约束整次运行。workingDir是增量的,且与isolation同时出现时会被拒绝;两个环境变量都是选择性启用;把校验器移入共享模块对 Agent 工具的行为没有任何改变,其错误文本逐字节一致,因为默认参数名就是working_dir。关联 Issue
Part of #8769.