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
53 changes: 45 additions & 8 deletions docs/how-to/create-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,50 @@ 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: |
#! /bin/sh
set -eux
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
```

**Note:** The bootstrap script is saved to a temporary file and executed. Include a shebang line (`#!/bin/sh` or `#!/bin/bash`) to specify the interpreter.

### 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 +217,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
112 changes: 107 additions & 5 deletions docs/reference/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,69 @@ 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: |
#! /bin/sh
set -eux
echo "Fetching project dependencies..." >&2
Copy link
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
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/sh
set -eux

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:**
- The bootstrap script is saved to a temporary executable file and run directly
- Use `#!/bin/sh` (POSIX shell) or `#!/bin/bash` (bash) as the first line to select the interpreter
- 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 +896,10 @@ stage: implementation
priority: high
team: backend
agent: cursor
bootstrap: |
#! /bin/sh
set -eux
echo "Running setup..." >&2
Copy link
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
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 +916,54 @@ 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: |
#! /bin/sh
set -eux
echo "Fetching JIRA data..." >&2
Copy link
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
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 +979,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 +1002,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
87 changes: 87 additions & 0 deletions integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,93 @@ 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: |
#!/bin/sh
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 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")
}
}

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: |
#!/bin/sh
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 output")
}

// 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
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 output")
}
}

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

Expand Down
44 changes: 41 additions & 3 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,42 @@ 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)

// Create a temporary file for the bootstrap script
tmpFile, err := os.CreateTemp("", "bootstrap-*.sh")
if err != nil {
return fmt.Errorf("failed to create temp file for bootstrap script from %s: %w", path, err)
}
tmpFilePath := tmpFile.Name()
defer os.Remove(tmpFilePath)

// Write the bootstrap script to the temp file
if _, err := tmpFile.WriteString(frontmatterBootstrap); err != nil {
tmpFile.Close()
return fmt.Errorf("failed to write bootstrap script from %s: %w", path, err)
}
tmpFile.Close()

// Make it executable
if err := os.Chmod(tmpFilePath, 0o755); err != nil {
return fmt.Errorf("failed to chmod bootstrap script from %s: %w", path, err)
}

cmd := exec.CommandContext(ctx, tmpFilePath)
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr

if err := cc.cmdRunner(cmd); err != nil {
return fmt.Errorf("frontmatter bootstrap script failed for %s: %w", path, err)
}
return nil
}

// 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 All @@ -602,7 +637,10 @@ func (cc *Context) runBootstrapScript(ctx context.Context, path string) error {
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr

return cc.cmdRunner(cmd)
if err := cc.cmdRunner(cmd); err != nil {
return fmt.Errorf("file-based bootstrap script failed for %s: %w", path, err)
}
return nil
}

// discoverSkills searches for skill directories and loads only their metadata (name and description)
Expand Down
Loading