From 7288f141c84fdf6f3dc5b2f115179fb12e2031b0 Mon Sep 17 00:00:00 2001 From: Amolith Date: Mon, 15 Sep 2025 18:00:18 -0600 Subject: [PATCH] feat: load user CRUSH.md and AGENTS.md from config Issue: charmbracelet/crush#1050 --- README.md | 25 +++++++++++++++++++++++++ internal/agent/prompt/prompt.go | 25 ++++++++++++++++++++----- internal/agent/templates/coder.md.tpl | 18 ++++++++++++++++-- internal/config/config.go | 1 + internal/config/load.go | 10 ++++++++++ schema.json | 11 +++++++++++ 6 files changed, 83 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e25c99a5cb84372414d68a63a511eba824ac9b76..8afa0af2b517505a662562b4db4e801db0b54d2f 100644 --- a/README.md +++ b/README.md @@ -303,6 +303,31 @@ using `$(echo $VAR)` syntax. } ``` +### Memory + +Crush automatically includes two memory 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 tools or workflows here. + +You can customize these paths using the `memory_paths` option in your +configuration: + +```json +{ + "$schema": "https://charm.land/crush.json", + "options": { + "memory_paths": [ + "/path/to/custom/memory/file.md", + "/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/prompt/prompt.go b/internal/agent/prompt/prompt.go index d10fbcae3c3a37f295ec9f9de637cb130d9b6abc..216e7d32c0aebd4415b75885bb5bc523dae499db 100644 --- a/internal/agent/prompt/prompt.go +++ b/internal/agent/prompt/prompt.go @@ -35,6 +35,7 @@ type PromptDat struct { Date string GitStatus string ContextFiles []ContextFile + MemoryFiles []ContextFile } type ContextFile struct { @@ -150,16 +151,27 @@ func (p *Prompt) promptData(ctx context.Context, provider, model string, cfg con workingDir := cmp.Or(p.workingDir, cfg.WorkingDir()) platform := cmp.Or(p.platform, runtime.GOOS) - files := map[string][]ContextFile{} + contextFiles := map[string][]ContextFile{} + memoryFiles := map[string][]ContextFile{} for _, pth := range cfg.Options.ContextPaths { expanded := expandPath(pth, cfg) pathKey := strings.ToLower(expanded) - if _, ok := files[pathKey]; ok { + if _, ok := contextFiles[pathKey]; ok { continue } content := processContextPath(expanded, cfg) - files[pathKey] = content + contextFiles[pathKey] = content + } + + for _, pth := range cfg.Options.MemoryPaths { + expanded := expandPath(pth, cfg) + pathKey := strings.ToLower(expanded) + if _, ok := memoryFiles[pathKey]; ok { + continue + } + content := processContextPath(expanded, cfg) + memoryFiles[pathKey] = content } isGit := isGitRepo(cfg.WorkingDir()) @@ -180,8 +192,11 @@ func (p *Prompt) promptData(ctx context.Context, provider, model string, cfg con } } - for _, contextFiles := range files { - data.ContextFiles = append(data.ContextFiles, contextFiles...) + for _, files := range contextFiles { + data.ContextFiles = append(data.ContextFiles, files...) + } + for _, files := range memoryFiles { + data.MemoryFiles = append(data.MemoryFiles, files...) } return data, nil } diff --git a/internal/agent/templates/coder.md.tpl b/internal/agent/templates/coder.md.tpl index 225e021efe5f08f2cfd68184d684af2cbd57684e..385e146f0942260ae25a35ebd20642e178bd600f 100644 --- a/internal/agent/templates/coder.md.tpl +++ b/internal/agent/templates/coder.md.tpl @@ -362,11 +362,25 @@ Diagnostics (lint/typecheck) included in tool output. {{end}} {{if .ContextFiles}} - +# Project-Specific Context +Make sure to follow the instructions in the context below. + {{range .ContextFiles}} {{.Content}} {{end}} - + +{{end}} + +{{if .MemoryFiles}} +# 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 .MemoryFiles}} + +{{.Content}} + +{{end}} + {{end}} diff --git a/internal/config/config.go b/internal/config/config.go index b8f1fcd0dbbef7e5d5d70e2c99515db6d1f6d7b5..f7bcb30c9fd148917cb118733ab3803e61552dd1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -217,6 +217,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"` + MemoryPaths []string `json:"memory_paths,omitempty" jsonschema:"description=Paths to files containing memory information for the AI,default=~/.config/crush/CRUSH.md,default=~/.config/AGENTS.md"` TUI *TUIOptions `json:"tui,omitempty" jsonschema:"description=Terminal user interface options"` Debug bool `json:"debug,omitempty" jsonschema:"description=Enable debug logging,default=false"` DebugLSP bool `json:"debug_lsp,omitempty" jsonschema:"description=Enable debug logging for LSP servers,default=false"` diff --git a/internal/config/load.go b/internal/config/load.go index 14dd0f8792bcbabaa865efe47e0db5e721cf3827..ca2775d9b040b61fc0cede6126b9ca9843de35a0 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -322,6 +322,16 @@ func (c *Config) setDefaults(workingDir, dataDir string) { if c.Options.ContextPaths == nil { c.Options.ContextPaths = []string{} } + if c.Options.MemoryPaths == nil { + crushConfigDir := filepath.Dir(GlobalConfig()) + c.Options.MemoryPaths = []string{ + filepath.Join(crushConfigDir, "CRUSH.md"), + filepath.Join(filepath.Dir(crushConfigDir), "AGENTS.md"), + } + } + slices.Sort(c.Options.MemoryPaths) + c.Options.MemoryPaths = slices.Compact(c.Options.MemoryPaths) + if dataDir != "" { c.Options.DataDirectory = dataDir } else if c.Options.DataDirectory == "" { diff --git a/schema.json b/schema.json index 974200854d28bb300c94613328485ea5a3e2165d..22660da0e4d52849bde30273c3346ca63f577975 100644 --- a/schema.json +++ b/schema.json @@ -367,6 +367,17 @@ "type": "array", "description": "Paths to files containing context information for the AI" }, + "memory_paths": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Paths to files containing memory information for the AI", + "default": [ + "~/.config/crush/CRUSH.md", + "~/.config/AGENTS.md" + ] + }, "tui": { "$ref": "#/$defs/TUIOptions", "description": "Terminal user interface options"