Skip to content

feat(review): emit Step 3A's fan-out as a workflow instead of a roster - #8846

Closed
qqqys wants to merge 5 commits into
QwenLM:mainfrom
qqqys:feat/review-emit-workflow
Closed

feat(review): emit Step 3A's fan-out as a workflow instead of a roster#8846
qqqys wants to merge 5 commits into
QwenLM:mainfrom
qqqys:feat/review-emit-workflow

Conversation

@qqqys

@qqqys qqqys commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds a deterministic qwen review emit-workflow subcommand that writes Review Step 3A's complete agent fan-out as a fixed workflow script plus an arguments file. The emitted workflow dispatches the same roster with the same prompts as the existing hand-launched path and reports missing agent returns by roster key.

Nothing routes through this command yet. The review skill and finding-delivery format remain unchanged so the code-driven dispatcher can be evaluated independently.

Why it's needed

The current hand-launched path asks an orchestrator to copy every roster entry into separate agent calls without shortening the roster or rewriting prompts. Those conventions can fail after planning has already succeeded. Emitting the fan-out as executable data makes roster width and prompt fidelity properties of the program, while preserving the existing briefs, coverage accounting, and findings flow.

The command fails closed for territory fan-outs and worktree reviews because the Step 3A workflow cannot yet represent those execution contracts safely.

Reviewer Test Plan

How to verify

From packages/cli, run npx vitest run src/commands/review.test.ts src/commands/review/emit-workflow.test.ts src/commands/review/workflow-script.test.ts and expect all 22 cases to pass. Then, from the repository root, run npm run dev -- review emit-workflow --plan <step-1-plan.json> --out <empty-output-directory> with a normal local or diff-only Step 1 plan. Expect the command to print the required agent count and the paths to script.js and args.json; the arguments file should contain every required roster key and its unmodified launch prompt, while stdout should not print the prompts themselves. A territory plan or a PR-worktree plan should stop with a reason and direct the reviewer to agent-prompt --roster instead of emitting a misleading workflow.

Evidence (Before & After)

N/A — this is an additive internal review helper and is not routed into user-visible review behavior yet.

Tested on

OS Status
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

Environment (optional)

Node.js 25.9.0, npm 11.12.1, no sandbox.

Risk & Scope

  • Main risk or tradeoff: The emitted script and arguments file must stay aligned with the workflow runtime contract and the existing review roster.
  • Not validated / out of scope: Routing the review skill through this command, territory fan-outs, worktree-pinned workflow agents, and changing the findings-delivery format.
  • Breaking changes / migration notes: None. This is an additive internal subcommand with no current caller.

Linked Issues

Part of #8769.

中文说明

本 PR 做了什么

新增确定性的 qwen review emit-workflow 子命令,把 Review Step 3A 的完整 agent 扇出写成固定 workflow 脚本和参数文件。生成的 workflow 使用与现有手工发射路径相同的 roster 和 prompt,并按 roster key 报告没有返回结果的 agent。

目前没有任何流程路由到该命令。review skill 和 finding 交付格式保持不变,以便独立评估代码驱动的分派器。

为什么需要

现有手工发射路径要求 orchestrator 把每个 roster 条目复制到独立的 agent 调用中,同时不能缩短 roster 或改写 prompt。这些约定可能在规划已经正确完成后仍被破坏。把扇出输出为可执行数据后,roster 宽度和 prompt 保真度由程序保证,同时保留现有 brief、覆盖率核算和 findings 流程。

对于 territory 扇出和 worktree review,本命令会 fail closed,因为 Step 3A workflow 目前无法安全表达这些执行契约。

Reviewer Test Plan

如何验证

packages/cli 中运行 npx vitest run src/commands/review.test.ts src/commands/review/emit-workflow.test.ts src/commands/review/workflow-script.test.ts,预期 22 条测试全部通过。然后在仓库根目录使用普通 local 或 diff-only Step 1 plan 运行 npm run dev -- review emit-workflow --plan <step-1-plan.json> --out <empty-output-directory>。预期命令打印所需 agent 数量以及 script.jsargs.json 的路径;参数文件包含每个必需 roster key 及其未经改写的启动 prompt,而标准输出不会打印 prompt 内容。使用 territory plan 或 PR-worktree plan 时,命令应说明拒绝原因并指向 agent-prompt --roster,而不是生成可能误导的 workflow。

证据(前后对比)

N/A——这是新增的内部 review helper,目前尚未接入用户可见的 review 行为。

测试平台

OS 状态
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

环境(可选)

Node.js 25.9.0、npm 11.12.1、无 sandbox。

风险与范围

  • 主要风险或取舍:生成的脚本和参数文件必须与 workflow 运行时契约以及现有 review roster 保持一致。
  • 未验证或不在范围内:把 review skill 路由到本命令、territory 扇出、worktree 定位的 workflow agent,以及改变 findings 交付格式。
  • 破坏性变更或迁移说明:无。这是新增的内部子命令,目前没有调用方。

关联 Issue

属于 #8769 的一部分。

`agent-prompt --roster` and the new `emit-workflow` build the same prompts,
from the same plan, through the same function. What differs is who launches
them.

`--roster` prints ~13 blocks and asks the orchestrator to copy each one into
an agent call, in a single response, without editing any of them. Those are
three conventions, and this skill's gate list exists because each has been
broken: a roster that got shortened, prompts that arrived paraphrased, agents
launched with no diff path at all. Every one of those gates detects the
failure after the fact.

`emit-workflow` writes the same blocks into an args file a fixed script
reads, so the orchestrator makes one tool call naming a path. The fan-out's
width becomes a `parallel()` over an array, and each prompt becomes a value
passed by code — neither is something a caller can shorten or reword.

What this deliberately does NOT change: the briefs, the prompts, the roster,
and how findings come back. The agents are the same agents reading the same
briefs. That is the point — this exists to be A/B'd against the hand-launched
path, and a change that also moved the finding format would be comparing two
of everything.

Three things carry the invariant rather than restating it:

- `buildLaunch` is exported and reused rather than reimplemented. Its own
  comment already says one body serves every caller because a drift would
  read as a rewritten launch on a run that did everything right; a second
  builder would have made byte-parity something a test asserts instead of
  something the code cannot break.
- The roster comes from `requiredAgents(plan)` — the same list `check-coverage`
  holds the run to — and the same key-mismatch guard `--roster` makes is made
  here, so a prompt that could never be matched to its requirement is refused
  rather than handed out.
- The script is a fixed constant, not generated source. Generated source can
  only be checked by parsing it; a constant can be executed against a stub
  dispatch and asserted on. It is also where "the orchestrator must not author
  the fan-out" stops being a convention and becomes a fact about the binary.

The script collects `parallel()`'s null elements into `missingRoles` by name.
A dead dispatch is otherwise indistinguishable from an agent that found
nothing, and an agent silently missing from the fan-out is the one regression
this path must not introduce.

Two refusals rather than fallbacks: a territory fan-out (3B) has a per-chunk
contract this script does not express, and a worktree review needs every agent
pinned to the PR worktree, which the workflow runtime cannot yet express.
Either would produce a run that looks complete and reviewed the wrong thing.

Not wired into SKILL.md: this build emits, nothing routes through it yet, so
review behaviour is unchanged.

Part of QwenLM#8769.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@qqqys Thanks for the PR — the direction is clear (it is exactly the emit-workflow step designed in #8769, and "nothing routes through it yet" is sensible scoping), but the body doesn't follow the PR template, so I have to stop here before code review.

What's missing:

  • ## Reviewer Test Plan — the big one. The ## Tests section describes the suite well, but there is no How to verify for a reviewer: what command to run, what plan input to feed qwen review emit-workflow, what output to expect — plus the Tested on OS matrix. The template opens with "Maintainers prioritize PRs with a clear reviewer test plan."
  • ## Risk & Scope — most of this is already in your "What it deliberately does not change" and "Two refusals, not fallbacks" sections; it just needs to sit under the template heading (main risk/tradeoff, what's not validated, breaking changes).
  • ## Linked IssuesPart of #8769 is only mentioned in prose at the end.
  • ## What / ## Why → the template's ## What this PR does / ## Why it's needed (the content itself can stay as-is).

