Skip to content

Commit 71b8d96

Browse files
Copilotalexec
andauthored
Add bootstrap script support for tasks (#93)
* Initial plan * Add bootstrap script support for tasks Co-authored-by: alexec <[email protected]> * Update documentation for task bootstrap support Co-authored-by: alexec <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: alexec <[email protected]>
1 parent 11da619 commit 71b8d96

File tree

3 files changed

+175
-8
lines changed

3 files changed

+175
-8
lines changed

README.md

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,13 @@ Each of these would have a corresponding `.md` file with `task_name` in the fron
151151
The tool assembles the context in the following order:
152152

153153
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.
154-
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.
154+
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.
155155
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.
156156
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`.
157-
5. **Parameter Expansion**: It substitutes variables in the task prompt using the `-p` flags.
158-
6. **Output**: It prints the content of all included rule files, followed by the expanded task prompt, to standard output.
159-
7. **Token Count**: A running total of estimated tokens is printed to standard error.
157+
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.
158+
6. **Parameter Expansion**: It substitutes variables in the task prompt using the `-p` flags.
159+
7. **Output**: It prints the content of all included rule files, followed by the expanded task prompt, to standard output.
160+
8. **Token Count**: A running total of estimated tokens is printed to standard error.
160161

161162
### File Search Paths
162163

@@ -458,13 +459,17 @@ If you need to filter on nested data, flatten your frontmatter structure to use
458459

459460
### Bootstrap Scripts
460461

461-
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.
462+
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.
462463

463-
**Example:**
464+
**Examples:**
464465
- Rule file: `.agents/rules/jira.md`
465-
- Bootstrap script: `.agents/rules/jira-bootstrap`
466+
- Rule bootstrap script: `.agents/rules/jira-bootstrap`
467+
- Task file: `.agents/tasks/fix-bug.md`
468+
- Task bootstrap script: `.agents/tasks/fix-bug-bootstrap`
466469

467-
If `jira-bootstrap` is an executable script, it will be run before its corresponding rule file is processed.
470+
Bootstrap scripts are executed in the following order:
471+
1. Rule bootstrap scripts run before their corresponding rule files are processed
472+
2. Task bootstrap scripts run after all rules are processed but before the task content is emitted
468473

469474
**`.agents/rules/jira-bootstrap`:**
470475
```bash
@@ -477,6 +482,14 @@ then
477482
fi
478483
```
479484

485+
**`.agents/tasks/fix-bug-bootstrap`:**
486+
```bash
487+
#!/bin/bash
488+
# This script fetches the latest issue details before the task runs.
489+
echo "Fetching issue information..." >&2
490+
# Fetch and prepare issue data
491+
```
492+
480493
### Emitting Task Frontmatter
481494

482495
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.

integration_test.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,3 +890,151 @@ This is a test task.
890890
t.Errorf("rule frontmatter should not be printed in output")
891891
}
892892
}
893+
894+
func TestTaskBootstrapFromFile(t *testing.T) {
895+
dirs := setupTestDirs(t)
896+
897+
// Create a simple task file
898+
taskFile := filepath.Join(dirs.tasksDir, "test-task.md")
899+
taskContent := `---
900+
task_name: test-task
901+
---
902+
# Test Task
903+
904+
This is a test task.
905+
`
906+
if err := os.WriteFile(taskFile, []byte(taskContent), 0o644); err != nil {
907+
t.Fatalf("failed to write task file: %v", err)
908+
}
909+
910+
// Create a bootstrap file for the task (test-task.md -> test-task-bootstrap)
911+
bootstrapFile := filepath.Join(dirs.tasksDir, "test-task-bootstrap")
912+
bootstrapContent := `#!/bin/bash
913+
echo "Running task bootstrap"
914+
`
915+
if err := os.WriteFile(bootstrapFile, []byte(bootstrapContent), 0o755); err != nil {
916+
t.Fatalf("failed to write task bootstrap file: %v", err)
917+
}
918+
919+
// Run the program
920+
output := runTool(t, "-C", dirs.tmpDir, "test-task")
921+
922+
// Check that bootstrap output appears before task content
923+
bootstrapIdx := strings.Index(output, "Running task bootstrap")
924+
taskIdx := strings.Index(output, "# Test Task")
925+
926+
if bootstrapIdx == -1 {
927+
t.Errorf("task bootstrap output not found in stdout")
928+
}
929+
if taskIdx == -1 {
930+
t.Errorf("task content not found in stdout")
931+
}
932+
if bootstrapIdx != -1 && taskIdx != -1 && bootstrapIdx > taskIdx {
933+
t.Errorf("task bootstrap output should appear before task content")
934+
}
935+
}
936+
937+
func TestTaskBootstrapFileNotRequired(t *testing.T) {
938+
dirs := setupTestDirs(t)
939+
940+
// Create a task file WITHOUT a bootstrap
941+
taskFile := filepath.Join(dirs.tasksDir, "no-bootstrap-task.md")
942+
taskContent := `---
943+
task_name: no-bootstrap-task
944+
---
945+
# Task Without Bootstrap
946+
947+
This task has no bootstrap script.
948+
`
949+
if err := os.WriteFile(taskFile, []byte(taskContent), 0o644); err != nil {
950+
t.Fatalf("failed to write task file: %v", err)
951+
}
952+
953+
// Run the program - should succeed without a bootstrap file
954+
output := runTool(t, "-C", dirs.tmpDir, "no-bootstrap-task")
955+
956+
// Check that task content is present
957+
if !strings.Contains(output, "# Task Without Bootstrap") {
958+
t.Errorf("task content not found in stdout")
959+
}
960+
}
961+
962+
func TestTaskBootstrapWithRuleBootstrap(t *testing.T) {
963+
dirs := setupTestDirs(t)
964+
965+
// Create a rule file with bootstrap
966+
ruleFile := filepath.Join(dirs.rulesDir, "setup.md")
967+
ruleContent := `---
968+
---
969+
# Setup Rule
970+
971+
Setup instructions.
972+
`
973+
if err := os.WriteFile(ruleFile, []byte(ruleContent), 0o644); err != nil {
974+
t.Fatalf("failed to write rule file: %v", err)
975+
}
976+
977+
ruleBootstrapFile := filepath.Join(dirs.rulesDir, "setup-bootstrap")
978+
ruleBootstrapContent := `#!/bin/bash
979+
echo "Running rule bootstrap"
980+
`
981+
if err := os.WriteFile(ruleBootstrapFile, []byte(ruleBootstrapContent), 0o755); err != nil {
982+
t.Fatalf("failed to write rule bootstrap file: %v", err)
983+
}
984+
985+
// Create a task file with bootstrap
986+
taskFile := filepath.Join(dirs.tasksDir, "deploy-task.md")
987+
taskContent := `---
988+
task_name: deploy-task
989+
---
990+
# Deploy Task
991+
992+
Deploy instructions.
993+
`
994+
if err := os.WriteFile(taskFile, []byte(taskContent), 0o644); err != nil {
995+
t.Fatalf("failed to write task file: %v", err)
996+
}
997+
998+
taskBootstrapFile := filepath.Join(dirs.tasksDir, "deploy-task-bootstrap")
999+
taskBootstrapContent := `#!/bin/bash
1000+
echo "Running task bootstrap"
1001+
`
1002+
if err := os.WriteFile(taskBootstrapFile, []byte(taskBootstrapContent), 0o755); err != nil {
1003+
t.Fatalf("failed to write task bootstrap file: %v", err)
1004+
}
1005+
1006+
// Run the program
1007+
output := runTool(t, "-C", dirs.tmpDir, "deploy-task")
1008+
1009+
// Check that both bootstrap scripts ran
1010+
if !strings.Contains(output, "Running rule bootstrap") {
1011+
t.Errorf("rule bootstrap output not found in stdout")
1012+
}
1013+
if !strings.Contains(output, "Running task bootstrap") {
1014+
t.Errorf("task bootstrap output not found in stdout")
1015+
}
1016+
1017+
// Check that both rule and task contents are present
1018+
if !strings.Contains(output, "# Setup Rule") {
1019+
t.Errorf("rule content not found in stdout")
1020+
}
1021+
if !strings.Contains(output, "# Deploy Task") {
1022+
t.Errorf("task content not found in stdout")
1023+
}
1024+
1025+
// Verify the order: rule bootstrap -> rule content -> task bootstrap -> task content
1026+
ruleBootstrapIdx := strings.Index(output, "Running rule bootstrap")
1027+
ruleContentIdx := strings.Index(output, "# Setup Rule")
1028+
taskBootstrapIdx := strings.Index(output, "Running task bootstrap")
1029+
taskContentIdx := strings.Index(output, "# Deploy Task")
1030+
1031+
if ruleBootstrapIdx > ruleContentIdx {
1032+
t.Errorf("rule bootstrap should run before rule content")
1033+
}
1034+
if ruleContentIdx > taskBootstrapIdx {
1035+
t.Errorf("rule content should appear before task bootstrap")
1036+
}
1037+
if taskBootstrapIdx > taskContentIdx {
1038+
t.Errorf("task bootstrap should run before task content")
1039+
}
1040+
}

main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ func (cc *codingContext) run(ctx context.Context, args []string) error {
116116
return fmt.Errorf("failed to find and execute rule files: %w", err)
117117
}
118118

119+
// Run bootstrap script for the task file if it exists
120+
taskExt := filepath.Ext(cc.matchingTaskFile)
121+
if err := cc.runBootstrapScript(ctx, cc.matchingTaskFile, taskExt); err != nil {
122+
return fmt.Errorf("failed to run task bootstrap script: %w", err)
123+
}
124+
119125
if err := cc.emitTaskFileContent(); err != nil {
120126
return fmt.Errorf("failed to emit task file content: %w", err)
121127
}

0 commit comments

Comments
 (0)