Skip to content

feat(core): require an explicit user opt-in before the model launches a workflow - #9806

Merged
wenshao merged 7 commits into
QwenLM:mainfrom
qqqys:feat/workflow-optin-gate
Aug 24, 2026
Merged

feat(core): require an explicit user opt-in before the model launches a workflow#9806
wenshao merged 7 commits into
QwenLM:mainfrom
qqqys:feat/workflow-optin-gate

Conversation

@qqqys

@qqqys qqqys commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds a rule to the Workflow tool description telling the model not to start a workflow unless the user actually asked for multi-agent orchestration, and enumerates the five ways a user can ask under this project's own triggers: the workflow keyword, a request in the user's own words, a skill or slash command whose instructions say to use the tool, naming a saved workflow to run, and asking to resume an earlier run.

The rule is placed above the existing "What a workflow is for" guidance rather than below it, so that it frames that guidance instead of reading as a footnote to it. It closes with an offer-and-ask path: when a workflow really would be the better tool but nobody asked for one, the model should say in a sentence what it would fan out over and roughly how many agents that is, then let the user decide.

Two deliberate choices. Upstream's opt-in marker is the keyword ultracode, which does not exist in this repository — the string appears once in the whole tree, inside a comment — so it is not among the enumerated forms; naming it would advertise a trigger no user here can pull. And the agent count in the rule's justification is interpolated from DEFAULT_MAX_AGENTS_PER_RUN rather than pasted as prose, matching how every other cap in this description is written, so raising the cap moves this sentence too.

Why it's needed

The description already carries a policy layer about orchestrating well — prefer pipeline() over a barrier, verify a finding before reporting it, log what was dropped. What it has never carried is a rule about when not to orchestrate at all. Read on its own, that prose is encouragement, and the model reasonably concludes that a task which would parallelize nicely is a task that should be parallelized.

That is an expensive default for this particular tool. A single run can dispatch up to the per-run agent cap and spend tokens accordingly, and today the only thing standing between an inferred fan-out and the spend is an approval dialog that says Run a workflow script (4127 chars) and nothing else. Until that dialog says more, the cheapest place to require an explicit request is the description the model reads before it decides.

This is a narrowing change: its intended effect is that the tool gets called less often, and only when someone asked for it.

Reviewer Test Plan

How to verify

The change is prose inside WORKFLOW_TOOL_DESCRIPTION plus one test that anchors it. Run the tool's own suite and the two adjacent config suites from packages/core:

cd packages/core
npx vitest run src/tools/workflow/ src/config/config.workflows.test.ts src/config/config.workflow-registration.test.ts

Observed: Test Files 3 passed (3), Tests 53 passed (53).

The new test, description gates the tool on an explicit user request, asserts more than presence. It checks that the gate's index in the description precedes the index of the **What a workflow is for** heading, because position is the point — a rule placed after the pitch qualifies the pitch instead of framing it. It checks that each of the five enumerated forms is a real trigger in this codebase. It asserts the description does not contain ultracode. It anchors the offer-and-ask sentences, which are the guard against the change's one real failure mode. And it anchors the agent count through the exported constant rather than as a literal.

To confirm the test is anchored to the prose rather than passing vacuously, revert only the production file and re-run: git stash push -- packages/core/src/tools/workflow/workflow.ts && cd packages/core && npx vitest run src/tools/workflow/workflow.test.ts. Observed: the new test fails, every other test in the file still passes.

WORKFLOW_TOOL_DESCRIPTION has exactly one consumer — the WorkflowTool constructor in the same file — and no snapshot captures it, so no other suite is affected.

Evidence (Before & After)

N/A — the change is model-facing prose in a tool description, with no TUI surface.

Tested on

OS Status
🍏 macOS ⚠️ not tested
🪟 Windows ⚠️ not tested
🐧 Linux ✅ tested

Environment

Unit tests only, via npx vitest run from packages/core. Also checked with npx tsc --noEmit -p tsconfig.json (exit 0), npx prettier --check, and npx eslint on both changed files.

Risk & Scope

  • Main risk or tradeoff: over-blocking. A rule that says "do not call this tool" can be read as "refuse", in which case a user who would have said yes to a workflow never gets asked. The mitigation is the fallback paragraph, which directs the model to describe what it would fan out over and hand the decision back — and that paragraph is anchored by its own assertions so it cannot be dropped silently in a later edit. The opposite risk, that the rule is too weak to change behaviour, is not addressed here and cannot be settled by a unit test; it is a judgement about prose.
  • Not validated / out of scope: no measurement of how model behaviour actually shifts — this is a description change, and the honest claim is that the rule is now stated, not that compliance has been observed. The approval dialog that would make the same disclosure at consent time is a separate change and is not touched here. Nothing about enablement, the keyword trigger's own implementation, or the script parameter's authoring contract changes.
  • Breaking changes / migration notes: none. No API, no schema, no settings key, no runtime behaviour — the tool's inputs, outputs, and limits are all unchanged.

Linked Issues

None.

中文说明

这个 PR 做了什么

在 Workflow 工具描述中加入一条规则:除非用户确实要求了多智能体编排,否则模型不应启动 workflow;并列出在本项目自身的触发方式下,用户"提出要求"的五种形式:workflow 关键词、用户用自己的话提出、指示使用该工具的 skill 或斜杠命令、指名运行某个已保存的 workflow、以及要求恢复此前的运行。

这条规则被放在现有的 "What a workflow is for" 指引之上而非之下,这样它是在为那段指引定框架,而不是读起来像它的一条脚注。规则结尾给出一条"提议并询问"的路径:当 workflow 确实是更合适的工具但没有人提出要求时,模型应当用一句话说明它会在什么上做扇出、大致需要多少个 agent,然后把决定权交还给用户。

有两处是刻意的取舍。上游用于此目的的标记是关键词 ultracode,而它在本仓库中并不存在——这个字符串在整棵代码树中只出现过一次,还是在一条注释里——因此它没有被列入这五种形式;列出它等于向模型宣传一个本地用户根本无法触发的入口。另外,规则中用于论证的 agent 数量是从 DEFAULT_MAX_AGENTS_PER_RUN 插值而来,而不是作为文字硬写,这与该描述中其它所有上限的写法一致,因此上调该上限时这句话会同步变化。

为什么需要

该描述中已经有一层关于"如何编排得好"的策略文字——优先用 pipeline() 而非 barrier、报告结论前先验证、把被丢弃的部分 log() 出来。它从来没有承载的,是一条关于"何时根本不该编排"的规则。单独去读,那些文字就是鼓励,模型于是合理地得出结论:一个适合并行化的任务就是一个应该被并行化的任务。

对这个工具而言,这是一个代价高昂的默认行为。单次运行最多可以派发到每次运行的 agent 上限,并相应地消耗 token,而目前挡在"推测出来的扇出"与这笔开销之间的,只有一个只会显示 Run a workflow script (4127 chars) 的审批对话框。在那个对话框能说出更多信息之前,要求用户明确提出请求,成本最低的位置就是模型做决定前所读的这段描述。

这是一个收紧性的改动:它的预期效果是这个工具被调用得更少,并且只在有人提出要求时才被调用。

审阅者测试计划

如何验证

改动是 WORKFLOW_TOOL_DESCRIPTION 内部的文字,外加一个为其兜底的测试。在 packages/core 下运行该工具自身的测试与两个相邻的 config 测试:

cd packages/core
npx vitest run src/tools/workflow/ src/config/config.workflows.test.ts src/config/config.workflow-registration.test.ts

实测结果:Test Files 3 passed (3)Tests 53 passed (53)

新增的测试 description gates the tool on an explicit user request 断言的不只是"存在"。它检查规则在描述中的位置早于 **What a workflow is for** 标题的位置,因为位置本身就是要点——放在推介之后的规则是在给推介加限定,而不是在给它定框架。它检查列出的五种形式每一种都是本代码库中真实存在的触发方式。它断言描述中包含 ultracode。它为"提议并询问"的那两句话加了锚点,那是本次改动唯一真实失败模式的防线。它还通过导出常量而非字面量来锚定 agent 数量。

要确认这个测试是真的锚定在这段文字上、而不是空转通过,可以只回滚生产文件后重跑:git stash push -- packages/core/src/tools/workflow/workflow.ts && cd packages/core && npx vitest run src/tools/workflow/workflow.test.ts。实测结果:新测试失败,该文件中其它所有测试仍然通过。

WORKFLOW_TOOL_DESCRIPTION 只有一个消费者——同一文件中的 WorkflowTool 构造函数——并且没有任何 snapshot 捕获它,因此不会影响其它测试套件。

证据(前后对比)

N/A——本次改动是工具描述中面向模型的文字,没有 TUI 表面。

测试环境

OS 状态
🍏 macOS ⚠️ 未测试
🪟 Windows ⚠️ 未测试
🐧 Linux ✅ 已测试

仅单元测试,通过在 packages/core 下执行 npx vitest run。另外对两个改动文件执行了 npx tsc --noEmit -p tsconfig.json(退出码 0)、npx prettier --checknpx eslint

风险与范围

  • 主要风险或取舍:过度阻断。一条"不要调用这个工具"的规则可能被读成"拒绝",那样一来,本来会同意使用 workflow 的用户就再也不会被问到。缓解措施是那段回退路径的文字,它引导模型说明自己会在什么上做扇出,并把决定权交还回去——而且这段文字有专门的断言兜底,因此不会在后续编辑中被悄悄删掉。相反方向的风险,即这条规则太弱以至于无法改变行为,本 PR 没有处理,也无法用单元测试来判定;那是一个关于文字表达的判断。
  • 未验证 / 不在范围内:没有测量模型行为实际发生了多大变化——这是一处描述改动,诚实的说法是这条规则现在被写下来了,而不是已经观察到了遵从。那个本应在授权时刻做出同样披露的审批对话框是另一处改动,本 PR 未触及。启用方式、关键词触发器本身的实现、以及 script 参数的编写契约,均未改变。
  • 破坏性变更 / 迁移说明:无。没有 API、没有 schema、没有设置项、没有运行时行为变化——该工具的输入、输出与各项上限均保持不变。

关联 Issue

无。

qqqys and others added 2 commits August 23, 2026 21:00
… a workflow

The Workflow tool description carried judgement heuristics for orchestrating
well, but no rule about when not to orchestrate at all. Read on its own the
prose is encouragement, and a run that can dispatch up to the per-run agent
cap is a large spend to enter on inference rather than on a request.

Prepend a gate above the existing guidance: do not call the tool unless the
user asked for multi-agent orchestration, with the five forms that count as
asking under this project's own triggers -- the `workflow` keyword, the
user's own words, a skill or slash command, a named saved workflow reached
through `workflow('<name>')` or `scriptPath`, and a resume. Upstream's
`ultracode` marker is deliberately not among them: it does not exist here,
and naming it would enumerate a trigger no user can pull.

The fallback path is the load-bearing half. Without an offer-and-ask route
the model reads a refusal rule as "refuse", and a user who would have said
yes never gets asked, so the text ends by telling it to say what a workflow
would fan out over and let the user decide.

The agent cap is interpolated from `DEFAULT_MAX_AGENTS_PER_RUN` rather than
pasted, matching the rest of the description.

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

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

Copy link
Copy Markdown
Collaborator

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

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Re-run context: new commits landed since the last pass (which reviewed 27f0c0e). The production prose is byte-identical; what changed is test-only tightening plus a hygiene revert — details below and in the review comment.

Problem: unchanged from the prior pass and re-verified against current main: this is a consent gap, not a crash. The Workflow description tells the model how to orchestrate well, but nothing in it says when orchestration was actually asked for — and one run can dispatch up to DEFAULT_MAX_AGENTS_PER_RUN (1000) subagents. The only consent surface today is the permission dialog, which shows nothing but Run a workflow script (4127 chars) (getDefaultPermission() returns 'ask', getDescription() shows a char count). No linked issue records an unprompted fan-out actually firing, and the PR says so — the gap is structural and verifiable in code rather than observed in the wild.

Direction: aligned. The file header declares the description's prose load-bearing, and this extends that pattern to the one policy the description has never carried. CHANGELOG: no direct reference to a gate like this one, but the area is clearly relevant.

Size: 12 production lines (additions only) + 51 test lines, all inside packages/core/src/tools/workflow/. Well under every escalation threshold; WORKFLOW_TOOL_DESCRIPTION has exactly one consumer (the WorkflowTool constructor) and no snapshot captures it, so the blast radius is fully enumerable.

Approach: scope is right, and the two commits since the last pass move it in exactly the right direction: c25fca4 pins the three sentences the round-4 review found mutation-soft (the any-of connective and both prohibition lines), and a59b6cd reverts a stray prettier-format delta in agent-core.ts that a merge resolution had dragged in, so the net diff is back to exactly the two files this change needs. The two deliberate choices still check out against current main: ultracode appears exactly once in this tree, inside a comment in workflow-budget.ts, so excluding it is correct; interpolating the agent cap follows the file's own convention.

Risk: no high-risk path matches, no elevated risk signals.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

Re-run 背景: 上一轮(审于 27f0c0e)之后有新提交。生产文字逐字节未变;变化仅在测试收紧与一处卫生性回退——详见下文与审查评论。

问题: 与上一轮判断一致,并已在当前 main 上重新核实:这是一个"授权缺口",而不是崩溃类 bug。Workflow 工具描述告诉模型如何把编排做好,却从不说什么情况下编排是用户真正要求的——而单次运行最多可以扇出到 DEFAULT_MAX_AGENTS_PER_RUN(1000)个 subagent。今天唯一的授权界面是权限对话框,而它只显示 Run a workflow script (4127 chars)getDefaultPermission() 返回 'ask'getDescription() 只显示字符数)。没有关联 issue 记录过未经请求的扇出实际发生,PR 自己也这么说——这个缺口是结构性的、可以在代码中验证,而非已在现实中观测到。

方向: 对齐。文件头注释声明这段描述中的文字是 load-bearing,本 PR 把同一模式扩展到描述从未承载过的那条策略。CHANGELOG 没有对这个 gate 的直接引用,但该领域明显相关。

规模: 12 行生产代码(纯新增)+ 51 行测试,全部位于 packages/core/src/tools/workflow/。远低于所有升级阈值;WORKFLOW_TOOL_DESCRIPTION 只有一个消费者(WorkflowTool 构造函数),也没有任何 snapshot 捕获它,影响面可以完整数清。

