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

// agentPathsConfig describes the search paths for a specific agent
type agentPathsConfig struct {
RulesPaths []string // Paths to search for rule files
Copy link
Contributor

Choose a reason for hiding this comment

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

@copilot Fields can be private

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in a5943c7. All fields in agentPathsConfig are now private (lowercase): rulesPaths, skillsPath, commandsPath, tasksPath.

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 provides access to agent-specific search paths
type AgentsPaths struct {
agent Agent
}

// RulesPaths returns the rules paths for the agent
func (ap AgentsPaths) RulesPaths() []string {
if paths, exists := agentsPaths[ap.agent]; exists {
return paths.RulesPaths
}
return nil
}

// SkillsPath returns the skills path for the agent
func (ap AgentsPaths) SkillsPath() string {
if paths, exists := agentsPaths[ap.agent]; exists {
return paths.SkillsPath
}
return ""
}

// CommandsPath returns the commands path for the agent
func (ap AgentsPaths) CommandsPath() string {
if paths, exists := agentsPaths[ap.agent]; exists {
return paths.CommandsPath
}
return ""
}

// TasksPath returns the tasks path for the agent
func (ap AgentsPaths) TasksPath() string {
if paths, exists := agentsPaths[ap.agent]; exists {
return paths.TasksPath
}
return ""
}

// Paths returns an AgentsPaths instance for accessing the agent's paths
func (a Agent) Paths() AgentsPaths {
return AgentsPaths{agent: a}
}

// 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
},
}
163 changes: 163 additions & 0 deletions pkg/codingcontext/agent_paths_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
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 := tt.agent.Paths()

// 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 := Agent("").Paths()

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 := range agentsPaths {
paths := agent.Paths()
if len(paths.RulesPaths()) == 0 {
t.Errorf("Agent %q should have at least one RulesPaths entry", agent)
}
}
}
Comment on lines 91 to 98
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

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

The test directly iterates over the agentsPaths map without a deterministic order. While this test itself may be stable, it could be affected by the same map iteration non-determinism as the production code. Consider using a fixed list of agents for testing to ensure consistent test execution.

Copilot uses AI. Check for mistakes.

func TestAgentPaths_NoAbsolutePaths(t *testing.T) {
// All paths should be relative (not absolute)
for agent := range agentsPaths {
paths := agent.Paths()
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())
}
}
}
Comment on lines 100 to 118
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

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

The test directly iterates over the agentsPaths map which has non-deterministic iteration order. Consider using a fixed list of agents to ensure deterministic test execution.

Copilot uses AI. Check for mistakes.

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: "",
},
{
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 := tt.agent.Paths()

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
}
Comment on lines 7 to 19
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

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

The functions rulePaths, taskSearchPaths, commandSearchPaths, and skillSearchPaths iterate over the agentsPaths map. In Go, map iteration order is not guaranteed to be consistent across executions. This means the order of returned paths will be non-deterministic, which could lead to flaky tests or unexpected behavior in code that depends on path ordering. Consider sorting the results before returning them, or using a slice of agents in a defined order instead of iterating over a map.

Copilot uses AI. Check for mistakes.

// 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
Loading