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 <noreply@anthropic.com>
This commit is contained in:
george
2026-01-25 13:32:46 -08:00
committed by Steve Yegge
parent 2f0f0763cc
commit b178d056f6
7 changed files with 2 additions and 546 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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