Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f46bb14
feat(skills): add disabled skill levels
zhangxy-zju Jul 29, 2026
b8927e7
Merge branch 'main' into feat/disable-bundled-skills-8054
qwen-code-dev-bot Jul 29, 2026
4a57eb6
Merge branch 'main' into feat/disable-bundled-skills-8054
wenshao Jul 29, 2026
d762df4
Merge branch 'main' into feat/disable-bundled-skills-8054
qwen-code-dev-bot Jul 29, 2026
136cbbf
Merge branch 'main' into feat/disable-bundled-skills-8054
qwen-code-dev-bot Jul 30, 2026
178a961
Merge branch 'main' into feat/disable-bundled-skills-8054
zhangxy-zju Jul 30, 2026
8b21844
Merge branch 'main' into feat/disable-bundled-skills-8054
qwen-code-dev-bot Jul 30, 2026
51f513f
Merge branch 'main' into feat/disable-bundled-skills-8054
wenshao Jul 30, 2026
8884fec
fix(core): guard getDisabledSkillLevels for partial Config shims (#8057)
Jul 30, 2026
24f102e
test(cli): cover disabledLevels safe/bare guard and daemon wiring (#8…
Jul 30, 2026
a4d5321
fix(cli): guard daemon disabledLevels for safe mode consistency (#8057)
Jul 30, 2026
268e3e0
Merge branch 'main' into feat/disable-bundled-skills-8054
zhangxy-zju Jul 30, 2026
3b575a6
Merge remote-tracking branch 'origin/main' into feat/disable-bundled-…
zhangxy-zju Jul 30, 2026
0667e66
Merge branch 'main' into feat/disable-bundled-skills-8054
zhangxy-zju Jul 31, 2026
f4670a6
Merge branch 'main' into feat/disable-bundled-skills-8054
zhangxy-zju Jul 31, 2026
ba09ffd
test(core): add regression guard for missing getDisabledSkillLevels (…
qwen-code-ci-bot Jul 31, 2026
d5ee013
Merge branch 'main' into feat/disable-bundled-skills-8054
zhangxy-zju Jul 31, 2026
fabe6e7
test(core): pin partial config skill discovery (#8057)
zhangxy-zju Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/users/configuration/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ Controls which [Skills](../features/skills) are exposed to the model.

| Setting | Type | Description | Default |
| ------------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `skills.disabledLevels` | array of strings | Skill discovery levels to skip entirely. Supported values are `project`, `user`, `extension`, and `bundled`. Merged as a union across settings scopes. Use `["bundled"]` to hide every bundled skill while retaining host-provided skills. | `undefined` |
| `skills.disabled` | array of strings | Hard-disabled skill names. Matched case-insensitively and **merged as a union** across settings scopes, so project settings cannot override a user or system entry. Hidden skills do not appear in `<available_skills>` or as `/<name>` slash commands. | `undefined` |
| `skills.defaultDisabled` | array of strings | Skill names that start disabled but can be opted into through `skills.enabled`. Matched case-insensitively and merged as a union across settings scopes. | `undefined` |
| `skills.enabled` | array of strings | Explicit opt-ins that override matching `skills.defaultDisabled` entries. Matched case-insensitively and merged as a union across settings scopes. This setting cannot override `skills.disabled`. | `undefined` |
Expand Down
35 changes: 35 additions & 0 deletions packages/cli/src/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4732,3 +4732,38 @@ describe('loadCliConfig skills.directories', () => {
expect(config.getCustomSkillDirs()).toEqual([]);
});
});

describe('loadCliConfig skills.disabledLevels', () => {
beforeEach(() => {
process.argv = ['node', 'script.js'];
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
});

afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});

it('passes valid disabled skill levels to core and ignores invalid values', async () => {
const argv = await parseArguments();
const settings: Settings = {
skills: {
disabledLevels: ['bundled', 'invalid', 42 as unknown as string, 'user'],
},
};

const config = await loadCliConfig(settings, argv);

expect(config.getDisabledSkillLevels()).toEqual(
new Set(['bundled', 'user']),
);
});

it('keeps every skill level enabled by default', async () => {
const argv = await parseArguments();

const config = await loadCliConfig({}, argv);

expect(config.getDisabledSkillLevels()).toEqual(new Set());
});
});
16 changes: 16 additions & 0 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
SchemaValidator,
type ConfigParameters,
type MCPServerConfig,
type SkillLevel,
type WebSearchSettings,
MAX_SUBAGENT_DEPTH_LIMIT,
} from '@qwen-code/qwen-code-core';
Expand Down Expand Up @@ -113,6 +114,17 @@ const VALID_APPROVAL_MODE_VALUES = [
'yolo',
] as const;

