feat: auto-bypass daemon for wisp operations (bd-ta4r)
Wisp operations (mol wisp *, mol burn, mol squash) and commands operating on ephemeral issue IDs (bd-eph-*, gt-eph-*, eph-*) now auto-bypass the daemon. Since wisps are ephemeral (Ephemeral=true) and never exported to JSONL, the daemon cannot help with them anyway. This eliminates the need to manually add --no-daemon for every wisp operation. Changes: - Add isWispOperation() helper to detect wisp commands and ephemeral IDs - Add FallbackWispOperation constant for daemon status tracking - Insert wisp detection in PersistentPreRunE daemon connection logic - Update error messages in wisp.go, mol_burn.go, mol_squash.go - Add comprehensive unit tests for isWispOperation()
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
"github.com/steveyegge/beads/internal/config"
|
"github.com/steveyegge/beads/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -329,3 +330,128 @@ func TestDaemonAutostart_RestartDaemonForVersionMismatch_Stubbed(t *testing.T) {
|
|||||||
t.Fatalf("expected socket removed")
|
t.Fatalf("expected socket removed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestIsWispOperation tests the wisp operation detection for auto-daemon-bypass (bd-ta4r)
|
||||||
|
func TestIsWispOperation(t *testing.T) {
|
||||||
|
// Helper to create a command with parent hierarchy
|
||||||
|
makeCmd := func(names ...string) *cobra.Command {
|
||||||
|
var current *cobra.Command
|
||||||
|
for i, name := range names {
|
||||||
|
cmd := &cobra.Command{Use: name}
|
||||||
|
if i == 0 {
|
||||||
|
current = cmd
|
||||||
|
} else {
|
||||||
|
current.AddCommand(cmd)
|
||||||
|
current = cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cmdNames []string // hierarchy: root, child, grandchild...
|
||||||
|
args []string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
// Wisp subcommands
|
||||||
|
{
|
||||||
|
name: "mol wisp (direct)",
|
||||||
|
cmdNames: []string{"bd", "mol", "wisp"},
|
||||||
|
args: []string{},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mol wisp create",
|
||||||
|
cmdNames: []string{"bd", "mol", "wisp", "create"},
|
||||||
|
args: []string{"some-proto"},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mol wisp list",
|
||||||
|
cmdNames: []string{"bd", "mol", "wisp", "list"},
|
||||||
|
args: []string{},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mol wisp gc",
|
||||||
|
cmdNames: []string{"bd", "mol", "wisp", "gc"},
|
||||||
|
args: []string{},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
// mol burn and squash (wisp-only operations)
|
||||||
|
{
|
||||||
|
name: "mol burn",
|
||||||
|
cmdNames: []string{"bd", "mol", "burn"},
|
||||||
|
args: []string{"bd-eph-abc"},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mol squash",
|
||||||
|
cmdNames: []string{"bd", "mol", "squash"},
|
||||||
|
args: []string{"bd-eph-abc"},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
// Ephemeral issue IDs in args
|
||||||
|
{
|
||||||
|
name: "close with bd-eph ID",
|
||||||
|
cmdNames: []string{"bd", "close"},
|
||||||
|
args: []string{"bd-eph-abc123"},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "show with gt-eph ID",
|
||||||
|
cmdNames: []string{"bd", "show"},
|
||||||
|
args: []string{"gt-eph-xyz"},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update with eph- prefix",
|
||||||
|
cmdNames: []string{"bd", "update"},
|
||||||
|
args: []string{"eph-test", "--status=closed"},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
// Non-wisp operations (should NOT bypass)
|
||||||
|
{
|
||||||
|
name: "regular show",
|
||||||
|
cmdNames: []string{"bd", "show"},
|
||||||
|
args: []string{"bd-abc123"},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regular close",
|
||||||
|
cmdNames: []string{"bd", "close"},
|
||||||
|
args: []string{"bd-xyz"},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mol pour (persistent)",
|
||||||
|
cmdNames: []string{"bd", "mol", "pour"},
|
||||||
|
args: []string{"some-formula"},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list command",
|
||||||
|
cmdNames: []string{"bd", "list"},
|
||||||
|
args: []string{},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
// Edge cases
|
||||||
|
{
|
||||||
|
name: "flag that looks like eph ID should be ignored",
|
||||||
|
cmdNames: []string{"bd", "show"},
|
||||||
|
args: []string{"--format=bd-eph-style", "bd-regular"},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cmd := makeCmd(tt.cmdNames...)
|
||||||
|
got := isWispOperation(cmd, tt.args)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("isWispOperation() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ const (
|
|||||||
FallbackAutoStartDisabled = "auto_start_disabled"
|
FallbackAutoStartDisabled = "auto_start_disabled"
|
||||||
FallbackAutoStartFailed = "auto_start_failed"
|
FallbackAutoStartFailed = "auto_start_failed"
|
||||||
FallbackDaemonUnsupported = "daemon_unsupported"
|
FallbackDaemonUnsupported = "daemon_unsupported"
|
||||||
|
FallbackWispOperation = "wisp_operation"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -634,10 +635,22 @@ var rootCmd = &cobra.Command{
|
|||||||
noDaemon = true
|
noDaemon = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wisp operations auto-bypass daemon (bd-ta4r)
|
||||||
|
// Wisps are ephemeral (Ephemeral=true) and never exported to JSONL,
|
||||||
|
// so daemon can't help anyway. This reduces friction in wisp workflows.
|
||||||
|
if isWispOperation(cmd, args) {
|
||||||
|
noDaemon = true
|
||||||
|
daemonStatus.FallbackReason = FallbackWispOperation
|
||||||
|
debug.Logf("wisp operation detected, using direct mode")
|
||||||
|
}
|
||||||
|
|
||||||
// Try to connect to daemon first (unless --no-daemon flag is set or worktree safety check fails)
|
// Try to connect to daemon first (unless --no-daemon flag is set or worktree safety check fails)
|
||||||
if noDaemon {
|
if noDaemon {
|
||||||
daemonStatus.FallbackReason = FallbackFlagNoDaemon
|
// Only set FallbackFlagNoDaemon if not already set by auto-bypass logic
|
||||||
debug.Logf("--no-daemon flag set, using direct mode")
|
if daemonStatus.FallbackReason == FallbackNone {
|
||||||
|
daemonStatus.FallbackReason = FallbackFlagNoDaemon
|
||||||
|
debug.Logf("--no-daemon flag set, using direct mode")
|
||||||
|
}
|
||||||
} else if shouldDisableDaemonForWorktree() {
|
} else if shouldDisableDaemonForWorktree() {
|
||||||
// In a git worktree without sync-branch configured - daemon is unsafe
|
// In a git worktree without sync-branch configured - daemon is unsafe
|
||||||
// because all worktrees share the same .beads directory and the daemon
|
// because all worktrees share the same .beads directory and the daemon
|
||||||
@@ -1091,3 +1104,45 @@ func handleFreshCloneError(err error, beadsDir string) bool {
|
|||||||
fmt.Fprintf(os.Stderr, "For more information: bd init --help\n")
|
fmt.Fprintf(os.Stderr, "For more information: bd init --help\n")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isWispOperation returns true if the command operates on ephemeral wisps.
|
||||||
|
// Wisp operations auto-bypass the daemon because wisps are local-only
|
||||||
|
// (Ephemeral=true issues are never exported to JSONL).
|
||||||
|
// Detects:
|
||||||
|
// - mol wisp subcommands (create, list, gc, or direct proto invocation)
|
||||||
|
// - mol burn (only operates on wisps)
|
||||||
|
// - mol squash (condenses wisps to digests)
|
||||||
|
// - Commands with ephemeral issue IDs in args (bd-*-eph-*, eph-*)
|
||||||
|
func isWispOperation(cmd *cobra.Command, args []string) bool {
|
||||||
|
cmdName := cmd.Name()
|
||||||
|
|
||||||
|
// Check command hierarchy for wisp subcommands
|
||||||
|
// bd mol wisp → parent is "mol", cmd is "wisp"
|
||||||
|
// bd mol wisp create → parent is "wisp", cmd is "create"
|
||||||
|
if cmd.Parent() != nil {
|
||||||
|
parentName := cmd.Parent().Name()
|
||||||
|
// Direct wisp command or subcommands under wisp
|
||||||
|
if parentName == "wisp" || cmdName == "wisp" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// mol burn and mol squash are wisp-only operations
|
||||||
|
if parentName == "mol" && (cmdName == "burn" || cmdName == "squash") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for ephemeral issue IDs in arguments
|
||||||
|
// Ephemeral IDs have "eph" segment: bd-eph-xxx, gt-eph-xxx, eph-xxx
|
||||||
|
for _, arg := range args {
|
||||||
|
// Skip flags
|
||||||
|
if strings.HasPrefix(arg, "-") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Check for ephemeral prefix patterns
|
||||||
|
if strings.Contains(arg, "-eph-") || strings.HasPrefix(arg, "eph-") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,14 +50,10 @@ func runMolBurn(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
ctx := rootCtx
|
ctx := rootCtx
|
||||||
|
|
||||||
// mol burn requires direct store access
|
// mol burn requires direct store access (daemon auto-bypassed for wisp ops)
|
||||||
if store == nil {
|
if store == nil {
|
||||||
if daemonClient != nil {
|
fmt.Fprintf(os.Stderr, "Error: no database connection\n")
|
||||||
fmt.Fprintf(os.Stderr, "Error: mol burn requires direct database access\n")
|
fmt.Fprintf(os.Stderr, "Hint: run 'bd init' or 'bd import' to initialize the database\n")
|
||||||
fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon mol burn %s ...\n", args[0])
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error: no database connection\n")
|
|
||||||
}
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,14 +66,10 @@ func runMolSquash(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
ctx := rootCtx
|
ctx := rootCtx
|
||||||
|
|
||||||
// mol squash requires direct store access
|
// mol squash requires direct store access (daemon auto-bypassed for wisp ops)
|
||||||
if store == nil {
|
if store == nil {
|
||||||
if daemonClient != nil {
|
fmt.Fprintf(os.Stderr, "Error: no database connection\n")
|
||||||
fmt.Fprintf(os.Stderr, "Error: mol squash requires direct database access\n")
|
fmt.Fprintf(os.Stderr, "Hint: run 'bd init' or 'bd import' to initialize the database\n")
|
||||||
fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon mol squash %s ...\n", args[0])
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error: no database connection\n")
|
|
||||||
}
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -139,14 +139,10 @@ func runWispCreate(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
ctx := rootCtx
|
ctx := rootCtx
|
||||||
|
|
||||||
// Wisp create requires direct store access
|
// Wisp create requires direct store access (daemon auto-bypassed for wisp ops)
|
||||||
if store == nil {
|
if store == nil {
|
||||||
if daemonClient != nil {
|
fmt.Fprintf(os.Stderr, "Error: no database connection\n")
|
||||||
fmt.Fprintf(os.Stderr, "Error: wisp create requires direct database access\n")
|
fmt.Fprintf(os.Stderr, "Hint: run 'bd init' or 'bd import' to initialize the database\n")
|
||||||
fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon mol wisp %s ...\n", args[0])
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error: no database connection\n")
|
|
||||||
}
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,14 +560,10 @@ func runWispGC(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wisp gc requires direct store access for deletion
|
// Wisp gc requires direct store access for deletion (daemon auto-bypassed for wisp ops)
|
||||||
if store == nil {
|
if store == nil {
|
||||||
if daemonClient != nil {
|
fmt.Fprintf(os.Stderr, "Error: no database connection\n")
|
||||||
fmt.Fprintf(os.Stderr, "Error: wisp gc requires direct database access\n")
|
fmt.Fprintf(os.Stderr, "Hint: run 'bd init' or 'bd import' to initialize the database\n")
|
||||||
fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon mol wisp gc\n")
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error: no database connection\n")
|
|
||||||
}
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user