Skip to content

Commit 238a071

Browse files
alexecCopilotCopilot
authored
Add memories and tasks flags and handling (#27)
* Add memories and tasks flags and handling Introduce -m and -t flags and set default memory to AGENTS.md. Directories passed with -d are prepended into the memories and tasks lists. The runner now walks the provided memory paths and searches for the prompt file from the tasks list, preserving existing bootstrap execution behavior. * Fix duplicate flag definition and task resolution logic (#28) * Initial plan * Fix duplicate flag and task file finding logic Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> * Update README with new flags and memory file formats Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> * Remove -d flag and set comprehensive defaults for -m and -t (#29) * Initial plan * Remove dirs flag and set suitable defaults for memories and tasks Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 056510b commit 238a071

File tree

3 files changed

+143
-67
lines changed

3 files changed

+143
-67
lines changed

README.md

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,19 +82,34 @@ coding-context [options] <task-name>
8282
8383
Options:
8484
-b Automatically run the bootstrap script after generating it
85-
-d <directory> Add a directory to include in the context (can be used multiple times)
86-
Default: .prompts, ~/.config/prompts, /var/local/prompts
85+
-m <path> Directory containing memories, or a single memory file (can be used multiple times)
86+
Defaults: AGENTS.md, .github/copilot-instructions.md, CLAUDE.md, .cursorrules,
87+
.cursor/rules/, .instructions.md, .continuerules, .prompts/memories,
88+
~/.config/prompts/memories, /var/local/prompts/memories
89+
-t <path> Directory containing tasks, or a single task file (can be used multiple times)
90+
Defaults: .prompts/tasks, ~/.config/prompts/tasks, /var/local/prompts/tasks
8791
-o <directory> Output directory for generated files (default: .)
8892
-p <key=value> Template parameter for prompt substitution (can be used multiple times)
8993
-s <key=value> Include memories with matching frontmatter (can be used multiple times)
9094
-S <key=value> Exclude memories with matching frontmatter (can be used multiple times)
9195
```
9296

97+
**Important:** The task file name **MUST** match the task name you provide on the command line. For example, if you run `coding-context my-task`, the tool will look for `my-task.md` in the task directories.
98+
9399
**Example:**
94100
```bash
95101
coding-context -p feature="Authentication" -p language=Go add-feature
96102
```
97103

104+
**Example with custom memory and task paths:**
105+
```bash
106+
# Specify explicit memory files or directories
107+
coding-context -m .github/copilot-instructions.md -m CLAUDE.md my-task
108+
109+
# Specify custom task directory
110+
coding-context -t ./custom-tasks my-task
111+
```
112+
98113
**Example with selectors:**
99114
```bash
100115
# Include only production memories
@@ -131,6 +146,8 @@ Memory files are included in every generated context. They contain reusable info
131146

132147
Prompt files define specific tasks. They can use template variables (like `${taskName}` or `$taskName`) that you provide via command-line parameters.
133148

149+
**IMPORTANT:** The file name **MUST** match the task name you'll use on the command line. For example, a file named `my-task.md` is invoked with `coding-context my-task`.
150+
134151
```markdown
135152
# Task: ${taskName}
136153

@@ -183,6 +200,10 @@ Each directory should contain:
183200

184201
Markdown files with YAML frontmatter and Go template support.
185202

203+
**CRITICAL:** The prompt file name (without the `.md` extension) **MUST** exactly match the task name you provide on the command line. For example:
204+
- To run `coding-context add-feature`, you need a file named `add-feature.md`
205+
- To run `coding-context my-custom-task`, you need a file named `my-custom-task.md`
206+
186207
**Example** (`.prompts/tasks/add-feature.md`):
187208
```markdown
188209
# Task: ${feature}
@@ -195,6 +216,8 @@ Run with:
195216
coding-context -p feature="User Login" -p language=Go add-feature
196217
```
197218

219+
This will look for `add-feature.md` in the task directories.
220+
198221
### Memory Files
199222

200223
Markdown files included in every generated context. Bootstrap scripts can be provided in separate files.
@@ -218,6 +241,36 @@ npm install
218241

219242
For each memory file `<name>.md`, you can optionally create a corresponding `<name>-bootstrap` file that will be executed during setup.
220243

244+
### Supported Memory File Formats
245+
246+
This tool can work with various memory file formats used by popular AI coding assistants. By default, it looks for `AGENTS.md` in the current directory. You can also specify additional memory files or directories using the `-m` flag.
247+
248+
#### Common Memory File Names
249+
250+
The following memory file formats are commonly used by AI coding assistants and can be used with this tool:
251+
252+
- **`AGENTS.md`** - Default memory file (automatically included)
253+
- **`.github/copilot-instructions.md`** - GitHub Copilot instructions file
254+
- **`CLAUDE.md`** - Claude-specific instructions
255+
- **`.cursorrules`** - Cursor editor rules (if in Markdown format)
256+
- **`.cursor/rules/`** - Directory containing Cursor-specific rule files
257+
- **`.instructions.md`** - General instructions file
258+
- **`.continuerules`** - Continue.dev rules (if in Markdown format)
259+
260+
**Example:** Using multiple memory sources
261+
```bash
262+
# Include GitHub Copilot instructions and CLAUDE.md
263+
coding-context -m .github/copilot-instructions.md -m CLAUDE.md my-task
264+
265+
# Include all rules from Cursor directory
266+
coding-context -m .cursor/rules/ my-task
267+
268+
# Combine default AGENTS.md with additional memories
269+
coding-context -m .instructions.md my-task
270+
```
271+
272+
**Note:** All memory files should be in Markdown format (`.md` extension) or contain Markdown-compatible content. The tool will automatically process frontmatter in YAML format if present.
273+
221274

222275
## Filtering Memories with Selectors
223276

integration_test.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ Please help with this task.
6666
}
6767

6868
// Run the binary
69-
cmd = exec.Command(binaryPath, "-d", contextDir, "-o", outputDir, "test-task")
69+
cmd = exec.Command(binaryPath, "-m", memoriesDir, "-t", tasksDir, "-o", outputDir, "test-task")
7070
cmd.Dir = tmpDir
7171
if output, err := cmd.CombinedOutput(); err != nil {
7272
t.Fatalf("failed to run binary: %v\n%s", err, output)
@@ -158,7 +158,7 @@ echo "Setting up Jira"
158158
}
159159

160160
// Run the binary
161-
cmd = exec.Command(binaryPath, "-d", contextDir, "-o", outputDir, "test-task")
161+
cmd = exec.Command(binaryPath, "-m", memoriesDir, "-t", tasksDir, "-o", outputDir, "test-task")
162162
cmd.Dir = tmpDir
163163
if output, err := cmd.CombinedOutput(); err != nil {
164164
t.Fatalf("failed to run binary: %v\n%s", err, output)
@@ -243,7 +243,7 @@ Please help with this task.
243243
}
244244

245245
// Run the binary
246-
cmd = exec.Command(binaryPath, "-d", contextDir, "-o", outputDir, "test-task")
246+
cmd = exec.Command(binaryPath, "-m", memoriesDir, "-t", tasksDir, "-o", outputDir, "test-task")
247247
cmd.Dir = tmpDir
248248
if output, err := cmd.CombinedOutput(); err != nil {
249249
t.Fatalf("failed to run binary: %v\n%s", err, output)
@@ -310,7 +310,7 @@ func TestMultipleBootstrapFiles(t *testing.T) {
310310
}
311311

312312
// Run the binary
313-
cmd = exec.Command(binaryPath, "-d", contextDir, "-o", outputDir, "test-task")
313+
cmd = exec.Command(binaryPath, "-m", memoriesDir, "-t", tasksDir, "-o", outputDir, "test-task")
314314
cmd.Dir = tmpDir
315315
if output, err := cmd.CombinedOutput(); err != nil {
316316
t.Fatalf("failed to run binary: %v\n%s", err, output)
@@ -370,7 +370,7 @@ func TestSelectorFiltering(t *testing.T) {
370370
}
371371

372372
// Test 1: Include by env=production
373-
cmd = exec.Command(binaryPath, "-d", contextDir, "-o", outputDir, "-s", "env=production", "test-task")
373+
cmd = exec.Command(binaryPath, "-m", memoriesDir, "-t", tasksDir, "-o", outputDir, "-s", "env=production", "test-task")
374374
cmd.Dir = tmpDir
375375
if output, err := cmd.CombinedOutput(); err != nil {
376376
t.Fatalf("failed to run binary: %v\n%s", err, output)
@@ -400,7 +400,7 @@ func TestSelectorFiltering(t *testing.T) {
400400
os.RemoveAll(outputDir)
401401

402402
// Test 2: Include by language=go (should include prod and test, and nofm)
403-
cmd = exec.Command(binaryPath, "-d", contextDir, "-o", outputDir, "-s", "language=go", "test-task")
403+
cmd = exec.Command(binaryPath, "-m", memoriesDir, "-t", tasksDir, "-o", outputDir, "-s", "language=go", "test-task")
404404
cmd.Dir = tmpDir
405405
if output, err := cmd.CombinedOutput(); err != nil {
406406
t.Fatalf("failed to run binary: %v\n%s", err, output)
@@ -428,7 +428,7 @@ func TestSelectorFiltering(t *testing.T) {
428428
os.RemoveAll(outputDir)
429429

430430
// Test 3: Exclude by env=production (should include dev and test, and nofm)
431-
cmd = exec.Command(binaryPath, "-d", contextDir, "-o", outputDir, "-S", "env=production", "test-task")
431+
cmd = exec.Command(binaryPath, "-m", memoriesDir, "-t", tasksDir, "-o", outputDir, "-S", "env=production", "test-task")
432432
cmd.Dir = tmpDir
433433
if output, err := cmd.CombinedOutput(); err != nil {
434434
t.Fatalf("failed to run binary: %v\n%s", err, output)
@@ -456,7 +456,7 @@ func TestSelectorFiltering(t *testing.T) {
456456
os.RemoveAll(outputDir)
457457

458458
// Test 4: Multiple includes env=production language=go (should include only prod and nofm)
459-
cmd = exec.Command(binaryPath, "-d", contextDir, "-o", outputDir, "-s", "env=production", "-s", "language=go", "test-task")
459+
cmd = exec.Command(binaryPath, "-m", memoriesDir, "-t", tasksDir, "-o", outputDir, "-s", "env=production", "-s", "language=go", "test-task")
460460
cmd.Dir = tmpDir
461461
if output, err := cmd.CombinedOutput(); err != nil {
462462
t.Fatalf("failed to run binary: %v\n%s", err, output)
@@ -484,7 +484,7 @@ func TestSelectorFiltering(t *testing.T) {
484484
os.RemoveAll(outputDir)
485485

486486
// Test 5: Mix of include and exclude -s env=production -S language=python (should include only prod with go)
487-
cmd = exec.Command(binaryPath, "-d", contextDir, "-o", outputDir, "-s", "env=production", "-S", "language=python", "test-task")
487+
cmd = exec.Command(binaryPath, "-m", memoriesDir, "-t", tasksDir, "-o", outputDir, "-s", "env=production", "-S", "language=python", "test-task")
488488
cmd.Dir = tmpDir
489489
if output, err := cmd.CombinedOutput(); err != nil {
490490
t.Fatalf("failed to run binary: %v\n%s", err, output)
@@ -543,7 +543,7 @@ The project is for $company.
543543

544544
// Run the binary with parameters
545545
cmd = exec.Command(binaryPath,
546-
"-d", contextDir,
546+
"-t", tasksDir,
547547
"-o", outputDir,
548548
"-p", "taskName=AddAuth",
549549
"-p", "feature=Authentication",
@@ -608,7 +608,7 @@ Missing var: ${missingVar}
608608

609609
// Run the binary with only one parameter
610610
cmd = exec.Command(binaryPath,
611-
"-d", contextDir,
611+
"-t", tasksDir,
612612
"-o", outputDir,
613613
"-p", "providedVar=ProvidedValue",
614614
"test-missing")
@@ -694,7 +694,7 @@ t.Fatalf("failed to write prompt file: %v", err)
694694
}
695695

696696
// Run the binary WITH the -b flag
697-
cmd = exec.Command(binaryPath, "-d", contextDir, "-o", outputDir, "-b", "test-task")
697+
cmd = exec.Command(binaryPath, "-m", memoriesDir, "-t", tasksDir, "-o", outputDir, "-b", "test-task")
698698
cmd.Dir = tmpDir
699699
if output, err := cmd.CombinedOutput(); err != nil {
700700
t.Fatalf("failed to run binary: %v\n%s", err, output)
@@ -773,7 +773,7 @@ t.Fatalf("failed to write prompt file: %v", err)
773773
}
774774

775775
// Run the binary WITHOUT the -b flag
776-
cmd = exec.Command(binaryPath, "-d", contextDir, "-o", outputDir, "test-task")
776+
cmd = exec.Command(binaryPath, "-m", memoriesDir, "-t", tasksDir, "-o", outputDir, "test-task")
777777
cmd.Dir = tmpDir
778778
if output, err := cmd.CombinedOutput(); err != nil {
779779
t.Fatalf("failed to run binary: %v\n%s", err, output)
@@ -842,7 +842,7 @@ t.Fatalf("failed to write prompt file: %v", err)
842842
}
843843

844844
// Run the binary WITH the -b flag and send interrupt signal
845-
cmd = exec.Command(binaryPath, "-d", contextDir, "-o", outputDir, "-b", "test-task")
845+
cmd = exec.Command(binaryPath, "-m", memoriesDir, "-t", tasksDir, "-o", outputDir, "-b", "test-task")
846846
cmd.Dir = tmpDir
847847

848848
// Start the command

main.go

Lines changed: 74 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@ import (
1818
var bootstrap string
1919

2020
var (
21-
dirs stringSlice
22-
outputDir = "."
23-
params = make(paramMap)
24-
includes = make(selectorMap)
25-
excludes = make(selectorMap)
26-
runBootstrap bool
21+
memories stringSlice
22+
tasks stringSlice
23+
outputDir = "."
24+
params = make(paramMap)
25+
includes = make(selectorMap)
26+
excludes = make(selectorMap)
27+
runBootstrap bool
2728
)
2829

2930
func main() {
@@ -36,13 +37,27 @@ func main() {
3637
os.Exit(1)
3738
}
3839

39-
dirs = []string{
40-
".prompts",
41-
filepath.Join(userConfigDir, "prompts"),
42-
"/var/local/prompts",
40+
memories = []string{
41+
"AGENTS.md",
42+
".github/copilot-instructions.md",
43+
"CLAUDE.md",
44+
".cursorrules",
45+
".cursor/rules/",
46+
".instructions.md",
47+
".continuerules",
48+
".prompts/memories",
49+
filepath.Join(userConfigDir, "prompts", "memories"),
50+
"/var/local/prompts/memories",
4351
}
4452

45-
flag.Var(&dirs, "d", "Directory to include in the context. Can be specified multiple times.")
53+
tasks = []string{
54+
".prompts/tasks",
55+
filepath.Join(userConfigDir, "prompts", "tasks"),
56+
"/var/local/prompts/tasks",
57+
}
58+
59+
flag.Var(&memories, "m", "Directory containing memories, or a single memory file. Can be specified multiple times.")
60+
flag.Var(&tasks, "t", "Directory containing tasks, or a single task file. Can be specified multiple times.")
4661
flag.StringVar(&outputDir, "o", ".", "Directory to write the context files to.")
4762
flag.Var(&params, "p", "Parameter to substitute in the prompt. Can be specified multiple times as key=value.")
4863
flag.Var(&includes, "s", "Include memories with matching frontmatter. Can be specified multiple times as key=value.")
@@ -87,15 +102,14 @@ func run(ctx context.Context, args []string) error {
87102
}
88103
defer output.Close()
89104

90-
for _, dir := range dirs {
91-
memoryDir := filepath.Join(dir, "memories")
105+
for _, memory := range memories {
92106

93-
// Skip if the directory doesn't exist
94-
if _, err := os.Stat(memoryDir); os.IsNotExist(err) {
107+
// Skip if the path doesn't exist
108+
if _, err := os.Stat(memory); os.IsNotExist(err) {
95109
continue
96110
}
97111

98-
err := filepath.Walk(memoryDir, func(path string, info os.FileInfo, err error) error {
112+
err := filepath.Walk(memory, func(path string, info os.FileInfo, err error) error {
99113
if err != nil {
100114
return err
101115
}
@@ -161,53 +175,62 @@ func run(ctx context.Context, args []string) error {
161175
}
162176

163177
taskName := args[0]
164-
for _, dir := range dirs {
165-
promptFile := filepath.Join(dir, "tasks", taskName+".md")
166-
167-
if _, err := os.Stat(promptFile); err == nil {
168-
fmt.Fprintf(os.Stdout, "Using prompt file: %s\n", promptFile)
169178

170-
content, err := parseMarkdownFile(promptFile, &struct{}{})
171-
if err != nil {
172-
return fmt.Errorf("failed to parse prompt file: %w", err)
179+
for _, path := range tasks {
180+
stat, err := os.Stat(path)
181+
if os.IsNotExist(err) {
182+
continue
183+
} else if err != nil {
184+
return fmt.Errorf("failed to stat task path %s: %w", path, err)
185+
}
186+
if stat.IsDir() {
187+
path = filepath.Join(path, taskName+".md")
188+
if _, err := os.Stat(path); os.IsNotExist(err) {
189+
continue
173190
}
191+
}
174192

175-
expanded := os.Expand(content, func(key string) string {
176-
if val, ok := params[key]; ok {
177-
return val
178-
}
179-
return ""
180-
})
193+
fmt.Fprintf(os.Stdout, "Using prompt file: %s\n", path)
181194

182-
if _, err := output.WriteString(expanded); err != nil {
183-
return fmt.Errorf("failed to write expanded prompt: %w", err)
184-
}
185-
186-
// Run bootstrap if requested
187-
if runBootstrap {
188-
bootstrapPath := filepath.Join(outputDir, "bootstrap")
195+
content, err := parseMarkdownFile(path, &struct{}{})
196+
if err != nil {
197+
return fmt.Errorf("failed to parse prompt file: %w", err)
198+
}
189199

190-
// Convert to absolute path
191-
absBootstrapPath, err := filepath.Abs(bootstrapPath)
192-
if err != nil {
193-
return fmt.Errorf("failed to get absolute path for bootstrap script: %w", err)
194-
}
200+
expanded := os.Expand(content, func(key string) string {
201+
if val, ok := params[key]; ok {
202+
return val
203+
}
204+
return ""
205+
})
195206

196-
fmt.Fprintf(os.Stdout, "Running bootstrap script: %s\n", absBootstrapPath)
207+
if _, err := output.WriteString(expanded); err != nil {
208+
return fmt.Errorf("failed to write expanded prompt: %w", err)
209+
}
197210

198-
cmd := exec.CommandContext(ctx, absBootstrapPath)
199-
cmd.Stdout = os.Stdout
200-
cmd.Stderr = os.Stderr
201-
cmd.Dir = outputDir
211+
// Run bootstrap if requested
212+
if runBootstrap {
213+
bootstrapPath := filepath.Join(outputDir, "bootstrap")
202214

203-
if err := cmd.Run(); err != nil {
204-
return fmt.Errorf("failed to run bootstrap script: %w", err)
205-
}
215+
// Convert to absolute path
216+
absBootstrapPath, err := filepath.Abs(bootstrapPath)
217+
if err != nil {
218+
return fmt.Errorf("failed to get absolute path for bootstrap script: %w", err)
206219
}
207220

208-
return nil
221+
fmt.Fprintf(os.Stdout, "Running bootstrap script: %s\n", absBootstrapPath)
222+
223+
cmd := exec.CommandContext(ctx, absBootstrapPath)
224+
cmd.Stdout = os.Stdout
225+
cmd.Stderr = os.Stderr
226+
cmd.Dir = outputDir
209227

228+
if err := cmd.Run(); err != nil {
229+
return fmt.Errorf("failed to run bootstrap script: %w", err)
230+
}
210231
}
232+
233+
return nil
211234
}
212235

213236
return fmt.Errorf("prompt file not found for task: %s", taskName)

0 commit comments

Comments
 (0)