const SKILL_LEVELS: readonly SkillLevel[] = [
'project',
'user',
'extension',
'bundled',
];
Comment thread
zhangxy-zju marked this conversation as resolved.
Resolved

function isSkillLevel(value: unknown): value is SkillLevel {
return SKILL_LEVELS.includes(value as SkillLevel);
}

function formatApprovalModeError(value: string): Error {
return new Error(
`Invalid approval mode: ${value}. Valid values are: ${VALID_APPROVAL_MODE_VALUES.join(
Expand Down Expand Up @@ -2068,6 +2080,10 @@ export async function loadCliConfig(
disabledSlashCommands.length > 0 ? disabledSlashCommands : undefined,
disabledSkillNamesProvider:
bareMode || safeMode ? undefined : disabledSkillNamesProvider,
disabledSkillLevels:
bareMode || safeMode || !Array.isArray(settings.skills?.disabledLevels)
? undefined
: settings.skills.disabledLevels.filter(isSkillLevel),
Comment thread
zhangxy-zju marked this conversation as resolved.
Resolved
customSkillDirs:
bareMode || safeMode
? undefined
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/src/config/settingsSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from '@qwen-code/qwen-code-core';
import {
getSettingsSchema,
MergeStrategy,
type SettingDefinition,
type Settings,
type SettingsSchema,
Expand Down Expand Up @@ -352,6 +353,22 @@ describe('SettingsSchema', () => {
expect(getSettingsSchema().mcp.requiresRestart).toBe(true);
});

it('defines disabled skill levels as a restart-required union setting', () => {
const disabledLevels =
getSettingsSchema().skills.properties.disabledLevels;

expect(disabledLevels.type).toBe('array');
expect(disabledLevels.default).toBeUndefined();
expect(disabledLevels.requiresRestart).toBe(true);
expect(disabledLevels.mergeStrategy).toBe(MergeStrategy.UNION);
expect(disabledLevels.items?.enum).toEqual([
'project',
'user',
'extension',
'bundled',
]);
});

it('should have consistent default values for boolean settings', () => {
const checkBooleanDefaults = (schema: SettingsSchema) => {
Object.entries(schema).forEach(([, definition]) => {
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1989,6 +1989,23 @@ const SETTINGS_SCHEMA = {
'the model.',
showInDialog: false,
properties: {
disabledLevels: {
type: 'array',
label: 'Disabled Skill Levels',
category: 'Advanced',
requiresRestart: true,
default: undefined as string[] | undefined,
description:
'Skill discovery levels to skip entirely. Supported levels are ' +
'project, user, extension, and bundled. UNION-merged across settings ' +
'scopes.',
showInDialog: false,
mergeStrategy: MergeStrategy.UNION,
items: {
type: 'string',
enum: ['project', 'user', 'extension', 'bundled'],
},
},
disabled: {
type: 'array',
label: 'Disabled Skills',
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ import { InputFormat, OutputFormat } from '../output/types.js';
import { PromptRegistry } from '../prompts/prompt-registry.js';
import { ResourceRegistry } from '../resources/resource-registry.js';
import { SkillManager } from '../skills/skill-manager.js';
import type { SkillLevel } from '../skills/types.js';
import { PermissionManager } from '../permissions/permission-manager.js';
import {
type AutoModeDenialState,
Expand Down Expand Up @@ -952,6 +953,11 @@ export interface ConfigParameters {
* Names returned must be lower-cased; consumers compare case-insensitively.
*/
disabledSkillNamesProvider?: () => ReadonlySet<string>;
/**
* Skill discovery levels that should not be loaded. Sourced from
* `settings.skills.disabledLevels`.
*/
disabledSkillLevels?: readonly SkillLevel[];
/**
* Additional directories to scan for skills (SKILL.md files).
* Sourced from `settings.skills.directories`. Paths are raw
Expand Down Expand Up @@ -1742,6 +1748,7 @@ export class Config {
private readonly disabledSkillNamesProvider:
| (() => ReadonlySet<string>)
| null;
private readonly disabledSkillLevels: ReadonlySet<SkillLevel>;
private readonly customSkillDirs: readonly string[];
// `disabledTools` is set at construction
// time but can be re-synced by the daemon mutation surface
Expand Down Expand Up @@ -2047,6 +2054,7 @@ export class Config {
...(params.disabledSlashCommands ?? []),
]);
this.disabledSkillNamesProvider = params.disabledSkillNamesProvider ?? null;
this.disabledSkillLevels = new Set(params.disabledSkillLevels ?? []);
this.customSkillDirs = Object.freeze([...(params.customSkillDirs ?? [])]);
this.disabledTools = new Set(params.disabledTools ?? []);
this.visibleTools = new Set(
Expand Down Expand Up @@ -4848,6 +4856,14 @@ export class Config {
return this.disabledSkillNamesProvider?.() ?? EMPTY_DISABLED_SKILL_NAMES;
}

/**
* Returns skill discovery levels excluded through
* `settings.skills.disabledLevels`.
*/
getDisabledSkillLevels(): ReadonlySet<SkillLevel> {
return this.disabledSkillLevels;
}

/**
* Returns additional skill directories from `settings.skills.directories`.
* Paths are raw (unexpanded); consumers must handle `~` expansion
Expand Down
26 changes: 26 additions & 0 deletions packages/core/src/skills/skill-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,32 @@ Review content`;
expect(simplifySkill!.level).toBe('bundled');
});

