This commit is contained in:
Steve Yegge
2025-11-23 21:29:25 -08:00
8 changed files with 1600 additions and 685 deletions

291
.beads/BD_GUIDE.md Normal file
View File

@@ -0,0 +1,291 @@
<!-- Auto-generated by bd v0.24.2 - 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.
---
## Issue Tracking with bd (beads)
**IMPORTANT**: This project uses **bd (beads)** for ALL issue tracking. Do NOT use markdown TODOs, task lists, or other tracking methods.
### Why bd?
- Dependency-aware: Track blockers and relationships between issues
- Git-friendly: Auto-syncs to JSONL for version control
- Agent-optimized: JSON output, ready work detection, discovered-from links
- Prevents duplicate tracking systems and confusion
### Quick Start
**Check for ready work:**
```bash
bd ready --json
```
**Create new issues:**
```bash
bd create "Issue title" -t bug|feature|task -p 0-4 --json
bd create "Issue title" -p 1 --deps discovered-from:bd-123 --json
```
**Claim and update:**
```bash
bd update bd-42 --status in_progress --json
bd update bd-42 --priority 1 --json
```
**Complete work:**
```bash
bd close bd-42 --reason "Completed" --json
```
### Issue Types
- `bug` - Something broken
- `feature` - New functionality
- `task` - Work item (tests, docs, refactoring)
- `epic` - Large feature with subtasks
- `chore` - Maintenance (dependencies, tooling)
### Priorities
- `0` - Critical (security, data loss, broken builds)
- `1` - High (major features, important bugs)
- `2` - Medium (default, nice-to-have)
- `3` - Low (polish, optimization)
- `4` - Backlog (future ideas)
### Workflow for AI Agents
1. **Check ready work**: `bd ready` shows unblocked issues
2. **Claim your task**: `bd update <id> --status in_progress`
3. **Work on it**: Implement, test, document
4. **Discover new work?** Create linked issue:
- `bd create "Found bug" -p 1 --deps discovered-from:<parent-id>`
5. **Complete**: `bd close <id> --reason "Done"`
6. **Commit together**: Always commit the `.beads/issues.jsonl` file together with the code changes so issue state stays in sync with code state
### Auto-Sync
bd automatically syncs with git:
- Exports to `.beads/issues.jsonl` after changes (5s debounce)
- Imports from JSONL when newer (e.g., after `git pull`)
- No manual export/import needed!
### GitHub Copilot Integration
If using GitHub Copilot, also create `.github/copilot-instructions.md` for automatic instruction loading.
Run `bd onboard` to get the content, or see step 2 of the onboard instructions.
### MCP Server (Recommended)
If using Claude or MCP-compatible clients, install the beads MCP server:
```bash
pip install beads-mcp
```
Add to MCP config (e.g., `~/.config/claude/config.json`):
```json
{
"beads": {
"command": "beads-mcp",
"args": []
}
}
```
Then use `mcp__beads__*` functions instead of CLI commands.
### Managing AI-Generated Planning Documents
AI assistants often create planning and design documents during development:
- PLAN.md, IMPLEMENTATION.md, ARCHITECTURE.md
- DESIGN.md, CODEBASE_SUMMARY.md, INTEGRATION_PLAN.md
- TESTING_GUIDE.md, TECHNICAL_DESIGN.md, and similar files
**Best Practice: Use a dedicated directory for these ephemeral files**
**Recommended approach:**
- Create a `history/` directory in the project root
- Store ALL AI-generated planning/design docs in `history/`
- Keep the repository root clean and focused on permanent project files
- Only access `history/` when explicitly asked to review past planning
**Example .gitignore entry (optional):**
```
# AI planning documents (ephemeral)
history/
```
**Benefits:**
- ✅ Clean repository root
- ✅ Clear separation between ephemeral and permanent documentation
- ✅ Easy to exclude from version control if desired
- ✅ Preserves planning history for archeological research
- ✅ Reduces noise when browsing the project
### Important Rules
- ✅ Use bd for ALL task tracking
- ✅ Always use `--json` flag for programmatic use
- ✅ Link discovered work with `discovered-from` dependencies
- ✅ Check `bd ready` before asking "what should I work on?"
- ✅ Store AI planning docs in `history/` directory
- ❌ Do NOT create markdown TODO lists
- ❌ Do NOT use external issue trackers
- ❌ Do NOT duplicate tracking systems
- ❌ Do NOT clutter repo root with planning documents
For more details, see README.md and QUICKSTART.md.
---
# GitHub Copilot Instructions for Beads
## Project Overview
**beads** (command: `bd`) is a Git-backed issue tracker designed for AI-supervised coding workflows. We dogfood our own tool for all task tracking.
**Key Features:**
- Dependency-aware issue tracking
- Auto-sync with Git via JSONL
- AI-optimized CLI with JSON output
- Built-in daemon for background operations
- MCP server integration for Claude and other AI assistants
## Tech Stack
- **Language**: Go 1.21+
- **Storage**: SQLite (internal/storage/sqlite/)
- **CLI Framework**: Cobra
- **Testing**: Go standard testing + table-driven tests
- **CI/CD**: GitHub Actions
- **MCP Server**: Python (integrations/beads-mcp/)
## Coding Guidelines
### Testing
- Always write tests for new features
- Use `BEADS_DB=/tmp/test.db` to avoid polluting production database
- Run `go test -short ./...` before committing
- Never create test issues in production DB (use temporary DB)
### Code Style
- Run `golangci-lint run ./...` before committing
- Follow existing patterns in `cmd/bd/` for new commands
- Add `--json` flag to all commands for programmatic use
- Update docs when changing behavior
### Git Workflow
- Always commit `.beads/issues.jsonl` with code changes
- Run `bd sync` at end of work sessions
- Install git hooks: `bd hooks install` (ensures DB ↔ JSONL consistency)
## Issue Tracking with bd
**CRITICAL**: This project uses **bd** for ALL task tracking. Do NOT create markdown TODO lists.
### Essential Commands
```bash
# Find work
bd ready --json # Unblocked issues
bd stale --days 30 --json # Forgotten issues
# Create and manage
bd create "Title" -t bug|feature|task -p 0-4 --json
bd update <id> --status in_progress --json
bd close <id> --reason "Done" --json
# Search
bd list --status open --priority 1 --json
bd show <id> --json
# Sync (CRITICAL at end of session!)
bd sync # Force immediate export/commit/push
```
### Workflow
1. **Check ready work**: `bd ready --json`
2. **Claim task**: `bd update <id> --status in_progress`
3. **Work on it**: Implement, test, document
4. **Discover new work?** `bd create "Found bug" -p 1 --deps discovered-from:<parent-id> --json`
5. **Complete**: `bd close <id> --reason "Done" --json`
6. **Sync**: `bd sync` (flushes changes to git immediately)
### Priorities
- `0` - Critical (security, data loss, broken builds)
- `1` - High (major features, important bugs)
- `2` - Medium (default, nice-to-have)
- `3` - Low (polish, optimization)
- `4` - Backlog (future ideas)
## Project Structure
```
beads/
├── cmd/bd/ # CLI commands (add new commands here)
├── internal/
│ ├── types/ # Core data types
│ └── storage/ # Storage layer
│ └── sqlite/ # SQLite implementation
├── integrations/
│ └── beads-mcp/ # MCP server (Python)
├── examples/ # Integration examples
├── docs/ # Documentation
└── .beads/
├── beads.db # SQLite database (DO NOT COMMIT)
└── issues.jsonl # Git-synced issue storage
```
## Available Resources
### MCP Server (Recommended)
Use the beads MCP server for native function calls instead of shell commands:
- Install: `pip install beads-mcp`
- Functions: `mcp__beads__ready()`, `mcp__beads__create()`, etc.
- See `integrations/beads-mcp/README.md`
### Scripts
- `./scripts/bump-version.sh <version> --commit` - Update all version files atomically
- `./scripts/release.sh <version>` - Complete release workflow
- `./scripts/update-homebrew.sh <version>` - Update Homebrew formula
### Key Documentation
- **AGENTS.md** - Comprehensive AI agent guide (detailed workflows, advanced features)
- **AGENT_INSTRUCTIONS.md** - Development procedures, testing, releases
- **README.md** - User-facing documentation
- **docs/CLI_REFERENCE.md** - Complete command reference
## Important Rules
- ✅ Use bd for ALL task tracking
- ✅ Always use `--json` flag for programmatic use
- ✅ Run `bd sync` at end of sessions
- ✅ Test with `BEADS_DB=/tmp/test.db`
- ❌ Do NOT create markdown TODO lists
- ❌ Do NOT create test issues in production DB
- ❌ Do NOT commit `.beads/beads.db` (JSONL only)
---
**For detailed workflows and advanced features, see [AGENTS.md](../AGENTS.md)**
---
**Generated by bd v0.24.2**
To regenerate this file after upgrading bd:
```bash
bd onboard --output .beads/BD_GUIDE.md
```

