Skip to content
Merged
66 changes: 66 additions & 0 deletions pkg/codingcontext/agent_paths.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package codingcontext

// agentPathsConfig describes the search paths for a specific agent.
// This is the internal configuration structure used by the agentsPaths map.
type agentPathsConfig struct {
rulesPaths []string // Paths to search for rule files
skillsPath string // Path to search for skill directories
commandsPath string // Path to search for command files
tasksPath string // Path to search for task files
}

// agentsPaths maps 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.
var agentsPaths = map[Agent]agentPathsConfig{
// Generic .agents directory structure (empty agent name)
Agent(""): {
rulesPaths: []string{".agents/rules"},
skillsPath: ".agents/skills",
commandsPath: ".agents/commands",
tasksPath: ".agents/tasks",
},
// Cursor agent paths
AgentCursor: {
rulesPaths: []string{".cursor/rules", ".cursorrules"},
skillsPath: ".cursor/skills",
commandsPath: ".cursor/commands",
// No tasks path defined for Cursor
},
// OpenCode agent paths
AgentOpenCode: {
rulesPaths: []string{".opencode/agent", ".opencode/rules"},
commandsPath: ".opencode/command",
// No skills or tasks paths defined for OpenCode
},
// Copilot agent paths
AgentCopilot: {
rulesPaths: []string{".github/copilot-instructions.md", ".github/agents"},
// No skills, commands, or tasks paths defined for Copilot
},
// Claude agent paths
AgentClaude: {
rulesPaths: []string{".claude", "CLAUDE.md", "CLAUDE.local.md"},
// No skills, commands, or tasks paths defined for Claude
},
// Gemini agent paths
AgentGemini: {
rulesPaths: []string{".gemini/styleguide.md", ".gemini", "GEMINI.md"},
// No skills, commands, or tasks paths defined for Gemini
},
// Augment agent paths
AgentAugment: {
rulesPaths: []string{".augment/rules", ".augment/guidelines.md"},
// No skills, commands, or tasks paths defined for Augment
},
// Windsurf agent paths
AgentWindsurf: {
rulesPaths: []string{".windsurf/rules", ".windsurfrules"},
// No skills, commands, or tasks paths defined for Windsurf
},
// Codex agent paths
AgentCodex: {
rulesPaths: []string{".codex", "AGENTS.md"},
// No skills, commands, or tasks paths defined for Codex
},
}
171 changes: 171 additions & 0 deletions pkg/codingcontext/agent_paths_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package codingcontext

import (
"testing"
)

func TestAgentPaths_Structure(t *testing.T) {
tests := []struct {
name string
agent Agent
}{
{
name: "empty agent (generic .agents)",
agent: Agent(""),
},
{
name: "cursor agent",
agent: AgentCursor,
},
{
name: "opencode agent",
agent: AgentOpenCode,
},
{
name: "copilot agent",
agent: AgentCopilot,
},
{
name: "claude agent",
agent: AgentClaude,
},
{
name: "gemini agent",
agent: AgentGemini,
},
{
name: "augment agent",
agent: AgentAugment,
},
{
name: "windsurf agent",
agent: AgentWindsurf,
},
{
name: "codex agent",
agent: AgentCodex,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
paths, exists := agentsPaths[tt.agent]
if !exists {
t.Errorf("Agent %q not found in agentsPaths", tt.agent)
return
}

// Check that at least one path is defined
hasAnyPath := len(paths.rulesPaths) > 0 ||
paths.skillsPath != "" ||
paths.commandsPath != "" ||
paths.tasksPath != ""

if !hasAnyPath {
t.Errorf("Agent %q has no paths defined", tt.agent)
}
})
}
}

func TestAgentPaths_EmptyAgentHasAllPaths(t *testing.T) {
paths, exists := agentsPaths[Agent("")]
if !exists {
t.Fatal("Empty agent not found in agentsPaths")
}

if len(paths.rulesPaths) == 0 {
t.Error("Empty agent should have rulesPaths defined")
}
if paths.skillsPath == "" {
t.Error("Empty agent should have skillsPath defined")
}
if paths.commandsPath == "" {
t.Error("Empty agent should have commandsPath defined")
}
if paths.tasksPath == "" {
t.Error("Empty agent should have tasksPath defined")
}
}

func TestAgentPaths_RulesPathsNotEmpty(t *testing.T) {
// Every agent should have at least one rules path
for agent, paths := range agentsPaths {
if len(paths.rulesPaths) == 0 {
t.Errorf("Agent %q should have at least one rulesPaths entry", agent)
}
}
}

func TestAgentPaths_NoAbsolutePaths(t *testing.T) {
// All paths should be relative (not absolute)
for agent, paths := range agentsPaths {
for _, rulePath := range paths.rulesPaths {
if len(rulePath) > 0 && rulePath[0] == '/' {
t.Errorf("Agent %q rulesPaths contains absolute path: %q", agent, rulePath)
}
}
if len(paths.skillsPath) > 0 && paths.skillsPath[0] == '/' {
t.Errorf("Agent %q skillsPath is absolute: %q", agent, paths.skillsPath)
}
if len(paths.commandsPath) > 0 && paths.commandsPath[0] == '/' {
t.Errorf("Agent %q commandsPath is absolute: %q", agent, paths.commandsPath)
}
if len(paths.tasksPath) > 0 && paths.tasksPath[0] == '/' {
t.Errorf("Agent %q tasksPath is absolute: %q", agent, paths.tasksPath)
}
}
}

