refactor(skills): replace 'project' with 'workspace' scope (#16380)

This commit is contained in:
N. Taylor Mullen
2026-01-14 13:05:26 -08:00
committed by GitHub
parent b3eecc3a50
commit c8c7b57a79
8 changed files with 56 additions and 26 deletions

View File

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

View File

@@ -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({

View File

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

View File

@@ -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.',
);
});

View File

@@ -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.',
}),
);
});

View File

@@ -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);
};

View File

@@ -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 });

View File

@@ -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(),
);