diff --git a/pkg/codingcontext/agent_paths.go b/pkg/codingcontext/agent_paths.go new file mode 100644 index 0000000..83e1e3a --- /dev/null +++ b/pkg/codingcontext/agent_paths.go @@ -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 + }, +} diff --git a/pkg/codingcontext/agent_paths_test.go b/pkg/codingcontext/agent_paths_test.go new file mode 100644 index 0000000..40793f4 --- /dev/null +++ b/pkg/codingcontext/agent_paths_test.go @@ -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) + } + }) + } +} diff --git a/pkg/codingcontext/context.go b/pkg/codingcontext/context.go index 49d3973..7d0655c 100644 --- a/pkg/codingcontext/context.go +++ b/pkg/codingcontext/context.go @@ -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 { diff --git a/pkg/codingcontext/paths.go b/pkg/codingcontext/paths.go index c9386f2..a015f52 100644 --- a/pkg/codingcontext/paths.go +++ b/pkg/codingcontext/paths.go @@ -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 } diff --git a/pkg/codingcontext/paths_test.go b/pkg/codingcontext/paths_test.go new file mode 100644 index 0000000..e87362d --- /dev/null +++ b/pkg/codingcontext/paths_test.go @@ -0,0 +1,140 @@ +package codingcontext + +import ( + "path/filepath" + "testing" +) + +func TestRulePaths(t *testing.T) { + tests := []struct { + name string + dir string + wantContains []string + }{ + { + name: "directory includes all agent paths", + dir: "/project", + wantContains: []string{ + filepath.Join("/project", ".agents", "rules"), + filepath.Join("/project", ".cursor", "rules"), + filepath.Join("/project", ".cursorrules"), + filepath.Join("/project", ".claude"), + filepath.Join("/project", ".codex"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + paths := rulePaths(tt.dir) + + // Check that expected paths are present + for _, want := range tt.wantContains { + found := false + for _, path := range paths { + if path == want { + found = true + break + } + } + if !found { + t.Errorf("Expected path %q not found in rulePaths", want) + } + } + }) + } +} + +func TestTaskSearchPaths(t *testing.T) { + dir := "/project" + paths := taskSearchPaths(dir) + + // Should contain at least the .agents/tasks path + expectedPath := filepath.Join(dir, ".agents", "tasks") + found := false + for _, path := range paths { + if path == expectedPath { + found = true + break + } + } + if !found { + t.Errorf("Expected path %q not found in taskSearchPaths", expectedPath) + } +} + +func TestCommandSearchPaths(t *testing.T) { + dir := "/project" + paths := commandSearchPaths(dir) + + // Should contain at least the .agents/commands path + expectedPaths := []string{ + filepath.Join(dir, ".agents", "commands"), + filepath.Join(dir, ".cursor", "commands"), + filepath.Join(dir, ".opencode", "command"), + } + + for _, expected := range expectedPaths { + found := false + for _, path := range paths { + if path == expected { + found = true + break + } + } + if !found { + t.Errorf("Expected path %q not found in commandSearchPaths", expected) + } + } +} + +func TestSkillSearchPaths(t *testing.T) { + dir := "/project" + paths := skillSearchPaths(dir) + + // Should contain at least the .agents/skills path + expectedPath := filepath.Join(dir, ".agents", "skills") + found := false + for _, path := range paths { + if path == expectedPath { + found = true + break + } + } + if !found { + t.Errorf("Expected path %q not found in skillSearchPaths", expectedPath) + } +} + +func TestPathsUseAgentsPaths(t *testing.T) { + // Verify that all path functions are using the agentsPaths configuration + // by checking that they return paths for all configured agents + + dir := "/test" + + // Get paths from functions + rulePaths := rulePaths(dir) + taskPaths := taskSearchPaths(dir) + commandPaths := commandSearchPaths(dir) + skillPaths := skillSearchPaths(dir) + + // Verify rulePaths contains paths from multiple agents + if len(rulePaths) < 5 { + t.Errorf("rulePaths should contain paths from multiple agents, got %d paths", len(rulePaths)) + } + + // Verify taskPaths is not empty + if len(taskPaths) == 0 { + t.Error("taskSearchPaths should return at least one path") + } + + // Verify commandPaths is not empty + if len(commandPaths) == 0 { + t.Error("commandSearchPaths should return at least one path") + } + + // Verify skillPaths is not empty + if len(skillPaths) == 0 { + t.Error("skillSearchPaths should return at least one path") + } +}