diff --git a/README.md b/README.md index f3ddaf7afa23014f3b0bb75297e0cac3da81318b..2c2559282624fbc7732b3ca7687f624e434c0065 100644 --- a/README.md +++ b/README.md @@ -402,6 +402,33 @@ down. There is a short grace window right after `POST /v1/workspaces` so a client that has created the workspace but not yet opened its event stream does not get reaped before it can attach. +### Global context files + +Crush automatically includes two files for cross-project instructions. + +- `~/.config/crush/CRUSH.md`: Crush-specific rules that would confuse other + agentic coding tools. If you only use Crush, this is the only one you need to + edit. +- `~/.config/AGENTS.md`: generic instructions that other coding tools might + read. Avoid referring to Crush-specific features or workflows here. You + probably only care about this if you use multiple agentic coding tools and + want to share instructions between them. + +You can customize these paths using the `global_context_paths` option in your +configuration: + +```jsonc +{ + "$schema": "https://charm.land/crush.json", + "options": { + "global_context_paths": [ + "~/path/to/custom/context/file.md", + "/full/path/to/folder/of/files/" // recursively load all .md files in folder + ] + } +} +``` + ### Ignoring Files Crush respects `.gitignore` files by default, but you can also create a diff --git a/internal/agent/common_test.go b/internal/agent/common_test.go index 8eb0f10c4ac35eee9b6dd76835d11d12cb233f74..bb1d4397827eff387f87b93c3e5d4b69a95c794e 100644 --- a/internal/agent/common_test.go +++ b/internal/agent/common_test.go @@ -152,6 +152,7 @@ func coderAgent(r *vcr.Recorder, env fakeEnv, large, small fantasy.LanguageModel cfg.Config().Options.SkillsPaths = nil cfg.Config().Options.DisabledSkills = []string{"crush-config"} cfg.Config().Options.ContextPaths = nil + cfg.Config().Options.GlobalContextPaths = nil cfg.Config().LSP = nil systemPrompt, err := prompt.Build(context.TODO(), large.Provider(), large.Model(), cfg) diff --git a/internal/agent/prompt/prompt.go b/internal/agent/prompt/prompt.go index 7609661f31940ac9bccde40334034dc1e16191f1..4917b25b2280700d068eaa6f2f53d4507ecb90ff 100644 --- a/internal/agent/prompt/prompt.go +++ b/internal/agent/prompt/prompt.go @@ -29,16 +29,17 @@ type Prompt struct { } type PromptDat struct { - Provider string - Model string - Config config.Config - WorkingDir string - IsGitRepo bool - Platform string - Date string - GitStatus string - ContextFiles []ContextFile - AvailSkillXML string + Provider string + Model string + Config config.Config + WorkingDir string + IsGitRepo bool + Platform string + Date string + GitStatus string + ContextFiles []ContextFile + GlobalContextFiles []ContextFile + AvailSkillXML string } type ContextFile struct { @@ -147,22 +148,27 @@ func expandPath(path string, store *config.ConfigStore) string { return path } -func (p *Prompt) promptData(ctx context.Context, provider, model string, store *config.ConfigStore) (PromptDat, error) { - workingDir := cmp.Or(p.workingDir, store.WorkingDir()) - platform := cmp.Or(p.platform, runtime.GOOS) - +// loadContextFiles loads and deduplicates context files from a list of paths. +func loadContextFiles(paths []string, store *config.ConfigStore) map[string][]ContextFile { files := map[string][]ContextFile{} - - cfg := store.Config() - for _, pth := range cfg.Options.ContextPaths { + for _, pth := range paths { expanded := expandPath(pth, store) pathKey := strings.ToLower(expanded) if _, ok := files[pathKey]; ok { continue } - content := processContextPath(expanded, store) - files[pathKey] = content + files[pathKey] = processContextPath(expanded, store) } + return files +} + +func (p *Prompt) promptData(ctx context.Context, provider, model string, store *config.ConfigStore) (PromptDat, error) { + workingDir := cmp.Or(p.workingDir, store.WorkingDir()) + platform := cmp.Or(p.platform, runtime.GOOS) + + cfg := store.Config() + contextFiles := loadContextFiles(cfg.Options.ContextPaths, store) + globalContextFiles := loadContextFiles(cfg.Options.GlobalContextPaths, store) // Discover and load skills metadata. var availSkillXML string @@ -217,8 +223,11 @@ func (p *Prompt) promptData(ctx context.Context, provider, model string, store * } } - for _, contextFiles := range files { - data.ContextFiles = append(data.ContextFiles, contextFiles...) + for _, files := range contextFiles { + data.ContextFiles = append(data.ContextFiles, files...) + } + for _, files := range globalContextFiles { + data.GlobalContextFiles = append(data.GlobalContextFiles, files...) } return data, nil } diff --git a/internal/agent/templates/coder.md.tpl b/internal/agent/templates/coder.md.tpl index 79b9e1af3ba56ef9be81352b9e04bb9374c69ee8..bfa06d6c23f019b492b2a3c86c4c4986c4ef99f3 100644 --- a/internal/agent/templates/coder.md.tpl +++ b/internal/agent/templates/coder.md.tpl @@ -396,11 +396,25 @@ If a skill mentions scripts, references, or assets, they live in the same folder {{end}} {{if .ContextFiles}} - +# Project-Specific Context +Make sure to follow the instructions in the context below. + {{range .ContextFiles}} {{.Content}} {{end}} - + +{{end}} +{{if .GlobalContextFiles}} + +# User context +The following is personal content added by the user that they'd like you to follow no matter what project you're working in. + +{{range .GlobalContextFiles}} + +{{.Content}} + +{{end}} + {{end}} diff --git a/internal/config/config.go b/internal/config/config.go index 38c8159109dbcaf945ceb3a00626fa1f0c34048a..abd36f3e71023393d6de7bfcb61f1d4d3c40bdc9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -260,6 +260,7 @@ func (Attribution) JSONSchemaExtend(schema *jsonschema.Schema) { type Options struct { ContextPaths []string `json:"context_paths,omitempty" jsonschema:"description=Paths to files containing context information for the AI,example=.cursorrules,example=CRUSH.md"` + GlobalContextPaths []string `json:"global_context_paths,omitempty" jsonschema:"description=Paths to files containing global context information for the AI,default=~/.config/crush/CRUSH.md,default=~/.config/AGENTS.md"` SkillsPaths []string `json:"skills_paths,omitempty" jsonschema:"description=Paths to directories containing Agent Skills (folders with SKILL.md files),example=~/.config/crush/skills,example=./skills"` TUI *TUIOptions `json:"tui,omitempty" jsonschema:"description=Terminal user interface options"` Debug bool `json:"debug,omitempty" jsonschema:"description=Enable debug logging,default=false"` diff --git a/internal/config/load.go b/internal/config/load.go index 2f0946e7bc45ca2e3dcc17c79cba1dc30106d975..8551070e17cb38fea1f59058c3322a859a8df59e 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -417,6 +417,16 @@ func (c *Config) setDefaults(workingDir, dataDir string) { if c.Options.TUI == nil { c.Options.TUI = &TUIOptions{} } + if len(c.Options.GlobalContextPaths) == 0 { + crushConfigDir := filepath.Dir(GlobalConfig()) + c.Options.GlobalContextPaths = []string{ + filepath.Join(crushConfigDir, "CRUSH.md"), + filepath.Join(filepath.Dir(crushConfigDir), "AGENTS.md"), + } + } + slices.Sort(c.Options.GlobalContextPaths) + c.Options.GlobalContextPaths = slices.Compact(c.Options.GlobalContextPaths) + if dataDir != "" { c.Options.DataDirectory = dataDir } else if c.Options.DataDirectory == "" { @@ -448,6 +458,7 @@ func (c *Config) setDefaults(workingDir, dataDir string) { // Add the default context paths if they are not already present c.Options.ContextPaths = append(slices.Clone(defaultContextPaths), c.Options.ContextPaths...) + slices.Sort(c.Options.ContextPaths) c.Options.ContextPaths = slices.Compact(c.Options.ContextPaths) diff --git a/schema.json b/schema.json index 9580a7be217151b8d6f2543071fb635f10763016..5fc0e0ac498bbf9a96ac287c1c75840f6d1886c6 100644 --- a/schema.json +++ b/schema.json @@ -405,6 +405,17 @@ "type": "array", "description": "Paths to files containing context information for the AI" }, + "global_context_paths": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Paths to files containing global context information for the AI", + "default": [ + "~/.config/crush/CRUSH.md", + "~/.config/AGENTS.md" + ] + }, "skills_paths": { "items": { "type": "string",