From c6693712ca97db725622d120efe23aeca61102d8 Mon Sep 17 00:00:00 2001 From: t-gok <19565085+t-gok@users.noreply.github.com> Date: Fri, 2 Oct 2026 17:22:29 +0000 Subject: [PATCH] fix: scope agent rule discovery to instruction paths --- README.md | 8 +- SPECIFICATION.md | 6 +- docs/reference/search-paths.md | 22 +++-- pkg/codingcontext/agent_paths.go | 10 ++- pkg/codingcontext/paths_test.go | 4 +- pkg/codingcontext/rule_scope_test.go | 130 +++++++++++++++++++++++++++ 6 files changed, 162 insertions(+), 18 deletions(-) create mode 100644 pkg/codingcontext/rule_scope_test.go diff --git a/README.md b/README.md index c5dce710..5158f44e 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. diff --git a/SPECIFICATION.md b/SPECIFICATION.md index a98d4072..3d645cf5 100644 --- a/SPECIFICATION.md +++ b/SPECIFICATION.md @@ -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 diff --git a/docs/reference/search-paths.md b/docs/reference/search-paths.md index 2c6d5295..26e154e9 100644 --- a/docs/reference/search-paths.md +++ b/docs/reference/search-paths.md @@ -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:** @@ -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 ``` @@ -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 diff --git a/pkg/codingcontext/agent_paths.go b/pkg/codingcontext/agent_paths.go index f662a8e2..af2820d8 100644 --- a/pkg/codingcontext/agent_paths.go +++ b/pkg/codingcontext/agent_paths.go @@ -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) @@ -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 }, @@ -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 }, diff --git a/pkg/codingcontext/paths_test.go b/pkg/codingcontext/paths_test.go index b495ed6d..148114bc 100644 --- a/pkg/codingcontext/paths_test.go +++ b/pkg/codingcontext/paths_test.go @@ -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"), }, }, } diff --git a/pkg/codingcontext/rule_scope_test.go b/pkg/codingcontext/rule_scope_test.go new file mode 100644 index 00000000..ff29266b --- /dev/null +++ b/pkg/codingcontext/rule_scope_test.go @@ -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))) + } +}