it('should skip disabled skill levels without scanning them', async () => {
const disabledConfig = makeFakeConfig({
disabledSkillLevels: ['bundled'],
});
vi.spyOn(disabledConfig, 'getProjectRoot').mockReturnValue(
'/test/project',
);
const disabledManager = new SkillManager(disabledConfig);
mockReaddirForLevels(new Set(['project', 'bundled']));
setupReviewSkillMocks();

const skills = await disabledManager.listSkills({ force: true });

expect(skills.map((skill) => [skill.name, skill.level])).toEqual([
['review', 'project'],
]);
expect(await disabledManager.loadSkill('simplify')).toBeNull();
expect(
vi
.mocked(fs.readdir)
.mock.calls.some(([dirPath]) =>
String(dirPath).endsWith(bundledDirSegment),
),
).toBe(false);
});

it('should prioritize project-level over bundled skills with same name', async () => {
mockReaddirForLevels(new Set(['project', 'bundled']));
setupReviewSkillMocks();
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/skills/skill-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,11 @@ export class SkillManager {
return [];
}

if (this.config.getDisabledSkillLevels().has(level)) {
debugLogger.debug(`Skipping disabled ${level} skill level`);
return [];
}

const projectRoot = this.config.getProjectRoot();
const homeDir = os.homedir();
const isHomeDirectory = path.resolve(projectRoot) === path.resolve(homeDir);
Expand Down
13 changes: 13 additions & 0 deletions packages/vscode-ide-companion/schemas/settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,19 @@
"description": "Configuration for skills (SKILL.md-based capabilities) exposed to the model.",
"type": "object",
"properties": {
"disabledLevels": {
"description": "Skill discovery levels to skip entirely. Supported levels are project, user, extension, and bundled. UNION-merged across settings scopes.",
"type": "array",
"items": {
"type": "string",
"enum": [
"project",
"user",
"extension",
"bundled"
]
}
},
"disabled": {
"description": "Skill names to hide. Matched case-insensitively against the skill name. Hidden skills do not appear in <available_skills> or as /<name> slash commands. UNION-merged across systemDefaults/user/workspace/system scopes — workspace cannot remove entries defined in higher scopes.",
"type": "array",
Expand Down
Loading