Skip to content

fix(core): validate skill commands against the live provider to survive late attach - #9824

Merged
yiliang114 merged 7 commits into
QwenLM:mainfrom
yiliang114:fix/9821-skill-command-late-provider
Aug 25, 2026
Merged

fix(core): validate skill commands against the live provider to survive late attach#9824
yiliang114 merged 7 commits into
QwenLM:mainfrom
yiliang114:fix/9821-skill-command-late-provider

Conversation

@yiliang114

Copy link
Copy Markdown
Collaborator

What this PR does

SkillTool.validateToolParams now consults the model-invocable-commands provider live instead of relying on the command set cached during construction. The live read applies the same file-based-skill name shadowing as collectAvailableSkillEntries — commands colliding with an active or pending path-activation skill are dropped — so the "gated by paths:" validation branch stays intact. The construction-time cache is kept as the fallback for when no provider is registered (SDK mode) or the provider throws. Adds regression tests and documents late-attach support on setModelInvocableCommandsProvider.

Why it's needed

Fixes the intermittent Skill "..." not found failures for user-level native slash commands reported in #9821. In interactive mode the modelInvocableCommandsProvider is only registered after CommandService.create(...) resolves (in the slashCommandProcessor effect), but Config.initialize()toolRegistry.warmAll() constructs SkillTool before that, so the constructor's refreshSkills() reads a still-null provider and caches an empty command set. Nothing re-notifies the tool when the provider is attached, so validation keeps rejecting commands for the whole session unless an unrelated SkillManager change event happens to re-run refreshSkills() — the source of the reported nondeterminism. Meanwhile the per-turn skills drain (drainSkillAndCommandReminders) collects entries fresh and announces the commands as available, so the model invokes a Skill tool that then rejects them. The provider is synchronous, so reading it live on each validation is cheap and makes the check robust to any late attach, not just the interactive CLI's.

Reviewer Test Plan

How to verify

Red-before-fix repro: with only the test changes applied (i.e. git stash of the skill.ts/config.ts changes), run cd packages/core && npx vitest run src/tools/skill.test.ts -t "issue #9821". The two late-attach tests fail with the exact reported signature:

AssertionError: expected 'Skill "late-command" not found. Available skills: code-review, testing' to be null

With the fix applied the same command passes all 4 regression tests. The tests construct SkillTool while the provider returns null (mirroring warmAll() racing CommandService.create), drain the constructor's refreshSkills(), then register the provider without firing any SkillManager change event. They cover: late attach passing validation, late-registered commands appearing in the not-found listing, path-gated skills remaining gated under a late provider, and unchanged behavior when no provider is ever registered.

Full targeted suites: cd packages/core && npx vitest run src/tools/skill.test.ts src/tools/skill-utils.test.ts → 94 passed. npm run typecheck --workspace @qwen-code/qwen-code-core (tsc --noEmit) passes; eslint and prettier clean on the changed files.

Evidence (Before & After)

N/A — non-user-visible validation-path change; evidence is the red/green test output in "How to verify".

Tested on

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

Environment (optional)

Unit tests only (vitest), Node v24.19.0 on Linux.

Risk & Scope

  • Main risk or tradeoff: validateToolParams now calls the provider on every validation. The provider is a synchronous getter over CommandService's command list, so the cost is negligible; if it throws, validation falls back to the cached snapshot instead of failing.
  • Not validated / out of scope: triage option 1 (CLI-side re-notification via skillManager.notifyConfigChanged()) is deliberately not implemented — validation no longer depends on cache freshness, so it is not required for correctness. Not validated end-to-end in a live interactive TUI session.
  • Breaking changes / migration notes: none.

Linked Issues

Fixes #9821

中文说明

本 PR 做了什么

SkillTool.validateToolParams 现在实时读取 model-invocable commands provider,而不再依赖构造时缓存的命令集。实时读取沿用了 collectAvailableSkillEntries 的同名遮蔽规则——与已激活或待路径激活的文件技能同名的命令会被剔除——因此 "gated by paths:" 校验分支保持不变。构造期缓存仍作为兜底:未注册 provider(SDK 模式)或 provider 抛错时使用。同时补充回归测试,并在 setModelInvocableCommandsProvider 上注明支持晚挂载。

为什么需要

修复 #9821 报告的用户级原生 slash 命令间歇性 Skill "..." not found。交互模式下 modelInvocableCommandsProvider 要等 CommandService.create(...) resolve 后(slashCommandProcessor 的 effect 中)才注册,而 Config.initialize()toolRegistry.warmAll() 在此之前就已构造 SkillTool,构造函数里的 refreshSkills() 读到 null provider,缓存了空命令集。provider 挂载后没有任何机制重新通知该工具,除非恰好有无关的 SkillManager 变更事件触发 refreshSkills(),否则整个会话校验都会拒绝命令——这正是报告中非确定性的来源。与此同时每轮 skills drain(drainSkillAndCommandReminders)实时收集并宣告这些命令可用,模型据此调用 Skill 工具,却被校验拒绝。provider 是同步的,每次校验实时读取开销可忽略,并且对任何晚挂载路径都健壮,而不只是交互 CLI。

审阅者测试计划

如何验证

修复前的红测试:只应用测试改动(即 skill.ts/config.ts 的改动被 git stash 时),运行 cd packages/core && npx vitest run src/tools/skill.test.ts -t "issue #9821",两个晚挂载用例会以与报告完全一致的签名失败:

AssertionError: expected 'Skill "late-command" not found. Available skills: code-review, testing' to be null

应用修复后同一命令 4 个回归用例全部通过。用例构造 SkillTool 时 provider 返回 null(模拟 warmAll()CommandService.create 的竞态),等构造期 refreshSkills() 落定后再注册 provider,且不触发任何 SkillManager 变更事件。覆盖:晚挂载后校验通过、晚注册命令出现在 not-found 列表、晚挂载 provider 下路径门控技能仍被门控、以及从未注册 provider 时行为不变。

完整目标套件:cd packages/core && npx vitest run src/tools/skill.test.ts src/tools/skill-utils.test.ts → 94 通过。npm run typecheck --workspace @qwen-code/qwen-code-coretsc --noEmit)通过;改动文件 eslint 与 prettier 干净。

前后证据

N/A——非用户可见的校验路径改动;证据见"如何验证"中的红/绿测试输出。

测试环境

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

运行环境(可选)

仅单元测试(vitest),Linux,Node v24.19.0。

风险与范围

  • 主要风险/权衡:validateToolParams 现在每次校验都会调用 provider。provider 只是对 CommandService 命令列表的同步取值,开销可忽略;若其抛错,校验回退到缓存快照而不是失败。
  • 未验证/超出范围:triage 方案一(在 CLI 侧经 skillManager.notifyConfigChanged() 重新通知)刻意未实现——校验已不再依赖缓存新鲜度,正确性上不需要。未在真实交互 TUI 会话中端到端验证。
  • 破坏性变更/迁移说明:无。

关联 Issue

Fixes #9821

…ve late attach

SkillTool caches the model-invocable command set during construction,
but in interactive mode the provider is only registered after
CommandService initialisation resolves — after Config.initialize() has
already warmed the tool registry. The cache then stays empty until an
unrelated SkillManager change event re-runs refreshSkills(), so
validateToolParams intermittently rejects commands announced by the
per-turn skills drain (issue QwenLM#9821).

Consult the synchronous provider live in validateToolParams instead,
applying the same file-based-skill name shadowing as
collectAvailableSkillEntries so path-gated skills stay gated. The
execute path already reads the executor live. Regression tests cover
late attach, paths gating, and the no-provider SDK path.
@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 — re-running the gate on the current head after five /review rounds.

Template still good ✓

Problem: still an observed bug with solid evidence — #9821 is open, type/bug + priority/P2, reported by a different user, and the regression tests reproduce the exact reported failure signature. Nothing has changed here since the first gate pass.

Direction: aligned — a correctness fix for an intermittent user-facing failure on the core Skill-tool path.

Size: production logic ≈ 115 lines (config.ts +5, skill-utils.ts +6, skill.ts +100/−4); test lines 250 (skill.test.ts). The diff grew since the first gate pass (≈ 61 production lines then): the additions are the hidden-skill/command-collision guard that round-2 review flagged as Critical, plus its coverage. Well under any threshold.

Approach: scope still feels right. The core fix (live provider read at validation time, cache as fallback) is unchanged, and the collision guard sits on the same validation/execute path — it closes the case where invoking a command that shares its name with a disable-model-invocation file skill would load the skill's body from disk, a real gap since loadSkillForRuntime resolves by name only. The structural suggestions — single-sourcing the shadow rule, extracting a shared executor helper, required hiddenSkillNames typing — were explicitly deferred by the author to keep this a bugfix; at round 5 that's the right call.

Risk: no elevated risk signals — no files on the revert-correlated path list.

Moving on to code review. 🔍

中文说明

感谢贡献——五轮 /review 之后在当前 head 上重跑门检。

模板依然完整 ✓

问题:仍是证据充分的已观测 bug——#9821 仍 open,带 type/bug + priority/P2,由另一位用户报告,回归测试能复现出与报告完全一致的失败签名。与首次门检相比没有变化。

方向:对齐——核心 Skill 工具路径上用户可见间歇性失败的正确性修复。

规模:生产逻辑约 115 行(config.ts +5、skill-utils.ts +6、skill.ts +100/−4);测试 250 行(skill.test.ts)。相比首次门检(约 61 行生产代码)diff 有所增长:新增部分是第 2 轮 review 标记为 Critical 的隐藏技能/命令同名冲突防护及其测试覆盖。远低于任何阈值。

方案:范围仍然合理。核心修复(校验时实时读取 provider、缓存兜底)未变;冲突防护位于同一条校验/执行路径上——它关闭了调用与 disable-model-invocation 文件技能同名的命令时会从磁盘加载技能体的问题,这是真实缺口:loadSkillForRuntime 只按名字解析。结构性建议——遮蔽规则单一来源、抽取共享 executor 辅助函数、hiddenSkillNames 必填类型——作者已明确推迟以保持 bugfix 范围;第 5 轮时这是正确选择。

风险:无升级风险信号——未命中与 revert 相关的路径清单。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at f1defaa1c6ab6c67d5e465b0bf49a9526169045f · 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

No Critical findings on this head — the round-2 Critical is resolved, and round 5 of /review posted zero new findings. What I verified in the diff:

  • Live-provider read (getModelInvocableCommands) — the order-independent fix for the Native slash commands intermittently missing from the Skill-tool surface (async modelInvocableCommands race) #9821 race. The trickiest semantic is provable, not eyeballed: the validation-side shadow set (availableSkills ∪ pendingConditionalSkillNames) equals the collect-side fileBasedSkillNames dedup set exactly, because isSkillActive returns true for every unconditional skill — so announcement and validation cannot diverge in either direction. Both fallbacks (no provider → cache; provider throws → cache with a debug warn) keep SDK mode unchanged, and both have tests.
  • Hidden-skill guard — the collision it closes is real: loadSkillForRuntime resolves by name and never consults disableModelInvocation, so before this guard, invoking a same-named command loaded the hidden skill's body from disk. The guard faithfully mirrors the existing disabled-skill guard, with two deliberate differences: the failure message is "not found" (a hidden skill must stay undiscoverable), and the executor-throw path logs a debug warning. Telemetry attribution matches the disabled-guard precedent exactly — no SkillLaunchEvent on successful delegation, recordSkillInvocation(failure) only when no fallback was attempted.
  • Consumers checked — the new optional hiddenSkillNames field is harmless to every collectAvailableSkillEntries caller (they all consume entries); SkillToolInvocation has exactly one construction site and the new constructor parameter is defaulted. Both provider-registration sites (interactive slashCommandProcessor after CommandService.create, headless nonInteractiveCliCommands) match the race the fix targets.

Open by author deferral, recorded but not blocking: single-sourcing the shadow rule (R1-1), extracting the shared executor fallback (R3-2), required hiddenSkillNames typing (R3-3), and two round-5 test-hygiene items. All Suggestion-level; reasonable as follow-ups.

Test evidence — the PR's own CI (review is static; no PR code was executed here)

The workflow-size gate failure that blocked the earlier head is resolved on main, so CI ran the full suite this time and is entirely green on this commit — notably Test (ubuntu-latest, Node 22.x) (unit suite including the four late-attach regressions and the hidden-guard cases) and precheck-pr / precheck (build + typecheck + lint). Integration tests and the macOS/Windows test legs are skipped for fork PRs, same as every prior round.

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
precheck-pr / precheck ✅ success
Dependency CVE audit ✅ success
Secret scan (TruffleHog) ✅ success
Integration Tests (CLI, No Sandbox) ⏭️ skipped (fork PR)
Test (macos-latest, Node 22.x) ⏭️ skipped (fork PR)
Test (windows-latest, Node 22.x) ⏭️ skipped (fork PR)

The regression tests pin the fix mechanically: they construct SkillTool with a null provider, drain the constructor's refreshSkills(), then attach the provider without any change event — against the old cache-only code that sequence fails with the exact reported signature, so a green suite here cannot be one that passes identically with the diff removed.

What unit CI cannot settle is the live-session claim itself — that an interactive TUI run with user-level slash commands no longer hits Skill "..." not found. The author's two self-reported end-to-end PASS comments are their claim, not evidence this review can adopt. The sandboxed verify job running alongside this triage run is the independent check for exactly that claim; its report lands in this thread when it completes. Not verified: live TUI behavior (pending that report); macOS/Windows legs (skipped in fork CI — the change is platform-agnostic TypeScript).

中文说明

代码审查

当前 head 无 Critical——第 2 轮的 Critical 已解决,第 5 轮 /review 没有新发现。diff 中核实的内容:

  • 实时 provider 读取(getModelInvocableCommands——对 Native slash commands intermittently missing from the Skill-tool surface (async modelInvocableCommands race) #9821 竞态的顺序无关修复。最微妙的语义是可证明而非目测的:校验侧遮蔽集(availableSkills ∪ pendingConditionalSkillNames)与收集侧 fileBasedSkillNames 去重集完全相等,因为 isSkillActive 对所有无条件技能返回 true——宣告与校验在任一方向上都不会分歧。两条兜底(无 provider → 缓存;provider 抛错 → 缓存并记 debug 警告)保持 SDK 模式不变,且都有测试。
  • 隐藏技能防护——所关闭的冲突真实存在:loadSkillForRuntime 只按名字解析、从不检查 disableModelInvocation,因此在此防护之前,调用同名命令会从磁盘加载隐藏技能的技能体。该防护忠实镜像现有 disabled 技能防护,有两处刻意差异:失败消息为 "not found"(隐藏技能必须保持不可发现)、executor 抛错路径记录 debug 警告。遥测归因与 disabled 防护的先例完全一致——成功委托时不发 SkillLaunchEvent,仅在未尝试兜底时 recordSkillInvocation(failure)
  • 消费方核查——新的可选 hiddenSkillNames 字段对所有 collectAvailableSkillEntries 调用方无害(均消费 entries);SkillToolInvocation 只有一个构造点,新构造参数有默认值。两处 provider 注册点(交互式 slashCommandProcessorCommandService.create 之后、headless nonInteractiveCliCommands)与修复针对的竞态吻合。

作者明确推迟、记录但不阻塞:遮蔽规则单一来源(R1-1)、共享 executor 兜底抽取(R3-2)、hiddenSkillNames 必填类型(R3-3)、两条第 5 轮测试卫生项。均为 Suggestion 级别,适合作为后续跟进。

测试证据——本 PR 自己的 CI(审查为静态,未执行任何 PR 代码)

阻塞早前 head 的工作流体积门禁失败已在 main 上解决,本次 CI 完整运行且在该提交上全绿——尤其是 Test (ubuntu-latest, Node 22.x)(含 4 个晚挂载回归与隐藏防护用例的单测套件)与 precheck-pr / precheck(build + typecheck + lint)。集成测试与 macOS/Windows 测试腿在 fork PR 上照旧跳过。

(CI 结论表格见上方英文部分)

回归测试在机制上钉住了修复:用例以 null provider 构造 SkillTool,等构造期 refreshSkills() 落定,再在不触发任何变更事件的情况下挂载 provider——旧的纯缓存代码在这一序列下会以与报告完全一致的签名失败,因此这里的绿色不可能是"去掉 diff 也照样过"的套件。

单测 CI 无法覆盖的是真实会话声明本身——交互式 TUI 下用户级 slash 命令不再出现 Skill "..." not found。作者两条自报的端到端 PASS 是其声明,不是本审查可采纳的证据。与本次 triage 并行运行的沙箱 verify 作业正是针对该声明的独立核查,完成后报告会发在本线程。未验证:真实 TUI 行为(等待该报告);macOS/Windows 测试腿(fork CI 跳过——改动是平台无关的 TypeScript)。

Qwen Code · qwen3.8-max

Reviewed at f1defaa1c6ab6c67d5e465b0bf49a9526169045f · 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 fix is proven rather than plausible (shadow-set equivalence, a real collision closed by the guard, regression tests that pin the change, fully green CI); docking one point only because the live-TUI end-to-end claim still rests on the author's word until the in-flight verify job reports.

Stepping back: this PR aged well across its five review rounds. The core fix is exactly what I'd have written — validation reads the provider live, the cache stays as fallback, and the shadowing semantics are provably identical to the announce side. The rounds added only what earned its place: the round-2 Critical was a real hole (a same-named command silently executing a disable-model-invocation skill's body), and the guard that closes it is a faithful mirror of the existing disabled-skill guard rather than a new mechanism. The author resisted scope pull in both directions — no grand re-plumbing of change notifications, and the structural suggestions (single-sourcing the shadow rule, a shared executor helper) explicitly deferred to follow-ups. Every line in the production diff has a job; six months from now this reads as two well-commented guards, not an excavation.

The one thing I still cannot attest to is behavior in a live interactive session. Unit CI pins the mechanics — the regression sequence fails with the exact reported signature on the old code — but the race only manifests under real CommandService.create timing. That leg is currently the author's self-reported PASS plus the sandboxed verify job running alongside this triage run. Approving on the strength of the static proof and green CI; if the verify report surfaces anything unexpected, that is the moment to reopen the question.

中文说明

回顾整体:这个 PR 在五轮 review 中经受住了检验。核心修复正是我会写的样子——校验时实时读取 provider,缓存保留为兜底,遮蔽语义与宣告侧可证明完全一致。各轮只增加了配得上的内容:第 2 轮的 Critical 是真实漏洞(同名命令会悄悄执行 disable-model-invocation 技能的技能体),关闭它的防护忠实镜像现有 disabled 技能防护,而非引入新机制。作者在两个方向上都顶住了范围扩张——没有大改变更通知机制,结构性建议(遮蔽规则单一来源、共享 executor 辅助函数)明确推迟为后续跟进。生产 diff 中每一行都有职责;六个月后回看,这是两个注释清晰的防护,而不是一片待挖掘的废墟。

唯一仍无法背书的是真实交互会话中的行为。单测 CI 钉住了机制——回归序列在旧代码上会以与报告完全一致的签名失败——但竞态只在真实 CommandService.create 时序下显现。这一环节目前是作者的自报 PASS 加上与本次 triage 并行运行的沙箱 verify 作业。基于静态证明与全绿 CI 批准;若 verify 报告出现任何意外,那时再重新讨论。

Qwen Code · qwen3.8-max

Reviewed at f1defaa1c6ab6c67d5e465b0bf49a9526169045f · 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.

Partially reviewed — gaps disclosed. Suggestions are inline.

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): 94 passed — this review observed 21013, 1685, 23494, 1653, 495, 4169, 599 passed.

中文说明

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

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

Test Plan(非阻断):94 passed — this review observed 21013, 1685, 23494, 1653, 495, 4169, 599 passed

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

Comment thread packages/core/src/tools/skill.ts
Comment thread packages/core/src/tools/skill.ts
Comment thread packages/core/src/tools/skill.ts

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

中文说明

仅完成部分审查,审查缺口已披露。

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

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

Comment thread packages/core/src/tools/skill.ts
Comment thread packages/core/src/tools/skill.ts
Comment thread packages/core/src/tools/skill.ts
Comment thread packages/core/src/tools/skill.ts
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Closeout for f3383d3: fixed the hidden-skill/command collision so disable-model-invocation skills no longer load when an unrelated same-named command is invoked, and added validation/fallback coverage for cached commands and hidden-skill names. Intentionally left the broader shadow-set single-sourcing suggestion out of this PR to avoid expanding the #9821 fix. Verified with Prettier and git diff --check; focused Vitest is blocked locally because this isolated worktree cannot resolve vitest/config.\n\n

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Follow-up for 9e7b089: fixed the CI TypeScript failure by defaulting optional hidden-skill collection data for existing test mocks. Prettier and git diff --check passed locally; GitHub CI restarted.\n\n

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed. Suggestions are inline.

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

Not explored to full depth (tool budget reached): "agent 5": running packages/core/src/tools/skill.test.ts to confirm the new tests are green — the worktree and the parent checkout have no node_modules or built dist …; "agent 6a": none — but note I did not run the test suite (verification stage's job) and did not trace telemetry consumers of SkillLaunchEvent beyond confirming the call-s….

Test Plan (not a blocker): 94 passed — this review observed 21049, 1685, 23525, 1654, 495, 4190, 610 passed.

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

  • packages/core/src/tools/skill.ts:296 — [review] R1-1 still stands — shadow set re-implements the collect-side name-shadowing rule; author explicitly deferred single-sourcing beyond the #9821 fix

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

中文说明

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

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

未探索到全部深度(达到工具调用预算):"agent 5"running packages/core/src/tools/skill.test.ts to confirm the new tests are green — the worktree and the parent checkout have no node_modules or built dist …"agent 6a"none — but note I did not run the test suite (verification stage's job) and did not trace telemetry consumers of SkillLaunchEvent beyond confirming the call-s…

Test Plan(非阻断):94 passed — this review observed 21049, 1685, 23525, 1654, 495, 4190, 610 passed

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

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

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

Comment thread packages/core/src/tools/skill.ts
Comment thread packages/core/src/tools/skill.ts
Comment thread packages/core/src/tools/skill-utils.ts
Comment thread packages/core/src/tools/skill.test.ts
Comment thread packages/core/src/tools/skill.ts Outdated
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Closeout update: fixed the hidden-skill command fallback terminal failure path by logging failed launches, preserving failed-invocation attribution when no command fallback exists, warning on swallowed executor errors, and adding hidden-guard coverage for executor error/throw/null/no-executor exits. Intentionally not changed: shared executor helper extraction and required hiddenSkillNames typing, both left out to avoid widening this bugfix. Verification: Prettier check and git diff --check passed; focused Vitest could not run locally because this isolated worktree dependency state cannot resolve ajv/dist/2020.js after the build prerequisite step.

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Closeout update: refreshed this fork branch with latest main to recover the exact-head CI failure. No conflicts; no product code changed beyond the merge. Verification is pending on the new CI run.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed. Suggestions are inline.

1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R3-2 hidden-skill guard remains a ~40-line near-verbatim copy of the disabled guard — already reported (comment 3840692075); author declined extraction in this bugfix

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

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

中文说明

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

本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

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

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

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

Comment thread packages/core/src/tools/skill.ts
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Closeout update: added hidden-skill fallback coverage for forwarding command args so same-named prompt delegation keeps user-supplied arguments. Verified with Prettier, git diff --check, full build, and focused core Vitest for the hidden-skill cases. New CI is pending on the pushed head.\n\n

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

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

  • packages/core/src/tools/skill.test.ts:1558 — [review] hidden-skill test inlines the createHiddenSkillInvocation helper setup instead of calling it
  • packages/core/src/tools/skill.test.ts:1592 — [probe] hidden-skill happy-path test missing recordSkillInvocation assertion (telemetry mutant survives 93/93)
中文说明

仅完成部分审查,审查缺口已披露。

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

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

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

@yiliang114

yiliang114 commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

Local runtime verification: PASS (late-attach race reproduced end-to-end)

# Probe Before (merge-base) After (PR head) Verdict
1 Late-attached command passes validateToolParams FAIL — Skill "late-cmd" not found. Available skills: <48 file skills> (construction-time cache was empty) PASS — returns null
2 Unknown name still rejected (no over-acceptance) PASS PASS
3 Throwing provider falls back to cached set, no crash PASS PASS

Before excerpt:

PROBE1 late-attached command validation => FAIL ("Skill \"late-cmd\" not found. Available skills: ...")

After excerpt:

PROBE1 late-attached command validation => PASS (null)
PROBE2 unknown skill still rejected   => PASS (rejected)
PROBE3 throwing provider no crash     => PASS (result: rejected )

So a command announced in <available_skills> after startup is now invocable through the Skill tool, while the shadowing/fallback guards stay intact. Runtime evidence only; the PR's regression tests already run in CI.

Visual evidence (terminal screenshots, same driver on each arm):

Before (merge-base 3892ca32cace) — late-attached command rejected:

before-9824

After (PR head f1defaa1c6ab) — late-attached command validates, guards intact:

after-9824

1 similar comment
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Local runtime verification: PASS (late-attach race reproduced end-to-end)

# Probe Before (merge-base) After (PR head) Verdict
1 Late-attached command passes validateToolParams FAIL — Skill "late-cmd" not found. Available skills: <48 file skills> (construction-time cache was empty) PASS — returns null
2 Unknown name still rejected (no over-acceptance) PASS PASS
3 Throwing provider falls back to cached set, no crash PASS PASS

Before excerpt:

PROBE1 late-attached command validation => FAIL ("Skill \"late-cmd\" not found. Available skills: ...")

After excerpt:

PROBE1 late-attached command validation => PASS (null)
PROBE2 unknown skill still rejected   => PASS (rejected)
PROBE3 throwing provider no crash     => PASS (result: rejected )

So a command announced in <available_skills> after startup is now invocable through the Skill tool, while the shadowing/fallback guards stay intact. Runtime evidence only; the PR's regression tests already run in CI.

Visual evidence (terminal screenshots, same driver on each arm):

Before (merge-base 3892ca32cace) — late-attached command rejected:

before-9824

After (PR head f1defaa1c6ab) — late-attached command validates, guards intact:

after-9824

@yiliang114

Copy link
Copy Markdown
Collaborator Author

@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: 38 passed · 0 failed · 38 total

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

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

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

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

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

Verification report

PR #9824 verification — fix(core): validate skill commands against the live provider to survive late attach

Verdict: merge-ready — 38/38 scripted assertions passed, 0 unexpected failures. Verified head: f1defaa1c6ab6c67d5e465b0bf49a9526169045f (merge 18f53d4ea0 onto base tip d128998779).

中文摘要
  • 结论:merge-ready(38/38 脚本断言通过,0 意外失败)
  • A/B 结论:中心声明(晚挂载 provider 后 validateToolParams 仍接受其命令)在真实启动路径上被证明 load-bearing——mock-free 夹具(真实 Config.initialize()warmAll() 构造 SkillTool、真实 SkillManager 扫描真实 SKILL.md)head 9/9、base 9/9 按各自预测行为运行,A4 单元格翻转(base 晚挂载后仍 not found,head 为 null);base 还复现了隐藏技能被执行体加载的次级缺陷(A8 翻转)。vitest 侧:base + head 测试文件 -t "issue #9821" 为 2 红(与 issue 报告签名逐字节一致)/2 绿,head 为 4 绿;base 全文件普查恰好 8 红(2 晚挂载 + 6 隐藏技能执行),其余 85 绿。
  • 变异矩阵:M0 对照 93/93 绿;M1–M7 七个变异全部被预期断言捕获(含同文件正向对照 M6),无幸存变异、无 vacuous 测试;M7 证明 skill-utils.tshiddenSkillNames 收集块同样 load-bearing。
  • 未覆盖:逐 commit 归因(浅克隆仅 3 个 commit,只验证聚合 diff);packages/core 全量套件(仅目标套件 + environmentContext);真实交互 TUI 端到端(PR 自身也声明未做);Windows/macOS。
  • 观察(非阻塞)CollectedAvailableSkills.hiddenSkillNames 标为可选且 refreshSkills?? new Set() 兜底,属类型契约驱动的冗余防御;PR 描述中 “94 passed” 相对合并头已漂移(实测 102,系 main 后续合入的测试,PR 自身净增 12 个 it,账目自洽)。

Scope

Central claim: SkillTool.validateToolParams consults the model-invocable-commands provider live, so commands registered after SkillTool construction (the interactive CLI's post-CommandService.create attach) pass validation — fixing #9821's intermittent Skill "…" not found.

Secondary claims: (1) the live read preserves shadowing semantics (path-gated skills stay gated; provider-throw and no-provider fall back to the cache); (2) hidden skills (disable-model-invocation) no longer execute their body when a same-named command exists — execute() delegates to the command executor instead, with correct not-found/telemetry behavior otherwise.

Premise statically confirmed before testing: Config.initializeInternal calls toolRegistry.warmAll() (config.ts:3188-3189) which instantiates the lazily-registered SkillTool factory (config.ts:8734-8737), while the interactive provider registration happens only inside slashCommandProcessor's async effect after await CommandService.create(...) (slashCommandProcessor.ts:761-776); headless mode registers before queries (nonInteractiveCliCommands.ts:427-439), matching the issue's interactive-only symptom.

A/B table — load-bearing proof

Cell set 1: mock-free harness (harness/mockfree-ab.mts, run via tsx inside per-arm worktrees; real Config, real SkillManager over real SKILL.md files on disk, real SkillTool constructed by Config.initialize(); provider attached late with no SkillManager change event). Witness: evidence/01-mockfree-ab-live.png.

cell oracle head (f1defaa) base (d128998)
A1 SkillTool constructed by initialize PASS PASS
A2 real skill discovered from disk (validity control) PASS PASS
A3 pre-attach validate('late-command') rejected (not-found) rejected (not-found)
A4 post-attach validate('late-command')THE FLIP null (accepted) not-found string (bug)
A5 negative control never-registered rejected rejected
A6 real skill still validates post-attach null null
A7 validate('hidden-cmd') post-attach null not-found (same stale-cache bug)
A8 execute('hidden-cmd') with executor — FLIP 2 runs executor (EXECUTED:hidden-cmd:hello, "Delegated to command") loads hidden skill body (bug)
A9 positive control: visible skill body executes PASS PASS

Both arms 9/9 against their arm-specific expectations (base expectations encode the buggy behavior, so a green base arm proves the contrast).

Cell set 2: vitest red/green — HEAD's skill.test.ts copied into the base worktree (compiles against base sources; only public API used). Witness: evidence/02-vitest-red-green.png.

run result
base + head tests, -t "issue #9821" 2 failed / 2 passed — failures are the two late-attach tests with the exact reported signature expected 'Skill "late-command" not found. Available skills: code-review, testing' to be null
head, -t "issue #9821" 4 passed
base + head tests, full file 8 failed / 85 passed — exactly the 2 late-attach tests + the 6 hidden-skill execute tests; no collateral

Cell set 3: mutation matrix (mutants applied to a scratch HEAD worktree, full skill.test.ts each, file restored after). Witnesses: evidence/03-mutant-m1-cache-only.png, evidence/04-mutant-m4-no-hidden-guard.png; raw per-mutant logs in logs/mutant-m*.txt.

mutant change result caught by (intended assertion?)
M0 none (control) 93/93 green
M1 live read → cache only (2 call sites) 2 failed the two late-attach tests — vacuity proof of the central claim
M2 delete throw-fallback 1 failed "falls back to cached commands when the live provider throws"
M3 delete name-shadowing filter 2 failed new late-gating test (validate returned null where a gated message was expected) + pre-existing "does not allow a pending conditional skill to be invoked via the command path"
M4 delete hidden-skill execute guard 6 failed all 6 hidden-skill execute tests (loadSkillForRuntime spy called with the hidden name)
M5 delete hidden-branch recordSkillInvocation 1 failed "returns not-found and records failure when no hidden skill command alternative exists"
M6 change empty-state not-found wording (positive control, same file) 2 failed the two tests pinning that exact string (skill.test.ts:415 + initialization degrade test)
M7 drop hiddenSkillNames from collectAvailableSkillEntries result 6 failed same 6 hidden tests — the skill-utils.ts hunk is load-bearing; ?? new Set() keeps the chain type-safe but empty

No survivors → no coverage gaps, no dead guards. M4 and M7 kill the same six tests: the hidden-skill hazard is closed by a two-hunk chain (collect → cache → execute), each hunk individually necessary.

Reviewer Test Plan walkthrough

plan step result
1. Red-before-fix, -t "issue #9821" on tests-only reproduced exactly (cell set 2, row 1)
2. Green with fix, 4 regression tests reproduced (cell set 2, row 2)
3. Targeted suites "94 passed" green at merge head with 102 passed (93 skill + 9 skill-utils static its expand to 102 with it.each); the delta vs 94 is main-side test drift merged after the PR's count was written — the PR's own contribution is exactly +12 it blocks (78→90 in skill.test.ts), consistent
4. tsc --noEmit on core exit 0, 0 errors (witness evidence/05-targeted-gate-typecheck.png)
5. eslint/prettier clean on changed files both clean; both gates proven live by planted violations (unused var → eslint error; malformed spacing in packages/core/src/tools/ → prettier warn)

Findings

No blocking findings. Non-blocking observations:

  1. Optional hiddenSkillNames field is redundant defence. CollectedAvailableSkills.hiddenSkillNames? is optional and refreshSkills guards with ?? new Set(), but the only producer (collectAvailableSkillEntriesUncached) always sets it. M7 shows the chain matters; the ?? itself defends only the type contract. Correct as-is.
  2. Live-read shadowing uses cached skill sets. getModelInvocableCommands() filters live commands against availableSkills/pendingConditionalSkillNames from the last refreshSkills(). A file-based skill created between refreshes would not shadow a same-named command until the SkillManager watcher fires. Window is bounded by the existing watcher mechanism and identical to the pre-existing 2 s collect memo; not a regression, noted for completeness.
  3. Description count drift ("94 passed" vs 102 at merge head) — main-side drift, arithmetic consistent with the PR's +12 tests; not a defect.

Not covered

  • Per-commit attribution: checkout is depth-2 (merge, base tip, PR head only); git rev-list HEAD^1..HEAD^2 returns 1 at the shallow boundary while the metadata lists 7 commits. Verified the aggregate HEAD^1..HEAD diff only.
  • Full packages/core suite: ran skill.test.ts, skill-utils.test.ts (102) and environmentContext.test.ts (64) at head, plus the base-side census; the giant client.test.ts/coreToolScheduler.test.ts suites mock collectAvailableSkillEntries and were not run.
  • Live interactive TUI E2E: the mock-free harness drives the real startup path (Config.initializewarmAll → late provider attach) but not a full TUI session; the PR also declares this out of scope.
  • Base-tree typecheck: not cited — my worktrees reused the root node_modules plus a symlinked package-local node_modules (external deps only; asserted no @qwen-code/* workspace links inside it), which produced a single @lydell/node-pty TS7016 resolution artifact absent in the main tree (0 errors). Environmental to my harness, not to the PR.
  • Windows/macOS (PR declares Linux-only).

Methodology

Environment: node:22-bookworm-class container, Node v22.23.2, no GitHub token. Arms: scratch git worktrees at HEAD^1 (base) and HEAD (mutant/head), each wired to the pre-installed root node_modules plus a symlink of packages/core/node_modules (verified external-deps-only, so no workspace code crosses the tree boundary; the code under test always resolved within its own tree via relative imports). Harnesses: harness/mockfree-ab.mts (real objects, tsx), harness/mutate.sh (7 mutants), harness/capture-all.sh (5 verify-capture.mjs PNGs in evidence/). Raw logs in logs/. Assertion counts in assertions.json map 1:1 to the cells/rows above; expected base-arm reds are encoded as passing assertions.

Flakiness gate log

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


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/core/src/tools/skill.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/skill.test.ts: P (exit 0)
round 2 · packages/core/src/tools/skill.test.ts: P (exit 0)
round 3 · packages/core/src/tools/skill.test.ts: P (exit 0)
round 4 · packages/core/src/tools/skill.test.ts: P (exit 0)
round 5 · packages/core/src/tools/skill.test.ts: P (exit 0)

Evidence images

01-mockfree-ab-live

02-vitest-red-green

03-mutant-m1-cache-only

04-mutant-m4-no-hidden-guard

05-targeted-gate-typecheck

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

Qwen Code · sandboxed verification

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

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

Scope: Standard tier — 4 files, 491 lines diff (mostly new tests).

Checked:

  • Root cause: SkillTool constructor calls refreshSkills() before CommandService registers the modelInvocableCommandsProvider. The fix reads the live provider on every validateToolParams call via getModelInvocableCommands(), falling back to the cached set when the provider is absent or throws.
  • Shadow filter: getModelInvocableCommands() filters commands whose names collide with availableSkills or pendingConditionalSkillNames — preserving the path-gating branch. Test keeps path-gated skills gated when the provider is late-registered pins this directly.
  • Hidden skill execute path: isSkillHidden is checked first in execute(). Command executor is tried; success returns the MCP result; throw/null falls through to not-found. All outcomes covered by the 5 new hidden-guard execute tests.
  • Prior critical R2-1: "command with same name as hidden skill passes validation" — this is the intended behavior; the test should accept a command with the same name as a hidden file skill explicitly validates it, and the execute path routes to the command executor correctly. Confirmed not a blocker.
  • Error handling: catch in getModelInvocableCommands() logs via debugLogger.warn and falls back to cache — correct resilience.

CI: precheck-pr SKIPPED — fork PR security model.

Cross-check: R2-1 was addressed (intentional design, confirmed by test). R3-x suggestions (shadow-set refactor, arg-forwarding test, telemetry on throw) are non-blockers. Latest prior APPROVED covers current head.

Reviewed with AI assistance.

@wenshao

wenshao commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Local deep verification — merge-ready ✅ (58/58 scripted assertions, 0 unexpected failures)

Maintainer-local verify round against head f1defaa1 (base 3892ca32), macOS, real builds of all arms (npm ci + npm run build per worktree). Full report and harnesses: tmp/pr9824-verify-20260825-133220/ in my local tree; screenshots below are hosted on this repo's pr-assets/9824-verify branch.

Central claim — proven with a three-arm A/B (the PR bundles two fixes, so I built the intermediate)

The PR's own commit history supplies the intermediate variant (e06203e, validation fix only), which turns the usual two-cell A/B into the table that actually explains the PR:

arm late-attach validation of late-command hidden skill + same-named command at execute()
base 3892ca32 Skill "late-command" not found. Available skills: alpha, … — the exact #9821 signature ✗ hidden skill body injected (HIDDEN SKILL BODY MUST NOT EXECUTE)
mid e06203e (validation fix only) ✓ passes still injects the hidden body; command executor never called
head f1defaa1 ✓ passes ✓ delegates to the command → COMMAND OUTPUT for dup, display Delegated to command: dup, args forwarded

The mid arm is the interesting one: the validation fix alone unmasks a quieter, worse failure. Once validation admits the command, loadSkillForRuntime resolves the same-named disable-model-invocation skill and injects its body — silently. On base, the validation bug masked the execution bug (validation rejected before execute ever ran). The second commit fixes exactly what the first unmasked; the bundling is load-bearing, not accidental.

The harness drives the compiled packages/core/dist — real SkillTool, real SkillManager scanning real on-disk skill files — with the two config seams (provider, executor) registered late and no SkillManager change event, mirroring slashCommandProcessor's post-CommandService.create registration. readlink -f confirmed each arm resolved @qwen-code/qwen-code-core into its own tree.

A/B: base vs mid vs head

Also scripted on all three arms: late-registered commands appear in the not-found listing (mid/head only); path-gated skills stay gated under a live provider (all arms — the live read's shadowing preserves the gated by paths: branch); provider-throw falls back to the cached snapshot; no-provider (SDK) mode unchanged. Perf: 1000 hit-validations with 2000 registered commands ≈ 100 ms on head vs ≈ 1 ms on base (~0.1 ms/validation); the not-found branch calls the provider twice. Negligible at real command counts.

Mutation matrix — the new tests are not vacuous

Head's test file against source variants (sources swapped via git checkout <oid> -- <files>, restored afterwards):

source variant result red cells
head (unmutated) 93/93 green none — control
mid e06203e (hidden guard removed) 6 failed / 87 passed exactly the 6 new hidden-skill tests
base (both fixes removed) 8 failed / 85 passed the 2 late-attach tests + the same 6

Both fix clusters are pinned by their own tests.

Mutation matrix

Gates

  • packages/core vitest (skill.test.ts + skill-utils.test.ts): head 102/102, base 90/90+12 passing, +0 failing.
  • tsc --noEmit (core): pass.
  • Trial merge into current main (c3d9279932): clean; main's only overlap is config.ts in an unrelated region (computer-use removal); affected suite on the merged tree: 102/102.

Findings (all non-blocking)

  1. The PR body describes only half the diff. "What this PR does" covers the validation live-read but not the execute-layer hidden-skill guard (~45 lines + hiddenSkillNames plumbing + 6 tests) that changes behavior when a disable-model-invocation skill shares a name with a model-invocable command. The commit messages do describe it; the PR description should too, since it's user-observable.
  2. [Minor] hidden+disabled wording change: execute() for a skill that is both hidden and user-disabled now returns Skill "hd" not found. where base returned … is disabled. Re-enable it via /skills …. validateToolParams still says "is disabled", and the scheduler validates first, so this is reachable mainly via direct/SDK invocation. Deliberate ordering consequence; no fix requested.
  3. [Nit] telemetry asymmetry: a command executed via the hidden fallback emits no SkillLaunchEvent(success) / onSkillLoaded — mirrors the pre-existing disabled-skill path (whose comment justifies it), so /context attribution skips such commands. Pattern extended, not a regression.

Sibling edge probe

Not covered

A live interactive TUI session end-to-end (the dist harness reproduces the construction/registration ordering that produces the race, but no real TUI was driven); non-interactive CLI ordering was code-read only; eslint/prettier not re-run locally (CI covers).

中文摘要(点击展开)

结论:可合并(merge-ready),58/58 脚本断言全部通过,0 个意外失败。

  • 核心声明成立:三臂 A/B(base / 仅校验修复的中间提交 e06203e / head)证明 provider 晚挂载且无 SkillManager 变更事件时,base 以 issue Native slash commands intermittently missing from the Skill-tool surface (async modelInvocableCommands race) #9821 的精确签名拒绝校验(Skill "late-command" not found. Available skills: …),head 实时读取 provider 后放行。harness 驱动编译后的 dist(真实 SkillTool + 真实 SkillManager + 真实磁盘技能文件),provider/executor 是生产代码自己使用的配置 seam,非 mock。
  • 捆绑的第二个修复是必要的且未被 PR 正文描述:中间臂显示,只打校验修复会让模型"安静地"收到 hidden skill 的 body 注入(比 base 的响亮报错更糟);第二个 commit 修的正是第一个揭开的问题,捆绑合理。建议作者把这一半补进 PR 描述。
  • 变异矩阵:head 测试对 base 源码 8 红(2 晚挂载 + 6 hidden)、对中间源码 6 红(仅 hidden)、对 head 93/93 绿——两簇修复都被钉死,非空测试。
  • 门禁:head 102/102、base 90/90(+12 通过 / +0 失败);core tsc --noEmit 通过;与当前 main 试合并无冲突,合并树上 102/102(main 对 config.ts 的改动在无关区域)。
  • 次要发现(不阻塞):hidden+disabled 技能的 execute 措辞从 "is disabled"(带恢复提示)变为 "not found"(深层边缘路径);hidden 回退执行的命令不计入 SkillLaunchEvent//context 归因(沿袭既有 disabled 路径的行为);2000 命令时每次校验 live 读取约 0.1ms——实际数量级下可忽略。
  • 未覆盖:真实交互式 TUI 端到端会话(harness 精确复现了竞态时序,但未驱动完整 TUI);非交互 CLI 注册时序仅代码走读;eslint/prettier 未本地重跑(CI 覆盖)。
  • 截图(三臂 A/B、变异矩阵、边缘探针)托管在 pr-assets/9824-verify 分支 verify/pr9824-local-20260825-133220/,正文已按 raw URL 引用;完整报告与 harness 在本地 tmp/pr9824-verify-20260825-133220/

@yiliang114
yiliang114 added this pull request to the merge queue Aug 25, 2026
Merged via the queue into QwenLM:main with commit 054eabb Aug 25, 2026
91 checks passed
@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.

Native slash commands intermittently missing from the Skill-tool surface (async modelInvocableCommands race)

4 participants