Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
49 changes: 41 additions & 8 deletions docs/how-to/create-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,46 @@ coding-context -s stage=implementation implement-feature

## Rules with Bootstrap Scripts

Create rules that fetch dynamic context:
Bootstrap scripts run before a rule is included, allowing you to fetch or generate dynamic context.

### Frontmatter Bootstrap (Preferred)

The preferred way is to define the bootstrap script directly in frontmatter:

**Rule file (`.agents/rules/jira-context.md`):**
```markdown
---
source: jira
bootstrap: |
if [ -z "$JIRA_ISSUE_KEY" ]; then
exit 0
fi

echo "Fetching JIRA issue: $JIRA_ISSUE_KEY" >&2

# Fetch and process JIRA data
curl -s -H "Authorization: Bearer $JIRA_API_TOKEN" \
"https://your-domain.atlassian.net/rest/api/3/issue/${JIRA_ISSUE_KEY}" \
| jq -r '.fields | {summary, description}' \
> /tmp/jira-context.json
---

# JIRA Context

Issue details are fetched by the bootstrap script.
```

Use with:
```bash
export JIRA_ISSUE_KEY="PROJ-123"
export JIRA_API_TOKEN="your-token"

coding-context -s source=jira fix-bug
```

### File-Based Bootstrap (Legacy)

Alternatively, create a separate executable file named `<rule-name>-bootstrap`:

**Rule file (`.agents/rules/jira-context.md`):**
```markdown
Expand Down Expand Up @@ -174,13 +213,7 @@ curl -s -H "Authorization: Bearer $JIRA_API_TOKEN" \
> /tmp/jira-context.json
```

Use with:
```bash
export JIRA_ISSUE_KEY="PROJ-123"
export JIRA_API_TOKEN="your-token"

coding-context -s source=jira fix-bug
```
**Note:** If both frontmatter and file-based bootstrap exist, frontmatter takes precedence.

## Best Practices

Expand Down
104 changes: 99 additions & 5 deletions docs/reference/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot

Suggested change
echo "Fetching project dependencies..." >&2
#! /bin/sh
set -eux
echo "Fetching project dependencies..." >&2

Copy link
Copy Markdown
Contributor Author

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) and set -eux for better error handling and debugging.

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
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example includes #!/bin/bash, but frontmatter bootstrap is executed via sh -c (the shebang line won’t select bash and may mislead users into writing bash-specific scripts). Suggest removing the shebang line from the example or updating the docs to show invoking bash explicitly (e.g., bash -c ...) if bash is required.

Suggested change
#!/bin/bash

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

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.

Copy link
Copy Markdown
Contributor Author

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

set -e

echo "Setting up environment..." >&2

if [ -z "$API_KEY" ]; then
echo "Warning: API_KEY not set" >&2
exit 0
fi

echo "Fetching data..." >&2
curl -s -H "Authorization: Bearer $API_KEY" \
"https://api.example.com/data" > /tmp/data.json

echo "Setup complete" >&2
---
```

**Usage:**
```bash
export API_KEY="your-api-key"
coding-context implement-feature
```

**Notes:**
- Output goes to stderr, not the assembled context
- If both frontmatter `bootstrap:` and file-based bootstrap exist, frontmatter takes precedence
- Environment variables from the parent process are available
- Exit code 0 is required for successful execution

**See also:** [Bootstrap Scripts](#bootstrap-scripts) for file-based alternative

**Other common fields:**
```yaml
---
Expand All @@ -833,6 +892,8 @@ stage: implementation
priority: high
team: backend
agent: cursor
bootstrap: |
echo "Running setup..." >&2
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot as above.

Copy link
Copy Markdown
Contributor Author

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.

---
```

Expand All @@ -849,23 +910,52 @@ Rules are discovered in many locations. See [Search Paths Reference](./search-pa

## Bootstrap Scripts

Bootstrap scripts are executable files that run before their associated rule file is processed.
Bootstrap scripts run before their associated rule file is processed. There are two ways to define bootstrap scripts:

### Frontmatter Bootstrap (Preferred)

Define the bootstrap script directly in the rule's frontmatter using the `bootstrap:` field. This is the **preferred method** because:
- The script is co-located with the rule content
- No separate file to manage or chmod
- Easier to version control and review

**Example:**
```markdown
---
bootstrap: |
echo "Fetching JIRA data..." >&2
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot add shebang.

Copy link
Copy Markdown
Contributor Author

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.

curl -s -H "Authorization: Bearer $JIRA_API_TOKEN" \
"https://api.example.com/issue/$JIRA_ISSUE_KEY" \
| jq -r '.fields' > /tmp/jira-data.json
echo "Data fetched" >&2
---

# JIRA Context

Issue data is available in /tmp/jira-data.json
```

**See:** [Rule Frontmatter - bootstrap field](#bootstrap-optional) for more details.

### File-Based Bootstrap (Legacy)

Alternatively, create a separate executable file for backward compatibility.

### Naming Convention
#### Naming Convention

For a rule file named `my-rule.md`, the bootstrap script must be named `my-rule-bootstrap` (no extension).

**Example:**
- Rule: `.agents/rules/jira-context.md`
- Bootstrap: `.agents/rules/jira-context-bootstrap`

### Requirements
#### Requirements

1. **Executable permission:** `chmod +x script-name`
2. **Same directory:** Must be in same directory as the rule file
3. **Naming:** Must match rule filename plus `-bootstrap` suffix

### Output Handling
#### Output Handling

- Bootstrap script output goes to **stderr**, not the main context
- The script's stdout is not captured
Expand All @@ -881,7 +971,7 @@ curl -s "https://api.example.com/data" > /tmp/data.json
echo "Data fetched successfully" >&2
```

