-
Notifications
You must be signed in to change notification settings - Fork 3
Support bootstrap scripts in rule frontmatter (preferred over file-based) #207
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
7ad7b2a
27def2c
64d4d8b
0c706e9
058f599
b23a569
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -824,6 +824,65 @@ coding-context -p version=1.2.3 my-task | |||
|
|
||||
| This is useful when rules contain template syntax that should be preserved for the AI agent to process. | ||||
|
|
||||
| #### `bootstrap` (optional) | ||||
|
|
||||
| **Type:** String (multiline) | ||||
| **Purpose:** Shell script to execute before the rule is included. This is the **preferred method** for defining bootstrap scripts. | ||||
|
|
||||
| The bootstrap script runs via `sh -c`, with output sent to stderr (not included in the AI context). This allows rules to fetch dynamic data, set up environment, or prepare context before the rule content is processed. | ||||
|
|
||||
| **Example:** | ||||
| ```yaml | ||||
| --- | ||||
| languages: | ||||
| - go | ||||
| bootstrap: | | ||||
| echo "Fetching project dependencies..." >&2 | ||||
| go list -m all > /tmp/go-deps.txt | ||||
| echo "Dependencies cached" >&2 | ||||
| --- | ||||
|
|
||||
| # Go Dependency Context | ||||
|
|
||||
| Dependencies are listed in /tmp/go-deps.txt | ||||
| ``` | ||||
|
|
||||
| **Multiple commands:** | ||||
| ```yaml | ||||
| --- | ||||
| bootstrap: | | ||||
| #!/bin/bash | ||||
|
||||
| #!/bin/bash |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot rather than making this a shell script, we should save the bootstrap script and run it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 0c706e9. Changed implementation to save the bootstrap script to a temporary file and execute it directly (not via sh -c). Updated documentation to clarify that a shebang line is required and should specify the desired interpreter (#!/bin/sh or #!/bin/bash).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot as above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in b23a569. Added #! /bin/sh and set -eux to the bootstrap example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot add shebang.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in b23a569. Added #! /bin/sh and set -eux to the bootstrap example.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -155,6 +155,91 @@ General information about the project. | |||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| func TestBootstrapFromFrontmatter(t *testing.T) { | ||||||||||||||||||||||||||||||||||||||||||||||
| dirs := setupTestDirs(t) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Create a rule file with bootstrap in frontmatter | ||||||||||||||||||||||||||||||||||||||||||||||
| ruleFile := filepath.Join(dirs.rulesDir, "setup.md") | ||||||||||||||||||||||||||||||||||||||||||||||
| ruleContent := `--- | ||||||||||||||||||||||||||||||||||||||||||||||
| bootstrap: | | ||||||||||||||||||||||||||||||||||||||||||||||
| echo "Bootstrap from frontmatter" | ||||||||||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||||||||||
| # Setup | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| This is a setup guide with frontmatter bootstrap. | ||||||||||||||||||||||||||||||||||||||||||||||
| ` | ||||||||||||||||||||||||||||||||||||||||||||||
| if err := os.WriteFile(ruleFile, []byte(ruleContent), 0o644); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| t.Fatalf("failed to write rule file: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| createStandardTask(t, dirs.tasksDir, "test-task") | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Run the program | ||||||||||||||||||||||||||||||||||||||||||||||
| output := runTool(t, "-C", dirs.tmpDir, "test-task") | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Check that bootstrap output appears | ||||||||||||||||||||||||||||||||||||||||||||||
| if !strings.Contains(output, "Bootstrap from frontmatter") { | ||||||||||||||||||||||||||||||||||||||||||||||
| t.Errorf("bootstrap output from frontmatter not found in stdout") | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Check that rule content is present | ||||||||||||||||||||||||||||||||||||||||||||||
| if !strings.Contains(output, "# Setup") { | ||||||||||||||||||||||||||||||||||||||||||||||
| t.Errorf("rule content not found in stdout") | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Check that task content is present | ||||||||||||||||||||||||||||||||||||||||||||||
| if !strings.Contains(output, "# Test Task") { | ||||||||||||||||||||||||||||||||||||||||||||||
| t.Errorf("task content not found in stdout") | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
| t.Errorf("bootstrap output from frontmatter not found in stdout") | |
| } | |
| // Check that rule content is present | |
| if !strings.Contains(output, "# Setup") { | |
| t.Errorf("rule content not found in stdout") | |
| } | |
| // Check that task content is present | |
| if !strings.Contains(output, "# Test Task") { | |
| t.Errorf("task content not found in stdout") | |
| t.Errorf("bootstrap output from frontmatter not found in output") | |
| } | |
| // Check that rule content is present | |
| if !strings.Contains(output, "# Setup") { | |
| t.Errorf("rule content not found in output") | |
| } | |
| // Check that task content is present | |
| if !strings.Contains(output, "# Test Task") { | |
| t.Errorf("task content not found in output") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 0c706e9. Updated error messages to refer to "output" instead of "stdout".
Copilot
AI
Feb 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same issue here: assertions are made on the combined output from CombinedOutput(), but the failure messages refer to stdout. Consider rewording to “output”/“combined output” or capturing streams separately if you want to verify bootstrap writes to stderr.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 0c706e9. Updated error messages to refer to "output" instead of "stdout".
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -565,7 +565,7 @@ func (cc *Context) findExecuteRuleFiles(ctx context.Context, homeDir string) err | |
| _, reason := cc.includes.MatchesIncludes(*baseFm) | ||
| cc.logger.Info("Including rule file", "path", path, "reason", reason, "tokens", tokens) | ||
|
|
||
| if err := cc.runBootstrapScript(ctx, path); err != nil { | ||
| if err := cc.runBootstrapScript(ctx, path, frontmatter.Bootstrap); err != nil { | ||
| return fmt.Errorf("failed to run bootstrap script: %w", err) | ||
| } | ||
|
|
||
|
|
@@ -578,7 +578,19 @@ func (cc *Context) findExecuteRuleFiles(ctx context.Context, homeDir string) err | |
| return nil | ||
| } | ||
|
|
||
| func (cc *Context) runBootstrapScript(ctx context.Context, path string) error { | ||
| func (cc *Context) runBootstrapScript(ctx context.Context, path string, frontmatterBootstrap string) error { | ||
| // Prefer frontmatter bootstrap if present | ||
| if frontmatterBootstrap != "" { | ||
| cc.logger.Info("Running bootstrap from frontmatter", "path", path) | ||
|
|
||
| cmd := exec.CommandContext(ctx, "sh", "-c", frontmatterBootstrap) | ||
| cmd.Stdout = os.Stderr | ||
| cmd.Stderr = os.Stderr | ||
|
|
||
| return cc.cmdRunner(cmd) | ||
|
||
| } | ||
|
|
||
| // Fall back to file-based bootstrap | ||
| // Check for a bootstrap file named <markdown-file-without-md/mdc-suffix>-bootstrap | ||
| // For example, setup.md -> setup-bootstrap, setup.mdc -> setup-bootstrap | ||
| baseNameWithoutExt := strings.TrimSuffix(path, filepath.Ext(path)) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -588,6 +588,59 @@ func TestContext_Run_Rules(t *testing.T) { | |
| } | ||
| }, | ||
| }, | ||
| { | ||
| name: "bootstrap from frontmatter is preferred", | ||
| setup: func(t *testing.T, dir string) { | ||
| createTask(t, dir, "frontmatter-bootstrap", "", "Task") | ||
| // Create rule with bootstrap in frontmatter | ||
| createRule(t, dir, ".agents/rules/rule-with-frontmatter.md", | ||
| "bootstrap: |\n echo 'frontmatter bootstrap'\n", | ||
| "Rule content") | ||
| }, | ||
| taskName: "frontmatter-bootstrap", | ||
| wantErr: false, | ||
| check: func(t *testing.T, result *Result) { | ||
| if len(result.Rules) != 1 { | ||
| t.Errorf("expected 1 rule, got %d", len(result.Rules)) | ||
| } | ||
| }, | ||
|
Comment on lines
+602
to
+607
|
||
| }, | ||
| { | ||
| name: "bootstrap from frontmatter preferred over file", | ||
| setup: func(t *testing.T, dir string) { | ||
| createTask(t, dir, "frontmatter-priority", "", "Task") | ||
| // Create rule with BOTH frontmatter and file bootstrap | ||
| createRule(t, dir, ".agents/rules/priority-rule.md", | ||
| "bootstrap: |\n echo 'using frontmatter'\n", | ||
| "Rule content") | ||
| // Also create a file-based bootstrap (should be ignored) | ||
| createBootstrapScript(t, dir, ".agents/rules/priority-rule.md", "#!/bin/sh\necho 'using file'") | ||
| }, | ||
| taskName: "frontmatter-priority", | ||
| wantErr: false, | ||
| check: func(t *testing.T, result *Result) { | ||
| if len(result.Rules) != 1 { | ||
| t.Errorf("expected 1 rule, got %d", len(result.Rules)) | ||
| } | ||
| }, | ||
|
Comment on lines
+625
to
+630
|
||
| }, | ||
| { | ||
| name: "bootstrap from file when frontmatter empty", | ||
| setup: func(t *testing.T, dir string) { | ||
| createTask(t, dir, "file-fallback", "", "Task") | ||
| // Create rule WITHOUT frontmatter bootstrap | ||
| createRule(t, dir, ".agents/rules/fallback-rule.md", "", "Rule content") | ||
| // Create file-based bootstrap (should be used) | ||
| createBootstrapScript(t, dir, ".agents/rules/fallback-rule.md", "#!/bin/sh\necho 'using file fallback'") | ||
| }, | ||
| taskName: "file-fallback", | ||
| wantErr: false, | ||
| check: func(t *testing.T, result *Result) { | ||
| if len(result.Rules) != 1 { | ||
| t.Errorf("expected 1 rule, got %d", len(result.Rules)) | ||
| } | ||
| }, | ||
|
Comment on lines
+645
to
+650
|
||
| }, | ||
| { | ||
| name: "agent option collects all rules", | ||
| setup: func(t *testing.T, dir string) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 058f599. Updated all frontmatter bootstrap examples in documentation to use
#! /bin/sh(with space) andset -euxfor better error handling and debugging.