diff --git a/README.md b/README.md index 7a78bad..fb40c2b 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ This tool collects context from predefined rule files and a task-specific prompt - **Dynamic Context Assembly**: Merges context from various source files. - **Task-Specific Prompts**: Use different prompts for different tasks (e.g., `feature`, `bugfix`). - **Rule-Based Context**: Define reusable context snippets (rules) that can be included or excluded. -- **Frontmatter Filtering**: Select rules based on metadata using frontmatter selectors (matches top-level YAML fields only). +- **Skill Support**: Define reusable capabilities that AI agents can leverage (see [agentskills.io](https://agentskills.io)). +- **Frontmatter Filtering**: Select rules and skills based on metadata using frontmatter selectors (matches top-level YAML fields only). - **Bootstrap Scripts**: Run scripts to fetch or generate context dynamically. - **Parameter Substitution**: Inject values into your task prompts. - **Token Estimation**: Get an estimate of the total token count for the generated context. @@ -29,9 +30,9 @@ This tool is compatible with configuration files from various AI coding agents a - **[OpenCode.ai](https://opencode.ai/)**: `.opencode/agent`, `.opencode/command`, `.opencode/rules` - **[GitHub Copilot](https://github.com/features/copilot)**: `.github/copilot-instructions.md`, `.github/agents` - **[Google Gemini](https://gemini.google.com/)**: `GEMINI.md`, `.gemini/styleguide.md` -- **Generic AI Agents**: `AGENTS.md`, `.agents/rules`, `.agents/commands` (reusable content blocks), `.agents/tasks` +- **Generic AI Agents**: `AGENTS.md`, `.agents/rules`, `.agents/commands` (reusable content blocks), `.agents/tasks`, `.agents/skills` (reusable capabilities) -The tool automatically discovers and includes rules from these locations in your project and user home directory (`~`). +The tool automatically discovers and includes rules and skills from these locations in your project and user home directory (`~`). ## Agentic Workflows diff --git a/docs/how-to/create-skills.md b/docs/how-to/create-skills.md new file mode 100644 index 0000000..70498c7 --- /dev/null +++ b/docs/how-to/create-skills.md @@ -0,0 +1,372 @@ +--- +layout: default +title: Create Skills +parent: How-To Guides +nav_order: 5 +--- + +# How to Create Skills + +This guide shows you how to create skill files that provide reusable capabilities for AI agents. + +## What are Skills? + +Skills are reusable capability definitions that AI agents can leverage during task execution. Unlike rules (which provide context and guidelines), skills define specific capabilities, methodologies, or domain expertise that agents can use. + +Skills follow the format described at [agentskills.io](https://agentskills.io) and are output in XML-like format for easy reference. + +## Basic Skill Structure + +Skills are stored in `.agents/skills/(skill-name)/SKILL.md`: + +``` +.agents/skills/ +├── code-review/ +│ └── SKILL.md +├── debugging/ +│ └── SKILL.md +└── testing/ + └── SKILL.md +``` + +## Creating Your First Skill + +### 1. Create the Directory Structure + +```bash +mkdir -p .agents/skills/code-review +``` + +### 2. Create the Skill File + +Create `.agents/skills/code-review/SKILL.md`: + +```markdown +--- +skill_name: code-review +languages: + - go + - python + - javascript +--- + +# Code Review Skill + +This skill provides comprehensive code review capabilities including: + +## Capabilities + +- **Style Checking**: Verify code follows established style guides +- **Security Analysis**: Identify potential security vulnerabilities +- **Performance Review**: Suggest performance optimizations +- **Best Practices**: Ensure code follows language-specific best practices + +## Usage + +When performing code reviews, consider: + +1. **Readability**: Is the code easy to understand? +2. **Maintainability**: Can it be easily modified? +3. **Test Coverage**: Are there adequate tests? +4. **Error Handling**: Are errors properly handled? +5. **Documentation**: Is the code well-documented? + +## Review Checklist + +- [ ] Code follows style guide +- [ ] No security vulnerabilities +- [ ] Adequate test coverage +- [ ] Clear error handling +- [ ] Documentation is complete +``` + +### 3. Test the Skill + +```bash +coding-context -s languages=go code-review-task +``` + +The skill will be included in a dedicated "Skills" section: + +```xml +# Skills + +The following skills are available for use in this task... + + +# Code Review Skill +... + +``` + +## Skill Selection with Frontmatter + +### Task-Specific Skills + +Create a skill that only applies to specific tasks: + +```markdown +--- +skill_name: debugging +task_names: + - fix-bug + - troubleshoot +--- + +# Debugging Skill + +Systematic approach to finding and fixing bugs... +``` + +This skill will only be included when running `fix-bug` or `troubleshoot` tasks. + +### Language-Specific Skills + +Create skills for specific programming languages: + +```markdown +--- +skill_name: go-concurrency +languages: + - go +--- + +# Go Concurrency Skill + +Best practices for Go goroutines and channels... +``` + +Use it: +```bash +coding-context -s languages=go implement-feature +``` + +### Agent-Specific Skills + +Create skills optimized for specific AI agents: + +```markdown +--- +skill_name: cursor-shortcuts +agent: cursor +--- + +# Cursor IDE Skill + +Keyboard shortcuts and features specific to Cursor... +``` + +## Common Skill Types + +### 1. Domain Expertise Skills + +```markdown +--- +skill_name: security-review +--- + +# Security Review Skill + +## Common Vulnerabilities + +- SQL Injection +- Cross-Site Scripting (XSS) +- Authentication Issues +... +``` + +### 2. Methodology Skills + +```markdown +--- +skill_name: tdd-approach +--- + +# Test-Driven Development Skill + +## TDD Cycle + +1. Write a failing test +2. Write minimal code to pass +3. Refactor +4. Repeat +``` + +### 3. Tool Usage Skills + +```markdown +--- +skill_name: git-workflow +--- + +# Git Workflow Skill + +## Branch Strategy + +- `main`: Production-ready code +- `develop`: Integration branch +- `feature/*`: New features +... +``` + +## Skill vs Rule Guidelines + +Use **Skills** for: +- Reusable capabilities agents can apply +- Methodologies and approaches +- Tool-specific instructions +- Domain expertise (security, testing, etc.) + +Use **Rules** for: +- Project-specific context +- Coding standards and style guides +- Architecture decisions +- Team conventions + +## Best Practices + +### 1. Clear Skill Names + +```yaml +# ✅ Good - descriptive name +skill_name: code-review + +# ❌ Bad - too generic +skill_name: review +``` + +### 2. Structured Content + +Organize skills with clear sections: +- Overview/Purpose +- Capabilities +- Usage instructions +- Examples +- Checklists + +### 3. Focused Scope + +Each skill should cover one area of expertise: + +```markdown +# ✅ Good - focused on one capability +skill_name: security-analysis + +# ❌ Bad - trying to do too much +skill_name: all-code-quality-checks +``` + +### 4. Use Selectors Wisely + +Add selectors to limit when skills are included: + +```yaml +# Include only for specific tasks +task_names: + - security-review + - penetration-test + +# Include only for specific languages +languages: + - go + - rust +``` + +## Example: Creating a Testing Skill + +Complete example of a comprehensive testing skill: + +**File:** `.agents/skills/testing/SKILL.md` + +```markdown +--- +skill_name: testing +languages: + - go + - python + - javascript +--- + +# Testing Skill + +Comprehensive testing approach for software development. + +## Test Types + +### Unit Tests +- Test individual functions/methods +- Fast execution +- No external dependencies + +### Integration Tests +- Test component interactions +- May use test databases +- Slower than unit tests + +### End-to-End Tests +- Test complete workflows +- Use real or staging environments +- Slowest but most comprehensive + +## Testing Strategy + +1. **Start with unit tests** - Cover all pure functions +2. **Add integration tests** - Test component boundaries +3. **Include edge cases** - Test error conditions +4. **Consider E2E tests** - For critical user workflows + +## Code Coverage Goals + +- **Minimum**: 70% for new code +- **Target**: 80-90% overall +- **Critical paths**: 100% coverage + +## Best Practices + +- ✅ Test behavior, not implementation +- ✅ Use descriptive test names +- ✅ Follow AAA pattern (Arrange, Act, Assert) +- ✅ Keep tests independent +- ✅ Mock external dependencies + +## Testing Checklist + +- [ ] All public functions have unit tests +- [ ] Edge cases are covered +- [ ] Error handling is tested +- [ ] Integration points are tested +- [ ] Tests run quickly +- [ ] Tests are maintainable +``` + +## Troubleshooting + +### Skill Not Being Included + +Check: +1. File is named exactly `SKILL.md` (case-sensitive) +2. File is in a subdirectory under `.agents/skills/` +3. Selectors match (use verbose logging to debug) + +```bash +# Check what's being included +coding-context your-task 2>&1 | grep "Including skill" +``` + +### Wrong Skills Being Included + +Review your selectors: + +```bash +# Check current selectors +coding-context your-task 2>&1 | grep "Selectors" +``` + +Add more specific selectors to your skill frontmatter. + +## See Also + +- [File Formats Reference](../reference/file-formats#skill-files) - Skill format specification +- [Search Paths Reference](../reference/search-paths#skill-file-search-paths) - Where skills are found +- [How to Create Rules](./create-rules) - Similar concepts for rules +- [agentskills.io](https://agentskills.io) - Skill format specification diff --git a/docs/how-to/index.md b/docs/how-to/index.md index 54765fd..1f3a1a7 100644 --- a/docs/how-to/index.md +++ b/docs/how-to/index.md @@ -15,6 +15,7 @@ These guides are problem-oriented and help you achieve specific goals. - [Create Task Files](./create-tasks) - Define what AI agents should do - [Create Rule Files](./create-rules) - Provide reusable context +- [Create Skill Files](./create-skills) - Define reusable capabilities - [Use Frontmatter Selectors](./use-selectors) - Filter rules and tasks - [Use Remote Directories](./use-remote-directories) - Load rules from Git, HTTP, or S3 - [Use with AI Agents](./use-with-ai-agents) - Integrate with various AI tools diff --git a/docs/reference/file-formats.md b/docs/reference/file-formats.md index 97a7d68..ccbd27b 100644 --- a/docs/reference/file-formats.md +++ b/docs/reference/file-formats.md @@ -775,6 +775,159 @@ curl -s -H "Authorization: Bearer $API_KEY" \ | jq -r '.fields' > /tmp/issue-data.json ``` +## Skill Files + +Skill files provide reusable capabilities that AI agents can leverage. They are stored in `.agents/skills/(skill-name)/SKILL.md` and follow a similar structure to rule files. + +### Format + +Skills must be named `SKILL.md` and placed in a subdirectory under `.agents/skills/`: + +``` +.agents/skills/ +├── code-review/ +│ └── SKILL.md +├── debugging/ +│ └── SKILL.md +└── testing/ + └── SKILL.md +``` + +Each skill file follows this format: + +```markdown +--- +skill_name: code-review +languages: + - go + - python +--- + +# Code Review Skill + +This skill provides comprehensive code review capabilities including: + +- Style checking +- Security analysis +- Performance review +``` + +### Frontmatter Fields (optional) + +#### `skill_name` (optional) + +**Type:** String +**Purpose:** Identifies the skill. Used in the XML-like output format. + +```yaml +--- +skill_name: debugging +--- +``` + +If not specified, a generic name will be used in the output. + +#### `task_names` (skill selector) + +Specifies which task(s) this skill applies to. Works the same as for rules. + +```yaml +--- +task_names: + - fix-bug + - debug-issue +--- +``` + +#### `languages` (skill selector) + +Specifies which programming language(s) this skill applies to. Works the same as for rules. + +```yaml +--- +languages: + - go + - python +--- +``` + +#### `agent` (skill selector) + +Specifies which AI agent this skill is intended for. + +```yaml +--- +agent: cursor +--- +``` + +#### `expand` (optional) + +Controls parameter expansion in skill content. Defaults to `true`. + +```yaml +--- +expand: false +--- +``` + +### Output Format + +Skills are included in a dedicated "Skills" section in the output, formatted with XML-like tags: + +```xml +# Skills + +The following skills are available for use in this task. Each skill provides specific capabilities that you can leverage. Use the XML-like format shown below to reference skills in your work. + + +# Code Review Skill + +This skill provides comprehensive code review capabilities... + + + +# Debugging Skill + +This skill helps with systematic debugging... + +``` + +### Selection Behavior + +Skills are selected using the same selector mechanism as rules: + +- Skills without frontmatter selectors are included for all tasks +- Skills with `task_names` are only included when the task name matches +- Skills with `languages` are only included when the language selector matches +- Multiple conditions use AND logic (all must match) +- Array values in frontmatter use OR logic (any element can match) + +**Example:** + +```bash +# Includes only skills with languages=go +coding-context -s languages=go implement-feature +``` + +### Use Cases + +Skills are ideal for: +- **Reusable Capabilities**: Define capabilities that agents can use across tasks +- **Domain Expertise**: Provide specialized knowledge (security, testing, debugging) +- **Tool Instructions**: Document how to use specific tools or frameworks +- **Best Practices**: Share methodology and approaches + +### Skills vs Rules + +| Aspect | Rules | Skills | +|--------|-------|--------| +| Location | `.agents/rules/` | `.agents/skills/(skill-name)/SKILL.md` | +| Purpose | Context and guidelines | Reusable capabilities | +| Output Format | Plain text | XML-like `` tags | +| Output Section | Inline with content | Dedicated "Skills" section | +| Selection | Selectors | Selectors | + ## YAML Frontmatter Specification ### Valid Frontmatter diff --git a/docs/reference/search-paths.md b/docs/reference/search-paths.md index 0ffd7c6..976d911 100644 --- a/docs/reference/search-paths.md +++ b/docs/reference/search-paths.md @@ -62,6 +62,60 @@ coding-context plan-feature → Uses ~/.agents/tasks/plan-feature.md (from **Note:** The working directory and home directory are automatically added to search paths, so tasks in those locations are found automatically. +## Skill File Search Paths + +Skill files provide reusable capabilities for AI agents. Within each directory, skill files are searched in: + +1. `.agents/skills/(skill-name)/SKILL.md` + +### Directory Structure + +Skills must be organized in subdirectories under `.agents/skills/`, with each skill in its own folder containing a `SKILL.md` file: + +``` +.agents/skills/ +├── code-review/ +│ └── SKILL.md +├── debugging/ +│ └── SKILL.md +└── testing/ + └── SKILL.md +``` + +### Discovery Rules + +- The CLI searches all subdirectories under `.agents/skills/` +- Each subdirectory must contain a `SKILL.md` file +- Skills are selected using the same selector mechanism as rules +- Skills without selectors are included for all tasks +- Multiple skills can be included in a single context assembly + +### Example + +``` +Project structure: +./.agents/skills/code-review/SKILL.md (skill for code review) +./.agents/skills/debugging/SKILL.md (skill for debugging, has task_names: [fix-bug]) +./.agents/skills/testing/SKILL.md (skill for testing) + +Commands: +coding-context implement-feature → Includes code-review and testing skills +coding-context fix-bug → Includes code-review, debugging, and testing skills +coding-context -s languages=go implement-feature → Includes only skills matching languages=go +``` + +Skills are output in a dedicated "Skills" section with XML-like formatting: + +```xml +# Skills + +The following skills are available for use in this task... + + +...skill content... + +``` + ## Rule File Search Paths Rule files are discovered from directories specified via the `-d` flag (plus automatically-added working directory and home directory). Within each directory, the CLI searches for all standard file patterns listed below. diff --git a/examples/agents/skills/code-review/SKILL.md b/examples/agents/skills/code-review/SKILL.md new file mode 100644 index 0000000..3fc2055 --- /dev/null +++ b/examples/agents/skills/code-review/SKILL.md @@ -0,0 +1,24 @@ +--- +skill_name: code-review +languages: + - go + - python + - javascript +--- + +# Code Review Skill + +This skill provides comprehensive code review capabilities including: + +- **Style Checking**: Verify code follows established style guides +- **Security Analysis**: Identify potential security vulnerabilities +- **Performance Review**: Suggest performance optimizations +- **Best Practices**: Ensure code follows language-specific best practices + +## Usage + +When performing code reviews, consider: +1. Readability and maintainability +2. Test coverage +3. Error handling +4. Documentation quality diff --git a/examples/agents/skills/debugging/SKILL.md b/examples/agents/skills/debugging/SKILL.md new file mode 100644 index 0000000..017b642 --- /dev/null +++ b/examples/agents/skills/debugging/SKILL.md @@ -0,0 +1,23 @@ +--- +skill_name: debugging +task_names: + - fix-bug + - debug-issue +--- + +# Debugging Skill + +This skill helps with systematic debugging and troubleshooting: + +- **Root Cause Analysis**: Identify the underlying cause of issues +- **Log Analysis**: Parse and interpret log files effectively +- **Stack Trace Reading**: Understand error traces and call stacks +- **Reproduction Steps**: Create minimal reproducible examples + +## Debugging Approach + +1. Understand the expected behavior +2. Identify the actual behavior +3. Form hypotheses about the cause +4. Test hypotheses systematically +5. Fix and verify the solution diff --git a/integration_test.go b/integration_test.go index 60fe9d1..7cbb757 100644 --- a/integration_test.go +++ b/integration_test.go @@ -1718,3 +1718,155 @@ task_name: whitespace-task }) } } + +func TestSkillDiscovery(t *testing.T) { + dirs := setupTestDirs(t) + + // Create skills directory structure + skillsDir := filepath.Join(dirs.tmpDir, ".agents", "skills") + codeReviewSkillDir := filepath.Join(skillsDir, "code-review") + debuggingSkillDir := filepath.Join(skillsDir, "debugging") + + if err := os.MkdirAll(codeReviewSkillDir, 0o755); err != nil { + t.Fatalf("failed to create code-review skill dir: %v", err) + } + if err := os.MkdirAll(debuggingSkillDir, 0o755); err != nil { + t.Fatalf("failed to create debugging skill dir: %v", err) + } + + // Create code-review skill + codeReviewSkillFile := filepath.Join(codeReviewSkillDir, "SKILL.md") + codeReviewContent := `--- +skill_name: code-review +--- +# Code Review Skill + +This skill provides code review capabilities. +` + if err := os.WriteFile(codeReviewSkillFile, []byte(codeReviewContent), 0o644); err != nil { + t.Fatalf("failed to write code-review skill file: %v", err) + } + + // Create debugging skill with task selector + debuggingSkillFile := filepath.Join(debuggingSkillDir, "SKILL.md") + debuggingContent := `--- +skill_name: debugging +task_names: + - fix-bug +--- +# Debugging Skill + +This skill helps with debugging. +` + if err := os.WriteFile(debuggingSkillFile, []byte(debuggingContent), 0o644); err != nil { + t.Fatalf("failed to write debugging skill file: %v", err) + } + + // Create a simple task + createStandardTask(t, dirs.tasksDir, "fix-bug") + + // Run the tool and check output includes skills + output := runTool(t, "-C", dirs.tmpDir, "fix-bug") + + // Check that Skills section is present + if !strings.Contains(output, "# Skills") { + t.Errorf("expected output to contain '# Skills', got:\n%s", output) + } + + // Check that skills are wrapped in XML-like format + if !strings.Contains(output, ``) { + t.Errorf("expected output to contain code-review skill in XML format, got:\n%s", output) + } + + if !strings.Contains(output, ``) { + t.Errorf("expected output to contain debugging skill in XML format, got:\n%s", output) + } + + // Check that skill content is included + if !strings.Contains(output, "This skill provides code review capabilities") { + t.Errorf("expected output to contain code-review skill content, got:\n%s", output) + } + + if !strings.Contains(output, "This skill helps with debugging") { + t.Errorf("expected output to contain debugging skill content, got:\n%s", output) + } + + // Check for preamble + if !strings.Contains(output, "The following skills are available for use") { + t.Errorf("expected output to contain skills preamble, got:\n%s", output) + } +} + +func TestSkillSelectors(t *testing.T) { + dirs := setupTestDirs(t) + + // Create skills directory structure + skillsDir := filepath.Join(dirs.tmpDir, ".agents", "skills") + goSkillDir := filepath.Join(skillsDir, "go-specific") + pythonSkillDir := filepath.Join(skillsDir, "python-specific") + + if err := os.MkdirAll(goSkillDir, 0o755); err != nil { + t.Fatalf("failed to create go-specific skill dir: %v", err) + } + if err := os.MkdirAll(pythonSkillDir, 0o755); err != nil { + t.Fatalf("failed to create python-specific skill dir: %v", err) + } + + // Create Go-specific skill + goSkillFile := filepath.Join(goSkillDir, "SKILL.md") + goContent := `--- +skill_name: go-specific +languages: + - go +--- +# Go Specific Skill + +This skill is for Go development. +` + if err := os.WriteFile(goSkillFile, []byte(goContent), 0o644); err != nil { + t.Fatalf("failed to write go-specific skill file: %v", err) + } + + // Create Python-specific skill + pythonSkillFile := filepath.Join(pythonSkillDir, "SKILL.md") + pythonContent := `--- +skill_name: python-specific +languages: + - python +--- +# Python Specific Skill + +This skill is for Python development. +` + if err := os.WriteFile(pythonSkillFile, []byte(pythonContent), 0o644); err != nil { + t.Fatalf("failed to write python-specific skill file: %v", err) + } + + // Create a task with language selector + taskFile := filepath.Join(dirs.tasksDir, "implement-feature.md") + taskContent := `--- +task_name: implement-feature +selectors: + languages: go +--- +# Implement Feature + +Implement a new feature. +` + if err := os.WriteFile(taskFile, []byte(taskContent), 0o644); err != nil { + t.Fatalf("failed to write task file: %v", err) + } + + // Run the tool + output := runTool(t, "-C", dirs.tmpDir, "implement-feature") + + // Check that only Go skill is included + if !strings.Contains(output, "This skill is for Go development") { + t.Errorf("expected output to contain Go skill, got:\n%s", output) + } + + // Check that Python skill is NOT included + if strings.Contains(output, "This skill is for Python development") { + t.Errorf("expected output to NOT contain Python skill, got:\n%s", output) + } +} diff --git a/pkg/codingcontext/context.go b/pkg/codingcontext/context.go index b37915c..01f7b37 100644 --- a/pkg/codingcontext/context.go +++ b/pkg/codingcontext/context.go @@ -26,8 +26,9 @@ type Context struct { manifestURL string searchPaths []string downloadedPaths []string - task markdown.Markdown[markdown.TaskFrontMatter] // Parsed task - rules []markdown.Markdown[markdown.RuleFrontMatter] // Collected rule files + task markdown.Markdown[markdown.TaskFrontMatter] // Parsed task + rules []markdown.Markdown[markdown.RuleFrontMatter] // Collected rule files + skills []markdown.Markdown[markdown.SkillFrontMatter] // Collected skill files totalTokens int logger *slog.Logger cmdRunner func(cmd *exec.Cmd) error @@ -42,6 +43,7 @@ func New(opts ...Option) *Context { params: make(taskparser.Params), includes: make(selectors.Selectors), rules: make([]markdown.Markdown[markdown.RuleFrontMatter], 0), + skills: make([]markdown.Markdown[markdown.SkillFrontMatter], 0), logger: slog.New(slog.NewTextHandler(os.Stderr, nil)), cmdRunner: func(cmd *exec.Cmd) error { return cmd.Run() @@ -330,20 +332,37 @@ func (cc *Context) Run(ctx context.Context, taskName string) (*Result, error) { return nil, fmt.Errorf("failed to find and execute rule files: %w", err) } + if err := cc.findSkillFiles(ctx); err != nil { + return nil, fmt.Errorf("failed to find skill files: %w", err) + } + // Estimate tokens for task cc.logger.Info("Total estimated tokens", "tokens", cc.totalTokens) - // Build the combined prompt from all rules and task content + // Build the combined prompt from all rules, skills, and task content var promptBuilder strings.Builder for _, rule := range cc.rules { promptBuilder.WriteString(rule.Content) promptBuilder.WriteString("\n") } + + // Add Skills section if skills are present + if len(cc.skills) > 0 { + promptBuilder.WriteString("\n# Skills\n\n") + promptBuilder.WriteString("The following skills are available for use in this task. Each skill provides specific capabilities that you can leverage. Use the XML-like format shown below to reference skills in your work.\n\n") + for _, skill := range cc.skills { + promptBuilder.WriteString(fmt.Sprintf("\n", skill.FrontMatter.SkillName)) + promptBuilder.WriteString(skill.Content) + promptBuilder.WriteString("\n\n\n") + } + } + promptBuilder.WriteString(cc.task.Content) // Build and return the result result := &Result{ Rules: cc.rules, + Skills: cc.skills, Task: cc.task, Tokens: cc.totalTokens, Agent: cc.agent, @@ -518,6 +537,94 @@ func (cc *Context) findExecuteRuleFiles(ctx context.Context, homeDir string) err return nil } +func (cc *Context) findSkillFiles(ctx context.Context) error { + // Skip skill file discovery if resume mode is enabled + if cc.resume || (cc.includes != nil && cc.includes.GetValue("resume", "true")) { + return nil + } + + for _, basePath := range cc.downloadedPaths { + skillDirs := skillSearchPaths(basePath) + + for _, skillDir := range skillDirs { + if _, err := os.Stat(skillDir); os.IsNotExist(err) { + continue + } else if err != nil { + return fmt.Errorf("failed to stat directory %s: %w", skillDir, err) + } + + // Walk through skill subdirectories + entries, err := os.ReadDir(skillDir) + if err != nil { + return fmt.Errorf("failed to read skill directory %s: %w", skillDir, err) + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + // Look for SKILL.md in each subdirectory + skillPath := filepath.Join(skillDir, entry.Name(), "SKILL.md") + if _, err := os.Stat(skillPath); os.IsNotExist(err) { + continue + } else if err != nil { + return fmt.Errorf("failed to stat skill file %s: %w", skillPath, err) + } + + // Parse the skill file + var frontmatter markdown.SkillFrontMatter + md, err := markdown.ParseMarkdownFile(skillPath, &frontmatter) + if err != nil { + // Skip files that can't be parsed + cc.logger.Info("Skipping unparseable skill file", "path", skillPath, "error", err) + continue + } + + // Check if skill matches selectors + var baseFM markdown.BaseFrontMatter + baseFM.Content = frontmatter.Content + if !cc.includes.MatchesIncludes(baseFM) { + continue + } + + // Use directory name as fallback if skill_name is not specified + if frontmatter.SkillName == "" { + frontmatter.SkillName = entry.Name() + } + + // Expand parameters only if expand is not explicitly set to false + var processedContent string + if shouldExpandParams(frontmatter.ExpandParams) { + processedContent, err = cc.expandParams(md.Content, nil) + if err != nil { + return fmt.Errorf("failed to expand parameters in skill file %s: %w", skillPath, err) + } + } else { + processedContent = md.Content + } + tokens := tokencount.EstimateTokens(processedContent) + + cc.skills = append(cc.skills, markdown.Markdown[markdown.SkillFrontMatter]{ + FrontMatter: frontmatter, + Content: processedContent, + Tokens: tokens, + }) + + cc.totalTokens += tokens + + cc.logger.Info("Including skill file", "path", skillPath, "tokens", tokens) + + if err := cc.runBootstrapScript(ctx, skillPath); err != nil { + return fmt.Errorf("failed to run bootstrap script: %w", err) + } + } + } + } + + return nil +} + func (cc *Context) runBootstrapScript(ctx context.Context, path string) error { // Check for a bootstrap file named -bootstrap // For example, setup.md -> setup-bootstrap, setup.mdc -> setup-bootstrap diff --git a/pkg/codingcontext/markdown/frontmatter.go b/pkg/codingcontext/markdown/frontmatter.go index e9be92a..5784a99 100644 --- a/pkg/codingcontext/markdown/frontmatter.go +++ b/pkg/codingcontext/markdown/frontmatter.go @@ -154,3 +154,48 @@ func (r *RuleFrontMatter) UnmarshalJSON(data []byte) error { return nil } + +// SkillFrontMatter represents the frontmatter fields for skill files +type SkillFrontMatter struct { + BaseFrontMatter `yaml:",inline"` + + // SkillName is an optional identifier for the skill + SkillName string `yaml:"skill_name,omitempty" json:"skill_name,omitempty"` + + // TaskNames specifies which task(s) this skill applies to + // Array of task names for OR logic + TaskNames []string `yaml:"task_names,omitempty" json:"task_names,omitempty"` + + // Languages specifies which programming language(s) this skill applies to + // Array of languages for OR logic (e.g., ["go", "python"]) + Languages []string `yaml:"languages,omitempty" json:"languages,omitempty"` + + // Agent specifies which AI agent this skill is intended for + Agent string `yaml:"agent,omitempty" json:"agent,omitempty"` + + // ExpandParams controls whether parameter expansion should occur + // Defaults to true if not specified + ExpandParams *bool `yaml:"expand,omitempty" json:"expand,omitempty"` +} + +// UnmarshalJSON custom unmarshaler that populates both typed fields and Content map +func (s *SkillFrontMatter) UnmarshalJSON(data []byte) error { + // First unmarshal into a temporary type to avoid infinite recursion + type Alias SkillFrontMatter + aux := &struct { + *Alias + }{ + Alias: (*Alias)(s), + } + + if err := json.Unmarshal(data, aux); err != nil { + return fmt.Errorf("failed to unmarshal skill frontmatter: %w", err) + } + + // Also unmarshal into Content map + if err := json.Unmarshal(data, &s.BaseFrontMatter.Content); err != nil { + return fmt.Errorf("failed to unmarshal skill frontmatter content: %w", err) + } + + return nil +} diff --git a/pkg/codingcontext/paths.go b/pkg/codingcontext/paths.go index b6ed74a..e41d8e5 100644 --- a/pkg/codingcontext/paths.go +++ b/pkg/codingcontext/paths.go @@ -48,3 +48,11 @@ func commandSearchPaths(dir string) []string { filepath.Join(dir, ".opencode", "command"), } } + +// skillSearchPaths returns the search paths for skill files in a directory +// Skills are located in .agents/skills/(skill-name)/SKILL.md +func skillSearchPaths(dir string) []string { + return []string{ + filepath.Join(dir, ".agents", "skills"), + } +} diff --git a/pkg/codingcontext/result.go b/pkg/codingcontext/result.go index e4bd923..d69a984 100644 --- a/pkg/codingcontext/result.go +++ b/pkg/codingcontext/result.go @@ -7,11 +7,12 @@ import ( // Result holds the assembled context from running a task type Result struct { - Rules []markdown.Markdown[markdown.RuleFrontMatter] // List of included rule files - Task markdown.Markdown[markdown.TaskFrontMatter] // Task file with frontmatter and content - Tokens int // Total token count - Agent Agent // The agent used (from task or -a flag) - Prompt string // Combined prompt: all rules and task content + Rules []markdown.Markdown[markdown.RuleFrontMatter] // List of included rule files + Skills []markdown.Markdown[markdown.SkillFrontMatter] // List of included skill files + Task markdown.Markdown[markdown.TaskFrontMatter] // Task file with frontmatter and content + Tokens int // Total token count + Agent Agent // The agent used (from task or -a flag) + Prompt string // Combined prompt: all rules, skills and task content } // MCPServers returns all MCP server configurations from rules. diff --git a/pkg/codingcontext/selectors/selectors.go b/pkg/codingcontext/selectors/selectors.go index b4b91a8..da1a894 100644 --- a/pkg/codingcontext/selectors/selectors.go +++ b/pkg/codingcontext/selectors/selectors.go @@ -98,8 +98,27 @@ func (includes *Selectors) MatchesIncludes(frontmatter markdown.BaseFrontMatter) } // Check if frontmatter value matches any element in the inner map (OR logic) - fmStr := fmt.Sprint(fmValue) - if !values[fmStr] { + matched := false + + // Handle array values in frontmatter (e.g., languages: [go, python]) + // If any element of the array matches any selector value, it's a match + if fmArray, ok := fmValue.([]any); ok { + for _, elem := range fmArray { + elemStr := fmt.Sprint(elem) + if values[elemStr] { + matched = true + break + } + } + } else { + // Handle scalar values + fmStr := fmt.Sprint(fmValue) + if values[fmStr] { + matched = true + } + } + + if !matched { return false } }