Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
139 changes: 139 additions & 0 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,145 @@ them to delegate tasks to other agents:
transfer_task(agent="developer", task="Create a login form", expected_output="HTML and CSS code")
```

## Skills

Skills provide specialized instructions for specific tasks that agents can load on demand. When a user's request matches a skill's description, the agent reads the skill's `SKILL.md` file to get detailed instructions for that task.

### Enabling Skills

Enable skills for an agent by setting `skills: true` in the agent configuration. The agent must also have a `filesystem` toolset with `read_file` capability:

```yaml
agents:
root:
model: openai/gpt-4o
instruction: You are a helpful assistant.
skills: true
toolsets:
- type: filesystem # Required for reading skill files
```

### How Skills Work

When skills are enabled:

1. cagent scans default locations for `SKILL.md` files
2. Skill metadata (name, description, location) is injected into the agent's system prompt
3. When a user request matches a skill's description, the agent uses `read_file` to load the full instructions
4. The agent follows the skill's instructions to complete the task

### SKILL.md Format

Skills are defined as Markdown files with YAML frontmatter:

```markdown
---
name: my-skill
description: A brief description of what this skill does and when to use it
license: Apache-2.0
compatibility: Requires docker and git
metadata:
author: my-org
version: "1.0"
allowed-tools:
- Bash(git:*)
- Read
- Write
---

# Skill Instructions

Detailed instructions for the agent to follow when this skill is activated...
```

Required fields:
- `name`: Unique identifier for the skill
- `description`: Brief description used by the agent to determine when to use this skill

Optional fields:
- `license`: License for the skill
- `compatibility`: Requirements or compatibility notes
- `metadata`: Key-value pairs for additional metadata
- `allowed-tools`: List of tools the skill is designed to work with

### Default Skill Search Paths

Skills are automatically discovered from the following locations (in order, later overrides earlier):

**Global locations** (from home directory):
- `~/.codex/skills/` — Recursive search (Codex format)
- `~/.claude/skills/` — Flat search (Claude format)
- `~/.agents/skills/` — Flat search (Agent Skills standard)

**Project locations** (from git root up to current directory):
- `.claude/skills/` — Flat search, only at current working directory
- `.agents/skills/` — Flat search, scanned from git root to current directory

### Project Skill Discovery

For `.agents/skills`, cagent walks up from the current working directory to the git repository root, loading skills from each directory along the way. Skills in directories closer to your current working directory take precedence over those higher up in the hierarchy.

**Example directory structure:**
```
my-repo/ # Git root
├── .git/
├── .agents/skills/
│ └── repo-skill/
│ └── SKILL.md # Available everywhere in repo
└── frontend/
├── .agents/skills/
│ └── frontend-skill/
│ └── SKILL.md # Available in frontend/ and below
└── src/ # Current working directory
```

When working in `my-repo/frontend/src/`:
- Both `repo-skill` and `frontend-skill` are available
- If both define the same skill name, `frontend-skill` wins (closer to cwd)

### Skill Precedence

When multiple skills have the same name, the later-loaded skill wins:

1. Global skills load first (`~/.codex/skills/`, `~/.claude/skills/`, `~/.agents/skills/`)
2. Project skills load next, from git root toward current directory
3. Skills closer to the current directory override those further away

This allows:
- Global skills to provide defaults
- Repository-level skills to customize for a project
- Subdirectory skills to specialize further

### Creating Skills

To create a skill:

1. Create a directory in one of the search paths (e.g., `~/.agents/skills/my-skill/`)
2. Add a `SKILL.md` file with frontmatter and instructions
3. The skill will automatically be available to agents with `skills: true`

**Example:**

```bash
mkdir -p ~/.agents/skills/create-dockerfile
cat > ~/.agents/skills/create-dockerfile/SKILL.md << 'EOF'
---
name: create-dockerfile
description: Create optimized Dockerfiles for applications
---

# Creating Dockerfiles

When asked to create a Dockerfile:

