From c8c7b57a79f7286ca93e02d734cd614c2b6283fb Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Wed, 14 Jan 2026 13:05:26 -0800 Subject: [PATCH] refactor(skills): replace 'project' with 'workspace' scope (#16380) --- docs/cli/skills.md | 16 +++++------ .../cli/src/commands/skills/disable.test.ts | 28 +++++++++++++++++++ packages/cli/src/commands/skills/disable.ts | 10 ++++--- .../cli/src/commands/skills/enable.test.ts | 6 ++-- .../cli/src/ui/commands/skillsCommand.test.ts | 6 ++-- packages/cli/src/utils/skillUtils.ts | 2 +- packages/core/src/skills/skillManager.test.ts | 8 +++--- packages/core/src/skills/skillManager.ts | 6 ++-- 8 files changed, 56 insertions(+), 26 deletions(-) diff --git a/docs/cli/skills.md b/docs/cli/skills.md index 5aebf00cb5..448734a14d 100644 --- a/docs/cli/skills.md +++ b/docs/cli/skills.md @@ -13,7 +13,7 @@ discoverable capability. ## Overview Unlike general context files ([`GEMINI.md`](./gemini-md.md)), which provide -persistent project-wide background, Skills represent **on-demand expertise**. +persistent workspace-wide background, Skills represent **on-demand expertise**. This allows Gemini to maintain a vast library of specialized capabilities—such as security auditing, cloud deployments, or codebase migrations—without cluttering the model's immediate context window. @@ -39,15 +39,15 @@ the full instructions and resources required to complete the task using the Gemini CLI discovers skills from three primary locations: -1. **Project Skills** (`.gemini/skills/`): Project-specific skills that are +1. **Workspace Skills** (`.gemini/skills/`): Workspace-specific skills that are typically committed to version control and shared with the team. 2. **User Skills** (`~/.gemini/skills/`): Personal skills available across all - your projects. + your workspaces. 3. **Extension Skills**: Skills bundled within installed [extensions](../extensions/index.md). **Precedence:** If multiple skills share the same name, higher-precedence -locations override lower ones: **Project > User > Extension**. +locations override lower ones: **Workspace > User > Extension**. ## Managing Skills @@ -61,7 +61,7 @@ Use the `/skills` slash command to view and manage available expertise: - `/skills reload`: Refreshes the list of discovered skills from all tiers. _Note: `/skills disable` and `/skills enable` default to the `user` scope. Use -`--scope project` to manage project-specific settings._ +`--scope workspace` to manage workspace-specific settings._ ### From the Terminal @@ -89,8 +89,8 @@ gemini skills uninstall my-expertise --scope workspace # Enable a skill (globally) gemini skills enable my-expertise -# Disable a skill. Can use --scope to specify project or user (defaults to project) -gemini skills disable my-expertise --scope project +# Disable a skill. Can use --scope to specify workspace or user (defaults to workspace) +gemini skills disable my-expertise --scope workspace ``` ## Creating a Skill @@ -147,7 +147,7 @@ You are an expert code reviewer. When reviewing code, follow this workflow: 1. **Analyze**: Review the staged changes or specific files provided. Ensure that the changes are scoped properly and represent minimal changes required to address the issue. -2. **Style**: Ensure code follows the project's conventions and idiomatic +2. **Style**: Ensure code follows the workspace's conventions and idiomatic patterns as described in the `GEMINI.md` file. 3. **Security**: Flag any potential security vulnerabilities. 4. **Tests**: Verify that new logic has corresponding test coverage and that diff --git a/packages/cli/src/commands/skills/disable.test.ts b/packages/cli/src/commands/skills/disable.test.ts index f4ae0d954e..4a5097471b 100644 --- a/packages/cli/src/commands/skills/disable.test.ts +++ b/packages/cli/src/commands/skills/disable.test.ts @@ -84,6 +84,34 @@ describe('skills disable command', () => { ); }); + it('should disable an enabled skill in workspace scope', async () => { + const mockSettings = { + forScope: vi.fn().mockReturnValue({ + settings: { skills: { disabled: [] } }, + path: '/workspace/.gemini/settings.json', + }), + setValue: vi.fn(), + }; + mockLoadSettings.mockReturnValue( + mockSettings as unknown as LoadedSettings, + ); + + await handleDisable({ + name: 'skill1', + scope: SettingScope.Workspace as LoadableSettingScope, + }); + + expect(mockSettings.setValue).toHaveBeenCalledWith( + SettingScope.Workspace, + 'skills.disabled', + ['skill1'], + ); + expect(emitConsoleLog).toHaveBeenCalledWith( + 'log', + 'Skill "skill1" disabled by adding it to the disabled list in workspace (/workspace/.gemini/settings.json) settings.', + ); + }); + it('should log a message if the skill is already disabled', async () => { const mockSettings = { forScope: vi.fn().mockReturnValue({ diff --git a/packages/cli/src/commands/skills/disable.ts b/packages/cli/src/commands/skills/disable.ts index fcb69c087d..95fd607924 100644 --- a/packages/cli/src/commands/skills/disable.ts +++ b/packages/cli/src/commands/skills/disable.ts @@ -42,14 +42,16 @@ export const disableCommand: CommandModule = { }) .option('scope', { alias: 's', - describe: 'The scope to disable the skill in (user or project).', + describe: 'The scope to disable the skill in (user or workspace).', type: 'string', - default: 'project', - choices: ['user', 'project'], + default: 'workspace', + choices: ['user', 'workspace'], }), handler: async (argv) => { const scope = - argv['scope'] === 'project' ? SettingScope.Workspace : SettingScope.User; + argv['scope'] === 'workspace' + ? SettingScope.Workspace + : SettingScope.User; await handleDisable({ name: argv['name'] as string, scope, diff --git a/packages/cli/src/commands/skills/enable.test.ts b/packages/cli/src/commands/skills/enable.test.ts index 5874072130..e204da2f66 100644 --- a/packages/cli/src/commands/skills/enable.test.ts +++ b/packages/cli/src/commands/skills/enable.test.ts @@ -64,7 +64,7 @@ describe('skills enable command', () => { path: '/user/settings.json', }; } - return { settings: {}, path: '/project/settings.json' }; + return { settings: {}, path: '/workspace/settings.json' }; }), setValue: vi.fn(), }; @@ -81,7 +81,7 @@ describe('skills enable command', () => { ); expect(emitConsoleLog).toHaveBeenCalledWith( 'log', - 'Skill "skill1" enabled by removing it from the disabled list in user (/user/settings.json) and project (/project/settings.json) settings.', + 'Skill "skill1" enabled by removing it from the disabled list in user (/user/settings.json) and workspace (/workspace/settings.json) settings.', ); }); @@ -122,7 +122,7 @@ describe('skills enable command', () => { ); expect(emitConsoleLog).toHaveBeenCalledWith( 'log', - 'Skill "skill1" enabled by removing it from the disabled list in project (/workspace/settings.json) and user (/user/settings.json) settings.', + 'Skill "skill1" enabled by removing it from the disabled list in workspace (/workspace/settings.json) and user (/user/settings.json) settings.', ); }); diff --git a/packages/cli/src/ui/commands/skillsCommand.test.ts b/packages/cli/src/ui/commands/skillsCommand.test.ts index d6e0bb30b7..8d55d343e6 100644 --- a/packages/cli/src/ui/commands/skillsCommand.test.ts +++ b/packages/cli/src/ui/commands/skillsCommand.test.ts @@ -225,7 +225,7 @@ describe('skillsCommand', () => { expect(context.ui.addItem).toHaveBeenCalledWith( expect.objectContaining({ type: MessageType.INFO, - text: 'Skill "skill1" disabled by adding it to the disabled list in project (/workspace) settings. Use "/skills reload" for it to take effect.', + text: 'Skill "skill1" disabled by adding it to the disabled list in workspace (/workspace) settings. Use "/skills reload" for it to take effect.', }), ); }); @@ -253,7 +253,7 @@ describe('skillsCommand', () => { expect(context.ui.addItem).toHaveBeenCalledWith( expect.objectContaining({ type: MessageType.INFO, - text: 'Skill "skill1" enabled by removing it from the disabled list in project (/workspace) and user (/user/settings.json) settings. Use "/skills reload" for it to take effect.', + text: 'Skill "skill1" enabled by removing it from the disabled list in workspace (/workspace) and user (/user/settings.json) settings. Use "/skills reload" for it to take effect.', }), ); }); @@ -292,7 +292,7 @@ describe('skillsCommand', () => { expect(context.ui.addItem).toHaveBeenCalledWith( expect.objectContaining({ type: MessageType.INFO, - text: 'Skill "skill1" enabled by removing it from the disabled list in project (/workspace) and user (/user/settings.json) settings. Use "/skills reload" for it to take effect.', + text: 'Skill "skill1" enabled by removing it from the disabled list in workspace (/workspace) and user (/user/settings.json) settings. Use "/skills reload" for it to take effect.', }), ); }); diff --git a/packages/cli/src/utils/skillUtils.ts b/packages/cli/src/utils/skillUtils.ts index 7acad4baf7..75acbae87d 100644 --- a/packages/cli/src/utils/skillUtils.ts +++ b/packages/cli/src/utils/skillUtils.ts @@ -47,7 +47,7 @@ export function renderSkillActionFeedback( const formatScopeItem = (s: { scope: SettingScope; path: string }) => { const label = - s.scope === SettingScope.Workspace ? 'project' : s.scope.toLowerCase(); + s.scope === SettingScope.Workspace ? 'workspace' : s.scope.toLowerCase(); return formatScope(label, s.path); }; diff --git a/packages/core/src/skills/skillManager.test.ts b/packages/core/src/skills/skillManager.test.ts index 5635ddf4c3..20cba08405 100644 --- a/packages/core/src/skills/skillManager.test.ts +++ b/packages/core/src/skills/skillManager.test.ts @@ -35,9 +35,9 @@ describe('SkillManager', () => { vi.restoreAllMocks(); }); - it('should discover skills from extensions, user, and project with precedence', async () => { + it('should discover skills from extensions, user, and workspace with precedence', async () => { const userDir = path.join(testRootDir, 'user'); - const projectDir = path.join(testRootDir, 'project'); + const projectDir = path.join(testRootDir, 'workspace'); await fs.mkdir(path.join(userDir, 'skill-a'), { recursive: true }); await fs.mkdir(path.join(projectDir, 'skill-b'), { recursive: true }); @@ -92,9 +92,9 @@ description: project-desc expect(names).toContain('skill-project'); }); - it('should respect precedence: Project > User > Extension', async () => { + it('should respect precedence: Workspace > User > Extension', async () => { const userDir = path.join(testRootDir, 'user'); - const projectDir = path.join(testRootDir, 'project'); + const projectDir = path.join(testRootDir, 'workspace'); await fs.mkdir(path.join(userDir, 'skill'), { recursive: true }); await fs.mkdir(path.join(projectDir, 'skill'), { recursive: true }); diff --git a/packages/core/src/skills/skillManager.ts b/packages/core/src/skills/skillManager.ts index f14a9de78d..b2ec9e660e 100644 --- a/packages/core/src/skills/skillManager.ts +++ b/packages/core/src/skills/skillManager.ts @@ -39,8 +39,8 @@ export class SkillManager { } /** - * Discovers skills from standard user and project locations, as well as extensions. - * Precedence: Extensions (lowest) -> User -> Project (highest). + * Discovers skills from standard user and workspace locations, as well as extensions. + * Precedence: Extensions (lowest) -> User -> Workspace (highest). */ async discoverSkills( storage: Storage, @@ -62,7 +62,7 @@ export class SkillManager { const userSkills = await loadSkillsFromDir(Storage.getUserSkillsDir()); this.addSkillsWithPrecedence(userSkills); - // 4. Project skills (highest precedence) + // 4. Workspace skills (highest precedence) const projectSkills = await loadSkillsFromDir( storage.getProjectSkillsDir(), );