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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,13 @@ This tool is compatible with configuration files from various AI coding agents a
### Primary Supported Agents (with dedicated `-a` flag)

- **[GitHub Copilot](https://github.com/features/copilot)**: `.github/copilot-instructions.md`, `.github/agents/` (`-a copilot`)
- **[Anthropic Claude](https://claude.ai/)**: `CLAUDE.md`, `CLAUDE.local.md`, `.claude/` (directory) (`-a claude`)
- **[Anthropic Claude](https://claude.ai/)**: `CLAUDE.md`, `CLAUDE.local.md`, `.claude/CLAUDE.md`, `.claude/CLAUDE.local.md`, `.claude/rules/` (`-a claude`)
- **[Cursor](https://cursor.sh/)**: `.cursor/rules/`, `.cursorrules` (`-a cursor`)
- **[Google Gemini](https://gemini.google.com/)**: `GEMINI.md`, `.gemini/styleguide.md`, `.gemini/` (directory) (`-a gemini`)
- **[Google Gemini](https://gemini.google.com/)**: `GEMINI.md`, `.gemini/GEMINI.md`, `.gemini/styleguide.md`, `.gemini/rules/` (`-a gemini`)
- **[Augment](https://augmentcode.com/)**: `.augment/rules/`, `.augment/guidelines.md` (`-a augment`)
- **[Windsurf](https://codeium.com/windsurf)**: `.windsurf/rules/`, `.windsurfrules` (`-a windsurf`)
- **[OpenCode.ai](https://opencode.ai/)**: `.opencode/agent/`, `.opencode/rules/` (rules); `.opencode/command/` (commands) (`-a opencode`)
- **[Codex](https://codex.ai/)**: `AGENTS.md`, `.codex/` (directory) (`-a codex`)
- **[Codex](https://codex.ai/)**: `AGENTS.md`, `.codex/AGENTS.md`, `.codex/rules/` (`-a codex`)

### Additional Compatible Agents

Expand Down Expand Up @@ -273,7 +273,7 @@ The tool automatically searches for task and rule files in various locations. Fo
- `./.agents/commands/` - Reusable command blocks
- `./.agents/skills/*/SKILL.md` - Specialized skills
- Various agent-specific paths (`.cursor/`, `.github/`, `.opencode/`, etc.)
- User-wide rules in `~/.agents/rules`, `~/.claude/`, `~/.codex/`, etc.
- User-wide rules in `~/.agents/rules`, `~/.claude/rules`, `~/.codex/rules`, and explicit instruction files

See the full [Search Paths Reference](https://kitproj.github.io/coding-context-cli/reference/search-paths) for the complete list of locations.

Expand Down
6 changes: 3 additions & 3 deletions SPECIFICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -1270,14 +1270,14 @@ tokens ≈ (characters / 4) + (words / 0.75)

### 12.1 Supported AI Agents

- **Anthropic Claude**: `CLAUDE.md`, `.claude/`
- **Anthropic Claude**: `CLAUDE.md`, `CLAUDE.local.md`, `.claude/CLAUDE.md`, `.claude/CLAUDE.local.md`, `.claude/rules/`
- **GitHub Copilot**: `.github/copilot-instructions.md`, `.github/agents/`
- **Cursor**: `.cursor/rules`, `.cursorrules`
- **OpenCode.ai**: `.opencode/agent`, `.opencode/command`, `.opencode/rules`
- **Augment**: `.augment/rules`, `.augment/guidelines.md`
- **Windsurf**: `.windsurf/rules`, `.windsurfrules`
- **Google Gemini**: `GEMINI.md`, `.gemini/styleguide.md`
- **Codex**: `AGENTS.md`, `.codex/AGENTS.md`
- **Google Gemini**: `GEMINI.md`, `.gemini/GEMINI.md`, `.gemini/styleguide.md`, `.gemini/rules/`
- **Codex**: `AGENTS.md`, `.codex/AGENTS.md`, `.codex/rules/`

### 12.2 Backward Compatibility

Expand Down
22 changes: 16 additions & 6 deletions docs/reference/search-paths.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,9 @@ Rule files are discovered from directories specified via the `-d` flag (plus aut
.opencode/agent/
.opencode/rules/
.github/agents/
.claude/
.codex/
.gemini/
.claude/rules/
.codex/rules/
.gemini/rules/
```

**Specific files:**
Expand All @@ -218,6 +218,10 @@ AGENTS.md
.windsurfrules
.github/copilot-instructions.md
.gemini/styleguide.md
.gemini/GEMINI.md
.claude/CLAUDE.md
.claude/CLAUDE.local.md
.codex/AGENTS.md
.augment/guidelines.md
```

Expand All @@ -229,16 +233,22 @@ The CLI automatically discovers rules from configuration files for these AI codi

| Agent | Rule Locations |
|-------|----------------|
| **Anthropic Claude** | `CLAUDE.md`, `CLAUDE.local.md`, `.claude/` (all `.md`/`.mdc` files in directory) |
| **Codex** | `AGENTS.md`, `.codex/` (all `.md`/`.mdc` files in directory) |
| **Anthropic Claude** | `CLAUDE.md`, `CLAUDE.local.md`, `.claude/CLAUDE.md`, `.claude/CLAUDE.local.md`, `.claude/rules/` |
| **Codex** | `AGENTS.md`, `.codex/AGENTS.md`, `.codex/rules/` |
| **Cursor** | `.cursor/rules/`, `.cursorrules` |
| **Augment** | `.augment/rules/`, `.augment/guidelines.md` |
| **Windsurf** | `.windsurf/rules/`, `.windsurfrules` |
| **OpenCode.ai** | `.opencode/agent/`, `.opencode/rules/` (rules); `.opencode/command/` (commands) |
| **GitHub Copilot** | `.github/copilot-instructions.md`, `.github/agents/` |
| **Google Gemini** | `GEMINI.md`, `.gemini/styleguide.md`, `.gemini/` (all `.md`/`.mdc` files in directory) |
| **Google Gemini** | `GEMINI.md`, `.gemini/GEMINI.md`, `.gemini/styleguide.md`, `.gemini/rules/` |
| **Generic** | `.agents/rules/` (rules), `.agents/tasks/` (tasks), `.agents/commands/` (commands) |

### Migrating rules from agent directories

The entire `.claude/`, `.codex/`, and `.gemini/` directories are no longer recursively scanned as rules. Move other `.md`/`.mdc` rule files previously stored directly in those directories (or arbitrary subdirectories) into their `rules/` directory, or into `.agents/rules/`. The explicit instruction files listed above remain supported.

Skill bodies and their reference files, commands, and cached plugin content outside rule directories are not inlined as rules. Skills in the supported `skills/` locations remain available through metadata-only discovery; agents can read a skill when needed. This prevents prompt size from growing with the full content of every installed skill.

## Discovery Behavior

### File Types
Expand Down
10 changes: 7 additions & 3 deletions pkg/codingcontext/agent_paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ type agentPathsConfig struct {
// getAgentsPaths returns the map of each agent to its specific search paths.
// Empty string agent ("") represents the generic .agents directory structure.
// If a path is empty, it is not defined for that agent.
// Rule directories must be scoped explicitly: walking an entire agent directory
// would also inline skills, references, commands, and cached plugins as rules.
func getAgentsPaths() map[Agent]agentPathsConfig {
return map[Agent]agentPathsConfig{
// Generic .agents directory structure (empty agent name)
Expand Down Expand Up @@ -43,13 +45,15 @@ func getAgentsPaths() map[Agent]agentPathsConfig {
},
// Claude agent paths
AgentClaude: {
rulesPaths: []string{".claude", "CLAUDE.md", "CLAUDE.local.md"},
// Keep the instruction files ahead of nested rules, in the same lexical
// order as the former directory walk, including bootstrap execution.
rulesPaths: []string{".claude/CLAUDE.local.md", ".claude/CLAUDE.md", ".claude/rules", "CLAUDE.md", "CLAUDE.local.md"},
skillsPath: ".claude/skills",
// No commands or tasks paths defined for Claude
},
// Gemini agent paths
AgentGemini: {
rulesPaths: []string{".gemini/styleguide.md", ".gemini", "GEMINI.md"},
rulesPaths: []string{".gemini/styleguide.md", ".gemini/GEMINI.md", ".gemini/rules", "GEMINI.md"},
skillsPath: ".gemini/skills",
// No commands or tasks paths defined for Gemini
},
Expand All @@ -67,7 +71,7 @@ func getAgentsPaths() map[Agent]agentPathsConfig {
},
// Codex agent paths
AgentCodex: {
rulesPaths: []string{".codex", "AGENTS.md"},
rulesPaths: []string{".codex/AGENTS.md", ".codex/rules", "AGENTS.md"},
skillsPath: ".codex/skills",
// No commands or tasks paths defined for Codex
},
Expand Down
4 changes: 2 additions & 2 deletions pkg/codingcontext/paths_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ func TestRulePaths(t *testing.T) {
filepath.Join(testProjectDir, ".agents", "rules"),
filepath.Join(testProjectDir, ".cursor", "rules"),
filepath.Join(testProjectDir, ".cursorrules"),
filepath.Join(testProjectDir, ".claude"),
filepath.Join(testProjectDir, ".codex"),
filepath.Join(testProjectDir, ".claude", "rules"),
filepath.Join(testProjectDir, ".codex", "rules"),
},
},
}
Expand Down
130 changes: 130 additions & 0 deletions pkg/codingcontext/rule_scope_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package codingcontext

import (
"context"
"errors"
"fmt"
"io"
"log/slog"
"os/exec"
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/require"
)

func TestRun_RuleDiscoveryDoesNotInlineAgentContent(t *testing.T) {
t.Parallel()

for _, agentDir := range []string{".claude", ".codex", ".gemini"} {
t.Run(agentDir, func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
createTask(t, dir, "edit-readme", "single_shot: true", "Add hi to README.")
createRule(t, dir, filepath.Join(agentDir, "rules", "nested", "conventions.md"), "", "REPOSITORY_RULE")
createBootstrapScript(t, dir, filepath.Join(agentDir, "rules", "nested", "conventions.md"), "#!/bin/sh\nexit 0")
for i := range 100 {
skillDir := filepath.Join(agentDir, "skills", fmt.Sprintf("skill-%d", i))
createSkill(t, dir, skillDir, fmt.Sprintf("---\nname: skill-%d\ndescription: A useful skill\nbootstrap: |\n #!/bin/sh\n exit 0\n---\n%s", i, strings.Repeat("SKILL_BODY ", 1500)))
createRule(t, dir, filepath.Join(skillDir, "references", "guide.md"), "", "SKILL_REFERENCE")
}
for _, path := range []string{"commands/edit.md", "plugins/cache/plugin/skills/tool/SKILL.md", "notes.md"} {
createRule(t, dir, filepath.Join(agentDir, path), "", "UNRELATED_CONTENT")
}

cc := New(
WithLenientSearchPaths("file://"+dir),
WithLenientAgent(AgentCursor),
WithLogger(slog.New(slog.NewTextHandler(io.Discard, nil))),
)
var bootstraps []string
cc.cmdRunner = func(cmd *exec.Cmd) error {
bootstraps = append(bootstraps, cmd.Path)
return nil
}
result, err := cc.Run(context.Background(), "edit-readme")
require.NoError(t, err)
require.Equal(t, 1, len(result.Rules))
require.Len(t, result.Skills.Skills, 100)
require.Contains(t, result.Prompt, "REPOSITORY_RULE")
require.Contains(t, result.Prompt, "skill-99")
require.NotContains(t, result.Prompt, "SKILL_BODY")
require.NotContains(t, result.Prompt, "SKILL_REFERENCE")
require.NotContains(t, result.Prompt, "UNRELATED_CONTENT")
require.Less(t, len(result.Prompt), 50000, "prompt size should depend on skill metadata, not bodies")
require.Equal(t, []string{filepath.Join(dir, agentDir, "rules", "nested", "conventions-bootstrap")}, bootstraps,
"only the real rule bootstrap should execute")
})
}
}

func TestRun_AgentInstructionsBootstrapBeforeNestedRules(t *testing.T) {
t.Parallel()
for _, instruction := range []string{".claude/CLAUDE.md", ".codex/AGENTS.md", ".gemini/GEMINI.md"} {
t.Run(instruction, func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
createTask(t, dir, "task", "", "TASK_INSTRUCTIONS")
createRule(t, dir, instruction, "", "INITIALIZATION_RULE")
createBootstrapScript(t, dir, instruction, "#!/bin/sh\nexit 0")
nested := filepath.Join(filepath.Dir(instruction), "rules", "dependent.md")
createRule(t, dir, nested, "", "DEPENDENT_RULE")
createBootstrapScript(t, dir, nested, "#!/bin/sh\nexit 0")
cc := New(WithSearchPaths("file://" + dir))
initialized := false
cc.cmdRunner = func(cmd *exec.Cmd) error {
if filepath.Base(cmd.Path) == "dependent-bootstrap" {
if !initialized {
return errors.New("agent instructions have not initialized the workspace")
}
return nil
}
initialized = true
return nil
}
result, err := cc.Run(context.Background(), "task")
require.NoError(t, err)
require.Len(t, result.Rules, 2)
require.Equal(t, []string{"INITIALIZATION_RULE", "DEPENDENT_RULE"}, []string{result.Rules[0].Content, result.Rules[1].Content})
})
}
}

func TestRun_ScopedRulesPreserveNamespaceAndSelectors(t *testing.T) {
t.Parallel()
dir := t.TempDir()
createRule(t, dir, ".agents/namespaces/team/tasks/task.md", "selectors:\n language: go", "TASK_INSTRUCTIONS")
createRule(t, dir, ".agents/namespaces/team/rules/team.md", "", "TEAM_RULE")
createRule(t, dir, ".claude/rules/go.md", "language: go", "GO_RULE")
createRule(t, dir, ".claude/rules/python.md", "language: python", "PYTHON_RULE")
createRule(t, dir, ".codex/AGENTS.md", "", "CODEX_INSTRUCTIONS")

result, err := New(WithSearchPaths("file://"+dir), WithAgent(AgentCodex)).Run(context.Background(), "team/task")
require.NoError(t, err)
require.Contains(t, result.Prompt, "TASK_INSTRUCTIONS")
require.Contains(t, result.Prompt, "TEAM_RULE")
require.Contains(t, result.Prompt, "GO_RULE")
require.NotContains(t, result.Prompt, "PYTHON_RULE")
require.Contains(t, result.Prompt, "CODEX_INSTRUCTIONS")
}

func TestRun_PreservesExplicitInstructionFiles(t *testing.T) {
t.Parallel()
dir := t.TempDir()
createTask(t, dir, "task", "", "Do the task.")
paths := []string{
"CLAUDE.md", "CLAUDE.local.md", ".claude/CLAUDE.md", ".claude/CLAUDE.local.md",
"AGENTS.md", ".codex/AGENTS.md",
"GEMINI.md", ".gemini/GEMINI.md", ".gemini/styleguide.md",
}
for i, path := range paths {
createRule(t, dir, path, "", fmt.Sprintf("instruction-%d", i))
}
result, err := New(WithSearchPaths("file://"+dir)).Run(context.Background(), "task")
require.NoError(t, err)
require.Len(t, result.Rules, len(paths), "each instruction file should be included exactly once")
for i := range paths {
require.Equal(t, 1, strings.Count(result.Prompt, fmt.Sprintf("instruction-%d", i)))
}
}
Loading