-
Notifications
You must be signed in to change notification settings - Fork 1
Add skills directory support for all agents #192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…surf agents - Added skillsPath configuration for OpenCode (.opencode/skills) - Added skillsPath configuration for GitHub Copilot (.github/skills) - Added skillsPath configuration for Augment (.augment/skills) - Added skillsPath configuration for Windsurf (.windsurf/skills) - Updated documentation to reflect new skills paths - Added comprehensive tests for all new skills directories - All tests pass and linting successful Co-authored-by: alexec <[email protected]>
|
@copilot please simply assume they all support skills using the same directory structure. |
Per feedback, now assuming all agents support skills with the same directory structure. - Added skillsPath configuration for Claude (.claude/skills) - Added skillsPath configuration for Gemini (.gemini/skills) - Added skillsPath configuration for Codex (.codex/skills) - Updated documentation to include all three new skills paths - Added comprehensive tests for all three new agents - All tests pass and linting successful Co-authored-by: alexec <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds skills directory support for seven AI coding agents (OpenCode, GitHub Copilot, Claude, Gemini, Augment, Windsurf, and Codex) by configuring their respective skillsPath values and adding corresponding test cases and documentation.
Key Changes:
- Added
skillsPathconfiguration for 7 agents using the pattern.{agent}/skills - Added 7 new test cases to verify skill discovery from each agent's directory
- Updated documentation to list all newly supported skill search paths with examples
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
| pkg/codingcontext/agent_paths.go | Added skillsPath configuration for 7 agents (OpenCode, Copilot, Claude, Gemini, Augment, Windsurf, Codex) following consistent .{agent}/skills pattern |
| pkg/codingcontext/context_test.go | Added 7 test cases verifying skill discovery from each newly supported agent directory |
| docs/reference/search-paths.md | Updated skill search paths documentation with new agent directories and added directory structure examples |
| ├── code-gen/ | ||
| │ └── SKILL.md | ||
| └── refactoring/ | ||
| └── SKILL.md |
Copilot
AI
Dec 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation example is missing directory structures for Augment and Windsurf skills, which were added to the search paths list above. For consistency and completeness, examples should be provided for all newly supported agent skill directories.
| └── SKILL.md | |
| └── SKILL.md | |
| .augment/skills/ | |
| ├── testing/ | |
| │ └── SKILL.md | |
| └── debugging/ | |
| └── SKILL.md | |
| .windsurf/skills/ | |
| ├── navigation/ | |
| │ └── SKILL.md | |
| └── layout/ | |
| └── SKILL.md |
| checkFunc: func(t *testing.T, result *Result) { | ||
| if len(result.Skills.Skills) != 1 { | ||
| t.Fatalf("expected 1 skill, got %d", len(result.Skills.Skills)) | ||
| } | ||
| skill := result.Skills.Skills[0] | ||
| if skill.Name != "opencode-skill" { | ||
| t.Errorf("expected skill name 'opencode-skill', got %q", skill.Name) | ||
| } | ||
| }, |
Copilot
AI
Dec 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test verification is incomplete compared to the existing .cursor/skills test. It should also verify the Description field and Location field to ensure the skill metadata is properly extracted and populated. The .cursor test at lines 2318-2323 checks both of these fields.
| checkFunc: func(t *testing.T, result *Result) { | ||
| if len(result.Skills.Skills) != 1 { | ||
| t.Fatalf("expected 1 skill, got %d", len(result.Skills.Skills)) | ||
| } | ||
| skill := result.Skills.Skills[0] | ||
| if skill.Name != "windsurf-skill" { | ||
| t.Errorf("expected skill name 'windsurf-skill', got %q", skill.Name) | ||
| } | ||
| }, |
Copilot
AI
Dec 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test verification is incomplete compared to the existing .cursor/skills test. It should also verify the Description field and Location field to ensure the skill metadata is properly extracted and populated. The .cursor test at lines 2318-2323 checks both of these fields.
| checkFunc: func(t *testing.T, result *Result) { | ||
| if len(result.Skills.Skills) != 1 { | ||
| t.Fatalf("expected 1 skill, got %d", len(result.Skills.Skills)) | ||
| } | ||
| skill := result.Skills.Skills[0] | ||
| if skill.Name != "claude-skill" { | ||
| t.Errorf("expected skill name 'claude-skill', got %q", skill.Name) | ||
| } | ||
| }, |
Copilot
AI
Dec 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test verification is incomplete compared to the existing .cursor/skills test. It should also verify the Description field and Location field to ensure the skill metadata is properly extracted and populated. The .cursor test at lines 2318-2323 checks both of these fields.
| { | ||
| name: "discover skills from .opencode/skills directory", | ||
| setup: func(t *testing.T, dir string) { | ||
| // Create task | ||
| createTask(t, dir, "test-task", "", "Test task content") | ||
|
|
||
| // Create skill in .opencode/skills directory | ||
| skillDir := filepath.Join(dir, ".opencode", "skills", "opencode-skill") | ||
| if err := os.MkdirAll(skillDir, 0o755); err != nil { | ||
| t.Fatalf("failed to create skill directory: %v", err) | ||
| } | ||
|
|
||
| skillContent := `--- | ||
| name: opencode-skill | ||
| description: A skill for OpenCode | ||
| --- | ||
| # OpenCode Skill | ||
| This is a skill for OpenCode. | ||
| ` | ||
| skillPath := filepath.Join(skillDir, "SKILL.md") | ||
| if err := os.WriteFile(skillPath, []byte(skillContent), 0o644); err != nil { | ||
| t.Fatalf("failed to create skill file: %v", err) | ||
| } | ||
| }, | ||
| taskName: "test-task", | ||
| wantErr: false, | ||
| checkFunc: func(t *testing.T, result *Result) { | ||
| if len(result.Skills.Skills) != 1 { | ||
| t.Fatalf("expected 1 skill, got %d", len(result.Skills.Skills)) | ||
| } | ||
| skill := result.Skills.Skills[0] | ||
| if skill.Name != "opencode-skill" { | ||
| t.Errorf("expected skill name 'opencode-skill', got %q", skill.Name) | ||
| } | ||
| }, | ||
| }, | ||
| { | ||
| name: "discover skills from .github/skills directory", | ||
| setup: func(t *testing.T, dir string) { | ||
| // Create task | ||
| createTask(t, dir, "test-task", "", "Test task content") | ||
|
|
||
| // Create skill in .github/skills directory | ||
| skillDir := filepath.Join(dir, ".github", "skills", "copilot-skill") | ||
| if err := os.MkdirAll(skillDir, 0o755); err != nil { | ||
| t.Fatalf("failed to create skill directory: %v", err) | ||
| } | ||
|
|
||
| skillContent := `--- | ||
| name: copilot-skill | ||
| description: A skill for GitHub Copilot | ||
| --- | ||
| # Copilot Skill | ||
| This is a skill for GitHub Copilot. | ||
| ` | ||
| skillPath := filepath.Join(skillDir, "SKILL.md") | ||
| if err := os.WriteFile(skillPath, []byte(skillContent), 0o644); err != nil { | ||
| t.Fatalf("failed to create skill file: %v", err) | ||
| } | ||
| }, | ||
| taskName: "test-task", | ||
| wantErr: false, | ||
| checkFunc: func(t *testing.T, result *Result) { | ||
| if len(result.Skills.Skills) != 1 { | ||
| t.Fatalf("expected 1 skill, got %d", len(result.Skills.Skills)) | ||
| } | ||
| skill := result.Skills.Skills[0] | ||
| if skill.Name != "copilot-skill" { | ||
| t.Errorf("expected skill name 'copilot-skill', got %q", skill.Name) | ||
| } | ||
| }, | ||
| }, | ||
| { | ||
| name: "discover skills from .augment/skills directory", | ||
| setup: func(t *testing.T, dir string) { | ||
| // Create task | ||
| createTask(t, dir, "test-task", "", "Test task content") | ||
|
|
||
| // Create skill in .augment/skills directory | ||
| skillDir := filepath.Join(dir, ".augment", "skills", "augment-skill") | ||
| if err := os.MkdirAll(skillDir, 0o755); err != nil { | ||
| t.Fatalf("failed to create skill directory: %v", err) | ||
| } | ||
|
|
||
| skillContent := `--- | ||
| name: augment-skill | ||
| description: A skill for Augment | ||
| --- | ||
| # Augment Skill | ||
| This is a skill for Augment. | ||
| ` | ||
| skillPath := filepath.Join(skillDir, "SKILL.md") | ||
| if err := os.WriteFile(skillPath, []byte(skillContent), 0o644); err != nil { | ||
| t.Fatalf("failed to create skill file: %v", err) | ||
| } | ||
| }, | ||
| taskName: "test-task", | ||
| wantErr: false, | ||
| checkFunc: func(t *testing.T, result *Result) { | ||
| if len(result.Skills.Skills) != 1 { | ||
| t.Fatalf("expected 1 skill, got %d", len(result.Skills.Skills)) | ||
| } | ||
| skill := result.Skills.Skills[0] | ||
| if skill.Name != "augment-skill" { | ||
| t.Errorf("expected skill name 'augment-skill', got %q", skill.Name) | ||
| } | ||
| }, | ||
| }, | ||
| { | ||
| name: "discover skills from .windsurf/skills directory", | ||
| setup: func(t *testing.T, dir string) { | ||
| // Create task | ||
| createTask(t, dir, "test-task", "", "Test task content") | ||
|
|
||
| // Create skill in .windsurf/skills directory | ||
| skillDir := filepath.Join(dir, ".windsurf", "skills", "windsurf-skill") | ||
| if err := os.MkdirAll(skillDir, 0o755); err != nil { | ||
| t.Fatalf("failed to create skill directory: %v", err) | ||
| } | ||
|
|
||
| skillContent := `--- | ||
| name: windsurf-skill | ||
| description: A skill for Windsurf | ||
| --- | ||
| # Windsurf Skill | ||
| This is a skill for Windsurf. | ||
| ` | ||
| skillPath := filepath.Join(skillDir, "SKILL.md") | ||
| if err := os.WriteFile(skillPath, []byte(skillContent), 0o644); err != nil { | ||
| t.Fatalf("failed to create skill file: %v", err) | ||
| } | ||
| }, | ||
| taskName: "test-task", | ||
| wantErr: false, | ||
| checkFunc: func(t *testing.T, result *Result) { | ||
| if len(result.Skills.Skills) != 1 { | ||
| t.Fatalf("expected 1 skill, got %d", len(result.Skills.Skills)) | ||
| } | ||
| skill := result.Skills.Skills[0] | ||
| if skill.Name != "windsurf-skill" { | ||
| t.Errorf("expected skill name 'windsurf-skill', got %q", skill.Name) | ||
| } | ||
| }, | ||
| }, | ||
| { | ||
| name: "discover skills from .claude/skills directory", | ||
| setup: func(t *testing.T, dir string) { | ||
| // Create task | ||
| createTask(t, dir, "test-task", "", "Test task content") | ||
|
|
||
| // Create skill in .claude/skills directory | ||
| skillDir := filepath.Join(dir, ".claude", "skills", "claude-skill") | ||
| if err := os.MkdirAll(skillDir, 0o755); err != nil { | ||
| t.Fatalf("failed to create skill directory: %v", err) | ||
| } | ||
|
|
||
| skillContent := `--- | ||
| name: claude-skill | ||
| description: A skill for Claude | ||
| --- | ||
| # Claude Skill | ||
| This is a skill for Claude. | ||
| ` | ||
| skillPath := filepath.Join(skillDir, "SKILL.md") | ||
| if err := os.WriteFile(skillPath, []byte(skillContent), 0o644); err != nil { | ||
| t.Fatalf("failed to create skill file: %v", err) | ||
| } | ||
| }, | ||
| taskName: "test-task", | ||
| wantErr: false, | ||
| checkFunc: func(t *testing.T, result *Result) { | ||
| if len(result.Skills.Skills) != 1 { | ||
| t.Fatalf("expected 1 skill, got %d", len(result.Skills.Skills)) | ||
| } | ||
| skill := result.Skills.Skills[0] | ||
| if skill.Name != "claude-skill" { | ||
| t.Errorf("expected skill name 'claude-skill', got %q", skill.Name) | ||
| } | ||
| }, | ||
| }, | ||
| { | ||
| name: "discover skills from .gemini/skills directory", | ||
| setup: func(t *testing.T, dir string) { | ||
| // Create task | ||
| createTask(t, dir, "test-task", "", "Test task content") | ||
|
|
||
| // Create skill in .gemini/skills directory | ||
| skillDir := filepath.Join(dir, ".gemini", "skills", "gemini-skill") | ||
| if err := os.MkdirAll(skillDir, 0o755); err != nil { | ||
| t.Fatalf("failed to create skill directory: %v", err) | ||
| } | ||
|
|
||
| skillContent := `--- | ||
| name: gemini-skill | ||
| description: A skill for Gemini | ||
| --- | ||
| # Gemini Skill | ||
| This is a skill for Gemini. | ||
| ` | ||
| skillPath := filepath.Join(skillDir, "SKILL.md") | ||
| if err := os.WriteFile(skillPath, []byte(skillContent), 0o644); err != nil { | ||
| t.Fatalf("failed to create skill file: %v", err) | ||
| } | ||
| }, | ||
| taskName: "test-task", | ||
| wantErr: false, | ||
| checkFunc: func(t *testing.T, result *Result) { | ||
| if len(result.Skills.Skills) != 1 { | ||
| t.Fatalf("expected 1 skill, got %d", len(result.Skills.Skills)) | ||
| } | ||
| skill := result.Skills.Skills[0] | ||
| if skill.Name != "gemini-skill" { | ||
| t.Errorf("expected skill name 'gemini-skill', got %q", skill.Name) | ||
| } | ||
| }, | ||
| }, | ||
| { | ||
| name: "discover skills from .codex/skills directory", | ||
| setup: func(t *testing.T, dir string) { | ||
| // Create task | ||
| createTask(t, dir, "test-task", "", "Test task content") | ||
|
|
||
| // Create skill in .codex/skills directory | ||
| skillDir := filepath.Join(dir, ".codex", "skills", "codex-skill") | ||
| if err := os.MkdirAll(skillDir, 0o755); err != nil { | ||
| t.Fatalf("failed to create skill directory: %v", err) | ||
| } | ||
|
|
||
| skillContent := `--- | ||
| name: codex-skill | ||
| description: A skill for Codex | ||
| --- | ||
| # Codex Skill | ||
| This is a skill for Codex. | ||
| ` | ||
| skillPath := filepath.Join(skillDir, "SKILL.md") | ||
| if err := os.WriteFile(skillPath, []byte(skillContent), 0o644); err != nil { | ||
| t.Fatalf("failed to create skill file: %v", err) | ||
| } | ||
| }, | ||
| taskName: "test-task", | ||
| wantErr: false, | ||
| checkFunc: func(t *testing.T, result *Result) { | ||
| if len(result.Skills.Skills) != 1 { | ||
| t.Fatalf("expected 1 skill, got %d", len(result.Skills.Skills)) | ||
| } | ||
| skill := result.Skills.Skills[0] | ||
| if skill.Name != "codex-skill" { | ||
| t.Errorf("expected skill name 'codex-skill', got %q", skill.Name) | ||
| } | ||
| }, | ||
| }, |
Copilot
AI
Dec 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is significant code duplication across all seven new test cases. According to the custom coding guideline, this project uses table-driven tests as the standard. Consider consolidating these similar test cases into a single table-driven test with different agent directories as parameters to reduce duplication and improve maintainability.
| checkFunc: func(t *testing.T, result *Result) { | ||
| if len(result.Skills.Skills) != 1 { | ||
| t.Fatalf("expected 1 skill, got %d", len(result.Skills.Skills)) | ||
| } | ||
| skill := result.Skills.Skills[0] | ||
| if skill.Name != "copilot-skill" { | ||
| t.Errorf("expected skill name 'copilot-skill', got %q", skill.Name) | ||
| } | ||
| }, |
Copilot
AI
Dec 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test verification is incomplete compared to the existing .cursor/skills test. It should also verify the Description field and Location field to ensure the skill metadata is properly extracted and populated. The .cursor test at lines 2318-2323 checks both of these fields.
| checkFunc: func(t *testing.T, result *Result) { | ||
| if len(result.Skills.Skills) != 1 { | ||
| t.Fatalf("expected 1 skill, got %d", len(result.Skills.Skills)) | ||
| } | ||
| skill := result.Skills.Skills[0] | ||
| if skill.Name != "augment-skill" { | ||
| t.Errorf("expected skill name 'augment-skill', got %q", skill.Name) | ||
| } | ||
| }, |
Copilot
AI
Dec 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test verification is incomplete compared to the existing .cursor/skills test. It should also verify the Description field and Location field to ensure the skill metadata is properly extracted and populated. The .cursor test at lines 2318-2323 checks both of these fields.
| checkFunc: func(t *testing.T, result *Result) { | ||
| if len(result.Skills.Skills) != 1 { | ||
| t.Fatalf("expected 1 skill, got %d", len(result.Skills.Skills)) | ||
| } | ||
| skill := result.Skills.Skills[0] | ||
| if skill.Name != "gemini-skill" { | ||
| t.Errorf("expected skill name 'gemini-skill', got %q", skill.Name) | ||
| } | ||
| }, |
Copilot
AI
Dec 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test verification is incomplete compared to the existing .cursor/skills test. It should also verify the Description field and Location field to ensure the skill metadata is properly extracted and populated. The .cursor test at lines 2318-2323 checks both of these fields.
| checkFunc: func(t *testing.T, result *Result) { | ||
| if len(result.Skills.Skills) != 1 { | ||
| t.Fatalf("expected 1 skill, got %d", len(result.Skills.Skills)) | ||
| } | ||
| skill := result.Skills.Skills[0] | ||
| if skill.Name != "codex-skill" { | ||
| t.Errorf("expected skill name 'codex-skill', got %q", skill.Name) | ||
| } | ||
| }, |
Copilot
AI
Dec 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test verification is incomplete compared to the existing .cursor/skills test. It should also verify the Description field and Location field to ensure the skill metadata is properly extracted and populated. The .cursor test at lines 2318-2323 checks both of these fields.
All AI coding agents now support skills directories. Added
skillsPathconfiguration for all agents using a consistent directory structure pattern.Changes
Agent Configuration (
pkg/codingcontext/agent_paths.go).opencode/skills.github/skills.claude/skills.gemini/skills.augment/skills.windsurf/skills.codex/skillsAll agents now follow the same directory structure pattern for skills support.
Testing (
pkg/codingcontext/context_test.go)Added 7 test cases verifying skill discovery from each new agent directory:
{ name: "discover skills from .opencode/skills directory", setup: func(t *testing.T, dir string) { skillDir := filepath.Join(dir, ".opencode", "skills", "opencode-skill") // Creates SKILL.md with frontmatter }, checkFunc: func(t *testing.T, result *Result) { // Verifies skill is discovered and metadata extracted }, }Similar tests added for Copilot, Claude, Gemini, Augment, Windsurf, and Codex.
Documentation (
docs/reference/search-paths.md)Updated skill search paths list and examples to include all agent directories.
Rationale
Assuming all agents support skills using the same directory structure pattern (
.{agent}/skills). This provides consistent functionality across all supported AI coding agents.Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.