Skip to content

Commit 2243b95

Browse files
authored
Add centralized agent path configuration and simplify path handling (#189)
1 parent e74668b commit 2243b95

File tree

5 files changed

+423
-40
lines changed

5 files changed

+423
-40
lines changed

pkg/codingcontext/agent_paths.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package codingcontext
2+
3+
// agentPathsConfig describes the search paths for a specific agent.
4+
// This is the internal configuration structure used by the agentsPaths map.
5+
type agentPathsConfig struct {
6+
rulesPaths []string // Paths to search for rule files
7+
skillsPath string // Path to search for skill directories
8+
commandsPath string // Path to search for command files
9+
tasksPath string // Path to search for task files
10+
}
11+
12+
// agentsPaths maps each agent to its specific search paths.
13+
// Empty string agent ("") represents the generic .agents directory structure.
14+
// If a path is empty, it is not defined for that agent.
15+
var agentsPaths = map[Agent]agentPathsConfig{
16+
// Generic .agents directory structure (empty agent name)
17+
Agent(""): {
18+
rulesPaths: []string{".agents/rules"},
19+
skillsPath: ".agents/skills",
20+
commandsPath: ".agents/commands",
21+
tasksPath: ".agents/tasks",
22+
},
23+
// Cursor agent paths
24+
AgentCursor: {
25+
rulesPaths: []string{".cursor/rules", ".cursorrules"},
26+
skillsPath: ".cursor/skills",
27+
commandsPath: ".cursor/commands",
28+
// No tasks path defined for Cursor
29+
},
30+
// OpenCode agent paths
31+
AgentOpenCode: {
32+
rulesPaths: []string{".opencode/agent", ".opencode/rules"},
33+
commandsPath: ".opencode/command",
34+
// No skills or tasks paths defined for OpenCode
35+
},
36+
// Copilot agent paths
37+
AgentCopilot: {
38+
rulesPaths: []string{".github/copilot-instructions.md", ".github/agents"},
39+
// No skills, commands, or tasks paths defined for Copilot
40+
},
41+
// Claude agent paths
42+
AgentClaude: {
43+
rulesPaths: []string{".claude", "CLAUDE.md", "CLAUDE.local.md"},
44+
// No skills, commands, or tasks paths defined for Claude
45+
},
46+
// Gemini agent paths
47+
AgentGemini: {
48+
rulesPaths: []string{".gemini/styleguide.md", ".gemini", "GEMINI.md"},
49+
// No skills, commands, or tasks paths defined for Gemini
50+
},
51+
// Augment agent paths
52+
AgentAugment: {
53+
rulesPaths: []string{".augment/rules", ".augment/guidelines.md"},
54+
// No skills, commands, or tasks paths defined for Augment
55+
},
56+
// Windsurf agent paths
57+
AgentWindsurf: {
58+
rulesPaths: []string{".windsurf/rules", ".windsurfrules"},
59+
// No skills, commands, or tasks paths defined for Windsurf
60+
},
61+
// Codex agent paths
62+
AgentCodex: {
63+
rulesPaths: []string{".codex", "AGENTS.md"},
64+
// No skills, commands, or tasks paths defined for Codex
65+
},
66+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package codingcontext
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestAgentPaths_Structure(t *testing.T) {
8+
tests := []struct {
9+
name string
10+
agent Agent
11+
}{
12+
{
13+
name: "empty agent (generic .agents)",
14+
agent: Agent(""),
15+
},
16+
{
17+
name: "cursor agent",
18+
agent: AgentCursor,
19+
},
20+
{
21+
name: "opencode agent",
22+
agent: AgentOpenCode,
23+
},
24+
{
25+
name: "copilot agent",
26+
agent: AgentCopilot,
27+
},
28+
{
29+
name: "claude agent",
30+
agent: AgentClaude,
31+
},
32+
{
33+
name: "gemini agent",
34+
agent: AgentGemini,
35+
},
36+
{
37+
name: "augment agent",
38+
agent: AgentAugment,
39+
},
40+
{
41+
name: "windsurf agent",
42+
agent: AgentWindsurf,
43+
},
44+
{
45+
name: "codex agent",
46+
agent: AgentCodex,
47+
},
48+
}
49+
50+
for _, tt := range tests {
51+
t.Run(tt.name, func(t *testing.T) {
52+
paths, exists := agentsPaths[tt.agent]
53+
if !exists {
54+
t.Errorf("Agent %q not found in agentsPaths", tt.agent)
55+
return
56+
}
57+
58+
// Check that at least one path is defined
59+
hasAnyPath := len(paths.rulesPaths) > 0 ||
60+
paths.skillsPath != "" ||
61+
paths.commandsPath != "" ||
62+
paths.tasksPath != ""
63+
64+
if !hasAnyPath {
65+
t.Errorf("Agent %q has no paths defined", tt.agent)
66+
}
67+
})
68+
}
69+
}
70+
71+
func TestAgentPaths_EmptyAgentHasAllPaths(t *testing.T) {
72+
paths, exists := agentsPaths[Agent("")]
73+
if !exists {
74+
t.Fatal("Empty agent not found in agentsPaths")
75+
}
76+
77+
if len(paths.rulesPaths) == 0 {
78+
t.Error("Empty agent should have rulesPaths defined")
79+
}
80+
if paths.skillsPath == "" {
81+
t.Error("Empty agent should have skillsPath defined")
82+
}
83+
if paths.commandsPath == "" {
84+
t.Error("Empty agent should have commandsPath defined")
85+
}
86+
if paths.tasksPath == "" {
87+
t.Error("Empty agent should have tasksPath defined")
88+
}
89+
}
90+
91+
func TestAgentPaths_RulesPathsNotEmpty(t *testing.T) {
92+
// Every agent should have at least one rules path
93+
for agent, paths := range agentsPaths {
94+
if len(paths.rulesPaths) == 0 {
95+
t.Errorf("Agent %q should have at least one rulesPaths entry", agent)
96+
}
97+
}
98+
}
99+
100+
func TestAgentPaths_NoAbsolutePaths(t *testing.T) {
101+
// All paths should be relative (not absolute)
102+
for agent, paths := range agentsPaths {
103+
for _, rulePath := range paths.rulesPaths {
104+
if len(rulePath) > 0 && rulePath[0] == '/' {
105+
t.Errorf("Agent %q rulesPaths contains absolute path: %q", agent, rulePath)
106+
}
107+
}
108+
if len(paths.skillsPath) > 0 && paths.skillsPath[0] == '/' {
109+
t.Errorf("Agent %q skillsPath is absolute: %q", agent, paths.skillsPath)
110+
}
111+
if len(paths.commandsPath) > 0 && paths.commandsPath[0] == '/' {
112+
t.Errorf("Agent %q commandsPath is absolute: %q", agent, paths.commandsPath)
113+
}
114+
if len(paths.tasksPath) > 0 && paths.tasksPath[0] == '/' {
115+
t.Errorf("Agent %q tasksPath is absolute: %q", agent, paths.tasksPath)
116+
}
117+
}
118+
}
119+
120+
func TestAgentPaths_Count(t *testing.T) {
121+
// Should have 9 entries: 1 empty agent + 8 named agents
122+
expectedCount := 9
123+
if len(agentsPaths) != expectedCount {
124+
t.Errorf("agentsPaths should have %d entries, got %d", expectedCount, len(agentsPaths))
125+
}
126+
}
127+
128+
func TestAgent_Paths(t *testing.T) {
129+
tests := []struct {
130+
name string
131+
agent Agent
132+
wantRulesPaths []string
133+
wantSkillsPath string
134+
}{
135+
{
136+
name: "cursor agent",
137+
agent: AgentCursor,
138+
wantRulesPaths: []string{".cursor/rules", ".cursorrules"},
139+
wantSkillsPath: ".cursor/skills",
140+
},
141+
{
142+
name: "empty agent",
143+
agent: Agent(""),
144+
wantRulesPaths: []string{".agents/rules"},
145+
wantSkillsPath: ".agents/skills",
146+
},
147+
}
148+
149+
for _, tt := range tests {
150+
t.Run(tt.name, func(t *testing.T) {
151+
paths, exists := agentsPaths[tt.agent]
152+
if !exists {
153+
t.Fatalf("Agent %q not found in agentsPaths", tt.agent)
154+
}
155+
156+
gotRulesPaths := paths.rulesPaths
157+
if len(gotRulesPaths) != len(tt.wantRulesPaths) {
158+
t.Errorf("rulesPaths length = %d, want %d", len(gotRulesPaths), len(tt.wantRulesPaths))
159+
}
160+
for i, want := range tt.wantRulesPaths {
161+
if i < len(gotRulesPaths) && gotRulesPaths[i] != want {
162+
t.Errorf("rulesPaths[%d] = %q, want %q", i, gotRulesPaths[i], want)
163+
}
164+
}
165+
166+
if got := paths.skillsPath; got != tt.wantSkillsPath {
167+
t.Errorf("skillsPath = %q, want %q", got, tt.wantSkillsPath)
168+
}
169+
})
170+
}
171+
}

pkg/codingcontext/context.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ func (cc *Context) findExecuteRuleFiles(ctx context.Context, homeDir string) err
502502
return nil
503503
}
504504

505-
err := cc.visitMarkdownFiles(func(path string) []string { return rulePaths(path, path == homeDir) }, func(path string) error {
505+
err := cc.visitMarkdownFiles(rulePaths, func(path string) error {
506506
var frontmatter markdown.RuleFrontMatter
507507
md, err := markdown.ParseMarkdownFile(path, &frontmatter)
508508
if err != nil {

pkg/codingcontext/paths.go

Lines changed: 45 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,57 +2,63 @@ package codingcontext
22

33
import "path/filepath"
44

5-
// DownloadedRulePaths returns the search paths for rule files in downloaded directories
6-
func rulePaths(dir string, home bool) []string {
7-
if home {
8-
return []string{
9-
// user
10-
filepath.Join(dir, ".agents", "rules"),
11-
filepath.Join(dir, ".claude", "CLAUDE.md"),
12-
filepath.Join(dir, ".codex", "AGENTS.md"),
13-
filepath.Join(dir, ".gemini", "GEMINI.md"),
14-
filepath.Join(dir, ".opencode", "rules"),
5+
// rulePaths returns the search paths for rule files in a directory.
6+
// It collects rule paths from all agents in the agentsPaths configuration.
7+
func rulePaths(dir string) []string {
8+
var paths []string
9+
10+
// Iterate through all configured agents
11+
for _, config := range agentsPaths {
12+
// Add each rule path for this agent
13+
for _, rulePath := range config.rulesPaths {
14+
paths = append(paths, filepath.Join(dir, rulePath))
1515
}
1616
}
17-
return []string{
18-
filepath.Join(dir, ".agents", "rules"),
19-
filepath.Join(dir, ".cursor", "rules"),
20-
filepath.Join(dir, ".augment", "rules"),
21-
filepath.Join(dir, ".windsurf", "rules"),
22-
filepath.Join(dir, ".opencode", "agent"),
23-
filepath.Join(dir, ".github", "copilot-instructions.md"),
24-
filepath.Join(dir, ".gemini", "styleguide.md"),
25-
filepath.Join(dir, ".github", "agents"),
26-
filepath.Join(dir, ".augment", "guidelines.md"),
27-
filepath.Join(dir, "AGENTS.md"),
28-
filepath.Join(dir, "CLAUDE.md"),
29-
filepath.Join(dir, "CLAUDE.local.md"),
30-
filepath.Join(dir, "GEMINI.md"),
31-
filepath.Join(dir, ".cursorrules"),
32-
filepath.Join(dir, ".windsurfrules"),
33-
}
17+
18+
return paths
3419
}
3520

36-
// taskSearchPaths returns the search paths for task files in a directory
21+
// taskSearchPaths returns the search paths for task files in a directory.
22+
// It collects task paths from all agents in the agentsPaths configuration.
3723
func taskSearchPaths(dir string) []string {
38-
return []string{
39-
filepath.Join(dir, ".agents", "tasks"),
24+
var paths []string
25+
26+
// Iterate through all configured agents
27+
for _, config := range agentsPaths {
28+
if config.tasksPath != "" {
29+
paths = append(paths, filepath.Join(dir, config.tasksPath))
30+
}
4031
}
32+
33+
return paths
4134
}
4235

43-
// commandSearchPaths returns the search paths for command files in a directory
36+
// commandSearchPaths returns the search paths for command files in a directory.
37+
// It collects command paths from all agents in the agentsPaths configuration.
4438
func commandSearchPaths(dir string) []string {
45-
return []string{
46-
filepath.Join(dir, ".agents", "commands"),
47-
filepath.Join(dir, ".cursor", "commands"),
48-
filepath.Join(dir, ".opencode", "command"),
39+
var paths []string
40+
41+
// Iterate through all configured agents
42+
for _, config := range agentsPaths {
43+
if config.commandsPath != "" {
44+
paths = append(paths, filepath.Join(dir, config.commandsPath))
45+
}
4946
}
47+
48+
return paths
5049
}
5150

52-
// skillSearchPaths returns the search paths for skill directories in a directory
51+
// skillSearchPaths returns the search paths for skill directories in a directory.
52+
// It collects skill paths from all agents in the agentsPaths configuration.
5353
func skillSearchPaths(dir string) []string {
54-
return []string{
55-
filepath.Join(dir, ".agents", "skills"),
56-
filepath.Join(dir, ".cursor", "skills"),
54+
var paths []string
55+
56+
// Iterate through all configured agents
57+
for _, config := range agentsPaths {
58+
if config.skillsPath != "" {
59+
paths = append(paths, filepath.Join(dir, config.skillsPath))
60+
}
5761
}
62+
63+
return paths
5864
}

0 commit comments

Comments
 (0)