Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <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

Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
148 changes: 148 additions & 0 deletions integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
6 changes: 6 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down