From bb4549abddf9053ff689df8523342cd847683fab Mon Sep 17 00:00:00 2001 From: beads/crew/emma Date: Wed, 21 Jan 2026 18:05:53 -0800 Subject: [PATCH] fix(daemon): allow read-only daemon commands with Dolt backend The daemon guard was blocking ALL daemon commands when using Dolt backend, including read-only commands like `status`, `stop`, `logs`. Changes: - Rename guard to `guardDaemonStartForDolt` (more accurate) - Remove `PersistentPreRunE` from `daemonCmd` and `daemonsCmd` - Add `PreRunE` guard only to `daemonStartCmd` and `daemonsRestartCmd` - Update test to use new function name and test start command Now: - `bd daemon status` works with Dolt backend - `bd daemon start` blocked unless `--federation` flag - `bd daemon start --federation` works (starts dolt sql-server) Fixes: bd-n7o47 Co-Authored-By: Claude Opus 4.5 --- cmd/bd/daemon.go | 1 - cmd/bd/daemon_guard.go | 10 +++++++--- cmd/bd/daemon_start.go | 1 + cmd/bd/daemons.go | 4 ++-- cmd/bd/dolt_singleprocess_test.go | 12 +++++++----- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/cmd/bd/daemon.go b/cmd/bd/daemon.go index ad48031b..c1cb820d 100644 --- a/cmd/bd/daemon.go +++ b/cmd/bd/daemon.go @@ -48,7 +48,6 @@ Common operations: bd daemon killall Stop all running daemons Run 'bd daemon --help' to see all subcommands.`, - PersistentPreRunE: guardDaemonUnsupportedForDolt, Run: func(cmd *cobra.Command, args []string) { start, _ := cmd.Flags().GetBool("start") stop, _ := cmd.Flags().GetBool("stop") diff --git a/cmd/bd/daemon_guard.go b/cmd/bd/daemon_guard.go index 5abf4798..717f5891 100644 --- a/cmd/bd/daemon_guard.go +++ b/cmd/bd/daemon_guard.go @@ -19,8 +19,8 @@ func singleProcessBackendHelp(backend string) string { return fmt.Sprintf("daemon mode is not supported with the %q backend (single-process only). To use daemon mode, initialize with %q (e.g. `bd init --backend sqlite`). Otherwise run commands in direct mode (default for dolt)", b, configfile.BackendSQLite) } -// guardDaemonUnsupportedForDolt blocks all daemon-related commands when the current -// workspace backend is Dolt. +// guardDaemonStartForDolt blocks daemon start/restart commands when the current +// workspace backend is Dolt, unless --federation is specified. // // Rationale: embedded Dolt is effectively single-writer at the OS-process level. The // daemon architecture relies on multiple processes (CLI + daemon + helper spawns), @@ -28,8 +28,12 @@ func singleProcessBackendHelp(backend string) string { // // Exception: --federation flag enables dolt sql-server mode which is multi-writer. // +// Note: This guard should only be attached to commands that START a daemon process +// (start, restart). Read-only commands (status, stop, logs, health, list) are allowed +// even with Dolt backend. +// // We still allow help output so users can discover the command surface. -func guardDaemonUnsupportedForDolt(cmd *cobra.Command, _ []string) error { +func guardDaemonStartForDolt(cmd *cobra.Command, _ []string) error { // Allow `--help` for any daemon subcommand. if helpFlag := cmd.Flags().Lookup("help"); helpFlag != nil { if help, _ := cmd.Flags().GetBool("help"); help { diff --git a/cmd/bd/daemon_start.go b/cmd/bd/daemon_start.go index 888e212f..12f96729 100644 --- a/cmd/bd/daemon_start.go +++ b/cmd/bd/daemon_start.go @@ -35,6 +35,7 @@ Examples: bd daemon start --foreground # Run in foreground (for systemd/supervisord) bd daemon start --local # Local-only mode (no git sync) bd daemon start --federation # Enable federation mode (dolt sql-server)`, + PreRunE: guardDaemonStartForDolt, Run: func(cmd *cobra.Command, args []string) { interval, _ := cmd.Flags().GetDuration("interval") autoCommit, _ := cmd.Flags().GetBool("auto-commit") diff --git a/cmd/bd/daemons.go b/cmd/bd/daemons.go index 81e51a8f..14ea6e79 100644 --- a/cmd/bd/daemons.go +++ b/cmd/bd/daemons.go @@ -79,7 +79,6 @@ Subcommands: logs - View daemon logs killall - Stop all running daemons restart - Restart a specific daemon (not yet implemented)`, - PersistentPreRunE: guardDaemonUnsupportedForDolt, } var daemonsListCmd = &cobra.Command{ Use: "list", @@ -225,7 +224,8 @@ var daemonsRestartCmd = &cobra.Command{ Short: "Restart a specific bd daemon", Long: `Restart a specific bd daemon by workspace path or PID. Stops the daemon gracefully, then starts a new one.`, - Args: cobra.ExactArgs(1), + Args: cobra.ExactArgs(1), + PreRunE: guardDaemonStartForDolt, Run: func(cmd *cobra.Command, args []string) { target := args[0] searchRoots, _ := cmd.Flags().GetStringSlice("search") diff --git a/cmd/bd/dolt_singleprocess_test.go b/cmd/bd/dolt_singleprocess_test.go index d611f554..3691f212 100644 --- a/cmd/bd/dolt_singleprocess_test.go +++ b/cmd/bd/dolt_singleprocess_test.go @@ -69,7 +69,7 @@ func TestDoltSingleProcess_TryAutoStartDoesNotCreateStartlock(t *testing.T) { } } -func TestDoltSingleProcess_DaemonGuardBlocksCommands(t *testing.T) { +func TestDoltSingleProcess_DaemonGuardBlocksStartCommand(t *testing.T) { oldDBPath := dbPath t.Cleanup(func() { dbPath = oldDBPath }) dbPath = "" @@ -78,12 +78,14 @@ func TestDoltSingleProcess_DaemonGuardBlocksCommands(t *testing.T) { beadsDir, _ := writeDoltWorkspace(t, ws) t.Setenv("BEADS_DIR", beadsDir) - // Ensure help flag exists (cobra adds it during execution; for unit testing we add it explicitly). - cmd := daemonCmd + // Use daemonStartCmd which has the guard attached. + // Ensure help and federation flags exist (cobra adds them during execution). + cmd := daemonStartCmd cmd.Flags().Bool("help", false, "help") - err := guardDaemonUnsupportedForDolt(cmd, nil) + // Note: federation flag is already registered in init() + err := guardDaemonStartForDolt(cmd, nil) if err == nil { - t.Fatalf("expected daemon guard error for dolt backend") + t.Fatalf("expected daemon guard error for dolt backend without --federation") } if !strings.Contains(err.Error(), "single-process") { t.Fatalf("expected error to mention single-process, got: %v", err)