Implement BD_GUIDE.md generation for version-stamped documentation (bd-woro)
This implements the ability to separate bd-specific instructions from project-specific instructions by generating a canonical BD_GUIDE.md file. ## Changes 1. Added `--output` flag to `bd onboard` command - Generates version-stamped BD_GUIDE.md at specified path - Includes both agentsContent and copilotInstructionsContent - Auto-generated header warns against manual editing 2. Version tracking integration - checkAndSuggestBDGuideUpdate() detects outdated BD_GUIDE.md - Suggests regeneration when bd version changes - Integrated with maybeShowUpgradeNotification() 3. Comprehensive test coverage - Tests for BD_GUIDE.md generation - Tests for version stamp validation - Tests for content inclusion 4. Documentation updates - Updated AGENTS.md with BD_GUIDE.md workflow - Added regeneration instructions to upgrade workflow ## Benefits - Clear separation of concerns (bd vs project instructions) - Deterministic updates (no LLM involved) - Git-trackable diffs show exactly what changed - Progressive disclosure (agents read when needed) ## Usage \`\`\`bash # Generate BD_GUIDE.md bd onboard --output .beads/BD_GUIDE.md # After upgrading bd bd onboard --output .beads/BD_GUIDE.md # Regenerate \`\`\` Closes bd-woro
This commit is contained in:
@@ -415,6 +415,66 @@ func renderOnboardInstructions(w io.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// bdGuideContent generates the canonical BD_GUIDE.md content
|
||||
const bdGuideHeader = `<!-- Auto-generated by bd v%s - DO NOT EDIT MANUALLY -->
|
||||
<!-- Run 'bd onboard --output .beads/BD_GUIDE.md' to regenerate -->
|
||||
|
||||
# BD (Beads) Guide for AI Agents
|
||||
|
||||
This file contains canonical bd (beads) workflow instructions for AI agents.
|
||||
It is auto-generated and version-stamped to track bd upgrades.
|
||||
|
||||
> **For project-specific instructions**, see AGENTS.md in the repository root.
|
||||
> This file only covers bd tool usage, not project-specific workflows.
|
||||
|
||||
---
|
||||
|
||||
`
|
||||
|
||||
// generateBDGuide creates a version-stamped BD_GUIDE.md file
|
||||
func generateBDGuide(outputPath string) error {
|
||||
// Create output file
|
||||
f, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create output file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Write header with version stamp
|
||||
if _, err := fmt.Fprintf(f, bdGuideHeader, Version); err != nil {
|
||||
return fmt.Errorf("failed to write header: %w", err)
|
||||
}
|
||||
|
||||
// Write AGENTS.md content (bd-specific instructions)
|
||||
if _, err := f.WriteString(agentsContent); err != nil {
|
||||
return fmt.Errorf("failed to write agents content: %w", err)
|
||||
}
|
||||
|
||||
// Write separator
|
||||
if _, err := f.WriteString("\n\n---\n\n"); err != nil {
|
||||
return fmt.Errorf("failed to write separator: %w", err)
|
||||
}
|
||||
|
||||
// Write Copilot instructions content (comprehensive technical guide)
|
||||
if _, err := f.WriteString(copilotInstructionsContent); err != nil {
|
||||
return fmt.Errorf("failed to write copilot content: %w", err)
|
||||
}
|
||||
|
||||
// Write footer with regeneration instructions
|
||||
footer := fmt.Sprintf("\n\n---\n\n"+
|
||||
"**Generated by bd v%s**\n\n"+
|
||||
"To regenerate this file after upgrading bd:\n"+
|
||||
"```bash\n"+
|
||||
"bd onboard --output .beads/BD_GUIDE.md\n"+
|
||||
"```\n", Version)
|
||||
|
||||
if _, err := f.WriteString(footer); err != nil {
|
||||
return fmt.Errorf("failed to write footer: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var onboardCmd = &cobra.Command{
|
||||
Use: "onboard",
|
||||
Short: "Display instructions for configuring AGENTS.md",
|
||||
@@ -422,8 +482,29 @@ var onboardCmd = &cobra.Command{
|
||||
|
||||
This command outputs instructions that AI agents should follow to integrate bd
|
||||
into the project's agent documentation. The agent will intelligently merge the
|
||||
content into AGENTS.md and update CLAUDE.md if present.`,
|
||||
content into AGENTS.md and update CLAUDE.md if present.
|
||||
|
||||
Use --output to generate a canonical BD_GUIDE.md file instead:
|
||||
bd onboard --output .beads/BD_GUIDE.md
|
||||
|
||||
The generated BD_GUIDE.md is version-stamped and auto-generated - it should
|
||||
never be manually edited. This separates bd-specific instructions (which change
|
||||
with bd upgrades) from project-specific instructions in AGENTS.md.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
outputPath, _ := cmd.Flags().GetString("output")
|
||||
|
||||
if outputPath != "" {
|
||||
// Generate BD_GUIDE.md instead of onboarding instructions
|
||||
if err := generateBDGuide(outputPath); err != nil {
|
||||
fmt.Fprintf(cmd.ErrOrStderr(), "Error generating BD_GUIDE.md: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("✓ Generated %s (bd v%s)\n", outputPath, Version)
|
||||
fmt.Println(" This file is auto-generated - do not edit manually")
|
||||
fmt.Println(" Update your AGENTS.md to reference this file instead of duplicating bd instructions")
|
||||
return
|
||||
}
|
||||
|
||||
if err := renderOnboardInstructions(cmd.OutOrStdout()); err != nil {
|
||||
if _, writeErr := fmt.Fprintf(cmd.ErrOrStderr(), "Error rendering onboarding instructions: %v\n", err); writeErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error rendering onboarding instructions: %v (stderr write failed: %v)\n", err, writeErr)
|
||||
@@ -434,5 +515,6 @@ content into AGENTS.md and update CLAUDE.md if present.`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
onboardCmd.Flags().String("output", "", "Generate BD_GUIDE.md at the specified path (e.g., .beads/BD_GUIDE.md)")
|
||||
rootCmd.AddCommand(onboardCmd)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -68,3 +70,120 @@ func TestOnboardCommand(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGenerateBDGuide(t *testing.T) {
|
||||
t.Run("generates BD_GUIDE.md with version stamp", func(t *testing.T) {
|
||||
// Create temp directory
|
||||
tmpDir := t.TempDir()
|
||||
outputPath := filepath.Join(tmpDir, "BD_GUIDE.md")
|
||||
|
||||
// Generate BD_GUIDE.md
|
||||
if err := generateBDGuide(outputPath); err != nil {
|
||||
t.Fatalf("generateBDGuide() error = %v", err)
|
||||
}
|
||||
|
||||
// Read generated file
|
||||
content, err := os.ReadFile(outputPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read generated file: %v", err)
|
||||
}
|
||||
|
||||
output := string(content)
|
||||
|
||||
// Verify version stamp in header
|
||||
if !strings.Contains(output, "Auto-generated by bd v"+Version) {
|
||||
t.Error("Generated file should contain version stamp in header")
|
||||
}
|
||||
|
||||
if !strings.Contains(output, "DO NOT EDIT MANUALLY") {
|
||||
t.Error("Generated file should contain DO NOT EDIT warning")
|
||||
}
|
||||
|
||||
// Verify regeneration instructions
|
||||
if !strings.Contains(output, "bd onboard --output") {
|
||||
t.Error("Generated file should contain regeneration instructions")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("includes agents content", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
outputPath := filepath.Join(tmpDir, "BD_GUIDE.md")
|
||||
|
||||
if err := generateBDGuide(outputPath); err != nil {
|
||||
t.Fatalf("generateBDGuide() error = %v", err)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(outputPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read generated file: %v", err)
|
||||
}
|
||||
|
||||
output := string(content)
|
||||
|
||||
// Verify key sections from agentsContent are present
|
||||
expectedSections := []string{
|
||||
"Issue Tracking with bd (beads)",
|
||||
"bd ready",
|
||||
"bd create",
|
||||
"MCP Server",
|
||||
}
|
||||
|
||||
for _, section := range expectedSections {
|
||||
if !strings.Contains(output, section) {
|
||||
t.Errorf("Generated file should contain '%s'", section)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("includes copilot instructions content", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
outputPath := filepath.Join(tmpDir, "BD_GUIDE.md")
|
||||
|
||||
if err := generateBDGuide(outputPath); err != nil {
|
||||
t.Fatalf("generateBDGuide() error = %v", err)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(outputPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read generated file: %v", err)
|
||||
}
|
||||
|
||||
output := string(content)
|
||||
|
||||
// Verify key sections from copilotInstructionsContent are present
|
||||
expectedSections := []string{
|
||||
"GitHub Copilot Instructions",
|
||||
"Project Structure",
|
||||
"Tech Stack",
|
||||
"Coding Guidelines",
|
||||
}
|
||||
|
||||
for _, section := range expectedSections {
|
||||
if !strings.Contains(output, section) {
|
||||
t.Errorf("Generated file should contain '%s'", section)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("has proper structure with separators", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
outputPath := filepath.Join(tmpDir, "BD_GUIDE.md")
|
||||
|
||||
if err := generateBDGuide(outputPath); err != nil {
|
||||
t.Fatalf("generateBDGuide() error = %v", err)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(outputPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read generated file: %v", err)
|
||||
}
|
||||
|
||||
output := string(content)
|
||||
|
||||
// Count separators (should have at least 3: after header, between sections, before footer)
|
||||
separatorCount := strings.Count(output, "---")
|
||||
if separatorCount < 3 {
|
||||
t.Errorf("Expected at least 3 separators (---), got %d", separatorCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -114,9 +114,67 @@ func maybeShowUpgradeNotification() {
|
||||
// Display notification
|
||||
fmt.Printf("🔄 bd upgraded from v%s to v%s since last use\n", previousVersion, Version)
|
||||
fmt.Println("💡 Run 'bd upgrade review' to see what changed")
|
||||
|
||||
// Check if BD_GUIDE.md exists and needs updating
|
||||
checkAndSuggestBDGuideUpdate()
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// checkAndSuggestBDGuideUpdate checks if .beads/BD_GUIDE.md exists and suggests regeneration if outdated.
|
||||
// bd-woro: Auto-update BD_GUIDE.md on version changes
|
||||
func checkAndSuggestBDGuideUpdate() {
|
||||
beadsDir := beads.FindBeadsDir()
|
||||
if beadsDir == "" {
|
||||
return
|
||||
}
|
||||
|
||||
guidePath := beadsDir + "/BD_GUIDE.md"
|
||||
|
||||
// Check if BD_GUIDE.md exists
|
||||
if _, err := os.Stat(guidePath); os.IsNotExist(err) {
|
||||
// File doesn't exist - no suggestion needed
|
||||
return
|
||||
}
|
||||
|
||||
// Read first few lines to check version stamp
|
||||
content, err := os.ReadFile(guidePath)
|
||||
if err != nil {
|
||||
return // Silent failure
|
||||
}
|
||||
|
||||
// Look for version in the first 200 bytes (should be in the header)
|
||||
header := string(content)
|
||||
if len(header) > 200 {
|
||||
header = header[:200]
|
||||
}
|
||||
|
||||
// Check if the file has the old version stamp
|
||||
oldVersionStamp := fmt.Sprintf("bd v%s", previousVersion)
|
||||
currentVersionStamp := fmt.Sprintf("bd v%s", Version)
|
||||
|
||||
if containsSubstring(header, oldVersionStamp) && !containsSubstring(header, currentVersionStamp) {
|
||||
// BD_GUIDE.md is outdated
|
||||
fmt.Printf("📄 BD_GUIDE.md is outdated (v%s → v%s)\n", previousVersion, Version)
|
||||
fmt.Printf("💡 Run 'bd onboard --output .beads/BD_GUIDE.md' to regenerate\n")
|
||||
}
|
||||
}
|
||||
|
||||
// containsSubstring checks if haystack contains needle (case-sensitive)
|
||||
func containsSubstring(haystack, needle string) bool {
|
||||
return len(haystack) >= len(needle) && findSubstring(haystack, needle) >= 0
|
||||
}
|
||||
|
||||
// findSubstring returns the index of needle in haystack, or -1 if not found
|
||||
func findSubstring(haystack, needle string) int {
|
||||
for i := 0; i <= len(haystack)-len(needle); i++ {
|
||||
if haystack[i:i+len(needle)] == needle {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// autoMigrateOnVersionBump automatically migrates the database when CLI version changes.
|
||||
// This function is best-effort - failures are silent to avoid disrupting commands.
|
||||
// Called from PersistentPreRun after daemon check but before opening DB for main operation.
|
||||
|
||||
Reference in New Issue
Block a user