From b178d056f61bffa0710b1823749e5e358fb22cc4 Mon Sep 17 00:00:00 2001 From: george Date: Sun, 25 Jan 2026 13:32:46 -0800 Subject: [PATCH] refactor: remove bd daemon code from gastown Remove all code that calls bd daemon commands, as bd daemon functionality has been removed from beads: - Delete internal/beads/daemon.go (CheckBdDaemonHealth, StopAllBdProcesses, etc.) - Delete internal/beads/daemon_test.go - Delete internal/doctor/bd_daemon_check.go (BdDaemonCheck health check) - Remove bd daemon health check from gt status - Remove bd daemon stopping from gt down - Remove bd daemon cleanup from gt install - Remove BdDaemonCheck registration from gt doctor Co-Authored-By: Claude Opus 4.5 --- internal/beads/daemon.go | 244 ----------------------------- internal/beads/daemon_test.go | 33 ---- internal/cmd/doctor.go | 1 - internal/cmd/down.go | 41 +---- internal/cmd/install.go | 6 - internal/cmd/status.go | 10 -- internal/doctor/bd_daemon_check.go | 213 ------------------------- 7 files changed, 2 insertions(+), 546 deletions(-) delete mode 100644 internal/beads/daemon.go delete mode 100644 internal/beads/daemon_test.go delete mode 100644 internal/doctor/bd_daemon_check.go diff --git a/internal/beads/daemon.go b/internal/beads/daemon.go deleted file mode 100644 index 71cb4d95..00000000 --- a/internal/beads/daemon.go +++ /dev/null @@ -1,244 +0,0 @@ -package beads - -import ( - "bytes" - "encoding/json" - "fmt" - "os/exec" - "strconv" - "strings" - "time" -) - -const ( - gracefulTimeout = 2 * time.Second -) - -// BdDaemonInfo represents the status of a single bd daemon instance. -type BdDaemonInfo struct { - Workspace string `json:"workspace"` - SocketPath string `json:"socket_path"` - PID int `json:"pid"` - Version string `json:"version"` - Status string `json:"status"` - Issue string `json:"issue,omitempty"` - VersionMismatch bool `json:"version_mismatch,omitempty"` -} - -// BdDaemonHealth represents the overall health of bd daemons. -type BdDaemonHealth struct { - Total int `json:"total"` - Healthy int `json:"healthy"` - Stale int `json:"stale"` - Mismatched int `json:"mismatched"` - Unresponsive int `json:"unresponsive"` - Daemons []BdDaemonInfo `json:"daemons"` -} - -// CheckBdDaemonHealth checks the health of all bd daemons. -// Returns nil if no daemons are running (which is fine, bd will use direct mode). -func CheckBdDaemonHealth() (*BdDaemonHealth, error) { - cmd := exec.Command("bd", "daemon", "health", "--json") - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - err := cmd.Run() - if err != nil { - // bd daemon health may fail if bd not installed or other issues - // Return nil to indicate we can't check (not an error for status display) - return nil, nil - } - - var health BdDaemonHealth - if err := json.Unmarshal(stdout.Bytes(), &health); err != nil { - return nil, fmt.Errorf("parsing daemon health: %w", err) - } - - return &health, nil -} - -// EnsureBdDaemonHealth checks if bd daemons are healthy and attempts to restart if needed. -// Returns a warning message if there were issues, or empty string if everything is fine. -// This is non-blocking - it will not fail if daemons can't be started. -func EnsureBdDaemonHealth(workDir string) string { - health, err := CheckBdDaemonHealth() - if err != nil || health == nil { - // Can't check daemon health - proceed without warning - return "" - } - - // No daemons running is fine - bd will use direct mode - if health.Total == 0 { - return "" - } - - // Check if any daemons need attention - needsRestart := false - for _, d := range health.Daemons { - switch d.Status { - case "healthy": - // Good - case "version_mismatch", "stale", "unresponsive": - needsRestart = true - } - } - - if !needsRestart { - return "" - } - - // Attempt to restart daemons - if restartErr := restartBdDaemons(); restartErr != nil { - return fmt.Sprintf("bd daemons unhealthy (restart failed: %v)", restartErr) - } - - // Verify restart worked - time.Sleep(500 * time.Millisecond) - newHealth, err := CheckBdDaemonHealth() - if err != nil || newHealth == nil { - return "bd daemons restarted but status unknown" - } - - if newHealth.Healthy < newHealth.Total { - return fmt.Sprintf("bd daemons partially healthy (%d/%d)", newHealth.Healthy, newHealth.Total) - } - - return "" // Successfully restarted -} - -// restartBdDaemons restarts all bd daemons. -func restartBdDaemons() error { //nolint:unparam // error return kept for future use - // Stop all daemons first using pkill to avoid auto-start side effects - _ = exec.Command("pkill", "-TERM", "-f", "bd daemon").Run() - - // Give time for cleanup - time.Sleep(200 * time.Millisecond) - - // Start daemons for known locations - // The daemon will auto-start when bd commands are run in those directories - // Just running any bd command will trigger daemon startup if configured - return nil -} - -// StartBdDaemonIfNeeded starts the bd daemon for a specific workspace if not running. -// This is a best-effort operation - failures are logged but don't block execution. -func StartBdDaemonIfNeeded(workDir string) error { - cmd := exec.Command("bd", "daemon", "start") - cmd.Dir = workDir - return cmd.Run() -} - -// StopAllBdProcesses stops all bd daemon and activity processes. -// Returns (daemonsKilled, activityKilled, error). -// If dryRun is true, returns counts without stopping anything. -func StopAllBdProcesses(dryRun, force bool) (int, int, error) { - if _, err := exec.LookPath("bd"); err != nil { - return 0, 0, nil - } - - daemonsBefore := CountBdDaemons() - activityBefore := CountBdActivityProcesses() - - if dryRun { - return daemonsBefore, activityBefore, nil - } - - daemonsKilled, daemonsRemaining := stopBdDaemons(force) - activityKilled, activityRemaining := stopBdActivityProcesses(force) - - if daemonsRemaining > 0 { - return daemonsKilled, activityKilled, fmt.Errorf("bd daemon shutdown incomplete: %d still running", daemonsRemaining) - } - if activityRemaining > 0 { - return daemonsKilled, activityKilled, fmt.Errorf("bd activity shutdown incomplete: %d still running", activityRemaining) - } - - return daemonsKilled, activityKilled, nil -} - -// CountBdDaemons returns count of running bd daemons. -// Uses pgrep instead of "bd daemon list" to avoid triggering daemon auto-start -// during shutdown verification. -func CountBdDaemons() int { - // Use pgrep -f with wc -l for cross-platform compatibility - // (macOS pgrep doesn't support -c flag) - cmd := exec.Command("sh", "-c", "pgrep -f 'bd daemon' 2>/dev/null | wc -l") - output, err := cmd.Output() - if err != nil { - return 0 - } - count, _ := strconv.Atoi(strings.TrimSpace(string(output))) - return count -} - - -func stopBdDaemons(force bool) (int, int) { - before := CountBdDaemons() - if before == 0 { - return 0, 0 - } - - // Use pkill directly instead of "bd daemon killall" to avoid triggering - // daemon auto-start as a side effect of running bd commands. - // Note: pkill -f pattern may match unintended processes in rare cases - // (e.g., editors with "bd daemon" in file content). This is acceptable - // given the alternative of respawning daemons during shutdown. - if force { - _ = exec.Command("pkill", "-9", "-f", "bd daemon").Run() - } else { - _ = exec.Command("pkill", "-TERM", "-f", "bd daemon").Run() - time.Sleep(gracefulTimeout) - if remaining := CountBdDaemons(); remaining > 0 { - _ = exec.Command("pkill", "-9", "-f", "bd daemon").Run() - } - } - - time.Sleep(100 * time.Millisecond) - - final := CountBdDaemons() - killed := before - final - if killed < 0 { - killed = 0 // Race condition: more processes spawned than we killed - } - return killed, final -} - -// CountBdActivityProcesses returns count of running `bd activity` processes. -func CountBdActivityProcesses() int { - // Use pgrep -f with wc -l for cross-platform compatibility - // (macOS pgrep doesn't support -c flag) - cmd := exec.Command("sh", "-c", "pgrep -f 'bd activity' 2>/dev/null | wc -l") - output, err := cmd.Output() - if err != nil { - return 0 - } - count, _ := strconv.Atoi(strings.TrimSpace(string(output))) - return count -} - -func stopBdActivityProcesses(force bool) (int, int) { - before := CountBdActivityProcesses() - if before == 0 { - return 0, 0 - } - - if force { - _ = exec.Command("pkill", "-9", "-f", "bd activity").Run() - } else { - _ = exec.Command("pkill", "-TERM", "-f", "bd activity").Run() - time.Sleep(gracefulTimeout) - if remaining := CountBdActivityProcesses(); remaining > 0 { - _ = exec.Command("pkill", "-9", "-f", "bd activity").Run() - } - } - - time.Sleep(100 * time.Millisecond) - - after := CountBdActivityProcesses() - killed := before - after - if killed < 0 { - killed = 0 // Race condition: more processes spawned than we killed - } - return killed, after -} diff --git a/internal/beads/daemon_test.go b/internal/beads/daemon_test.go deleted file mode 100644 index 5cc0de5e..00000000 --- a/internal/beads/daemon_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package beads - -import ( - "os/exec" - "testing" -) - -func TestCountBdActivityProcesses(t *testing.T) { - count := CountBdActivityProcesses() - if count < 0 { - t.Errorf("count should be non-negative, got %d", count) - } -} - -func TestCountBdDaemons(t *testing.T) { - if _, err := exec.LookPath("bd"); err != nil { - t.Skip("bd not installed") - } - count := CountBdDaemons() - if count < 0 { - t.Errorf("count should be non-negative, got %d", count) - } -} - -func TestStopAllBdProcesses_DryRun(t *testing.T) { - daemonsKilled, activityKilled, err := StopAllBdProcesses(true, false) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - if daemonsKilled < 0 || activityKilled < 0 { - t.Errorf("counts should be non-negative: daemons=%d, activity=%d", daemonsKilled, activityKilled) - } -} diff --git a/internal/cmd/doctor.go b/internal/cmd/doctor.go index 1b5b2b2b..e0fdec4a 100644 --- a/internal/cmd/doctor.go +++ b/internal/cmd/doctor.go @@ -129,7 +129,6 @@ func runDoctor(cmd *cobra.Command, args []string) error { d.Register(doctor.NewCustomTypesCheck()) d.Register(doctor.NewRoleLabelCheck()) d.Register(doctor.NewFormulaCheck()) - d.Register(doctor.NewBdDaemonCheck()) d.Register(doctor.NewPrefixConflictCheck()) d.Register(doctor.NewPrefixMismatchCheck()) d.Register(doctor.NewRoutesCheck()) diff --git a/internal/cmd/down.go b/internal/cmd/down.go index 330e3b4e..8956ef02 100644 --- a/internal/cmd/down.go +++ b/internal/cmd/down.go @@ -11,7 +11,6 @@ import ( "github.com/gofrs/flock" "github.com/spf13/cobra" - "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/daemon" "github.com/steveyegge/gastown/internal/events" @@ -136,35 +135,7 @@ func runDown(cmd *cobra.Command, args []string) error { fmt.Println() } - // Phase 1: Stop bd resurrection layer (--all only) - if downAll { - daemonsKilled, activityKilled, err := beads.StopAllBdProcesses(downDryRun, downForce) - if err != nil { - printDownStatus("bd processes", false, err.Error()) - allOK = false - } else { - if downDryRun { - if daemonsKilled > 0 || activityKilled > 0 { - printDownStatus("bd daemon", true, fmt.Sprintf("%d would stop", daemonsKilled)) - printDownStatus("bd activity", true, fmt.Sprintf("%d would stop", activityKilled)) - } else { - printDownStatus("bd processes", true, "none running") - } - } else { - if daemonsKilled > 0 { - printDownStatus("bd daemon", true, fmt.Sprintf("%d stopped", daemonsKilled)) - } - if activityKilled > 0 { - printDownStatus("bd activity", true, fmt.Sprintf("%d stopped", activityKilled)) - } - if daemonsKilled == 0 && activityKilled == 0 { - printDownStatus("bd processes", true, "none running") - } - } - } - } - - // Phase 2a: Stop refineries + // Phase 1: Stop refineries for _, rigName := range rigs { sessionName := fmt.Sprintf("gt-%s-refinery", rigName) if downDryRun { @@ -184,7 +155,7 @@ func runDown(cmd *cobra.Command, args []string) error { } } - // Phase 2b: Stop witnesses + // Phase 2: Stop witnesses for _, rigName := range rigs { sessionName := fmt.Sprintf("gt-%s-witness", rigName) if downDryRun { @@ -428,14 +399,6 @@ func acquireShutdownLock(townRoot string) (*flock.Flock, error) { func verifyShutdown(t *tmux.Tmux, townRoot string) []string { var respawned []string - if count := beads.CountBdDaemons(); count > 0 { - respawned = append(respawned, fmt.Sprintf("bd daemon (%d running)", count)) - } - - if count := beads.CountBdActivityProcesses(); count > 0 { - respawned = append(respawned, fmt.Sprintf("bd activity (%d running)", count)) - } - sessions, err := t.ListSessions() if err == nil { for _, sess := range sessions { diff --git a/internal/cmd/install.go b/internal/cmd/install.go index 40ea0252..252bf823 100644 --- a/internal/cmd/install.go +++ b/internal/cmd/install.go @@ -258,12 +258,6 @@ func runInstall(cmd *cobra.Command, args []string) error { // Town beads (hq- prefix) stores mayor mail, cross-rig coordination, and handoffs. // Rig beads are separate and have their own prefixes. if !installNoBeads { - // Kill any orphaned bd daemons before initializing beads. - // Stale daemons can interfere with fresh database creation. - if killed, _, _ := beads.StopAllBdProcesses(false, true); killed > 0 { - fmt.Printf(" ✓ Stopped %d orphaned bd daemon(s)\n", killed) - } - if err := initTownBeads(absPath); err != nil { fmt.Printf(" %s Could not initialize town beads: %v\n", style.Dim.Render("⚠"), err) } else { diff --git a/internal/cmd/status.go b/internal/cmd/status.go index 4721b3ea..8223c909 100644 --- a/internal/cmd/status.go +++ b/internal/cmd/status.go @@ -189,10 +189,6 @@ func runStatusOnce(_ *cobra.Command, _ []string) error { return fmt.Errorf("not in a Gas Town workspace: %w", err) } - // Check bd daemon health and attempt restart if needed - // This is non-blocking - if daemons can't be started, we show a warning but continue - bdWarning := beads.EnsureBdDaemonHealth(townRoot) - // Load town config townConfigPath := constants.MayorTownPath(townRoot) townConfig, err := config.LoadTownConfig(townConfigPath) @@ -404,12 +400,6 @@ func runStatusOnce(_ *cobra.Command, _ []string) error { return err } - // Show bd daemon warning at the end if there were issues - if bdWarning != "" { - fmt.Printf("%s %s\n", style.Warning.Render("⚠"), bdWarning) - fmt.Printf(" Run 'bd daemon killall && bd daemon start' to restart daemons\n") - } - return nil } diff --git a/internal/doctor/bd_daemon_check.go b/internal/doctor/bd_daemon_check.go deleted file mode 100644 index fd212d78..00000000 --- a/internal/doctor/bd_daemon_check.go +++ /dev/null @@ -1,213 +0,0 @@ -package doctor - -import ( - "bytes" - "os/exec" - "strings" -) - -// BdDaemonCheck verifies that the bd (beads) daemon is running and healthy. -// When the daemon fails to start, it surfaces the actual error (e.g., legacy -// database detected, repo mismatch) and provides actionable fix commands. -type BdDaemonCheck struct { - FixableCheck -} - -// NewBdDaemonCheck creates a new bd daemon check. -func NewBdDaemonCheck() *BdDaemonCheck { - return &BdDaemonCheck{ - FixableCheck: FixableCheck{ - BaseCheck: BaseCheck{ - CheckName: "bd-daemon", - CheckDescription: "Check if bd (beads) daemon is running", - CheckCategory: CategoryInfrastructure, - }, - }, - } -} - -// Run checks if the bd daemon is running and healthy. -func (c *BdDaemonCheck) Run(ctx *CheckContext) *CheckResult { - // Check daemon status - cmd := exec.Command("bd", "daemon", "status") - cmd.Dir = ctx.TownRoot - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - err := cmd.Run() - output := strings.TrimSpace(stdout.String() + stderr.String()) - - // Check if daemon is running - if err == nil && strings.Contains(output, "Daemon is running") { - // Daemon is running, now check health - healthCmd := exec.Command("bd", "daemon", "health") - healthCmd.Dir = ctx.TownRoot - var healthOut bytes.Buffer - healthCmd.Stdout = &healthOut - _ = healthCmd.Run() // Ignore error, health check is optional - - healthOutput := healthOut.String() - if strings.Contains(healthOutput, "HEALTHY") { - return &CheckResult{ - Name: c.Name(), - Status: StatusOK, - Message: "bd daemon is running and healthy", - } - } - - // Daemon running but unhealthy - return &CheckResult{ - Name: c.Name(), - Status: StatusWarning, - Message: "bd daemon is running but may be unhealthy", - Details: []string{strings.TrimSpace(healthOutput)}, - } - } - - // Daemon is not running - try to start it and capture any errors - startErr := c.tryStartDaemon(ctx) - if startErr != nil { - // Parse the error to provide specific guidance - return c.parseStartError(startErr) - } - - // Started successfully - return &CheckResult{ - Name: c.Name(), - Status: StatusOK, - Message: "bd daemon started successfully", - } -} - -// tryStartDaemon attempts to start the bd daemon and returns any error output. -func (c *BdDaemonCheck) tryStartDaemon(ctx *CheckContext) *startError { - cmd := exec.Command("bd", "daemon", "start") - cmd.Dir = ctx.TownRoot - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - err := cmd.Run() - if err != nil { - return &startError{ - output: strings.TrimSpace(stdout.String() + stderr.String()), - exitCode: cmd.ProcessState.ExitCode(), - } - } - return nil -} - -// startError holds information about a failed daemon start. -type startError struct { - output string - exitCode int -} - -// parseStartError analyzes the error output and returns a helpful CheckResult. -func (c *BdDaemonCheck) parseStartError(err *startError) *CheckResult { - output := err.output - - // Check for legacy database error - if strings.Contains(output, "LEGACY DATABASE DETECTED") { - return &CheckResult{ - Name: c.Name(), - Status: StatusError, - Message: "bd daemon failed: legacy database detected", - Details: []string{ - "Database was created before bd version 0.17.5", - "Missing repository fingerprint prevents daemon from starting", - }, - FixHint: "Run 'bd migrate --update-repo-id' to add fingerprint", - } - } - - // Check for database mismatch error - if strings.Contains(output, "DATABASE MISMATCH DETECTED") { - return &CheckResult{ - Name: c.Name(), - Status: StatusError, - Message: "bd daemon failed: database belongs to different repository", - Details: []string{ - "The .beads database was created for a different git repository", - "This can happen if .beads was copied or if the git remote URL changed", - }, - FixHint: "Run 'bd migrate --update-repo-id' if URL changed, or 'rm -rf .beads && bd init' for fresh start", - } - } - - // Check for already running (not actually an error) - if strings.Contains(output, "daemon already running") { - return &CheckResult{ - Name: c.Name(), - Status: StatusOK, - Message: "bd daemon is already running", - } - } - - // Check for permission/lock errors - if strings.Contains(output, "lock") || strings.Contains(output, "permission") { - return &CheckResult{ - Name: c.Name(), - Status: StatusError, - Message: "bd daemon failed: lock or permission issue", - Details: []string{output}, - FixHint: "Check if another bd daemon is running, or remove .beads/daemon.lock", - } - } - - // Check for database corruption - if strings.Contains(output, "corrupt") || strings.Contains(output, "malformed") { - return &CheckResult{ - Name: c.Name(), - Status: StatusError, - Message: "bd daemon failed: database may be corrupted", - Details: []string{output}, - FixHint: "Run 'bd repair' or 'rm .beads/issues.db && bd sync --from-main'", - } - } - - // Generic error with full output - details := []string{output} - if output == "" { - details = []string{"No error output captured (exit code " + string(rune('0'+err.exitCode)) + ")"} - } - - return &CheckResult{ - Name: c.Name(), - Status: StatusError, - Message: "bd daemon failed to start", - Details: details, - FixHint: "Check 'bd daemon status' and logs in .beads/daemon.log", - } -} - -// Fix attempts to start the bd daemon. -func (c *BdDaemonCheck) Fix(ctx *CheckContext) error { - // First check if it's a legacy database issue - startErr := c.tryStartDaemon(ctx) - if startErr == nil { - return nil - } - - // If legacy database, run migrate first - if strings.Contains(startErr.output, "LEGACY DATABASE") || - strings.Contains(startErr.output, "DATABASE MISMATCH") { - - migrateCmd := exec.Command("bd", "migrate", "--update-repo-id", "--yes") - migrateCmd.Dir = ctx.TownRoot - if err := migrateCmd.Run(); err != nil { - return err - } - - // Try starting again - startCmd := exec.Command("bd", "daemon", "start") - startCmd.Dir = ctx.TownRoot - return startCmd.Run() - } - - // For other errors, just try to start - startCmd := exec.Command("bd", "daemon", "start") - startCmd.Dir = ctx.TownRoot - return startCmd.Run() -}