From 96c0eb58864555742351f68e87b114195d5c9b2d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 02:07:48 +0000 Subject: [PATCH 1/5] Initial plan From 8d731f1709a8f0a0e77aefa8bcb4f1e76647aca1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 02:13:58 +0000 Subject: [PATCH 2/5] Add -o flag for output file support Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> --- README.md | 21 +++++- integration_test.go | 172 ++++++++++++++++++++++++++++++++++++++++++++ main.go | 32 ++++++--- 3 files changed, 214 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 5e63d7f..169ffa3 100644 --- a/README.md +++ b/README.md @@ -33,15 +33,19 @@ Usage: Options: -C string Change to directory before doing anything. (default ".") + -S value + Exclude rules with matching frontmatter. Can be specified multiple times as key=value. + -o string + Output file. If unspecified, output is written to stdout. -p value Parameter to substitute in the prompt. Can be specified multiple times as key=value. -s value Include rules with matching frontmatter. Can be specified multiple times as key=value. - -S value - Exclude rules with matching frontmatter. Can be specified multiple times as key=value. ``` -### Example +### Examples + +**Example 1: Pipe to another program** ```bash coding-agent-context-cli -p jira_issue_key=PROJ-1234 fix-bug | llm -m gemini-pro @@ -56,6 +60,17 @@ This command will: 6. Print the combined context (rules + task) to `stdout`. 7. Pipe the output to another program (in this case, `llm`). +**Example 2: Save to a file** + +```bash +coding-agent-context-cli -p jira_issue_key=PROJ-1234 -o context.txt fix-bug +``` + +This command will: +1. Generate the combined context as in Example 1. +2. Write the output to `context.txt` instead of stdout. +3. Print diagnostic messages (file inclusions, token counts) to stderr. + ### Example Tasks The `` is the name of the task you want the agent to perform. Here are some common examples: diff --git a/integration_test.go b/integration_test.go index 5261d87..45ecc31 100644 --- a/integration_test.go +++ b/integration_test.go @@ -535,3 +535,175 @@ Please help with this task. t.Errorf(".mdc file content not found in stdout") } } + +func TestOutputFileFlag(t *testing.T) { + // Build the binary + binaryPath := filepath.Join(t.TempDir(), "coding-context") + cmd := exec.Command("go", "build", "-o", binaryPath, ".") + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("failed to build binary: %v\n%s", err, output) + } + + // Create a temporary directory structure + tmpDir := t.TempDir() + rulesDir := filepath.Join(tmpDir, ".agents", "rules") + tasksDir := filepath.Join(tmpDir, ".agents", "tasks") + + if err := os.MkdirAll(rulesDir, 0755); err != nil { + t.Fatalf("failed to create rules dir: %v", err) + } + if err := os.MkdirAll(tasksDir, 0755); err != nil { + t.Fatalf("failed to create tasks dir: %v", err) + } + + // Create a rule file + ruleFile := filepath.Join(rulesDir, "info.md") + ruleContent := `--- +--- +# Project Info + +General information about the project. +` + if err := os.WriteFile(ruleFile, []byte(ruleContent), 0644); err != nil { + t.Fatalf("failed to write rule file: %v", err) + } + + // Create a task file + taskFile := filepath.Join(tasksDir, "test-task.md") + taskContent := `--- +--- +# Test Task + +Please help with this task. +` + if err := os.WriteFile(taskFile, []byte(taskContent), 0644); err != nil { + t.Fatalf("failed to write task file: %v", err) + } + + // Create output file path + outputFile := filepath.Join(tmpDir, "output.txt") + + // Run the binary with -o flag + cmd = exec.Command(binaryPath, "-C", tmpDir, "-o", outputFile, "test-task") + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("failed to run binary: %v\n%s", err, output) + } + + // Check that stdout is empty (since output went to file) + stdoutStr := string(output) + if strings.Contains(stdoutStr, "# Project Info") { + t.Errorf("rule content should not be in stdout when using -o flag") + } + if strings.Contains(stdoutStr, "# Test Task") { + t.Errorf("task content should not be in stdout when using -o flag") + } + + // Check that diagnostic messages are still in stderr (CombinedOutput combines stdout and stderr) + if !strings.Contains(stdoutStr, "Including rule file") { + t.Errorf("diagnostic messages should still be in stderr") + } + + // Read the output file and verify content + fileContent, err := os.ReadFile(outputFile) + if err != nil { + t.Fatalf("failed to read output file: %v", err) + } + + fileStr := string(fileContent) + if !strings.Contains(fileStr, "# Project Info") { + t.Errorf("rule content not found in output file") + } + if !strings.Contains(fileStr, "# Test Task") { + t.Errorf("task content not found in output file") + } + + // Verify diagnostic messages are NOT in the output file + if strings.Contains(fileStr, "Including rule file") { + t.Errorf("diagnostic messages should not be in output file") + } +} + +func TestOutputFileWithBootstrap(t *testing.T) { + // Build the binary + binaryPath := filepath.Join(t.TempDir(), "coding-context") + cmd := exec.Command("go", "build", "-o", binaryPath, ".") + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("failed to build binary: %v\n%s", err, output) + } + + // Create a temporary directory structure + tmpDir := t.TempDir() + rulesDir := filepath.Join(tmpDir, ".agents", "rules") + tasksDir := filepath.Join(tmpDir, ".agents", "tasks") + + if err := os.MkdirAll(rulesDir, 0755); err != nil { + t.Fatalf("failed to create rules dir: %v", err) + } + if err := os.MkdirAll(tasksDir, 0755); err != nil { + t.Fatalf("failed to create tasks dir: %v", err) + } + + // Create a rule file with bootstrap + ruleFile := filepath.Join(rulesDir, "setup.md") + ruleContent := `--- +--- +# Setup + +Setup instructions. +` + if err := os.WriteFile(ruleFile, []byte(ruleContent), 0644); err != nil { + t.Fatalf("failed to write rule file: %v", err) + } + + bootstrapFile := filepath.Join(rulesDir, "setup-bootstrap") + bootstrapContent := `#!/bin/bash +echo "Running setup bootstrap" +` + if err := os.WriteFile(bootstrapFile, []byte(bootstrapContent), 0755); err != nil { + t.Fatalf("failed to write bootstrap file: %v", err) + } + + // Create a task file + taskFile := filepath.Join(tasksDir, "test-task.md") + taskContent := `--- +--- +# Test Task + +Please help with this task. +` + if err := os.WriteFile(taskFile, []byte(taskContent), 0644); err != nil { + t.Fatalf("failed to write task file: %v", err) + } + + // Create output file path + outputFile := filepath.Join(tmpDir, "output.txt") + + // Run the binary with -o flag + cmd = exec.Command(binaryPath, "-C", tmpDir, "-o", outputFile, "test-task") + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("failed to run binary: %v\n%s", err, output) + } + + // Check that bootstrap output is in stderr (part of CombinedOutput) + stdoutStr := string(output) + if !strings.Contains(stdoutStr, "Running setup bootstrap") { + t.Errorf("bootstrap output should still be in stderr") + } + + // Read the output file + fileContent, err := os.ReadFile(outputFile) + if err != nil { + t.Fatalf("failed to read output file: %v", err) + } + + fileStr := string(fileContent) + if !strings.Contains(fileStr, "# Setup") { + t.Errorf("rule content not found in output file") + } + if !strings.Contains(fileStr, "# Test Task") { + t.Errorf("task content not found in output file") + } +} + diff --git a/main.go b/main.go index 77f678f..6736448 100644 --- a/main.go +++ b/main.go @@ -14,10 +14,11 @@ import ( ) var ( - workDir string - params = make(paramMap) - includes = make(selectorMap) - excludes = make(selectorMap) + workDir string + outputFile string + params = make(paramMap) + includes = make(selectorMap) + excludes = make(selectorMap) ) func main() { @@ -25,6 +26,7 @@ func main() { defer cancel() flag.StringVar(&workDir, "C", ".", "Change to directory before doing anything.") + flag.StringVar(&outputFile, "o", "", "Output file. If unspecified, output is written to stdout.") flag.Var(¶ms, "p", "Parameter to substitute in the prompt. Can be specified multiple times as key=value.") flag.Var(&includes, "s", "Include rules with matching frontmatter. Can be specified multiple times as key=value.") flag.Var(&excludes, "S", "Exclude rules with matching frontmatter. Can be specified multiple times as key=value.") @@ -40,14 +42,28 @@ func main() { } flag.Parse() - if err := run(ctx, flag.Args()); err != nil { + // Setup output writer + var output *os.File + if outputFile != "" { + f, err := os.Create(outputFile) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: failed to create output file: %v\n", err) + os.Exit(1) + } + defer f.Close() + output = f + } else { + output = os.Stdout + } + + if err := run(ctx, flag.Args(), output); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) flag.Usage() os.Exit(1) } } -func run(ctx context.Context, args []string) error { +func run(ctx context.Context, args []string, output *os.File) error { if len(args) != 1 { return fmt.Errorf("invalid usage") } @@ -188,7 +204,7 @@ func run(ctx context.Context, args []string) error { tokens := estimateTokens(content) totalTokens += tokens fmt.Fprintf(os.Stderr, "⪢ Including rule file: %s (~%d tokens)\n", path, tokens) - fmt.Println(content) + fmt.Fprintln(output, content) return nil @@ -216,7 +232,7 @@ func run(ctx context.Context, args []string) error { totalTokens += tokens fmt.Fprintf(os.Stderr, "⪢ Including task file: %s (~%d tokens)\n", taskPromptPath, tokens) - fmt.Println(expanded) + fmt.Fprintln(output, expanded) // Print total token count fmt.Fprintf(os.Stderr, "⪢ Total estimated tokens: %d\n", totalTokens) From ea49c823b8ebc39c8594804d73dcd36f8345fb88 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 02:15:43 +0000 Subject: [PATCH 3/5] Fix code formatting and update .gitignore Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> --- .gitignore | 1 + integration_test.go | 3 +-- token_counter_test.go | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 5fc0199..d3bd6b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ # Binary output coding-context +coding-agent-context-cli diff --git a/integration_test.go b/integration_test.go index 45ecc31..6673683 100644 --- a/integration_test.go +++ b/integration_test.go @@ -72,7 +72,7 @@ Please help with this task. outputStr := string(output) bootstrapIdx := strings.Index(outputStr, "Running bootstrap") setupIdx := strings.Index(outputStr, "# Development Setup") - + if bootstrapIdx == -1 { t.Errorf("bootstrap output not found in stdout") } @@ -706,4 +706,3 @@ Please help with this task. t.Errorf("task content not found in output file") } } - diff --git a/token_counter_test.go b/token_counter_test.go index 3daeb82..86a4b6c 100644 --- a/token_counter_test.go +++ b/token_counter_test.go @@ -4,9 +4,9 @@ import "testing" func TestEstimateTokens(t *testing.T) { tests := []struct { - name string - text string - want int + name string + text string + want int }{ { name: "empty string", From 390845c8565aa20086511f564aff72c55b742dfa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 02:18:10 +0000 Subject: [PATCH 4/5] Refactor to use io.Writer interface for better testability Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> --- main.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 6736448..f0d9793 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( _ "embed" "flag" "fmt" + "io" "os" "os/exec" "os/signal" @@ -43,7 +44,7 @@ func main() { flag.Parse() // Setup output writer - var output *os.File + var output io.Writer if outputFile != "" { f, err := os.Create(outputFile) if err != nil { @@ -63,7 +64,7 @@ func main() { } } -func run(ctx context.Context, args []string, output *os.File) error { +func run(ctx context.Context, args []string, output io.Writer) error { if len(args) != 1 { return fmt.Errorf("invalid usage") } From 834c9e4263fca68639bdd1f8f16d172fb16594ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 02:23:40 +0000 Subject: [PATCH 5/5] Move output writer setup into run function Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> --- main.go | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/main.go b/main.go index f0d9793..d0dc624 100644 --- a/main.go +++ b/main.go @@ -43,28 +43,26 @@ func main() { } flag.Parse() + if err := run(ctx, flag.Args()); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + flag.Usage() + os.Exit(1) + } +} + +func run(ctx context.Context, args []string) error { // Setup output writer var output io.Writer if outputFile != "" { f, err := os.Create(outputFile) if err != nil { - fmt.Fprintf(os.Stderr, "Error: failed to create output file: %v\n", err) - os.Exit(1) + return fmt.Errorf("failed to create output file: %w", err) } defer f.Close() output = f } else { output = os.Stdout } - - if err := run(ctx, flag.Args(), output); err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - flag.Usage() - os.Exit(1) - } -} - -func run(ctx context.Context, args []string, output io.Writer) error { if len(args) != 1 { return fmt.Errorf("invalid usage") }