refactor: Split init.go from 1928 to 705 lines (bd-xtf5)
Extract init.go into multiple focused files: - init_git_hooks.go: Git hooks installation and merge driver setup (~480 lines) - init_stealth.go: Stealth mode and fork/exclude configuration (~310 lines) - init_agent.go: AGENTS.md and Claude settings setup (~170 lines) - init_templates.go: config.yaml and README.md templates (~180 lines) This reduces init.go from 1928 lines to 705 lines, meeting the <800 line target. Each extracted file handles a cohesive set of functionality. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
182
cmd/bd/init_agent.go
Normal file
182
cmd/bd/init_agent.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/steveyegge/beads/internal/ui"
|
||||
)
|
||||
|
||||
// landingThePlaneSection is the "landing the plane" instructions for AI agents
|
||||
// This gets appended to AGENTS.md and @AGENTS.md during bd init
|
||||
const landingThePlaneSection = `
|
||||
## Landing the Plane (Session Completion)
|
||||
|
||||
**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until ` + "`git push`" + ` succeeds.
|
||||
|
||||
**MANDATORY WORKFLOW:**
|
||||
|
||||
1. **File issues for remaining work** - Create issues for anything that needs follow-up
|
||||
2. **Run quality gates** (if code changed) - Tests, linters, builds
|
||||
3. **Update issue status** - Close finished work, update in-progress items
|
||||
4. **PUSH TO REMOTE** - This is MANDATORY:
|
||||
` + "```bash" + `
|
||||
git pull --rebase
|
||||
bd sync
|
||||
git push
|
||||
git status # MUST show "up to date with origin"
|
||||
` + "```" + `
|
||||
5. **Clean up** - Clear stashes, prune remote branches
|
||||
6. **Verify** - All changes committed AND pushed
|
||||
7. **Hand off** - Provide context for next session
|
||||
|
||||
**CRITICAL RULES:**
|
||||
- Work is NOT complete until ` + "`git push`" + ` succeeds
|
||||
- NEVER stop before pushing - that leaves work stranded locally
|
||||
- NEVER say "ready to push when you are" - YOU must push
|
||||
- If push fails, resolve and retry until it succeeds
|
||||
`
|
||||
|
||||
// addLandingThePlaneInstructions adds "landing the plane" instructions to AGENTS.md
|
||||
func addLandingThePlaneInstructions(verbose bool) {
|
||||
// File to update (AGENTS.md is the standard comprehensive documentation file)
|
||||
agentFile := "AGENTS.md"
|
||||
|
||||
if err := updateAgentFile(agentFile, verbose); err != nil {
|
||||
// Non-fatal - continue with other files
|
||||
if verbose {
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to update %s: %v\n", agentFile, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateAgentFile creates or updates an agent instructions file with landing the plane section
|
||||
func updateAgentFile(filename string, verbose bool) error {
|
||||
// Check if file exists
|
||||
//nolint:gosec // G304: filename comes from hardcoded list in addLandingThePlaneInstructions
|
||||
content, err := os.ReadFile(filename)
|
||||
if os.IsNotExist(err) {
|
||||
// File doesn't exist - create it with basic structure
|
||||
newContent := fmt.Sprintf(`# Agent Instructions
|
||||
|
||||
This project uses **bd** (beads) for issue tracking. Run `+"`bd onboard`"+` to get started.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
`+"```bash"+`
|
||||
bd ready # Find available work
|
||||
bd show <id> # View issue details
|
||||
bd update <id> --status in_progress # Claim work
|
||||
bd close <id> # Complete work
|
||||
bd sync # Sync with git
|
||||
`+"```"+`
|
||||
%s
|
||||
`, landingThePlaneSection)
|
||||
|
||||
// #nosec G306 - markdown needs to be readable
|
||||
if err := os.WriteFile(filename, []byte(newContent), 0644); err != nil {
|
||||
return fmt.Errorf("failed to create %s: %w", filename, err)
|
||||
}
|
||||
if verbose {
|
||||
fmt.Printf(" %s Created %s with landing-the-plane instructions\n", ui.RenderPass("✓"), filename)
|
||||
}
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to read %s: %w", filename, err)
|
||||
}
|
||||
|
||||
// File exists - check if it already has landing the plane section
|
||||
if strings.Contains(string(content), "Landing the Plane") {
|
||||
if verbose {
|
||||
fmt.Printf(" %s already has landing-the-plane instructions\n", filename)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Append the landing the plane section
|
||||
newContent := string(content)
|
||||
if !strings.HasSuffix(newContent, "\n") {
|
||||
newContent += "\n"
|
||||
}
|
||||
newContent += landingThePlaneSection
|
||||
|
||||
// #nosec G306 - markdown needs to be readable
|
||||
if err := os.WriteFile(filename, []byte(newContent), 0644); err != nil {
|
||||
return fmt.Errorf("failed to update %s: %w", filename, err)
|
||||
}
|
||||
if verbose {
|
||||
fmt.Printf(" %s Added landing-the-plane instructions to %s\n", ui.RenderPass("✓"), filename)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupClaudeSettings creates or updates .claude/settings.local.json with onboard instruction
|
||||
func setupClaudeSettings(verbose bool) error {
|
||||
claudeDir := ".claude"
|
||||
settingsPath := filepath.Join(claudeDir, "settings.local.json")
|
||||
|
||||
// Create .claude directory if it doesn't exist
|
||||
if err := os.MkdirAll(claudeDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create .claude directory: %w", err)
|
||||
}
|
||||
|
||||
// Check if settings.local.json already exists
|
||||
var existingSettings map[string]interface{}
|
||||
// #nosec G304 - user config path
|
||||
if content, err := os.ReadFile(settingsPath); err == nil {
|
||||
if err := json.Unmarshal(content, &existingSettings); err != nil {
|
||||
// Don't silently overwrite - the user has a file with invalid JSON
|
||||
// that likely contains important settings they don't want to lose
|
||||
return fmt.Errorf("existing %s contains invalid JSON: %w\nPlease fix the JSON syntax manually before running bd init", settingsPath, err)
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
// File exists but couldn't be read (permissions issue, etc.)
|
||||
return fmt.Errorf("failed to read existing %s: %w", settingsPath, err)
|
||||
} else {
|
||||
// File doesn't exist - create new empty settings
|
||||
existingSettings = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Add or update the prompt with onboard instruction
|
||||
onboardPrompt := "Before starting any work, run 'bd onboard' to understand the current project state and available issues."
|
||||
|
||||
// Check if prompt already contains onboard instruction
|
||||
if promptValue, exists := existingSettings["prompt"]; exists {
|
||||
if promptStr, ok := promptValue.(string); ok {
|
||||
if strings.Contains(promptStr, "bd onboard") {
|
||||
if verbose {
|
||||
fmt.Printf("Claude settings already configured with bd onboard instruction\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Update existing prompt to include onboard instruction
|
||||
existingSettings["prompt"] = promptStr + "\n\n" + onboardPrompt
|
||||
} else {
|
||||
// Existing prompt is not a string, replace it
|
||||
existingSettings["prompt"] = onboardPrompt
|
||||
}
|
||||
} else {
|
||||
// Add new prompt with onboard instruction
|
||||
existingSettings["prompt"] = onboardPrompt
|
||||
}
|
||||
|
||||
// Write updated settings
|
||||
updatedContent, err := json.MarshalIndent(existingSettings, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal settings JSON: %w", err)
|
||||
}
|
||||
|
||||
// #nosec G306 - config file needs 0644
|
||||
if err := os.WriteFile(settingsPath, updatedContent, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write claude settings: %w", err)
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Printf("Configured Claude settings with bd onboard instruction\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user