1. Analyze the application type and language
2. Use multi-stage builds for compiled languages
3. Minimize image size by using slim base images
4. Follow security best practices (non-root user, etc.)
...
EOF
```

## RAG (Retrieval-Augmented Generation)

Give your agents access to document knowledge bases using cagent's modular RAG system. It supports:
Expand Down
86 changes: 84 additions & 2 deletions pkg/skills/skills.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,15 @@ type Skill struct {

// Load discovers and loads all skills from standard locations.
// Skills are loaded from (in order, later overrides earlier):
//
// Global locations (from home directory):
// - ~/.codex/skills/ (recursive)
// - ~/.claude/skills/ (flat)
// - ./.claude/skills/ (flat, project-local)
// - ~/.agents/skills/ (flat)
//
// Project locations (from git root up to cwd, closest wins):
// - .claude/skills/ (flat, only at cwd)
// - .agents/skills/ (flat, scanned from git root to cwd)
func Load() []Skill {
skillMap := make(map[string]Skill)

Expand All @@ -44,13 +50,26 @@ func Load() []Skill {
for _, skill := range loadSkillsFromDir(filepath.Join(homeDir, ".claude", "skills"), false) {
skillMap[skill.Name] = skill
}
// Load from agents user directory (flat)
for _, skill := range loadSkillsFromDir(filepath.Join(homeDir, ".agents", "skills"), false) {
skillMap[skill.Name] = skill
}
}

// Load from project directory (flat)
// Load from project directories
if cwd, err := os.Getwd(); err == nil {
// Load .claude/skills from cwd only (backward compatibility)
for _, skill := range loadSkillsFromDir(filepath.Join(cwd, ".claude", "skills"), false) {
skillMap[skill.Name] = skill
}

// Load .agents/skills from git root up to cwd (closest wins)
// We iterate from root to cwd so that later (closer) directories override earlier ones
for _, dir := range projectSearchDirs(cwd) {
for _, skill := range loadSkillsFromDir(filepath.Join(dir, ".agents", "skills"), false) {
skillMap[skill.Name] = skill
}
}
}

result := make([]Skill, 0, len(skillMap))
Expand All @@ -60,6 +79,69 @@ func Load() []Skill {
return result
}

// projectSearchDirs returns directories from git root to cwd (inclusive).
// If not in a git repo, returns only cwd.
// The returned slice is ordered from root to cwd so that closer directories
// can override skills from parent directories.
func projectSearchDirs(cwd string) []string {
absPath, err := filepath.Abs(cwd)
if err != nil {
return []string{cwd}
}

// Find git root by walking up
gitRoot := findGitRoot(absPath)
if gitRoot == "" {
// Not in a git repo, just return cwd
return []string{absPath}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent return value types - may return relative path in error case while returning absolute path in success case

The projectSearchDirs function has an inconsistency: when filepath.Abs fails (line 98), it returns []string{cwd} which could be a relative path, but when gitRoot is empty (line 105), it returns []string{absPath} which is an absolute path.

While this doesn't cause immediate functional failures because filepath.Join and os.ReadDir both handle relative and absolute paths, it represents a logic inconsistency that violates the function's implicit contract. The comment on lines 89-92 says "returns directories" without specifying absolute vs relative.

This inconsistency could cause subtle issues with path handling, logging, or if the paths are later used in contexts that expect absolute paths.

Suggestion: Return []string{absPath} in the error case on line 98 as well, or return an empty slice []string{} if Abs fails, rather than returning the potentially relative cwd. This would maintain consistency that all returned paths are absolute.

// Build list of directories from git root to cwd
var dirs []string
current := absPath
for {
dirs = append(dirs, current)
if current == gitRoot {
break
}
parent := filepath.Dir(current)
if parent == current {
// Reached filesystem root without finding git root (shouldn't happen)
break
}
current = parent
}

// Reverse so we go from root to cwd (earlier entries get overridden by later)
for i, j := 0, len(dirs)-1; i < j; i, j = i+1, j-1 {
dirs[i], dirs[j] = dirs[j], dirs[i]
}

return dirs
}

// findGitRoot finds the git repository root by looking for .git directory or file.
// Returns empty string if not in a git repository.
func findGitRoot(dir string) string {
current := dir
for {
gitPath := filepath.Join(current, ".git")
if info, err := os.Stat(gitPath); err == nil {
// .git can be a directory (normal repo) or a file (worktree/submodule)
if info.IsDir() || info.Mode().IsRegular() {
return current
}
}

parent := filepath.Dir(current)
if parent == current {
// Reached filesystem root
return ""
}
current = parent
}
}

// BuildSkillsPrompt generates a prompt section describing available skills.
func BuildSkillsPrompt(skills []Skill) string {
if len(skills) == 0 {
Expand Down
Loading
Loading