Your recent PRs (#8694, #8735) follow the template exactly, so this reads as a formatting slip rather than a disagreement with it. Restructuring the existing content under the template headings — and adding the reviewer verification plan — should be enough to move this into code review.

中文说明

@qqqys 感谢这个 PR——方向很明确(正是 #8769 设计中的 emit-workflow 那一步,"目前没有任何东西路由到它"的范围控制也很合理),但 PR 正文没有遵循 PR 模板,所以必须先停在模板检查这一步,无法进入代码审查。

缺失的部分:

  • ## Reviewer Test Plan——最关键的一项。## Tests 一节对测试套件描述得很好,但缺少给 reviewer 的 How to verify:运行什么命令、给 qwen review emit-workflow 喂什么 plan 输入、期望看到什么输出,以及 Tested on 操作系统矩阵。模板开头写明"维护者优先处理有清晰评审验证计划的 PR"。
  • ## Risk & Scope——内容其实已经在 "What it deliberately does not change" 和 "Two refusals, not fallbacks" 两节里了,只需要挪到模板标题下(主要风险/权衡、未验证内容、破坏性变更)。
  • ## Linked Issues——Part of #8769 目前只是正文结尾的一句提及。
  • ## What / ## Why → 模板的 ## What this PR does / ## Why it's needed(内容可以原样保留)。

你最近的 PR(#8694#8735)都严格遵循了模板,所以这次看起来只是格式上的疏忽。把现有内容重新组织到模板标题下、并补上给 reviewer 的验证计划,就可以进入代码审查了。

Qwen Code · qwen3.8-max

@qqqys

qqqys commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

已修复

验证证据:

  • ad6578c1ca 同步 emit-workflow 的命令注册契约与 bare-command 提示。
  • cd packages/cli && npx vitest run src/commands/review.test.ts src/commands/review/emit-workflow.test.ts src/commands/review/workflow-script.test.ts:22/22 通过。
  • npx prettier --check src/commands/review.ts src/commands/review.test.tsgit diff --check:通过。

@qqqys

qqqys commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

中文说明

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

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Not explored to full depth (tool budget reached): PR #8846 (QwenLM/qwen-code) adds qwen review emit-workfl...: did not execute the new vitest suites ( emit-workflow.test.ts , workflow-script.test.ts ); findings here are static contract mismatches that unit tests of the …`.

中文说明

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

未探索到全部深度(达到工具调用预算):PR #8846 (QwenLM/qwen-code) adds qwen review emit-workfl...:did not execute the new vitest suites ( emit-workflow.test.ts , workflow-script.test.ts ); findings here are static contract mismatches that unit tests of the …`。

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

Comment on lines +123 to +128
const { key, prompt } = buildLaunch(
report,
planPath,
{ role: req.role, file: req.file },
rules,
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R1-1: buildWorkflowArgs builds every launch prompt via buildLaunch but never calls recordPrompt, while the sibling --roster path records every prompt it hands out (runRoster, agent-prompt.ts:1764). The delivery gate reads only those records: coverageFromTranscripts builds its built map from readRecordedPrompts (lib/coverage.ts:384), which reads only the .txt files recordPrompt writes — the *.brief.md files writeBrief creates are explicitly not what the gate compares.

Failure scenario: anyone who follows this command's own stdout ("make one Workflow call with the paths below") and dispatches the emitted workflow gets a fully correct run that check-coverage then fails wholesale — every roster role classifies as briefless, and the gate exits 3 with "required briefs never reached their agents", prescribing the wrong repair (agent-prompt --roster). Probe-verified at this commit: the handler produces 13 prompts and 0 records; adding the line below flips the probe to 13/13 records with the PR's tests still green.

Fix (mirror runRoster):

recordPrompt(planPath, key, prompt); // after the key-mismatch guard; import from './lib/prompt-record.js'
中文说明

buildWorkflowArgs 通过 buildLaunch 为每个 agent 构建启动 prompt,但从未调用 recordPrompt;而姊妹路径 --rosterrunRoster,agent-prompt.ts:1764)会为它发出的每个 prompt 做记录。交付门禁只读这些记录:coverageFromTranscriptsbuilt 表来自 readRecordedPrompts(lib/coverage.ts:384),只认 recordPrompt 写出的 .txt 文件——writeBrief 写出的 *.brief.md 明确不是门禁比对的对象。

失败场景:任何人按本命令自己 stdout 的指引("make one Workflow call with the paths below")分发这个 workflow,都会得到一次完全正确、却被 check-coverage 整体判失败的运行——每个 roster 角色都被归为 briefless,门禁以 "required briefs never reached their agents" 退出(exit 3),并给出错误的补救指引(agent-prompt --roster)。已在本提交上用探针验证:handler 产出 13 个 prompt、0 条记录;加上下面这一行后探针变为 13/13,且 PR 自带测试仍然全绿。

修复(对齐 runRoster):在 key 不一致守卫之后加 recordPrompt(planPath, key, prompt);(从 ./lib/prompt-record.js 导入)。

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

Comment on lines +182 to +186
writeStdoutLine(
`${payload.agents.length} agents required. The fan-out is a workflow — ` +
'make one Workflow call with the paths below and do not build agent ' +
'calls by hand for this step.',
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R1-2: The printed handoff contract — "make one Workflow call with the paths below" — cannot be honored against the Workflow tool in this build, for three code-verified reasons:

  1. The tool's args parameter accepts inline JSON only ("Pass actual JSON, not a stringified value" — WORKFLOW_PARAM_SCHEMA in packages/core/src/tools/workflow/workflow.ts); there is no argsPath parameter. Passing the printed args: path string binds that string to the sandbox args global, so args.agents is undefined and the script dies inside the vm with a bare TypeError at agents.length.
  2. scriptPath is resolved through resolveSavedWorkflowScriptreadWorkflowFileSecurely, which refuses any path outside .qwen/workflows/ or ~/.qwen/workflows/ — the emitted --out dir is arbitrary.
  3. workflowsEnabled defaults to false (config.ts), so a default session has no Workflow tool to call at all.

Failure scenario: an orchestrator runs emit-workflow and mechanically follows its printed output (the command's stated purpose) — and hits one of the three walls above. The only executable route today is reading args.json and inlining ~13 full launch prompts into the tool call, which re-introduces exactly the relay burden this command's header says it exists to remove ("the prompts are in the args file, which no one is asked to read, retype, or relay").

Suggested fix: land the interface the artifact assumes before routing — an argsPath parameter on WorkflowTool read at execution time like scriptPath, plus a trusted source for review-emitted scripts outside the saved-workflow dirs, gated on isWorkflowsEnabled(). Until then, the printed text should not describe a call the tool cannot honor.

中文说明

打印出来的交接契约——"make one Workflow call with the paths below"——在当前构建的 Workflow 工具上是无法执行的,原因有三(均已在代码中核实):

  1. 工具的 args 参数只接受内联 JSON("Pass actual JSON, not a stringified value"——packages/core/src/tools/workflow/workflow.ts 中的 WORKFLOW_PARAM_SCHEMA),不存在 argsPath 参数。把打印出的 args: 路径字符串传进去,会把该字符串绑定到沙箱的 args 全局变量,于是 args.agentsundefined,脚本在 vm 内部以裸 TypeErroragents.length)终止。
  2. scriptPath 经过 resolveSavedWorkflowScriptreadWorkflowFileSecurely 解析,会拒绝 .qwen/workflows/~/.qwen/workflows/ 之外的任何路径——而 --out 目录是任意的。
  3. workflowsEnabled 默认为 false(config.ts),默认会话里根本没有 Workflow 工具可调。

失败场景:orchestrator 运行 emit-workflow 并机械地照打印输出执行(这正是该命令的既定用途)——然后撞上上述三堵墙之一。当前唯一可执行的路径是读取 args.json 并把约 13 条完整启动 prompt 内联进工具调用,这恰好重新引入了本命令头注释声称要消除的中转负担("prompts 都在 args 文件里,没有人需要阅读、转述或转发它们")。

建议修复:在接线(routing)之前先落地该产物所假定的接口——给 WorkflowTool 增加一个在执行期读取的 argsPath 参数(与 scriptPath 相同),并为 review 产出的脚本提供 saved-workflow 目录之外的可信来源,且以 isWorkflowsEnabled() 作门禁。在此之前,打印文本不应描述一个工具无法履行的调用。

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

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.

Verified against this commit — all three walls are real:

  1. WORKFLOW_PARAM_SCHEMA (packages/core/src/tools/workflow/workflow.ts) has no argsPath parameter; args is an inline-only structured value.
  2. scriptPath resolves through resolveSavedWorkflowScriptreadWorkflowFileSecurely (packages/core/src/agents/runtime/workflow-saved.ts), which refuses any path outside the saved-workflow directories — the arbitrary --out dir included.
  3. workflowsEnabled defaults to false (packages/core/src/config/config.ts), and the Workflow tool is only registered when it is enabled.

So the finding stands: the printed handoff contract cannot be honored by the Workflow tool in this build. What I am NOT doing is settling the fix direction unilaterally, because it is a scope/sequencing call, not a code call:

  • (a) Keep this PR emit-only (its declared scoping: "nothing routes through it yet") and land the Workflow-tool interface the artifact assumes — an argsPath parameter plus a trusted source for review-emitted scripts, gated on isWorkflowsEnabled() — as the next Proposal: rebuild /review Step 3–5 orchestration on the workflow engine #8769 step, before routing. This keeps the core-package interface change in its own dedicated change.
  • (b) Widen this PR to land that interface now. That is a packages/core tool-schema and script-loading-policy change, a scope expansion beyond the PR's stated emit-only intent.
  • (c) Soften the stdout text so it stops describing a call the tool cannot yet honor, at the cost of rewriting the handoff contract Proposal: rebuild /review Step 3–5 orchestration on the workflow engine #8769 designs against.

Recommendation: (a) — the artifact and its contract are the design's current step; the interface is the next one, and landing it in a focused change keeps both reviewable. The thread is left open for that decision.

中文说明

已在当前提交上核实——三堵墙都真实存在:

  1. WORKFLOW_PARAM_SCHEMA(packages/core/src/tools/workflow/workflow.ts)没有 argsPath 参数;args 只接受内联的结构化值。
  2. scriptPathresolveSavedWorkflowScriptreadWorkflowFileSecurely(packages/core/src/agents/runtime/workflow-saved.ts)解析,会拒绝 saved-workflow 目录之外的任何路径——任意的 --out 目录也不例外。
  3. workflowsEnabled 默认为 false(packages/core/src/config/config.ts),Workflow 工具只在启用时才注册。

因此该发现成立:打印出的交接契约在当前构建的 Workflow 工具上无法履行。我没有擅自定死修复方向,因为这是范围/时序决策,不是代码问题:

  • (a)保持本 PR 只做 emit(其声明的范围:"目前没有任何东西路由到它"),把该产物所假定的 Workflow 工具接口——argsPath 参数,加上 review 产出脚本在 saved-workflow 目录之外的可信来源,以 isWorkflowsEnabled() 作门禁——作为 Proposal: rebuild /review Step 3–5 orchestration on the workflow engine #8769 的下一步、在接线之前单独落地。这让 core 包的接口变更保持在自己独立的改动里。
  • (b)扩大本 PR,现在就把该接口落地。那是 packages/core 的工具 schema 与脚本加载策略变更,超出了本 PR 声明的 emit-only 意图。
  • (c)弱化 stdout 文本,让它不再描述工具尚无法履行的调用——代价是重写 Proposal: rebuild /review Step 3–5 orchestration on the workflow engine #8769 设计所依赖的交接契约。

建议:(a)——产物与契约是设计的当前一步,接口是下一步,单独落地可以让两者都可评审。线程保持打开,等待该决策。

'bug in the CLI, not in the call.',
);
}
return { key, label: rosterLabel(req), prompt };

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: WorkflowAgentSpec.label is computed via rosterLabel() and serialized into every args file, but has zero read sites: the emitted script — the only consumer of args.json — dispatches with agent(a.prompt, { label: a.key, ... }), and the runtime's progress display consumes opts.label. The doc comment ("Human-readable identity, for the run's progress display") describes a use no code performs. Project rule (AGENTS.md): "For every added field, option, or optional parameter, grep its read sites."

Concrete cost: every args file carries ~13 sanitized label strings nothing reads; the progress display shows terse roster keys (1a, 3b, 7) while the field's doc promises it the human-readable identity; a maintainer who changes or localizes rosterLabel output (exported by this same PR) expecting the workflow run's display to change sees no effect.

Suggested fix: dispatch with the carried label — agent(a.prompt, { label: a.label, phase: 'Review' }) in workflow-script.ts, updating the test that pins label to the key — or drop the field from WorkflowAgentSpec until a consumer exists.

中文说明

WorkflowAgentSpec.labelrosterLabel() 计算并写入每个 args 文件,但没有任何读取方:唯一消费 args.json 的脚本在分发时用的是 agent(a.prompt, { label: a.key, ... }),而运行时的进度展示消费的是 opts.label。字段注释("Human-readable identity, for the run's progress display")描述了一个没有任何代码实现的用途。项目规则(AGENTS.md):"对每个新增字段、选项、可选参数,都要 grep 它的读取方"。

具体代价:每个 args 文件都携带约 13 条无人读取的、已清洗的 label 字符串;进度展示显示的是简短的 roster key(1a3b7),而字段注释承诺的是人类可读的名称;维护者若修改或本地化 rosterLabel 的输出(本 PR 同时导出了它),以为 workflow 运行的展示会随之变化,实际不会有任何变化。

建议修复:在 workflow-script.ts 中分发时携带该 label——agent(a.prompt, { label: a.label, phase: 'Review' }),并更新当前把 label 钉为 key 的测试;或者在出现消费方之前,从 WorkflowAgentSpec 中移除该字段。

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

Comment on lines +64 to +65
/** Bumped when the script's expectations of this file change. */
version: 1;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R1-4: WorkflowArgsFile.version is written (line 143: return { version: 1, plan: planPath, mode, agents }) and asserted in the test, but read by nothing — the string version never appears in the script constant and the runtime passes args through opaquely, so the compatibility protocol the doc comment implies has no enforcement point. That is against this file's own stated philosophy of "refusals rather than fallbacks".

Concrete cost: when the payload shape next changes, the author bumps version per the comment; no consumer checks it, so a stale args.json left in a reused --out dir against a rebuilt script (e.g. a CI retry after a CLI upgrade where only one of the two files is regenerated) misreads the payload silently — args.agents undefined or carrying missing fields — instead of failing closed.

Suggested fix: have the script fail closed on a mismatch (string-concatenation style per the file's constraints), or drop the field until a reader exists:

if (args.version !== 1) {
  throw new Error('args version ' + args.version + ' does not match this script - re-run emit-workflow');
}
中文说明

WorkflowArgsFile.version 被写入(第 143 行:return { version: 1, plan: planPath, mode, agents })并在测试中断言,但没有任何读取方——脚本常量中完全没有出现 version 字样,运行时也只是透明地传递 args,因此注释所暗示的兼容协议没有任何执行点。这与本文件自己声明的 "拒绝而非降级"(refusals rather than fallbacks)原则相悖。

具体代价:下次 payload 结构变化时,作者按注释把 version 加一;但没有任何消费方检查它,于是残留在复用 --out 目录里的旧 args.json 配上重建后的脚本(例如 CLI 升级后的 CI 重试只重新生成了两个文件之一)会静默地误读 payload——args.agents 为 undefined 或缺字段——而不是 fail closed。

建议修复:让脚本在版本不匹配时 fail closed(按本文件的约束用字符串拼接风格),或在出现读取方之前移除该字段(见上方代码)。

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

Comment on lines +133 to +135
if (key !== req.key) {
throw new Error(
`emit-workflow: built "${key}" where the roster requires "${req.key}" ` +

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-5: The roster→buildLaunch→key-match loop is a second implementation of runRoster's mapping (agent-prompt.ts:1739-1766), guarded by the same key-parity invariant — two copies of one guard, the exact shape the new buildLaunch doc comment warns against ("a third caller that rebuilt this would be a second implementation of the invariant"). The implementations already differ: runRoster handles chunk specs and calls recordPrompt; this loop omits both — and that omission is not hypothetical drift, it is the live divergence behind R1-1.

Concrete cost: if key derivation or guard semantics ever change (e.g. the role--file key format for invariant agents), both files must change together; missing one makes --roster fail closed while emit-workflow silently emits agents whose keys check-coverage can never match — the exact false "brief never reached an agent" failure the guard exists to prevent, on only one dispatch path. The #8769 next dispatcher would copy the guard a third time.

Suggested fix: extract one mapper beside buildLaunch (e.g. buildRosterLaunches(report, planPath, rules)) owning requiredAgentsbuildLaunch → the key guard; runRoster and buildWorkflowArgs both consume it, each adding its caller-specific concerns (recordPrompt / chunk refusal).

中文说明

roster→buildLaunch→key 匹配的循环是 runRoster 映射逻辑(agent-prompt.ts:1739-1766)的第二份实现,由同一个 key 一致性不变量守护——同一个守卫有了两份拷贝,正是新增的 buildLaunch 文档注释所警告的形态("第三个重建此逻辑的调用方将构成该不变量的第二份实现")。两份实现已经出现差异:runRoster 处理 chunk spec 并调用 recordPrompt;本循环两者都省略了——这个省略不是假想的漂移,而是 R1-1 背后的现实分歧。

具体代价:一旦 key 推导或守卫语义发生变化(例如 invariant agent 的 role--file key 格式),两个文件必须同步修改;漏改其一,--roster 会 fail closed,而 emit-workflow 会静默发出 check-coverage 永远无法匹配的 agent key——正是该守卫要防止的 "brief never reached an agent" 误报,且只发生在其中一条分发路径上。#8769 的下一个 dispatcher 将第三次复制这个守卫。

建议修复:在 buildLaunch 旁边抽出一个映射函数(例如 buildRosterLaunches(report, planPath, rules)),统一持有 requiredAgentsbuildLaunch → key 守卫;runRosterbuildWorkflowArgs 都消费它,各自附加调用方特有的逻辑(recordPrompt / chunk 拒绝)。

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

Comment on lines +128 to +130
it('reports the review mode it built for', () => {
expect(buildWorkflowArgs(localPlan(), planPath).mode).toBe('local');
});

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-9: The diff-only review mode never passes through buildWorkflowArgs in any test: every fixture is a local plan (untrackedFiles: []), and the single mode assertion uses a local plan. Mutant physically verified: hardcoding mode: 'local' in the return (emit-workflow.ts:143) survives all 10 tests.

Concrete cost: a cross-repo lightweight review (the plan-diff path, which reviewMode classifies as diff-only) would emit args.json declaring "mode": "local" without any test noticing — misdescribing the run's capabilities (e.g. implying a tree available for build/grep) in every record derived from the artifact. roster.test.ts covers reviewMode itself; the gap is emit-workflow's wiring of the diff-only shape, including its reduced roster (1c/7 absent).

Suggested fix:

expect(
  buildWorkflowArgs(localPlan({ untrackedFiles: undefined }), planPath).mode,
).toBe('diff-only');
中文说明

diff-only review 模式在任何测试中都没有流经 buildWorkflowArgs:所有 fixture 都是 local plan(untrackedFiles: []),唯一的 mode 断言也用的是 local plan。已实际做变异验证:把返回值里的 mode 硬编码为 'local'(emit-workflow.ts:143),全部 10 个测试依然通过。

具体代价:一次跨仓库轻量 review(plan-diff 路径,reviewMode 将其归类为 diff-only)会产出声明 "mode": "local"args.json 而没有任何测试发现——在由该产物派生的每一条记录中错误描述该运行的能力(例如暗示存在可用于 build/grep 的代码树)。roster.test.ts 覆盖了 reviewMode 本身;缺口在 emit-workflow 对 diff-only 形态的接线,包括它缩减后的 roster(不含 1c/7)。

建议修复:见上方代码——补一个 diff-only 形态的用例。

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

Comment on lines +41 to +42
const agents = args.agents;
log(agents.length + ' agents required by the plan');

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-10: The emitted script performs no shape validation on args. Probe-reproduced by executing the real constant with three malformed bindings — args = undefined, args = '/tmp/out/args.json' (a path string — the exact R1-2 mix-up), and args without an agents key: all three crash with an opaque vm TypeError (Cannot read properties of undefined) naming nothing about the args file or the emit-workflow command.

Concrete cost: the debugger of a failed workflow run is left to guess between a script bug, a tool-wiring bug, and a bad args file — an opaque sandbox crash whose cause lives in a different command's stdout text.

Suggested fix (guard at the top of the script body; plain quotes only — the script forbids backticks):

Suggested change
const agents = args.agents;
log(agents.length + ' agents required by the plan');
if (!args || !Array.isArray(args.agents)) {
throw new Error('review-step-3a: args.agents is missing or not an array - pass the args.json that qwen review emit-workflow wrote');
}
const agents = args.agents;
log(agents.length + ' agents required by the plan');
中文说明

产出的脚本没有对 args 做任何形状校验。已用真实常量实际执行复现:三种错误绑定——args = undefinedargs = '/tmp/out/args.json'(路径字符串——正是 R1-2 的混淆情形)、以及缺少 agents 键的 args——全部以不透明的 vm TypeErrorCannot read properties of undefined)崩溃,错误信息中完全不提 args 文件或 emit-workflow 命令。

具体代价:一次 workflow 运行失败后,排查者只能在脚本 bug、工具接线 bug、坏 args 文件之间猜测——一个不透明的沙箱崩溃,而原因却藏在另一条命令的 stdout 文本里。

建议修复:在脚本体开头加守卫(见上方 suggestion;注意只能用普通引号——脚本禁止反引号)。探针验证:加上守卫后三种错误绑定都变为可操作的错误信息,原有 7 个正常路径测试仍全绿。

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

0,
REVIEW_STEP_3A_WORKFLOW_SCRIPT.indexOf('\n};') + 3,
);
expect(metaBlock).not.toMatch(/\$\{|\bfunction\b|\(\s*\)|\.\.\./);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R1-11: The meta pure-literal assertion only detects ${, the function keyword, empty-paren calls, and spread — identifier references and calls-with-arguments pass it, so the contract its own comment states ("must contain no variables, calls") is not actually pinned.

Failure scenario (probe-confirmed against the real extractAndStripMeta from core): a future edit introducing description: 'Review Step 3A: ' + args.mode into the meta literal contains none of the regex's shapes, so this test and the whole CLI suite pass green — but the sandbox evaluates meta in a bare vm context with a null-prototyped globalThis and no bridge globals, so dispatch dies with extractAndStripMeta: failed to evaluate meta object literal: ReferenceError: args is not defined before any agent launches. That is exactly the runtime failure the test's comment ("the runtime reads it before executing anything") claims to prevent; the sandbox's own regression test (workflow-sandbox.test.ts:246) pins that throw.

Suggested fix (verified: flips the mutant to failing, original script still passes): do what the runtime does — slice the meta object literal, evaluate (${metaSource}) in a bare node:vm context (vm.createContext(Object.create(null))), and assert it yields the expected plain object (name, description, phases[].title).

中文说明

meta "纯字面量" 断言只能检出 ${function 关键字、空括号调用和 spread——标识符引用和带参数的调用都能通过它,因此该测试注释自己声明的契约("不得包含变量、调用")实际上并未被钉住。

失败场景(已用 core 中真实的 extractAndStripMeta 探针验证):未来若在 meta 字面量中引入 description: 'Review Step 3A: ' + args.mode,它不含该正则覆盖的任何形态,于是本测试乃至整个 CLI 测试套件全绿——但沙箱会在裸 vm 上下文(globalThis 无原型、无 bridge 全局变量)中求值 meta,分发会在任何 agent 启动之前就以 extractAndStripMeta: failed to evaluate meta object literal: ReferenceError: args is not defined 终止。这正是该测试注释("运行时在执行任何内容之前先读取它")声称要防止的运行时失败;沙箱自己的回归测试(workflow-sandbox.test.ts:246)钉住了这个抛错。

建议修复(已验证:可使上述变异体失败,原脚本仍通过):做运行时所做的事——切出 meta 对象字面量,在裸 node:vm 上下文(vm.createContext(Object.create(null)))中求值 (${metaSource}),断言它产出预期的纯对象(namedescriptionphases[].title)。

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

* test asserts rather than something the code cannot break.
*/
function buildLaunch(
export function buildLaunch(

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-12: Test-efficacy probe (harness validated): reverting this hunk — the export on buildLaunch plus the rewritten byte-parity comment — on its own left every probed test green. The change ships with nothing in the collocated suite gating it; the only in-diff pin is emit-workflow.test.ts' import + byte-parity test, which the probe's affected set did not exercise.

Concrete cost: a future change that undoes this hunk or the invariant it documents (e.g. gives emit-workflow its own prompt builder) keeps the collocated suite green at change time, and the byte drift between the workflow fan-out and the hand-launched --roster path surfaces only in production, where the delivery check reads a correctly-run workflow review as "the prompt was rewritten".

Suggested fix: either confirm emit-workflow.test.ts' byte-parity test is considered the gate for this hunk, or add an assertion in the collocated agent-prompt.test.ts that the exported builder is what emit-workflow consumes.

中文说明

测试有效性探针(harness 已验证):单独还原这个 hunk——buildLaunchexport 加上重写后的字节一致性注释——所有被探测的测试依然全绿。该改动上线时没有任何同目录(collocated)测试为其把关;diff 内唯一的钉住来自 emit-workflow.test.ts 的导入 + 字节一致性测试,而探针的影响集没有运行到它。

具体代价:未来若有改动撤销这个 hunk 或它所记录的不变量(例如给 emit-workflow 一个自己的 prompt 构建器),同目录测试套件在改动当时仍为绿色,workflow 扇出与手工发射 --roster 路径之间的字节漂移只会在生产环境暴露——届时交付检查会把一次正确运行的 workflow review 读成 "prompt 被改写"。

建议修复:要么确认 emit-workflow.test.ts 的字节一致性测试被视为该 hunk 的把关者,要么在同目录的 agent-prompt.test.ts 中补一条断言,验证导出的构建器就是 emit-workflow 所消费的。

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Declined (no code change) with evidence: the gate for this hunk exists and is exercised — the probe's affected set simply did not include it. emit-workflow.test.ts imports buildLaunch from ./agent-prompt.js for its byte-parity test, and the production/test compilation also depends on the export. Mutation-verified at this commit: removing the export makes the byte-parity test fail (TypeError: buildLaunch is not a function), and npm run typecheck fails too (the test file is inside the typechecked src/**/*.ts include); reverting restores green. So we confirm the first option the finding offers: emit-workflow.test.ts' byte-parity test IS the gate for this hunk, by deliberate placement — the parity assertion lives with the consumer that depends on it.

中文说明

拒绝修改(不动代码),附证据:这个 hunk 的把关测试存在且会运行——只是探针的影响集没有包含它。emit-workflow.test.ts./agent-prompt.js 导入 buildLaunch 用于其字节一致性测试,生产/测试代码的编译也依赖该导出。已在本提交上做变异验证:去掉 export 后字节一致性测试失败(TypeError: buildLaunch is not a function),npm run typecheck 同样失败(该测试文件在被类型检查的 src/**/*.ts include 之内);还原后恢复全绿。因此确认该发现提供的第一个选项:emit-workflow.test.ts 的字节一致性测试就是这个 hunk 的把关者,且是刻意放置的——一致性断言与依赖它的消费方放在一起。

* spaces, and the separator glyph is stripped so a name cannot imitate one.
*/
function rosterLabel(req: RequiredAgent): string {
export function rosterLabel(req: RequiredAgent): string {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R1-13: Test-efficacy probe (harness validated): reverting this hunk — the export on rosterLabel — on its own left every probed test green. emit-workflow.ts writes label: rosterLabel(req) into every agent record, but no test asserts anything about label (and the script labels dispatches by a.key — see R1-3).

Concrete cost: a future refactor of emit-workflow.ts:140 substituting a different/empty label source keeps every test green, so the workflow args file silently carries wrong agent display names and any consumer keyed on label misidentifies agents with no test signal at the point of divergence.

Suggested fix: add an assertion in emit-workflow.test.ts that each emitted agent's label matches rosterLabel's format for that requirement (e.g. the role label shape).

中文说明

测试有效性探针(harness 已验证):单独还原这个 hunk——rosterLabelexport——所有被探测的测试依然全绿。emit-workflow.ts 把 label: rosterLabel(req) 写入每一条 agent 记录,但没有任何测试对 label 做断言(而且脚本分发时用的是 a.key 作标签——见 R1-3)。

具体代价:未来若重构 emit-workflow.ts:140、改用其他/空的 label 来源,所有测试仍为绿色,workflow args 文件会静默携带错误的 agent 展示名称,任何以 label 为键的消费方都会在分叉点上误认 agent,且得不到任何测试信号。

建议修复:在 emit-workflow.test.ts 中补一条断言,验证每个产出 agent 的 label 符合 rosterLabel 对该要求的格式(例如 role 标签形态)。

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

@qwen-code-dev-bot

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

Copy link
Copy Markdown
Collaborator

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

中文说明

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

'bug in the CLI, not in the call.',
);
}
return { key, label: rosterLabel(req), prompt };

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.

buildWorkflowArgs never calls recordPrompt, so the coverage gate sees the whole roster as unlaunched.

runRoster (agent-prompt.ts, roster loop ~1743-1765) calls recordPrompt(planPath, key, prompt) for every agent. buildWorkflowArgs copies that loop but drops the call — it only returns { key, label, prompt }, and the file does not even import recordPrompt.

lib/coverage.ts reads those records via readRecordedPrompts(planPath) to compute builtOf(key). With no .txt records the map is empty, so nobodyBuiltAnything (roster.length > 1 && every role briefless) is true. A review run entirely through emit-workflow — where all 11–13 agents actually ran and delivered findings — makes check-coverage emit the collapsed disclosure "every dimension — none of the N required agents is on record as launched with a prompt this skill built", exit 3, and compose-review renders that line as the CHANGES_REQUESTED body.

The fan-out is byte-identical to --roster, yet the run is reported to the PR author as entirely unreviewed.

// CLI computed; this loop cannot shorten it, and there is no branch in which
// an agent is skipped.
const returns = await parallel(
agents.map((a) => () => agent(a.prompt, { label: a.key, phase: 'Review' })),

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.

Workflow-dispatched agents write no subagent JSONL, which is the only evidence check-coverage accepts that an agent ran.

coverage.ts calls readTranscripts(mtimeMs, env, plan.diffPathAbsolute), which reads <QWEN_CODE_PROJECT_DIR>/subagents/<QWEN_CODE_SESSION_ID>/*.jsonl (transcripts.ts:99-109). Those files are written only by the Agent tool — getAgentJsonlPath is called solely in packages/core/src/tools/agent/agent.ts and background-agent-resume.ts.

This dispatch is a bare agent(a.prompt, { label, phase }), so the runtime takes the workflow fast path: createProductionDispatch builds an AgentHeadless directly and never wires a transcript path. A fan-out dispatched by this script therefore produces zero transcripts — readTranscripts either throws TranscriptsUnavailableError ("no subagent transcripts at …") when the dir does not exist, or returns an empty array so every roster entry reads as not-launched.

Together with the missing recordPrompt, Step 3A halts at the coverage gate no matter how well the agents performed.

mkdirSync(outDir, { recursive: true });
const scriptPath = resolve(outDir, 'script.js');
const argsPath = resolve(outDir, 'args.json');
writeFileSync(scriptPath, REVIEW_STEP_3A_WORKFLOW_SCRIPT, 'utf8');

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.

script.js is written into an arbitrary --out directory, but Workflow({scriptPath}) refuses to load any file outside the saved-workflow dirs.

resolveSavedWorkflowScript (packages/core/src/agents/runtime/workflow-saved.ts:222) routes every scriptPath through readWorkflowFileSecurely, which realpaths the file and rejects it unless it sits under <repo>/.qwen/workflows or ~/.qwen/workflows (workflow-saved.ts:167-172, getSavedWorkflowDirs :84-88).

--out is an unconstrained yargs string with no validation and no steering — the tests use mkdtempSync(tmpdir()), and the review skill's scratch dirs live under .qwen/tmp/. An orchestrator that follows the printed scriptPath: line gets:

workflow({scriptPath: '/…/out/script.js'}): refusing to load a workflow file outside the saved-workflow directories

Step 3A launches zero agents and the review stalls at the fan-out.

'calls by hand for this step.',
);
writeStdoutLine(`scriptPath: ${scriptPath}`);
writeStdoutLine(`args: ${argsPath}`);

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.

The printed args: <path> line is not something the Workflow tool can consume — args takes inline JSON, and there is no argsPath.

WORKFLOW_PARAM_SCHEMA (packages/core/src/tools/workflow/workflow.ts:33-58) accepts script XOR scriptPath, plus args, documented as "Optional structured value bound to the args global. Pass actual JSON, not a stringified value." Nothing in core reads an args.json from disk — grepping for args.json/argsPath finds no reader.

An orchestrator that follows "make one Workflow call with the paths below" literally calls Workflow({scriptPath: '…/script.js', args: '…/args.json'}). Inside the sandbox args is then the path string, args.agents is undefined, and agents.length in the emitted script throws TypeError: Cannot read properties of undefined (reading 'length'). Zero agents dispatched.

It also undercuts the stated premise that the model "never sees a prompt": the only call that actually works requires reading args.json and inlining all ~13 prompts into the tool call.

* compares agents against records — a drift between the two paths would read as a
* rewritten launch on a run that did everything right.
* One body for every caller on purpose: the single-agent path, `--roster` and
* `emit-workflow` must emit byte-identical prompts for the same agent, because

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.

Byte-identical prompts do not mean identical agents: workflow dispatch replaces the system prompt with one that orders the agent to be terse.

This docstring's invariant is that --roster and emit-workflow launch the same agent. But the emitted script dispatches with only label/phase, which takes the workflow fast path, and that path overrides the system prompt with WORKFLOW_SUBAGENT_SYSTEM_PROMPT (workflow-orchestrator.ts:437-439) — it tells the agent its final text is a return value to a script and ends with "Be concise. The script will parse your output."

Review briefs ask for multi-field findings with file/line/failure-scenario prose. The same roster run through the workflow yields shorter, thinner findings than --roster for the same diff. Since the PR's stated purpose is an A/B comparing two dispatchers, the comparison silently varies the agents' output contract as well as who launches them.

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.

Escalated to maintainer — not fixable from this PR's emitted artifact. Verified at the code: the terseness directive comes from the workflow runtime's fast path — WORKFLOW_SUBAGENT_SYSTEM_PROMPT (workflow-orchestrator.ts) replaces the general-purpose system prompt on dispatches without agentType, and KNOWN_AGENT_OPTS in workflow-sandbox.ts offers no opt-out. So the same roster run through the workflow really does get a different output contract than --roster; that is a property of the runtime, not of the script this command emits.

Options as I see them:

  1. Accept and document that this A/B compares dispatcher + runtime policy as one bundle (cheapest; the comparison stays internally valid, just narrower than the stated premise).
  2. Add a runtime opt that keeps the general-purpose system prompt for dispatched review agents (core change in workflow-orchestrator.ts; also needed for the per-agent caps in the sibling thread).
  3. Hold the A/B until (2) lands.

My recommendation: (1) now, with (2) tracked as a follow-up — but this is an experiment-design call, so I am leaving the question open: which of the three do you want?

中文说明

升级给维护者——无法在本 PR 的发出物层面修复。 已在代码中核实:简洁化指令来自 workflow 运行时的快速路径——WORKFLOW_SUBAGENT_SYSTEM_PROMPT(workflow-orchestrator.ts)在不带 agentType 的分发中替换通用系统提示,而 workflow-sandbox.ts 的 KNOWN_AGENT_OPTS 不提供退出选项。因此同一份 roster 经 workflow 运行时的确会得到与 --roster 不同的输出契约;这是运行时的属性,不是本命令所发脚本的属性。

我看到的选项:

  1. 接受并声明:本 A/B 比较的是分发器 + 运行时策略这一整体(成本最低;比较仍然内部有效,只是范围比声明的前提更窄)。
  2. 为分发的评审 agent 增加保留通用系统提示的运行时选项(workflow-orchestrator.ts 的 core 改动;姊妹线程中的每 agent 上限也需要它)。
  3. 在 (2) 落地前暂缓该 A/B。

我的建议:先按 (1) 执行,并把 (2) 作为后续工作跟踪——但这属于实验设计决策,因此问题保持开放:三者之中您希望选哪一个?

'bug in the CLI, not in the call.',
);
}
return { key, label: rosterLabel(req), prompt };

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.

WorkflowAgentSpec.label has no read site — the script labels agents by key.

The field is documented as "Human-readable identity, for the run's progress display", but workflow-script.ts:48 dispatches agent(a.prompt, { label: a.key, phase: 'Review' }). The orchestrator's emitter.agentDispatched(label) therefore feeds the TUI raw roster keys (1a, 3c, invariant-a--src/x.ts) instead of Agent 1a: Line-by-line correctness — exactly the display this field was added for.

Grepping .label across the diff and the repo finds no consumer of WorkflowAgentSpec.label; the rosterLabel export newly added to agent-prompt.ts exists only to populate it. Either pass label: a.label in the script, or drop the field and the export.

return { key, label: rosterLabel(req), prompt };
});

return { version: 1, plan: planPath, mode, 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.

WorkflowArgsFile.version is written but never read, so the compatibility check it claims to provide does not exist.

The field is documented "Bumped when the script's expectations of this file change", but REVIEW_STEP_3A_WORKFLOW_SCRIPT reads only args.agents — it never inspects args.version. The sole read site in the repo is expect(args.version).toBe(1) in emit-workflow.test.ts.

If the payload shape later changes and the version is bumped, an older script.js sitting in a stale --out directory still runs against the new args and fails on the shape rather than on the version — precisely the outcome the field was added to prevent. Per AGENTS.md, a field whose only reader is its own test is a dead switch: either have the script assert the version, or drop the field.


phase('Review');

const agents = args.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.

args.agents is dereferenced with no shape check, so a malformed Workflow call dies on an anonymous TypeError.

Because the payload has to be inlined by hand into the Workflow call (there is no argsPath — see the args: <path> comment), an orchestrator that passes args: {}, forgets the key, or passes the path string gets:

TypeError: Cannot read properties of undefined (reading 'length')

with no mention of args.json, the roster, or emit-workflow. The review step fails with a message that points at the sandbox rather than at the malformed call. A one-line guard (if (!Array.isArray(args?.agents)) throw new Error('review workflow: args.agents must be the array written by \qwen review emit-workflow`')`) turns this into a diagnosable failure.

log(missingRoles.length + ' agent(s) returned nothing: ' + missingRoles.join(', '));
}

return {

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.

Collapsing the whole roster into one workflow run puts every agent behind a single wall-clock cap with no partial-result path.

The sandbox arms a wall-clock watchdog (default 30 min, workflow-sandbox.ts:~1120/1155) that aborts and rejects the whole run. Since this script only returns after parallel() settles, a high-effort review whose slowest agent runs past the cap loses every already-completed agent's findings — the run rejects with Workflow execution exceeded … ms and delivers nothing.

The --roster path has no such all-or-nothing failure: each agent returns to the orchestrator independently as it finishes, so a single slow agent costs one dimension, not the review. Worth either raising/plumbing the cap for this run or checkpointing per-agent results as they settle.

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.

Escalated to maintainer — core-runtime decision, same cluster as the per-agent cap threads. Verified: the sandbox arms a wall-clock watchdog (default 30 min, QWEN_CODE_MAX_WORKFLOW_SECONDS) that rejects the whole run, and this script returns only after parallel() settles — so a single slow agent past the cap discards every already-completed agent's findings, where --roster loses one dimension at most. That asymmetry is real.

What this round did within the artifact: a zero-delivery fan-out now throws instead of returning a clean shape (so total failure is at least legible), and partial failures still return their delivered results. What the artifact cannot do: checkpoint per-agent results as they settle — the sandbox has no filesystem, and log() output on a rejected run is not a retrieval path the skill can consume. So the substantive fixes are runtime-side: raising/plumbing the wall-clock cap for review fan-outs, or a journal/snapshot path that survives an abort (both also serve #8769's journal-based coverage). Recommendation: track with the sibling threads as one follow-up; maintainer decision requested there.

中文说明

升级给维护者——core 运行时决策,与每 agent 上限线程同属一簇。 已核实:沙箱装有墙钟看门狗(默认 30 分钟,QWEN_CODE_MAX_WORKFLOW_SECONDS),到点即拒绝整个运行;而本脚本只在 parallel() 全部落定后才返回——因此一个慢 agent 越过上限会丢弃所有已完成 agent 的发现,而 --roster 至多损失一个维度。这个不对称是真实的。

本轮在发出物范围内做到的:零交付的扇出现在会抛错而不是返回一个形状"干净"的结果(至少让整体失败可读);部分失败仍正常返回已交付结果。发出物做不到的:在每个 agent 落定时做检查点——沙箱没有文件系统,被拒运行的 log() 输出也不是 skill 可消费的回取路径。因此实质修复在运行时侧:为评审扇出提高/接通墙钟上限,或提供能在中止后留存的 journal/snapshot 路径(两者同时服务 #8769 的 journal 版 coverage)。建议:与姊妹线程合并为一个后续工作跟踪;在那里请求维护者决策。

writeStdoutLine(`args: ${argsPath}`);
}

export const emitWorkflowCommand: CommandModule = {

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.

emit-workflow has no caller — no skill, doc, or code outside its own two files references it.

git grep -n "emit-workflow\|emitWorkflow" on the PR branch hits only review.ts:28 (import), review.ts:61 (registration), and the feature's own files/tests. .qwen/skills/** — including the review SKILL.md that actually drives Step 3A — docs, and every other code path never invoke it.

Combined with the pr-worktree refusal (which excludes every PR review), the subcommand is reachable only by hand on a local uncommitted diff. The CLI's public subcommand list and help string grow by an entry that no supported flow can use, and the dead-code check for the whole feature reduces to "the tests call it". If this is intentionally staged ahead of the skill change, saying so in the PR body (or gating it behind an env flag until the skill lands) would make that legible.

@wenshao

wenshao commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Code review — 10 findings (high effort)

Ten inline comments are on the diff. Summary of what they add up to:

The emit-workflow path cannot complete a review end to end. Four independent breaks, any one of which is fatal:

  1. buildWorkflowArgs never calls recordPrompt, the one thing --roster does for every agent. lib/coverage.ts reads those records, finds none, and nobodyBuiltAnything fires. (r3748748934)
  2. Agents dispatched through the workflow runtime write no subagent JSONL — the only other evidence check-coverage accepts that an agent ran. (r3748749123)
  3. script.js lands in an arbitrary --out dir, but Workflow({scriptPath}) refuses anything outside .qwen/workflows. (r3748749274)
  4. The printed args: <path> line is not a Workflow parameter — args takes inline JSON, there is no argsPath. (r3748749523)

(3) and (4) mean the fan-out errors before dispatching a single agent; (1) and (2) mean that even if it dispatched, a run in which all 11–13 agents delivered findings is reported to the PR author as entirely unreviewed — collapsed disclosure, exit 3, CHANGES_REQUESTED. Note (4) also undercuts the "the model never sees a prompt" premise: the only Workflow call that actually works requires inlining all ~13 prompts into the tool call.

The dispatcher-only A/B is not clean. Byte-identical prompts do not mean identical agents — the workflow fast path substitutes WORKFLOW_SUBAGENT_SYSTEM_PROMPT ("Be concise. The script will parse your output.") for the general-purpose system prompt (r3748749680), and collapsing the roster into one run puts every agent behind a single 30-minute all-or-nothing wall clock (r3748750409). Both change the measured variable alongside the dispatcher.

Cleanup: WorkflowAgentSpec.label (r3748749844) and WorkflowArgsFile.version (r3748750039) are written and never read; args.agents is dereferenced with no shape check (r3748750232); and no skill, doc, or code path outside the feature's own two files calls emit-workflow at all (r3748750573).

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Review round summary (PR #8846)

Commit: f748caf6e1fix(review): record emit-workflow prompts and fail closed on bad args (5 files, +230/−75). 11 of the 13 inline findings are resolved in code; 1 is declined with evidence; 1 (Critical R1-2) is verified but escalated for a maintainer scope/sequencing decision, with its thread left open and an in-thread reply posted.

Resolved in code

  • R1-1 (Critical)buildWorkflowArgs now calls recordPrompt(planPath, key, prompt) for every agent it emits, after the key-parity guard (which moved into the shared mapper, so recording can only happen for a matched key). The delivery gate reads exactly these records, so a correctly dispatched workflow run no longer fails check-coverage as "briefless". Pinned by a new assertion: every emitted agent's recorded prompt equals its args-file prompt byte-for-byte.
  • R1-3 — the emitted script now dispatches with the carried label (agent(a.prompt, { label: a.label, ... })), giving WorkflowAgentSpec.label a real read site matching its doc comment; the script test that pinned labels to roster keys now pins them to the human-readable labels.
  • R1-4 — the script fails closed on a version mismatch (args.version !== 1 → throw, telling the caller to re-run emit-workflow to regenerate both files together), plus a test.
  • R1-5 — extracted buildRosterLaunches(report, planPath, rules) beside buildLaunch in agent-prompt.ts, owning requiredAgentsbuildLaunch → the key-parity guard once. runRoster and buildWorkflowArgs both consume it, each keeping only its caller-specific concern (recordPrompt / chunk refusal). The guard no longer exists in two copies, and the live divergence behind R1-1 is structurally gone. --roster output is byte-identical (the existing 214-test suite, including the literal block-header pins, passes unchanged).
  • R1-6 — new handler test: a readable --rules file reaches the ## Project rules section of every emitted brief — including the declared exception pinned the other way: Agent 7 (Build & Test) must NOT receive rules, per the SKILL.md rule already enforced in buildRoleBrief.
  • R1-7stdioHelpers is now mocked (house pattern) in the write test, which asserts exactly three stdout lines: the count line, scriptPath: <out>/script.js, and args: <out>/args.json — the label/value pairing is pinned with exact equality, so the swap mutant the finding describes now fails.
  • R1-8 — the written script.js is now asserted with toBe(REVIEW_STEP_3A_WORKFLOW_SCRIPT) (byte-for-byte against the exhaustively tested constant) instead of two substrings.
  • R1-9buildWorkflowArgs is now exercised with a diff-only plan (untrackedFiles: undefined): the mode field follows the plan, and the reduced roster (no 1c, no 7) is asserted.
  • R1-10 — the script validates args shape before dispatch (!args || !Array.isArray(args.agents) → throw with a message naming the args file and emit-workflow), tested against the three malformed bindings from the finding: undefined, a path string, and an object without agents.
  • R1-11 — the meta pure-literal test now does what the runtime does: it slices the meta object literal, evaluates it in a bare node:vm context (createContext(Object.create(null)) — no bridge globals), and asserts the resulting plain object (name, description, phases[].title). An identifier reference or call inside the literal now fails the test the same way it fails the sandbox.
  • R1-13 — new assertion that every emitted agent's label equals rosterLabel(req) for its requirement, so a substitution of a different label source fails at the point of divergence.

Declined with evidence

  • R1-12 — no code change; an in-thread reply records the evidence. The gate for the buildLaunch export hunk exists and runs: emit-workflow.test.ts imports buildLaunch for its byte-parity test, and the file is inside the typechecked src/**/*.ts include. Mutation-verified on this commit: removing the export fails that test (TypeError: buildLaunch is not a function) and npm run typecheck; reverting restores green. The finding's first option — confirming the byte-parity test is the gate — is what holds.

Escalated for a maintainer decision (thread left open, reply posted)

  • R1-2 (Critical) — verified: the printed handoff contract ("make one Workflow call with the paths below") cannot be honored against the Workflow tool in this build, for all three stated reasons (no argsPath parameter; scriptPath confined to the saved-workflow directories by readWorkflowFileSecurely; workflowsEnabled defaults to false so the tool is unregistered). The suggested fix — landing that interface (argsPath plus a trusted source for review-emitted scripts, gated on isWorkflowsEnabled()) — is a packages/core tool-schema and script-loading change and a scope decision against this PR's explicit emit-only scoping ("nothing routes through it yet"). Options laid out in the thread reply: (a) keep this PR emit-only and land the interface as the next Proposal: rebuild /review Step 3–5 orchestration on the workflow engine #8769 step before routing — recommended; (b) widen this PR to land it now; (c) soften the stdout text meanwhile. Awaiting the maintainer's call.

Review-level items (not inline findings)

  • PR-body template review — the stage-1a finding that the PR body does not follow the template (missing ## Reviewer Test Plan, ## Risk & Scope, ## Linked Issues, and the template's What/Why headings) is accurate, but it can only be fixed by editing the PR body on GitHub, which this mode cannot do from the checkout. It needs an author/workflow edit on the PR itself; the existing body content can be restructured under the template headings as the review suggests.
  • "Integration tests not run" review note — this is a statement about the automated reviewer's own coverage, not a finding. The behavior added here is exercised by unit tests that drive the command handler directly; there is no integration-harness coverage of review emit-workflow to run (checked integration-tests/). The focused suites below are the applicable verification.
  • @qqqys "已修复" comment — acknowledged as the prior round's fix record (ad6578c1ca); nothing to address.

No base-conflict merge was performed (--conflict false).

Verification

Commands actually run, in order:

  • npx vitest run src/commands/review/emit-workflow.test.ts src/commands/review/workflow-script.test.ts src/commands/review/agent-prompt.test.ts src/commands/review.test.ts (in packages/cli) — 240 passed (after the fix; an intermediate run exposed two defects in my own first cut — a missed roster.length reference and a wrong assumption that Agent 7 receives rules — both fixed and re-verified).
  • npx vitest run src/commands/review/ src/commands/review.test.ts (in packages/cli) — 67 files, 2322 passed, 4 skipped.
  • npm run buildpassed (exit 0).
  • npm run typecheckpassed (exit 0).
  • npm run lintpassed (exit 0).
  • npx prettier --check on the five touched files — passed (one file was reformatted with prettier --write first).
  • Mutation check for R1-12 evidence: un-exporting buildLaunch → byte-parity test fails (TypeError: buildLaunch is not a function); reverted → green.
  • Integration tests after npm run bundle: not run — the touched behavior is exercised by the unit tests above, not by the bundled CLI or the integration harness, so they are not the applicable check under the repository's rules.
中文说明

评审轮次总结(PR #8846

提交:f748caf6e1fix(review): record emit-workflow prompts and fail closed on bad args(5 个文件,+230/−75)。13 条行内发现中 11 条已在代码中解决;1 条附证据拒绝;1 条(Critical R1-2)已核实但升级给维护者做范围/时序决策,线程保持打开并已在该线程内回复。

已在代码中解决

  • R1-1(Critical)——buildWorkflowArgs 现在为它产出的每个 agent 调用 recordPrompt(planPath, key, prompt),位置在 key 一致性守卫之后(该守卫已移入共享映射函数,因此只有匹配成功的 key 才会被记录)。交付门禁读取的正是这些记录,一次正确分发的 workflow 运行不会再被 check-coverage 判为 "briefless"。新增断言钉住:每个产出 agent 的记录 prompt 与 args 文件中的 prompt 逐字节相等。
  • R1-3——产出的脚本现在携带 label 分发(agent(a.prompt, { label: a.label, ... })),使 WorkflowAgentSpec.label 有了与文档注释一致的真实读取方;原来把 label 钉为 roster key 的脚本测试改为钉住人类可读的 label。
  • R1-4——脚本在版本不匹配时 fail closed(args.version !== 1 → 抛错,提示重新运行 emit-workflow 以成对重新生成两个文件),并补测试。
  • R1-5——在 agent-prompt.ts 的 buildLaunch 旁抽出 buildRosterLaunches(report, planPath, rules),统一持有 requiredAgentsbuildLaunch → key 一致性守卫。runRosterbuildWorkflowArgs 都消费它,各自只保留调用方特有的逻辑(recordPrompt / chunk 拒绝)。守卫不再有两份拷贝,R1-1 背后的现实分歧在结构上消失。--roster 输出逐字节不变(既有的 214 条测试——包括对块头字面量的钉住——原样通过)。
  • R1-6——新增 handler 测试:可读的 --rules 文件进入每个产出 brief 的 ## Project rules 部分——同时以反方向钉住已声明的例外:Agent 7(Build & Test)不得接收 rules(SKILL.md 规则,buildRoleBrief 已强制执行)。
  • R1-7——write 测试现在 mock stdioHelpers(本包惯例),断言恰好三行 stdout:计数行、scriptPath: <out>/script.jsargs: <out>/args.json——标签与值的配对用精确相等钉住,发现中描述的互换变异体现在会失败。
  • R1-8——写出的 script.js 现在用 toBe(REVIEW_STEP_3A_WORKFLOW_SCRIPT) 断言(与被充分测试的常量逐字节比对),替代两个子串断言。
  • R1-9——buildWorkflowArgs 现在被 diff-only plan(untrackedFiles: undefined)执行:mode 字段跟随 plan,并断言缩减后的 roster(无 1c、无 7)。
  • R1-10——脚本在分发前校验 args 形状(!args || !Array.isArray(args.agents) → 抛错,错误信息点名 args 文件与 emit-workflow),用发现中的三种错误绑定测试:undefined、路径字符串、缺少 agents 键的对象。
  • R1-11——meta 纯字面量测试现在做运行时所做的事:切出 meta 对象字面量,在裸 node:vm 上下文(createContext(Object.create(null))——无桥接全局变量)中求值,断言产出的纯对象(namedescriptionphases[].title)。字面量中的标识符引用或调用现在会像沙箱失败一样让测试失败。
  • R1-13——新增断言:每个产出 agent 的 label 等于其 requirement 的 rosterLabel(req),替换其他 label 来源会在分叉点上失败。

附证据拒绝

  • R1-12——不改代码,证据记录在线程回复中。buildLaunch 导出 hunk 的把关测试存在且会运行:emit-workflow.test.ts 为其字节一致性测试导入 buildLaunch,且该文件在被类型检查的 src/**/*.ts include 之内。已在本提交上做变异验证:去掉 export 使该测试失败(TypeError: buildLaunch is not a function)并使 npm run typecheck 失败;还原后恢复全绿。该发现提供的第一个选项——确认字节一致性测试即把关者——成立。

升级给维护者决策(线程保持打开,已回复)

  • R1-2(Critical)——已核实:打印出的交接契约("make one Workflow call with the paths below")在当前构建的 Workflow 工具上无法履行,三个原因全部成立(无 argsPath 参数;scriptPathreadWorkflowFileSecurely 限制在 saved-workflow 目录内;workflowsEnabled 默认 false,工具未注册)。建议的修复——落地该接口(argsPath 加上 review 产出脚本的可信来源,以 isWorkflowsEnabled() 作门禁)——是 packages/core 的工具 schema 与脚本加载策略变更,且是针对本 PR 明确 emit-only 范围("目前没有任何东西路由到它")的范围决策。线程回复中列出选项:(a) 保持本 PR emit-only,把接口作为 Proposal: rebuild /review Step 3–5 orchestration on the workflow engine #8769 的下一步、在接线之前落地——推荐;(b) 扩大本 PR 现在落地;(c) 先弱化 stdout 文本。等待维护者决定。

评审级条目(非行内发现)

  • PR 正文模板评审——stage-1a 关于 PR 正文不符合模板(缺 ## Reviewer Test Plan## Risk & Scope## Linked Issues 及模板的 What/Why 标题)的发现属实,但只能在 GitHub 上编辑 PR 正文解决,本模式无法从 checkout 完成。需要作者/工作流在 PR 上操作;现有正文内容可以按评审建议重组到模板标题下。
  • "集成测试未运行" 评审备注——这是自动评审器对自身覆盖面的说明,不是发现。此处新增的行为由直接驱动命令 handler 的单元测试覆盖;integration-tests/ 中不存在 review emit-workflow 的集成覆盖(已检查)。下方的定向测试套件即适用的验证。
  • @qqqys 的 "已修复" 评论——确认为上一轮的修复记录(ad6578c1ca),无需处理。

未执行 base 冲突合并(--conflict false)。

验证

实际执行的命令,按顺序:

  • npx vitest run src/commands/review/emit-workflow.test.ts src/commands/review/workflow-script.test.ts src/commands/review/agent-prompt.test.ts src/commands/review.test.ts(在 packages/cli 内)——240 通过(修复后;中间一轮暴露了我第一版的两处缺陷——漏改的一处 roster.length 引用,以及误以为 Agent 7 会接收 rules——均已修复并重新验证)。
  • npx vitest run src/commands/review/ src/commands/review.test.ts(在 packages/cli 内)——67 个文件,2322 通过,4 跳过
  • npm run build ——通过(exit 0)。
  • npm run typecheck ——通过(exit 0)。
  • npm run lint ——通过(exit 0)。
  • 对五个改动文件执行 npx prettier --check ——通过(其中一个文件先以 prettier --write 重新格式化)。
  • R1-12 证据的变异检查:去掉 buildLaunch 的导出 → 字节一致性测试失败(TypeError: buildLaunch is not a function);还原 → 全绿。
  • npm run bundle 之后的集成测试:未运行——本次改动的行为由上述单元测试覆盖,而非经捆绑后的 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


const outDir = resolve(args.out);
mkdirSync(outDir, { recursive: true });
const scriptPath = resolve(outDir, 'script.js');

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.

script.js is written outside the saved-workflow directories the Workflow loader accepts.

The script goes into the arbitrary --out directory and the path is printed to the orchestrator as a Workflow scriptPath. But WorkflowRunner.startresolveSavedWorkflowScript({scriptPath})readWorkflowFileSecurely computes

const inside = dirs.some((d) => real === d || real.startsWith(d + sep));

over getSavedWorkflowDirs() (workflow-saved.ts:167), i.e. only <project>/.qwen/workflows and ~/.qwen/workflows. Any other --out throws:

workflow({scriptPath: '…/script.js'}): refusing to load a workflow file outside the saved-workflow directories

So the single Workflow call this command instructs the orchestrator to make fails before dispatching anything — Step 3A launches zero agents and the fan-out never starts. Either write the script into a saved-workflow dir, or ship it as a named saved workflow and print name: instead of scriptPath:.

'calls by hand for this step.',
);
writeStdoutLine(`scriptPath: ${scriptPath}`);
writeStdoutLine(`args: ${argsPath}`);

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.

args: <path> is not a form the Workflow tool accepts — args is inline-only.

WORKFLOW_PARAM_SCHEMA (packages/core/src/tools/workflow/workflow.ts:128) declares args as "Optional structured value bound to the args global. Pass actual JSON, not a stringified value", and there is no argsPath parameter anywhere in the codebase.

Following the printed instruction verbatim, the orchestrator passes args: "/…/args.json" and the emitted script's own guard fires — review-step-3a: args.agents is missing or not an array (the exact case workflow-script.test.ts enumerates) — so no agent runs.

The only way to actually dispatch from this output is to open args.json and retype ~13 full launch prompts into the tool call by hand, which is precisely the prompt-rewrite failure mode (rewrittenPrompts in check-coverage) this command exists to eliminate.

// The delivery gate reads recorded prompts, not briefs: without this
// record, a run dispatched from these args would fail check-coverage as
// "briefless" despite reviewing with exactly the built prompts.
recordPrompt(planPath, key, prompt);

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.

Recording the prompts is only half the gate — workflow-dispatched agents write no subagent transcript.

The comment here asserts that recordPrompt makes a workflow-dispatched run pass check-coverage. It does not. <projectDir>/subagents/<sessionId>/agent-*.jsonl is written only by attachJsonlTranscriptWriter, which is called solely from the Agent tool (packages/core/src/tools/agent/agent.ts:3190, :3969). The workflow path builds AgentHeadless directly in runSingleDispatch and attaches no transcript writer.

So after a successful workflow fan-out, qwen review check-coverage --plan …readTranscripts either throws TranscriptsUnavailableError ("no subagent transcripts at …", the directory never being created) or matches no record, and every roster role lands in missingRoles as "its prompt was built, but no agent on record was launched with it". check-coverage exits 3 and the review cannot certify the diff — even though every agent ran with exactly the built prompts.

The delivery gate needs an evidence source the workflow path actually produces before this migration can land.

// an agent is skipped. The label is the human-readable roster identity the
// args carry — the runtime's progress display consumes it.
const returns = await parallel(
agents.map((a) => () => agent(a.prompt, { label: a.label, phase: 'Review' })),

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.

Workflow dispatch imposes a hard 50-turn / 10-minute cap per agent that the --roster (Agent tool) path does not, and a capped agent loses all its work.

agent(a.prompt, { label, phase }) with no agentType/model/isolation/schema takes the fast path in workflow-orchestrator.ts, which hard-wires max_turns: 50, max_time_minutes: 10 (constants at :152-153, applied at :447-448) and then throws Workflow subagent … did not complete (terminate mode: TIMEOUT|MAX_TURNS) for any non-GOAL terminal.

Agent 7 (Build & Test) runs the project build and full test suite, which exceeds 10 minutes on this repo. parallel() converts the thrown dispatch to null, the script folds it into missingRoles, and the agent's entire output — including everything it had already found — is discarded. The run still returns a result that reads as a completed fan-out with a one-line log, so the build/test dimension is silently unreviewed on exactly the large PRs where it matters most.

The --roster path launches through the Agent tool with no wall-clock cap and returns partial work, so this is a capability regression, not a wash.

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.

Escalated to maintainer — core-runtime decision, sibling of the system-prompt thread. Verified: WORKFLOW_SUBAGENT_MAX_TURNS = 50 and WORKFLOW_SUBAGENT_MAX_TIME_MINUTES = 10 are hard-wired in workflow-orchestrator.ts for both dispatch paths, and parallel() converts a capped/failed dispatch to null, so a capped agent's partial work is discarded. On a large repo, Agent 7 (Build & Test) can genuinely exceed 10 minutes, and --roster has no equivalent cap — so this is a real capability difference, not a wash.

It cannot be fixed from the emitted script: the caps are applied by the runtime around the dispatch, and KNOWN_AGENT_OPTS exposes no override. Options: (1) raise/plumb the caps for this workflow run only (core change), (2) accept the difference and document it as part of the measured variable, (3) hold the A/B. I recommend (2) now + a follow-up issue for (1), together with the sibling thread — but the call is the maintainer's; same question stands there.

中文说明

升级给维护者——core 运行时决策,与系统提示线程同族。 已核实:WORKFLOW_SUBAGENT_MAX_TURNS = 50WORKFLOW_SUBAGENT_MAX_TIME_MINUTES = 10 在 workflow-orchestrator.ts 中对两条分发路径均为硬编码,且 parallel() 会把触顶/失败的分发转为 null,因此触顶 agent 的已完成工作会被丢弃。在大型仓库上,Agent 7(Build & Test)确实可能超过 10 分钟,而 --roster 没有等价上限——所以这是真实的能力差异,不是打平。

无法从发出的脚本修复:上限由运行时在分发外围施加,KNOWN_AGENT_OPTS 不暴露覆盖项。选项:(1) 仅为本 workflow 运行提高/接通上限(core 改动);(2) 接受差异并将其作为被测变量的一部分如实记录;(3) 暂缓 A/B。我的建议:先按 (2),并与姊妹线程合并开一个后续 issue 跟踪 (1)——但决定权在维护者;同一问题在那里同样开放。


// Two paths and a count. Nothing here is a prompt: the prompts are in the
// args file, which no one is asked to read, retype, or relay.
writeStdoutLine(

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 precondition check that workflows are enabled, and by this point every prompt record is already on disk.

Workflows are opt-in and experimental — Config.isWorkflowsEnabled (packages/core/src/config/config.ts:6474-6480) is off unless QWEN_CODE_ENABLE_WORKFLOWS=1 or the setting is on. This file has zero references to that gate; its only refusals are for territory fan-outs and pr-worktree.

buildWorkflowArgs has already called recordPrompt for all ~13 roles by the time this prints "make one Workflow call with the paths below and do not build agent calls by hand for this step", and the printed output deliberately contains no prompts. On a session without the Workflow tool the orchestrator has nothing to call and no prompt text to fall back on. If it then hand-builds launches, check-coverage compares them against the records already written and reports every role as rewritten or missing.

--roster had no such dependency — its output was self-sufficient. At minimum this should refuse up front (before any record is written) when workflows are disabled.

}

if (missingRoles.length > 0) {
log(missingRoles.length + ' agent(s) returned nothing: ' + missingRoles.join(', '));

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.

A completely failed fan-out is logged and returned as a successful step result.

null dispatches are collected into missingRoles, a log() line is emitted, and the script returns normally with rosterSize/delivered/missingRoles. Nothing throws; nothing sets a failure status.

If the provider is overloaded and all 13 dispatches fail, the Workflow tool hands the orchestrator a well-formed result object — "13 agents required" plus a missingRoles list buried in a log line — and, since the coverage gate cannot see workflow agents at all (see the transcript comment on emit-workflow.ts), the run proceeds to Step 4/5 and composes a review from zero agent returns.

Under --roster, a failed Agent tool call surfaces per launch as an error the orchestrator has to act on and cannot roll into a successful step result. Consider throwing when delivered === 0, or when missingRoles is non-empty.

// would run in the user's main checkout and review whatever is there —
// producing findings that look plausible and describe the wrong tree.
const mode = reviewMode(plan);
if (mode === 'pr-worktree') {

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.

Refusing pr-worktree excludes the dominant review path; the real blocker is one layer down.

fetch-pr always creates a worktree, so reviewMode returns 'pr-worktree' for every PR review — the path this skill primarily exists for. With this refusal the new dispatcher can only ever run for uncommitted-local or cross-repo diff-only reviews, i.e. the minority of runs.

The deeper fix is in the mechanism the refusal names: agent()'s option allowlist (KNOWN_AGENT_OPTS = label, phase, schema, model, isolation, agentType, stallMs in workflow-sandbox.ts) has no working-directory option. Adding one there is a small change and unblocks the actual use case. Shipping the CLI half first means a second migration later plus a dead refusal branch to delete.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Declined for this PR — recorded as follow-up. The diagnosis is right: the refusal excludes PR-worktree reviews, and the deeper fix is a working-directory option on agent(). But that is a core-runtime feature — new KNOWN_AGENT_OPTS surface in workflow-sandbox.ts plus dispatch/AgentHeadless cwd plumbing — and it decides which tree agents execute in, which deserves a dedicated security-minded review rather than a drive-by inside a review-tooling PR. Until it lands, the refusal is the correct fail-closed behavior: the alternative (dispatching anyway) reviews the wrong tree and looks complete doing it.

Suggestion: file the agent() working-directory capability as its own issue; when it exists, this PR's refusal branch is the thing to delete. I have left the refusal comment pointing at exactly that missing capability so the removal is obvious.

中文说明

本 PR 内拒绝——已记录为后续工作。 诊断是对的:该拒绝把 PR worktree 评审排除在外,更深层的修复是给 agent() 增加工作目录选项。但那是一个 core 运行时特性——workflow-sandbox.ts 中新的 KNOWN_AGENT_OPTS 表面,加分发/AgentHeadless 的 cwd 接线——它决定 agent 在哪棵树上执行,应当走一次专门的、带安全视角的评审,而不是顺手带进一个评审工具 PR。在其落地之前,拒绝是唯一正确的 fail-closed 行为:替代方案(照常派发)会评审错误的树,并且看起来完整。

建议:把 agent() 工作目录能力单独立一个 issue;它落地之时,本 PR 的拒绝分支就是要删除的对象。我已让拒绝注释恰好指向这个缺失能力,使将来的移除一目了然。


function runEmitWorkflow(args: EmitWorkflowArgs): void {
let report: PlanReport;
try {

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.

Plan-read / rules-read blocks are copy-pasted, making a fourth near-identical implementation in this directory.

The try { JSON.parse(readFileSync(...)) } catch { throw new Error('<cmd>: cannot read the plan …') } shape already exists verbatim at agent-prompt.ts:2280, build-test.ts:264 and script-lint.ts:532. The --rules block below is a shortened copy of agent-prompt.ts:2293-2305 — and this copy silently drops the trailing sentence explaining why a bad rules path is refused, so the error text has already drifted.

Any future change to plan validation (e.g. rejecting a plan that parses but has no diffPathAbsolute) now has to be applied in four places. A readPlan(path, command) / readRules(path, command) pair in lib/ would be one implementation.

/** The plan these agents were derived from. */
plan: string;
/** `local` or `diff-only` — `pr-worktree` is refused (see the header). */
mode: string;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

mode and plan are written into args.json but never read.

The emitted script (workflow-script.ts) reads only args.agents and args.version; grepping the branch finds no other reader of .mode or .plan outside emit-workflow.test.ts's own assertions. mode additionally costs a second reviewMode(plan) call, since requiredAgents already computes it internally.

These are declared-and-set-but-never-read fields — dead switches future readers have to reason about and keep in sync with a payload nothing consumes.

@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 将重新运行。

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not explored to full depth (tool budget reached): PR #8846 adds a qwen review emit-workflow subcommand th...: did not read the full 1838-line workflow-sandbox.ts , only the processing points the script depends on.; PR #8846 adds a qwen review emit-workflow subcommand th...: full emit-workflow.test.ts execution blocked on the missing node_modules ..

Test Plan (not a blocker): src/commands/review.test.tsno such file or directory; src/commands/review/emit-workflow.test.tsno such file or directory; src/commands/review/workflow-script.test.tsno such file or directory.

中文说明

未探索到全部深度(达到工具调用预算):PR #8846 adds a qwen review emit-workflow subcommand th...:did not read the full 1838-line workflow-sandbox.ts , only the processing points the script depends on.;PR #8846 adds a qwen review emit-workflow subcommand th...:full emit-workflow.test.ts execution blocked on the missing node_modules .

Test Plan(非阻断):src/commands/review.test.tsno such file or directory; src/commands/review/emit-workflow.test.tsno such file or directory; src/commands/review/workflow-script.test.tsno such file or directory

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

Comment on lines +179 to +180
writeStdoutLine(`scriptPath: ${scriptPath}`);
writeStdoutLine(`args: ${argsPath}`);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R1-2: The printed handoff contract — "make one Workflow call with the paths below" — cannot be honored by the Workflow tool as printed. Re-verified at the reviewed commit, all three walls stand: (1) scriptPath outside .qwen/workflows or ~/.qwen/workflows is refused by readWorkflowFileSecurely (workflow-saved.ts:170); (2) WORKFLOW_PARAM_SCHEMA accepts inline args JSON only — there is no argsPath parameter for the printed args: <path> line; (3) workflowsEnabled defaults to false (config.ts), so a default install has no Workflow tool at all.

Failure scenario: a reviewer runs qwen review emit-workflow --plan plan.json --out .qwen/tmp/review-x and follows the printed instruction verbatim — on a default install there is no Workflow tool to call; with workflows enabled, the emitted script.js is refused because --out is not a saved-workflow dir, and the args: line has no parameter it can be passed through. The one-call dispatch the command promises is unexecutable as printed; the run falls back to hand-launching the roster, and the A/B this feature is built for never happens.

Suggested fix: make the emitted contract executable or refuse to emit it — validate --out/scriptPath against the saved-workflow dirs (or write into .qwen/workflows/), add an argsPath-style parameter to the Workflow tool, and gate on isWorkflowsEnabled(). Until then, the printed text should not describe a call the tool cannot honor.

中文说明

[Critical] R1-2:打印出来的交接契约("make one Workflow call with the paths below")按原样无法被 Workflow 工具执行。已在本评审提交上重新核实,三堵墙均成立:(1) .qwen/workflows~/.qwen/workflows 之外的 scriptPath 会被 readWorkflowFileSecurely(workflow-saved.ts:170)拒绝;(2) WORKFLOW_PARAM_SCHEMA 只接受内联 args JSON——不存在 argsPath 参数来承接打印出的 args: <path> 行;(3) workflowsEnabled 默认为 false(config.ts),默认安装根本没有 Workflow 工具。

失败场景:运行 qwen review emit-workflow --plan plan.json --out .qwen/tmp/review-x 并照打印输出执行——默认安装下无工具可调;启用 workflows 后,--out 不是 saved-workflow 目录导致 script.js 被拒,args: 行也没有可传入的参数。命令承诺的一次调用按原样不可执行;回退到手工发射 roster 后,本特性要做的 A/B 永远不会发生。

建议修复:让发出的契约可执行,或拒绝发出——校验 --out/scriptPath 位于 saved-workflow 目录(或自动写入 .qwen/workflows/),给 Workflow 工具增加 argsPath 风格参数,并用 isWorkflowsEnabled() 把关。在此之前,打印文本不应描述一个工具无法履行的调用。

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

Comment on lines +140 to +142
try {
report = JSON.parse(readFileSync(args.plan, 'utf8')) as PlanReport;
} catch (err) {

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] runEmitWorkflow adds a 6th copy of the read-plan-or-throw block and a 2nd copy of the read-rules-or-throw block, duplicating logic that already exists in agent-prompt.ts, build-test.ts, test-plan.ts, script-lint.ts and base-tree.ts.

Failure scenario: the refusal semantics are now maintained in six copies that have already drifted (base-tree omits the command prefix; build-test alone validates the parsed value is a JSON object; emit-workflow's rules message truncates agent-prompt's explanation). Any future change to the refusal must be re-made in every copy, and each new command copies whichever variant is nearest, so the copies can only diverge further.

Suggested fix: extract a shared readPlanOrThrow(path, command) / readRulesOrThrow(path, command) into review/lib/ and call it from the new command — and opportunistically from the existing five, which is what makes the extraction pay for itself.

中文说明

[Suggestion] runEmitWorkflow 新增了第 6 份 read-plan-or-throw 代码块和第 2 份 read-rules-or-throw 代码块,与 agent-prompt.tsbuild-test.tstest-plan.tsscript-lint.tsbase-tree.ts 中已有的逻辑重复。

失败场景:拒绝语义现在由六份拷贝维护,且已经出现漂移(base-tree 省略命令前缀;只有 build-test 校验解析值是 JSON 对象;emit-workflow 的 rules 消息截断了 agent-prompt 的解释)。任何对拒绝逻辑的改动都要在每个拷贝中重做,新命令只会复制最近的变体,拷贝只会越来越分叉。

建议修复:把共享的 readPlanOrThrow(path, command) / readRulesOrThrow(path, command) 提取到 review/lib/,新命令调用它——并顺手改造现有五个调用点,提取才有收益。

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

Comment on lines +117 to +119
const chunk = requiredAgents(plan).find((r) => r.role === 'chunk');
if (chunk) {
throw new Error(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The chunk-agent guard in buildWorkflowArgs is unreachable dead code — requiredAgents emits role: 'chunk' only inside its own isTerritoryFanOut branch in the same file (lib/roster.ts), and that same predicate already threw two statements above in this function.

Failure scenario: the trigger cannot be constructed through the public API: the only chunk-emission site in requiredAgents is gated on isTerritoryFanOut(plan), which this function already refuses first. The check is consequently also untestable (no fixture can trip it without mocking requiredAgents). The concrete cost is reader tax — a reader must trace roster.ts to discover the error "This is a bug in the CLI" can never be true today. (Agents 3c and 6c judged it a deliberate, commented future-change assertion consistent with the file's defensive-guard house style; either way it is provably dead today.)

Suggested fix: delete the check and its five-line comment — the isTerritoryFanOut refusal above already covers every plan the roster can produce. If defense-in-depth for future roster changes is genuinely wanted, derive the refusal and the roster from one shared predicate so the coupling is structural rather than asserted at a call site.

中文说明

[Suggestion] buildWorkflowArgs 中的 chunk-agent 守卫是不可达的死代码——requiredAgents 只在 lib/roster.ts 同一文件内的 isTerritoryFanOut 分支里产出 role: 'chunk',而本函数在上面两行已经用同一谓词抛出了。

失败场景:该触发条件无法通过公开 API 构造:requiredAgents 唯一的 chunk 产出点受 isTerritoryFanOut(plan) 门控,而本函数已先拒绝它。因此该检查也无法测试(不 mock requiredAgents 就没有 fixture 能触发)。实际代价是读者负担——读者必须追踪 roster.ts 才能发现 "This is a bug in the CLI" 今天永远不可能为真。(3c 和 6c 认为这是刻意的、带注释的未来变更断言,符合本文件防御式守卫风格;无论如何它今天确实不可达。)

建议修复:删除该检查及其五行注释——上面的 isTerritoryFanOut 拒绝已覆盖 roster 能产出的所有 plan。若确实想要针对未来 roster 变化的纵深防御,应让拒绝与 roster 派生自同一个共享谓词,使耦合成为结构性的而非在调用点断言。

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

Comment on lines +77 to +79
* Separated from the command boundary so the refusals and the roster
* derivation are testable without a filesystem round trip.
*/

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] buildWorkflowArgs is documented as a pure "build the payload" function but performs delivery-critical filesystem writes (buildLaunch writes each brief; the map calls recordPrompt per agent), and runEmitWorkflow performs those writes before validating/creating --out. Probe-verified: with --out pointing at an existing file, the handler throws a raw EEXIST naming neither emit-workflow nor --out, with 13 prompt records already on disk.

Failure scenario: a user passes an unusable --out (an existing file, or an unwritable path) — mkdirSync throws after all briefs and records for the whole roster are already on disk, leaving a record set for a workflow that was never emitted, with a raw fs error that hides where the records went. Separately, any second caller of this exported "build" function that wants only the payload (a dry-run — a plausible next consumer) silently writes prompt records the delivery gate reads as obligations, failing check-coverage with the cause invisible in the calling code. The docblock's "testable without a filesystem round trip" is also literally false — the tests must create temp dirs because the function writes.

Suggested fix: move mkdirSync(outDir) / writeability validation ahead of buildWorkflowArgs, and correct the docblock to state the function writes each agent's brief and records its prompt under the plan's record dir (or move recordPrompt out to runEmitWorkflow to match runRoster's structure).

中文说明

[Suggestion] buildWorkflowArgs 被注释为纯"构建 payload"函数,但实际执行了交付关键的文件系统写入(buildLaunch 写每个 brief;map 中为每个 agent 调用 recordPrompt),且 runEmitWorkflow 在验证/创建 --out 之前就完成了这些写入。探针已验证:--out 指向已存在文件时,handler 抛出裸 EEXIST,既不提 emit-workflow 也不提 --out,且 13 条 prompt 记录已在磁盘上。

失败场景:用户传入不可用的 --out(已存在文件或不可写路径)——mkdirSync 在整份 roster 的 brief 与记录已落盘后抛错,留下从未发出的 workflow 的记录集,且裸 fs 错误看不出记录写到了哪里。另外,任何想要纯 payload 的第二个调用者(dry-run——很可能的后续消费者)会静默写入交付门禁当作义务读取的 prompt 记录,导致 check-coverage 失败而调用代码里看不出原因。docblock 声称的"无需文件系统往返即可测试"也不成立——正因为函数会写入,测试必须创建临时目录。

建议修复:把 mkdirSync(outDir)/可写性校验移到 buildWorkflowArgs 之前,并修正 docblock 说明该函数会写入每个 agent 的 brief 并把 prompt 记录到 plan 的 record 目录(或把 recordPrompt 移到 runEmitWorkflow,对齐 runRoster 的结构)。

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

Comment on lines +44 to +46
if (!args || !Array.isArray(args.agents)) {
throw new Error('review-step-3a: args.agents is missing or not an array - pass the args.json that qwen review emit-workflow wrote');
}

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 script's fail-closed validation covers missing/mis-bound/wrong-version args but not an empty agents array, which yields a clean zero-agent "complete" result. Probe-verified by executing the real constant: {version: 1, agents: []} passes both guards, parallel([]) resolves to [], and the script returns {rosterSize: 0, delivered: [], missingRoles: []} — byte-identical in shape to a fully-complete review with zero agents run.

Failure scenario: a corrupted or hand-edited args.json containing {"version":1,"agents":[]} (the exact shape a truncated/partial write or a future emitter bug could produce — a 3A review always requires multiple agents) passes both guards. Any consumer of the workflow return that treats "no missing roles" as "all dimensions reviewed" would certify a review in which no agent ran. The test enumerates bad shapes [undefined, path-string, {version: 1}] but not the empty-array shape.

Suggested fix: after the array check, add if (args.agents.length === 0) throw new Error('review-step-3a: args.agents is empty - ...') (or fold into the existing condition).

中文说明

[Suggestion] 脚本的 fail-closed 校验覆盖了缺失/错误绑定/版本不符的 args,但没有覆盖空的 agents 数组,后者会产出干净的零 agent "完整"结果。探针已验证(执行真实常量):{version: 1, agents: []} 通过两道守卫,parallel([]) 解析为 [],脚本返回 {rosterSize: 0, delivered: [], missingRoles: []}——与零 agent 运行、完全完成的评审形状逐字节相同。

失败场景:被损坏或手工编辑的 args.json{"version":1,"agents":[]}(截断/部分写入或未来 emitter bug 可能产生的确切形状——3A 评审必然需要多个 agent)即可通过两道守卫。任何把"无 missingRoles"当作"所有维度均已评审"的消费者都会为一次没有运行任何 agent 的评审背书。测试枚举了坏形状 [undefined, path-string, {version: 1}],但没有空数组形状。

建议修复:数组检查后加上 if (args.agents.length === 0) throw new Error('review-step-3a: args.agents is empty - ...')(或并入现有条件)。

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

Comment on lines +39 to +41
phase('Review');

// Fail closed on a payload this script cannot dispatch: a missing, stale or

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 script's fail-closed guards run after its only other observable side effect — the phase('Review') transition — so an undispatchable payload advances the runtime's phase state before the script throws. Probe-verified: with corrupt args ({version: 1}, no agents), the shipped script records phases=["Review"] before throwing; with the guards moved above phase('Review'), the same input throws with phases=[].

Failure scenario: the same corruption the guards exist to catch (a truncated/hand-edited args.json, or a version the script does not read) advances the run's phase/progress state before failing — the error record and the registry's phaseStarted emission both show a Review phase that never dispatched anything. On a resume-replay runtime — the determinism property this script's header is explicitly built around — the replay re-enters the same started phase against the same corrupt args. Nothing requires the current order.

Suggested fix: move both guard blocks (the args.agents array check and the args.version check) above phase('Review');.

中文说明

[Suggestion] 脚本的 fail-closed 守卫运行在其唯一可观察副作用——phase('Review') 状态转换——之后,因此一个不可派发的 payload 会在脚本抛错前先推进运行时的 phase 状态。探针已验证:对损坏的 args({version: 1},无 agents),现脚本先记录 phases=["Review"] 再抛错;把守卫移到 phase('Review') 之前后,同一输入抛错时 phases=[]

失败场景:守卫要拦截的同一类损坏(截断/手工编辑的 args.json,或脚本不认识的 version)会先推进运行的 phase/进度状态再失败——错误记录和注册表的 phaseStarted 事件都显示一个从未派发任何东西的 Review phase。在 resume-replay 运行时(本脚本头部明确为之构建的确定性属性)上,重放会带着同样的损坏 args 再次进入同一个已开始的 phase。当前顺序没有任何理由要求。

建议修复:把两个守卫块(args.agents 数组检查和 args.version 检查)移到 phase('Review'); 之上。

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

Comment on lines +85 to +87
const plan = report as RosterPlan;

// 3B is not a bigger 3A. Its chunk agents carry a per-territory contract —

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] runEmitWorkflow validates only that the plan file parses; a plan file containing JSON null (or any primitive) passes the parse and then dies in roster.ts internals with a raw uncaught TypeError instead of the command's graceful refusal. Probe-verified: a plan file containing null throws TypeError: Cannot read properties of null (reading 'srcDiffLines') at isTerritoryFanOut (roster.ts:108), with no emit-workflow: attribution — unlike every other refusal this command produces. (agent-prompt --roster crashes identically on the same input, so this is new code copying a pre-existing weakness — but the crash is newly reachable through this command's own read-validate-build chain.)

Failure scenario: a corrupted or hand-edited plan.json that happens to contain nullreadFileSync reads fine, JSON.parse returns null, and isTerritoryFanOut(plan) dereferences plan.srcDiffLines on the null, producing a bare TypeError with a stack pointing at roster.ts, indistinguishable from a CLI bug in a pipeline log.

Suggested fix: after JSON.parse, validate the shape — if (typeof report !== 'object' || report === null || Array.isArray(report)) throw new Error('emit-workflow: the plan file ... is not a plan object') — or fold the check into a shared plan reader (the extraction f-3a proposes).

中文说明

[Suggestion] runEmitWorkflow 只校验 plan 文件能否解析;包含 JSON null(或任何基本类型)的 plan 文件通过解析后在 roster.ts 内部以裸的未捕获 TypeError 崩溃,而不是命令的优雅拒绝。探针已验证:内容为 null 的 plan 文件在 isTerritoryFanOut(roster.ts:108)抛出 TypeError: Cannot read properties of null (reading 'srcDiffLines'),没有任何 emit-workflow: 前缀——与本命令的其他所有拒绝都不同。(agent-prompt --roster 对同一输入同样崩溃,所以这是新代码复制了一个既有弱点——但该崩溃通过本命令自己的 read-validate-build 链变得新可达。)

失败场景:恰好包含 null 的损坏/手工编辑 plan.json——readFileSync 正常读取,JSON.parse 返回 nullisTerritoryFanOut(plan) 对 null 解引用 plan.srcDiffLines,产生指向 roster.ts 的裸 TypeError,在流水线日志中与 CLI bug 无法区分。

建议修复:JSON.parse 之后校验形状——if (typeof report !== 'object' || report === null || Array.isArray(report)) throw new Error('emit-workflow: the plan file ... is not a plan object')——或把检查并入共享的 plan reader(f-3a 提议的提取)。

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

Comment on lines +125 to +126
const agents = buildRosterLaunches(report, planPath, rules).map(
({ req, key, prompt }): WorkflowAgentSpec => {

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] runEmitWorkflow validates only that the plan parses; a plan file that is a valid JSON object but not a plan (e.g. {}, a settings file, an old plan format) passes all three refusals and dies deep in agent-prompt.ts internals with an agent-prompt:-attributed error — or, for a partial plan, silently emits a full roster from a non-plan. Probe-verified: {} produces 11 agents and throws agent-prompt: the plan has no diffPathAbsolute; a truncated plan (diffPathAbsolute + chunks, no files/budget) completes, writing 11 briefs + 11 prompt records + args.json and printing "11 agents required". Sibling of the null case (ra4-2) whose typeof check does not catch {}.

Failure scenario: qwen review emit-workflow --plan <file> where <file> parses but is not a plan. The {} case dies with a misattributed agent-prompt: error indistinguishable from a genuine agent-prompt failure in a pipeline log. The truncated-plan variant silently emits a full roster from a non-plan: a later check-coverage reads those records as obligations — a review emitted from a non-plan with no error at all. Neither case has a test (the suite covers only unreadable-file, territory, and worktree refusals).

Suggested fix: after JSON.parse, validate plan shape — diffPathAbsolute is a non-empty string, chunks is a non-empty array, files is an array — and throw emit-workflow: --plan <path> is not a plan report; alternatively wrap the buildWorkflowArgs call in runEmitWorkflow so build-chain errors are re-attributed to emit-workflow (same remedy f-6c1 asks for at the writeBrief site, extended to the requireDiffPath/diffReadingBlock sites this command newly reaches).

中文说明

[Suggestion] runEmitWorkflow 只校验 plan 能否解析;是合法 JSON 对象但不是 plan 的文件(如 {}、设置文件、旧格式 plan)能通过全部三道拒绝,然后在 agent-prompt.ts 深处以 agent-prompt: 前缀的错误死亡——而对于部分 plan,则会静默地从非 plan 发出一整份 roster。探针已验证:{} 产出 11 个 agent 并抛 agent-prompt: the plan has no diffPathAbsolute;截断的 plan(有 diffPathAbsolute + chunks,缺 files/budget)则正常完成,写出 11 份 brief + 11 条 prompt 记录 + args.json 并打印 "11 agents required"。这是 null 情形(ra4-2)的兄弟问题,ra4-2 的 typeof 检查拦不住 {}

失败场景:qwen review emit-workflow --plan <file><file> 可解析但不是 plan。{} 情形以错误归属的 agent-prompt: 错误死亡,与流水线日志中真正的 agent-prompt 失败无法区分。截断 plan 变体则静默地从非 plan 发出一整份 roster:后续 check-coverage 会把这些记录当作义务读取——一次从非 plan 发出的评审,全程无任何报错。两种情形都没有测试(套件只覆盖不可读文件、territory、worktree 三类拒绝)。

建议修复:JSON.parse 之后校验 plan 形状——diffPathAbsolute 是非空字符串、chunks 是非空数组、files 是数组——并抛 emit-workflow: --plan <path> is not a plan report;或者在 runEmitWorkflow 里包裹 buildWorkflowArgs 调用,把构建链错误重新归属到 emit-workflow(f-6c1 在 writeBrief 处要求的同样补救,扩展到本命令新触达的 requireDiffPath/diffReadingBlock 处)。

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

Comment on lines +57 to +59
const returns = await parallel(
agents.map((a) => () => agent(a.prompt, { label: a.label, phase: 'Review' })),
);

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 script's fail-closed guards validate the container (args.agents is an array, version matches) but not the elements, and a null (or other primitive) element crashes the fan-out with a raw TypeError instead of the named refusal the guards exist to produce. Probe-verified against the real constant: {"version":1,"agents":[{...},{...},null]} passes both guards; under the real sandbox parallel the accounting loop throws Cannot read properties of null (reading 'key') (under the test-harness analogue it dies mid-map with reading 'prompt') — a bare vm TypeError after phase('Review') and after earlier agents have already dispatched.

Failure scenario: a hand-edited or truncated args.json containing {"version":1,"agents":[{...},{...},null]} passes both guards. The thunk for the null element dereferences a.prompt when parallel invokes it: agents before the null element have already started real subagent dispatches (tokens spent, results discarded) and the fan-out dies raw, mid-dispatch, with no attribution — exactly the failure the guard's own comment says it exists to prevent. Distinct from ra3-1 (element missing prompt → silent promptless dispatch): a null element is a crash path.

Suggested fix: after the array/version guards, validate elements before dispatch, e.g. for (const a of agents) if (a === null || typeof a !== 'object' || typeof a.prompt !== 'string' || typeof a.key !== 'string') throw new Error('review-step-3a: args.agents contains a malformed agent entry — re-run qwen review emit-workflow') (this also subsumes ra3-1's prompt check).

中文说明

[Suggestion] 脚本的 fail-closed 守卫只校验容器(args.agents 是数组、version 匹配),不校验元素;一个 null(或其他基本类型)元素会让扇出以裸 TypeError 崩溃,而不是守卫本应产出的具名拒绝。探针已验证(执行真实常量):{"version":1,"agents":[{...},{...},null]} 通过两道守卫;在真实沙箱 parallel 下,记账循环抛 Cannot read properties of null (reading 'key')(在测试 harness 模拟下则是在 map 中途以 reading 'prompt' 死亡)——在 phase('Review') 之后、且前面的 agent 已经派发之后,出现裸的 vm TypeError。

失败场景:手工编辑或截断的 args.json{"version":1,"agents":[{...},{...},null]} 即可通过两道守卫。parallel 调用时,null 元素的 thunk 对 a.prompt 解引用:null 元素之前的 agent 已开始真实子代理派发(token 已花、结果被丢弃),扇出在中途裸死、无任何归属——正是守卫自己注释声称要防止的失败。与 ra3-1(元素缺 prompt → 静默无 prompt 派发)不同:null 元素是崩溃路径。

建议修复:数组/version 守卫之后,在派发前校验元素,如 for (const a of agents) if (a === null || typeof a !== 'object' || typeof a.prompt !== 'string' || typeof a.key !== 'string') throw new Error('review-step-3a: args.agents contains a malformed agent entry — re-run qwen review emit-workflow')(这也涵盖 ra3-1 的 prompt 检查)。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix ran out of time before finishing (idle-timeout (no output for 1200000ms — the sandbox likely hung at startup)) (attempt 2/100) — it will retry on the next scan.

What I found before stopping:
Qwen failed during address-review: idle-timeout (no output for 1200000ms — the sandbox likely hung at startup).

See the Qwen Autofix agent step logs for model/tool output.

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


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

Test Plan (not a blocker): src/commands/review.test.tsno such file or directory; src/commands/review/emit-workflow.test.tsno such file or directory; src/commands/review/workflow-script.test.tsno such file or directory.

中文说明

Test Plan(非阻断):src/commands/review.test.tsno such file or directory; src/commands/review/emit-workflow.test.tsno such file or directory; src/commands/review/workflow-script.test.tsno such file or directory

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

Comment on lines +127 to +130
// The delivery gate reads recorded prompts, not briefs: without this
// record, a run dispatched from these args would fail check-coverage as
// "briefless" despite reviewing with exactly the built prompts.
recordPrompt(planPath, key, prompt);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R2-1: The round-1 fix is only half the delivery gate. recordPrompt populates the register coverageFromTranscripts reads via readRecordedPrompts (lib/coverage.ts), but the gate reads a second register too — subagent transcripts (readTranscripts, <projectDir>/subagents/<sessionId>/agent-*.jsonl) — and workflow dispatch never writes one: runSingleDispatchAgentHeadless attaches no JSONL transcript writer (the only production callers of attachJsonlTranscriptWriter are the Agent tool and background-agent-resume). This is the still-open sibling entrance of round-1 blocker R1-1 — the briefless entrance is closed, the transcript entrance is not — and it corroborates @wenshao's open thread. — Failure scenario: dispatch the emitted workflow and run check-coverage: every built role lands in missingRoles ("its prompt was built, but no agent on record was launched with it") — or TranscriptsUnavailableError in a fresh session — and the gate exits 3, so a fully correct run is reported to the PR author as entirely unreviewed. Probe-verified at this commit with a flip arm: fabricating one Agent-tool-style JSONL per recorded role clears missingRoles; without it every role fails. The new recorded-prompt-parity assertion passes while the property it exists to protect does not hold. Suggested fix: do not claim gate acceptance via prompt records on the workflow path — either sequence per #8769 (land journal-based coverage before routing anything through this artifact, and adjust the comment/test framing meanwhile), or make workflow dispatch attach the same JSONL transcript writer the Agent tool uses.

中文说明

第一轮修复只覆盖了交付门禁的一半。recordPrompt 填充了 coverageFromTranscripts 通过 readRecordedPrompts(lib/coverage.ts)读取的登记表,但门禁还读取第二个登记表——子代理 transcript(readTranscripts<projectDir>/subagents/<sessionId>/agent-*.jsonl)——而 workflow 分发从不写入它:runSingleDispatchAgentHeadless 没有挂载任何 JSONL transcript writer(attachJsonlTranscriptWriter 仅有的生产调用方是 Agent 工具与 background-agent-resume)。这是第一轮 blocker R1-1 仍然敞开的同族入口——briefless 入口已关闭,transcript 入口没有——并与 @wenshao 的开放线程相互印证。失败场景:分发该 workflow 并运行 check-coverage:每个已构建角色落入 missingRoles("its prompt was built, but no agent on record was launched with it")——新会话则直接 TranscriptsUnavailableError——门禁以 exit 3 结束,一次完全正确的运行被报告为"完全未评审"。已在本提交上用探针加翻转臂验证:为每个已记录角色伪造一条 Agent 工具风格的 JSONL 后 missingRoles 清空;不伪造则全部失败。新增的"记录 prompt 逐字节一致"断言通过,但它所要保护的性质并不成立。建议修复:不要在 workflow 路径上用 prompt 记录声称门禁通过——要么按 #8769 排序(先落地基于 journal 的 coverage 再接线,期间修正注释/测试表述),要么让 workflow 分发挂载与 Agent 工具相同的 JSONL transcript writer。

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

// mis-bound args file would otherwise die inside the vm as a bare TypeError
// that names nothing about emit-workflow or the args file.
if (!args || !Array.isArray(args.agents)) {
throw new Error('review-step-3a: args.agents is missing or not an array - pass the args.json that qwen review emit-workflow wrote');

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] R2-2: The guard's remediation hint is circular for the most likely misuse: passing the args file's path — exactly what emit-workflow's stdout instructs ("make one Workflow call with the paths below", args: <path>) — IS passing "the args.json that qwen review emit-workflow wrote". The actual remediation is never named: read the file and pass its PARSED JSON CONTENTS inline, because the vm sandbox has no filesystem and Workflow accepts args inline-only ("Pass actual JSON, not a stringified value"). — Concrete cost: an orchestrator that follows the stdout line passes the path string, is told to pass what it just passed, reads the demand as unsatisfiable, and retries the identical call; the real cause is nowhere in the error. The PR's own test pins the path-string case to this same hint.

Suggested change
throw new Error('review-step-3a: args.agents is missing or not an array - pass the args.json that qwen review emit-workflow wrote');
throw new Error('review-step-3a: args.agents is missing or not an array - args must be the PARSED CONTENTS of the args.json that qwen review emit-workflow wrote (pass the JSON value, not the path; the sandbox cannot read files)');
中文说明

守卫的补救提示对最可能的误用是循环的:传入 args 文件的路径——这正是 emit-workflow 的 stdout 所指示的("make one Workflow call with the paths below"、args: <path>)——本身就是"传入 emit-workflow 写出的 args.json"。真正的补救方式从未被点名:读取文件并把其解析后的 JSON 内容以内联值传入,因为 vm 沙箱没有文件系统,Workflowargs 只接受内联 JSON("Pass actual JSON, not a stringified value")。具体代价:orchestrator 照 stdout 传入路径字符串,却被要求"传入你刚刚传入的东西",会认为该要求无法满足,从而原样重试同一个调用;错误的真实原因在报错里完全看不到。PR 自己的测试也把路径字符串这一用例钉在了同一个提示上。

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

Comment on lines +132 to +133
const emitted = payload.agents.find((a) => a.key === req.key);
expect(emitted?.label).toBe(rosterLabel(req));

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] R2-4: rosterLabel's output is pinned only by a circular oracle — this assertion compares each emitted label to rosterLabel(req) itself, so it verifies the wiring (emit-workflow uses rosterLabel) but not the output: both could be wrong together. No literal expected strings exist anywhere for the chunk case, the role case, or the control-character flattening. Test-efficacy probe (harness validated): reverting the rosterLabel export hunk on its own left the colocated agent-prompt suite green; this tautological cross-file assertion is the only gate. Residual of round-1 R1-13. — Failure scenario: a regression corrupting rosterLabel output (a chunk labelled as a role, a separator glyph leaking through the flattening) ships green and mislabels every emitted workflow step in the runtime's progress display. Suggested fix: add direct unit tests for rosterLabel with literal expected strings (chunk case, role case, control-character flattening, separator stripping), independent of the emitted payload.

中文说明

rosterLabel 的输出只被一个循环 oracle 钉住——该断言把每个产出的 label 与 rosterLabel(req) 本身比较,因此只验证了接线(emit-workflow 使用 rosterLabel),没有验证输出:两者可以同时是错的。chunk 分支、role 分支、控制字符扁平化都不存在任何字面期望字符串。测试效力探针(harness 已验证):单独还原 rosterLabel 的导出 hunk 后,同目录的 agent-prompt 测试套件仍全绿;这个同义反复的跨文件断言是唯一的把关。这是第一轮 R1-13 的残留。失败场景:一个破坏 rosterLabel 输出的回归(把 chunk 标成 role、分隔符图形从扁平化中泄漏)会全绿通过,并在运行时进度展示中错标每个 workflow 步骤。建议修复:为 rosterLabel 增加带字面期望字符串的直接单元测试(chunk、role、控制字符扁平化、分隔符剥离),独立于产出的 payload。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix ran out of time before finishing (idle-timeout (no output for 1200000ms — the sandbox likely hung at startup)) (attempt 3/100) — it will retry on the next scan.

What I found before stopping:
Qwen failed during address-review: idle-timeout (no output for 1200000ms — the sandbox likely hung at startup).

See the Qwen Autofix agent step logs for model/tool output.

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


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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix stopped: this counting window now contains 3 time-budget exhaustions (pushed rounds in between included; this round itself may have failed differently). That is 3 full agent runs that pushed nothing. 3 of those were silent-sandbox (idle) timeouts that no budget increase can cure — investigate the sandbox image and runner docker daemon for those. A human should investigate the sandbox image and runner docker daemon, then comment @qwen-code /retry to re-arm. Until then future scans will skip this PR.

What I found before stopping:
Qwen failed during address-review: idle-timeout (no output for 1200000ms — the sandbox likely hung at startup).

See the Qwen Autofix agent step logs for model/tool output.

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


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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

⏸️ Takeover paused: this PR reached its round cap (100/100). Comment @qwen-code /takeover to re-arm a fresh window and continue management, or @qwen-code /takeover stop to release.

中文说明

⏸️ 托管已暂停:本 PR 达到轮次上限(100/100)。评论 @qwen-code /takeover 可重新武装、开启新窗口继续托管;或评论 @qwen-code /takeover stop 释放。

@qqqys

qqqys commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover stop

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

Copy link
Copy Markdown
Collaborator

👋 Takeover released: the autofix loop will no longer engage this PR (an in-flight round, if any, completes its bounded work). Re-apply autofix/takeover (or comment @qwen-code /takeover) to re-engage.

中文说明

👋 已释放:autofix 循环不再介入此 PR(在飞的一轮如有,将完成其有界工作)。重新打上 autofix/takeover 标签(或评论 @qwen-code /takeover)即可再次接管。

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Test Plan (not a blocker): src/commands/review.test.tsno such file or directory; src/commands/review/emit-workflow.test.tsno such file or directory; src/commands/review/workflow-script.test.tsno such file or directory.

中文说明

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

Test Plan(非阻断):src/commands/review.test.tsno such file or directory; src/commands/review/emit-workflow.test.tsno such file or directory; src/commands/review/workflow-script.test.tsno such file or directory

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

Comment on lines +130 to +131
recordPrompt(planPath, key, prompt);
return { key, label: rosterLabel(req), prompt };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R2-1: The round-1 fix is only half the delivery gate. recordPrompt populates the register coverageFromTranscripts reads via readRecordedPrompts, but the gate reads a second register too — subagent transcripts (readTranscripts, <projectDir>/subagents/<sessionId>/agent-*.jsonl) — and workflow dispatch never writes one: attachJsonlTranscriptWriter is called only by the Agent tool and background-agent-resume.

Failure scenario: dispatch the emitted workflow and run check-coverage: every built role lands in missingRoles ("its prompt was built, but no agent on record was launched with it") — or TranscriptsUnavailableError in a fresh session — and the gate exits 3, so a fully correct run is reported to the PR author as entirely unreviewed.

Suggested fix: do not claim gate acceptance via prompt records on the workflow path — either sequence per #8769 (land journal-based coverage before routing anything through this artifact, and adjust the comment/test framing meanwhile), or make workflow dispatch attach the same JSONL transcript writer the Agent tool uses.

中文说明

第一轮修复只覆盖了交付门禁的一半。recordPrompt 填充了 coverageFromTranscripts 通过 readRecordedPrompts 读取的登记表,但门禁还读取第二个登记表——子代理 transcript(readTranscripts<projectDir>/subagents/<sessionId>/agent-*.jsonl)——而 workflow 分发从不写入它:attachJsonlTranscriptWriter 仅有的生产调用方是 Agent 工具与 background-agent-resume。

失败场景:分发该 workflow 并运行 check-coverage:每个已构建角色落入 missingRoles("its prompt was built, but no agent on record was launched with it")——新会话则直接 TranscriptsUnavailableError——门禁以 exit 3 结束,一次完全正确的运行被报告为"完全未评审"。

建议修复:不要在 workflow 路径上用 prompt 记录声称门禁通过——要么按 #8769 排序(先落地基于 journal 的 coverage 再接线,期间修正注释/测试表述),要么让 workflow 分发挂载与 Agent 工具相同的 JSONL transcript writer。

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

Comment on lines +151 to +154
let rules: string | undefined;
if (args.rules) {
try {
rules = readFileSync(args.rules, 'utf8');

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: runEmitWorkflow adds a 6th copy of the read-plan-or-throw block and a 2nd read-rules-or-throw block, duplicating agent-prompt.ts — and the copy has already drifted: the agent-prompt rules error ends with "...would silently review without the project rules it was told to enforce" while this copy ends at "Omit --rules if this review has none."

Failure scenario: a future fix to one copy (e.g. distinguishing ENOENT from JSON parse failure) will not reach the others; a run under emit-workflow with a bad --rules path gets the shorter, less informative refusal.

Suggested fix: extract the pair into lib/ — e.g. readPlanFile(path, cmd) and readRulesFile(path, cmd) — and call them from both agent-prompt.ts and emit-workflow.ts.

中文说明

runEmitWorkflow 新增了第 6 份 read-plan-or-throw 代码块和第 2 份 read-rules-or-throw 代码块,与 agent-prompt.ts 重复——而且这份拷贝已经开始漂移:agent-prompt 的 rules 报错以"...would silently review without the project rules it was told to enforce"结尾,而这份拷贝只到"Omit --rules if this review has none."为止。

失败场景:未来对某一处的修复(例如区分 ENOENT 与 JSON 解析失败)不会到达其他拷贝;在 emit-workflow 下以错误的 --rules 路径运行,用户只会得到更短、信息更少的拒绝信息。

建议修复:把这一对抽取到 lib/——例如 readPlanFile(path, cmd)readRulesFile(path, cmd)——并让 agent-prompt.tsemit-workflow.ts 共同调用。

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

Comment on lines +117 to +118
const chunk = requiredAgents(plan).find((r) => r.role === 'chunk');
if (chunk) {

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 chunk-agent guard in buildWorkflowArgs is unreachable dead code — requiredAgents emits role: 'chunk' only inside its own isTerritoryFanOut branch (lib/roster.ts), the exact predicate the territory refusal ~20 lines above already throws on; the guard also computes the full roster a second time per emit.

Failure scenario: no runtime trigger exists — the branch can never fire; the cost is a duplicated requiredAgents walk per invocation and a branch implying a failure mode the preceding gates make impossible.

Suggested fix: delete the guard (the territory refusal is the real gate), or move the assert into buildRosterLaunches where both dispatchers pass through.

中文说明

buildWorkflowArgs 中的 chunk-agent 守卫是不可达的死代码——requiredAgents 只在 lib/roster.ts 自身的 isTerritoryFanOut 分支内产出 role: 'chunk',而这正是上方约 20 行处 territory 拒绝已经抛错的同一谓词;该守卫还让每次 emit 额外完整计算一次 roster。

失败场景:不存在运行时触发条件——该分支永远不会执行;代价是每次调用重复计算 requiredAgents,以及一个暗示着前面两道门禁已经排除的失败模式的分支。

建议修复:删除该守卫(territory 拒绝才是真正的门禁),或把断言移到两个分发器都经过的 buildRosterLaunches 中。

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

Comment on lines +80 to +84
export function buildWorkflowArgs(
report: PlanReport,
planPath: string,
rules?: string,
): WorkflowArgsFile {

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-4: buildWorkflowArgs is documented as a pure "build the payload" function but performs delivery-critical filesystem writes (buildRosterLaunchesbuildLaunchwriteBrief; recordPrompt per agent) before the --out write is ever validated. Probe-verified: with --out naming an existing file, the run throws EEXIST after 13 briefs and 13 prompt records are already on disk.

Failure scenario: a run that never dispatched and never will has populated the delivery gate's obligation register (readRecordedPrompts), and nothing distinguishes an aborted emit's records from a completed build's.

Suggested fix: move recordPrompt (and document the brief writes) to the command boundary after the --out write succeeds, or correct the doc comment to state the builder commits briefs/records.

中文说明

buildWorkflowArgs 的文档声称它是纯粹的"构建 payload"函数,但它会在 --out 写入被验证之前执行交付关键的文件系统写入(buildRosterLaunchesbuildLaunchwriteBrief;每个 agent 的 recordPrompt)。已用探针验证:当 --out 指向已存在的文件时,运行在 13 份 brief 与 13 条 prompt 记录落盘之后才抛出 EEXIST

失败场景:一次从未分发、也永远不会分发的运行已经填充了交付门禁的义务登记表(readRecordedPrompts),而中止的 emit 记录与完成的构建记录没有任何区别。

建议修复:把 recordPrompt(并在注释中说明 brief 写入)移到 --out 写入成功之后的命令边界,或修正文档注释以说明该 builder 会提交 brief/记录。

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

Comment on lines +47 to +50
if (args.version !== 1) {
throw new Error('review-step-3a: args version ' + args.version + ' does not match this script - re-run qwen review emit-workflow to regenerate both files together');
}
const agents = args.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] R1-5: The script's fail-closed validation covers missing/mis-bound/wrong-version args but not an empty agents array, which yields a clean zero-agent "complete" result. Probe-verified by executing the real constant: {version: 1, agents: []} returns {rosterSize: 0, delivered: [], missingRoles: []} — no error.

Failure scenario: a hand-written or truncated args.json with an empty agents array produces a "completed review that reviewed nothing", indistinguishable from a clean run.

Suggested fix: reject agents.length === 0 in the same guard block (throw the same emit-workflow-attributed message).

中文说明

脚本的 fail-closed 校验覆盖了缺失/绑定错误/版本不符的 args,但没有覆盖空的 agents 数组——空数组会得到干净的零 agent "完成"结果。已通过执行真实常量用探针验证:{version: 1, agents: []} 返回 {rosterSize: 0, delivered: [], missingRoles: []}——没有任何报错。

失败场景:手写或被截断的 args.json 携带空 agents 数组时,产生"评审了但什么也没评审"的结果,与干净运行无法区分。

建议修复:在同一守卫块中拒绝 agents.length === 0(抛出同样的 emit-workflow 归因错误信息)。

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

);
}

const agents = buildRosterLaunches(report, planPath, rules).map(

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-8: runEmitWorkflow validates only that the plan parses; a valid-JSON non-plan object (e.g. {}, a settings file, an old plan format) passes all three refusals and dies deep in agent-prompt internals with an agent-prompt:-attributed error, or silently emits a full roster for a plan that never was one. Probe-verified: {} throws agent-prompt: the plan has no \diffPathAbsolute`...` — pointing the user at the wrong command.

Failure scenario: a caller feeds a non-plan JSON object; the command fails inside buildRosterLaunches with an error naming the wrong command, or emits a roster derived from garbage fields.

Suggested fix: validate plan shape (diffPathAbsolute, chunks[]) up front in runEmitWorkflow with an emit-workflow:-prefixed error, as check-coverage's readPlan already does.

中文说明

runEmitWorkflow 只校验 plan 文件能解析;一个合法 JSON 但非 plan 的对象(例如 {}、settings 文件、旧版 plan 格式)会通过全部三道拒绝,然后死在 agent-prompt 内部深处,抛出 agent-prompt: 归因的错误,或为一份根本不是 plan 的文件静默发出完整 roster。已用探针验证:{} 抛出 agent-prompt: the plan has no \diffPathAbsolute`...`——把用户指向了错误的命令。

失败场景:调用者传入非 plan 的 JSON 对象;命令在 buildRosterLaunches 内部失败,错误指向错误的命令,或基于垃圾字段发出 roster。

建议修复:在 runEmitWorkflow 中提前校验 plan 形状(diffPathAbsolutechunks[]),抛出 emit-workflow: 前缀的错误——正如 check-coveragereadPlan 所做。

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

Comment on lines +165 to +166
const outDir = resolve(args.out);
mkdirSync(outDir, { recursive: true });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R2-2: The --out write path silently overwrites existing script.js/args.json and throws raw unwrapped Node errors (EEXIST/EACCES) on failure — the only failure path in the command without an emit-workflow: diagnostic and without a test. Probe-verified: pre-existing user files with those names are clobbered with no existence check; --out naming an existing file throws EEXIST: file already exists, mkdir '<path>'.

Failure scenario: run qwen review emit-workflow --plan p.json --out . in a populated directory — an existing script.js (a common build-script name) or args.json is silently destroyed; or --out names an existing file and the raw EEXIST error names neither --out nor that a directory is required.

Suggested fix: fail closed when either target already exists (or require an empty/newly-created out dir), and wrap the mkdir/write block in a try/catch rethrowing emit-workflow: cannot write into --out <path>: <message>.

中文说明

--out 写入路径会静默覆盖已存在的 script.js/args.json,并在失败时抛出原始未包装的 Node 错误(EEXIST/EACCES)——这是该命令中唯一没有 emit-workflow: 诊断、也没有测试的失败路径。已用探针验证:同名用户文件在没有存在性检查的情况下被覆盖;--out 指向已存在的文件时抛出 EEXIST: file already exists, mkdir '<path>'

失败场景:在已有内容的目录中运行 qwen review emit-workflow --plan p.json --out .——已存在的 script.js(常见的构建脚本名)或 args.json 被静默销毁;或者 --out 指向一个已存在的文件,原始 EEXIST 错误既不提及 --out 也不说明需要目录。

建议修复:当任一目标已存在时 fail closed(或要求 out 目录为空/新建),并用 try/catch 包装 mkdir/write 块,重抛为 emit-workflow: cannot write into --out <path>: <message>

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

Comment on lines +1653 to +1655
if (key !== req.key) {
throw new Error(
`built "${key}" where the roster requires "${req.key}" — the ` +

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] R3-2: Extracting the key-mismatch guard into the shared buildRosterLaunches silently dropped the agent-prompt: --roster prefix the --roster path's error carried before this diff — the mismatch error is now the one un-attributed diagnostic on the path that produced it, and no test pins either message.

Failure scenario: run qwen review agent-prompt --plan plan.json --roster on a plan where the roster key derivation and buildLaunch's key disagree (the exact internal-CLI-bug condition the guard exists for): the user gets an unprefixed error while every other error from the same build path keeps its agent-prompt: prefix.

Suggested fix: restore command attribution in the shared message, e.g. `agent-prompt: built "${key}" where the roster requires "${req.key}" — ...` — or parametrize the prefix so --roster and emit-workflow each get their own.

中文说明

把 key 不一致守卫抽取进共享的 buildRosterLaunches 时,静默丢失了 --roster 路径在本 diff 之前错误信息所带的 agent-prompt: --roster 前缀——这条不匹配错误现在成为该构建路径上唯一没有命令归属的诊断,且两个信息都无测试固定。

失败场景:在 roster key 派生与 buildLaunch key 不一致的 plan 上运行 qwen review agent-prompt --plan plan.json --roster(这正是该守卫存在的内部 CLI bug 条件):用户得到无前缀的错误,而同一条构建路径上的其他错误都保留 agent-prompt: 前缀。

建议修复:在共享信息中恢复命令归属,例如 `agent-prompt: built "${key}" where the roster requires "${req.key}" — ...`——或将前缀参数化,让 --rosteremit-workflow 各自得到正确归属。

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

Comment on lines +69 to +71
const value = returns[i];
if (value === null || value === undefined) {
missingRoles.push(agents[i].key);

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] R3-3: The script's fail-closed accounting treats an empty-string return as delivered, not missing — '' is neither null nor undefined, so it lands in delivered with text: '' and the run reports a clean missingRoles: []. Probe-verified: a GOAL termination with empty final text resolves to '' through the real dispatch chain and is counted as delivered.

Failure scenario: a workflow subagent that terminates GOAL with empty visible text (an analysis-only or empty final message) is counted as delivered-with-no-text — {rosterSize: N, delivered: [...], missingRoles: []} — exactly the "completed review that reviewed nothing" shape the script's own comments say it exists to prevent.

Suggested fix: treat empty/whitespace-only returns as missing too: if (value === null || value === undefined || (typeof value === 'string' && value.trim().length === 0)).

中文说明

脚本的 fail-closed 记账把空字符串返回视为 delivered 而非 missing——'' 既不是 null 也不是 undefined,所以它会以 text: '' 进入 delivered,运行报告出干净的 missingRoles: []。已用探针验证:最终文本为空的 GOAL 终止会通过真实分发链解析为 '' 并被计入 delivered。

失败场景:workflow 子代理以空可见文本终止 GOAL(仅分析或空最终消息)时,被记为"已交付但无文本"——{rosterSize: N, delivered: [...], missingRoles: []}——正是脚本自身注释声称要防止的"评审了但什么也没评审"形态。

建议修复:把空/纯空白返回也视为 missing:if (value === null || value === undefined || (typeof value === 'string' && value.trim().length === 0))

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

Comment on lines +47 to +50
const body = REVIEW_STEP_3A_WORKFLOW_SCRIPT.replace(
'export const meta =',
'const meta =',
);

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 script is executed for testing only in the Node host realm via new AsyncFunction — never under the real createWorkflowSandbox vm, which differs in three named ways: it runs the body as (async () => {...})(), on a null-prototype globalThis with no process/require, and it removes the entire meta declaration (stripExportMeta) where the harness merely rewrites export const meta = to const meta =. Probe-verified: a body using process.env.X or referencing meta passes all 9 tests and throws inside the vm.

Failure scenario: a future edit adding a host-only global (process.env.X) or referencing meta passes all 9 tests and throws ReferenceError at workflow dispatch, where no unit test sees it.

Suggested fix: execute the constant through the real sandbox (export createWorkflowSandbox or add a minimal runWorkflowScript helper), or add a host-realm isolation test running the body in a fresh vm.createContext(Object.create(null)) with only the documented globals injected.

中文说明

脚本仅通过 new AsyncFunction 在 Node 宿主 realm 中执行测试——从未在真实的 createWorkflowSandbox vm 下运行,后者有三处不同:以 (async () => {...})() 包裹执行、在无 process/require 的 null-prototype globalThis 上运行、并且会整体移除 meta 声明stripExportMeta),而测试 harness 只是把 export const meta = 改写为 const meta =。已用探针验证:使用 process.env.X 或引用 meta 的脚本体通过全部 9 条测试,但在 vm 内抛错。

失败场景:未来加入宿主专属全局(process.env.X)或引用 meta 的编辑通过全部 9 条测试,却在 workflow 分发时抛出 ReferenceError——任何单元测试都看不到。

建议修复:通过真实 sandbox 执行该常量(导出 createWorkflowSandbox 或添加最小 runWorkflowScript 助手),或增加宿主隔离测试,在只注入文档化全局的全新 vm.createContext(Object.create(null)) 中运行脚本体。

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

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not reviewed: integration tests (CLI, no sandbox) — the applicable GitHub check was skipped and Agent 7's scoped build-test run did not execute this integration configuration.

Test Plan (not a blocker): src/commands/review.test.tsno such file or directory; src/commands/review/emit-workflow.test.tsno such file or directory; src/commands/review/workflow-script.test.tsno such file or directory.

中文说明

未审查:integration tests (CLI, no sandbox) — the applicable GitHub check was skipped and Agent 7's scoped build-test run did not execute this integration configuration。

Test Plan(非阻断):src/commands/review.test.tsno such file or directory; src/commands/review/emit-workflow.test.tsno such file or directory; src/commands/review/workflow-script.test.tsno such file or directory

— gpt-5.6-sol via Qwen Code /review (v0.21.9)

Comment on lines +44 to +47
if (!args || !Array.isArray(args.agents)) {
throw new Error('review-step-3a: args.agents is missing or not an array - pass the args.json that qwen review emit-workflow wrote');
}
if (args.version !== 1) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R2-2: The workflow trusts any caller-supplied version-1 agents array instead of binding it to the CLI-emitted roster. — Failure scenario: an orchestrator omits Agent 2 or rewrites a prompt in a version-1 payload; the script dispatches and reports success from that forged array, defeating the feature's roster-width and prompt-fidelity guarantee. Bind the complete payload to immutable CLI-produced provenance, such as a digest encoded into the emitted artifact.

中文说明

Workflow 信任任意由调用方提供的 version-1 agents 数组,而没有把它绑定到 CLI 发出的 roster。失败场景:orchestrator 在 version-1 payload 中删掉 Agent 2 或改写某个 prompt;脚本仍会分发该伪造数组,并以同一数组报告成功,从而破坏本功能要保证的 roster 宽度与 prompt 保真。请把完整 payload 绑定到不可变的 CLI 产物溯源,例如在发出的 artifact 中编码 digest。

— gpt-5.6-sol via Qwen Code /review (v0.21.9)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Declined — the proposed binding cannot hold under this trust model. For a digest to detect a forged agents array, the verifier of the digest must be outside the forger's control. Here both halves travel together in the same model-authored Workflow call: args is inline-only (verified in WORKFLOW_PARAM_SCHEMA), so a caller that rewrites the array rewrites any digest beside it. Baking the digest into the emitted script does not help either — the Workflow tool accepts fully model-authored inline script (the XOR with scriptPath), so the script itself is not an immutable anchor the model cannot bypass.

The design's enforcement point is the delivery gate, not the script: check-coverage pairs harness-written evidence against CLI-written records — a dropped Agent 2 lands in missingRoles, a rewritten prompt lands in rewrittenPrompts. That is exactly the reported failure scenario, detected after the fact with evidence the orchestrator cannot retcon. The one genuine gap in that story — workflow dispatch writing no transcripts — is the R2-1 cluster (sequenced per #8769, comments/tests corrected this round). Adding a digest field the script cannot actually verify would be a dead switch per Simplicity First; happy to revisit if a runtime-side binding (e.g. the journal paired against the records) is wanted as a follow-up.

中文说明

拒绝——在该信任模型下所提议的绑定无法成立。 digest 要能检测伪造的 agents 数组,digest 的校验方必须在伪造者的控制之外。而此处两半都经由同一个模型编写的 Workflow 调用传递:args 仅支持内联(已在 WORKFLOW_PARAM_SCHEMA 核实),能改写数组的调用方同样能改写其旁的任何 digest。把 digest 烧进发出的脚本也无济于事——Workflow 工具同样接受完全由模型编写的内联 script(与 scriptPath 互斥),脚本本身并不是模型绕不过去的不可篡改锚点。

本设计的强制点是交付门禁,而非脚本:check-coverage 把 harness 写入的证据与 CLI 写入的记录配对——被丢弃的 Agent 2 落入 missingRoles,被改写的 prompt 落入 rewrittenPrompts。这恰好覆盖所报告的失败场景,且是事后以 orchestrator 无法篡改的证据检出。该链条上真正的缺口——workflow 分发不写 transcript——即 R2-1 聚类(已按 #8769 排序,本轮修正了注释/测试表述)。按 Simplicity First,为脚本加一个它实际无法校验的 digest 字段只是死开关;如果希望改为运行时侧绑定(例如 journal 与记录配对),很乐意作为后续工作再议。

Comment on lines +70 to +73
if (value === null || value === undefined) {
missingRoles.push(agents[i].key);
} else {
delivered.push({ key: agents[i].key, text: value });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R2-3: An empty model-visible agent result is counted as successfully delivered. — Failure scenario: a GOAL subagent returns analysis-only output that is stripped to ''; this branch adds the role to delivered and omits it from missingRoles, so the review appears complete without a finding or examination receipt. Treat non-string and empty-after-trimming results as missing, and add an empty-string regression test.

中文说明

空的 model-visible agent 结果会被计为成功交付。失败场景:一个 GOAL subagent 只返回 analysis 内容,清洗后变成空字符串 '';当前分支仍把该角色加入 delivered,且不放入 missingRoles,于是 review 在没有 finding 或检查回执的情况下看起来已完成。请把非字符串和 trim 后为空的结果视为 missing,并添加空字符串回归测试。

— gpt-5.6-sol via Qwen Code /review (v0.21.9)

@wenshao

wenshao commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

@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 11, 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)即可释放。

… fail closed

Address PR review: the printed handoff described a Workflow call the tool
cannot honor. Write the script into the saved-workflow dir (the only place
`Workflow({scriptPath})` loads from), print an args contract that matches
the tool's inline-only `args`, and refuse up front when workflows are
disabled. Harden the emitted script fail-closed (guard order, payload and
element shape, empty-roster and empty-return accounting, zero-delivery
throw), restore error attribution, drop dead payload fields and the
unreachable chunk guard, validate plan shape, and share one plan/rules
reader between agent-prompt and emit-workflow.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Review feedback round — PR #8846 (feat/review-emit-workflow)

One commit this round: 7e9c945dbcfix(review): make emit-workflow's contract executable and the fan-out fail closed (7 files, +764/−139, including one new lib module). No conflicts; --conflict was false and no merge was performed.

What changed, by finding cluster

1. The printed handoff contract is now executable (Critical R1-2 and twins)

Findings: rc:3749682254 (R1-2), rc:3748749274, rc:3749076012 (script refused outside saved dirs), rc:3748749523, rc:3749076258 (args: <path> has no parameter), rc:3749076945 (no enablement gate). All three walls were re-verified at the reviewed commit before fixing:

  • The emitted script is now written into the project's saved-workflow dir (<cwd>/.qwen/workflows/review-step-3a.js) — the only place Workflow({scriptPath}) loads from (readWorkflowFileSecurely, workflow-saved.ts). A pre-existing file at that name is refused unless it is byte-identical to this command's own script (idempotent re-emit); anything else is the user's and is never overwritten.
  • The printed contract now matches what the Workflow tool can honor: scriptPath points at the saved script, and the args line says to read the args file and pass its parsed JSON contents inlineargs is inline-only on the tool (verified: WORKFLOW_PARAM_SCHEMA has no path form). The guard message inside the script was updated to name the same remediation instead of circularly telling the caller to "pass the args.json" (rc:3750235547, R2-2-bot).
  • The command now refuses up front — before any brief or prompt record is written — when workflows are disabled (kill switch QWEN_CODE_DISABLE_WORKFLOWS=1 honored over the opt-in). The gate mirrors Config.isWorkflowsEnabled() exactly as wired today: the settings-based config flag has no live setter (setWorkflowsEnabled is never called outside tests; no settings-schema entry), so the env opt-in is the sole enable path; a code comment records that parity. This is also the "gate it behind an env flag until the skill lands" option rc:3748750573 offered; the staged-ahead-of-skill state is now documented in the file header (see cluster 2).
  • The honest cost is stated in the header and in this summary: with args inline-only, the prompts cross the orchestrator's context once (the tool call). Prompt fidelity across that crossing is enforced by the coverage gate's verbatim check, not by trust. An argsPath-style Workflow tool parameter would restore the original "prompts never cross the model" premise; that is a core-tool change beyond this PR's scope and is noted as follow-up material, not implemented here.

2. Delivery-gate honesty (Critical R2-1 and twins)

Findings: rc:3750235533, rc:3754892531 (R2-1), rc:3748749123, rc:3749076474, rc:3748748934. Verified at the code: coverageFromTranscripts reads two registers — recorded prompts (readRecordedPrompts) and subagent transcripts (readTranscripts), and workflow dispatch attaches no JSONL transcript writer (attachJsonlTranscriptWriter is called only by the Agent tool and background-agent-resume). So recordPrompt alone does not make a workflow run pass check-coverage. This round implements the reviewer's option (a) — sequence per #8769:

  • The false claim is gone: the recordPrompt comment now states that records close the gate's briefless half only, that the transcript half is not produced by workflow dispatch, and that a workflow-dispatched run cannot pass check-coverage until coverage reads the workflow journal instead (Proposal: rebuild /review Step 3–5 orchestration on the workflow engine #8769). The matching test comment was corrected the same way.
  • The file header states plainly that the skill does not route Step 3A through this command until the journal-based gate lands. Nothing is wired to the skill in this PR, so no run can be misreported as a result of this diff.

3. Empty/failed results fail closed (Critical R2-3 + suggestions)

Findings: rc:3754990723 (R2-3), rc:3754892554 (R3-3), rc:3749077101. In the emitted script: null/undefined/non-string/empty/whitespace-only agent returns now all count as missingRoles (previously '' was counted as delivered), and a fan-out where zero agents returned anything now throws instead of returning a clean-shaped result. Partial results still return normally so one dead agent costs one dimension, not the run. Regression tests added for each case.

4. Script fail-closed hardening (suggestions)

  • Guards moved before phase('Review') so a corrupt payload never records a Review phase that dispatched nothing (rc:3749682282, R1-6) — test pins phases === [] on refusal.
  • Empty agents array refused (rc:3749682277, rc:3754892542, R1-5).
  • Per-element validation before any dispatch: null/truncated entries can no longer die mid-fan-out as a bare TypeError (rc:3749682306, R1-9; rc:3748750232).
  • The script now runs in a bare-vm test mirroring the real sandbox (meta stripped outright, async-IIFE wrap, null-prototype globalThis with only the documented globals injected), plus a scan that the executed body references no host handles (process, require(, meta) (rc:3754892555, R4-1).

5. emit-workflow command hardening (suggestions)

  • Output destinations are validated/created before any brief or prompt record is written (rc:3749682271, rc:3754892538, R1-4): an unusable --out (existing file, existing args.json, foreign file at the saved script name) now refuses with an emit-workflow:-attributed error and leaves zero records behind — probe case re-tested. The docblock no longer claims the builder is filesystem-free.
  • --out write failures are wrapped (emit-workflow: cannot create the --out directory … / cannot write the emitted workflow: …); existing script.js/args.json are never silently clobbered (rc:3754892546, R2-2-wenshao).
  • Plan shape validated up front (rc:3749682286 R1-7; rc:3749682298/rc:3754892544 R1-8): non-object JSON (null/array/primitive) is refused by the shared reader with command attribution; objects missing diffPathAbsolute, chunks[], or files[] are refused as "not a plan report" — the truncated-plan probe that used to silently emit a full roster now stops.
  • Dead code/fields removed per Simplicity First: the unreachable chunk-agent guard (and its second full roster derivation) (rc:3749682266, rc:3754892537, rc:3749077511, R1-3); the mode and plan payload fields that nothing reads (rc:3749078380) — the payload is now exactly {version, agents}, pinned by a test.
  • rosterLabel output pinned by direct unit tests with literal expected strings (chunk case, role case, invariant-file case, control-character flattening, separator-glyph/backtick stripping), independent of the emitted payload (rc:3750235574, R2-4).
  • Shared readPlanFile/readRulesFile extracted to review/lib/plan-file.ts and consumed by both agent-prompt and emit-workflow (rc:3749682262, rc:3754892534, rc:3749078187, R1-1) — the drifted rules message is unified on the fuller agent-prompt text. The four other pre-existing copies (build-test, test-plan, script-lint, base-tree) were deliberately not migrated this round: each has drifted semantics, and unifying them touches four commands this PR does not otherwise touch; recorded here as a follow-up.
  • Key-mismatch error attribution restored via a command parameter on buildRosterLaunches (agent-prompt: --roster / emit-workflow) (rc:3754892551, R3-2).
  • --out describe updated (it now receives args.json only; the script goes to the saved-workflow dir).

6. Re-verified as already fixed (round 1)

  • rc:3748748934 (recordPrompt missing): the round-1 fix stands — every emitted agent is recorded (test pins record-vs-payload parity); the remaining half is cluster 2.
  • rc:3748749844 (label never read): the script dispatches label: a.label; pinned by test.
  • rc:3748750039 (version never read): the script asserts args.version !== 1; pinned by test.

Declined / escalated (threads left open, replies posted)

  • rc:3754990718 (R2-2, provenance digest) — DECLINED with evidence. A digest passed alongside the payload binds nothing: both flow through the same model-authored Workflow call (args is inline-only), and the tool also accepts fully model-authored inline script, so the emitted script is not an immutable anchor either. The design's enforcement point is check-coverage pairing harness-written evidence against CLI-written records — a dropped agent is missingRoles, a rewritten prompt is rewrittenPrompts, which is exactly the reported scenario (its workflow-path transcript gap is the R2-1 cluster above). An unverifiable digest field would be dead weight per Simplicity First.
  • rc:3748749680, rc:3748750409, rc:3749076742 (A/B cleanliness: terse system prompt, 50-turn/10-min per-agent caps, single wall-clock) — ESCALATED to maintainer. These are properties of the workflow runtime's dispatch paths (workflow-orchestrator.ts constants; no opt-out in KNOWN_AGENT_OPTS), not of this PR's emitted artifact, and changing them is a core-runtime + experiment-design decision. Options and recommendation posted in each thread; the open question rides along until a maintainer answers.
  • rc:3749077314 (pr-worktree refusal; add a working-directory option to agent()) — DECLINED for this PR, follow-up recorded. The option would mean new KNOWN_AGENT_OPTS surface plus dispatch/AgentHeadless cwd plumbing in core — a security-relevant runtime feature that deserves its own review, not a drive-by here. Until it lands, the refusal is the correct fail-closed behavior (dispatched agents would otherwise review the wrong tree).

Also addressed in this summary rather than inline: the three top-level CHANGES_REQUESTED review bodies and the issue-level summary comment (all their concrete points map to the findings above), and the "Test Plan (not a blocker)" notes — the three named files exist at packages/cli/src/commands/review.test.ts, .../review/emit-workflow.test.ts, .../review/workflow-script.test.ts and all pass (the review paths omitted the packages/cli prefix).

Verification

Commands actually run this round (repo root unless noted; runner had a clean npm ci + build beforehand):

  • npm run build — passed (exit 0; the initial failure was a type error my own change introduced — readPlanFile returned lib/report.ts's PlanReport where agent-prompt.ts uses its own local interface — fixed and re-verified).
  • npm run typecheck — passed (exit 0, all workspaces).
  • npx tsc --noEmit in packages/cli — passed (0 errors).
  • npx eslint on all seven changed/new files — passed (0 findings).
  • npx prettier --check on all changed files — passed.
  • npx vitest run src/commands/review/ in packages/cli66 files, 2353 passed | 4 pre-existing skips, re-run after the final edit and again on the committed state (266 passed across the four directly-touched files).
  • Integration tests after npm run bundlenot run: the changed behavior (review subcommand handlers and the emitted-script constant) is exercised directly by the unit tests above, not only through the bundled CLI or integration harness; per the round rules they apply only in that case. CI's integration checks remain the final gate.
中文说明

评审反馈处理轮次 — PR #8846feat/review-emit-workflow

本轮一个提交:7e9c945dbcfix(review): make emit-workflow's contract executable and the fan-out fail closed(7 个文件,+764/−139,含一个新的 lib 模块)。无冲突;--conflict 为 false,未执行任何合并。

按发现聚类的改动

1. 打印的交接契约现在可被执行(Critical R1-2 及其同族)

涉及发现:rc:3749682254(R1-2)、rc:3748749274、rc:3749076012(saved 目录之外的脚本被拒载)、rc:3748749523、rc:3749076258(args: <路径> 无对应参数)、rc:3749076945(无启用门禁)。修复前已在被评审提交上逐一复核三堵墙:

  • 发出的脚本现在写入项目级 saved-workflow 目录(<cwd>/.qwen/workflows/review-step-3a.js)——这是 Workflow({scriptPath}) 唯一允许加载的位置(readWorkflowFileSecurely,workflow-saved.ts)。该名下已有文件时,除非与本命令自己的脚本逐字节相同(幂等重发),否则拒绝;其他任何内容视为用户文件,绝不覆盖。
  • 打印的契约现在与 Workflow 工具能履行的形式一致:scriptPath 指向 saved 脚本,args 行明确要求"读取 args 文件并将其解析后的 JSON 内容内联传入"——该工具的 args 只接受内联值(已核实 WORKFLOW_PARAM_SCHEMA 没有路径形式)。脚本内的守卫报错也更新为指明同一补救方式,而不是循环地要求调用方"传入那个 args.json"(rc:3750235547,R2-2-bot)。
  • 命令现在会在写入任何 brief 或 prompt 记录之前先行拒绝——当 workflows 被禁用时(kill switch QWEN_CODE_DISABLE_WORKFLOWS=1 优先于启用开关)。该门禁与 Config.isWorkflowsEnabled() 的现有接线完全一致:settings 侧的配置位没有任何活的设置方(setWorkflowsEnabled 仅在测试中被调用;settings schema 无此项),因此 env 启用开关是唯一启用路径;代码注释记录了这一等价性。这也是 rc:3748750573 提供的"在 skill 落地前先用 env 开关门控"选项;"先于 skill 分阶段落地"的状态现已写入文件头(见聚类 2)。
  • 代价在文件头与本总结中如实说明:在 args 仅支持内联的前提下,prompts 会经过 orchestrator 上下文一次(工具调用)。这次传递的保真由 coverage 门禁的逐字校验强制,而非依赖信任。给 Workflow 工具增加 argsPath 式参数可以恢复"prompts 永不经过模型"的原始前提;那属于 core 工具改动,超出本 PR 范围,仅作为后续工作记录,未在本轮实现。

2. 交付门禁的诚实性(Critical R2-1 及其同族)

涉及发现:rc:3750235533、rc:3754892531(R2-1)、rc:3748749123、rc:3749076474、rc:3748748934。已在代码中核实:coverageFromTranscripts 读取两个登记表——记录的 prompts(readRecordedPrompts subagent transcripts(readTranscripts),而 workflow 分发不挂接任何 JSONL transcript writer(attachJsonlTranscriptWriter 仅被 Agent 工具与 background-agent-resume 调用)。因此仅有 recordPrompt 并不能让 workflow 运行通过 check-coverage。本轮实现评审者给出的选项 (a)——按 #8769 排序:

  • 虚假声明已移除:recordPrompt 处注释现明确说明记录只关闭门禁的 briefless 一半;transcript 一半不是 workflow 分发的产物;在 coverage 改读 workflow journal(Proposal: rebuild /review Step 3–5 orchestration on the workflow engine #8769)之前,workflow 分发的运行无法通过 check-coverage。测试中对应注释同步修正。
  • 文件头如实声明:在 journal 版门禁落地之前,skill 不会把 Step 3A 接入本命令。本 PR 不包含任何 skill 接线,因此本 diff 不会导致任何运行被误报。

3. 空/失败结果按失败处理(Critical R2-3 + 建议)

涉及发现:rc:3754990723(R2-3)、rc:3754892554(R3-3)、rc:3749077101。发出的脚本中:null/undefined/非字符串/空串/纯空白返回全部计入 missingRoles(此前 '' 被计为已交付);当零个 agent 有返回时,脚本抛错而不是返回一个形状"干净"的结果。部分结果仍正常返回——一个死掉的 agent 只损失一个维度,不损失整轮运行。每种情形均有回归测试。

4. 脚本 fail-closed 加固(建议)

  • 守卫移至 phase('Review') 之前:损坏的 payload 不再留下一个"从未派发任何东西的 Review 阶段"(rc:3749682282,R1-6)——测试钉住拒绝时 phases === []
  • 拒绝空的 agents 数组(rc:3749682277、rc:3754892542,R1-5)。
  • 派发前逐元素校验:null/被截断的条目不再会在扇出中途以裸 TypeError 死掉(rc:3749682306,R1-9;rc:3748750232)。
  • 脚本新增裸 vm 测试,镜像真实沙箱(meta 声明被整体移除、async-IIFE 包裹、null-prototype globalThis 且只注入文档化全局),并扫描执行体不引用任何宿主句柄(processrequire(meta)(rc:3754892555,R4-1)。

5. emit-workflow 命令加固(建议)

  • 输出目的地在任何 brief/prompt 记录写入之前完成校验与创建(rc:3749682271、rc:3754892538,R1-4):不可用的 --out(已存在的文件、已存在的 args.json、saved 脚本名下是他人文件)现在以 emit-workflow: 归因报错且不留下任何记录——探针场景已重测。docblock 不再声称 builder 与文件系统无关。
  • --out 写入失败被包裹(emit-workflow: cannot create the --out directory … / cannot write the emitted workflow: …);已存在的 script.js/args.json 永不被静默覆盖(rc:3754892546,R2-2-wenshao)。
  • plan 形状前置校验(rc:3749682286 R1-7;rc:3749682298/rc:3754892544 R1-8):非对象 JSON(null/数组/基本类型)由共享读取器以命令归因拒绝;缺 diffPathAbsolutechunks[]files[] 的对象被拒绝为 "not a plan report"——过去会静默发出完整 roster 的截断 plan 探针现在会被拦下。
  • 按 Simplicity First 移除死代码/死字段:不可达的 chunk-agent 守卫(及其第二次完整 roster 推导)(rc:3749682266、rc:3754892537、rc:3749077511,R1-3);无任何读取方的 modeplan 载荷字段(rc:3749078380)——载荷现在恰好是 {version, agents},并由测试钉住。
  • rosterLabel 的输出改为用带字面期望字符串的直接单元测试钉住(chunk、role、invariant-file、控制字符扁平化、分隔符图形/反引号剥离),独立于发出的载荷(rc:3750235574,R2-4)。
  • 共享的 readPlanFile/readRulesFile 提取至 review/lib/plan-file.ts,由 agent-promptemit-workflow 共同调用(rc:3749682262、rc:3754892534、rc:3749078187,R1-1)——已漂移的 rules 报错统一为信息更完整的 agent-prompt 版本。其余四处既有拷贝(build-test、test-plan、script-lint、base-tree)本轮刻意不迁移:各自语义已有漂移,统一它们会触及本 PR 本不改动的四个命令;作为后续工作记录在案。
  • 通过给 buildRosterLaunches 增加 command 参数恢复 key 不一致报错的命令归属(agent-prompt: --roster / emit-workflow)(rc:3754892551,R3-2)。
  • --out 的 describe 已更新(现在只接收 args.json;脚本写入 saved-workflow 目录)。

6. 复核确认已在第一轮修复

  • rc:3748748934(缺 recordPrompt):第一轮修复成立——每个发出的 agent 都有记录(测试钉住记录与载荷逐字节一致);剩余一半即聚类 2。
  • rc:3748749844(label 无读取方):脚本以 label: a.label 派发;测试已钉住。
  • rc:3748750039(version 无读取方):脚本断言 args.version !== 1;测试已钉住。

拒绝 / 升级(线程保持打开,已回帖)

  • rc:3754990718(R2-2,provenance digest)——拒绝,附证据。 与载荷一同传入的 digest 什么也绑定不了:两者都经由同一个模型编写的 Workflow 调用传递(args 仅支持内联),且该工具同样接受完全由模型编写的内联 script,因此发出的脚本也不是不可篡改的锚点。本设计的强制点在 check-coverage:它把 harness 写入的证据与 CLI 写入的记录配对——丢弃的 agent 表现为 missingRoles,被改写的 prompt 表现为 rewrittenPrompts,恰好覆盖所报告的场景(其 workflow 路径上的 transcript 缺口即上文 R2-1 聚类)。按 Simplicity First,一个脚本无法真正校验的 digest 字段只是死重。
  • rc:3748749680、rc:3748750409、rc:3749076742(A/B 纯净性:简洁化系统提示、每 agent 50 轮/10 分钟上限、单一墙钟)——升级给维护者。 这些是 workflow 运行时分发路径的属性(workflow-orchestrator.ts 常量;KNOWN_AGENT_OPTS 无退出选项),不是本 PR 发出物可以改变的;修改它们属于 core 运行时 + 实验设计决策。各线程已附选项与建议,问题留待维护者答复。
  • rc:3749077314(pr-worktree 拒绝;给 agent() 增加工作目录选项)——本 PR 拒绝,后续工作已记录。 该选项意味着新的 KNOWN_AGENT_OPTS 表面加分发/AgentHeadless cwd 接线——一个涉及安全的运行时特性,应走独立评审,而不是顺手带进本 PR。在其落地之前,拒绝是唯一正确的 fail-closed 行为(否则派发的 agent 会评审错误的树)。

另在本总结(而非行内)中处理:三条顶层 CHANGES_REQUESTED 评审正文与 issue 级总结评论(其所有具体点都映射到上述发现),以及 "Test Plan (not a blocker)" 说明——被点名的三个文件实际存在于 packages/cli/src/commands/review.test.ts.../review/emit-workflow.test.ts.../review/workflow-script.test.ts 且全部通过(评审中的路径漏了 packages/cli 前缀)。

验证

本轮实际执行的命令(未注明者均在仓库根目录;runner 开始前已有干净的 npm ci + build):

  • npm run build — 通过(退出码 0;最初的失败是本轮改动自己引入的类型错误——readPlanFile 返回了 lib/report.tsPlanReport,而 agent-prompt.ts 使用自己的本地接口——已修复并重新验证)。
  • npm run typecheck — 通过(退出码 0,全部 workspaces)。
  • packages/clinpx tsc --noEmit — 通过(0 错误)。
  • 对全部七个改动/新增文件执行 npx eslint — 通过(0 发现)。
  • 对全部改动文件执行 npx prettier --check — 通过。
  • packages/clinpx vitest run src/commands/review/66 个文件,2353 通过 | 4 个既有跳过;在最后一次编辑后重跑,并在已提交状态上再次运行(直接触及的四个文件 266 通过)。
  • npm run bundle 后的集成测试 — 未执行:被改动的行为(review 子命令 handler 与发出的脚本常量)由上述单元测试直接执行,并非只经由打包后的 CLI 或集成测试框架触达;按本轮规则,仅在那种情况下才需要集成测试。CI 的集成检查仍是最终门禁。

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

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


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

@qqqys
qqqys dismissed stale reviews from qwen-code-ci-bot, wenshao, qwen-code-ci-bot, wenshao, qwen-code-ci-bot, and wenshao August 11, 2026 14:20

Superseded by head 7e9c945; no current unreplied Critical remains. Clearing stale CHANGES_REQUESTED state at the PR author request.

@qqqys

qqqys commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #8943 — closing this one.

The review was right and the handoff layer here does not work, so this is a rewrite rather than a patch:

  • Workflow({scriptPath}) loads through readWorkflowFileSecurely, which realpaths the file and refuses anything outside the saved-workflow directories, so the script could never be written to --out.
  • args is inline JSON with no path form, so args: <path> was not a consumable instruction.
  • And because the sandbox has no filesystem, an args-carried roster is a roster the model has to retype — reintroducing, one layer up, exactly the failure this change exists to remove. feat(review): dispatch Step 3A's fan-out from a generated workflow script, and route to it #8943 bakes the roster into the generated script instead, so the call carries one path and no payload.

Also addressed there: the missing recordPrompt (without it the whole roster read as unlaunched), the workflows-enabled precondition, the all-agents-failed path returning a value instead of throwing, cleanup of the generated script out of the user's saved-workflow dir, the dead label/version/mode/plan fields, the unreachable chunk guard, and the sixth copy of the plan-read block.

The claim that this was a one-variable A/B was also wrong, and thanks for catching it: workflow dispatch substitutes its own terse subagent persona, so identical prompts were producing different agents. #8943 passes agentType: 'general-purpose' and tests it. The per-agent turn ceiling and the run-level wall clock remain genuine differences and are named rather than papered over.

@qqqys qqqys closed this Aug 11, 2026
qqqys added a commit to qqqys/qwen-code that referenced this pull request Aug 17, 2026
…ript

Supersedes QwenLM#8846, whose handoff layer did not work: it wrote the script to
an arbitrary `--out` directory and told the caller to pass `args: <path>`.
Neither is a form the Workflow tool accepts. `readWorkflowFileSecurely`
realpaths `scriptPath` and refuses anything outside the saved-workflow
directories, and `args` is inline JSON with no path form. Both were verified
against the tool's own contract this time, not just the sandbox's.

The sandbox has no filesystem — its globals are agent/parallel/pipeline/
phase/log/console/args/budget/workflow and nothing that opens a file — so a
roster carried in `args` is a roster the model has to retype into its tool
call. That is the failure this change exists to remove, reintroduced one
layer up. The roster is therefore baked into the generated script, and the
model's call carries one path and no payload.

`--roster` and `emit-workflow` still build the same prompts from the same
plan through the same `buildLaunch`. What differs is who launches them:
`--roster` asks the orchestrator to copy ~13 blocks into agent calls, in one
response, unedited; this writes them into a file the runtime reads.

The generated file is a fixed body plus one JSON literal. No logic is
generated, so the part that can be wrong is the part the tests execute — and
they execute the real output of the generator, including a roster with
backticks, `${`, backslashes and newlines in the prompts.

Not a one-variable change, and the difference is now handled rather than
claimed away: workflow dispatch substitutes its own terse subagent persona
unless an agentType is given, so the script passes
`agentType: 'general-purpose'` — the subagent type SKILL.md requires of the
hand-launched path. Otherwise the two paths would run different agents over
identical prompts and an A/B between them would not mean anything.

Also from the QwenLM#8846 review:

- Prompts are recorded. `check-coverage` compares each launch against what
  the CLI recorded handing out, so without this the whole roster read as
  unlaunched.
- The workflows gate is checked before anything is written, so a run that
  cannot execute what this emits leaves no script and no prompt records
  implying it did.
- A fan-out where every agent failed throws instead of returning a value.
  Returned, it would let the caller aggregate over a diff no agent read.
- Cleanup sweeps the generated script. It has to live in the user's
  saved-workflow dir, where every file is a `/<name>` slash command, so a
  review that left one behind would hand the user a permanent command for a
  diff that no longer exists.
- The dead `label` / `version` / `mode` / `plan` fields are gone with the
  args file, and the unreachable chunk guard with them.
- `readPlanReport` is one definition in lib/report.ts rather than a sixth
  copy of the try/parse/rethrow block; moving the existing five onto it is
  mechanical and left to its own change.

Still refused, unchanged: a territory fan-out (3B), whose chunk agents carry
a per-chunk contract this script does not express, and a worktree review,
which needs every agent pinned to the PR worktree.

Draft: this depends on QwenLM#8839 for coverage evidence — workflow-dispatched
agents write no subagent transcript without it — and nothing routes through
this yet, so review behaviour is unchanged.

Part of QwenLM#8769.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.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.

4 participants