func TestAgentPaths_Count(t *testing.T) {
// Should have 9 entries: 1 empty agent + 8 named agents
expectedCount := 9
if len(agentsPaths) != expectedCount {
t.Errorf("agentsPaths should have %d entries, got %d", expectedCount, len(agentsPaths))
}
}

func TestAgent_Paths(t *testing.T) {
tests := []struct {
name string
agent Agent
wantRulesPaths []string
wantSkillsPath string
}{
{
name: "cursor agent",
agent: AgentCursor,
wantRulesPaths: []string{".cursor/rules", ".cursorrules"},
wantSkillsPath: ".cursor/skills",
},
{
name: "empty agent",
agent: Agent(""),
wantRulesPaths: []string{".agents/rules"},
wantSkillsPath: ".agents/skills",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
paths, exists := agentsPaths[tt.agent]
if !exists {
t.Fatalf("Agent %q not found in agentsPaths", tt.agent)
}

gotRulesPaths := paths.rulesPaths
if len(gotRulesPaths) != len(tt.wantRulesPaths) {
t.Errorf("rulesPaths length = %d, want %d", len(gotRulesPaths), len(tt.wantRulesPaths))
}
for i, want := range tt.wantRulesPaths {
if i < len(gotRulesPaths) && gotRulesPaths[i] != want {
t.Errorf("rulesPaths[%d] = %q, want %q", i, gotRulesPaths[i], want)
}
}

if got := paths.skillsPath; got != tt.wantSkillsPath {
t.Errorf("skillsPath = %q, want %q", got, tt.wantSkillsPath)
}
})
}
}
2 changes: 1 addition & 1 deletion pkg/codingcontext/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ func (cc *Context) findExecuteRuleFiles(ctx context.Context, homeDir string) err
return nil
}

err := cc.visitMarkdownFiles(func(path string) []string { return rulePaths(path, path == homeDir) }, func(path string) error {
err := cc.visitMarkdownFiles(rulePaths, func(path string) error {
var frontmatter markdown.RuleFrontMatter
md, err := markdown.ParseMarkdownFile(path, &frontmatter)
if err != nil {
Expand Down
84 changes: 45 additions & 39 deletions pkg/codingcontext/paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,63 @@ package codingcontext

import "path/filepath"

// DownloadedRulePaths returns the search paths for rule files in downloaded directories
func rulePaths(dir string, home bool) []string {
if home {
return []string{
// user
filepath.Join(dir, ".agents", "rules"),
filepath.Join(dir, ".claude", "CLAUDE.md"),
filepath.Join(dir, ".codex", "AGENTS.md"),
filepath.Join(dir, ".gemini", "GEMINI.md"),
filepath.Join(dir, ".opencode", "rules"),
// rulePaths returns the search paths for rule files in a directory.
// It collects rule paths from all agents in the agentsPaths configuration.
func rulePaths(dir string) []string {
var paths []string

// Iterate through all configured agents
for _, config := range agentsPaths {
// Add each rule path for this agent
for _, rulePath := range config.rulesPaths {
paths = append(paths, filepath.Join(dir, rulePath))
}
}
return []string{
filepath.Join(dir, ".agents", "rules"),
filepath.Join(dir, ".cursor", "rules"),
filepath.Join(dir, ".augment", "rules"),
filepath.Join(dir, ".windsurf", "rules"),
filepath.Join(dir, ".opencode", "agent"),
filepath.Join(dir, ".github", "copilot-instructions.md"),
filepath.Join(dir, ".gemini", "styleguide.md"),
filepath.Join(dir, ".github", "agents"),
filepath.Join(dir, ".augment", "guidelines.md"),
filepath.Join(dir, "AGENTS.md"),
filepath.Join(dir, "CLAUDE.md"),
filepath.Join(dir, "CLAUDE.local.md"),
filepath.Join(dir, "GEMINI.md"),
filepath.Join(dir, ".cursorrules"),
filepath.Join(dir, ".windsurfrules"),
}

return paths
}

// taskSearchPaths returns the search paths for task files in a directory
// taskSearchPaths returns the search paths for task files in a directory.
// It collects task paths from all agents in the agentsPaths configuration.
func taskSearchPaths(dir string) []string {
return []string{
filepath.Join(dir, ".agents", "tasks"),
var paths []string

// Iterate through all configured agents
for _, config := range agentsPaths {
if config.tasksPath != "" {
paths = append(paths, filepath.Join(dir, config.tasksPath))
}
}

return paths
}

// commandSearchPaths returns the search paths for command files in a directory
// commandSearchPaths returns the search paths for command files in a directory.
// It collects command paths from all agents in the agentsPaths configuration.
func commandSearchPaths(dir string) []string {
return []string{
filepath.Join(dir, ".agents", "commands"),
filepath.Join(dir, ".cursor", "commands"),
filepath.Join(dir, ".opencode", "command"),
var paths []string

// Iterate through all configured agents
for _, config := range agentsPaths {
if config.commandsPath != "" {
paths = append(paths, filepath.Join(dir, config.commandsPath))
}
}

return paths
}

// skillSearchPaths returns the search paths for skill directories in a directory
// skillSearchPaths returns the search paths for skill directories in a directory.
// It collects skill paths from all agents in the agentsPaths configuration.
func skillSearchPaths(dir string) []string {
return []string{
filepath.Join(dir, ".agents", "skills"),
filepath.Join(dir, ".cursor", "skills"),
var paths []string

// Iterate through all configured agents
for _, config := range agentsPaths {
if config.skillsPath != "" {
paths = append(paths, filepath.Join(dir, config.skillsPath))
}
}

return paths
}
Loading