### Environment Access
#### Environment Access

Bootstrap scripts can access all environment variables from the parent process.

Expand All @@ -904,6 +994,10 @@ curl -s -H "Authorization: Bearer $API_KEY" \
| jq -r '.fields' > /tmp/issue-data.json
```

### Priority

If a rule has **both** frontmatter `bootstrap:` and a file-based bootstrap script, the **frontmatter bootstrap is used** (file is ignored).

## YAML Frontmatter Specification

### Valid Frontmatter
Expand Down
85 changes: 85 additions & 0 deletions integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runTool uses cmd.CombinedOutput(), so this test is asserting against combined stdout+stderr, not stdout specifically. The error message text should reflect that (or the test should be updated to capture stdout/stderr separately) to avoid confusion about where bootstrap output is expected to go.

Suggested change
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")

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

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

}
}

func TestBootstrapFrontmatterPreferredOverFile(t *testing.T) {
dirs := setupTestDirs(t)

// Create a rule file with bootstrap in frontmatter
ruleFile := filepath.Join(dirs.rulesDir, "setup.md")
ruleContent := `---
bootstrap: |
echo "Using frontmatter bootstrap"
---
# Priority Test

Testing that frontmatter bootstrap is preferred.
`
if err := os.WriteFile(ruleFile, []byte(ruleContent), 0o644); err != nil {
t.Fatalf("failed to write rule file: %v", err)
}

// Also create a file-based bootstrap (should be ignored)
bootstrapFile := filepath.Join(dirs.rulesDir, "setup-bootstrap")
bootstrapContent := `#!/bin/bash
echo "Using file bootstrap"
`
if err := os.WriteFile(bootstrapFile, []byte(bootstrapContent), 0o755); err != nil {
t.Fatalf("failed to write bootstrap file: %v", err)
}

createStandardTask(t, dirs.tasksDir, "test-task")

// Run the program
output := runTool(t, "-C", dirs.tmpDir, "test-task")

// Check that frontmatter bootstrap is used
if !strings.Contains(output, "Using frontmatter bootstrap") {
t.Errorf("frontmatter bootstrap output not found in stdout")
}

// Check that file bootstrap is NOT used
if strings.Contains(output, "Using file bootstrap") {
t.Errorf("file bootstrap should not be used when frontmatter bootstrap is present")
}
Comment on lines +229 to +237
Copy link

Copilot AI Feb 18, 2026

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

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


// Check that rule content is present
if !strings.Contains(output, "# Priority Test") {
t.Errorf("rule content not found in stdout")
}
}

func TestMultipleBootstrapFiles(t *testing.T) {
dirs := setupTestDirs(t)

Expand Down
16 changes: 14 additions & 2 deletions pkg/codingcontext/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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)
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the frontmatter bootstrap command fails, the returned error won’t include which rule file triggered it (it will just look like a generic sh failure). Consider wrapping the cc.cmdRunner(cmd) error with the rule path (and indicate it was frontmatter bootstrap) to make failures actionable in logs/CI.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0c706e9. Error messages now include rule path and indicate whether it was frontmatter or file-based bootstrap that failed.

}

// 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))
Expand Down
53 changes: 53 additions & 0 deletions pkg/codingcontext/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case doesn’t actually verify that the frontmatter bootstrap executed (or that the correct script was run); it only checks the number of rules. Since the PR adds new bootstrap behavior, consider asserting which command was invoked by overriding c.cmdRunner to record cmd.Args (or capturing stderr) and verifying it matches the expected frontmatter script.

Copilot generated this review using guidance from repository custom instructions.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged. The unit tests verify that rules are included. The integration tests verify which bootstrap actually runs by checking the output. Given the test infrastructure, this provides good coverage of the feature behavior.

},
{
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
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check doesn’t validate the precedence rule (frontmatter should win over file-based bootstrap). As written, it would still pass if only the file-based bootstrap ran. Consider overriding c.cmdRunner (or capturing stderr) and asserting that the executed command was sh -c with the frontmatter content, and that the file-based bootstrap path was not executed.

Copilot generated this review using guidance from repository custom instructions.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged. The integration tests verify precedence by checking output content. Given the test infrastructure, this provides good coverage of the priority behavior.

},
{
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
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check doesn’t confirm the fallback behavior (file-based bootstrap should run when frontmatter bootstrap is absent). Consider asserting the executed command path (e.g., <rule>-bootstrap) by intercepting c.cmdRunner, rather than only checking rule count.

Copilot generated this review using guidance from repository custom instructions.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged. The integration tests verify fallback behavior by checking output content. Given the test infrastructure, this provides good coverage.

},
{
name: "agent option collects all rules",
setup: func(t *testing.T, dir string) {
Expand Down
Loading