方案: 范围合理,且上一轮之后的两个提交方向完全正确:c25fca4 把第 4 轮审查中发现对变异测试不设防的三句话钉死(any-of 连接句与两条禁止句),a59b6cd 回退了合并解决时带进来的 agent-core.ts prettier 格式差异,净 diff 重新收敛到恰好是本次改动所需的两个文件。两处刻意取舍在当前 main 上依然核实成立:ultracode 在整棵树里只在 workflow-budget.ts 的一条注释中出现过一次,不列入是对的;agent 上限走常量插值符合该文件自己的约定。

风险: 未命中任何高风险路径,无升级风险信号。

进入代码审查 🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

Independent baseline first: for a consent gap in a tool description, the minimal correct change is a short rule at the top of the description — enumerate what counts as an explicit request, add an offer-and-ask fallback so "don't call it" can't read as "refuse", and anchor the whole thing with a test. The PR matches that baseline; this re-run reviews what changed since the last pass at 27f0c0e.

Delta since the last pass — production prose untouched, tests tightened, hygiene restored:

  • The workflow.ts change is byte-identical to what was reviewed last time; the gate still sits above **What a workflow is for**, so it frames the pitch instead of footnoting it.
  • c25fca4 adds 9 test lines pinning exactly the three sentences the round-4 /review found mutation-soft: the It counts as requested when any of these holds: connective (an any→all mutation now fails the suite) and both prohibition sentences (deleting either now fails). The round-4 review ledger closed with zero outstanding findings.
  • a59b6cd reverts a prettier-format-only delta in agent-core.ts that a merge resolution had dragged in — the net diff is exactly two files again.
  • Every prose claim re-verified against current main (the branch merged main three times since the last pass): the workflow keyword trigger and its <system-reminder> steering notice are live in AppContainer; DEFAULT_MAX_AGENTS_PER_RUN is 1000 and interpolated the same way every other cap in this description is; workflow('<name>') / scriptPath / resumeFromRunId are all real parameters; ultracode appears exactly once in the tree, inside a comment. One note: the round-4 review's "no such file or directory" on the two src/config/config.workflow*.test.ts files is a false positive — both exist on current main, so the PR's verification commands are valid.
  • Blast radius unchanged: one consumer of the constant, registration gated on isWorkflowsEnabled(), no snapshot captures the text.

No blockers.

Test evidence (the PR's own CI — the gate never runs PR code)

All checks are settled on the reviewed commit, and everything that ran is green — including Test (ubuntu-latest, Node 22.x), the suite that contains this PR's anchored test. The web-shell E2E Smoke failure on the previously reviewed commit does not reproduce on this one. The macOS/Windows unit legs report skipped (fork-PR pattern, unchanged from the last pass); the cancelled route check is bot orchestration, not PR CI.

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

What this evidence can and cannot prove: CI settles whether the anchored prose is present, correctly placed, and correctly interpolated. The author's mutation reproductions (any→all and sentence-deletion now fail the focused suite) are their claim, and the assertions read consistently with them. CI cannot prove the model complies with the rule, and the PR says so itself. Not verified: model-side compliance — no unit suite can verify it, and the PR explicitly scopes it out. A sandboxed @qwen-code /verify run is already in flight in this workflow run and will post its report to this thread; it can falsify registration and behavioural regressions against the base build, though live-model compliance is beyond any lane, as the PR itself concedes.

Real-scenario (tmux) testing: N/A — unattended CI run, and the change has no TUI surface to drive in the first place.

中文说明

代码审查

先说独立基线:对于工具描述中的授权缺口,最小且正确的改动是在描述顶部加一条简短规则——列举什么算"明确请求",加上"提议并询问"的回退路径使"不要调用"不会被读成"拒绝",并用一个测试把整段锚定。PR 与该基线一致;本次 re-run 审的是自上一轮(27f0c0e)以来的变化。

与上一轮的差异——生产文字未动、测试收紧、卫生恢复:

  • workflow.ts 的改动与上次审到的逐字节一致;门禁仍位于 **What a workflow is for** 之上,为推介定框架而不是当脚注。
  • c25fca4 新增 9 行测试,恰好钉死第 4 轮 /review 发现对变异测试不设防的三句话:It counts as requested when any of these holds: 连接句(any→all 变异现在会使测试失败)与两条禁止句(删掉任一句现在都会失败)。第 4 轮审查台账以零未决发现收尾。
  • a59b6cd 回退了合并解决时带进来的 agent-core.ts 纯 prettier 格式差异——净 diff 重新收敛为恰好两个文件。
  • 文字中的所有声明已在当前 main 上重新核实(上一轮之后该分支三次合并 main):workflow 关键词触发器及其 <system-reminder> 引导提示在 AppContainer 中生效;DEFAULT_MAX_AGENTS_PER_RUN 为 1000,插值方式与该描述中其它所有上限一致;workflow('<name>') / scriptPath / resumeFromRunId 都是真实参数;ultracode 在整棵树中只出现一次,且在一条注释里。附注:第 4 轮审查对两个 src/config/config.workflow*.test.ts 文件报"文件不存在"是误报——两者在当前 main 上都存在,PR 的验证命令有效。
  • 影响面不变:该常量只有一个消费者,注册受 isWorkflowsEnabled() 门控,没有 snapshot 捕获这段文字。

无阻塞项。

测试证据(来自该 PR 自己的 CI——门禁不运行 PR 代码)

受审提交上的所有检查已落定,且运行的全部为绿——包括承载本 PR 锚定测试的 Test (ubuntu-latest, Node 22.x)。上一受审提交上的 web-shell E2E Smoke 失败在本提交上未复现。macOS/Windows 单元测试报告 skipped(fork PR 的既有模式,与上一轮一致);被取消的 route 检查是机器人编排任务,不是 PR CI。

这些证据能证明与不能证明的:CI 能确认锚定的文字在场、位置正确、插值正确。作者的变异复现(any→all 与删句现在会使聚焦套件失败)是其自述,与断言内容一致。CI 无法证明模型会遵守这条规则,PR 自己也这么说。未验证:模型侧的遵从性——任何单元测试套件都无法验证,PR 明确将其划在范围之外。沙箱 @qwen-code /verify 运行已在本次工作流运行中进行,报告会发布到本线程;它能对基线构建证伪注册与行为回归,但真实模型的遵从性超出任何通道所能判定——PR 自己也承认这一点。

真实场景(tmux)测试:N/A——无人值守 CI 运行,且该改动本来就没有可驱动的 TUI 表面。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — the same clean, minimal consent gate as the last pass, now with every review finding closed and the diff back to exactly two files; the only reservation remains the unmeasurable one the PR itself names.

Stepping back: the re-run asks one question — did the new commits help? They did, precisely. The production prose reviewed last time is byte-identical; c25fca4 closes both mutation-testing gaps the round-4 review found (the any-of connective and the two prohibition sentences are now pinned exactly), and a59b6cd removes the stray formatting churn in agent-core.ts so the net diff is the minimal two files again. CI is fully green on the new head, including the suite that carries the anchored test. If I have to maintain this in six months, the anchors tell me what is load-bearing and why — the test comments read like the intent, not noise.

The reservation, unchanged and stated plainly: nobody has observed a model complying (or not) with a gate like this, and no unit test ever will. The claim on offer is "the rule is now stated and anchored", which is exactly what the diff delivers. Post-merge, over-blocking remains the thing to watch — a user who would have said yes to a workflow never gets asked — and the anchored fallback paragraph is the sentence to tune if it shows up. The sandboxed /verify report will land in this thread separately for anyone who wants the A/B signal.

Verdict: approve — CI is settled green on the reviewed commit, so nothing is deferred.

中文说明

Confidence: 4/5 —— 与上一轮相同的干净、最小化的授权门禁;现在所有审查发现都已关闭,diff 重新收敛到恰好两个文件;唯一的保留意见仍是 PR 自己也点名的那个不可测量项。

退一步看:这次 re-run 只问一个问题——新提交有没有让事情变好?有,而且很精准。上次审到的生产文字逐字节未变;c25fca4 关闭了第 4 轮审查发现的两处变异测试缺口(any-of 连接句与两条禁止句现在被精确钉死),a59b6cd 移除了 agent-core.ts 里夹带的格式扰动,净 diff 重新收敛为最小的两个文件。新 head 上 CI 全绿,包括承载锚定测试的套件。如果六个月后由我来维护,这些锚点能告诉我什么是 load-bearing、为什么——测试注释读起来像意图本身,而不是噪音。

保留意见,不变且直说:没有人观察到模型会(或不会)遵守这样的门禁,单元测试也永远无法证明。它给出的主张是"规则现在被写下并被锚定",这正是 diff 所交付的。合并后值得留意的仍是过度阻断——本会同意使用 workflow 的用户再也没被问到——那段有锚点的回退文字就是实践出现问题时要去调整的句子。沙箱 /verify 报告会另行发布在本线程,供需要 A/B 信号的人参考。

结论:批准——受审提交上的 CI 已全部落定为绿,无需推迟。

Qwen Code · qwen3.8-max

Reviewed at c25fca4ad07f62de5ac452b86f304d345eceb044 · re-run with @qwen-code /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.

⚠️ Downgraded from Approve to Comment: CI failing: web-shell E2E Smoke (ubuntu-latest, Node 22.x). Partially reviewed — gaps disclosed.

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

Test Plan (not a blocker): src/config/config.workflows.test.tsno such file or directory; src/config/config.workflow-registration.test.tsno such file or directory.

中文说明

⚠️ 已从批准降级为评论:CI failing: web-shell E2E Smoke (ubuntu-latest, Node 22.x)。 仅完成部分审查,审查缺口已披露。

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

Test Plan(非阻断):src/config/config.workflows.test.tsno such file or directory; src/config/config.workflow-registration.test.tsno such file or directory

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No blocking issues. LGTM! ✅

Test Plan (not a blocker): src/config/config.workflows.test.tsno such file or directory; src/config/config.workflow-registration.test.tsno such file or directory.

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

  • packages/core/src/tools/workflow/workflow.test.ts:165 — [probe] the new gate test anchors the gate's enumeration but not its logical load-bearers (prohibition sentences and the any-of connective) — both drift mutations stay green (probe-ver…
中文说明

无阻断问题。LGTM!✅

Test Plan(非阻断):src/config/config.workflows.test.tsno such file or directory; src/config/config.workflow-registration.test.tsno such file or directory

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

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

qqqys and others added 3 commits August 24, 2026 10:09
…-gate

# Conflicts:
#	packages/core/src/tools/workflow/workflow.test.ts
The merge commit's pre-commit hook ran prettier over every file the merge
staged, not just the ones this branch changes, and reformatted a type union
in agent-core.ts. main's copy does not satisfy the repo's pinned prettier
3.6.1, so the hook produced a real diff in a file this PR has no business
touching. Restore main's bytes to keep the PR scoped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDuRWMhKbqEYZXbbLhn3en

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestions are inline.

Test Plan (not a blocker): src/config/config.workflows.test.tsno such file or directory; src/config/config.workflow-registration.test.tsno such file or directory.

中文说明

已审查——无阻断问题。 建议见行内评论。

Test Plan(非阻断):src/config/config.workflows.test.tsno such file or directory; src/config/config.workflow-registration.test.tsno such file or directory

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

Comment on lines +167 to +168
expect(description).toMatch(/let the user decide/);
expect(description).toMatch(/skips the ask/);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The new gate test anchors the heading, the five trigger bullets, and the offer-and-ask path, but none of its assertions cover the gate's prohibition sentences — "Do not call this tool unless the user has asked for multi-agent orchestration." and "Otherwise do not call it, however well the task would parallelize." in workflow.ts. A probe-verified mutation deletes both sentences while keeping the heading, bullets, and offer paragraph, and every assertion stays green: the description loses the negative instruction that is the point of the gate, silently reintroducing the unrequested multi-agent dispatch this PR was written to prevent. Adding the two anchors below kills the mutant and stays green on the real prose.

Witness (probe in a scratch tree at the reviewed commit):

MUTANT (both prohibition sentences removed): Tests 53 passed (53)
MUTANT + proposed anchors: AssertionError: expected 'Execute a workflow script that orches…' to match /Do not call this tool unless/
ORIGINAL + proposed anchors: Tests 53 passed (53)
Suggested change
expect(description).toMatch(/let the user decide/);
expect(description).toMatch(/skips the ask/);
expect(description).toMatch(/let the user decide/);
expect(description).toMatch(/skips the ask/);
expect(description).toMatch(/Do not call this tool unless/);
expect(description).toMatch(/Otherwise do not call it/);
中文说明

新增的门禁测试锚定了标题、五种触发形式与“提议并询问”路径,但没有任何断言覆盖门禁中的禁止句——即 workflow.ts 中的 “Do not call this tool unless the user has asked for multi-agent orchestration.” 与 “Otherwise do not call it, however well the task would parallelize.”。经探针验证的变异(删除这两句、保留标题、触发列表与提议段落)下所有断言仍然全绿:描述由此失去门禁真正依赖的否定指令,悄悄重新引入本 PR 要防止的未经请求的多智能体扇出。补上下面两个锚点后该变异被杀死,且在真实文字上保持绿色。

见证(受审提交上的隔离树探针):MUTANT(删除两句禁止句):53 通过;MUTANT + 建议锚点:AssertionError(无法匹配 /Do not call this tool unless/);原文 + 建议锚点:53 通过。

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in c25fca4. The existing gate test now pins both prohibition sentences exactly. Independent mutation reproduction confirmed that deleting them previously left the focused suite green and now fails. Verification: workflow.test.ts 53/53; npm run build; npm run typecheck; targeted ESLint and Prettier; pre-commit hook.

Comment on lines +152 to +153
expect(description).toMatch(/contains the word `workflow`/);
expect(description).toMatch(/in their own words/);

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 test anchors each of the five trigger bullets but not the disjunction that makes them sufficient conditions — "It counts as requested when any of these holds:" (workflow.ts:814) has no assertion anywhere; the phrase appears only in the description. A probe-verified mutation flips the quantifier to "only when all of these hold:" and the whole suite stays green. All-five-at-once is effectively unsatisfiable (one message would need the keyword, a paraphrase, a skill/slash command, a named saved workflow, and a resume id together), so the gate would then refuse explicit orchestration requests — over-blocking, which this test's own comment names as the change's one real failure mode, shipped by a green run of the test written to catch exactly that. The anchor below kills the mutant and passes on the real prose.

Witness (probe in a scratch tree at the reviewed commit):

MUTANT ("It counts as requested only when all of these hold:"): Tests 1 passed (1)
MUTANT + proposed anchor: AssertionError: expected 'Execute a workflow script that orches…' to match /counts as requested when any of these…/
ORIGINAL + proposed anchor: green
Suggested change
expect(description).toMatch(/contains the word `workflow`/);
expect(description).toMatch(/in their own words/);
expect(description).toMatch(/contains the word `workflow`/);
expect(description).toMatch(/in their own words/);
expect(description).toMatch(/counts as requested when any of these holds/);
中文说明

测试锚定了五种触发形式中的每一种,但没有锚定使它们成为充分条件的析取句——“It counts as requested when any of these holds:”(workflow.ts:814)没有任何断言覆盖,该短语只出现在描述中。经探针验证的变异把量词翻转为 “only when all of these hold:” 后整个套件仍然全绿。五种形式同时满足实际上不可能(一条消息需要同时包含关键词、同义转述、skill/斜杠命令、指名的已保存 workflow 和 resume id),门禁因此会拒绝明确的编排请求——这正是本测试自己的注释点名的唯一真实失败模式(过度阻断),且会在这条本应捕获它的测试全绿的情况下被放行。补上下面这个锚点后该变异被杀死,在真实文字上保持绿色。

见证(受审提交上的隔离树探针):MUTANT(翻转为 all):1 通过;MUTANT + 建议锚点:AssertionError(无法匹配 /counts as requested when any of these…/);原文 + 建议锚点:绿色。

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in c25fca4. The gate test now pins the any-of connective exactly. Independent mutation reproduction confirmed that changing any to all previously left the focused suite green and now fails. Verification: workflow.test.ts 53/53; npm run build; npm run typecheck; targeted ESLint and Prettier; pre-commit hook.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

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

Test Plan (not a blocker): src/config/config.workflows.test.tsno such file or directory; src/config/config.workflow-registration.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/config/config.workflows.test.tsno such file or directory; src/config/config.workflow-registration.test.tsno such file or directory

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

@wenshao

wenshao commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 104 passed · 0 failed · 104 total

Flakiness gate: ✅ 1 changed test file(s) x 5 identical rounds, no divergence

中文 — 判定:✅ 通过 · 可合入(agent 判定)

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

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

抖动门:✅ 1 changed test file(s) x 5 identical rounds, no divergence

Verification report

PR #9806 Deep Verification — feat(core): require an explicit user opt-in before the model launches a workflow

Verdict: merge-ready — 104/104 scripted assertions passed (0 fail). Verified head: c25fca4ad07f62de5ac452b86f304d345eceb044 (git rev-parse HEAD^2), merge-ref 254b915716 over base tip 717ad101e2.

中文摘要

结论:merge-ready。104/104 条脚本化断言全部通过,无失败。

  • A/B 结论:在基线构建(HEAD^1)中,工具描述里没有准入条款(indexOf('**Only on an explicit request**') === -1);在 PR 构建中该条款存在(索引 74)、位于 **What a workflow is for**(索引 1223)之前、且是描述中的第一个小节标题;"head 描述减去准入条款块 ≡ base 描述"逐字节成立(Δ=1149B,即唯一对模型可见的变化)。翻转成立,见 01-ab-gate-flip-base-vs-head.png
  • 测试非空:对新增测试做了变异矩阵——整体回退生产代码块、移动条款位置、删除五种触发形式之一、删除 offer-and-ask 逃生段,四个变异全部以预期的断言杀死新测试;正向对照(改动既有测试锚点)证明运行器有效。唯一存活变异(把插值常量粘贴为字面量 1000)是内容等价的预期存活。见 02-mutation-matrix.png
  • 事实核查:描述中的事实性陈述全部核验通过——workflow 关键词触发在交互式 TUI 中确有接线(AppContainer.tsx,含前置 system reminder、遥测、可关闭设置);ultracode 在 PR 前的源码中恰好出现 1 次且在注释中;WORKFLOW_TOOL_DESCRIPTION 恰有一个消费者(同文件构造函数);无快照捕获该描述;1000 的代理上限真实强制。见 03-prose-claims-census.png
  • findings:无阻塞项。两条低优先级观察:(1) "system reminder 确认"一句只在交互式 TUI 成立,非交互/ACP/频道表面不会注入该 reminder(对门控正确性无影响,属措辞精度问题,且接线为本 PR 之前的既有代码);(2) PR 正文写的 Tests 53 passed (53) 实际为 63(53 仅是 workflow.test.ts 单文件的数量)。
  • 未覆盖范围:模型行为是否真的因此减少未经请求的 fan-out(作者已声明超出单测范围);逐提交归属(浅克隆 depth 2,仅验证聚合 diff);仓库全量测试套件。

Central claim and A/B

Central claim: the Workflow tool's model-visible description gains an opt-in gate — do not call the tool unless the user asked for multi-agent orchestration — positioned above the "what a workflow is for" pitch so it frames rather than footnotes it, enumerating five request forms and closing with an offer-and-ask escape hatch.

This is a prose-only change (+12 lines in workflow.ts, +51 test lines), so the A/B oracle is the rendered description itself — tool.description and the wire form tool.schema.description — read from the compiled dist/ of both builds via a mock-free harness (harness-ab.mjs, constructs the real WorkflowTool).

cell build oracle result
head merge-ref HEAD (packages/core built at HEAD by CI) gate index in rendered description 74 (present, first heading)
base HEAD^1 worktree, packages/core rebuilt there same −1 (absent)
both pitch index **What a workflow is for** position head 1223 / base 74
both byte equivalence head − gate block ≡ base exact (Δ = 1149 B = the gate block only)

Witness: 01-ab-gate-flip-base-vs-head.png — 18/18 head assertions (gate present, first heading, precedes pitch, five forms enumerated, no ultracode, prohibition + escape-hatch sentences, cap interpolated as 1000, wire form identical) and 5/5 base assertions (expected absence encoded as pass).

Control purity: the emitted workflow.js files from the two builds differ by exactly the 12 added gate lines (logs/emitted-workflow-js.diff) — the base-side tsc run reported unrelated type errors in nested-tree modules (telemetry/ajv type resolution artifacts), but the module under test emitted byte-identically to head-minus-hunk, which is the strongest possible evidence the control differs by nothing else. packages/core has zero workspace dependencies and the PR leaves package.json/package-lock.json untouched, so the shared root node_modules is a clean control; the core-local node_modules (ajv v8 etc.) was symlinked into the base tree and readlink -f confirms it resolves into the head tree's identical dependency set — no @qwen-code/* links are crossed because core has none.

Corrections

  • PR body, Reviewer Test Plan: "Observed: Test Files 3 passed (3), Tests 53 passed (53)" — re-running the exact same command gives 3 files passed but 63 tests (workflow.test.ts 53 + config.workflows.test.ts 4 + config.workflow-registration.test.ts 6). The 53 figure is the single-file count. The gate itself is green; this is a description correction, not a code issue.

Findings (none blocking)

  1. Low — the reminder clause is true only on the interactive TUI surface. The gate's first bullet says the workflow keyword is confirmed by "a system reminder". The mechanism exists and is wired — AppContainer.tsx:2655-2678 detects the keyword on the user's original text and prepends <system-reminder>…</system-reminder> to the submitted turn, gated on isWorkflowsEnabled() and the disableWorkflowKeywordTrigger setting, with telemetry — but that is the only production call site of both detectWorkflowKeyword and buildWorkflowSteeringNotice (scripted census: 1 each, both in AppContainer.tsx). Headless (nonInteractiveCli.ts), ACP sessions, and channel surfaces never inject it, so on those surfaces the clause overstates. Practical impact nil — the keyword form of the gate is still satisfied model-side because the model reads the user message — and the wiring is pre-existing, so this is a prose-precision note: "in the interactive TUI, a system reminder confirms it" would be exact.
  2. Info — the prepend wiring has no direct test. Detector semantics (workflow-keyword.test.ts) and the footer indicator (Footer.test.tsx:328) are tested; the handleSubmit prepend itself is not pinned by any test. Pre-existing gap, now referenced by model-visible prose.
  3. Info — expected mutation survivor. Pasting 1000 instead of interpolating ${DEFAULT_MAX_AGENTS_PER_RUN} survives the new test (content-equivalent while the constant is 1000). The pin becomes live the moment the constant moves. Completeness note, not a defect.

Not covered

  • Model-behavior shift: whether the gate actually reduces unrequested fan-outs is not measurable by unit tests; the PR honestly says so. Not attempted.
  • Per-commit attribution: metadata lists 7 commits; the checkout is depth-2 (merge, base tip, PR head only), so only the aggregate HEAD^1..HEAD diff was exercised. The aggregate diff is exactly the two expected files (+63 lines).
  • Repo-wide suites: only the three suites named in the PR's test plan were run (63/63), per targeted-gate policy.
  • Live TUI E2E of the reminder flow: wiring verified by census plus its existing unit tests, not by driving a real session.
  • Snapshot baseRefOid drift: the metadata snapshot names base 5cff52c6… (fix(goal): count catalog previews in the unit their budget is written in #9835), newer than the merge-ref base 717ad101e2. Verified directly: git diff HEAD^1 5cff52c6 touches 37 files but zero under packages/core/src/tools/workflow/, so the PR's surface merges conflict-free against the newer main.

Methodology

Environment: CI container (node:22-bookworm, Node v22.23.2), working tree at merge-ref 254b915716 with npm ci + npm run build already run at HEAD. Base control: git worktree add tmp/base-tree HEAD^1, packages/core rebuilt there via node scripts/build_package.js (base tree removed after capture). Harnesses (harness-ab.mjs, harness-claims.mjs, run-mutations.py, print-matrix.mjs) import the compiled dist/ directly by absolute path — real constructors, no stubs; every assertion is a scripted comparison that can fail, and expected base-arm absences are encoded as passes. Mutation matrix: each mutant edits workflow.ts in place, runs npx vitest run src/tools/workflow/workflow.test.ts, records the summary and failing assertion text, then restores via git checkout -- (tree verified clean afterwards). Raw logs: logs/ab-run.log, logs/claims-run.log, logs/gate-baseline.log, logs/mutant-M*.log, logs/base-build.log, logs/emitted-workflow-js.diff. Self-audit note: an early call-site census using grep --include='*.ts' missed the .tsx wiring file and briefly suggested the keyword trigger was unwired; the language-agnostic census harness corrected this, and the corrected state (wired, one call site) is what the report carries.

Flakiness gate log

rounds=5 files=1 skipped=0
file packages/core/src/tools/workflow/workflow.test.ts: (cd packages/core) npx --no-install vitest run ./src/tools/workflow/workflow.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/core/src/tools/workflow/workflow.test.ts: PPPPP

verdict: pass
summary: 1 changed test file(s) x 5 identical rounds, no divergence

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/core/src/tools/workflow/workflow.test.ts: P (exit 0)
round 2 · packages/core/src/tools/workflow/workflow.test.ts: P (exit 0)
round 3 · packages/core/src/tools/workflow/workflow.test.ts: P (exit 0)
round 4 · packages/core/src/tools/workflow/workflow.test.ts: P (exit 0)
round 5 · packages/core/src/tools/workflow/workflow.test.ts: P (exit 0)

Evidence images

01-ab-gate-flip-base-vs-head

02-mutation-matrix

03-prose-claims-census

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

Qwen Code · sandboxed verification

@wenshao

wenshao commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Local verification report for PR #9806

Verified the workflow opt-in gate change in a clean local worktree checked out from qqqys/feat/workflow-optin-gate.

Environment

  • Repository: QwenLM/qwen-code
  • Branch under test: pr-9806-verify (PR head feat/workflow-optin-gate)
  • Node.js: v22.22.2
  • Command: npm run build && npm run typecheck && cd packages/core && npx vitest run src/tools/workflow/workflow.test.ts

What was verified

  1. Full project build succeeds (npm run build) — all packages including @qwen-code/qwen-code-core compiled without errors.
  2. Typecheck is clean (npm run typecheck) — no TypeScript errors across workspaces.
  3. Workflow tool tests pass — all 53 tests in packages/core/src/tools/workflow/workflow.test.ts pass, including the new description gates the tool on an explicit user request assertion.
  4. Gate content and ordering — a standalone verification script instantiates WorkflowTool, extracts the description, and confirms:
    • The **Only on an explicit request** section exists.
    • It appears before **What a workflow is for**.
    • The five qwen triggers are enumerated (workflow keyword, own words, skill/slash command, saved workflow name, resume).
    • ultracode is not mentioned (it is not a qwen trigger).
    • The agent cap is interpolated from DEFAULT_MAX_AGENTS_PER_RUN (currently 1000).
    • The offer-and-ask escape hatch is present.

Results

Build: ✅ success
Typecheck: ✅ clean
Workflow tests: ✅ 53/53 passed
Gate verification: ✅ all checks passed

Screenshots

Terminal output from the test run:
test-output.png

Standalone gate verification:
verify-output.png

Conclusion

The PR applies cleanly, builds cleanly, type-checks cleanly, and the new opt-in gate is present in the expected location with the expected content. From a local build/test perspective this looks ready to merge.


🌐 中文版(点击展开)

PR #9806 本地验证报告

在从 qqqys/feat/workflow-optin-gate 检出的干净本地工作区中验证了 workflow 显式请求门槛的改动。

环境

  • 仓库:QwenLM/qwen-code
  • 被测分支:pr-9806-verify(PR head feat/workflow-optin-gate
  • Node.js:v22.22.2
  • 执行命令:npm run build && npm run typecheck && cd packages/core && npx vitest run src/tools/workflow/workflow.test.ts

验证内容

  1. 全项目构建通过npm run build)—— 包括 @qwen-code/qwen-code-core 在内的所有包均编译成功。
  2. 类型检查通过npm run typecheck)—— 所有工作区无 TypeScript 错误。
  3. Workflow 工具测试通过 —— packages/core/src/tools/workflow/workflow.test.ts 中的 53 个测试全部通过,包括新增的 description gates the tool on an explicit user request 断言。
  4. 门槛内容与顺序 —— 独立验证脚本实例化 WorkflowTool 并提取描述,确认:
    • **Only on an explicit request** 段落存在;
    • 它出现在 **What a workflow is for** 之前
    • 枚举了五种 qwen 触发方式(workflow 关键字、用户自己的话、skill/slash 命令、已保存 workflow 名称、resume);
    • 未提及 ultracode(这不是 qwen 的触发词);
    • 智能体上限从 DEFAULT_MAX_AGENTS_PER_RUN 插值(当前为 1000);
    • 提供了“告知用户并让其决定”的逃生通道。

结果

构建: ✅ 成功
类型检查: ✅ 通过
Workflow 测试: ✅ 53/53 通过
门槛验证: ✅ 全部通过

截图

测试运行的终端输出:
test-output.png

独立的门槛验证:
verify-output.png

结论

该 PR 应用干净、构建干净、类型检查干净,且新的显式请求门槛出现在预期位置并包含预期内容。从本地构建/测试的角度来看,已可合并。

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

LGTM, looks ready to ship. ✅

@wenshao
wenshao added this pull request to the merge queue Aug 24, 2026
Merged via the queue into QwenLM:main with commit 22bb5e8 Aug 24, 2026
130 of 131 checks passed
yiliang114 added a commit to yiliang114/qwen-code that referenced this pull request Aug 24, 2026
…QwenLM#9827)

Merging origin/main brought ToolNames.REQUEST_SHUTDOWN (QwenLM#9806) but no TOOL_NAME_ALIASES entry, which the resolveToolName exhaustiveness test added on this branch pins. Map request_shutdown / RequestShutdown so permission rules can address the tool.
digital-codes pushed a commit to digital-codes/qwen-code that referenced this pull request Aug 25, 2026
…he model (QwenLM#9829)

* fix(core): make permissions.allow restrict the tool schemas sent to the model (QwenLM#9827)

permissions.allow only auto-approved calls; it never gated tool
registration, so the outgoing tools array kept every built-in schema
even when an allowlist was configured — contradicting the settings
docs migration table ("unlisted tools are disabled at registry
level") and breaking backends like llama.cpp that compile all tool
schemas into a single grammar.

- Activate a registry-level allowlist when settings.permissions.allow
  has at least one valid rule: built-in tools not covered by any allow
  rule are no longer registered (absent from /tools and the API
  request). MCP tools and the structured_output contract stay exempt;
  session-granted rules ("always allow", skill allowedTools) extend
  membership but never activate the allowlist mid-session.
  --allowed-tools / SDK allowedTools / legacy tools.allowed keep their
  pure auto-approval semantics.
- Complete the rule alias map so the display names shown by /tools
  (SendMessage, UpdateGoal, ...) match in allow/deny rules.

* fix(core): honor permissions.allow in the list_directory opt-in gate (QwenLM#9827)

isLsToolEnabled() only read tools.listDirectory.enabled and the coreTools
allowlist, so an explicitly allowlisted list_directory passed
PermissionManager.isToolEnabled() but was never registered — absent from
/tools and the model request, with calls failing TOOL_NOT_REGISTERED. This
broke the documented tools.core -> permissions.allow migration equivalence
for exactly this tool. Consult getRegistryAllowList() with the same
coverage semantics the registry gate uses (toolMatchesRuleToolName, so
Read / ListFiles / specifier forms all count).

* fix(core): keep plan-mode lifecycle tools registered under the allowlist (QwenLM#9827)

The permissions.allow registry gate covered exit_plan_mode /
enter_plan_mode / ask_user_question, so the exact reporter configuration
unregistered them. The plan-mode system reminder still instructs the model
to present its plan by calling exit_plan_mode, whose schema is then never
sent, so the sanctioned plan flow cannot complete. Exempt the three
plan-mode lifecycle tools alongside structured_output (same synthetic-
system-tool class the CORE_TOOLS docstring names; deny rules still apply).

* docs(sdk): correct allowedTools registry-allowlist contract (QwenLM#9827)

The JSDoc added for QueryOptions.allowedTools (and the coreTools block)
claimed the SDK allowedTools param activates the registry allowlist and
hides unlisted built-in schemas. It does not: ProcessTransport maps it to
the CLI --allowed-tools flag, and this PR's CLI wiring builds
registryAllowList only from settings.permissions.allow. Reword both JSDoc
blocks and the two hand-maintained SDK doc pages (sdk-typescript.md,
sdk-typescript/README.md) to the shipped contract: allowedTools stays a
pure auto-approval grant; only permissions.allow in settings.json
(requires restart) activates the registry allowlist.

* docs(settings): note plan-mode lifecycle exemption in the allowlist (QwenLM#9827)

The permissions.allow registry-allowlist exemption list named only MCP
tools and the structured_output contract. Add the plan-mode lifecycle
tools (exit_plan_mode / enter_plan_mode / ask_user_question) exempted in
b8ba258 so the documented exemption set matches the gate.

* fix(core): exempt the computer_use__* family from the registry allowlist (QwenLM#9827)

* fix(core): gate command-discovered tools through the registry allowlist (QwenLM#9827)

* fix(core): make registry-allowlist membership monotonic within the session (QwenLM#9827)

* fix(core): narrow the skill allowedTools grant contract to restart-scoped registration (QwenLM#9827)

* fix(core): count ask rules toward registry-allowlist membership (QwenLM#9827)

A tool covered only by a permissions.ask rule was silently deregistered
whenever the permissions.allow registry allowlist was active: allow
["ReadFile"] + ask ["Shell"] hid the whole shell family from the model,
so the documented "always require user confirmation" silently became
"tool unavailable" and the ask rule could never fire.

Ask rules express "this tool must stay usable, with confirmation", so
they now count toward registry membership (frozen at startup for the
same restart-scoped monotonicity as allow rules).

* docs(settings): note that ask rules keep tools registered under allowlist (QwenLM#9827)

* test(cli): pin registry-allowlist strip in bare mode (QwenLM#9827)

The wiring tests only covered the safe-mode half of
registryAllowList: bareMode || safeMode ? undefined : ... — a mutant
dropping the bareMode guard survived the suite and would activate the
allowlist from settings while bare mode strips those same rules from
the merged allow set, leaving the bare registry's minimal toolset
ungated. Mirror the safe-mode test for --bare.

* fix(core): attribute registry-allowlist misses to permissions.allow (QwenLM#9827)

An allowlist-miss rejection surfaced as "Qwen Code requires permission
to use X, but that permission was declined" citing a deny rule that
does not exist (findMatchingDenyRule finds nothing) and never
mentioning permissions.allow. When no deny rule matched and the
registry allowlist is active, emit a distinct message pointing at the
real config knob.

* test(core): pin resolveToolName coverage of every ToolNames entry (QwenLM#9827)

TOOL_NAME_ALIASES hand-maintains the canonical/display-name mappings
that tool-names.ts declares; nothing enforced the sync, so a tool added
to tool-names.ts without an alias entry would compile, pass every test,
and silently never match a permission rule — the exact QwenLM#9827 bug class,
now with higher stakes since a missed entry also breaks allowlist
coverage. Walk every ToolNames/ToolDisplayNames pair and assert it
round-trips through resolveToolName.

* fix(core): expose isPermissionsAllowListActive on scoped PM shims (QwenLM#9827)

* fix(core): honour ask-only list_directory coverage in the opt-in gate (QwenLM#9827)

* docs: align registry-allowlist contract wording across docs and JSDoc (QwenLM#9827)

* docs: scope settings.md removal and whole-tool-deny claims precisely (QwenLM#9827)

* fix(core): count merged allow coverage in the list_directory opt-in gate (QwenLM#9827)

isLsToolEnabled() scanned only the settings-sourced getRegistryAllowList() for allow coverage while PermissionManager.isToolEnabled() counts the merged allow set (settings + --allowed-tools + SDK allowedTools + legacy tools.allowed). Under an active allowlist, list_directory covered only by a merged rule passed isToolEnabled but was never offered to registerLazy — it vanished from /tools and the model request while calls failed TOOL_NOT_REGISTERED. Count the merged allow set for coverage (activation still requires a valid settings rule) and filter empty/whitespace-only entries from activation exactly like PermissionManager.initialize's parseRules does, so a degenerate [""] entry cannot activate the gate here while the permission system reports the allowlist inactive.

* test(core): pin activation source and merged-allow coverage of the list_directory gate (QwenLM#9827)

Every existing isLsToolEnabled test fed the identical array as both allow and registryAllowList, so the settings-only vs merged-allow distinction was unpinned and the R4-1 divergence shipped uncovered. Add three cases shaped like the CLI wiring: coverage by a merged (non-settings) allow rule under an active allowlist registers the tool; merged-only coverage with no settings rule does not activate the allowlist; an empty settings entry ([""]) does not activate it either.

* fix(core): attribute scheduler denials to permissions.allow only for uncovered tools (QwenLM#9827)

The allowlist-miss message fired for any disabled tool with no matching deny rule while the allowlist is active — including tools rejected by the legacy coreTools gate that ARE covered by an allow rule, where 'not covered by any permissions.allow rule' is wrong and the remediation a no-op. Expose isCoveredByAllowOrAskRule on PermissionManager and take the allowlist branch only when the tool is genuinely uncovered; covered tools fall back to the generic declined message. The optional call keeps scoped PermissionManager shims (installed via 'as unknown as PermissionManager') from throwing until they grow the delegation.

* test(core): pin the covered-tool fallback for scheduler denial messages (QwenLM#9827)

Add a scheduler-level case where the allowlist is active, no deny rule matches, and the disabled tool IS covered by an allow rule (the legacy coreTools gate shape): the message must be the generic declined one, not the permissions.allow attribution. Also make the existing allowlist-miss stub explicit about coverage.

* fix(core): register request_shutdown in the permission rule alias map (QwenLM#9827)

Merging origin/main brought ToolNames.REQUEST_SHUTDOWN (QwenLM#9806) but no TOOL_NAME_ALIASES entry, which the resolveToolName exhaustiveness test added on this branch pins. Map request_shutdown / RequestShutdown so permission rules can address the tool.

* fix(core): guard the list_directory allowlist gate against non-string rules (QwenLM#9827)

isLsToolEnabled()'s activation check and coverage scan called raw.trim() / parseRule(raw) directly while PermissionManager.initialize computes the same thing through parseRules, whose r && r.trim() filter skips falsy entries. Settings load performs no element-type validation (the schema declares only type: array), so a stray null in settings.permissions.allow/ask — or in the legacy tools.allowed key riding the merged coverage set — threw TypeError during createToolRegistry and crashed startup while PermissionManager.initialize tolerated the same settings file. Mirror the parseRules guard with a typeof check in both the activation check and the coverage predicate, and pin both arms (tolerated entries still activate/cover, a [null]-only list keeps the gate closed).

* fix(core): exempt task_stop from the permissions.allow registry gate (QwenLM#9827)

task_stop satisfies the PR's own two written exemption criteria but was missing from the set: it is shouldDefer=true (task-stop.ts), the exact deferred-schema property the computer_use__* exemption cites, and it is advertised to the model by a registered tool's copy — run_shell_command's schema says to use task_stop to stop a background command (and not to use broad process-name kills), and the background-promotion result instructs task_stop({ task_id }) verbatim. Under the reporter configuration the suite pins, run_shell_command stays listed while task_stop was gated out, so the sanctioned stop flow failed. Add the exemption and pin it next to the plan-mode exemption tests, including that a whole-tool deny rule still wins via the existing evaluate pass.

* fix(core): keep shim denials on the pre-QwenLM#9827 message when coverage is unknown (QwenLM#9827)

The optional isCoveredByAllowOrAskRule call's : true fallback routed shim-mediated rejections of COVERED tools into the new allowlist-attribution message, contradicting the comment above it ('they keep the pre-QwenLM#9827 message meanwhile'). Both production shims (memory-scoped-agent-config.ts, skillReviewAgentPlanner.ts) Pick a partial interface without isCoveredByAllowOrAskRule, so for them the ternary always took the allowlist arm — telling the user a covered tool 'is not covered by any permissions.allow rule' when a different gate (e.g. the legacy coreTools allowlist) rejected it. Flip the fallback to false so unknown coverage stays on the pre-QwenLM#9827 declined message, and update the shim test to pin that message instead of the allowlist one.

* test(core): pin that ask-only rules never activate the allowlist (QwenLM#9827)

The suite pins ask rules counting toward allowlist membership, but nothing pins the complementary activation boundary: no test constructed a PermissionManager with only permissionsAsk (no permissionsAllow) and asserted the allowlist stays inactive. Current behavior is correct; this guards against a future edit folding ask rules into activation, which would turn an ask-only posture (permissions.ask: ["Shell"], no allow rules — a natural 'always confirm shell' config) into an active allowlist that deregisters every unlisted built-in. The nearest existing test ('no allow rules → allowlist inactive') uses no rules at all and would still pass.

* fix(core): exempt tool_search from the permissions.allow registry gate (QwenLM#9827)

Under a narrow active allowlist, tool_search itself was gated out of the
registry. Without ToolSearch, client.ts resolveDeferredToolsForReminder
eagerly force-reveals every registered deferred tool (all mcp__* and the
deferred computer_use__* family) into the eager model request, and
preloadDeferredToolsWithinBudget early-returns — inverting the
schema-shrink goal into maximal schema bloat for exactly the deferred
families the other exemptions preserve for ToolSearch discoverability.
Pre-QwenLM#9827 tool_search always bypassed the legacy coreTools gate as a
non-core tool.

* test(core): pin the deny-rule arm's precedence in the scheduler permission message (QwenLM#9827)

The three-way message branch in CoreToolScheduler covers the allowlist-miss
arm and the generic fallback arm, but every findMatchingDenyRule mock
returned undefined, so the deny-rule arm — whose position FIRST in the
if/else-if chain is what makes a real denial cite the matching rule instead
of the allowlist attribution — had no scheduler-level coverage. Add two
tests where findMatchingDenyRule returns a matching rule: one with the
allowlist arm armed (active allowlist + uncovered tool) pinning the
if/else-if ordering, one without an active allowlist pinning the deny arm
over the generic declined fallback. Mutation-checked: disabling the deny
arm fails both tests.

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

* test(core): pin deny/ask sibling semantics at the discovery gate (QwenLM#9827)

The discovery-gate test built its PermissionManager with EMPTY ask/deny
lists, so the gate's two documented sibling semantics were unpinned:
settings.md says a whole-tool deny rule removes a discovered tool from
the registry even under an active allowlist, and an ask rule keeps a
discovered tool registered ("always require confirmation" must never
silently become "tool unavailable"). Add two discovery-gate tests with
deny-covered and ask-covered PermissionManager configurations: the denied
tool is also allow-covered so only the deny branch of isToolEnabled can
reject it, and the ask test carries an uncovered control tool proving the
gate is active in the same run. Mutation-checked: ignoring deny decisions
fails the deny test only; dropping ask coverage from
isCoveredByAllowOrAskRule fails the ask test only.

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

* docs: match the allowlist activation wording to the real predicate (QwenLM#9827)

Four surfaces said the permissions.allow registry allowlist activates
"when at least one allow rule is configured", but
PermissionManager.initialize computes activation as at least one VALID
rule from settings.permissions.allow only (getRegistryAllowList): a
malformed entry never activates it, and auto-approval-only sources such
as the --allowed-tools CLI flag / the SDK allowedTools parameter never
do either. Reword settings.md, the SDK docs, the sdk-typescript README
and the coreTools JSDoc to the exact predicate, and complete their
exemption lists with task_stop and tool_search, which isToolEnabled
exempts but the docs did not name. Docs-only.

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

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.22.2.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants