Skip to content

feat(skills): add overridable default-disabled state - #7357

Merged
wenshao merged 10 commits into
QwenLM:mainfrom
samuelhsin:codex/feat-skills-default-disabled
Jul 25, 2026
Merged

feat(skills): add overridable default-disabled state#7357
wenshao merged 10 commits into
QwenLM:mainfrom
samuelhsin:codex/feat-skills-default-disabled

Conversation

@samuelhsin

@samuelhsin samuelhsin commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR adds an overridable default-disabled skill state. skills.defaultDisabled supplies soft defaults, skills.enabled records explicit opt-ins, and the existing skills.disabled remains a hard disable that always wins. Skill names are matched case-insensitively and persisted with their canonical names.

The same precedence is applied by CLI startup, ACP status, daemon status and persistence, and the existing /skills picker: disabled > enabled > defaultDisabled.

The implementation is intentionally scoped to settings resolution and the existing status/toggle surfaces. It does not modify core skill loading, skill discovery, extension loading, or locale copy.

Why it's needed

Extensions and higher-level configuration need a way to ship skills turned off by default while still allowing a user or project to opt in. Reusing skills.disabled cannot express that distinction because it intentionally remains a hard union across scopes.

Reviewer Test Plan

How to verify

  1. Put a mixed-case skill name in user-level skills.defaultDisabled; confirm the skill is default-disabled, unlocked, and unchecked in /skills.
  2. Enable that skill at project scope; confirm it becomes available and the canonical name is stored once in skills.enabled.
  3. Repeat the enable; confirm it is a no-op with no duplicate settings.
  4. Disable it again; confirm the opt-in is removed, the canonical name is stored in skills.disabled, and status becomes hard-disabled.
  5. Put the skill in user-level skills.disabled, then try to enable it from a project workspace; confirm the picker shows the higher-scope lock and the daemon rejects the toggle without creating project settings.

Focused configuration, status, mapping, facade, ACP, and daemon tests passed. Full build, typecheck, lint, and bundle generation passed. The final bundle was also exercised through the daemon HTTP flow and a real PTY /skills session.

Evidence (Before & After)

Before, the default-disabled skill is unchecked:

[ ] opt-in-skill

After pressing Space, it is checked, and saving writes {"skills":{"enabled":["opt-in-skill"]}} without writing skills.disabled:

[x] opt-in-skill

Tested on

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

Environment (optional)

Node.js 22+, locally rebuilt and bundled CLI, daemon HTTP flow on localhost, real PTY with sandboxing disabled; no model request was sent.

Risk & Scope

  • Main risk or tradeoff: The three setting lists must keep one precedence rule across CLI, ACP, and daemon consumers; focused tests cover default opt-in, canonical persistence, no-op repetition, and higher-scope hard locks.
  • Not validated / out of scope: Windows and Linux were not tested locally. Core skill loading, skill discovery, extension loading, and locale copy are unchanged.
  • Breaking changes / migration notes: None. Existing skills.disabled behavior remains hard and unchanged; the new fields are optional.

Linked Issues

Fixes #7347

中文说明

本 PR 做了什么

本 PR 新增可覆盖的技能默认禁用状态。skills.defaultDisabled 提供软默认值,skills.enabled 记录显式启用,现有 skills.disabled 仍是始终优先的硬禁用。技能名称按大小写不敏感匹配,并以规范名称持久化。

CLI 启动、ACP 状态、daemon 状态与持久化,以及现有 /skills 选择器使用相同优先级:disabled > enabled > defaultDisabled

实现范围刻意收敛在设置解析和现有状态/切换界面,不修改 core 技能加载、技能发现、扩展加载或多语言文案。

为什么需要

扩展和更高层级配置需要让技能默认关闭,同时仍允许用户或项目显式启用。复用 skills.disabled 无法表达这一差异,因为它仍会在各作用域间合并为硬禁用并集。

Reviewer Test Plan

如何验证

  1. 在用户级 skills.defaultDisabled 中放入大小写混合的技能名称;确认该技能默认禁用、未锁定,并在 /skills 中未选中。
  2. 在项目作用域启用该技能;确认它变为可用,并且 skills.enabled 只保存一次规范名称。
  3. 重复启用;确认操作为 no-op,不产生重复设置。
  4. 再次禁用;确认显式启用被移除,规范名称写入 skills.disabled,状态变为硬禁用。
  5. 将该技能放入用户级 skills.disabled,再从项目工作区尝试启用;确认选择器显示更高作用域锁定,daemon 拒绝切换,且不会创建项目设置。

配置、状态、映射、facade、ACP 和 daemon 的聚焦测试均已通过;完整 build、typecheck、lint 和 bundle 生成已通过。最终 bundle 还通过 daemon HTTP 流程和真实 PTY /skills 会话验证。

证据(之前与之后)

之前,默认禁用技能未选中:

[ ] opt-in-skill

按 Space 后变为选中;保存会写入 {"skills":{"enabled":["opt-in-skill"]}},且不会写入 skills.disabled

[x] opt-in-skill

测试平台

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

环境(可选)

Node.js 22+、本地重新构建并打包的 CLI、localhost daemon HTTP 流程、关闭 sandbox 的真实 PTY;未发送模型请求。

风险与范围

  • 主要风险或权衡:三个设置列表必须在 CLI、ACP 和 daemon 消费端保持同一优先级;聚焦测试覆盖默认启用、规范名称持久化、重复 no-op 和更高作用域硬锁。
  • 未验证 / 范围外:Windows 和 Linux 未在本地测试。core 技能加载、技能发现、扩展加载和多语言文案保持不变。
  • 破坏性变更 / 迁移说明:无。现有 skills.disabled 的硬禁用行为保持不变;新增字段均为可选。

关联 Issue

Fixes #7347

@samuelhsin

samuelhsin commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

E2E test report

Validated the latest-main-rebased final bundle at head 6a86cc9 (0.20.0, SHA-256 70bcdc31d2c09ba0e45d7fa8ce667d92d8d60e2ae2350c884348f5d4768b50fc) on macOS with sandboxing disabled. No model request was sent.

Daemon HTTP flow:

  • A default-disabled skill was reported as overridable and unlocked.
  • Enabling with a mixed-case name returned 200, persisted the canonical name once in skills.enabled, and changed status to enabled.
  • Repeating the enable returned changed: false and created no duplicate.
  • Disabling removed the explicit enable, persisted the canonical hard disable, and changed status to hard-disabled.
  • A user-level hard disable rejected a project-level mixed-case enable with 409 skill_not_toggleable, reported locked/user, and created no project settings.

Real PTY /skills flow:

  • Initial state: [ ] opt-in-skill.
  • After Space: [x] opt-in-skill.
  • After Esc, the workspace setting was exactly {"skills":{"enabled":["opt-in-skill"]}}; skills.disabled was not written.

The daemon, PTY process, and temporary fixtures were cleaned up after verification.

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: real and well-evidenced. Issue #7347 describes a genuine gap — skills.disabled is a hard union across scopes, so there is no way today to ship a skill that is off by default yet enable-able per project. The issue was triaged and accepted for exploration. This is not theoretical.

Direction: aligned. Skills are a first-class capability and the override surfaces (/skills picker, daemon workspace toggle) already exist. An overridable defaultDisabled separate from the hard denylist is a natural, well-scoped addition. CHANGELOG has no direct reference to this exact pattern in Claude Code (they have disableBundledSkills and disableSkillShellExecution but not a soft-default/override split), but the area is clearly relevant to skill management.

Size: this PR touches core paths (packages/core/src/config/config.ts, packages/core/src/tools/skill-utils.ts, packages/core/src/tools/skill.ts, packages/core/src/skills/skill-manager.ts) and spans 5 packages. Production logic: ~600 lines (additions + deletions, excluding tests and schema). Test lines: ~246. Schema: ~14. Docs: ~119. i18n: ~39. Flagging for maintainer awareness given the 500+ production-line threshold on core-touching feat PRs.

Approach: the three-list precedence model (disabled > enabled > defaultDisabled) is clean and the design doc (docs/design/2026-07-20-skills-default-disabled.md) is thorough — it names every downstream consumer, explains why replacement semantics were rejected in favor of an explicit enabled list, and scopes boundaries clearly. The diff carries no unrelated changes; every file serves the stated goal. The scope feels right for the problem.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:真实且有据。Issue #7347 描述了一个实际的缺口——skills.disabled 在各作用域间是硬并集,因此目前没有办法提供一个「默认关闭但可按项目启用」的 skill。该 issue 已被分诊并接受进入探索阶段。这不是理论性问题。

方向:对齐。Skills 是一等能力,覆盖入口(/skills 选择器、daemon workspace toggle)已经存在。在硬黑名单之外新增可覆盖的 defaultDisabled 是自然、范围合理的增量。CHANGELOG 中 Claude Code 没有完全相同的模式(他们有 disableBundledSkillsdisableSkillShellExecution,但没有软默认/覆盖分离),但该领域显然与技能管理相关。

规模:本 PR 触及核心路径(packages/core/src/config/config.tspackages/core/src/tools/skill-utils.tspackages/core/src/tools/skill.tspackages/core/src/skills/skill-manager.ts),横跨 5 个包。生产逻辑约 600 行(增删合计,不含测试和 schema)。测试约 246 行。Schema 约 14 行。文档约 119 行。i18n 约 39 行。因触及核心且生产行超过 500,标记供维护者关注。

方案:三列表优先级模型(disabled > enabled > defaultDisabled)清晰,设计文档(docs/design/2026-07-20-skills-default-disabled.md)详尽——列出了所有下游消费者,解释了为何拒绝替换语义而选择显式 enabled 列表,并明确了范围边界。Diff 不含无关改动,每个文件都服务于既定目标。范围与问题匹配。

进入代码审查 🔍

Qwen Code · qwen3.7-max

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

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

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

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

Qwen Code · serve A/B

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. Suggestions are inline.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/serve/run-qwen-serve.ts Outdated
@wenshao

wenshao commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Code Review — feat(skills): add overridable default-disabled state (#7357)

Reviewed the full diff (50 files, +775/-243) plus surrounding code on the head branch. Overall this is a clean, well-factored change: the three-way precedence (disabled > enabled > defaultDisabled) is computed in one place and threaded uniformly through CLI startup, ACP, daemon status, and daemon toggle persistence. Nice work.

What it does

  • Adds skills.defaultDisabled (soft, overridable defaults) and skills.enabled (explicit opt-ins) next to the existing hard skills.disabled denylist. Effective disabled set = disabled ∪ (defaultDisabled − enabled).
  • Introduces resolveSkillSettings() as the single resolver and a SkillDisablement (hard/default + optional lockedScope) detail type consumed by the two status producers and the /skills picker.
  • Extends the daemon toggle + /skills dialog to persist canonical names into skills.enabled/skills.disabled and emits a settings_changed event per changed key.

Correctness — looks sound

I traced the precedence logic in resolveSkillSettings and the persistence in updateWorkspaceSkillSettingLists:

  • enabled correctly overrides defaultDisabled but never disabled — the hard loop runs after the default loop and unconditionally overwrites, so a name in both enabled and disabled still resolves to hard. ✔
  • Lock-scope reporting (systemDefaults → user → system, last-wins) matches the previous daemon behavior and correctly excludes workspace-scope hard disables (so a workspace can still toggle its own entry). ✔
  • The daemon lock check (disablement.reason === 'hard' && disablement.lockedScope) is equivalent to the old per-scope scan. ✔
  • Defensive guard preserved: the old inline Array.isArray guard against a user writing "disabled": "all" (which would otherwise crash the .filter() in the no-try/catch hot path at skill.ts:224/:356) is now centralized in normalizeSkillNames/skillSettingStrings and is directly covered by the "ignores malformed and empty list entries" test. Good that this wasn't lost in the refactor.
  • updateTarget correctly collapses case-variants/duplicates to one canonical entry while preserving orphan (unloaded-skill) entries.

Conventions

  • All 9 locale files were updated for the changed All available skills are disabled… key — no stale/untranslated key left behind. ✔
  • settingsSchema.ts and the generated vscode-ide-companion JSON schema are updated consistently. ✔
  • SkillDisablement is exported via core/src/index.ts (export * from './config/config.js'), so the CLI's @qwen-code/qwen-code-core import resolves. All three non-test callers of mapSkillConfigToStatus were migrated to the ReadonlyMap signature. ✔

Tests

Solid unit coverage on the new pure logic: resolveSkillSettings, updateWorkspaceSkillSettingLists, mapSkillConfigToStatus, daemon persistence (run-qwen-serve), status provider, and config integration.

  • Gap: SkillsManagerDialog.handleConfirm has no direct test. Its new transition-diff logic (initialSelectedKeys vs selected, previousDisabled locked-name filtering, per-transition calls into updateWorkspaceSkillSettingLists) is the trickiest new code and is only exercised indirectly through the helper's unit tests. The PR's own risk note acknowledges TUI behavior wasn't automated — a focused test over handleConfirm (enable a default-disabled row → writes skills.enabled; disable an opted-in row → moves it to skills.disabled and drops the opt-in; open+confirm with no toggles → no write) would close the highest-value gap and lock in the invariant that merely opening the dialog never converts inherited defaults into workspace hard disables.

Notes / minor

  • Daemon disable is unconditional (by design, worth confirming intent): persistDisabledSkills(ws, name, false) always writes a workspace skills.disabled entry. So a client sending enabled:false for a skill that is already default-disabled converts it default → hard and writes to disk (returns changed:true). Matches the documented step-3 behavior, but it's a state-changing side effect on an otherwise redundant call; the CLI dialog avoids this by only acting on real checkbox transitions. Fine to keep, just flagging the asymmetry.
  • Change detection via JSON.stringify(a) !== JSON.stringify(b) (daemon + dialog) is correct here only because updateTarget preserves order; it reads a little fragile but is fine.
  • Inline import('@qwen-code/qwen-code-core').SkillDisablement type refs in config.ts signatures work but a top-level import type would read cleaner.

Security / performance

No new external input surface — all three lists are user-controlled settings already, and malformed values are normalized rather than trusted. The resolver runs on settings reads/writes (not per token), so no hot-path cost concern.

Verdict: Approve pending the dialog-confirm test. No blocking correctness issues found; the design is coherent and backward-compatible (existing skills.disabled semantics unchanged, new fields optional).


🤖 Reviewed with Claude Code · model: Opus 4.8 (1M context)

@samuelhsin
samuelhsin force-pushed the codex/feat-skills-default-disabled branch from 8c4c1ff to 6a86cc9 Compare July 21, 2026 01:57
@github-actions

Copy link
Copy Markdown
Contributor

Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration.

中文

请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。

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

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/serve/workspace-service/index.ts
Comment thread packages/cli/src/ui/components/skills/SkillsManagerDialog.tsx Outdated
@wenshao

wenshao commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Code Review — feat(skills): add overridable default-disabled state (#7357)

Reviewed the full diff (24 files, +664/−195). This is a clean, well-scoped change that adds skills.defaultDisabled (soft default) and skills.enabled (explicit opt-in) alongside the existing hard skills.disabled, with a single precedence rule disabled > enabled > defaultDisabled applied uniformly across CLI startup, the /skills picker, ACP status, and the daemon toggle/persistence paths.

Overview

  • New CLI-local resolver skill-settings.ts centralizes the effective-disabled computation (disabled + (defaultDisabled − enabled)) and the workspace read-modify-write helper. All consumers now flow through it, so the rule lives in exactly one place.
  • buildDisabledSkillNamesProvider delegates to resolveSkillSettings(...).disabledNames, so every core consumer (getDisabledSkillNames() → skill validation/execution, command/skill loaders) inherits the new semantics without touching core.
  • Status cells gain disabledReason (hard | default | inactive_extension) and optional lockedScope, wired through both the acp-bridge and SDK wire types.

Strengths

  • Correctness of the precedence logic is solid. I traced the resolver and the toggle helper against the tests:
    • Hard disables always overwrite default reasons and carry the higher-scope lockedScope; workspace-scope hard disables have no lockedScope, so they stay toggleable. Matches the pre-PR lock behavior exactly.
    • The daemon lock check (reason === 'hard' && lockedScope) throws before any setValues, so a higher-scope-locked toggle never creates project settings (PR test-plan item 5). ✓
    • The "already inherited-enabled → no-op" case (defaultDisabled && !enabled gate on the opt-in write) correctly avoids writing a redundant workspace skills.enabled entry. ✓
  • Backward compatibility is preserved. With only skills.disabled set, defaultDisabled/enabled are empty and the effective set is identical to before. Malformed inputs ("disabled": "all", [42, null]) still degrade to an empty set via normalizeSkillNames, keeping the "never crash skill invocation" guarantee that the old inline provider documented.
  • The legacy disabledReason === undefined fallback in workspace-service/index.ts preserves the old inactive-extension rejection for status producers that predate the new field — I checked all four branches and they're equivalent to the original !disabledNames.has(...) logic.
  • Docs, settings.md, qwen-serve.md, the protocol doc, and the vscode settings.schema.json are all updated consistently; a design doc is included.

Findings

1. Coverage gap: the /skills picker save handler is untested (Medium).
SkillsManagerDialog.tsx received the most intricate rewrite in this PR — it now threads updateWorkspaceSkillSettingLists across every changed unlocked skill, filters locked names out of the baseline, and JSON-diffs disabled/enabled to emit a two-key setValues. There is no SkillsManagerDialog.test.tsx; this path is only exercised by the manual PTY session in the test plan. Every other surface (resolver, daemon persist, mapping, status, ACP, facade) got a focused test — the TUI save-diff deserves the same, especially the double-write case (uncheck a default-disabled+opted-in skill → both skills.disabled gains the canonical name and skills.enabled drops it). That double-key-change case also isn't asserted in the daemon test (run-qwen-serve.test.ts covers enabled-only and disabled-only, not both at once).

2. SettingScope is string-cast rather than value-imported (Low / robustness).
skill-settings.ts does const USER_SCOPE = 'User' as SettingScope; (and System/SystemDefaults) instead of importing the enum as a value. It's correct today because SettingScope is a string enum whose values equal their names, and a mismatch would throw loudly in forScope's default case rather than silently misbehave — but the cast defeats the type check that would otherwise protect these three literals. The sibling test file already does import { SettingScope } as a value; doing the same in the source would be safer and cost nothing at runtime.

3. Nit: stale skills.enabled entries aren't reclaimed.
An enabled opt-in for a skill that is no longer in defaultDisabled is inert but never cleaned up (re-enabling re-canonicalizes it rather than dropping it). Harmless — enabled only matters against defaultDisabled — but worth a comment or a future tidy-up.

4. Note (not a regression): getDisabledSkillNames() is now heavier per call.
The provider runs resolveSkillSettings (3× normalize + 3× forScope + two loops) on every invocation instead of one array filter. The closure was already non-memoized and it's called once per skill validation/execution (not in a tight loop), so this is negligible in practice — flagging only for awareness.

Verdict

Correctness looks good — I did not find a functional bug; the precedence, lock, and no-op semantics all hold up against the tests and the pre-PR behavior. The main actionable item is a focused test for the /skills picker save handler (finding #1), since it's the most complex rewritten code and currently only manually verified. Findings #2#4 are low priority.

@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. Not reviewed: reverse audit — an auditor ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and what the agent was actually asked is not what this skill certifies.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /takeover

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

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

)

- Fix disabledChanged comparison in SkillsManagerDialog to use
  previousDisabled (locked names filtered) instead of workspaceDisabled,
  preventing spurious settings writes when a skill is disabled at both
  workspace and higher scope
- Import SettingScope as a value instead of string-casting literals in
  skill-settings.ts for compile-time safety
- Add dual-key change test: enabling a workspace-hard-disabled
  default-disabled skill produces both skills.disabled and
  skills.enabled changes in one operation
- Add legacy inactive-extension branch tests: reject when
  disabledReason is undefined and skill is not in settings
  disablements; allow when it is disabled by settings
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on:

Review feedback addressed

Inline comments

[rc:3619128890] disabledChanged inconsistent baseline comparison — FIXED

The disabledChanged comparison in SkillsManagerDialog.tsx used workspaceDisabled (raw workspace-scope entries, including names locked at higher scopes) against next.disabled (derived from previousDisabled, which filters locked names out). When a skill appeared in both workspace skills.disabled and a higher scope, this produced a false-positive change on every dialog close, triggering a spurious setValues write plus the downstream reloadCommands + notifyConfigChanged refresh cycle.

Fix: Changed the comparison to use previousDisabled (the same locked-name-filtered baseline that next.disabled is derived from), so the diff is consistent.

[rc:3617091523] Dual-key change path untested — IMPLEMENTED

Added a test in run-qwen-serve.test.ts that enables a skill which is both workspace-hard-disabled AND default-disabled, asserting changed: true with settingsChanges containing both skills.disabled (cleared) and skills.enabled (added), and verifying the persisted settings file.

[rc:3619128870] Legacy inactive-extension branch untested — IMPLEMENTED

Added two tests in facade.test.ts:

  1. An extension skill with status: 'disabled', disabledReason: undefined, and NOT in the settings disablements map → rejects with reason: 'inactive_extension'.
  2. The same setup but with the skill present in workspace skills.disabled → allows the toggle to proceed (disabled by settings, not by inactive extension).

Issue-level review findings

@wenshao finding #2: SettingScope string-cast — IMPLEMENTED

Replaced the three string-cast constants ('SystemDefaults' as SettingScope, etc.) in skill-settings.ts with a value import of the SettingScope enum and direct member references (SettingScope.SystemDefaults, etc.), giving compile-time validation of the scope literals.

@wenshao finding #1 (dialog-confirm test), #3 (stale enabled entries), #4 (performance note) — DEFERRED

  • Finding pre-release: fix ci #1 (SkillsManagerDialog save-handler test): The dialog's handleConfirm path requires Ink/React component testing infrastructure not yet set up for this component. The underlying logic is covered by the updateWorkspaceSkillSettingLists unit tests and the new daemon dual-key test. Deferred to a follow-up.
  • Finding 如何自定义密钥文件 .env可能与其他文件冲突 #3 (stale skills.enabled entries): Harmless by design — enabled only matters against defaultDisabled. No action needed.
  • Finding Are you interested in AI Terminal? #4 (resolver weight): Acknowledged, negligible in practice (called once per skill validation, not in a tight loop).

Verification

  • npm run build
  • npm run typecheck
  • npm run lint
  • vitest run src/config/skill-settings.test.ts — 7 passed ✅
  • vitest run src/serve/run-qwen-serve.test.ts -t "workspace skill settings persistence" — 2 passed ✅
  • vitest run src/serve/workspace-service/__tests__/facade.test.ts — 89 passed ✅
中文说明

已处理的审查反馈

行内评论

[rc:3619128890] disabledChanged 基线比较不一致 — 已修复

SkillsManagerDialog.tsx 中的 disabledChanged 比较使用了 workspaceDisabled(包含更高作用域锁定名称的原始工作区条目)与 next.disabled(源自过滤了锁定名称的 previousDisabled)。当一个技能同时出现在工作区 skills.disabled 和更高作用域时,每次关闭对话框都会产生误报变更,触发多余的 setValues 写入以及下游的 reloadCommands + notifyConfigChanged 刷新周期。

修复: 将比较改为使用 previousDisabled(与 next.disabled 相同的过滤后基线),使差异比较保持一致。

[rc:3617091523] 双键变更路径未测试 — 已实现

run-qwen-serve.test.ts 中添加了测试:启用一个同时被工作区硬禁用和默认禁用的技能,断言 changed: truesettingsChanges 包含 skills.disabled(已清除)和 skills.enabled(已添加),并验证持久化的设置文件。

[rc:3619128870] 旧版未激活扩展分支未测试 — 已实现

facade.test.ts 中添加了两个测试:

  1. 扩展技能 status: 'disabled'disabledReason: undefined 且不在设置禁用列表中 → 以 reason: 'inactive_extension' 拒绝。
  2. 相同设置但技能存在于工作区 skills.disabled 中 → 允许切换继续(由设置禁用,而非未激活扩展)。

Issue 级审查发现

@wenshao 发现 #2SettingScope 字符串强转 — 已实现

skill-settings.ts 中的三个字符串强转常量替换为 SettingScope 枚举的值导入和直接成员引用(SettingScope.SystemDefaults 等),获得编译时作用域字面量验证。

@wenshao 发现 #1(对话框确认测试)、#3(过期 enabled 条目)、#4(性能说明) — 延后处理

  • 发现 pre-release: fix ci #1(SkillsManagerDialog 保存处理测试):对话框的 handleConfirm 路径需要 Ink/React 组件测试基础设施,该组件尚未搭建。底层逻辑已由 updateWorkspaceSkillSettingLists 单元测试和新的守护进程双键测试覆盖。延后至后续 PR。
  • 发现 如何自定义密钥文件 .env可能与其他文件冲突 #3(过期 skills.enabled 条目):设计上无害 — enabled 仅对 defaultDisabled 有意义。无需操作。
  • 发现 Are you interested in AI Terminal? #4(解析器权重):已知悉,实际中可忽略(每次技能验证调用一次,非紧密循环)。

验证结果

  • npm run build
  • npm run typecheck
  • npm run lint
  • vitest run src/config/skill-settings.test.ts — 7 通过 ✅
  • vitest run src/serve/run-qwen-serve.test.ts -t "workspace skill settings persistence" — 2 通过 ✅
  • vitest run src/serve/workspace-service/__tests__/facade.test.ts — 89 通过 ✅

Base-conflict check: no conflict with main.

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human.


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

@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. 1 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here.

— qwen3.7-max via Qwen Code /review

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point:

No action taken

Review feedback: The automated reviewer (qwen-code-ci-bot) reported 1 Suggestion-level finding that could not be anchored to a changed line and was dropped. No inline comments or issue-level comments require a response.

CI failure (Test (ubuntu-latest Node 22.x): FAILURE): Verified as pre-existing on main. The failing tests are in src/config/settings.test.ts (12 failures) and src/config/config.test.ts (5 failures on this branch; 220 failures on main's code). Reproduced by checking out origin/main's config.ts and settingsSchema.ts and running the same test files — identical or worse failures occur without this PR's changes. These are environment-sensitive tests (filesystem mocking of home directories, .env resolution, sandbox image resolution) that fail in this sandbox environment regardless of the branch.

All PR-specific tests pass:

  • src/config/skill-settings.test.ts — 7/7 ✓
  • src/config/config.integration.test.ts — 26/26 ✓
  • src/serve/run-qwen-serve.test.ts — 190/190 ✓
  • src/serve/workspace-skills-mapping.test.ts — ✓
  • src/serve/workspace-skills-status.test.ts — ✓
  • src/serve/workspace-service/__tests__/facade.test.ts — 89/89 ✓
  • src/acp-integration/ — 978/978 ✓
  • packages/acp-bridge — 858/858 ✓

Other checks pass locally: npm run typecheck ✓, npm run lint ✓, Prettier ✓, settings schema up-to-date ✓, npm run check-i18n ✓.

中文说明

未采取行动

审查反馈: 自动审查机器人 (qwen-code-ci-bot) 报告了 1 个建议级别的发现,但无法锚定到已更改的行,因此被丢弃。没有需要回复的行内评论或 issue 级别评论。

CI 失败 (Test (ubuntu-latest Node 22.x): FAILURE): 已验证为 main 分支上的既有问题。失败的测试位于 src/config/settings.test.ts(12 个失败)和 src/config/config.test.ts(本分支 5 个失败;main 代码上 220 个失败)。通过检出 origin/mainconfig.tssettingsSchema.ts 并运行相同的测试文件进行复现——在没有本 PR 更改的情况下出现相同或更严重的失败。这些是对环境敏感的测试(主目录的文件系统模拟、.env 解析、沙箱镜像解析),无论分支如何,在此沙箱环境中都会失败。

所有 PR 相关测试均通过:

  • src/config/skill-settings.test.ts — 7/7 ✓
  • src/config/config.integration.test.ts — 26/26 ✓
  • src/serve/run-qwen-serve.test.ts — 190/190 ✓
  • src/serve/workspace-skills-mapping.test.ts — ✓
  • src/serve/workspace-skills-status.test.ts — ✓
  • src/serve/workspace-service/__tests__/facade.test.ts — 89/89 ✓
  • src/acp-integration/ — 978/978 ✓
  • packages/acp-bridge — 858/858 ✓

其他检查在本地通过: npm run typecheck ✓、npm run lint ✓、Prettier ✓、设置 schema 为最新 ✓、npm run check-i18n ✓。

Base-conflict check: no conflict with main.


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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Could not address the latest feedback automatically (round 2/100). A human should take over this PR.

What I found before stopping:
Qwen failed during address-review: timeout (3000000ms).

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

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


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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. Suggestions are inline.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/ui/components/skills/SkillsManagerDialog.tsx Outdated
Comment thread packages/cli/src/serve/workspace-skills-status.test.ts
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Could not address the latest feedback automatically (round 3/100). A human should take over this PR.

What I found before stopping:
Qwen failed during address-review: timeout (3000000ms).

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

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


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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 6/100). What changed, and what I pushed back on:

Review feedback addressed

[rc:3628065665] Safe-mode status/execution disagreement — Implemented

Finding: isSafeModeEnv() returned an empty disablements Map in the daemon-local skills status path (workspace-skills-status.ts), but the execution-time path (buildDisabledSkillNamesProvider in config.ts) resolves skills.disabled unconditionally — even in safe mode. This caused a bundled skill listed in skills.disabled to appear as status: 'ok' in the /workspace/skills response while invocation would fail.

Decision: Required — real status/execution disagreement affecting clients that rely on the status API (e.g. Web Shell skill picker).

Change: Removed the isSafeModeEnv() guard in buildWorkspaceSkillsStatus so resolveSkillSettings is always called, matching the execution-time behavior. The isSafeModeEnv import is retained — it is still used for the SkillManager config shim's isSafeMode property (which controls bundled-only listing, a separate concern from disablements).

Test: Added 'resolves disablements in safe mode (status matches execution)' — sets QWEN_CODE_SAFE_MODE=1, creates a workspace with skills.disabled: ['blocked'], and verifies the status API reports the skill as disabled with disabledReason: 'hard'.

Conflict notes

No conflicts (--conflict false).

Verification

  • npm run build
  • npm run typecheck
  • npm run lint
  • npx vitest run src/serve/workspace-skills-status.test.ts — 7/7 passed ✅
  • npx vitest run src/serve/workspace-skills-mapping.test.ts src/config/skill-settings.test.ts — 17/17 passed ✅
中文说明

已处理的评审反馈

[rc:3628065665] 安全模式下状态/执行不一致 — 已实现

发现: isSafeModeEnv() 在守护进程本地技能状态路径(workspace-skills-status.ts)中返回空的 disablements Map,但执行时路径(config.ts 中的 buildDisabledSkillNamesProvider)无条件地解析 skills.disabled——即使在安全模式下也是如此。这导致在 skills.disabled 中列出的内置技能在 /workspace/skills 响应中显示为 status: 'ok',但实际调用会失败。

决定: 必须修复——存在真实的状态/执行不一致,影响依赖状态 API 的客户端(如 Web Shell 技能选择器)。

变更: 移除了 buildWorkspaceSkillsStatus 中的 isSafeModeEnv() 守卫,使 resolveSkillSettings 始终被调用,与执行时行为保持一致。isSafeModeEnv 导入保留——它仍用于 SkillManager 配置 shim 的 isSafeMode 属性(控制仅列出内置技能,与禁用是不同的关注点)。

测试: 添加了 'resolves disablements in safe mode (status matches execution)' 测试——设置 QWEN_CODE_SAFE_MODE=1,创建一个包含 skills.disabled: ['blocked'] 的工作区,验证状态 API 将该技能报告为 disableddisabledReason'hard'

冲突说明

无冲突(--conflict false)。

验证结果

  • npm run build
  • npm run typecheck
  • npm run lint
  • npx vitest run src/serve/workspace-skills-status.test.ts — 7/7 通过 ✅
  • npx vitest run src/serve/workspace-skills-mapping.test.ts src/config/skill-settings.test.ts — 17/17 通过 ✅

Base-conflict check: no conflict with main.

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human.


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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point:

Autofix review triage — PR #7357

Outcome: no action taken.

Feedback triage

No review comments were found to triage — no inline comments, issue-level comments, or review bodies from maintainers or the automated reviewer.

Failed checks investigation

Test (ubuntu-latest Node 22.x): FAILURE

All tests touched by this PR pass locally:

Test file Result
src/config/skill-settings.test.ts (11 tests) ✅ pass
src/config/config.integration.test.ts (26 tests) ✅ pass
src/config/settingsSchema.test.ts (32 tests) ✅ pass
src/serve/run-qwen-serve.test.ts (191 tests) ✅ pass
src/serve/workspace-skills-mapping.test.ts (6 tests) ✅ pass
src/serve/workspace-skills-status.test.ts (7 tests) ✅ pass
src/serve/workspace-service/__tests__/facade.test.ts (89 tests) ✅ pass
src/acp-integration/acpAgent.test.ts (299 tests) ✅ pass
src/ui/commands/skillsCommand.test.ts (10 tests) ✅ pass

Full package test suites for packages/acp-bridge (893 tests) and packages/sdk-typescript (1403 tests) also pass.

The only local failures found are in settings.test.ts (12 tests) and config.test.ts (5 tests) — these are pre-existing environment-specific failures (home-directory and sandbox-image resolution) that reproduce identically on the unmodified working tree. The PR does not modify settings.ts, settings.test.ts, config.test.ts, or any sandbox-related file.

Additional local verification:

  • npm run build
  • npm run typecheck
  • npm run lint
  • npm run generate:settings-schema — no diff (schema up-to-date) ✅
  • Prettier formatting on all changed files ✅
  • npm run check-i18n

Without access to CI logs, the exact failing test in the CI "Test" check cannot be identified, but no evidence links the failure to this PR's changes.

review-pr: FAILURE

The automated code review check failed but generated no review comments or findings to address.

中文说明

Autofix 审查分类 — PR #7357

结果:未采取任何操作。

反馈分类

未发现需要分类的审查评论——没有行内评论、issue 级别评论或来自维护者或自动审查器的审查正文。

失败检查调查

Test (ubuntu-latest Node 22.x): FAILURE

本 PR 涉及的所有测试在本地均通过:

测试文件 结果
src/config/skill-settings.test.ts(11 个测试) ✅ 通过
src/config/config.integration.test.ts(26 个测试) ✅ 通过
src/config/settingsSchema.test.ts(32 个测试) ✅ 通过
src/serve/run-qwen-serve.test.ts(191 个测试) ✅ 通过
src/serve/workspace-skills-mapping.test.ts(6 个测试) ✅ 通过
src/serve/workspace-skills-status.test.ts(7 个测试) ✅ 通过
src/serve/workspace-service/__tests__/facade.test.ts(89 个测试) ✅ 通过
src/acp-integration/acpAgent.test.ts(299 个测试) ✅ 通过
src/ui/commands/skillsCommand.test.ts(10 个测试) ✅ 通过

packages/acp-bridge(893 个测试)和 packages/sdk-typescript(1403 个测试)的完整包测试套件也全部通过。

本地发现的唯一失败在 settings.test.ts(12 个测试)和 config.test.ts(5 个测试)中——这些是预先存在的环境特定失败(主目录和沙箱镜像解析),在未修改的工作树上完全相同地复现。本 PR 未修改 settings.tssettings.test.tsconfig.test.ts 或任何沙箱相关文件。

其他本地验证:

  • npm run build
  • npm run typecheck
  • npm run lint
  • npm run generate:settings-schema — 无差异(schema 已是最新) ✅
  • 所有变更文件的 Prettier 格式 ✅
  • npm run check-i18n

由于无法访问 CI 日志,无法确定 CI "Test" 检查中具体失败的测试,但没有证据将该失败与本 PR 的变更相关联。

review-pr: FAILURE

自动代码审查检查失败,但未生成任何需要处理的审查评论或发现。

Base-conflict check: no conflict with main.


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

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

Architecture Review — PR #7357

Verdict: ✅ Approve — clean three-tier skill availability model.

Design pattern: Three-tier precedence with disablement tracking

skills.disabled  (hard denylist — always wins)
  > skills.enabled  (explicit opt-in — overrides defaultDisabled)
    > skills.defaultDisabled  (soft default — project can opt in)

The key insight: skills.disabled is a policy mechanism (operator lock), while skills.defaultDisabled is a default mechanism (starts off, available for opt-in). Conflating them (the old model) made it impossible for a project to enable a skill the user had soft-disabled.

Key design decisions

  1. Centralized resolver: resolveSkillSettings() is the single source of truth for effective skill availability. It replaces 4+ scattered inline normalization blocks (config.ts, run-qwen-serve.ts, workspace-service/index.ts, workspace-skills-status.ts) with one function. Every consumer reads through the same lens.

  2. Disablement reason + lock scope: disabledReason: 'hard' | 'default' | 'inactive_extension' + lockedScope — clients can distinguish WHY a skill is disabled and WHETHER it can be overridden. This is what makes the UI toggle correctly gray out locked skills while allowing opt-in for default-disabled ones.

  3. Orphan preservation: Workspace disables for skills not currently loaded (different git branch, uninstalled extension, deleted skills dir) are preserved verbatim. The picker only mutates currently-loaded skills. This is load-bearing — without it, opening /skills on a different branch would silently drop the user's prior disable settings.

  4. Canonical name persistence: Case-insensitive matching but canonical name persisted. updateTarget() collapses case variants and duplicates in one pass.

  5. Batch settings mutation: setValues() for atomic multi-key updates — when enabling a workspace-hard-disabled default-disabled skill, both skills.disabled (remove) and skills.enabled (add opt-in) change in one write.

Test coverage

228-line skill-settings.test.ts covers the resolver, the workspace list updater, and the batch update computer. The orphan preservation case is explicitly pinned. The integration test in run-qwen-serve.test.ts verifies the dual-write path end-to-end.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point:

Autofix review pass — no code changes

The new feedback on this PR is a single architecture review that approves
the change (verdict: ✅ Approve, "clean three-tier skill availability model").
It raises no defects and contains no inline comments, so there is nothing to
implement or decline this round.

Still-red checks were investigated

  • Test (ubuntu-latest Node 22.x) — I could not fetch the CI log (this
    flow has no GitHub credentials), so I reproduced locally instead. Every test
    file touched by this PR passes, and npm run build, npm run typecheck, and
    npm run lint all pass; the generated settings schema is also up-to-date.
    Running the full packages/cli src/config + src/serve suites surfaced 17
    failures in settings.test.ts and config.test.ts (environment-variable
    resolution, sandbox-image resolution, and home-directory workspace handling).
    I checked out origin/main and confirmed the exact same 17 tests fail on
    the base branch
    in this environment. Those files and the code they exercise
    are untouched by this PR, so these are pre-existing, environment-specific
    failures — not regressions introduced here. Without the CI log there is no
    evidence-based code hypothesis for the remote Test failure to act on; the
    workflow's independent CI remains the final verification gate.
  • review-pr — this is the automated reviewer's own check status. No
    automated-reviewer findings are listed in this feedback round, so there is
    nothing to address.

No commits were made and the working tree is unchanged.

中文说明

Autofix 复查结论 —— 无代码改动

本轮针对该 PR 的新反馈是一条批准该改动的架构评审(结论:✅ Approve,"清晰的三层 skill 可用性模型")。它没有指出任何缺陷,也没有行内评论,因此本轮没有需要实现或拒绝的内容。

已排查仍处于失败状态的检查项

  • Test (ubuntu-latest Node 22.x) —— 我无法获取 CI 日志(此流程没有 GitHub 凭据),因此改为在本地复现。本 PR 触及的每个测试文件都通过,npm run buildnpm run typechecknpm run lint 全部通过;生成的 settings schema 也是最新的。运行 packages/clisrc/config + src/serve 完整测试套件时,在 settings.test.tsconfig.test.ts 中出现了 17 个失败(环境变量解析、sandbox 镜像解析、以及主目录作为 workspace 的处理)。我检出了 origin/main,确认完全相同的这 17 个测试在基线分支上于本环境中同样失败。这些文件及其所测试的代码均未被本 PR 改动,因此它们是既有的、与环境相关的失败,而非本次改动引入的回归。在没有 CI 日志的情况下,没有基于证据的代码层面假设可供处理远端 Test 失败;工作流独立的 CI 仍是最终的验证关口。
  • review-pr —— 这是自动化评审器自身的检查状态。本轮反馈中没有列出任何来自自动化评审器的发现项,因此没有需要处理的内容。

未创建任何提交,工作树保持不变。

Base-conflict check: no conflict with main.


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

@wenshao

wenshao commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Local verification of #7357 — overridable default-disabled skills

I built this PR from source and verified it end-to-end on my machine. Feature behavior is correct and well-covered, but there is one required-CI merge blocker with a root cause and a verified fix below.

Environment: PR HEAD 419611ef, bundled qwen CLI v0.20.1 built from this branch, Node 22.22, Linux. No model requests are involved — /skills is a settings-only flow.

TL;DR

  • Behavior is correct — verified against real settings.json files across scopes, a real SkillManager scanning a real SKILL.md, and the real bundled CLI driven in a terminal (screenshots below).
  • All focused suites pass — 330 tests across the 6 touched files, plus 4 real-file integration tests I added to cover what the PR's own (fake-settings) tests don't.
  • ⚠️ Merge blocker: the required job Test (ubuntu-latest, Node 22.x) → step Check serve fast-path bundle closure fails (CI run). Root cause pinpointed + fix verified below (merge-base passes, this PR fails, fix makes it pass again).

1. Behavior — real /skills picker (bundled CLI)

Scenario from the reviewer test plan: user-scope skills.defaultDisabled = ["Opt-In-Skill"] (mixed case) + a real project skill opt-in-skill.

/skills opt-in before/after

  • opt-in-skill renders [ ] unchecked (soft-disabled by default); the 9 bundled skills stay [x] — so the unchecked state is genuinely driven by defaultDisabled, matched case-insensitively.
  • ② Space → [x] checked; ③ Esc → ● Skills configuration saved.
  • Persisted workspace .qwen/settings.json = {"skills":{"enabled":["opt-in-skill"]}} — the canonical skill name, and no skills.disabled written. ✔️

A hard disable at a higher scope still wins and is shown as locked:

/skills locked

With user-scope skills.disabled = ["Opt-In-Skill"], opt-in-skill moves to “Locked by higher-scope settings” with [locked: User] and cannot be toggled at workspace scope. ✔️

2. Tests

Suite Result
config/skill-settings.test.ts
config/config.integration.test.ts
serve/workspace-skills-status.test.ts (real-file daemon status)
serve/workspace-skills-mapping.test.ts
serve/workspace-service/__tests__/facade.test.ts
serve/run-qwen-serve.test.ts ✅ 191
Total 330 passed

I also added two real-file integration checks, because the PR's precedence tests use a fake LoadedSettings and never exercise the real settings.json merge:

  • Real multi-scope merge: wrote real system-defaults / user / workspace settings, ran the real loadSettings + resolveSkillSettings. Confirms defaultDisabled is UNION-merged across scopes, disabled > enabled > defaultDisabled precedence holds, case-insensitive canonicalisation works, and lockedScope is reported (user / systemDefaults). ✅
  • Real /skills render + persist: rendered the actual SkillsManagerDialog against a real SkillManager and asserted the exact bytes written back. ✅

3. ⚠️ Merge blocker — serve fast-path bundle closure

Root cause. run-qwen-serve.ts is a serve pre-listen root, and the PR makes it statically import ../config/skill-settings.js:

// run-qwen-serve.ts (top level)
import {
  resolveSkillSettings,
  skillSettingStrings,
  updateWorkspaceSkillSettingLists,
} from '../config/skill-settings.js';

skill-settings.js statically imports settings.js, which drags @iarna/toml and glob into the daemon cold-start closure. scripts/check-serve-fast-path-bundle.js forbids exactly those from the pre-listen path:

Serve fast-path bundle closure includes pre-listen runtime modules:
- glob vendor package        static path: run-qwen-serve-*.js -> chunk-*.js
- @iarna/toml vendor package static path: run-qwen-serve-*.js -> chunk-*.js

Negative control (so this is the PR, not the environment): with the identical local build method, the merge-base 962f9e0 passes (Startup bundle closure checks passed.) and this PR fails. This matches CI.

Those three symbols are used only inside persistDisabledSkillsFn — a post-listen skill-toggle handler (inside withSettingsLock) — so a dynamic import is safe and mirrors how the sibling workspace-service / workspace-skills-status modules are already dynamically imported in this file.

Verified fix (rebuilt → Startup bundle closure checks passed., and all 191 run-qwen-serve tests still green):

-import {
-  resolveSkillSettings,
-  skillSettingStrings,
-  updateWorkspaceSkillSettingLists,
-} from '../config/skill-settings.js';
@@ persistDisabledSkillsFn … withSettingsLock(workspace, async () => {
+        const {
+          resolveSkillSettings,
+          skillSettingStrings,
+          updateWorkspaceSkillSettingLists,
+        } = await import('../config/skill-settings.js');
         const fresh = settingsRuntime.settings.loadSettings(workspace);

Note: the ACP side of the same gate is fine — acpAgent.ts's new resolveSkillSettings / loadSettings imports do not pull ink/react into the ACP boundary; that half passes.

Recommendation

The feature itself is solid and matches the described precedence model — I'd be happy to merge once the serve fast-path gate is green. Please apply the dynamic-import fix above (or equivalent) so run-qwen-serve.ts's cold-start closure no longer statically pulls in skill-settings.js → settings.js.

中文说明

本地验证 #7357 —— 可覆盖的“默认禁用”技能

我从源码构建并在本地做了端到端验证。功能行为正确、测试覆盖充分,但有一个必过 CI 的合并阻塞项,下面给出根因和已验证的修复。

环境: PR HEAD 419611ef,从该分支构建的打包 CLI v0.20.1,Node 22.22,Linux。全程不涉及模型请求 —— /skills 是纯设置流程。

结论速览

  • 行为正确 —— 用真实的多作用域 settings.json、真实 SkillManager 扫描真实 SKILL.md、以及真实打包 CLI 在终端里实际操作验证(见下方截图)。
  • 聚焦测试全过 —— 6 个改动文件共 330 个测试通过;我另外补了 4 个真实文件集成测试,覆盖 PR 自带测试(用假的 settings)没覆盖到的部分。
  • ⚠️ 合并阻塞: 必过任务 Test (ubuntu-latest, Node 22.x) → 步骤 Check serve fast-path bundle closure 失败(CI 记录)。已定位根因并验证修复(merge-base 通过、本 PR 失败、修复后重新通过)。

1. 行为 —— 真实 /skills 选择器(打包 CLI)

按 reviewer 测试计划:用户级 skills.defaultDisabled = ["Opt-In-Skill"](大小写混合)+ 真实项目技能 opt-in-skill

(见上方第一张截图)

  • opt-in-skill 显示为 [ ] 未选中(默认软禁用);9 个内置技能仍是 [x] —— 说明未选中确实由 defaultDisabled 驱动,且大小写不敏感匹配。
  • ② 空格 → [x] 选中;③ Esc → ● Skills configuration saved.
  • 落盘的工作区 .qwen/settings.json = {"skills":{"enabled":["opt-in-skill"]}} —— 写入的是规范名称,且不会skills.disabled。✔️

高作用域的硬禁用仍然优先,并显示为锁定(见上方第二张截图):用户级 skills.disabled = ["Opt-In-Skill"] 时,opt-in-skill 进入 “Locked by higher-scope settings”,标注 [locked: User],在工作区作用域无法切换。✔️

2. 测试

6 个改动文件共 330 个测试通过(其中 run-qwen-serve.test.ts 191 个)。
另外我补了两个真实文件集成测试,因为 PR 自带的优先级测试用的是假的 LoadedSettings,从未走真实的 settings.json 合并:

  • 真实多作用域合并: 真实写入 system-defaults / user / workspace 设置,跑真实的 loadSettings + resolveSkillSettings,确认 defaultDisabled 跨作用域 UNION 合并、disabled > enabled > defaultDisabled 优先级成立、大小写规范化正确、lockedScopeuser / systemDefaults)上报正确。✅
  • 真实 /skills 渲染 + 落盘: 用真实 SkillManager 渲染真实的 SkillsManagerDialog,断言回写的确切内容。✅

3. ⚠️ 合并阻塞 —— serve fast-path 打包闭包

根因。 run-qwen-serve.ts 是 serve 的 pre-listen 根,本 PR 让它静态 import ../config/skill-settings.js;而 skill-settings.js 静态 import settings.js,从而把 @iarna/tomlglob 拉进了守护进程冷启动闭包。scripts/check-serve-fast-path-bundle.js 恰好禁止这些模块出现在 pre-listen 路径里。

反向对照(证明是 PR 而非环境问题): 用完全相同的本地构建方法,merge-base 962f9e0 通过本 PR 失败,与 CI 一致。

这三个符号只在 persistDisabledSkillsFnlisten 之后的技能切换处理函数,位于 withSettingsLock 内)里用到,所以改成动态 import 是安全的,也与本文件里 workspace-service / workspace-skills-status 已经采用的动态 import 一致。

已验证的修复(重新打包后 → Startup bundle closure checks passed.,且 191 个 run-qwen-serve 测试仍全过):把顶层静态 import 删掉,改为在 persistDisabledSkillsFnawait import('../config/skill-settings.js')(diff 见上方英文部分)。

补充:同一 gate 的 ACP 部分没问题 —— acpAgent.ts 新增的 resolveSkillSettings / loadSettings import 不会把 ink/react 拉进 ACP 边界,那一半是通过的。

建议

功能本身没问题、与描述的优先级模型一致 —— serve fast-path gate 变绿后我很乐意合入。请应用上面的动态 import 修复(或等价方案),让 run-qwen-serve.ts 的冷启动闭包不再静态拉入 skill-settings.js → settings.js

🤖 Verified locally with Claude Code (Opus 4.8): source build of the bundled CLI, real /skills run in a terminal, real multi-scope settings merge, and a merge-base negative control reproducing the CI gate.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 7/100). What changed, and what I pushed back on:

Review feedback addressed

Thanks to @wenshao for the thorough local verification. One required merge
blocker was raised; the remaining points were positive confirmations of the
feature behavior and test coverage that need no code change.

1. Required — serve fast-path bundle closure failure (merge blocker)

Finding. The required CI job Test (ubuntu-latest, Node 22.x) → step
Check serve fast-path bundle closure fails on this PR. Root cause:
run-qwen-serve.ts (a serve pre-listen root) statically imported
../config/skill-settings.js, which statically imports settings.js and drags
@iarna/toml and glob into the daemon cold-start closure — both forbidden
from the pre-listen path by scripts/check-serve-fast-path-bundle.js. The
reviewer confirmed the merge-base passes and this PR fails the same gate.

Decision: fixed. The three symbols (resolveSkillSettings,
skillSettingStrings, updateWorkspaceSkillSettingLists) are used only inside
persistDisabledSkillsFn, a post-listen skill-toggle handler that already runs
inside withSettingsLock(workspace, async () => { … }). Converted the top-level
static import into a dynamic await import('../config/skill-settings.js') at
the top of that async callback. This mirrors how the sibling
workspace-service / workspace-skills-status modules are already dynamically
imported in this file, and removes skill-settings.js → settings.js (and thus
@iarna/toml / glob) from the cold-start closure.

Change (packages/cli/src/serve/run-qwen-serve.ts, 5 insertions /
5 deletions):

-import {
-  resolveSkillSettings,
-  skillSettingStrings,
-  updateWorkspaceSkillSettingLists,
-} from '../config/skill-settings.js';
@@ persistDisabledSkillsFn … withSettingsLock(workspace, async () => {
+        const {
+          resolveSkillSettings,
+          skillSettingStrings,
+          updateWorkspaceSkillSettingLists,
+        } = await import('../config/skill-settings.js');
         const fresh = settingsRuntime.settings.loadSettings(workspace);

2. Informational — feature behavior and tests confirmed correct

The reviewer verified the real /skills picker (bundled CLI), multi-scope
settings precedence (disabled > enabled > defaultDisabled, case-insensitive
canonicalisation, lockedScope reporting), and 330 focused tests passing. No
action required; this change does not alter that behavior — the dynamic import
is behavior-preserving (same module, same symbols, awaited before first use).

Conflict notes

--conflict false; no merge of origin/main was performed.

Verification

Commands actually run on this branch after the change:

  • npm run build — passed
  • npm run typecheck — passed
  • npx eslint packages/cli/src/serve/run-qwen-serve.ts — passed (no findings)
  • npx vitest run src/serve/run-qwen-serve.test.ts (touched package) — 191 passed
  • npm run check:serve-fast-path-bundle (clean + build --cli-only + DEV=true bundle + scripts/check-serve-fast-path-bundle.js, the exact CI gate that was failing) — Startup bundle closure checks passed.
中文说明

已处理的评审反馈

感谢 @wenshao 详尽的本地验证。评审提出了一项必过的合并阻塞项;其余各点均为对功能行为与测试覆盖的正面确认,无需改动代码。

1. 必过项 —— serve fast-path 打包闭包失败(合并阻塞)

问题。 必过 CI 任务 Test (ubuntu-latest, Node 22.x) → 步骤 Check serve fast-path bundle closure 在本 PR 上失败。根因:run-qwen-serve.ts(serve 的 pre-listen 根)静态 import 了 ../config/skill-settings.js,而后者又静态 import settings.js,从而把 @iarna/tomlglob 拖进守护进程冷启动闭包 —— 这两者正是 scripts/check-serve-fast-path-bundle.js 在 pre-listen 路径里所禁止的。评审已确认 merge-base 通过、本 PR 在同一 gate 上失败。

处理:已修复。 这三个符号(resolveSkillSettingsskillSettingStringsupdateWorkspaceSkillSettingLists)仅在 persistDisabledSkillsFn 内使用,该函数是 listen 之后的技能切换处理函数,且本就运行在 withSettingsLock(workspace, async () => { … }) 内。将顶层的静态 import 改为在该 async 回调开头使用动态 await import('../config/skill-settings.js')。这与本文件中 workspace-service / workspace-skills-status 等相邻模块已采用的动态 import 方式一致,并把 skill-settings.js → settings.js(以及 @iarna/toml / glob)从冷启动闭包中移除。

改动packages/cli/src/serve/run-qwen-serve.ts,5 行新增 / 5 行删除):见上方英文部分的 diff。

2. 信息性 —— 功能行为与测试已确认正确

评审验证了真实 /skills 选择器(打包 CLI)、多作用域设置优先级(disabled > enabled > defaultDisabled、大小写不敏感规范化、lockedScope 上报),以及 330 个聚焦测试全部通过。无需处理;本次改动不改变该行为 —— 动态 import 保持行为一致(同一模块、同一符号,并在首次使用前 await)。

冲突说明

--conflict false;未执行 origin/main 的合并。

验证

改动后在本分支实际运行的命令:

  • npm run build —— 通过
  • npm run typecheck —— 通过
  • npx eslint packages/cli/src/serve/run-qwen-serve.ts —— 通过(无问题)
  • npx vitest run src/serve/run-qwen-serve.test.ts(改动所在包)—— 191 个通过
  • npm run check:serve-fast-path-bundle(clean + build --cli-only + DEV=true bundle + scripts/check-serve-fast-path-bundle.js,即此前失败的 CI gate 本身)—— Startup bundle closure checks passed.

Base-conflict check: no conflict with main.

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human.


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

…ault-disabled

# Conflicts:
#	packages/cli/src/config/settingsSchema.ts
#	packages/vscode-ide-companion/schemas/settings.schema.json
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 8/100). What changed, and what I pushed back on:

Address Review Summary — PR #7357

Feedback points

No review feedback (reviews, inline comments, issue-level comments, or failed checks) was present in this round.

Conflict resolution

Merged origin/main (19 commits since last merge) into the PR branch. Two files had content conflicts:

  1. packages/cli/src/config/settingsSchema.ts — Our branch added skills.defaultDisabled and skills.enabled properties; main added skills.directories (from feat(cli): support custom skill directories via settings #7395). Resolution: kept all three properties in order (disableddefaultDisabledenableddirectories), each with its own showInDialog and mergeStrategy fields.

  2. packages/vscode-ide-companion/schemas/settings.schema.json — Same conflict in the generated JSON schema. Resolution: kept all three property entries. Regenerated via npm run generate:settings-schema and confirmed the output matches the manual resolution exactly (zero diff).

No semantic conflicts: the directories feature (custom skill directory scanning in skill-manager.ts) is orthogonal to our defaultDisabled/enabled logic (skill enable/disable resolution in skill-settings.ts).

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npm run generate:settings-schema — passed, regenerated schema matches manual resolution (zero diff)
  • vitest packages/cli (touched: skill-settings, config.integration, run-qwen-serve, workspace-skills-mapping, workspace-skills-status, facade) — 6 files, 330 tests passed
中文说明

处理审查总结 — PR #7357

反馈要点

本轮没有审查反馈(无评审意见、行内评论、issue 级别评论或失败的检查)。

冲突解决

origin/main(自上次合并以来的 19 个提交)合并到 PR 分支。两个文件存在内容冲突:

  1. packages/cli/src/config/settingsSchema.ts — 我们的分支添加了 skills.defaultDisabledskills.enabled 属性;main 分支添加了 skills.directories(来自 feat(cli): support custom skill directories via settings #7395)。解决方式:按顺序保留全部三个属性(disableddefaultDisabledenableddirectories),每个属性都有各自的 showInDialogmergeStrategy 字段。

  2. packages/vscode-ide-companion/schemas/settings.schema.json — 生成的 JSON schema 中存在相同冲突。解决方式:保留全部三个属性条目。通过 npm run generate:settings-schema 重新生成,确认输出与手动解决结果完全一致(零差异)。

无语义冲突:directories 功能(skill-manager.ts 中的自定义技能目录扫描)与我们的 defaultDisabled/enabled 逻辑(skill-settings.ts 中的技能启用/禁用解析)完全正交。

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • npm run generate:settings-schema — 通过,重新生成的 schema 与手动解决结果一致(零差异)
  • vitest packages/cli(涉及:skill-settings、config.integration、run-qwen-serve、workspace-skills-mapping、workspace-skills-status、facade)— 6 个文件,330 个测试通过

Base-conflict check: conflicted with main — resolved in this push.

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human.


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

@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 issues found. LGTM! ✅

— qwen3.7-max via Qwen Code /review

@ZijianZhang989 ZijianZhang989 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. 2 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here.

— qwen3.7-max via Qwen Code /review

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix Review Response — PR #7357

No changes made. The latest review round contains no actionable feedback:

  • Review (@ZijianZhang989): COMMENTED — no blockers. Two Suggestion-level findings could not be anchored to a changed line and were dropped.
  • Inline comments: none
  • Issue-level comments: none
  • Failed checks: none
  • Conflicts: none

No code changes are required for this round.

中文说明

Autofix 审查回应 — PR #7357

未做任何更改。最新一轮审查没有需要处理的反馈:

  • 审查@ZijianZhang989):COMMENTED — 无阻塞项。两条 Suggestion 级别的发现因无法锚定到已更改的代码行而被丢弃。
  • 行内评论:无
  • Issue 级别评论:无
  • 失败的检查:无
  • 冲突:无

本轮无需进行任何代码更改。

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


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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix review summary (PR #7357)

No review feedback needed addressing this round: the feedback batch contained no
reviews, inline comments, issue-level comments, or failed checks newer than the
last evaluation. The work for this round was resolving the merge conflict with
origin/main (--conflict true).

Conflict resolution

origin/main advanced well past the branch's last merge base. Five files in the
serve/skills area conflicted because this PR refactored skill toggling into a new
skill-settings module (dual skills.disabled / skills.enabled lists) while
origin/main independently added a workspace-generation guard
(assertGenerationOpen) and a trust-aware settings loader
(loadSettingsForPersistence / loadBoundSettings). Each conflict was resolved
by keeping the PR's skill-settings logic and adopting origin/main's
generation-guard and trust-awareness, rather than taking one side:

  • packages/cli/src/serve/run-qwen-serve.tspersistDisabledSkillsFn keeps
    the PR's resolveSkillSettings / updateWorkspaceSkillSettingLists /
    setValues flow, now loading via loadSettingsForPersistence(workspace),
    asserting assertGenerationOpen?.() early and before commit, and passing
    assertGenerationOpen as the setValues commit guard.
  • packages/cli/src/serve/workspace-service/index.ts — keeps the PR's
    disabledBySettings (via resolveSkillSettings(...).disabledNames) inactive
    extension check and settingsChanges event loop, routes the settings read
    through origin/main's trust-aware loadBoundSettings(true), and adds
    assertActiveGeneration() before publishing settings events.
  • packages/cli/src/serve/workspace-skills-status.ts — keeps the PR's
    resolveSkillSettings(...).disablements (required by mapSkillConfigToStatus)
    with origin/main's trust-aware load options; origin/main's now-unused
    readDisabledSkillNames helper was dropped.
  • packages/cli/src/serve/run-qwen-serve.test.ts — kept the PR's dual-list
    assertions and preserved origin/main's generation-guard test, adapted to the
    PR implementation: skills persist via setValues(writes, onScopeCommitted, assertCanCommit) (guard is the 3rd arg) and tools via setValue(scope, key, value, assertCanCommit) (guard is the 4th arg). The locked-skill rejection
    and ['orphan','alpha','beta'] assertion already live in the first test.
  • packages/cli/src/serve/workspace-skills-status.test.ts — the PR and
    origin/main each added different new tests at the same location; both sets were
    kept (PR: hard-disable reason + safe-mode disablement; origin/main: untrusted
    workspace skip + corruption-env safety).

One auto-merged test needed a follow-up fix: workspace-service/__tests__/facade.test.ts
asserted persistDisabledSkills was called with 3 arguments, but the merged
source now passes the 4th assertGenerationOpen argument; added the trailing
undefined to match origin/main's convention for the same assertion.

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • vitest run src/serve/run-qwen-serve.test.ts (touched) — 203 passed
  • vitest run src/serve/workspace-skills-status.test.ts src/serve/workspace-skills-mapping.test.ts src/config/skill-settings.test.ts src/serve/workspace-service/__tests__/facade.test.ts (touched) — passed (facade 92 passed after the 4th-arg fix)
  • vitest run src/serve src/config — 155 files passed; src/config/settings.test.ts (12) and src/config/config.test.ts (5) failed only because of sandbox environment variables (QWEN_HOME, SANDBOX, QWEN_CODE_CUSTOM_SANDBOX_IMAGE, QWEN_CODE_SESSION_ID) in files this change does not touch. Proven environmental: with those vars unset, config.test.ts passes 300/300 and settings.test.ts passes fully. These pass in a clean CI environment.
中文说明

Autofix 审查总结(PR #7357

本轮没有需要处理的审查反馈:反馈批次中不包含任何比上次评估更新的审查、行内评论、issue 级评论或失败检查。本轮的工作是解决与 origin/main 的合并冲突(--conflict true)。

冲突解决

origin/main 已远超本分支上次的合并基点。serve/skills 区域有五个文件发生冲突,原因是本 PR 将技能开关重构为新的 skill-settings 模块(双列表 skills.disabled / skills.enabled),而 origin/main 独立地新增了工作区生成守卫(assertGenerationOpen)和信任感知的设置加载器(loadSettingsForPersistence / loadBoundSettings)。每个冲突的解决方式都是保留 PR 的 skill-settings 逻辑,同时采纳 origin/main 的生成守卫与信任感知,而非简单取某一边:

  • packages/cli/src/serve/run-qwen-serve.tspersistDisabledSkillsFn 保留 PR 的 resolveSkillSettings / updateWorkspaceSkillSettingLists / setValues 流程,改为通过 loadSettingsForPersistence(workspace) 加载,在开头和提交前断言 assertGenerationOpen?.(),并将 assertGenerationOpen 作为 setValues 的提交守卫传入。
  • packages/cli/src/serve/workspace-service/index.ts — 保留 PR 的 disabledBySettings(经由 resolveSkillSettings(...).disabledNames)失活扩展检查与 settingsChanges 事件循环,将设置读取改走 origin/main 信任感知的 loadBoundSettings(true),并在发布设置事件前增加 assertActiveGeneration()
  • packages/cli/src/serve/workspace-skills-status.ts — 保留 PR 的 resolveSkillSettings(...).disablementsmapSkillConfigToStatus 所需),并采用 origin/main 的信任感知加载选项;origin/main 中因此不再使用的 readDisabledSkillNames 辅助函数被删除。
  • packages/cli/src/serve/run-qwen-serve.test.ts — 保留 PR 的双列表断言,并保留 origin/main 的生成守卫测试,同时适配 PR 的实现:技能经由 setValues(writes, onScopeCommitted, assertCanCommit) 持久化(守卫为第 3 个参数),工具经由 setValue(scope, key, value, assertCanCommit) 持久化(守卫为第 4 个参数)。locked-skill 拒绝与 ['orphan','alpha','beta'] 断言已存在于第一个测试中。
  • packages/cli/src/serve/workspace-skills-status.test.ts — PR 与 origin/main 在同一位置各自新增了不同的测试;两套测试都予以保留(PR:硬禁用原因 + 安全模式禁用判定;origin/main:非信任工作区跳过 + 损坏环境变量安全性)。

有一个自动合并的测试需要后续修复:workspace-service/__tests__/facade.test.ts 断言 persistDisabledSkills 以 3 个参数被调用,但合并后的源码现在会传入第 4 个 assertGenerationOpen 参数;已按照 origin/main 对同一断言的约定补上末尾的 undefined

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • vitest run src/serve/run-qwen-serve.test.ts(受影响)— 203 个通过
  • vitest run src/serve/workspace-skills-status.test.ts src/serve/workspace-skills-mapping.test.ts src/config/skill-settings.test.ts src/serve/workspace-service/__tests__/facade.test.ts(受影响)— 通过(facade 在补上第 4 个参数后 92 个通过)
  • vitest run src/serve src/config — 155 个文件通过;src/config/settings.test.ts(12 个)与 src/config/config.test.ts(5 个)失败仅因沙箱环境变量QWEN_HOMESANDBOXQWEN_CODE_CUSTOM_SANDBOX_IMAGEQWEN_CODE_SESSION_ID)所致,且这些文件本次改动并未触碰。已证明为环境因素:取消设置这些变量后,config.test.ts 300/300 通过,settings.test.ts 全部通过。它们在干净的 CI 环境中可通过。

Base-conflict check · 基分支冲突检查: conflicted with main — resolved in this push. · 与 main 有冲突——已在本次推送中解决。

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


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

@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Jul 25, 2026

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No issues found. LGTM! ✅

— qwen3.7-max via Qwen Code /review

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. 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-preview via Qwen Code /review

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix review triage — no action taken

The only new feedback on PR #7357 since the last evaluation is a single
COMMENTED review from the automated reviewer (qwen3.8-max-preview via
Qwen Code /review):

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

This is an informational disclosure of the reviewer's own coverage limit, not
a change request. It is a COMMENTED review (not CHANGES_REQUESTED), and it
names no defect, requests no code change, and asks no question. There are no
inline comments, no issue-level comments, no failed checks, and no
still-red checks in this round.

Because there is nothing actionable, no code change was made and no review
thread needs resolving. The note simply records that the "Integration Tests
(CLI, No Sandbox)" suite was skipped in CI and was not run locally by the
reviewer; it does not assert any problem with the PR's changes.

中文说明

Autofix 审查分诊 — 未采取任何操作

自上次评估以来,PR #7357 上唯一的新反馈是来自自动审查器
qwen3.8-max-preview,通过 Qwen Code /review)的一条 COMMENTED
(仅评论)审查:

已审查。未审查:build-and-test — Integration Tests (CLI, No Sandbox) 在 CI
中被跳过,且其测试套件未在本地运行。

这是审查器对自身审查覆盖范围限制的信息性说明,并非变更请求。该审查为
COMMENTED(仅评论)类型(而非 CHANGES_REQUESTED 请求变更类型),它没有
指出任何缺陷,没有请求任何代码变更,也没有提出任何问题。本轮中没有行内评论、
没有 issue 级评论、没有失败的检查,也没有持续红色的检查。

由于没有任何可操作的内容,因此未做任何代码变更,也没有需要解决的审查线程。
该说明仅记录了 “Integration Tests (CLI, No Sandbox)” 套件在 CI 中被跳过、
且审查器未在本地运行它这一事实;它并未断言 PR 的改动存在任何问题。

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


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

@wenshao

wenshao commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Local re-verification of #7357 (round 2) — head 085398f

I rebuilt this PR from source after the latest main merge and re-ran the whole thing end to end on my machine. The merge blocker I reported on 22 Jul is fixed and I could no longer find a functional problem — this is good to merge from my side.

Environment: PR head 085398f (merge of main, 25 Jul), isolated worktree, npm ci + npm run build + npm run bundle → bundled CLI v0.21.0, Node 22.23.1, macOS (arm64). No model request is involved anywhere — /skills, the settings resolution and the daemon toggle are settings-only flows (the configured base URL is a dead port and is never contacted).

TL;DR

Area Result
Serve fast-path bundle closure (my earlier blocker) fixed — gate passes, and a negative control proves the fix is load-bearing
Reviewer test plan steps 1–5 via the real /skills TUI ✅ all pass (screenshots below)
Reviewer test plan steps 2–5 via a real qwen serve daemon over HTTP ✅ 14/14 wire checks
Does the setting actually withhold the skill (not just uncheck a box)? ✅ verified at the slash-command layer, both directions
Acceptance criteria of #7347 ✅ all four met
Focused suites / typecheck / lint / i18n / schema regen ✅ 347 tests, all gates clean
Mutation matrix (9 mutations of the new logic) ✅ 9/9 caught — no vacuous tests
Safe mode (--safe-mode) status vs. execution ℹ️ divergence exists, pre-existing on main, not a blocker — details at the end

1. The merge blocker is fixed — with a negative control

npm run check:serve-fast-path-bundle now prints Startup bundle closure checks passed. at this head. To make sure that is the fix and not my environment, I restored the old static import … from '../config/skill-settings.js' in run-qwen-serve.ts and rebuilt: the gate fails again, pulling glob, chokidar and fzf into the pre-listen closure. Reverting restores green.

gates and mutation matrix

2. The real /skills picker

Fixture: a real project with two real project skills, real settings.json files at user and workspace scope, driven through a pty against the bundled CLI.

User-scope skills.defaultDisabled: ["Opt-In-Skill"] (mixed case) → the skill is the only unchecked row; Space checks it:

/skills default-disabled before/after

After Esc, the workspace file is exactly {"skills":{"enabled":["opt-in-skill"]}} — canonical (lower-cased) name, and no skills.disabled key. Opening the dialog and pressing Esc without toggling anything writes no file at all (the workspace .qwen/settings.json never appears).

The picker's save handler — orphan preservation + a dual-key write in one Esc (this was the "no direct test" gap I flagged in my first review; it is now extracted into computeWorkspaceSkillListUpdates and covered, and here it is against real files):

picker save handler

ghost-skill (an entry for a skill that is not loaded) survives verbatim, the case-variant Always-On-Skill is removed from skills.disabled when re-checked, and the opt-in lands in skills.enabled — all from a single dialog close.

User-scope skills.disabled still hard-locks the row:

locked row

3. Availability oracle — the setting really withholds the skill

A checkbox proves nothing on its own, so I checked whether the skill is actually registered as a command in a live session:

slash command availability

  • default-disabled → /opt-in offers nothing, while the control skill completes in the same session;
  • after a project-scope skills.enabled opt-in (settings file only, user settings untouched) → the command is back — which is exactly issue feat(skills): add overridable defaultDisabled separate from skills.disabled #7347's ask;
  • user skills.disabled + workspace skills.enabled → still withheld, so disabled > enabled > defaultDisabled holds at execution, not only in the UI.

4. Real daemon over HTTP — 14/14

Started a real qwen serve against the same fixture and walked the reviewer test plan over the wire, reading the workspace settings.json back off disk after every call:

daemon HTTP flow

Also confirmed on a live session (not just the deferred path): the dual-key change emits one settings_changed event per changed key (skills.disabled then skills.enabled) on the session SSE stream, and the toggle returns activation: "applied", sessionsRefreshed: 1, sessionsFailed: 0.

5. Tests, gates, and a mutation matrix

  • skill-settings / config.integration / workspace-skills-mapping / workspace-skills-status / run-qwen-serve / workspace-service facade6 files, 347 tests, all green.
  • npm run typecheck ✅ · npm run lint:ci ✅ · npm run check-i18n ✅ · npm run generate:settings-schema → no diff (generated vscode schema is in sync).
  • Mutation matrix (screenshot in §1): I flipped nine load-bearing lines one at a time — the enabled override, lockedScope reporting, case folding, the redundant-opt-in guard, locked-name filtering, orphan preservation, the daemon's skills.enabled change emission, the daemon lock rejection, and the status mapping's new fields. All nine turn the suites red, so the new tests are genuinely load-bearing.

6. One note (not a blocker): safe mode reports disablements it does not enforce

With QWEN_CODE_SAFE_MODE=1, GET /workspace/skills reports review as {"status":"disabled","disabledReason":"hard","lockedScope":"user"} and stuck as {"disabledReason":"default"} — but the real CLI in safe mode still registers /review and /stuck. The reason is packages/cli/src/config/config.ts:2048-2049, which passes disabledSkillNamesProvider: undefined in safe/bare mode, so Config.getDisabledSkillNames() is empty and nothing is enforced.

To be clear about ownership: reading the base file, readDisabledSkillNames() on main had no safe-mode gate either, so the skills.disabled half of this divergence pre-dates this PR; the PR extends the same reporting to the new default reason. I mention it only because round 6 (fix(cli): resolve skill disablements in safe mode for status API, test name resolves disablements in safe mode (status matches execution)) reads as if this were settled — in safe mode, execution enforces nothing at all. Worth a follow-up issue rather than more churn here.

Minor, also non-blocking: a skills.enabled entry whose name is no longer in defaultDisabled stays in the file (inert; it self-cleans on the next disable/enable cycle).

Verdict

The feature does what #7347 asked for, the precedence rule holds at every layer I could reach (picker, status API, toggle API, command registration), backward compatibility with plain skills.disabled is intact, and the CI blocker I raised earlier is genuinely fixed. LGTM — happy to merge.

中文说明

本地复验 #7357(第 2 轮)—— head 085398f

在最新一次合并 main 之后,我重新从源码构建并完整跑了一遍端到端验证。我 7 月 22 日提出的合并阻塞项已修复,本轮没有再发现功能性问题 —— 从我这边看可以合入。

环境: PR head 085398f(7 月 25 日合并 main),独立 worktree,npm ci + npm run build + npm run bundle 生成打包 CLI v0.21.0,Node 22.23.1,macOS(arm64)。全程不涉及模型请求 —— /skills、设置解析与 daemon 切换都是纯设置流程(配置的 base URL 是死端口,从未被访问)。

结论速览

项目 结果
serve fast-path 打包闭包(我此前的阻塞项) 已修复 —— gate 通过,并用反向对照证明该修复是必要的
真实 /skills TUI 跑 reviewer 测试计划第 1–5 步 ✅ 全部通过(见截图)
真实 qwen serve daemon 走 HTTP 跑第 2–5 步 ✅ 14/14 项线上检查通过
设置是否真的让技能不可用(而不只是取消勾选) ✅ 在 slash 命令层双向验证
issue #7347 的验收标准 ✅ 四条全部满足
聚焦测试 / typecheck / lint / i18n / schema 重新生成 ✅ 347 个测试,所有 gate 干净
变异测试(对新逻辑做 9 处变异) ✅ 9/9 全部被测试捕获,无空转测试
安全模式(--safe-mode)状态与执行 ℹ️ 存在不一致,但main 上即已存在,非阻塞 —— 见文末

1. 合并阻塞项已修复(含反向对照)

在该 head 上 npm run check:serve-fast-path-bundle 输出 Startup bundle closure checks passed.。为确认这是修复起作用而非环境差异,我把 run-qwen-serve.ts 中的动态 import 改回旧的静态 import 并重新打包:gate 再次失败,把 globchokidarfzf 拉进了 pre-listen 闭包;改回后重新变绿。

2. 真实 /skills 选择器

用真实项目、两个真实项目技能、用户级与工作区级真实 settings.json,通过 pty 驱动打包 CLI:

  • 用户级 skills.defaultDisabled: ["Opt-In-Skill"](大小写混合)→ 该技能是唯一未勾选行;空格勾选后按 Esc,工作区文件恰好为 {"skills":{"enabled":["opt-in-skill"]}}(规范小写名,且不会skills.disabled)。打开对话框但不做任何切换直接 Esc,则完全不会写文件。
  • 选择器保存逻辑(我第一轮 review 指出缺少直接测试的部分,现已抽成 computeWorkspaceSkillListUpdates 并有测试覆盖):孤儿条目 ghost-skill 原样保留、大小写变体 Always-On-Skill 重新勾选后从 skills.disabled 移除、opt-in 写入 skills.enabled —— 一次 Esc 同时写两个 key。
  • 用户级 skills.disabled 仍然硬锁定该行,显示在「Locked by higher-scope settings」并标注 [locked: User],关闭对话框不会写任何工作区设置。

3. 可用性验证 —— 设置确实屏蔽了技能

仅看勾选框说明不了问题,因此我检查了技能是否真的注册为可用命令:默认禁用时输入 /opt-in 没有任何补全(同一会话中的对照技能正常补全);仅通过项目级 skills.enabled(不改用户设置)opt-in 后命令恢复 —— 这正是 issue #7347 的诉求;用户级 skills.disabled + 工作区 skills.enabled 时依然不可用,说明 disabled > enabled > defaultDisabled 在执行层同样成立,而不只是 UI 表现。

4. 真实 daemon HTTP 流程 —— 14/14

启动真实 qwen serve,按 reviewer 测试计划走接口,并在每次调用后回读磁盘上的工作区 settings.json:默认禁用上报 disabledReason: "default";大小写混合的启用返回 200 并只写入一次规范名;重复启用为 changed:false 且文件不变;禁用后移除 opt-in 并写入规范硬禁用;高作用域硬禁用时上报 lockedScope: "user",工作区启用被 409 skill_not_toggleable 拒绝且不会创建项目设置文件。

另外在有活跃会话的情况下确认:双 key 变更会在会话 SSE 流上按每个变更 key 各发一次 settings_changed 事件skills.disabledskills.enabled),接口返回 activation: "applied", sessionsRefreshed: 1, sessionsFailed: 0

5. 测试、gate 与变异测试

  • 6 个相关测试文件共 347 个测试全部通过
  • npm run typecheck ✅ · npm run lint:ci ✅ · npm run check-i18n ✅ · npm run generate:settings-schema 无 diff(生成的 vscode schema 与源同步)。
  • 变异测试:逐个翻转 9 处关键逻辑(enabled 覆盖、lockedScope 上报、大小写归一、冗余 opt-in 的防护、锁定名过滤、孤儿条目保留、daemon 的 skills.enabled 变更发布、daemon 锁定拒绝、状态映射新增字段),9 处全部让测试变红,说明新增测试确实有效。

6. 一个说明(非阻塞):安全模式会上报它并不执行的禁用状态

设置 QWEN_CODE_SAFE_MODE=1 后,GET /workspace/skills 会把 review 报为 {"status":"disabled","disabledReason":"hard","lockedScope":"user"}stuck 报为 {"disabledReason":"default"},但安全模式下的真实 CLI 仍然注册 /review/stuck。原因在 packages/cli/src/config/config.ts:2048-2049:safe/bare 模式下传入 disabledSkillNamesProvider: undefined,因此 Config.getDisabledSkillNames() 为空集,什么也不会被执行层拦截。

需要说明归属:查阅 base 上的 readDisabledSkillNames() 同样没有安全模式判断,因此 skills.disabled 这一半的不一致在本 PR 之前就存在;本 PR 只是把同样的上报扩展到新的 default 原因。我提这一点,是因为第 6 轮(提交 fix(cli): resolve skill disablements in safe mode for status API,测试名 resolves disablements in safe mode (status matches execution))读起来像是已经解决 —— 而安全模式下执行层其实什么都不拦。建议另开 issue 跟进,不必在本 PR 继续折腾。

