Skip to content

feat(core): trust a generated-scripts root for workflow scriptPath loads - #9987

Merged
yiliang114 merged 11 commits into
QwenLM:mainfrom
qqqys:feat/workflow-generated-dir
Aug 26, 2026
Merged

feat(core): trust a generated-scripts root for workflow scriptPath loads#9987
yiliang114 merged 11 commits into
QwenLM:mainfrom
qqqys:feat/workflow-generated-dir

Conversation

@qqqys

@qqqys qqqys commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds a third trusted root for loading a workflow by path: <projectDir>/workflows/generated, beside the run snapshots and resume journals in the runtime directory. Workflow({scriptPath}) and the in-script workflow({scriptPath}) accept any file that resolves inside it, under the same realpath boundary check and symlinked-root refusal the saved-workflow directories already get. Nothing in it is a saved workflow: it is never enumerated for /<name> slash commands, and workflow('<name>') cannot reach it. Layout below the root is the writer's choice (per-session subdirectories are fine); the loader trusts the whole subtree. The Workflow tool's description also stops claiming that scriptPath accepts a path anywhere — it never did — and now names the three roots that are accepted.

Why it's needed

The loader only accepts scripts inside .qwen/workflows or ~/.qwen/workflows, and every .js file in those two directories is simultaneously a slash command in the user's session. So a tool that generates a workflow script for one run has no legitimate place to put it. The first attempt at emitting /review's fan-out as a workflow (#8943) ran straight into this: the script had to be written into .qwen/workflows, which turned every review into a permanent /qwen-review-<digest> command, which in turn required digest naming, a cleanup sweep, symlink guards, and writer/sweeper parity tests — several hundred lines and a recurring cluster of review findings, all to work around a missing runtime capability. A generated-scripts root that is loadable but not addressable removes the entire category: the emitting tool writes there, the model's call carries one path, and nothing leaks into the command namespace or the project tree.

Reviewer Test Plan

How to verify

  • Place a script at <projectDir>/workflows/generated/anything.js (the project dir is ~/.qwen/projects/<sanitized-cwd> by default, or under QWEN_RUNTIME_DIR; a subprocess reaches it as $QWEN_CODE_PROJECT_DIR/workflows/generated), then call the Workflow tool with that scriptPath. Expected: it runs. A nested path such as generated/s-<session>/fanout.js runs too.
  • Expected: the script does not appear as a slash command, and workflow('anything') from inside another workflow reports "no workflow with that name".
  • Expected refusals, each with the "outside the saved-workflow and generated-workflow directories" error. Create these under the project dir alongside the root (none of these paths exist in this repository — they are fixtures the tester creates): a file in a sibling directory that shares the root's prefix (<projectDir>/workflows/generated-evil/x.js), a file inside the root that is a symlink to somewhere outside, and any file behind a generated directory that is itself a symlink.
  • Unit tests cover each of the above: cd packages/core && npx vitest run src/agents/runtime/workflow-saved.test.ts src/config/storage.test.ts src/tools/workflow/workflow.test.ts — 3 files, 201 passed (6 new in the generated-root block, 1 new storage path test). src/agents/runtime/ as a whole: 16 files passed, 1 skipped; 765 passed, 6 skipped. (Both re-measured on head ff86990.)

Evidence (Before & After)

N/A — no user-visible change; behavior is pinned by the unit tests above.

Tested on

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

Environment (optional)

Unit tests only (vitest, Node 22).

Risk & Scope

  • Main risk or tradeoff: the trust boundary widens by one directory. It sits in the runtime dir (not the project tree, so a checked-in repo cannot plant a file there), and the existing realpath containment and symlinked-root refusal apply to it unchanged. Nothing writes there yet — this PR adds the root and the loader's trust; writers arrive in follow-ups.
  • Not validated / out of scope: no writer helper or cleanup policy for the generated root — a writer owns its own layout and lifetime. No change to the permission rule shape (Workflow(scriptPath:<path>) still pre-approves one exact path).
  • Breaking changes / migration notes: none. The refusal message text changed from "outside the saved-workflow directories" to "outside the saved-workflow and generated-workflow directories".

Linked Issues

Part of #8769 — the runtime prerequisite that lets a CLI-generated review fan-out be loaded without occupying the user's saved-workflow namespace. Follows #8971 and #8972.

中文说明

本 PR 做了什么

为按路径加载 workflow 增加第三个受信根目录:<projectDir>/workflows/generated,与 runtime 目录中的 run 快照和 resume journal 并列。Workflow({scriptPath}) 和脚本内的 workflow({scriptPath}) 接受解析到该目录内的任何文件,并沿用保存目录已有的 realpath 边界检查与软链根目录拒绝逻辑。其中的文件都不是"保存的 workflow":不会被枚举为 /<name> slash command,workflow('<name>') 也无法触达。根目录以下的布局由写入方决定(按 session 分子目录也可以),loader 信任整棵子树。Workflow 工具描述也不再声称 scriptPath 可以是任意路径——它从来不是——而是列出实际接受的三个根目录。

为什么需要

loader 只接受 .qwen/workflows~/.qwen/workflows 内的脚本,而这两个目录里的每个 .js 同时也是用户会话中的 slash command。因此,一个为单次运行生成 workflow 脚本的工具没有合法的落点。第一次尝试把 /review 的扇出以 workflow 形式发出(#8943)正是撞上了这一点:脚本只能写进 .qwen/workflows,于是每次 review 都变成一个永久的 /qwen-review-<digest> 命令,进而需要 digest 命名、清理扫描、软链守卫和写入/清理双方的对齐测试——几百行代码加上一簇反复出现的评审 finding,全是在绕一个 runtime 缺失的能力。一个"可加载但不可寻址"的生成脚本根目录消灭了整个类别:发出脚本的工具写到那里,模型的调用只带一个路径,不会有任何东西泄漏进命令命名空间或项目树。

评审验证计划

如何验证

  • <projectDir>/workflows/generated/anything.js 放一个脚本(project dir 默认是 ~/.qwen/projects/<sanitized-cwd>,或在 QWEN_RUNTIME_DIR 下;子进程可通过 $QWEN_CODE_PROJECT_DIR/workflows/generated 定位),然后以该 scriptPath 调用 Workflow 工具。预期:正常运行。嵌套路径如 generated/s-<session>/fanout.js 同样可运行。
  • 预期:该脚本不会出现在 slash command 中,在另一个 workflow 内调用 workflow('anything') 报 "no workflow with that name"。
  • 预期拒绝(错误信息均为 "outside the saved-workflow and generated-workflow directories")。以下路径需要验证者自己在 project dir 下创建,本仓库中并不存在:与根目录共享前缀的兄弟目录中的文件(<projectDir>/workflows/generated-evil/x.js)、根目录内软链到外部的文件、以及 generated 目录本身是软链时其后的任何文件。
  • 单元测试覆盖以上每一项:cd packages/core && npx vitest run src/agents/runtime/workflow-saved.test.ts src/config/storage.test.ts src/tools/workflow/workflow.test.ts —— 3 个文件、201 通过(generated-root 块新增 6 个,storage 路径测试新增 1 个)。整个 src/agents/runtime/:16 个文件通过、1 个跳过,765 通过、6 跳过。(均在 head ff86990 上重新实测。)

证据(前后对比)

N/A —— 无用户可见改动;行为由上述单元测试钉住。

测试平台

OS 状态
🍏 macOS ⚠️
🪟 Windows ⚠️
🐧 Linux

环境(可选)

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

风险与范围

  • 主要风险或取舍:信任边界多了一个目录。它位于 runtime 目录(不在项目树里,因此被 checkout 的仓库无法在其中植入文件),已有的 realpath 包含检查与软链根目录拒绝逻辑原样适用。目前没有任何写入方——本 PR 只增加根目录和 loader 的信任,写入方在后续 PR 中到来。
  • 未验证 / 范围之外:不提供生成目录的写入 helper 或清理策略——写入方自行负责布局与生命周期。权限规则形态不变(Workflow(scriptPath:<path>) 仍只预批准一个精确路径)。
  • 破坏性变更 / 迁移说明:无。拒绝信息文本由 "outside the saved-workflow directories" 改为 "outside the saved-workflow and generated-workflow directories"。

关联 Issue

Part of #8769 —— 让 CLI 生成的 review 扇出脚本可以被加载而不占用用户保存的 workflow 命名空间的 runtime 前置。承接 #8971#8972

https://claude.ai/code/session_017cUwuTey4APA8wAyAM6ScS

`Workflow({scriptPath})` and `workflow({scriptPath})` only load files that
resolve inside the two saved-workflow directories, and every file in those
directories is also a `/<name>` slash command. A tool that generates a
workflow script for a single run therefore had no place to put it: writing
into `.qwen/workflows` hands the user a permanent command for a run that is
already over, and any other path is refused by the loader.

Add `<projectDir>/workflows/generated` as a third trusted root for
`{scriptPath}` loads only. It lives in the runtime dir beside the run
snapshots and journals, is never enumerated by `listSavedWorkflows`, and
cannot be reached by `workflow('<name>')`. The same realpath boundary check
and symlinked-root refusal apply. The tool description stops claiming that
`scriptPath` accepts a path anywhere.

Part of QwenLM#8769.

Claude-Session: https://claude.ai/code/session_017cUwuTey4APA8wAyAM6ScS
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

⚠️ Deferred approval withheld — 1 PR CI workflow run(s) on d972ade did not finish green; see the updated table in the Stage 2 comment. Re-run @qwen-code /triage after fixes. finalize run

⚠️ 延迟审批已搁置 —— d972ade 有 1 个 PR CI workflow 未以绿色完成,详见 Stage 2 评论中已更新的表格。修复后可重新运行 @qwen-code /triage查看 finalize 运行

@qqqys

qqqys commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

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

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

  • Template: complete ✓
  • Problem: real and observed, not theoretical. The loader today only accepts a scriptPath inside the two saved scopes (.qwen/workflows, ~/.qwen/workflows) — verified in readWorkflowFileSecurely — and every .js in those dirs is simultaneously a slash command (listSavedWorkflows → the CLI's saved-workflow-loader). feat(review): dispatch Step 3A's fan-out from a generated workflow script, and route to it #8943, the first attempt at emitting the review fan-out as a workflow, closed unmerged precisely because a generated script had nowhere to land without becoming a permanent /qwen-review-<digest> command.
  • Direction: aligned. This is a runtime prerequisite named in Proposal: rebuild /review Step 3–5 orchestration on the workflow engine #8769 (open, roadmap/multi-agent, under active maintainer discussion there) and continues the series already merged via feat(core): write per-agent transcripts for workflow dispatches #8971 and feat(core): let a workflow agent pin a directory and outlive the default bounds #8972. No CHANGELOG anchor for a runtime-internal root, but the area is squarely in scope.
  • Size: core paths touched — 68 production lines (workflow-saved.ts +28/−7, storage.ts +16, workflow.ts +11/−6) vs 109 test lines. Well under the 500-line awareness threshold.
  • Approach: the scope feels right; I'd cut nothing. The "loadable but not addressable" split is drawn at exactly the right seam — only the boundary set of readWorkflowFileSecurely widens, while discovery and name-resolution keep using getSavedWorkflowDirs. The alternative (an exclusion naming convention inside .qwen/workflows) would keep polluting the project tree and risk leaking into command enumeration. The tool-description rewrite also fixes a pre-existing inaccuracy — scriptPath never actually accepted "a path anywhere".
  • Risk: no elevated risk signals — none of the changed files match the revert-correlated high-risk paths. The trust boundary does widen by one directory, but it sits in the runtime dir (a checked-in repo cannot plant files there) and inherits the existing realpath containment and symlinked-root refusal unchanged.

Moving on to code review. 🔍

中文说明

感谢贡献!

  • 模板:完整 ✓
  • 问题:真实且已被观测到,不是理论问题。当前 loader 只接受两个 saved 目录内的 scriptPath.qwen/workflows~/.qwen/workflows)——已在 readWorkflowFileSecurely 中核实——而这两个目录里的每个 .js 同时都是 slash command(listSavedWorkflows → CLI 的 saved-workflow-loader)。feat(review): dispatch Step 3A's fan-out from a generated workflow script, and route to it #8943(第一次把 review 扇出做成 workflow 的尝试)正是因为生成的脚本无处安放、只能变成永久的 /qwen-review-<digest> 命令而未合并关闭。
  • 方向:对齐。这是 Proposal: rebuild /review Step 3–5 orchestration on the workflow engine #8769(open,roadmap/multi-agent,维护者正在其中讨论)点名的 runtime 前置,承接已合并的 feat(core): write per-agent transcripts for workflow dispatches #8971feat(core): let a workflow agent pin a directory and outlive the default bounds #8972。runtime 内部根目录没有 CHANGELOG 锚点,但该领域明确在范围内。
  • 规模:触及核心路径——生产代码 68 行(workflow-saved.ts +28/−7、storage.ts +16、workflow.ts +11/−6),测试 109 行。远低于 500 行的关注阈值。
  • 方案:范围合理,没什么可砍的。"可加载但不可寻址"的切分位置正确——只放宽 readWorkflowFileSecurely 的边界集合,发现与按名解析继续使用 getSavedWorkflowDirs。替代方案(在 .qwen/workflows 里用命名约定排除)会持续污染项目树,且有泄漏进命令枚举的风险。工具描述的重写还修正了一个既有错误表述——scriptPath 从来不接受"任意路径"。
  • 风险:无升级风险信号——改动文件均未命中与 revert 相关的高风险路径。信任边界确实多了一个目录,但它位于 runtime 目录(被 checkout 的仓库无法植入文件),且原样沿用既有的 realpath 包含检查与软链根目录拒绝逻辑。

进入代码审查 🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

Before reading the diff, my independent proposal was: add a runtime-dir root (<projectDir>/workflows/generated, beside the run snapshots and journals), widen only the scriptPath boundary check to include it, and leave discovery/name-resolution untouched. That is exactly what this PR does — same placement, same seam. I found no simpler path it missed.

  • Boundary mechanicsreadWorkflowFileSecurely now draws its containment set from getWorkflowScriptRoots (both saved scopes + the generated root); the realpath check, the startsWith(dir + path.sep) containment, and the symlinked-root exclusion are the existing logic applied to one more root. No new code path, no relaxation. A missing root still contributes a boundary via the path.resolve fallback, same as an absent saved dir today.
  • AddressabilitylistSavedWorkflows (the CLI slash-command source) and workflow('<name>') resolution both keep using getSavedWorkflowDirs, and name lookup validates against WORKFLOW_NAME_PATTERN before joining, so a generated script is loadable by path but never a /<name> command and never name-reachable. Tests pin all three properties, including the empty "(none)" available-names case.
  • Coexistence with runs/journals<projectDir>/workflows/ already holds <runId>.json snapshots and <runId>/journal.jsonl journals; every consumer of that dir is gated on the .json suffix or the wf_<hex> run-id shape (listWorkflowSnapshots, pruneSnapshots, deleteWorkflowSnapshot), and journals are only ever opened by explicit getWorkflowRunJournalPath(runId). A generated/ subtree can't be misread as a run, pruned, or deleted.
  • Refusal paths — prefix-sibling (generated-evil/), file-symlink-out, and symlinked-root cases each have a dedicated test; containment is d + path.sep, so the sibling can't slip through a string prefix. Both assertions matching the old refusal message are updated, and nothing else in the repo references the old text.
  • Tool description — the rewrite fixes a pre-existing falsehood (the runtime section used to claim scriptPath "takes an absolute path to a script anywhere", which the boundary check never allowed) and now names the three real roots. workflow.test.ts anchors the description text, so the in-flight CI suite is the check that every anchor survived the rewrite.

No blockers, no AGENTS.md violations, nothing to cut.

Testing evidence — the PR's own CI

Unattended run: PR code is not built or executed here; the signal below is the PR's own CI on the reviewed commit, fetched via the API.

Final CI results for d972ade (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Test (ubuntu-latest, Node 22.x) ❌ failure
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
route ✅ success
Secret scan (TruffleHog) ✅ success
Serve A/B (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

The unit suite is still running on the reviewed commit — nothing red so far, but green is not yet attested; the table above updates in place once CI settles. The behavioral surface here is the loader's accept/refuse semantics, and the seven new unit tests pin it by construction: each builds files under the new root (or its refusal cases), so without the change they would throw or enumerate differently — a suite that passes identically with the diff removed is not what this is. The PR's test plan reports 194 passed across the touched suites locally; on this unattended path CI is the signal I can attest to, and it supersedes that report. Nothing user-visible changes (no TUI surface), so no real-scenario capture applies here.

中文说明

代码审查

看 diff 之前我的独立方案是:在 runtime 目录加一个根(<projectDir>/workflows/generated,与 run 快照、journal 并列),只放宽 scriptPath 的边界检查使其包含该根,发现/按名解析保持不动。这个 PR 正是这么做的——位置相同、切缝相同,我没有找到它遗漏的更简路径。

  • 边界机制readWorkflowFileSecurely 的包含集合改由 getWorkflowScriptRoots(两个 saved 目录 + generated 根)提供;realpath 检查、startsWith(dir + path.sep) 包含判断、软链根目录排除都是既有逻辑原样多作用一个根,没有新代码路径、没有放宽。根目录不存在时仍走 path.resolve 兜底,与今天缺失的 saved 目录行为一致。
  • 可寻址性listSavedWorkflows(CLI slash command 来源)与 workflow('<name>') 解析继续使用 getSavedWorkflowDirs,按名查找先过 WORKFLOW_NAME_PATTERN 校验再拼路径,因此生成脚本只能按路径加载,永远不会成为 /<name> 命令、也无法按名触达。三条性质都有测试钉住,包括可用列表为空时的 "(none)"。
  • 与 runs/journals 共存<projectDir>/workflows/ 下已有 <runId>.json 快照与 <runId>/journal.jsonl journal;该目录的所有消费者都以 .json 后缀或 wf_<hex> run id 形状为门槛(listWorkflowSnapshotspruneSnapshotsdeleteWorkflowSnapshot),journal 也只通过 getWorkflowRunJournalPath(runId) 显式打开。generated/ 子树不会被误认为 run、不会被清理或删除。
  • 拒绝路径:前缀兄弟目录(generated-evil/)、文件软链逃逸、软链根目录三种情况各有专门测试;包含判断用 d + path.sep,兄弟目录不会因字符串前缀蒙混。旧拒绝信息的两处断言均已更新,全仓无其他地方引用旧文案。
  • 工具描述:重写修正了一个既有错误表述(运行时部分曾声称 scriptPath 接受"任意路径",而边界检查从来不允许),现在列出真实的三个根。workflow.test.ts 对描述文本做了锚点断言,进行中的 CI 套件就是检验重写是否保住所有锚点的关口。

无阻塞项、无 AGENTS.md 违规、无可删减。

测试证据 —— 本 PR 自己的 CI

无人值守运行:此处不构建、不执行 PR 代码;上方信号来自 API 拉取的、被审提交上 PR 自己的 CI。单元测试仍在被审提交上运行,目前无失败,最终结论以表格就地更新为准。行为面(loader 的接受/拒绝语义)由 7 个新单元测试按构造钉死:每个测试都在新根下造文件(或构造其拒绝场景),缺了这个改动它们必然抛错或枚举不同。PR 测试计划自报本地 194 通过,无人值守路径上以 CI 为准。无用户可见改动(无 TUI 面),不适用真实场景捕获。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — clean, minimal, well-pinned change; the only reservation is structural (a root with no writer yet), and CI has not settled.

Stepping back: this is the third staged prerequisite in the #8769 series, and it reads like the two that already merged — small, precisely scoped, test-heavy. My independent proposal (widen only the scriptPath boundary, leave discovery untouched, put the root in the runtime dir) is exactly what landed, and I couldn't find a simpler seam. The security-sensitive part — widening script-load trust — is handled the conservative way: one more entry in the existing containment set, with each refusal path (prefix sibling, symlink-out, symlinked root) pinned by a dedicated test. The "loadable but never addressable" invariant is the whole point of the PR, and it is tested from both sides (not listed, not name-resolvable). Six months from now this reads as a clean module with a doc comment that explains the why.

The honest reservation: nothing writes to the new root yet, so its value arrives with the follow-up writer(s). That is a series-staging observation, not a code defect — the same pattern landed in #8971/#8972, and settling the loader's semantics (including the refusal message) before writers build on it is the safer order. If the #8769 direction stalls, this root is dead weight — but it is 68 production lines in one module, cheap to remove, and the parent proposal is active.

Verdict: approve. The unit suites (Qwen Code CI, Serve A/B) are still running on the reviewed commit, so approval is deferred until CI lands green on d972ade968906a143d5d81c5d106b6aa333ae09a.

中文说明

信心:4/5 —— 干净、最小化、测试钉得牢;唯一的保留意见是结构性的(根目录尚无写入方),且 CI 尚未定局。

退一步看:这是 #8769 系列的第三个阶段性前置,风格与已合并的两个一致——小、边界精确、测试密集。我的独立方案(只放宽 scriptPath 边界、发现逻辑不动、根放 runtime 目录)与落地的完全一致,且没找到更简单的切缝。安全敏感部分(放宽脚本加载信任)用了保守做法:只在既有包含集合里多加一项,每条拒绝路径(前缀兄弟目录、软链逃逸、软链根目录)各有专门测试钉住。"可加载但永不可寻址"是本 PR 的核心不变量,正反两面都有测试(不被枚举、不可按名解析)。六个月后再看,这是一个带"为什么"注释的干净模块。

诚实的保留意见:目前还没有任何写入方写这个新根,价值要等后续写入方落地。这是系列节奏的观察,不是代码缺陷——#8971/#8972 用了同样的模式,且先定 loader 语义(包括拒绝文案)再让写入方构建其上,是更稳的顺序。如果 #8769 方向停滞,这个根就成了死代码——但它只是一个模块里的 68 行生产代码,移除成本低,且父提案仍在推进。

结论:批准。单元测试(Qwen Code CIServe A/B)仍在被审提交上运行,批准推迟到 CI 在 d972ade968906a143d5d81c5d106b6aa333ae09a 上全绿之后,届时由 finalize 工作流代为执行。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

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

No response changes against the PR base across 12 scenario(s).

Qwen Code · serve A/B

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. Suggestions are inline.

Not explored to full depth (tool budget reached): "agent 4": none — no check was cut short.; "agent 6c": none — all checks above ran to completion..

Test Plan (not a blocker): workflows/generated-evil/x.jsno such file or directory; 194 passed — this review observed 21452, 1702, 23916, 1659, 601, 4226, 627 passed; 784 passed — this review observed 21452, 1702, 23916, 1659, 601, 4226, 627 passed.

中文说明

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

未探索到全部深度(达到工具调用预算):"agent 4"none — no check was cut short."agent 6c"none — all checks above ran to completion.

Test Plan(非阻断):workflows/generated-evil/x.jsno such file or directory; 194 passed — this review observed 21452, 1702, 23916, 1659, 601, 4226, 627 passed; 784 passed — this review observed 21452, 1702, 23916, 1659, 601, 4226, 627 passed

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

Comment on lines +186 to +187
'`~/.qwen/workflows`) or the generated-scripts root ' +
'(`<projectDir>/workflows/generated`) — any other path is refused. ' +

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The model-facing description names the new trusted root as <projectDir>/workflows/generated, but <projectDir> is never defined in model-visible text, while the same paragraph uses <projectRoot> for the workspace (<projectRoot>/.qwen/workflows). The actual trusted root is Storage.getProjectDir() — the per-project runtime session-storage directory, outside the workspace tree. The same ambiguity appears in the Runtime paragraph further down. The concrete cost: a writer reading this description literally — the exact consumer this PR exists for — writes <workspace>/workflows/generated/fanout.js and hands it to Workflow({scriptPath}); the loader refuses it, the run fails, and the refusal is misleading because the refused path literally contains workflows/generated. A plausible workaround (dropping the script into .qwen/workflows/ instead) pollutes the slash-command namespace — the exact outcome this feature exists to prevent. Enforcement is code-side, so this ambiguity causes refusal, never a trust bypass. Suggested wording (apply the same to the Runtime paragraph):

'(`<projectDir>/workflows/generated`) — any other path is refused. ' +
→
'(`$QWEN_CODE_PROJECT_DIR/workflows/generated` — the per-project runtime dir, not the project tree) — any other path is refused. ' +
中文说明

模型可见的描述将新的受信根目录写作 <projectDir>/workflows/generated,但 <projectDir> 在模型可见文本中从未定义,而同一段落用 <projectRoot> 表示工作区(<projectRoot>/.qwen/workflows)。实际的受信根是 Storage.getProjectDir() —— 每个项目的 runtime 会话存储目录,位于项目树之外。下方的 Runtime 段落存在同样的歧义。具体代价:按字面理解该描述的写入方(正是本 PR 面向的消费者)会写出 <workspace>/workflows/generated/fanout.js 并交给 Workflow({scriptPath}),loader 将拒绝加载,运行失败,且该拒绝具有误导性——被拒路径本身包含 workflows/generated。一个可能的绕路做法(把脚本放进 .qwen/workflows/)会污染 slash command 命名空间——这正是本功能要避免的结果。边界检查在代码侧,因此该歧义只会导致拒绝,不会造成信任绕过。建议措辞如下(同样应用于 Runtime 段落)。

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

Comment on lines +691 to +693
* out of the project tree. Layout below the root is the writer's; the
* loader trusts the whole subtree. A subprocess reaches it as
* `$QWEN_CODE_PROJECT_DIR/workflows/generated`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The subprocess reachability contract this docstring introduces — $QWEN_CODE_PROJECT_DIR + workflows/generated equals Storage.getGeneratedWorkflowsDir() — is asserted by no test; each side is tested only in isolation (shellContextEnv.test.ts asserts literal passthrough, and the new tests derive the path from the same Storage method the loader uses). If either side moves — e.g. the generated root is relocated under getProjectTempDir(), or the session-project-dir export changes — a tool that emits its one-run script to the documented path gets refused at load time, the workflow run fails, and the suite stays green because both halves still pass their own tests. Consider one effect-shaped assertion composing the two halves: register a session project dir, resolve getShellContextEnvVars(), write a script to path.join(env['QWEN_CODE_PROJECT_DIR']!, 'workflows', 'generated', 'x.js'), and assert resolveSavedWorkflowScript({ scriptPath: <that path> }, config) loads it.

中文说明

该 docstring 引入的子进程可达性契约——$QWEN_CODE_PROJECT_DIR + workflows/generated 等于 Storage.getGeneratedWorkflowsDir()——没有任何测试断言。两侧目前只被各自独立测试(shellContextEnv.test.ts 只断言字面量透传;新增测试从 loader 所用的同一个 Storage 方法推导路径)。若任一侧变动——例如 generated 根目录被迁到 getProjectTempDir() 下,或 session-project-dir 的导出方式改变——按文档路径写入一次性脚本的工具会在加载时被拒绝,workflow 运行失败,而测试套件仍为绿色(因为两侧各自的测试仍然通过)。建议增加一个端到端断言:注册 session 项目目录,解析 getShellContextEnvVars(),将脚本写入 path.join(env['QWEN_CODE_PROJECT_DIR']!, 'workflows', 'generated', 'x.js'),并断言 resolveSavedWorkflowScript({ scriptPath: <该路径> }, config) 能加载它。

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

if (!inside) {
throw new Error(
`refusing to load a workflow file outside the saved-workflow directories: '${filePath}'.`,
`refusing to load a workflow file outside the saved-workflow and generated-workflow directories: '${filePath}'.`,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The refusal message names the directory kinds but not the roots actually checked, and this diff adds the first root whose location is not common knowledge — it resolves through the runtime-base fallback chain (pinned context > QWEN_RUNTIME_DIR > configured runtime base > global Qwen dir > os.tmpdir()/.qwen). A debugger facing this refusal sees only the offending path, checks the plausible-looking <projectRoot>/workflows/generated (absent — the root lives in the runtime dir), and must then read Storage source to locate the real root. dirs is already in scope at the throw site, so including it is free.

Suggested change
`refusing to load a workflow file outside the saved-workflow and generated-workflow directories: '${filePath}'.`,
`refusing to load a workflow file outside the workflow script roots (checked: ${dirs.join(', ')}): '${filePath}'.`,
中文说明

拒绝信息只说明了目录类别,没有列出实际检查的根目录;而本 diff 新增了第一个位置不属于常识的根目录——它经由 runtime base 回退链解析(pinned context > QWEN_RUNTIME_DIR > 配置的 runtime base > 全局 Qwen 目录 > os.tmpdir()/.qwen)。调试者遇到该拒绝时只能看到被拒路径,会先检查貌似合理的 <projectRoot>/workflows/generated(并不存在——根目录在 runtime 目录中),随后不得不阅读 Storage 源码才能定位真正的根目录。dirs 在抛出点作用域内已存在,将其包含进来没有成本。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

中文说明

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. Suggestions are inline.

Test Plan (not a blocker): workflows/generated-evil/x.jsno such file or directory; 194 passed — this review observed 21485, 1702, 23930, 1659, 601, 4226, 627 passed; 784 passed — this review observed 21485, 1702, 23930, 1659, 601, 4226, 627 passed.

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

  • packages/core/src/config/storage.ts:695 — [review] consumer half (trust root + model-facing contract) landed with no producer/writer in the tree
  • packages/core/src/tools/workflow/workflow.ts:184 — [probe] generated-root scriptPath gets a strictly weaker consent gate than an inline script (path-only dialog, pre-approval offered)
  • packages/core/src/agents/runtime/workflow-saved.ts:102 — [review] nested workflow({scriptPath}) route reaches the generated root with no consent dialog
  • packages/core/src/config/storage.ts:695 — [probe] generated root keyed on the lossy sanitizeCwd id — path-colliding projects share one trust root
中文说明

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

Test Plan(非阻断):workflows/generated-evil/x.jsno such file or directory; 194 passed — this review observed 21485, 1702, 23930, 1659, 601, 4226, 627 passed; 784 passed — this review observed 21485, 1702, 23930, 1659, 601, 4226, 627 passed

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

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

'this run hands you its path the same way. The file must resolve ' +
'inside a saved-workflow directory (`.qwen/workflows`, ' +
'`~/.qwen/workflows`) or the generated-scripts root ' +
'(`<projectDir>/workflows/generated`) — any other path is refused. ' +

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R1-1: The model-facing description names the new trusted root as <projectDir>/workflows/generated, but <projectDir> is never defined in model-visible text, while the same paragraph uses <projectRoot> for the workspace (<projectRoot>/.qwen/workflows). The actual trusted root is Storage.getProjectDir() — the per-project runtime session-storage directory, outside the workspace tree. The same ambiguity appears in the Runtime paragraph further down.

Re-checked at the round-2 head da095844 — the description text is unchanged since round 1, so this still stands. The concrete cost: a writer reading this description literally — the exact consumer this PR exists for — writes <workspace>/workflows/generated/fanout.js and hands it to Workflow({scriptPath}); the loader refuses it, the run fails, and the refusal is misleading because the refused path literally contains workflows/generated. A plausible workaround (dropping the script into .qwen/workflows/ instead) pollutes the slash-command namespace — the exact outcome this feature exists to prevent. Enforcement is code-side, so this ambiguity causes refusal, never a trust bypass. Verified by probe this round (scratch tree at the reviewed commit):

projectRoot : /tmp/probe-proj-c89w82
generatedDir: /tmp/probe-home-lI3laY/.qwen/projects/-tmp-probe-proj-c89w82/workflows/generated
generatedDir inside projectRoot? false

Suggested wording (apply the same to the Runtime paragraph):

'(`<projectDir>/workflows/generated`) — any other path is refused. ' +
→
'(`$QWEN_CODE_PROJECT_DIR/workflows/generated` — the per-project runtime dir, not the project tree) — any other path is refused. ' +
中文说明

模型可见的描述将新的受信根目录写作 <projectDir>/workflows/generated,但 <projectDir> 在模型可见文本中从未定义,而同一段落用 <projectRoot> 表示工作区(<projectRoot>/.qwen/workflows)。实际的受信根是 Storage.getProjectDir() —— 每个项目的 runtime 会话存储目录,位于项目树之外。下方的 Runtime 段落存在同样的歧义。

已在 round 2 头提交 da095844 复查——描述文本自 round 1 以来未变,该问题仍然存在。具体代价:按字面理解该描述的写入方(正是本 PR 面向的消费者)会写出 <workspace>/workflows/generated/fanout.js 并交给 Workflow({scriptPath}),loader 将拒绝加载,运行失败,且该拒绝具有误导性——被拒路径本身包含 workflows/generated。一个可能的绕路做法(把脚本放进 .qwen/workflows/)会污染 slash command 命名空间——这正是本功能要避免的结果。边界检查在代码侧,因此该歧义只会导致拒绝,不会造成信任绕过。本轮已通过探针验证(在受审提交的独立工作树中运行)。

建议措辞如下(同样应用于 Runtime 段落)。

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

* business in the user's command namespace, and the runtime dir keeps it
* out of the project tree. Layout below the root is the writer's; the
* loader trusts the whole subtree. A subprocess reaches it as
* `$QWEN_CODE_PROJECT_DIR/workflows/generated`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R1-2: The subprocess reachability contract this docstring introduces — $QWEN_CODE_PROJECT_DIR + workflows/generated equals Storage.getGeneratedWorkflowsDir() — is asserted by no test; each side is tested only in isolation (shellContextEnv.test.ts asserts literal passthrough, and the new tests derive the path from the same Storage method the loader uses).

Re-checked at the round-2 head da095844 — the code is unchanged since round 1, and a fresh sweep confirms the gap still exists: the literal workflows/generated appears in zero test files, and no test ties QWEN_CODE_PROJECT_DIR to the generated dir. If either side moves — e.g. the generated root is relocated under getProjectTempDir(), or the session-project-dir export changes — a tool that emits its one-run script to the documented path gets refused at load time, the workflow run fails, and the suite stays green because both halves still pass their own tests. Consider one effect-shaped assertion composing the two halves: register a session project dir, resolve getShellContextEnvVars(), write a script to path.join(env['QWEN_CODE_PROJECT_DIR']!, 'workflows', 'generated', 'x.js'), and assert resolveSavedWorkflowScript({ scriptPath: <that path> }, config) loads it.

中文说明

该 docstring 引入的子进程可达性契约——$QWEN_CODE_PROJECT_DIR + workflows/generated 等于 Storage.getGeneratedWorkflowsDir()——没有任何测试断言。两侧目前只被各自独立测试(shellContextEnv.test.ts 只断言字面量透传;新增测试从 loader 所用的同一个 Storage 方法推导路径)。

已在 round 2 头提交 da095844 复查——代码自 round 1 以来未变,新一轮扫描确认缺口仍在:workflows/generated 字面量在测试文件中零出现,也没有测试将 QWEN_CODE_PROJECT_DIR 与 generated 目录关联。若任一侧变动——例如 generated 根目录被迁到 getProjectTempDir() 下,或 session-project-dir 的导出方式改变——按文档路径写入一次性脚本的工具会在加载时被拒绝,workflow 运行失败,而测试套件仍为绿色(因为两侧各自的测试仍然通过)。建议增加一个端到端断言:注册 session 项目目录,解析 getShellContextEnvVars(),将脚本写入 path.join(env['QWEN_CODE_PROJECT_DIR']!, 'workflows', 'generated', 'x.js'),并断言 resolveSavedWorkflowScript({ scriptPath: <该路径> }, config) 能加载它。

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

if (!inside) {
throw new Error(
`refusing to load a workflow file outside the saved-workflow directories: '${filePath}'.`,
`refusing to load a workflow file outside the saved-workflow and generated-workflow directories: '${filePath}'.`,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R1-3: The refusal message names the directory kinds but not the roots actually checked, and this diff adds the first root whose location is not common knowledge — it resolves through the runtime-base fallback chain (pinned context > QWEN_RUNTIME_DIR > configured runtime base > global Qwen dir > os.tmpdir()/.qwen). A debugger facing this refusal sees only the offending path, checks the plausible-looking <projectRoot>/workflows/generated (absent — the root lives in the runtime dir), and must then read Storage source to locate the real root. dirs is already in scope at the throw site, so including it is free.

Re-checked at the round-2 head da095844 — the message is unchanged since round 1, so this still stands; a probe this round reproduced the refusal and confirmed the message names none of the three checked roots (fix-flip verified: appending the roots flips the check to true).

Suggested change
`refusing to load a workflow file outside the saved-workflow and generated-workflow directories: '${filePath}'.`,
`refusing to load a workflow file outside the workflow script roots (checked: ${dirs.join(', ')}): '${filePath}'.`,
中文说明

拒绝信息只说明了目录类别,没有列出实际检查的根目录;而本 diff 新增了第一个位置不属于常识的根目录——它经由 runtime base 回退链解析(pinned context > QWEN_RUNTIME_DIR > 配置的 runtime base > 全局 Qwen 目录 > os.tmpdir()/.qwen)。调试者遇到该拒绝时只能看到被拒路径,会先检查貌似合理的 <projectRoot>/workflows/generated(并不存在——根目录在 runtime 目录中),随后不得不阅读 Storage 源码才能定位真正的根目录。dirs 在抛出点作用域内已存在,将其包含进来没有成本。

已在 round 2 头提交 da095844 复查——拒绝信息自 round 1 以来未变,该问题仍然存在;本轮探针复现了该拒绝,并确认信息未列出三个被检查根目录中的任何一个(修复翻转已验证:将根目录追加进信息后检查变为 true)。

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

@qwen-code-dev-bot

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

Copy link
Copy Markdown
Collaborator

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

中文说明

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

…roots (QwenLM#9987)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Round summary

All three open suggestions (R1-1, R1-2, R1-3 — each raised in review rounds 1 and 2) are addressed in one commit. No Critical or Request-changes findings were open. No conflict resolution was needed (--conflict false; origin/main was not merged).

Feedback points

R1-1 — Model-facing description names the trusted root as <projectDir>/workflows/generated (rc:3850827134, rc:3851997488) — Implemented

The claim checks out: <projectDir> is never defined in model-visible text while the same paragraphs use <projectRoot> for the workspace, and the real root is Storage.getProjectDir() — a per-project runtime session-storage dir outside the workspace tree. A writer following the description literally would land on a refused path. Fixed exactly as suggested in both places:

  • scriptPath parameter description: ($QWEN_CODE_PROJECT_DIR/workflows/generated — the per-project runtime dir, not the project tree) — any other path is refused.
  • Runtime paragraph: the generated-scripts root ($QWEN_CODE_PROJECT_DIR/workflows/generated — the per-project runtime dir, not the project tree — where a tool emitting a one-run script writes it; never a slash command, never resolvable by name)

Text-only change; no behavioral test pins tool-description prose in this codebase, so no witness test was added (deliberate, per Simplicity First).

R1-2 — The $QWEN_CODE_PROJECT_DIR subprocess reachability contract is asserted by no test (rc:3850827148, rc:3851997499) — Implemented

Added the suggested composed, effect-shaped assertion in workflow-saved.test.ts (new resolveSavedWorkflowScript — subprocess reachability contract describe): mirror Config at session start by registering the storage project dir under a session id, resolve getShellContextEnvVars() inside that session's ALS frame, write a script to the LITERAL path $QWEN_CODE_PROJECT_DIR/workflows/generated/x.js, and assert resolveSavedWorkflowScript({ scriptPath }) loads it. Literal path segments on purpose — the test must fail if either half of the contract moves.

Mutation probes (see Verification): relocating the generated root ('generated''generated2' in Storage.getGeneratedWorkflowsDir) fails exactly this test; dropping the QWEN_CODE_PROJECT_DIR export in getShellContextEnvVars also fails exactly this test. Both halves are witnessed.

R1-3 — Refusal message names directory kinds but not the roots actually checked (rc:3850827158, rc:3851997504) — Implemented

Changed the throw site in readWorkflowFileSecurely to the suggested wording: refusing to load a workflow file outside the workflow script roots (checked: ${dirs.join(', ')}): '${filePath}'.dirs was already in scope, zero cost. Updated the five refusal-message regexes in the tests and strengthened the refuses a sibling of the generated root test to assert the message contains (checked: and the actual generated root path, so the roots-inclusion itself is witnessed (mutation probe below).

Review-body deferred items (round-2 review rv:5017760871) — untouched

The round-2 review body lists four items under "Deferred under the convergence posture — recorded, not requested in this round" (no producer/writer in tree, consent-gate asymmetry, nested workflow({scriptPath}) route, lossy sanitizeCwd keying). As marked, they were not requested this round and no code change was made for them; they remain recorded in the review thread.

Informational notes — no action required

  • "Not explored to full depth (tool budget reached)" — the reviewer's own answers state no check was cut short.
  • Test Plan number mismatches — reviewer-marked "not a blocker"; the plan text lives in the PR body, which this flow does not edit.
  • serve daemon A/B (ic:5406957133): ✅ no response changes against the PR base across 12 scenarios.

Failed check: Test (ubuntu-latest Node 22.x) — diagnosed, not reproducible

No GitHub credentials are available in this flow, so the CI log itself could not be fetched; diagnosis is from local reproduction instead.

  1. Running the packages/core suite in this runner's ambient environment fails, but every failure is fully explained by runner artifacts, none in files this PR touches:
    • 3 storage.test.ts failures: this shell exports QWEN_HOME as a BARE directory (the harness's isolated home, not ending in .qwen), while those tests hardcode os.homedir()/.qwen. With QWEN_HOME unset they pass (101/101). CI sets no QWEN_HOME.
    • 102 further failures with QWEN_HOME unset: EACCES: permission denied, mkdir '/home/github-runner/.qwen' — the runner's real home is not writable from the sandbox. CI overrides HOME to a writable ${{ runner.temp }}/qwen-ci-home for exactly this.
    • 7 editor.test.ts failures: this shell exports SANDBOX=qwen-code-…, which allowEditorTypeInSandbox reads to refuse GUI editors. CI sets no SANDBOX.
  2. Under a CI-equivalent environment (fresh writable HOME, no SANDBOX, no QWEN_HOME), the full packages/core suite at this round's commit is completely green: 602 files / 21,561 tests passed, 0 failed — exactly the reviewer's observed round-2 count at the same head, plus this round's one new test.
  3. The round-2 automated review independently observed all suites passing at the same head SHA (da095844): 21485, 1702, 23930, 1659, 601, 4226, 627 passed.

Conclusion: no evidence ties the red check to this PR's changes (which touch only a model-facing description string, an error-message string, and test additions). If the failure persists on the workflow's independent re-run of CI, it is environmental or in machinery this flow may not modify.

Changes

  • packages/core/src/tools/workflow/workflow.ts — R1-1 wording in the scriptPath parameter description and the Runtime paragraph.
  • packages/core/src/agents/runtime/workflow-saved.ts — R1-3 refusal message names the checked roots.
  • packages/core/src/agents/runtime/workflow-saved.test.ts — R1-2 composed contract test; refusal-regex updates; strengthened sibling-refusal assertion (R1-3 witness).

Verification

Commands actually run (from the repository root unless noted), at commit 3a555a182f:

  • npm run build — passed (exit 0)
  • npm run typecheck — passed (exit 0)
  • npm run lint — passed (exit 0)
  • Focused vitest (cd packages/core && npx vitest run src/agents/runtime/workflow-saved.test.ts src/config/storage.test.ts src/services/shellContextEnv.test.ts src/tools/workflow/workflow.test.ts, CI-equivalent env) — 4 files, 232 tests passed
  • Full packages/core suite (CI-equivalent env) — 602 files, 21,561 tests passed, 0 failed (1 file skipped, 10 tests skipped, as on base)
  • Mutation probe R1-3: temporarily restored the old refusal message → workflow-saved.test.ts flipped to 5 failed / 35 passed; restored the fix → 40/40 green
  • Mutation probe R1-2 (loader half): temporarily getWorkflowRunsDir(), 'generated2' → exactly the new contract test failed (1 failed / 39 passed); restored → 40/40 green
  • Mutation probe R1-2 (producer half): temporarily blanked the QWEN_CODE_PROJECT_DIR export in getShellContextEnvVars → exactly the new contract test failed; restored → green (git status confirms both probe files are unmodified)
  • R1-1 is a text-only wording change with no behavioral guard, so it has no mutation probe by design
  • Not run: integration tests (the touched behavior is fully exercised by the unit suites; no bundled-CLI-only path changed), npm run generate:settings-schema (no settings source touched)
中文说明

本轮摘要

三个未决建议(R1-1、R1-2、R1-3 —— 分别在第 1 轮与第 2 轮评审中提出)已在一个提交中全部处理。没有未决的 Critical 或 Request-changes 级别发现。无需解决冲突(--conflict false;未合并 origin/main)。

反馈条目

R1-1 —— 模型可见描述将受信根写作 <projectDir>/workflows/generated(rc:3850827134、rc:3851997488)—— 已实现

该问题属实:<projectDir> 在模型可见文本中从未定义,而同一段落用 <projectRoot> 表示工作区;实际根目录是 Storage.getProjectDir() —— 位于工作区树之外的每项目 runtime 会话存储目录。按字面理解该描述的写入方会落在一个被拒绝的路径上。已按建议在两处修复:

  • scriptPath 参数描述:($QWEN_CODE_PROJECT_DIR/workflows/generated — the per-project runtime dir, not the project tree) — any other path is refused.
  • Runtime 段落:the generated-scripts root ($QWEN_CODE_PROJECT_DIR/workflows/generated — the per-project runtime dir, not the project tree — where a tool emitting a one-run script writes it; never a slash command, never resolvable by name)

纯文本修改;本代码库没有为工具描述文本编写锁定测试的惯例,因此未添加见证测试(遵循 Simplicity First 原则的有意决定)。

R1-2 —— $QWEN_CODE_PROJECT_DIR 子进程可达性契约没有任何测试断言(rc:3850827148、rc:3851997499)—— 已实现

workflow-saved.test.ts 中新增了建议的组合式效果断言(新的 resolveSavedWorkflowScript — subprocess reachability contract describe):模拟 Config 在会话启动时的行为,将 storage 项目目录注册到会话 id 下,在该会话的 ALS 帧内解析 getShellContextEnvVars(),向字面路径 $QWEN_CODE_PROJECT_DIR/workflows/generated/x.js 写入脚本,并断言 resolveSavedWorkflowScript({ scriptPath }) 能加载它。刻意使用字面路径段 —— 契约任一侧变动时该测试必须失败。

变异探针(见 Verification):移动 generated 根目录(Storage.getGeneratedWorkflowsDir'generated''generated2')恰好使该测试失败;移除 getShellContextEnvVars 中的 QWEN_CODE_PROJECT_DIR 导出也恰好使该测试失败。契约两侧均有见证。

R1-3 —— 拒绝信息只说明目录类别、未列出实际检查的根目录(rc:3850827158、rc:3851997504)—— 已实现

readWorkflowFileSecurely 的抛出点改为建议的措辞:refusing to load a workflow file outside the workflow script roots (checked: ${dirs.join(', ')}): '${filePath}'. —— dirs 本就在作用域内,零成本。更新了测试中五处拒绝信息的正则,并加强了 refuses a sibling of the generated root 测试,断言信息包含 (checked: 以及实际的 generated 根目录路径,使"列出根目录"这一行为本身有见证(见下方变异探针)。

评审正文中的延后条目(第 2 轮评审 rv:5017760871)—— 未处理

第 2 轮评审正文在 "Deferred under the convergence posture — recorded, not requested in this round"(收敛姿态下延后 —— 已记录,本轮不要求)标题下列出四个条目(树中无生产者/写入方、同意门槛不对称、嵌套 workflow({scriptPath}) 路由、有损 sanitizeCwd 键控)。按其标注,本轮不要求修改,未做代码变更;它们仍记录在评审线程中。

信息性备注 —— 无需处理

  • "Not explored to full depth (tool budget reached)" —— 评审者自己的回答表明没有检查被中途切断。
  • Test Plan 中的数字不一致 —— 评审者标注"非阻断";计划文本位于 PR 正文中,本流程不编辑。
  • serve daemon A/B(ic:5406957133):✅ 与 PR base 相比 12 个场景均无响应变化。

失败检查:Test (ubuntu-latest Node 22.x) —— 已诊断,无法复现

本流程中没有可用的 GitHub 凭据,无法获取 CI 日志本身;诊断基于本地复现。

  1. 在本 runner 的环境变量下运行 packages/core 测试套件会失败,但每个失败都能完全用 runner 环境因素解释,且都不在本 PR 触碰的文件中:
    • 3 个 storage.test.ts 失败:本 shell 将 QWEN_HOME 导出为裸目录(隔离用的 home,不以 .qwen 结尾),而这些测试硬编码 os.homedir()/.qwen。取消 QWEN_HOME 后全部通过(101/101)。CI 不设置 QWEN_HOME
    • 取消 QWEN_HOME 后又出现 102 个失败:EACCES: permission denied, mkdir '/home/github-runner/.qwen' —— 沙箱中不可写入 runner 的真实 home。CI 正是为此将 HOME 覆盖为可写的 ${{ runner.temp }}/qwen-ci-home
    • 7 个 editor.test.ts 失败:本 shell 导出了 SANDBOX=qwen-code-…allowEditorTypeInSandbox 据此拒绝 GUI 编辑器。CI 不设置 SANDBOX
  2. 在 CI 等效环境(全新可写 HOME,无 SANDBOX、无 QWEN_HOME)下,本轮提交的 packages/core 完整套件全绿:602 个文件 / 21,561 个测试通过,0 失败 —— 与评审者在同一 head 上观察到的第 2 轮计数完全一致,再加上本轮新增的 1 个测试。
  3. 第 2 轮自动评审在同一 head SHA(da095844)上独立观察到所有套件通过:21485, 1702, 23930, 1659, 601, 4226, 627 passed

结论:没有证据将该红色检查与本 PR 的变更(仅涉及模型可见描述字符串、错误信息字符串与测试新增)关联起来。若该失败在工作流独立重跑的 CI 中仍然存在,则属于环境因素或本流程不可修改的机制。

变更内容

  • packages/core/src/tools/workflow/workflow.ts —— R1-1:scriptPath 参数描述与 Runtime 段落措辞。
  • packages/core/src/agents/runtime/workflow-saved.ts —— R1-3:拒绝信息列出被检查的根目录。
  • packages/core/src/agents/runtime/workflow-saved.test.ts —— R1-2:组合式契约测试;拒绝正则更新;加强的 sibling 拒绝断言(R1-3 见证)。

验证

实际执行的命令(如无说明均在仓库根目录),位于提交 3a555a182f

  • npm run build —— 通过(exit 0)
  • npm run typecheck —— 通过(exit 0)
  • npm run lint —— 通过(exit 0)
  • 定向 vitest(cd packages/core && npx vitest run src/agents/runtime/workflow-saved.test.ts src/config/storage.test.ts src/services/shellContextEnv.test.ts src/tools/workflow/workflow.test.ts,CI 等效环境)—— 4 个文件,232 个测试通过
  • packages/core 完整套件(CI 等效环境)—— 602 个文件,21,561 个测试通过,0 失败(1 个文件跳过、10 个测试跳过,与 base 一致)
  • 变异探针 R1-3:临时恢复旧拒绝信息 → workflow-saved.test.ts 翻转为 5 失败 / 35 通过;恢复修复 → 40/40 全绿
  • 变异探针 R1-2(loader 侧):临时改为 getWorkflowRunsDir(), 'generated2' → 恰好新契约测试失败(1 失败 / 39 通过);恢复 → 40/40 全绿
  • 变异探针 R1-2(生产者侧):临时移除 getShellContextEnvVars 中的 QWEN_CODE_PROJECT_DIR 导出 → 恰好新契约测试失败;恢复 → 全绿(git status 确认两个探针文件均未被修改)
  • R1-1 为纯文本措辞修改、无行为守卫,因此按设计不做变异探针
  • 未执行:集成测试(所触碰的行为已由单元测试套件完整覆盖;未改动仅捆绑 CLI 可见的路径)、npm run generate:settings-schema(未触碰 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/模型 qwen3.8-max

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. Suggestions are inline.

Test Plan (not a blocker): workflows/generated-evil/x.jsno such file or directory; 194 passed — this review observed 21486, 1702, 23927, 1659, 601, 4226, 627 passed; 784 passed — this review observed 21486, 1702, 23927, 1659, 601, 4226, 627 passed.

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

  • packages/core/src/agents/runtime/workflow-saved.ts:102 — [probe] generated-root scripts are model/tool-authored but inherit scriptPath's path-only consent and grantable always-allow rule — consent granted for one file's content silently tra…

Convergence: round 3 posted 2 inline comment(s), 2 of them reported for the first time; the previous round posted 3 (0 new). Findings keep coming back to the same files: packages/core/src/agents/runtime/workflow-saved.ts (findings in round 1; 1 more now); packages/core/src/tools/workflow/workflow.ts (findings in round 1; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. No Critical finding is open on this round, so merging and moving the remaining Suggestion threads to a follow-up issue is available as an ending — a merged pull request cannot diverge further. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

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

Test Plan(非阻断):workflows/generated-evil/x.jsno such file or directory; 194 passed — this review observed 21486, 1702, 23927, 1659, 601, 4226, 627 passed; 784 passed — this review observed 21486, 1702, 23927, 1659, 601, 4226, 627 passed

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

收敛情况:第 3 轮发布了 2 条行内评论,其中 2 条是首次提出;上一轮发布了 3 条(其中 0 条首次提出)。发现反复回到同一批文件:packages/core/src/agents/runtime/workflow-saved.ts(第 1 轮已出过发现,本轮又有 1 条);packages/core/src/tools/workflow/workflow.ts(第 1 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。本轮没有未决的 Critical,因此"合入后把剩余 Suggestion 线程转到后续 issue"是一个可选的结束方式——已合入的 PR 不会继续发散。(仅为观察——本轮评审未因此扣留任何内容。)

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

**Runtime** — see the \`script\` parameter for the detailed authoring contract.

\`phase(title)\`, \`log(msg)\`, \`agent(prompt, opts?)\`, \`parallel(thunks)\`, \`pipeline(items, ...stages)\`, \`workflow(nameOrRef, args?)\`, plus the \`args\` and \`budget\` globals. \`workflow()\` runs a saved workflow inline under this run's caps and nests one level only — a workflow reached through \`workflow()\` cannot call \`workflow()\` itself, and doing so throws. Saved workflows are \`<name>.js\` files under \`<projectRoot>/.qwen/workflows\` (project scope, also surfaced as \`/<name>\` slash commands) or \`~/.qwen/workflows\` (user scope, lower precedence when both define the same name); \`workflow('<name>')\` resolves against those two directories, while \`scriptPath\` takes an absolute path to a script anywhere. Default \`max(1, min(16, cpus-2))\` agents in flight per run (\`${MAX_WORKFLOW_CONCURRENCY_ENV}\`), up to ${DEFAULT_MAX_AGENTS_PER_RUN} agents total (\`${MAX_WORKFLOW_AGENTS_ENV}\`), under a 30-minute wall-clock cap per run (\`QWEN_CODE_MAX_WORKFLOW_SECONDS\`) — a fan-out near the agent cap will not fit inside the default cap. Each subagent attempt is separately capped at ${DEFAULT_WORKFLOW_SUBAGENT_MAX_TURNS} turns (\`${WORKFLOW_SUBAGENT_MAX_TURNS_ENV}\`) and ${DEFAULT_WORKFLOW_SUBAGENT_MAX_TIME_MINUTES} minutes (\`${WORKFLOW_SUBAGENT_MAX_MINUTES_ENV}\`) — an attempt that hits either becomes \`null\` in \`parallel()\`/\`pipeline()\`, indistinguishable from a missing agent, so raise them for legitimately long work. A per-run output-token cap may also be in effect: read \`budget.total\` (\`null\` = uncapped) before committing to a large fan-out, because once the cap is reached every further \`agent()\` call is refused — a bare sequential \`await agent()\` sees the rejection, while inside \`parallel()\`/\`pipeline()\` the refused slot becomes \`null\` and the script keeps running on partial results. Per-call \`agent({ schema, agentType, model, isolation: 'worktree', workingDir, stallMs })\` covers structured-output contracts, declarative-agent selection, model override, git-worktree-isolated subagents, pinning an agent to a caller-owned worktree, and the no-progress stall watchdog (\`stallMs: 0\` disables it). \`resumeFromRunId\` resumes a prior run — agent() calls whose rolling prefix-hash matches the journal are served from cache for the longest unchanged prefix. Runs appear in the background-tasks view and the \`/workflows\` dialog (live phase tree, token usage, cooperative pause/resume, cancel); \`run_in_background: true\` returns a run handle immediately in the interactive TUI and delivers completion through the conversation. Scripts run in a node:vm sandbox with no filesystem or shell access — all I/O happens through the spawned agents.
\`phase(title)\`, \`log(msg)\`, \`agent(prompt, opts?)\`, \`parallel(thunks)\`, \`pipeline(items, ...stages)\`, \`workflow(nameOrRef, args?)\`, plus the \`args\` and \`budget\` globals. \`workflow()\` runs a saved workflow inline under this run's caps and nests one level only — a workflow reached through \`workflow()\` cannot call \`workflow()\` itself, and doing so throws. Saved workflows are \`<name>.js\` files under \`<projectRoot>/.qwen/workflows\` (project scope, also surfaced as \`/<name>\` slash commands) or \`~/.qwen/workflows\` (user scope, lower precedence when both define the same name); \`workflow('<name>')\` resolves against those two directories, while \`scriptPath\` takes an absolute path to a script inside either of them or inside the generated-scripts root (\`$QWEN_CODE_PROJECT_DIR/workflows/generated\` — the per-project runtime dir, not the project tree — where a tool emitting a one-run script writes it; never a slash command, never resolvable by name); a path outside those roots is refused. Default \`max(1, min(16, cpus-2))\` agents in flight per run (\`${MAX_WORKFLOW_CONCURRENCY_ENV}\`), up to ${DEFAULT_MAX_AGENTS_PER_RUN} agents total (\`${MAX_WORKFLOW_AGENTS_ENV}\`), under a 30-minute wall-clock cap per run (\`QWEN_CODE_MAX_WORKFLOW_SECONDS\`) — a fan-out near the agent cap will not fit inside the default cap. Each subagent attempt is separately capped at ${DEFAULT_WORKFLOW_SUBAGENT_MAX_TURNS} turns (\`${WORKFLOW_SUBAGENT_MAX_TURNS_ENV}\`) and ${DEFAULT_WORKFLOW_SUBAGENT_MAX_TIME_MINUTES} minutes (\`${WORKFLOW_SUBAGENT_MAX_MINUTES_ENV}\`) — an attempt that hits either becomes \`null\` in \`parallel()\`/\`pipeline()\`, indistinguishable from a missing agent, so raise them for legitimately long work. A per-run output-token cap may also be in effect: read \`budget.total\` (\`null\` = uncapped) before committing to a large fan-out, because once the cap is reached every further \`agent()\` call is refused — a bare sequential \`await agent()\` sees the rejection, while inside \`parallel()\`/\`pipeline()\` the refused slot becomes \`null\` and the script keeps running on partial results. Per-call \`agent({ schema, agentType, model, isolation: 'worktree', workingDir, stallMs })\` covers structured-output contracts, declarative-agent selection, model override, git-worktree-isolated subagents, pinning an agent to a caller-owned worktree, and the no-progress stall watchdog (\`stallMs: 0\` disables it). \`resumeFromRunId\` resumes a prior run — agent() calls whose rolling prefix-hash matches the journal are served from cache for the longest unchanged prefix. Runs appear in the background-tasks view and the \`/workflows\` dialog (live phase tree, token usage, cooperative pause/resume, cancel); \`run_in_background: true\` returns a run handle immediately in the interactive TUI and delivers completion through the conversation. Scripts run in a node:vm sandbox with no filesystem or shell access — all I/O happens through the spawned agents.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The approval/activity surface still labels every scriptPath run as a saved workflow: resolveMeta() returns null whenever script is absent, so buildConfirmationPrompt prints Saved workflow: <path> and getDescription() prints Run saved workflow (<basename>) — for generated-root scripts too. That presents a throwaway generated artifact as a persistent user-saved command, the opposite of the distinction this same description draws ("never a slash command, never resolvable by name"): a user who sees "Saved workflow: …/workflows/generated/x.js" approves (or pre-approves the path rule) under a misidentification of what the artifact is. Verified by probe at this commit: a generated-root scriptPath renders Run saved workflow (fanout-1a2b3c.js) / Saved workflow: …/workflows/generated/fanout-1a2b3c.js, byte-identical to a real saved workflow's labels. Consider a neutral label for scriptPath runs without saved-workflow meta — e.g. Workflow script: <path> / Run workflow script (<basename>) in getDescription() and buildConfirmationPrompt, or detect the generated root via Storage.getGeneratedWorkflowsDir() and print Generated workflow script: <path>.

中文说明

审批/活动界面仍然把每一次 scriptPath 运行标注为"已保存的 workflow":只要 script 缺省,resolveMeta() 就返回 null,于是 buildConfirmationPrompt 打印 Saved workflow: <path>getDescription() 打印 Run saved workflow (<basename>) —— 对 generated 根目录下的脚本也是如此。这会把一次性的生成产物呈现为用户持久保存的命令,与这段描述自己所划的界限("never a slash command, never resolvable by name")正好相反:用户看到 "Saved workflow: …/workflows/generated/x.js" 时,是在对产物身份的误认下批准(或预批准该路径规则)。已在本次提交上用探针验证:generated 根的 scriptPath 渲染出 Run saved workflow (fanout-1a2b3c.js) / Saved workflow: …/workflows/generated/fanout-1a2b3c.js,与真正的已保存 workflow 标签逐字节相同。建议对没有 saved-workflow 元信息的 scriptPath 运行改用中性标签——例如在 getDescription()buildConfirmationPrompt 中使用 Workflow script: <path> / Run workflow script (<basename>),或通过 Storage.getGeneratedWorkflowsDir() 识别 generated 根并打印 Generated workflow script: <path>

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

if (!inside) {
throw new Error(
`refusing to load a workflow file outside the saved-workflow directories: '${filePath}'.`,
`refusing to load a workflow file outside the workflow script roots (checked: ${dirs.join(', ')}): '${filePath}'.`,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The new "(checked: …)" list can omit one of the three roots: a root that is itself a symlink maps to null inside readWorkflowFileSecurely's Promise.all and is dropped by the filter before dirs — and the message — are built. Verified by probe at this commit: with the generated root symlinked (<runtime>/workflows/generated -> /elsewhere, the exact case the new "refuses a symlinked generated root" test exercises) the refusal reads outside the workflow script roots (checked: <project>/.qwen/workflows, <user>/.qwen/workflows) — the generated root absent — while the non-symlinked control lists all three. A debugger hitting this refusal concludes the loader never considered the generated root and "fixes" things by moving the file into .qwen/workflows, when the actionable cause is the symlinked root itself, which the message never hints at — defeating the stated point of listing the checked roots. Keep refused-but-considered roots visible, e.g. collect the symlinked ones separately instead of discarding them in the filter, and mark them in the message: (checked: <project>/.qwen/workflows, <user>/.qwen/workflows; refused symlinked root: <runtime>/workflows/generated).

中文说明

新的 "(checked: …)" 列表可能漏掉三个根目录之一:如果某个根本身是软链接,它会在 readWorkflowFileSecurelyPromise.all 中被映射为 null,并在 dirs(以及这条错误信息)构建之前被 filter 丢弃。已在本次提交上用探针验证:当 generated 根是软链接时(<runtime>/workflows/generated -> /elsewhere,正是新增测试 "refuses a symlinked generated root" 覆盖的场景),拒绝信息为 outside the workflow script roots (checked: <project>/.qwen/workflows, <user>/.qwen/workflows) —— generated 根不在其中——而非软链接的对照组会列出全部三个根。调试者看到这条拒绝信息会认为 loader 从未考虑过 generated 根,于是把文件挪进 .qwen/workflows 来"修复",而真正可处置的原因是软链接根目录本身——信息对此毫无提示,这恰恰落空了"列出被检查的根"这一修改的初衷。建议保留"被考虑但被拒绝"的根目录:不要把软链接根在 filter 中丢弃,而是单独收集,并在信息中标注,例如:(checked: <project>/.qwen/workflows, <user>/.qwen/workflows; refused symlinked root: <runtime>/workflows/generated)

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

中文说明

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully. The review pipeline failed before a review could be posted. A transient error is retried automatically; if you are seeing this, retry with @qwen-code /review. See workflow logs.

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Round 4 review

Reviewed at 98863f0 (current head, a merge of upstream/main into the feature branch). Key changes since round 3 (at 3a555a182f): only a merge commit — no new code changes since the round-3 review.

Previous findings status

R1-1 (Suggestion)workflow.ts description named <projectDir> (undefined in model-visible text). Fixed in autofix round 1 at 3a555a182f: now names $QWEN_CODE_PROJECT_DIR/workflows/generated with a clarifying note.

R1-2 (Suggestion) — No test asserted the $QWEN_CODE_PROJECT_DIR subprocess reachability contract. Fixed in autofix round 1: new composed contract test witnesses both halves of the contract.

R1-3 (Suggestion) — Refusal message named directory kinds but not the actual roots checked. Fixed in autofix round 1: now lists the checked roots inline.

R3-1 (Suggestion, still open)workflow.ts:852 — Approval/activity surface still labels every scriptPath run as a "saved workflow" even when loaded from the generated root. The consumption side (activity entry, consent dialog) hasn't been updated to distinguish the generated root. Deferred under convergence posture — the PR only adds the loader; the consumer-writer pairing arrives in a follow-up.

R3-2 (Suggestion, still open)workflow-saved.ts:190 — The (checked: …) list can omit a root when that root is a symlink (the isSymlinkedDir filter drops it silently). The error message shows the remaining roots without explaining the omission. Minor: the user sees (checked: /a, /b) and may wonder why /c isn't checked. Adding a note like (symlinked-root /c: excluded from check) would clarify.

New findings (round 4)

No new Criticals, no new Suggestions. The code is clean, the merge commit introduces no conflicts, and the behavior is well-pinned by tests.

Limitations of this round

  • No worktree could be created (git fetch blocked by getaddrinfo on this Windows machine). Review is based on the diff from gh pr diff.
  • No build, typecheck, or test run was performed.
  • Round 4 (this review) is a diff-only pass.

Convergence: 0 Criticals open, 2 Suggestions open (both from round 3, neither a blocker). Merging and moving the remaining Suggestion threads to a follow-up issue is available as an ending.

— via Qwen Code /review

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Round 5 review (second opinion)

Reviewed at 98863f0 — head unchanged since round 4. Independent re-review pass.

CI state changed since round 4 — base-induced, not this PR

Both Test jobs are red on this head, but neither failure is in this PR's code path:

  • ubuntu: permission-manager.test.ts (resolveToolName exhaustiveness over the report_findings alias) — the branch last merged main at 12:51, and main itself carried that inconsistency until a6d30ebc ("fix(core): register report_findings in the permission alias table") landed at 15:50, after this PR's CI ran. The next update-branch merge clears it.
  • windows: daemon-git-worktree-guard* and dws-event-stream failures — windows-lane breakage unrelated to this change.

This PR's own tests are green on both platforms: workflow-saved.test.ts 40/40 and storage.test.ts 101/101 on ubuntu, same on windows (including the new symlink-escape tests).

Previous findings status

  • R1-1, R1-2, R1-3 — fixed in autofix round 1 (3a555a182f); re-verified at head.
  • R3-1 (Suggestion, still open) — approval/activity surfaces still label generated-root loads as "saved workflow". For whoever takes it: the family is getDescription() ("Run saved workflow (…)") and the confirmation body ("Saved workflow: …"), plus the P7b interface comment and the XOR validation message in workflow.ts.
  • R3-2 (Suggestion, still open) — a symlinked root is silently dropped from the (checked: …) refusal list.

Independent checks added this round

  • The run-artifact consumers of the workflow runs dir (workflow-snapshot.ts) cannot collide with the new generated/ subdirectory: snapshot listing filters .json files, and every recursive delete is gated on the ^wf_[0-9a-f]+$ run-id shape.
  • The subprocess reachability contract holds in production, not just in the new test: Config.initialize registers storage.getProjectDir() and getShellContextEnvVars reads it back.
  • Slash-command discovery and name resolution never touch the generated root, and scriptPath approval grants are scoped to the exact path, so a grant for one generated script does not extend to any other file under the root.
  • No other code, tests, or docs reference the old refusal message.

Verdict

No Criticals, no new Suggestions. 0 Criticals / 2 Suggestions open (both from round 3, neither a blocker).

Diff-only pass again this round: git fetch remains blocked on this Windows machine, so no local worktree/build/test — the CI logs above stand in as test evidence.

— via Qwen Code /review

@qqqys

qqqys commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /review

@github-actions

Copy link
Copy Markdown
Contributor

Qwen Code review request accepted. Review is queued in workflow run.

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully. The review pipeline failed before a review could be posted. A transient error is retried automatically; if you are seeing this, retry with @qwen-code /review. See workflow logs.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round summary — PR #9987

Commit: 9254a2c58b on feat/workflow-generated-dir (additive; no history rewrite, no conflict — --conflict false, head already carries the latest upstream/main merge).

Feedback points and dispositions

rc:3852905908 — R3-1 (Suggestion, workflow.ts:852): Resolved in code

Claim: every scriptPath run is labeled "saved workflow" on the approval/activity surface, including generated-root scripts, so a throwaway generated artifact is approved under a saved-workflow identity.

Reproduced by probe on the pre-round code (source-blind verification): a generated-root scriptPath rendered Run saved workflow (fanout-1a2b3c.js) / Saved workflow: …/workflows/generated/fanout-1a2b3c.js, byte-identical to a real saved workflow's labels.

Fix (the finding's second option — detect the generated root, keeping the accurate "saved" label for real saved workflows):

  • New isGeneratedWorkflowScriptPath() classifier (lexical segment-prefix match against config.storage.getGeneratedWorkflowsDir(); documented as label-only — the security boundary remains the loader's realpath check).
  • getDescription() now renders Run generated workflow script (<basename>) vs Run saved workflow (<basename>).
  • buildConfirmationPrompt() now renders Generated workflow script: <path> vs Saved workflow: <path> (receives the config from getConfirmationDetails).
  • The same family named in round 5 is aligned: the P7b interface comment and the XOR validation message no longer call every scriptPath "a saved workflow file".

Witnessed by two new tests pinning both surfaces in both directions (generated path → generated labels and no "Saved workflow" substring; saved-root path → saved labels).

rc:3852905915 — R3-2 (Suggestion, workflow-saved.ts:190): Resolved in code

Claim: a root that is itself a symlink is silently dropped from the (checked: …) refusal list, so the message hides the actionable cause.

Reproduced by probe on the pre-round code: with the generated root symlinked, the refusal listed only the other two roots, exactly as the finding describes.

Fix: readWorkflowFileSecurely now keeps refused-but-considered roots and appends them: (checked: a, b; refused symlinked root: c) (pluralizes roots when >1). With no symlinked roots the message is byte-identical to before, so the existing root-enumeration assertions are unaffected. The refusal logic itself is unchanged.

Witnessed by extended assertions in both symlinked-root tests: the generated-root case pins the full (checked: …; refused symlinked root: …) shape, and the project-root case pins the refused-root note.

rv:5018818583 — review body "Test Plan" note (not a blocker): Answered, no code change

  • workflows/generated-evil/x.jsno such file or directory: that path is a per-test temp fixture created under the OS tmp dir by refuses a sibling of the generated root (no prefix match) and deleted in the test's cleanup; it never exists in the repo tree, so running it as a file from the checkout cannot work. The test itself is green on this head.
  • 194 passed / 784 passed vs the reviewer's observed counts: those numbers in earlier round reports describe the focused vitest runs of those rounds, not a full-package run, which is why they don't line up with the reviewer's observation. The authoritative current numbers are in ## Verification below.

ic:5411510978 — review pipeline fallback comment: No action

Transient review-pipeline failure ("retry automatically"); the review itself landed separately. Nothing to address in code.

Failed checks (precheck-pr, authorize, ack-review-request, delay-automatic-review, resolve-pr, review-pr, fallback-comment — all CANCELLED): No action

These are the review pipeline's own jobs, not code checks; the "Still-red checks" list is empty.

CI state (per round 5 analysis, re-checked locally)

The ubuntu Test failure (permission-manager.test.ts over the report_findings alias) was base-induced and fixed by a6d30ebc on main, which this branch already merged (aef0af85d3). Locally, permission-manager.test.ts passes 411/411 on this head — the next CI run should clear it. The windows-lane failures (daemon-git-worktree-guard*, dws-event-stream) are unrelated breakage per round 5.

Mutation probes (guard witnesses)

  1. Forced isGeneratedWorkflowScriptPath() to falselabels a generated-root scriptPath … FAILED (restored → green).
  2. Removed the refusedNote suffix from the refusal message → both symlinked-root tests FAILED (restored → green).

Verification

  • npm run build — passed
  • npm run typecheck — passed (exit 0, no errors)
  • npm run lint — passed (eslint . --ext .ts,.tsx && eslint integration-tests, exit 0)
  • npx prettier --check on the four changed files — passed (after a format fixup)
  • cd packages/core && npx vitest run src/tools/workflow/workflow.test.ts src/agents/runtime/workflow-saved.test.ts — 96 passed (56 + 40), post-commit re-run green
  • cd packages/core && env -u QWEN_HOME npx vitest run src/config/storage.test.ts src/permissions/permission-manager.test.ts (together with the two files above in one run) — storage 101 passed, permission-manager 411 passed; combined focused run 608 passed
    • Note: storage.test.ts fails 3 assertions when run WITH this runner's QWEN_HOME env var, because those tests compare against os.homedir()-based ~/.qwen paths while QWEN_HOME redirects the global dir; with the variable removed it is 101/101. This is a runner-environment artifact of the autofix sandbox (the variable is not set in CI), not a code defect — this diff does not touch storage.ts or its tests.
  • Settings schema: no settings source changed → generate:settings-schema not required. No bundled-CLI-only behavior changed → no integration run required.
中文说明

Autofix 轮次总结 — PR #9987

提交:9254a2c58b,位于 feat/workflow-generated-dir 分支(追加式提交;不改写历史,无冲突 —— --conflict false,HEAD 已包含最新的 upstream/main 合并)。

反馈点及处理

rc:3852905908 — R3-1(Suggestion,workflow.ts:852):已在代码中解决

论点:每一次 scriptPath 运行在审批/活动界面上都被标注为 "saved workflow",generated 根目录下的脚本也不例外,导致一次性的生成产物以"已保存 workflow"的身份被批准。

已在本轮前的代码上用探针复现(来源无关验证):generated 根的 scriptPath 渲染出 Run saved workflow (fanout-1a2b3c.js) / Saved workflow: …/workflows/generated/fanout-1a2b3c.js,与真正已保存 workflow 的标签逐字节相同。

修复(采用该发现的第二个选项 —— 识别 generated 根,同时为真正的已保存 workflow 保留准确的 "saved" 标签):

  • 新增 isGeneratedWorkflowScriptPath() 分类器(对 config.storage.getGeneratedWorkflowsDir() 做词法上的整段前缀匹配;已注明仅用于标签 —— 安全边界仍是 loader 的 realpath 检查)。
  • getDescription() 现在渲染 Run generated workflow script (<basename>)Run saved workflow (<basename>)
  • buildConfirmationPrompt() 现在渲染 Generated workflow script: <path>Saved workflow: <path>(由 getConfirmationDetails 传入 config)。
  • 第 5 轮点名的同族文本也已对齐:P7b 接口注释与 XOR 校验错误信息不再把所有 scriptPath 称为 "a saved workflow file"。

由两个新测试见证,分别钉住两个界面在两个方向上的行为(generated 路径 → generated 标签且不含 "Saved workflow" 子串;saved 根路径 → saved 标签)。

rc:3852905915 — R3-2(Suggestion,workflow-saved.ts:190):已在代码中解决

论点:本身就是软链接的根目录会被悄悄从 (checked: …) 拒绝列表中丢弃,使错误信息隐藏了真正可处置的原因。

已在本轮前的代码上用探针复现:当 generated 根是软链接时,拒绝信息只列出另外两个根,与发现描述完全一致。

修复:readWorkflowFileSecurely 现在保留"被考虑但被拒绝"的根目录并追加到信息中:(checked: a, b; refused symlinked root: c)(多于 1 个时用复数 roots)。没有软链接根时,信息与之前逐字节相同,因此既有的根枚举断言不受影响。拒绝逻辑本身未变。

由两个软链接根测试中扩展的断言见证:generated 根场景钉住完整的 (checked: …; refused symlinked root: …) 形状,project 根场景钉住被拒根的标注。

rv:5018818583 — 评审正文中的 "Test Plan" 备注(非阻断):已回应,无代码改动

  • workflows/generated-evil/x.jsno such file or directory:该路径是 refuses a sibling of the generated root (no prefix match) 测试在操作系统临时目录下创建的逐测试临时夹具,测试清理时即删除;它从不位于仓库目录树中,因此无法作为检出文件直接运行。该测试在本 HEAD 上是绿的。
  • 194 passed / 784 passed 与评审观察到的数量不一致:早前轮次报告中的那些数字描述的是那些轮次自己的聚焦 vitest 运行,而不是整包运行,因此与评审观察对不上。权威的最新数字见上方 ## Verification

ic:5411510978 — 评审流水线兜底评论:无需处理

评审流水线的瞬时失败("会自动重试");评审本身已另行发出。代码层面无需处理。

失败的检查(precheck-pr、authorize、ack-review-request、delay-automatic-review、resolve-pr、review-pr、fallback-comment —— 均为 CANCELLED):无需处理

这些是评审流水线自身的任务,不是代码检查;"仍红的检查" 列表为空。

CI 状态(基于第 5 轮分析,已在本地复核)

ubuntu 的 Test 失败(permission-manager.test.ts 关于 report_findings 别名)由基线引起,已由 main 上的 a6d30ebc 修复,且本分支已合并该提交(aef0af85d3)。本地在该 HEAD 上运行 permission-manager.test.ts 为 411/411 通过 —— 下次 CI 运行应转绿。windows 通道的失败(daemon-git-worktree-guard*dws-event-stream)按第 5 轮结论属无关的通道损坏。

变异探针(守卫见证)

  1. 强制 isGeneratedWorkflowScriptPath() 返回 falselabels a generated-root scriptPath … 测试失败(恢复后转绿)。
  2. 移除拒绝信息中的 refusedNote 后缀 → 两个软链接根测试均失败(恢复后转绿)。

验证

  • npm run build — 通过
  • npm run typecheck — 通过(exit 0,无错误)
  • npm run lint — 通过(eslint . --ext .ts,.tsx && eslint integration-tests,exit 0)
  • 对四个改动文件运行 npx prettier --check — 通过(先做了一次格式化修正)
  • cd packages/core && npx vitest run src/tools/workflow/workflow.test.ts src/agents/runtime/workflow-saved.test.ts — 96 通过(56 + 40),提交后复跑仍为绿
  • cd packages/core && env -u QWEN_HOME npx vitest run src/config/storage.test.ts src/permissions/permission-manager.test.ts(与上述两个文件合并在一次运行中)— storage 101 通过,permission-manager 411 通过;合并聚焦运行共 608 通过
    • 说明:如果带着本 runner 的 QWEN_HOME 环境变量运行,storage.test.ts 会有 3 个断言失败,原因是这些测试以 os.homedir() 下的 ~/.qwen 路径为期望值,而 QWEN_HOME 重定向了全局目录;去掉该变量后为 101/101。这是 autofix 沙箱的 runner 环境产物(CI 中不设置该变量),不是代码缺陷 —— 本次改动未触碰 storage.ts 及其测试。
  • 设置 schema:未改动任何设置源 → 无需运行 generate:settings-schema。没有仅经捆绑 CLI 才可行使的行为改动 → 无需集成测试运行。

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

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


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

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

自动代码审查汇总(PR head 已漂移)

本次审查基于的 commit 为 aef0af85d3eb17f7115c0bf6065214dfcd7e4e52,当前 PR head 已更新为 9254a2c58b938b98428c1d80f98f85ce8346dd19。由于 head 漂移,原 inline 评论可能无法准确锚定到行,故仅保留汇总。

结论

  • 模型:deepseek-v4-flash
  • 结果:0 Critical / 1 Suggestion
  • 总体:COMMENT(建议修复后合并)

Findings

R3-2 — Suggestion:symlinked root 在错误消息中静默省略

当某个 workflow script root 本身是 symlink 时,代码会将其过滤掉(return null),导致它在错误消息的 (checked: …) 列表中被静默省略。用户看到 (checked: dir1, dir2) 时,无从得知第三个 root 因是 symlink 而被排除。

  • 影响:调试体验 / 错误信息完整性
  • 安全层面:该拒绝行为本身是正确的(symlinked root 被正确拒绝)
  • 建议:在错误消息中显式说明被排除的 symlinked root,或至少保留占位提示,避免调试困惑

备注

  • 本地 Windows runner 在创建 worktree 时遇到已知网络/DNS 问题(getaddrinfo() thread failed to start),因此 qwen 通过 gh api 拉取 diff 完成审查,未自动生成 inline comments。
  • 其余改动(新增 getWorkflowScriptRoots()、扩展 containment boundary、workflow.ts 描述修正、新增 7 个单元测试)均符合预期,未发现新问题。

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. Suggestions are inline.

Test Plan (not a blocker): workflows/generated-evil/x.jsno such file or directory.

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

  • packages/core/src/tools/workflow/workflow.ts:326 — [probe] generated-root scripts inherit scriptPath's path-only consent and grantable always-allow rule — consent granted for one file's content silently transfers to whatever occupies the pa…

Convergence: round 4 posted 2 inline comment(s), 2 of them reported for the first time; the previous round posted 2 (2 new). Findings keep coming back to the same files: packages/core/src/tools/workflow/workflow.ts (findings in round 3; 1 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. No Critical finding is open on this round, so merging and moving the remaining Suggestion threads to a follow-up issue is available as an ending — a merged pull request cannot diverge further. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

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

Test Plan(非阻断):workflows/generated-evil/x.jsno such file or directory

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

收敛情况:第 4 轮发布了 2 条行内评论,其中 2 条是首次提出;上一轮发布了 2 条(其中 2 条首次提出)。发现反复回到同一批文件:packages/core/src/tools/workflow/workflow.ts(第 3 轮已出过发现,本轮又有 1 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。本轮没有未决的 Critical,因此"合入后把剩余 Suggestion 线程转到后续 issue"是一个可选的结束方式——已合入的 PR 不会继续发散。(仅为观察——本轮评审未因此扣留任何内容。)

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

Comment on lines +653 to +659
function isGeneratedWorkflowScriptPath(
config: Config,
scriptPath: string,
): boolean {
const root = config.storage.getGeneratedWorkflowsDir();
return scriptPath === root || scriptPath.startsWith(root + path.sep);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R4-1: This classifier decides "generated" vs "saved" by a lexical prefix match on the raw scriptPath, while the loader (readWorkflowFileSecurely) decides loadability canonically via fs.realpath — so the label shown at approval can be the opposite of what actually loads. A scriptPath with .. segments (e.g. <generated-root>/../../../../workflows/audit.js) matches the generated prefix here, and the dialog says "Generated workflow script", but realpath normalizes it to ~/.qwen/workflows/audit.js and the loader runs the user's saved workflow — the approval is granted under the wrong identity, and the persisted rule Workflow(scriptPath:<raw ..-path>) never matches a later canonical invocation. The mirror corner also flips the label: on a host whose runtime base sits under a symlinked ancestor (macOS /tmp -> /private/tmp), a writer that canonicalizes the path labels a tool-generated script "Saved workflow". The security boundary itself holds — this is identity/disclosure, which this function exists to get right. Note the predicate also duplicates the shared isWithinRoot helper (fileUtils.ts), which already normalizes both sides.

Witness (probe on the unmodified PR code): raw .. scriptPath → description Run generated workflow script (audit.js) while the loader canonically loaded the saved script; persisted rule keeps the raw .. spelling.

Minimal fix — normalize both sides (this closes the .. class; the symlinked-ancestor corner needs deriving the label from the loader's canonical comparison):

Suggested change
function isGeneratedWorkflowScriptPath(
config: Config,
scriptPath: string,
): boolean {
const root = config.storage.getGeneratedWorkflowsDir();
return scriptPath === root || scriptPath.startsWith(root + path.sep);
}
function isGeneratedWorkflowScriptPath(
config: Config,
scriptPath: string,
): boolean {
const root = path.resolve(config.storage.getGeneratedWorkflowsDir());
const candidate = path.resolve(scriptPath);
return candidate === root || candidate.startsWith(root + path.sep);
}
中文说明

[Suggestion] R4-1:该分类器通过原始 scriptPath 的词法前缀匹配来判定 "generated" 与 "saved",而 loader(readWorkflowFileSecurely)通过 fs.realpath 以规范化方式判定可加载性——因此审批界面显示的标签可能与实际加载的内容相反。带 .. 段的 scriptPath(例如 <generated-root>/../../../../workflows/audit.js)在此处会匹配 generated 前缀,对话框显示 "Generated workflow script",但 realpath 会将其规范化为 ~/.qwen/workflows/audit.js,loader 实际运行的是用户保存的 workflow——审批在错误身份下被授予,且持久化规则 Workflow(scriptPath:<原始 ..-路径>) 永远无法匹配后续规范化的调用。镜像情形同样会翻转标签:在 runtime 根目录位于软链祖先之下的主机上(macOS /tmp -> /private/tmp),写入方若先规范化路径,会把工具生成的脚本标记为 "Saved workflow"。安全边界本身不受影响——这是身份/披露问题,而获取正确身份正是本函数存在的意义。另外该谓词与共享的 isWithinRoot helper(fileUtils.ts)重复,后者已对两侧做规范化。

证据(在未改动的 PR 代码上运行探针):原始 .. scriptPath → 描述为 Run generated workflow script (audit.js),而 loader 实际规范化加载了保存的脚本;持久化规则保留了原始 .. 拼写。

最小修复——对两侧做规范化(可消除 .. 一类问题;软链祖先情形需要从 loader 的规范化比较结果推导标签)。

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

// A generated-root script is a throwaway artifact a tool emitted for this
// run. Labeling it as a saved workflow would have the user approve — and
// maybe pre-approve the path rule — under a wrong identity.
it('labels a generated-root scriptPath as a generated script, not a saved workflow', async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R4-2: The new label tests only pin scripts sitting directly under the generated root, but nested paths are the writer pattern this PR documents ("trusts the whole subtree", load-tested in workflow-saved.test.ts) — and a plausible one-line mutation survives: change return scriptPath === root || scriptPath.startsWith(root + path.sep); to return scriptPath === root || path.dirname(scriptPath) === root; and both label tests still pass, yet <generated-root>/s-abc/fanout.js is then labeled "Saved workflow" in the approval dialog — the wrong-identity approval the label exists to prevent. Witness: in a scratch-tree mutation run, both label tests passed with the mutant applied while a nested probe flipped from Run generated workflow script (fanout.js) to Run saved workflow (fanout.js); reverting the mutant flipped it back.

Add one nested-path assertion beside these tests, e.g.:

const scriptPath = path.join(
  storage.getGeneratedWorkflowsDir(),
  's-abc',
  'fanout.js',
);
// expect getDescription() === 'Run generated workflow script (fanout.js)'
// and details.prompt to contain `Generated workflow script: ${scriptPath}`
中文说明

[Suggestion] R4-2:新增的标签测试只钉住了直接位于 generated 根目录下的脚本,但嵌套路径才是本 PR 文档中描述的写入方布局("信任整棵子树",workflow-saved.test.ts 中已有加载测试)——而且一个合理的一行变异可以存活:把 return scriptPath === root || scriptPath.startsWith(root + path.sep); 改为 return scriptPath === root || path.dirname(scriptPath) === root;,两个标签测试仍然通过,但 <generated-root>/s-abc/fanout.js 会在审批对话框中被标记为 "Saved workflow"——正是该标签要防止的错误身份审批。证据:在临时树变异运行中,施加变异后两个标签测试仍通过,而嵌套探针从 Run generated workflow script (fanout.js) 翻转为 Run saved workflow (fanout.js);还原变异后翻转回来。

在这些测试旁补一个嵌套路径断言即可(示例代码见上方英文部分)。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round summary — PR #9987

Commit: b5f0eb6304 on feat/workflow-generated-dir (additive; no history rewrite; --conflict false — no merge performed).

Feedback points and dispositions

rc:3856248687 — R4-1 (Suggestion, workflow.ts:659): Resolved in code

Claim: the generated/saved label classifier compares the raw scriptPath lexically while the loader decides loadability canonically via fs.realpath, so a ..-laced path can be approved under the opposite identity from what actually loads; the predicate also duplicates the shared isWithinRoot helper.

Reproduced by probe on the pre-round code (source-blind verification): a new witness test asserting the correct label for <generated-root>/../../../../workflows/audit.js FAILED on the unmodified head with expected 'Run generated workflow script (audit.…' to be 'Run saved workflow (audit.js)' — the raw spelling matched the generated prefix while the path normalizes outside the generated root, exactly as the finding describes.

Fix (the finding's suggested minimal fix — normalize both sides — implemented through the shared helper it also names, which resolves the duplication in the same change): isGeneratedWorkflowScriptPath() now delegates to isWithinRoot() from utils/fileUtils.ts, which path.resolves both sides before the containment comparison. The stale "Lexical match on purpose" doc comment was updated to match.

One residual corner, as the finding itself notes: on a host whose runtime base sits under a symlinked ancestor, a writer that canonicalizes its path before handing it over could still see the label flip. Closing that requires deriving the label from the loader's async realpath comparison, which cannot run inside the synchronous getDescription(); no in-tree writer exists yet (the producer/writer pairing is the announced follow-up), and the approval itself is unaffected — the security boundary remains the loader's realpath check. Kept the minimal fix per the finding's own framing.

Witnessed by the new test classifies a ..-laced scriptPath by its normalized location (red pre-round, green post-fix; mutation probe 1 below).

rc:3856248697 — R4-2 (Suggestion, workflow.test.ts:362): Resolved in code

Claim: the label tests only pin scripts directly under the generated root, so a path.dirname(scriptPath) === root mutant survives and a nested script (<generated-root>/s-abc/fanout.js — the writer pattern this PR documents) would be mislabeled "Saved workflow" without any test noticing.

Implemented the suggested nested-path test: labels a nested generated-root scriptPath as a generated script pins both surfaces (getDescription()Run generated workflow script (fanout.js), confirmation prompt → Generated workflow script: <path>, and no Saved workflow substring). Mutation probe 2 below confirms it kills the exact mutant named in the finding.

rv:5022072720 — R3-2 (Suggestion, automated review body): Already resolved at the current head before this round

The review states it ran against the stale commit aef0af85d3 while the head had moved on ("PR head 已漂移"). Its single finding — a symlinked root silently omitted from the (checked: …) refusal list — was fixed by the very next commit 9254a2c58b ("name refused symlinked roots"), which adds the ; refused symlinked root: … note in readWorkflowFileSecurely and pins it with assertions in both symlinked-root tests. Verified present in the current code; no further change.

rv:5022780945 — round-4 review body notes: Answered, no code change

  • Test Plan note workflows/generated-evil/x.js — no such file or directory (marked not a blocker): same note as round 3, answered in round 2 — that path is a per-test temp fixture created under the OS tmp dir by refuses a sibling of the generated root (no prefix match) and deleted in its cleanup; it never exists in the repo tree, so running it from the checkout reports ENOENT by design. The test itself is green at this head (re-run this round, 40/40 in workflow-saved.test.ts).
  • Deferred probe finding at workflow.ts:326 (generated-root consent inheritance): explicitly "recorded, not requested in this round" by the reviewer under the convergence posture — no action taken this round; it remains recorded in the review thread.
  • Convergence observation: advisory only; noted. The "merge and move remaining Suggestions to a follow-up issue" option is a maintainer decision, not one this loop makes for itself.

ic:5414350415 — review-pipeline fallback comment: No action

Transient pipeline failure ("retried automatically"); the round-4 review itself landed separately at 18:57. Nothing to address in code.

Failed checks on the pre-round head

  • Secret scan (TruffleHog): CANCELLED — cancelled, not failed; transient. No code action available.
  • Test (windows-latest Node 22.x): FAILURE — diagnosed from the available evidence; CI logs are not reachable from this runner (no GitHub credentials), so this is an evidence-chain assessment, not a log-verified one:
    1. This PR's footprint is six files under packages/core (workflow + storage); it does not touch packages/cli or packages/channels at all.
    2. At the immediately preceding head (98863f0), the round-5 second-opinion reviewer — who had CI-log access — identified the windows-lane failures as daemon-git-worktree-guard* (packages/cli/src/serve) and dws-event-stream (packages/channels/dws), i.e. breakage outside this PR's footprint, and reported this PR's own tests green on both platforms (workflow-saved.test.ts 40/40, storage.test.ts 101/101, same on Windows including the symlink-escape tests).
    3. At the current head, the ubuntu and macOS Test checks are SUCCESS; only the windows lane is red.
    4. The delta since that analysis is confined to the label classifier (pure path-string logic) and tests, all verified green locally this round.
      No evidence ties the red check to this PR's code, and no evidence-backed code-level fix is available from this Linux runner. If the lane stays red, it needs the CI log access this flow does not have; the workflow's independent CI re-run remains the final verification gate.

Changes

  • packages/core/src/tools/workflow/workflow.tsisGeneratedWorkflowScriptPath() now uses the shared isWithinRoot() (both sides path.resolved) instead of a raw lexical prefix match; doc comment updated; import added.
  • packages/core/src/tools/workflow/workflow.test.ts — two witness tests beside the existing label tests: the ..-laced path is classified by its normalized location; a nested generated-root path keeps the generated label.

Mutation probes (guard witnesses)

  1. Reverted isGeneratedWorkflowScriptPath() to the pre-round lexical startsWith body → classifies a ..-laced scriptPath by its normalized location FAILED (1 failed | 6 passed in the focused run); restored → green. The normalization fix is witnessed.
  2. Replaced the containment check with candidate === root || path.dirname(candidate) === root (the exact mutant named in R4-2) → labels a nested generated-root scriptPath as a generated script FAILED (1 failed | 6 passed); restored → green. The nested-path coverage is witnessed.

Verification

  • cd packages/core && npx vitest run src/tools/workflow/workflow.test.ts -t "classifies" on the pre-round code — 1 failed (reproduction of R4-1, as required before fixing)
  • cd packages/core && env -u QWEN_HOME npx vitest run src/tools/workflow/workflow.test.ts src/agents/runtime/workflow-saved.test.ts src/config/storage.test.ts src/utils/fileUtils.test.ts — 391 passed (58 + 40 + 101 + 192), post-fix
  • Mutation probe runs (see above) — each witness failed under its mutant, green after restore
  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed (eslint . --ext .ts,.tsx && eslint integration-tests, exit 0)
  • npx prettier --write on the two changed files — applied; workflow.ts already formatted
  • Settings schema: no settings source changed → generate:settings-schema not required. The label surface is exercised by the unit tests above, not only through the bundled CLI → no integration run required.
中文说明

Autofix 轮次总结 — PR #9987

提交:feat/workflow-generated-dir 分支上的 b5f0eb6304(仅增量提交;未重写历史;--conflict false — 未执行合并)。

反馈点与处理

rc:3856248687 — R4-1(Suggestion,workflow.ts:659):已在代码中解决

主张:generated/saved 标签分类器对原始 scriptPath 做词法比较,而 loader 通过 fs.realpath 以规范化方式判定可加载性,因此带 .. 的路径可能在"与实际加载内容相反的身份"下被批准;该谓词还重复了共享的 isWithinRoot helper。

在轮前代码上以探针复现(来源无关验证):断言 <generated-root>/../../../../workflows/audit.js 应得到正确标签的新见证测试,在未改动的 head 上失败,输出为 expected 'Run generated workflow script (audit.…' to be 'Run saved workflow (audit.js)' —— 原始拼写匹配了 generated 前缀,而该路径规范化后位于 generated 根之外,与发现描述完全一致。

修复(采用发现建议的最小修复——对两侧做规范化——并通过它所点名的共享 helper 实现,同一次改动也消除了重复):isGeneratedWorkflowScriptPath() 现在委托给 utils/fileUtils.tsisWithinRoot(),后者在包含关系比较前对两侧执行 path.resolve。过时的 "Lexical match on purpose" 文档注释已同步更新。

正如发现本身所指出的,仍存在一个残留角落情形:在 runtime 根位于软链祖先之下的主机上,若写入方在交出路径前先做规范化,标签仍可能翻转。要消除它需要从 loader 的异步 realpath 比较结果推导标签,而这无法在同步的 getDescription() 中完成;目前树内尚不存在写入方(生产者/写入方配对是已宣布的后续工作),且审批本身不受影响——安全边界仍是 loader 的 realpath 检查。按发现自身的定位,保留最小修复。

由新测试 classifies a ..-laced scriptPath by its normalized location 见证(轮前红、修复后绿;见下方变异探针 1)。

rc:3856248697 — R4-2(Suggestion,workflow.test.ts:362):已在代码中解决

主张:标签测试只钉住了直接位于 generated 根下的脚本,因此 path.dirname(scriptPath) === root 变异可以存活,嵌套脚本(<generated-root>/s-abc/fanout.js —— 本 PR 文档中描述的写入方布局)会被错误标记为 "Saved workflow" 而无任何测试察觉。

按建议实现了嵌套路径测试:labels a nested generated-root scriptPath as a generated script 钉住两个展示面(getDescription()Run generated workflow script (fanout.js),确认对话框 → Generated workflow script: <路径>,且不含 Saved workflow 子串)。下方变异探针 2 确认它杀死了发现中所点名的那个变异。

rv:5022072720 — R3-2(Suggestion,自动评审正文):本轮开始前已在当前 head 上解决

该评审自述基于过时的提交 aef0af85d3,当时 head 已前进("PR head 已漂移")。其唯一发现——软链根在 (checked: …) 拒绝列表中被静默省略——已由紧随其后的提交 9254a2c58b("name refused symlinked roots")修复:readWorkflowFileSecurely 追加了 ; refused symlinked root: … 说明,两个软链根测试均以断言钉住。已验证当前代码中存在;无需进一步改动。

rv:5022780945 — 第 4 轮评审正文备注:已答复,无代码改动

  • Test Plan 备注 workflows/generated-evil/x.js — no such file or directory(已标注非阻断):与第 3 轮相同的备注,第 2 轮已答复——该路径是 refuses a sibling of the generated root (no prefix match) 测试在 OS 临时目录下创建的临时 fixture,并在清理阶段删除;它从不曾存在于仓库树中,因此从 checkout 直接运行它按设计就会报 ENOENT。该测试本身在当前 head 上是绿的(本轮重跑,workflow-saved.test.ts 40/40)。
  • workflow.ts:326 处延后的探针发现(generated 根的授权继承):评审方在收敛姿态下明确标注"已记录,本轮不要求修改"——本轮不做处理;该发现仍保留在评审线程中。
  • 收敛性观察:仅为提示;已记录。"合入并把剩余 Suggestion 线程转入后续 issue"这一选项是维护者的决定,本循环不代为做出。

ic:5414350415 — 评审流水线 fallback 评论:无需处理

流水线瞬时故障("会自动重试");第 4 轮评审本身已于 18:57 单独发出。代码层面无需处理。

轮前 head 上的失败检查

  • Secret scan (TruffleHog):CANCELLED —— 是取消而非失败;瞬时现象。无代码层面的处理可做。
  • Test (windows-latest Node 22.x):FAILURE —— 基于现有证据诊断;本 runner 无法访问 CI 日志(无 GitHub 凭据),因此这是证据链评估,而非日志实证的结论:
    1. 本 PR 的改动面是 packages/core 下的六个文件(workflow + storage);完全未触及 packages/clipackages/channels
    2. 在紧邻的前一个 head(98863f0)上,第 5 轮第二意见评审者——当时可以访问 CI 日志——将 windows 通道的失败定位为 daemon-git-worktree-guard*packages/cli/src/serve)与 dws-event-streampackages/channels/dws),即本 PR 改动面之外的损坏,并报告本 PR 自身的测试在两个平台上均为绿(workflow-saved.test.ts 40/40、storage.test.ts 101/101,Windows 上相同,含软链逃逸测试)。
    3. 在当前 head 上,ubuntu 与 macOS 的 Test 检查为 SUCCESS;仅 windows 通道为红。
    4. 自该分析以来的增量仅为标签分类器(纯路径字符串逻辑)与测试,本轮已在本地全部验证为绿。
      没有证据把该红色检查与本 PR 的代码关联起来,本 Linux runner 上也没有可落地的、有证据支撑的代码级修复。若该通道持续为红,需要本流程所不具备的 CI 日志访问权限;工作流的独立 CI 重跑仍是最终验证关口。

改动

  • packages/core/src/tools/workflow/workflow.ts —— isGeneratedWorkflowScriptPath() 改用共享的 isWithinRoot()(两侧均经 path.resolve)替代原始词法前缀匹配;文档注释更新;新增 import。
  • packages/core/src/tools/workflow/workflow.test.ts —— 在既有标签测试旁新增两个见证测试:带 .. 的路径按其规范化位置分类;嵌套的 generated 根路径保持 generated 标签。

变异探针(守卫见证)

  1. isGeneratedWorkflowScriptPath() 还原为轮前的词法 startsWith 实现 → classifies a ..-laced scriptPath by its normalized location 失败(聚焦运行 1 failed | 6 passed);还原修复 → 绿。规范化修复有见证。
  2. 将包含关系检查替换为 candidate === root || path.dirname(candidate) === root(R4-2 点名的变异)→ labels a nested generated-root scriptPath as a generated script 失败(1 failed | 6 passed);还原 → 绿。嵌套路径覆盖有见证。

验证

  • cd packages/core && npx vitest run src/tools/workflow/workflow.test.ts -t "classifies"(轮前代码)—— 1 个失败(按修复前先复现的要求,复现 R4-1)
  • cd packages/core && env -u QWEN_HOME npx vitest run src/tools/workflow/workflow.test.ts src/agents/runtime/workflow-saved.test.ts src/config/storage.test.ts src/utils/fileUtils.test.ts —— 391 通过(58 + 40 + 101 + 192),修复后
  • 变异探针运行(见上)—— 每个见证测试在对应变异下失败,还原后为绿
  • npm run build —— 通过
  • npm run typecheck —— 通过
  • npm run lint —— 通过(eslint . --ext .ts,.tsx && eslint integration-tests,退出码 0)
  • 对两个改动文件执行 npx prettier --write —— 已应用;workflow.ts 本就符合格式
  • Settings schema:未改动任何 settings 源 → 无需 generate:settings-schema。标签展示面由上述单元测试覆盖,并非仅经打包后的 CLI 才能触达 → 无需集成测试运行。

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

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. Suggestions are inline.

Test Plan (not a blocker): workflows/generated-evil/x.jsno such file or directory.

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

  • packages/core/src/tools/workflow/workflow.ts:186 — [review] generated-root contract prose (schema description + Runtime paragraph) is anchored by no test despite the file's prose-anchoring convention

Convergence: round 5 posted 3 inline comment(s), 2 of them reported for the first time; the previous round posted 2 (2 new). Findings keep coming back to the same files: packages/core/src/tools/workflow/workflow.test.ts (findings in round 4; 2 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. No Critical finding is open on this round, so merging and moving the remaining Suggestion threads to a follow-up issue is available as an ending — a merged pull request cannot diverge further. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

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

Test Plan(非阻断):workflows/generated-evil/x.jsno such file or directory

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

收敛情况:第 5 轮发布了 3 条行内评论,其中 2 条是首次提出;上一轮发布了 2 条(其中 2 条首次提出)。发现反复回到同一批文件:packages/core/src/tools/workflow/workflow.test.ts(第 4 轮已出过发现,本轮又有 2 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。本轮没有未决的 Critical,因此"合入后把剩余 Suggestion 线程转到后续 issue"是一个可选的结束方式——已合入的 PR 不会继续发散。(仅为观察——本轮评审未因此扣留任何内容。)

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

config: Config,
scriptPath: string,
): boolean {
return isWithinRoot(scriptPath, config.storage.getGeneratedWorkflowsDir());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R4-1: The round-4 fix landed as asked — the classifier now normalizes both sides through isWithinRoot, so the reported ..-laced misclassification is closed and the new test pins its witness shape. One branch of the same divergence remains: the classifier resolves paths lexically (path.resolve) while the loader decides loadability with fs.realpath, so a scriptPath whose spelling diverges from its realpath gets the opposite provenance label from the content that actually loads — and the doc comment rewritten in this commit still says a path is classified at "the same location the loader decides to load", which holds for .. segments but not for symlinks.

Concrete trigger, reproduced by a probe against this commit: <projectDir>/workflows/generated/run.js shipped as a symlink to <projectRoot>/.qwen/workflows/deploy.js — both paths sit inside trusted roots, so the loader accepts — the approval dialog prints Generated workflow script: …run.js (a throwaway-artifact identity) while the content that loads is the durable saved workflow also surfaced as /deploy; the mirror spelling labels a loaded generated script Saved workflow; and a symlinked ancestor of the runtime dir (the macOS /tmp/private/tmp class) makes a loaded generated script read as Saved workflow too.

Impact is confined to the label: the permission rule keys on the raw scriptPath, hideAlwaysAllow on inline-script-ness, and the loader's realpath boundary still makes the security decision, so no grant is widened — but the label is exactly what this change exists to get right. Either classify best-effort with the same canonicalization the loader uses (fs.realpath of both sides, with a lexical fallback for a not-yet-existing path; the confirmation surface is already async), or — if the lexical design is deliberate — correct the doc comment to say the classifier normalizes .. lexically only and may disagree with the loader across symlinks.

Witness — probe on unmodified PR code driving the real loader and label surfaces with real symlinks:

CASE1 symlink <genRoot>/run.js -> <savedRoot>/deploy.js
  loader: ACCEPTED, loaded content = the saved workflow
  dialog: Generated workflow script: <genRoot>/run.js
CASE2 symlink <savedRoot>/toolgen.js -> <genRoot>/s-abc/throwaway.js
  loader: ACCEPTED, loaded content = the generated script
  dialog: Saved workflow: <savedRoot>/toolgen.js

Applying the suggested realpath classification flipped both labels to match the loaded content, with all 58 PR tests still passing.

中文说明

[Suggestion] R4-1:第 4 轮的修复已按要求落地——分类器现在通过 isWithinRoot 对两侧做归一化,因此当初报告的含 .. 路径误分类已被封闭,新增测试也钉住了该见证场景。同一分歧还剩一个分支:分类器以词法方式解析路径(path.resolve),而 loader 用 fs.realpath 决定可加载性,因此拼写与其 realpath 不一致的 scriptPath 会得到与实际加载内容相反的来源标签——而本次提交重写的文档注释仍声称路径会在「loader 决定加载的同一位置」被分类;这对 .. 片段成立,对符号链接不成立。

具体触发场景(已通过针对本提交的探针复现):<projectDir>/workflows/generated/run.js 以符号链接形式指向 <projectRoot>/.qwen/workflows/deploy.js——两个路径都在受信根内,因此 loader 接受加载——审批对话框显示 Generated workflow script: …run.js(一次性产物的身份),而实际加载的内容是同时以 /deploy 呈现的持久化已保存 workflow;反向拼写则会把加载的生成脚本标成 Saved workflow;runtime 目录的符号链接祖先目录(macOS /tmp/private/tmp 一类)同样会让已加载的生成脚本显示为 Saved workflow

影响仅限于标签:权限规则以原始 scriptPath 为键,hideAlwaysAllow 取决于是否内联脚本,安全决策仍由 loader 的 realpath 边界做出,因此没有任何授权被放宽——但标签正是本次改动要修正的东西。要么按与 loader 相同的规范化方式做尽力而为的分类(对两侧取 fs.realpath,对尚不存在的路径回退到词法解析;确认路径本来就是 async 的),要么——如果词法设计是有意为之——修正文档注释,说明分类器只在词法层面归一化 ..,在存在符号链接时可能与 loader 不一致。

见证——在未改动的 PR 代码上,用真实符号链接驱动真实 loader 与标签界面的探针输出:

CASE1 symlink <genRoot>/run.js -> <savedRoot>/deploy.js
  loader: ACCEPTED, loaded content = the saved workflow
  dialog: Generated workflow script: <genRoot>/run.js
CASE2 symlink <savedRoot>/toolgen.js -> <genRoot>/s-abc/throwaway.js
  loader: ACCEPTED, loaded content = the generated script
  dialog: Saved workflow: <savedRoot>/toolgen.js

应用建议的 realpath 分类后,两个标签都翻转为与实际加载内容一致,且 58 个 PR 测试全部仍然通过。

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

Comment on lines +406 to +409
it('classifies a ..-laced scriptPath by its normalized location', async () => {
const storage = new Storage(
path.join(os.tmpdir(), 'workflow-label-test'),
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] This new test inlines the same 4-line Storage fixture that the PR's own configWithStorage() helper (line 55) exists to provide, instead of calling it — the identical block now appears 5 times in this file (helper at :57 plus the label tests at :364, :386, :408 and :437). The helper is bypassed because of its return type: it returns only Config, while these tests need the storage handle to derive the expected roots — the file's own precedent configWithRegistry() returns { config, registry } for exactly this reason. The cost is concrete: any fixture change — the Storage constructor signature, the shared 'workflow-label-test' root, or isolating each test in its own tmp dir — must be made in five synchronized spots, and a miss is silent, because each test derives its assertion root from its own Storage instance and the label logic does no disk I/O, so every test stays green against a root the helper no longer shares.

function configWithStorage(): { config: Config; storage: Storage } {
  const storage = new Storage(
    path.join(os.tmpdir(), 'workflow-label-test'),
  );
  return { config: { storage } as unknown as Config, storage };
}

// in each label test:
const { config, storage } = configWithStorage();
中文说明

[Suggestion] 这个新测试没有调用本 PR 自己添加的 configWithStorage() 辅助函数(第 55 行),而是内联构造了同一个 4 行 Storage 夹具——完全相同的代码块现在在本文件中出现 5 次(辅助函数 :57,加上 :364、:386、:408、:437 四个标签测试)。辅助函数被绕过的原因是它的返回类型:它只返回 Config,而这些测试需要 storage 句柄来推导期望的根目录——同文件中现成的先例 configWithRegistry() 正是为此返回 { config, registry }。代价是具体的:任何夹具改动——Storage 构造函数签名、共享的 'workflow-label-test' 根目录、或者把每个测试隔离到各自的 tmp 目录——都必须同步改五处,而且漏改不会报错:每个测试都从自己的 Storage 实例推导断言根目录,标签逻辑又不做任何磁盘 I/O,所以即使某个测试的夹具根目录与辅助函数不再一致,整套测试仍然全绿。

function configWithStorage(): { config: Config; storage: Storage } {
  const storage = new Storage(
    path.join(os.tmpdir(), 'workflow-label-test'),
  );
  return { config: { storage } as unknown as Config, storage };
}

// 在每个标签测试中:
const { config, storage } = configWithStorage();

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

Comment on lines +435 to +438
it('labels a nested generated-root scriptPath as a generated script', async () => {
const storage = new Storage(
path.join(os.tmpdir(), 'workflow-label-test'),
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Same duplication as the sibling comment at line 406: this new test also inlines the 4-line Storage fixture the PR's own configWithStorage() helper exists to provide — the identical block now appears 5 times in this file (helper at :57 plus tests at :364, :386, :408 and :437). The helper returns only Config, so the label tests cannot reach the storage handle they derive expected roots from; returning { config, storage } like the adjacent configWithRegistry() lets every test use it. As written, a fixture change must touch five synchronized spots and a miss stays green — each test asserts against its own Storage instance's root, so the suite passes even when a test's fixture and the helper's no longer agree.

function configWithStorage(): { config: Config; storage: Storage } {
  const storage = new Storage(
    path.join(os.tmpdir(), 'workflow-label-test'),
  );
  return { config: { storage } as unknown as Config, storage };
}

// in each label test:
const { config, storage } = configWithStorage();
中文说明

[Suggestion] 与 406 行的姊妹评论相同的重复:这个新测试同样内联了本 PR 自己的 configWithStorage() 辅助函数本该提供的 4 行 Storage 夹具——完全相同的代码块现在在本文件中出现 5 次(辅助函数 :57,加上 :364、:386、:408、:437 四个测试)。辅助函数只返回 Config,因此标签测试拿不到用于推导期望根目录的 storage 句柄;像相邻的 configWithRegistry() 一样返回 { config, storage } 即可让所有测试复用它。按现状写法,夹具改动必须同步五处,且漏改仍然全绿——每个测试都对自己的 Storage 实例根目录做断言,因此即使某个测试的夹具与辅助函数不再一致,测试套件依旧通过。

function configWithStorage(): { config: Config; storage: Storage } {
  const storage = new Storage(
    path.join(os.tmpdir(), 'workflow-label-test'),
  );
  return { config: { storage } as unknown as Config, storage };
}

// 在每个标签测试中:
const { config, storage } = configWithStorage();

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round summary — PR #9987

Commit: a25c7b13f0 on feat/workflow-generated-dir (additive; no history rewrite; --conflict false — no merge performed; the merge-base of origin/main and HEAD is origin/main's tip, so no base update was needed).

Feedback points and dispositions

rc:3857387219 — R4-1 follow-up (Suggestion, workflow.ts:660): Resolved in code

Claim: the approval dialog classifies scriptPath lexically (path.resolve) while the loader decides loadability with fs.realpath, so a path whose spelling diverges from its realpath gets the opposite provenance label from the content that actually loads; the doc comment rewritten in the previous commit still said classification happens at "the same location the loader decides to load", which holds for .. segments but not for symlinks.

Reproduced before changing anything. Two new tests drive the real dialog surface with real symlinks and failed on the unmodified code exactly as the finding describes:

  • <generated-root>/run.js → <saved-root>/deploy.js: the loader accepts and loads the saved workflow, but the dialog printed Generated workflow script: …run.js.
  • <saved-root>/toolgen.js → <generated-root>/s-abc/throwaway.js: the loader loads the generated script, but the dialog printed Saved workflow: …toolgen.js.

Fix (the finding's first option). The consent surface now classifies with the loader's own normalization: fs.realpath of both sides, a lexical fallback where a side does not exist yet (matching the loader's path.resolve fallback for a missing root), and a symlinked generated-scripts root counts as not-generated because the loader refuses it outright. The symlinked-root predicate is the loader's own isSymlinkedRoot, now exported from workflow-saved.ts and reused, so the two surfaces share one rule instead of re-deriving it. buildConfirmationPrompt takes the precomputed label and no longer receives the Config (subtractive). The doc comment the finding cites is rewritten: the synchronous surface (getDescription(), the transcript row) is documented as lexical-only — it is a string-typed abstract method shared by every tool, so canonicalizing it would mean changing core tool infrastructure beyond this PR's scope — and the confirmation dialog is documented as canonical. The security decision remains the loader's realpath boundary; no grant is widened by this change.

Witnesses: labels a symlinked scriptPath by the content that loads (both spelling directions) and labels nothing generated through a symlinked generated root. Mutation probes: pointing the dialog back at the lexical classifier makes both symlink tests fail; deleting the symlinked-root guard makes the symlinked-root test fail; restored → 60/60 green.

rc:3857387225 + rc:3857387235 (Suggestions, workflow.test.ts:409 / :438): Resolved in code (one shared root cause)

Both comments describe the same duplication: four label tests inlined the 4-line Storage fixture because configWithStorage() returned only Config while the tests need the storage handle to derive expected roots. As suggested, it now returns { config, storage } (the same shape as the file's existing configWithRegistry() precedent), the four inline blocks collapse to one-line calls, and the two config-only call sites use .config. The fixture exists exactly once now; a fixture change is one edit, and a test that drifted from it fails instead of staying silently green. Mutation probe: returning a config whose storage diverges from the returned handle fails two label tests; restored → green.

rv:5024154327 — review body, "Test Plan (not a blocker): workflows/generated-evil/x.js — no such file or directory": Declined with evidence

The observation is accurate and reflects designed behavior: readWorkflowFileSecurely realpaths the file first (throws ENOENT if absent), so a path that does not exist fails with ENOENT before any boundary message can be produced. The refusal the test plan describes is pinned by workflow-saved.test.tsrefuses a sibling of the generated root (no prefix match) creates …/generated-evil/x.js first and asserts both the outside the workflow script roots (checked: …) message and that the generated root is named in it. The reviewer marked the note a non-blocker, and the PR-description text is not something this mode edits.

Failed check — Test (windows-latest, Node 22.x): Diagnosed as base-branch breakage, reproduced on the base

No CI logs are reachable from this runner (no GitHub credentials), so this round verified the failure against the base branch through the public checks API instead: Test (windows-latest, Node 22.x) concluded failure on a6d30ebc6b — the exact tip of origin/main this branch is based on (check completed 2026-08-25T19:47:21Z). The ubuntu and macOS Test lanes are SUCCESS on this PR's head. The prior round's CI-log-sourced analysis named the failing windows-lane suites as daemon-git-worktree-guard* (packages/cli/src/serve) and dws-event-stream (packages/channels/dws); both test files are byte-identical to origin/main in this checkout, and this PR's diff touches neither area. Fixing main's Windows lane is a separate change outside this PR's footprint.

Deferred under the convergence posture (recorded, not requested this round)

workflow.ts:186 — generated-root contract prose (schema description + Runtime paragraph) anchored by no test. No action taken, as instructed by the round's feedback.

Verification

  • Reproduction (pre-fix): npx vitest run src/tools/workflow/workflow.test.ts -t "symlinked" in packages/core2 failed on the unmodified dialog classifier, matching the finding's probe output
  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npx prettier --check (3 changed files) — passed (after one --write on the test file)
  • npx vitest run src/tools/workflow/workflow.test.ts in packages/core — 60 passed
  • npx vitest run src/tools/workflow/workflow.test.ts src/agents/runtime/workflow-saved.test.ts src/config/storage.test.ts in packages/core — 201 passed
  • Mutation probe A (dialog reverted to the lexical classifier) — both new symlink tests fail; restored → green
  • Mutation probe B (symlinked-root guard removed) — the symlinked-root test fails; restored → green
  • Mutation probe C (helper returns diverged config/storage handles) — 2 label tests fail; restored → green
  • Base-branch reproduction of the Windows lane: GitHub public checks API on origin/main tip a6d30ebc6bTest (windows-latest, Node 22.x) = failure (base breakage, outside this PR's footprint)
中文说明

Autofix 轮次总结 — PR #9987

提交:feat/workflow-generated-dir 分支上的 a25c7b13f0(追加式提交;不改写历史;--conflict false —— 未执行合并;origin/main 与 HEAD 的 merge-base 就是 origin/main 的最新提交,因此无需更新 base)。

反馈点与处置

rc:3857387219 — R4-1 后续(Suggestion,workflow.ts:660):已在代码中解决

主张:审批对话框以词法方式(path.resolve)分类 scriptPath,而 loader 用 fs.realpath 决定可加载性,因此拼写与其 realpath 不一致的路径会得到与实际加载内容相反的来源标签;上一个提交重写的文档注释仍声称分类发生在「loader 决定加载的同一位置」——这对 .. 片段成立,对符号链接不成立。

改动前已复现。 两个新测试用真实符号链接驱动真实对话框界面,在未修改的代码上按发现描述的方式失败:

  • <generated-root>/run.js → <saved-root>/deploy.js:loader 接受并加载已保存的 workflow,但对话框显示 Generated workflow script: …run.js
  • <saved-root>/toolgen.js → <generated-root>/s-abc/throwaway.js:loader 加载的是生成脚本,但对话框显示 Saved workflow: …toolgen.js

修复(采用发现给出的第一选项)。 确认(同意)界面现在使用与 loader 相同的规范化方式分类:对两侧取 fs.realpath,对尚不存在的一侧回退到词法解析(与 loader 对缺失根目录的 path.resolve 回退一致),并且当 generated-scripts 根目录本身是符号链接时视为非生成——因为 loader 会直接拒绝加载。符号链接根目录判定直接复用 loader 自己的 isSymlinkedRoot(现从 workflow-saved.ts 导出),两个界面共享同一条规则而不是各自推导。buildConfirmationPrompt 改为接收预计算的标签,不再接收 Config(做了减法)。发现所引用的文档注释已重写:同步界面(getDescription(),即转录行)被明确记录为仅词法归一化——它是所有工具共享的 string 型抽象方法,将其规范化意味着改动超出本 PR 范围的核心工具基础设施——而确认对话框被记录为使用规范化分类。安全决策仍由 loader 的 realpath 边界做出;本改动没有放宽任何授权。

见证测试:labels a symlinked scriptPath by the content that loads(两个拼写方向)与 labels nothing generated through a symlinked generated root。变异探针:把对话框改回词法分类器后,两个符号链接测试均失败;删除符号链接根目录守卫后,对应测试失败;恢复后 60/60 全绿。

rc:3857387225 + rc:3857387235(Suggestion,workflow.test.ts:409 / :438):已在代码中解决(同一根因)

两条评论描述的是同一处重复:四个标签测试内联了同一个 4 行 Storage 夹具,原因是 configWithStorage() 只返回 Config,而测试需要 storage 句柄来推导期望的根目录。按建议,它现在返回 { config, storage }(与同文件中现成的 configWithRegistry() 先例同形),四处内联代码块收敛为单行调用,两个只需要 config 的调用点改用 .config。夹具现在只存在于一处;改动夹具只需编辑一处,任何与夹具脱节的测试会失败而不是静默全绿。变异探针:让返回的 config 持有与句柄不同的 storage 后,两个标签测试失败;恢复后全绿。

rv:5024154327 — 评审正文「Test Plan(非阻断):workflows/generated-evil/x.js — no such file or directory」:以证据驳回

该观察准确,且反映的是设计内行为:readWorkflowFileSecurely 首先对文件取 realpath(throws ENOENT if absent),因此不存在的路径会在产生任何边界消息之前先以 ENOENT 失败。Test Plan 描述的拒绝行为已由 workflow-saved.test.ts 钉住——refuses a sibling of the generated root (no prefix match) 会先创建 …/generated-evil/x.js,然后断言 outside the workflow script roots (checked: …) 消息,并断言生成根目录出现在消息中。评审者已将该条标注为非阻断,且 PR 描述文本不是本模式会编辑的对象。

失败检查 — Test (windows-latest, Node 22.x)诊断为 base 分支损坏,已在 base 上复现

本 runner 无法访问 CI 日志(无 GitHub 凭据),因此本轮通过公开的 checks API 在 base 分支上验证该失败:Test (windows-latest, Node 22.x)a6d30ebc6b —— 即本分支所基于的 origin/main 最新提交 —— 上的结论为 failure(该检查于 2026-08-25T19:47:21Z 完成)。本 PR head 上 ubuntu 与 macOS 的 Test 通道均为 SUCCESS。上一轮可访问 CI 日志的分析将 windows 通道的失败定位为 daemon-git-worktree-guard*packages/cli/src/serve)与 dws-event-streampackages/channels/dws);这两个测试文件与本检出中的 origin/main 逐字节一致,且本 PR 的 diff 完全不涉及这两个区域。修复 main 的 Windows 通道属于本 PR 改动面之外的独立改动。

收敛姿态下延后(已记录,本轮不要求)

workflow.ts:186 —— generated-root 契约文案(schema 描述 + Runtime 段落)缺少测试锚定。按本轮反馈的指示,未采取行动。

验证

  • 复现(修复前):在 packages/core 中运行 npx vitest run src/tools/workflow/workflow.test.ts -t "symlinked" —— 在未修改的对话框分类器上 2 个失败,与发现给出的探针输出一致
  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • npx prettier --check(3 个改动文件)— 通过(测试文件先执行了一次 --write
  • packages/core 中运行 npx vitest run src/tools/workflow/workflow.test.ts — 60 通过
  • packages/core 中运行 npx vitest run src/tools/workflow/workflow.test.ts src/agents/runtime/workflow-saved.test.ts src/config/storage.test.ts — 201 通过
  • 变异探针 A(对话框改回词法分类器)— 两个新符号链接测试失败;恢复后全绿
  • 变异探针 B(删除符号链接根目录守卫)— 符号链接根目录测试失败;恢复后全绿
  • 变异探针 C(helper 返回不一致的 config/storage 句柄)— 2 个标签测试失败;恢复后全绿
  • Windows 通道的 base 分支复现:对 origin/main 最新提交 a6d30ebc6b 查询 GitHub 公开 checks API —— Test (windows-latest, Node 22.x) = failure(base 损坏,超出本 PR 改动面)

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

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚠️ Downgraded from Approve to Comment: CI failing: Test (windows-latest, Node 22.x). Reviewed.

Test Plan (not a blocker): workflows/generated-evil/x.jsno such file or directory; 194 passed — this review observed 21621, 1702, 24736, 1659, 601, 4237, 630 passed; 784 passed — this review observed 21621, 1702, 24736, 1659, 601, 4237, 630 passed.

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

  • packages/core/src/tools/workflow/workflow.ts:701 — [review] canonical classifier hand-copies the loader's per-root canonicalization — next root-policy edit must patch both files or the consent label diverges
  • packages/core/src/tools/workflow/workflow.ts:312 — [probe] grant axis does not branch on isGeneratedScriptPath — generated scripts are offered always-allow and a persistable exact-path rule
中文说明

⚠️ 已从批准降级为评论:CI failing: Test (windows-latest, Node 22.x)。 已审查。

Test Plan(非阻断):workflows/generated-evil/x.jsno such file or directory; 194 passed — this review observed 21621, 1702, 24736, 1659, 601, 4237, 630 passed; 784 passed — this review observed 21621, 1702, 24736, 1659, 601, 4237, 630 passed

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

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round: no action taken

The only feedback item this round is the failed check Test (windows-latest, Node 22.x). There were no inline comments, no issue-level comments, and the review posted 0 findings (its two remaining observations are explicitly deferred under the convergence posture, recorded and not requested). After evidence-based investigation, the Windows failure is not caused by this PR — it is a broken Windows test lane on main that reproduces across unrelated branches. No code change was made, and nothing was committed.

Evidence: the failing tests are outside this PR's footprint

The failing run (check run 97982673902, head a25c7b13f0) carries exactly four test failures, none in code this PR touches:

  • packages/channels/dws/src/dws-event-stream.test.ts — "clears stale process errors after a healthy event": a timing-sensitive assertion on a spawned child's error (retryable: false observed where the test expects undefined).
  • packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.test.ts — two tests hardcode a POSIX expectation file:///workspace/export.html while the Windows run produces file:///D:/workspace/export.html.
  • packages/node-repl/src/node-repl.semantics.test.ts — "resolves bare packages from a symlinked cwd node_modules": transient EBUSY: resource busy or locked, rmdir during temp-dir cleanup (a Windows file-lock flake, under an 8.3 short path C:\Users\RUNNER~1\...).

This PR's diff (git diff origin/main...HEAD) touches exactly six files, all under packages/core (storage.ts, workflow-saved.ts, workflow.ts and their tests). A git diff --stat against packages/channels/dws, packages/vscode-ide-companion, and packages/node-repl is empty — this PR changes nothing those tests exercise.

Evidence: the same failure reproduces on unrelated branches

Two unrelated PRs from the same window fail the Windows lane with the identical signature:

  • Run 32898750011 (fix/9724-safe-archive-symlinks): the same two SessionMessageHandler failures plus the same node-repl.semantics EBUSY failure.
  • Run 32906006047 (fix/workflow-run-lifecycle): the same three failures again.

The failing set also shifts between runs (earlier heads of this branch failed on daemon-git-worktree-guard* + dws-event-stream per the round-5 second-opinion review, which independently concluded "windows-lane breakage unrelated to this change" and "this PR's own tests are green on both platforms"). A shifting, cross-PR failure set is a broken lane, not a regression.

Evidence: this PR's own tests pass on Windows

The failing run has no failure annotations for workflow-saved.test.ts, workflow.test.ts, or storage.test.ts — every test this PR adds or modifies passes on Windows in that same run.

No base update is available

merge-base(HEAD, origin/main) equals the origin/main tip (a6d30ebc6b), so the branch already contains all of main; merging again would be a no-op. The lane repair belongs on main itself (and in packages/vscode-ide-companion / packages/node-repl / packages/channels/dws), which this round cannot touch: the footprint rules reject expanding a PR into areas it never touched.

Local verification (Linux)

  • cd packages/core && npx vitest run src/agents/runtime/workflow-saved.test.ts src/config/storage.test.ts src/tools/workflow/workflow.test.ts — 3 files, 201/201 passed (the PR's own suite).
  • packages/node-repl: npx vitest run src/node-repl.semantics.test.ts18/18 passed on Linux.
  • packages/channels/dws: npx vitest run src/dws-event-stream.test.ts6/6 passed on Linux.
  • packages/vscode-ide-companion: not runnable on this runner (its suite requires the packages/webui vite build, absent here); the CI annotation above is the evidence, and its hardcoded POSIX expectation is untouched by this PR.

Conclusion: the failed check is base-induced Windows-lane breakage reproduced across unrelated branches; this PR's own tests are green on both platforms. The correct action is no action on this branch.

中文说明

Autofix 本轮:未采取任何改动

本轮唯一的反馈项是失败检查 Test (windows-latest, Node 22.x)。没有行内评论,没有 issue 级评论,审查本身发布了 0 条发现(其余两条观察项已在收敛姿态下明确延后——仅记录、本轮不要求处理)。经基于证据的调查,该 Windows 失败并非本 PR 引起——而是 main 上 Windows 测试通道本身的损坏,且在不相关的分支上同样复现。本轮未做任何代码改动,也没有提交。

证据:失败的测试不在本 PR 的影响范围内

失败运行(check run 97982673902,head a25c7b13f0)恰好有四个测试失败,均不在本 PR 触碰的代码中:

  • packages/channels/dws/src/dws-event-stream.test.ts ——"clears stale process errors after a healthy event":对一个派生子进程错误的时序敏感断言(观察到 retryable: false,而测试期望 undefined)。
  • packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.test.ts —— 两个测试硬编码了 POSIX 期望值 file:///workspace/export.html,而 Windows 运行产生的是 file:///D:/workspace/export.html
  • packages/node-repl/src/node-repl.semantics.test.ts ——"resolves bare packages from a symlinked cwd node_modules":清理临时目录时的瞬时 EBUSY: resource busy or locked, rmdir(Windows 文件锁导致的 flake,路径还是 8.3 短路径 C:\Users\RUNNER~1\...)。

本 PR 的 diff(git diff origin/main...HEAD)恰好只改动六个文件,全部位于 packages/core 之下(storage.tsworkflow-saved.tsworkflow.ts 及其测试)。对 packages/channels/dwspackages/vscode-ide-companionpackages/node-repl 执行 git diff --stat 结果为空——本 PR 没有改动这些测试所执行的任何内容。

证据:同样的失败在不相关分支上复现

同一时间窗内的两个不相关 PR 以完全相同的特征在 Windows 通道上失败:

  • 运行 32898750011fix/9724-safe-archive-symlinks):相同的两个 SessionMessageHandler 失败,加上相同的 node-repl.semantics EBUSY 失败。
  • 运行 32906006047fix/workflow-run-lifecycle):再次出现相同的三个失败。

失败集合在不同运行之间还会漂移(按第 5 轮第二意见审查所述,本分支更早的 head 上失败的是 daemon-git-worktree-guard* + dws-event-stream;该审查还独立得出结论:"windows 通道损坏,与本改动无关",且"本 PR 自身的测试在两个平台上均为绿色")。一个跨 PR 复现且不断漂移的失败集合,说明是通道损坏,而不是回归。

证据:本 PR 自身的测试在 Windows 上通过

失败运行中,workflow-saved.test.tsworkflow.test.tsstorage.test.ts 没有任何失败标注——本 PR 新增或修改的全部测试在同一次 Windows 运行中都通过了。

没有可用的 base 更新

merge-base(HEAD, origin/main) 等于 origin/main 的顶端(a6d30ebc6b),因此本分支已经包含 main 的全部内容;再次合并将是空操作。通道的修复应落在 main 本身(以及 packages/vscode-ide-companion / packages/node-repl / packages/channels/dws),而本轮不能触碰这些区域:footprint 规则禁止 PR 扩展到其从未触碰过的范围。

本地验证(Linux)

  • cd packages/core && npx vitest run src/agents/runtime/workflow-saved.test.ts src/config/storage.test.ts src/tools/workflow/workflow.test.ts —— 3 个文件,201/201 通过(本 PR 自身的测试套件)。
  • packages/node-replnpx vitest run src/node-repl.semantics.test.ts —— Linux 上 18/18 通过
  • packages/channels/dwsnpx vitest run src/dws-event-stream.test.ts —— Linux 上 6/6 通过
  • packages/vscode-ide-companion:无法在本运行器上执行(其测试套件需要 packages/webui 的 vite 构建产物,本机缺失);上方的 CI 标注即为证据,且其硬编码的 POSIX 期望值并未被本 PR 改动。

结论:失败检查是 main 引起的 Windows 通道损坏,已在不相关分支上复现;本 PR 自身的测试在两个平台上均为绿色。对本分支的正确操作就是不操作。

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


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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No blocking issues. LGTM! ✅

Not explored to full depth (tool budget reached): "agent 6b": none — all checks above ran to completion..

Test Plan (not a blocker): workflows/generated-evil/x.jsno such file or directory; 194 passed — this review observed 21621, 1702, 1659, 601, 4235, 630 passed; 784 passed — this review observed 21621, 1702, 1659, 601, 4235, 630 passed.

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

  • packages/core/src/tools/workflow/workflow.ts:687 — [review] canonical classifier duplicates the loader's root-normalization policy — next root-policy edit must patch both files or the consent label diverges
  • packages/core/src/tools/workflow/workflow.ts:928 — [review] generated-root contract prose in the tool description is anchored by no test despite the file's prose-anchoring convention
  • packages/core/src/agents/runtime/workflow-saved.test.ts:298 — [review] subprocess contract test mirrors the Config registration instead of exercising it — the registered value stays unpinned
中文说明

无阻断问题。LGTM!✅

未探索到全部深度(达到工具调用预算):"agent 6b"none — all checks above ran to completion.

Test Plan(非阻断):workflows/generated-evil/x.jsno such file or directory; 194 passed — this review observed 21621, 1702, 1659, 601, 4235, 630 passed; 784 passed — this review observed 21621, 1702, 1659, 601, 4235, 630 passed

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

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

@yiliang114
yiliang114 added this pull request to the merge queue Aug 26, 2026
Merged via the queue into QwenLM:main with commit ba657c0 Aug 26, 2026
58 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.22.2.

euntaek-hong pushed a commit to wrongbutworks/qwen-code that referenced this pull request Aug 28, 2026
…QwenLM#10119)

* feat(review): emit the Step 3A fan-out as a generated workflow script

`qwen review emit-workflow` builds the roster the same way
`agent-prompt --roster` does — same plan, same `buildLaunch`, same briefs,
same prompts, same recorded delivery evidence — and writes those prompts into
a runnable workflow script instead of printing thirteen blocks for the
orchestrator to copy. The script lives under the generated-scripts root the
Workflow loader trusts since QwenLM#9987 (`$QWEN_CODE_PROJECT_DIR/workflows/
generated/review/<session>/`), so it is never a slash command and needs no
cleanup sweep.

The generated file is a fixed body plus three literals — the roster, the
worktree pin, the subagent type. No logic is generated, only data, and the
tests execute the generator's real output. A territory fan-out (Step 3B) and
an unsized plan are refused before anything is written, because a workflow
returns every agent through one tool result and a roster that grows with
the diff is silently truncated there.

Nothing routes through the command yet: the skill still builds its roster
with `agent-prompt --roster`. Routing is its own change.

Part of QwenLM#8769.

Claude-Session: https://claude.ai/code/session_017cUwuTey4APA8wAyAM6ScS

* test(review): mirror the sandbox runtime in fan-out script tests (QwenLM#10119)

Address review feedback on the emit-workflow PR:

- Run the generated fan-out script in a vm context that mirrors the
  workflow sandbox's execution shape: the meta block is stripped instead
  of executed, the body is wrapped in the runtime's strict-mode async
  IIFE, only the sandbox globals are bound, and the agent stub applies
  the runtime's option gates.
- Extend the determinism guard to the sandbox's full Date surface
  (Date.parse, Date.UTC, bare Date calls).
- Exercise the failed-write half of the temp-and-rename cleanup.
- Cover the handler-level --rules happy path end to end.

* fix(review): harden the generated fan-out path and fail closed (QwenLM#10119)

Address the four review blockers on the emit-workflow PR:

- Share the loader's canonical-containment policy on the write side:
  refuse a symlinked directory from the generated root down to the
  session dir, and prove the canonical session dir stays under the
  canonical root, before any brief, prompt record, or script is written.
- Keep colliding sanitized session ids apart by appending a digest of
  the RAW session id to the readable prefix, so two concurrent sessions
  can never select the same script target for the same plan.
- Canonicalize an existing plan path with realpath before hashing it,
  so one plan keeps one script name under divergent spellings of the
  same file (macOS /var vs /private/var, or a link).
- Fail the fan-out whenever any required agent delivered nothing,
  instead of returning a shortened delivered list; a missing role is a
  failed step, not a shorter finding set.

* test(review): canonicalize fixtures and pin the review-dir symlink guard (QwenLM#10119)

* test(review): pin the emit-workflow cannot-read-the-plan guard (QwenLM#10119)

* test(review): pin emit-workflow dispatch guidance and clean-tree silence (QwenLM#10119)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): drop the duplicated telemetry-swap mock property

The `Merge branch 'main'` in 1f670a1 brought main's TS1117 in with it:
`client.telemetrySwap.test.ts` declares `getToolRegistry` twice in the
same object literal, which fails `tsc` and kills `packages/core`'s build
before a single test runs — the Test job dies in "Install dependencies".

Not this branch's doing. Two main commits added the property
independently and neither saw the other:

  032b907 feat(serve): backfill session PR bindings ... (QwenLM#9729)
  8241905 test(core): give the telemetry-swap client mock a
             getToolRegistry (QwenLM#10220)

`upstream/main` at 053f17b still carries both — checking that exact
file out here and running `tsc --noEmit -p packages/core` reproduces
`client.telemetrySwap.test.ts(103,5): error TS1117` verbatim, so main is
red on its own and every branch that merges it inherits this.

Keeps QwenLM#10220's copy — it was added for this purpose and carries the
explanation — and drops QwenLM#9729's incidental one. main needs the same
removal; this only unblocks the branch.

Verified: `tsc --noEmit -p packages/core` clean,
`client.telemetrySwap.test.ts` 10 passed.

Claude-Session: https://claude.ai/code/session_01M7z4PccYfDPyyfg3oGr8V1

* fix(review): hermetic probe fixtures and honest fan-out failure messages (QwenLM#10119)

The deterministic gate's `--changed` run collects test-efficacy.test.ts
through this PR's lib/paths.ts change, and its skip-worktree guard test
died on a persistent runner: the fixture's raw git calls inherited an
ambient discovery redirect (GIT_INDEX_FILE reproduces the exact failure)
while the guard reads a sanitized env, so the bit landed in another
index and the refusal never fired. Run every fixture git call with the
same sanitized env the guard uses.

Address the maintainer verification of this PR:

- A fan-out where EVERY agent failed prescribed re-running
  emit-workflow, which regenerates the identical script with the
  identical baked-in pin — a loop. Name the dispatch instead.
- The territory refusal claimed results are "silently truncated away";
  the scheduler persists large results and hands the model a pointer.
  Restate the real bound: the run's wall-clock caps and the fail-closed
  guard a per-chunk roster makes near-certain.
- A refused plan no longer leaves the empty session directory (blocker
  check moved ahead of the mkdir).
- The roster-key mismatch guard gains its missing test.

Also drop the duplicate getToolRegistry the merge of main brought into
client.telemetrySwap.test.ts (TS1117 broke `npm run build`; the two
entries were byte-identical).

* test(review): isolate test-efficacy fixtures from host git config (QwenLM#10119)

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants