diff --git a/README.md b/README.md index ef02c57..862510d 100644 --- a/README.md +++ b/README.md @@ -151,12 +151,13 @@ Each of these would have a corresponding `.md` file with `task_name` in the fron The tool assembles the context in the following order: 1. **Rule Files**: It searches a list of predefined locations for rule files (`.md` or `.mdc`). These locations include the current directory, ancestor directories, user's home directory, and system-wide directories. -2. **Bootstrap Scripts**: For each rule file found (e.g., `my-rule.md`), it looks for an executable script named `my-rule-bootstrap`. If found, it runs the script before processing the rule file. These scripts are meant for bootstrapping the environment (e.g., installing tools) and their output is sent to `stderr`, not into the main context. +2. **Rule Bootstrap Scripts**: For each rule file found (e.g., `my-rule.md`), it looks for an executable script named `my-rule-bootstrap`. If found, it runs the script before processing the rule file. These scripts are meant for bootstrapping the environment (e.g., installing tools) and their output is sent to `stderr`, not into the main context. 3. **Filtering**: If `-s` (include) flag is used, it parses the YAML frontmatter of each rule file to decide whether to include it. Note that selectors can only match top-level YAML fields (e.g., `language: go`), not nested fields. 4. **Task Prompt**: It searches for a task file with `task_name: ` in its frontmatter. The filename doesn't matter. If selectors are provided with `-s`, they are used to filter between multiple task files with the same `task_name`. -5. **Parameter Expansion**: It substitutes variables in the task prompt using the `-p` flags. -6. **Output**: It prints the content of all included rule files, followed by the expanded task prompt, to standard output. -7. **Token Count**: A running total of estimated tokens is printed to standard error. +5. **Task Bootstrap Script**: For the task file found (e.g., `fix-bug.md`), it looks for an executable script named `fix-bug-bootstrap`. If found, it runs the script before processing the task file. This allows task-specific environment setup or data preparation. +6. **Parameter Expansion**: It substitutes variables in the task prompt using the `-p` flags. +7. **Output**: It prints the content of all included rule files, followed by the expanded task prompt, to standard output. +8. **Token Count**: A running total of estimated tokens is printed to standard error. ### File Search Paths @@ -458,13 +459,17 @@ If you need to filter on nested data, flatten your frontmatter structure to use ### Bootstrap Scripts -A bootstrap script is an executable file that has the same name as a rule file but with a `-bootstrap` suffix. These scripts are used to prepare the environment, for example by installing necessary tools. The output of these scripts is sent to `stderr` and is not part of the AI context. +A bootstrap script is an executable file that has the same name as a rule or task file but with a `-bootstrap` suffix. These scripts are used to prepare the environment, for example by installing necessary tools. The output of these scripts is sent to `stderr` and is not part of the AI context. -**Example:** +**Examples:** - Rule file: `.agents/rules/jira.md` -- Bootstrap script: `.agents/rules/jira-bootstrap` +- Rule bootstrap script: `.agents/rules/jira-bootstrap` +- Task file: `.agents/tasks/fix-bug.md` +- Task bootstrap script: `.agents/tasks/fix-bug-bootstrap` -If `jira-bootstrap` is an executable script, it will be run before its corresponding rule file is processed. +Bootstrap scripts are executed in the following order: +1. Rule bootstrap scripts run before their corresponding rule files are processed +2. Task bootstrap scripts run after all rules are processed but before the task content is emitted **`.agents/rules/jira-bootstrap`:** ```bash @@ -477,6 +482,14 @@ then fi ``` +**`.agents/tasks/fix-bug-bootstrap`:** +```bash +#!/bin/bash +# This script fetches the latest issue details before the task runs. +echo "Fetching issue information..." >&2 +# Fetch and prepare issue data +``` + ### Emitting Task Frontmatter The `-t` flag allows you to include the task's YAML frontmatter at the beginning of the output. This is useful when the AI agent or downstream tool needs access to metadata about the task being executed. diff --git a/integration_test.go b/integration_test.go index b6ccfea..d9ce7c8 100644 --- a/integration_test.go +++ b/integration_test.go @@ -890,3 +890,151 @@ This is a test task. t.Errorf("rule frontmatter should not be printed in output") } } + +func TestTaskBootstrapFromFile(t *testing.T) { + dirs := setupTestDirs(t) + + // Create a simple task file + taskFile := filepath.Join(dirs.tasksDir, "test-task.md") + taskContent := `--- +task_name: test-task +--- +# Test Task + +This is a test task. +` + if err := os.WriteFile(taskFile, []byte(taskContent), 0o644); err != nil { + t.Fatalf("failed to write task file: %v", err) + } + + // Create a bootstrap file for the task (test-task.md -> test-task-bootstrap) + bootstrapFile := filepath.Join(dirs.tasksDir, "test-task-bootstrap") + bootstrapContent := `#!/bin/bash +echo "Running task bootstrap" +` + if err := os.WriteFile(bootstrapFile, []byte(bootstrapContent), 0o755); err != nil { + t.Fatalf("failed to write task bootstrap file: %v", err) + } + + // Run the program + output := runTool(t, "-C", dirs.tmpDir, "test-task") + + // Check that bootstrap output appears before task content + bootstrapIdx := strings.Index(output, "Running task bootstrap") + taskIdx := strings.Index(output, "# Test Task") + + if bootstrapIdx == -1 { + t.Errorf("task bootstrap output not found in stdout") + } + if taskIdx == -1 { + t.Errorf("task content not found in stdout") + } + if bootstrapIdx != -1 && taskIdx != -1 && bootstrapIdx > taskIdx { + t.Errorf("task bootstrap output should appear before task content") + } +} + +func TestTaskBootstrapFileNotRequired(t *testing.T) { + dirs := setupTestDirs(t) + + // Create a task file WITHOUT a bootstrap + taskFile := filepath.Join(dirs.tasksDir, "no-bootstrap-task.md") + taskContent := `--- +task_name: no-bootstrap-task +--- +# Task Without Bootstrap + +This task has no bootstrap script. +` + if err := os.WriteFile(taskFile, []byte(taskContent), 0o644); err != nil { + t.Fatalf("failed to write task file: %v", err) + } + + // Run the program - should succeed without a bootstrap file + output := runTool(t, "-C", dirs.tmpDir, "no-bootstrap-task") + + // Check that task content is present + if !strings.Contains(output, "# Task Without Bootstrap") { + t.Errorf("task content not found in stdout") + } +} + +func TestTaskBootstrapWithRuleBootstrap(t *testing.T) { + dirs := setupTestDirs(t) + + // Create a rule file with bootstrap + ruleFile := filepath.Join(dirs.rulesDir, "setup.md") + ruleContent := `--- +--- +# Setup Rule + +Setup instructions. +` + if err := os.WriteFile(ruleFile, []byte(ruleContent), 0o644); err != nil { + t.Fatalf("failed to write rule file: %v", err) + } + + ruleBootstrapFile := filepath.Join(dirs.rulesDir, "setup-bootstrap") + ruleBootstrapContent := `#!/bin/bash +echo "Running rule bootstrap" +` + if err := os.WriteFile(ruleBootstrapFile, []byte(ruleBootstrapContent), 0o755); err != nil { + t.Fatalf("failed to write rule bootstrap file: %v", err) + } + + // Create a task file with bootstrap + taskFile := filepath.Join(dirs.tasksDir, "deploy-task.md") + taskContent := `--- +task_name: deploy-task +--- +# Deploy Task + +Deploy instructions. +` + if err := os.WriteFile(taskFile, []byte(taskContent), 0o644); err != nil { + t.Fatalf("failed to write task file: %v", err) + } + + taskBootstrapFile := filepath.Join(dirs.tasksDir, "deploy-task-bootstrap") + taskBootstrapContent := `#!/bin/bash +echo "Running task bootstrap" +` + if err := os.WriteFile(taskBootstrapFile, []byte(taskBootstrapContent), 0o755); err != nil { + t.Fatalf("failed to write task bootstrap file: %v", err) + } + + // Run the program + output := runTool(t, "-C", dirs.tmpDir, "deploy-task") + + // Check that both bootstrap scripts ran + if !strings.Contains(output, "Running rule bootstrap") { + t.Errorf("rule bootstrap output not found in stdout") + } + if !strings.Contains(output, "Running task bootstrap") { + t.Errorf("task bootstrap output not found in stdout") + } + + // Check that both rule and task contents are present + if !strings.Contains(output, "# Setup Rule") { + t.Errorf("rule content not found in stdout") + } + if !strings.Contains(output, "# Deploy Task") { + t.Errorf("task content not found in stdout") + } + + // Verify the order: rule bootstrap -> rule content -> task bootstrap -> task content + ruleBootstrapIdx := strings.Index(output, "Running rule bootstrap") + ruleContentIdx := strings.Index(output, "# Setup Rule") + taskBootstrapIdx := strings.Index(output, "Running task bootstrap") + taskContentIdx := strings.Index(output, "# Deploy Task") + + if ruleBootstrapIdx > ruleContentIdx { + t.Errorf("rule bootstrap should run before rule content") + } + if ruleContentIdx > taskBootstrapIdx { + t.Errorf("rule content should appear before task bootstrap") + } + if taskBootstrapIdx > taskContentIdx { + t.Errorf("task bootstrap should run before task content") + } +} diff --git a/main.go b/main.go index e58ac4f..a13b313 100644 --- a/main.go +++ b/main.go @@ -116,6 +116,12 @@ func (cc *codingContext) run(ctx context.Context, args []string) error { return fmt.Errorf("failed to find and execute rule files: %w", err) } + // Run bootstrap script for the task file if it exists + taskExt := filepath.Ext(cc.matchingTaskFile) + if err := cc.runBootstrapScript(ctx, cc.matchingTaskFile, taskExt); err != nil { + return fmt.Errorf("failed to run task bootstrap script: %w", err) + } + if err := cc.emitTaskFileContent(); err != nil { return fmt.Errorf("failed to emit task file content: %w", err) }