另一个非阻塞的小问题:若某个名字已不在 defaultDisabled 中,其 skills.enabled 条目会滞留在文件里(无副作用,且在下一次禁用/启用循环中会自动清理)。

结论

该特性实现了 #7347 的诉求,优先级规则在我能触达的每一层(选择器、状态 API、切换 API、命令注册)都成立,与原有 skills.disabled 的兼容性完好,我此前提出的 CI 阻塞项也确实修好了。LGTM —— 可以合入。

Verified locally with Claude Code (Opus 5): source build of the bundled CLI, real /skills pty session, real qwen serve HTTP flow, a fast-path gate negative control, and a 9-mutation matrix.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

No action needed

The only new feedback on PR #7357 is an issue-level comment from @wenshao containing a comprehensive local re-verification (round 2, head 085398f). The verdict is LGTM — happy to merge.

There are no inline comments, no review items, no failed checks, and no change requests.

Non-blocking observations (no action taken)

  1. Safe-mode status/execution divergenceGET /workspace/skills reports disablements that safe mode does not enforce. @wenshao confirms this is pre-existing on main (the skills.disabled half predates this PR) and explicitly recommends a follow-up issue rather than more churn here.

  2. Inert skills.enabled entries — an entry whose name is no longer in defaultDisabled stays in the settings file. Noted as inert and self-cleaning on the next disable/enable cycle. Non-blocking.

中文说明

无需操作

PR #7357 上唯一的新反馈是 @wenshao 发布的一条 issue 级评论,包含完整的本地复验结果(第 2 轮,head 085398f)。结论为 LGTM —— 可以合入

没有行内评论、没有 review 条目、没有失败的 CI 检查、也没有变更请求。

非阻塞观察(未采取行动)

  1. 安全模式状态/执行不一致 —— GET /workspace/skills 会上报安全模式实际并不执行的禁用状态。@wenshao 确认这在 main 上即已存在(skills.disabled 部分早于本 PR),并明确建议另开 issue 跟进,不必在本 PR 继续折腾。

  2. 无副作用的 skills.enabled 条目 —— 若某个名字已不在 defaultDisabled 中,其 skills.enabled 条目会滞留在设置文件里。被标注为无副作用且在下一次禁用/启用循环中自动清理。非阻塞。

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


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

@wenshao
wenshao added this pull request to the merge queue Jul 25, 2026
Merged via the queue into QwenLM:main with commit 3a6c8e0 Jul 25, 2026
69 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.1.

@yiliang114

Copy link
Copy Markdown
Collaborator

⚠️ Failed to process this request. Please re-mention the bot to retry.

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

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) review/self-reported The linked issue was opened by the PR author (self-reported)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(skills): add overridable defaultDisabled separate from skills.disabled

7 participants