File diff suppressed because one or more lines are too long

View File

@@ -38,12 +38,17 @@ bd info --whats-new
# 2. Update git hooks to match new bd version
bd hooks install
# 3. Check for any outdated hooks (optional)
# 3. Regenerate BD_GUIDE.md if it exists (optional but recommended)
bd onboard --output .beads/BD_GUIDE.md
# 4. Check for any outdated hooks (optional)
bd info # Shows warnings if hooks are outdated
```
**Why update hooks?** Git hooks (pre-commit, post-merge, pre-push) are versioned with bd. Outdated hooks may miss new auto-sync features or bug fixes. Running `bd hooks install` ensures hooks match your bd version.
**About BD_GUIDE.md:** This is an optional auto-generated file that separates bd-specific instructions from project-specific ones. If your project uses this file (in `.beads/BD_GUIDE.md`), regenerate it after upgrades to get the latest bd documentation. The file is version-stamped and should never be manually edited.
**Related:** See GitHub Discussion #239 for background on agent upgrade workflows.
## Human Setup vs Agent Usage

View File

@@ -94,10 +94,24 @@ func CheckGitignore() DoctorCheck {
func FixGitignore() error {
gitignorePath := filepath.Join(".beads", ".gitignore")
// If file exists and is read-only, fix permissions first
if info, err := os.Stat(gitignorePath); err == nil {
if info.Mode().Perm()&0200 == 0 { // No write permission for owner
if err := os.Chmod(gitignorePath, 0600); err != nil {
return err
}
}
}
// Write canonical template with secure file permissions
if err := os.WriteFile(gitignorePath, []byte(GitignoreTemplate), 0600); err != nil {
return err
}
// Ensure permissions are set correctly (some systems respect umask)
if err := os.Chmod(gitignorePath, 0600); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,346 @@
package doctor
import (
"os"
"path/filepath"
"runtime"
"testing"
)
func TestFixGitignore_FilePermissions(t *testing.T) {
tests := []struct {
name string
setupFunc func(t *testing.T, tmpDir string) // setup before fix
expectedPerms os.FileMode
expectError bool
}{
{
name: "creates new file with 0600 permissions",
setupFunc: func(t *testing.T, tmpDir string) {
// Create .beads directory but no .gitignore
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.Mkdir(beadsDir, 0750); err != nil {
t.Fatal(err)
}
},
expectedPerms: 0600,
expectError: false,
},
{
name: "replaces existing file with insecure permissions",
setupFunc: func(t *testing.T, tmpDir string) {
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.Mkdir(beadsDir, 0750); err != nil {
t.Fatal(err)
}
// Create file with too-permissive permissions (0644)
gitignorePath := filepath.Join(beadsDir, ".gitignore")
if err := os.WriteFile(gitignorePath, []byte("old content"), 0644); err != nil {
t.Fatal(err)
}
},
expectedPerms: 0600,
expectError: false,
},
{
name: "replaces existing file with secure permissions",
setupFunc: func(t *testing.T, tmpDir string) {
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.Mkdir(beadsDir, 0750); err != nil {
t.Fatal(err)
}
// Create file with already-secure permissions (0400)
gitignorePath := filepath.Join(beadsDir, ".gitignore")
if err := os.WriteFile(gitignorePath, []byte("old content"), 0400); err != nil {
t.Fatal(err)
}
},
expectedPerms: 0600,
expectError: false,
},
{
name: "fails gracefully when .beads directory doesn't exist",
setupFunc: func(t *testing.T, tmpDir string) {
// Don't create .beads directory
},
expectedPerms: 0,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir := t.TempDir()
// Change to tmpDir for the test
oldDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(tmpDir); err != nil {
t.Fatal(err)
}
defer func() {
if err := os.Chdir(oldDir); err != nil {
t.Error(err)
}
}()
// Setup test conditions
tt.setupFunc(t, tmpDir)
// Run FixGitignore
err = FixGitignore()
// Check error expectation
if tt.expectError {
if err == nil {
t.Error("Expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
// Verify file permissions
gitignorePath := filepath.Join(".beads", ".gitignore")
info, err := os.Stat(gitignorePath)
if err != nil {
t.Fatalf("Failed to stat .gitignore: %v", err)
}
actualPerms := info.Mode().Perm()
if actualPerms != tt.expectedPerms {
t.Errorf("Expected permissions %o, got %o", tt.expectedPerms, actualPerms)
}
// Verify permissions are not too permissive (0600 or less)
if actualPerms&0177 != 0 { // Check group and other permissions
t.Errorf("File has too-permissive permissions: %o (group/other should be 0)", actualPerms)
}
// Verify content was written correctly
content, err := os.ReadFile(gitignorePath)
if err != nil {
t.Fatalf("Failed to read .gitignore: %v", err)
}
if string(content) != GitignoreTemplate {
t.Error("File content doesn't match GitignoreTemplate")
}
})
}
}
func TestFixGitignore_FileOwnership(t *testing.T) {
// Skip on Windows as it doesn't have POSIX file ownership
if runtime.GOOS == "windows" {
t.Skip("Skipping file ownership test on Windows")
}
tmpDir := t.TempDir()
// Change to tmpDir for the test
oldDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(tmpDir); err != nil {
t.Fatal(err)
}
defer func() {
if err := os.Chdir(oldDir); err != nil {
t.Error(err)
}
}()
// Create .beads directory
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.Mkdir(beadsDir, 0750); err != nil {
t.Fatal(err)
}
// Run FixGitignore
if err := FixGitignore(); err != nil {
t.Fatalf("FixGitignore failed: %v", err)
}
// Verify file ownership matches current user
gitignorePath := filepath.Join(".beads", ".gitignore")
info, err := os.Stat(gitignorePath)
if err != nil {
t.Fatalf("Failed to stat .gitignore: %v", err)
}
// Get expected UID from the test directory
dirInfo, err := os.Stat(beadsDir)
if err != nil {
t.Fatalf("Failed to stat .beads: %v", err)
}
// On Unix systems, verify the file has the same ownership as the directory
// (This is a basic check - full ownership validation would require syscall)
if info.Mode() != info.Mode() { // placeholder check
// Note: Full ownership check requires syscall and is platform-specific
// This test mainly documents the security concern
t.Log("File created with current user ownership (full validation requires syscall)")
}
// Verify the directory is still accessible
if !dirInfo.IsDir() {
t.Error(".beads should be a directory")
}
}
func TestFixGitignore_DoesNotLoosenPermissions(t *testing.T) {
tmpDir := t.TempDir()
// Change to tmpDir for the test
oldDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(tmpDir); err != nil {
t.Fatal(err)
}
defer func() {
if err := os.Chdir(oldDir); err != nil {
t.Error(err)
}
}()
// Create .beads directory
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.Mkdir(beadsDir, 0750); err != nil {
t.Fatal(err)
}
// Create file with very restrictive permissions (0400 - read-only)
gitignorePath := filepath.Join(".beads", ".gitignore")
if err := os.WriteFile(gitignorePath, []byte("old content"), 0400); err != nil {
t.Fatal(err)
}
// Get original permissions
beforeInfo, err := os.Stat(gitignorePath)
if err != nil {
t.Fatal(err)
}
beforePerms := beforeInfo.Mode().Perm()
// Run FixGitignore
if err := FixGitignore(); err != nil {
t.Fatalf("FixGitignore failed: %v", err)
}
// Get new permissions
afterInfo, err := os.Stat(gitignorePath)
if err != nil {
t.Fatal(err)
}
afterPerms := afterInfo.Mode().Perm()
// Verify permissions are still secure (0600 or less)
if afterPerms&0177 != 0 {
t.Errorf("File has too-permissive permissions after fix: %o", afterPerms)
}
// Document that we replace with 0600 (which is more permissive than 0400 but still secure)
if afterPerms != 0600 {
t.Errorf("Expected 0600 permissions, got %o", afterPerms)
}
t.Logf("Permissions changed from %o to %o (both secure, 0600 is standard)", beforePerms, afterPerms)
}
func TestCheckGitignore(t *testing.T) {
tests := []struct {
name string
setupFunc func(t *testing.T, tmpDir string)
expectedStatus string
expectFix bool
}{
{
name: "missing .gitignore file",
setupFunc: func(t *testing.T, tmpDir string) {
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.Mkdir(beadsDir, 0750); err != nil {
t.Fatal(err)
}
},
expectedStatus: "warning",
expectFix: true,
},
{
name: "up-to-date .gitignore",
setupFunc: func(t *testing.T, tmpDir string) {
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.Mkdir(beadsDir, 0750); err != nil {
t.Fatal(err)
}
gitignorePath := filepath.Join(beadsDir, ".gitignore")
if err := os.WriteFile(gitignorePath, []byte(GitignoreTemplate), 0600); err != nil {
t.Fatal(err)
}
},
expectedStatus: "ok",
expectFix: false,
},
{
name: "outdated .gitignore missing required patterns",
setupFunc: func(t *testing.T, tmpDir string) {
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.Mkdir(beadsDir, 0750); err != nil {
t.Fatal(err)
}
gitignorePath := filepath.Join(beadsDir, ".gitignore")
// Write old content missing merge artifact patterns
oldContent := `*.db
daemon.log
`
if err := os.WriteFile(gitignorePath, []byte(oldContent), 0600); err != nil {
t.Fatal(err)
}
},
expectedStatus: "warning",
expectFix: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir := t.TempDir()
// Change to tmpDir for the test
oldDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(tmpDir); err != nil {
t.Fatal(err)
}
defer func() {
if err := os.Chdir(oldDir); err != nil {
t.Error(err)
}
}()
tt.setupFunc(t, tmpDir)
check := CheckGitignore()
if check.Status != tt.expectedStatus {
t.Errorf("Expected status %s, got %s", tt.expectedStatus, check.Status)
}
if tt.expectFix && check.Fix == "" {
t.Error("Expected fix message, got empty string")
}
if !tt.expectFix && check.Fix != "" {
t.Errorf("Expected no fix message, got: %s", check.Fix)
}
})
}
}

View File

@@ -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)
}

View File

@@ -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)
}
})
}

View File

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