feat: Add sandbox escape hatches for daemon lock issues (GH #353 Phase 1)

Implements three quick fixes for users stuck in sandboxed environments
(e.g., Codex) where daemon cannot be stopped:

1. **--force flag for bd import**
   - Forces metadata update even when DB is synced with JSONL
   - Fixes stuck state caused by stale daemon cache
   - Shows: "Metadata updated (database already in sync with JSONL)"

2. **--allow-stale global flag**
   - Emergency escape hatch to bypass staleness check
   - Shows warning: "⚠️  Staleness check skipped (--allow-stale)"
   - Allows operations on potentially stale data

3. **Improved error message**
   - Added sandbox-specific guidance to staleness error
   - Suggests --sandbox, --force, and --allow-stale flags
   - Provides clear fix steps for different scenarios

Also fixed:
- Removed unused import in cmd/bd/duplicates_test.go

Follow-up work filed:
- bd-u3t: Phase 2 - Sandbox auto-detection
- bd-e0o: Phase 3 - Daemon robustness enhancements
- bd-9nw: Documentation updates

Fixes #353 (Phase 1)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-11-21 18:53:34 -05:00
parent 2101b2543f
commit b3e76ff1f6
7 changed files with 454 additions and 3 deletions

View File

@@ -2,7 +2,6 @@ package main
import (
"context"
"path/filepath"
"testing"
"github.com/steveyegge/beads/internal/types"

View File

@@ -69,6 +69,7 @@ NOTE: Import requires direct database access and does not work with daemon mode.
dedupeAfter, _ := cmd.Flags().GetBool("dedupe-after")
clearDuplicateExternalRefs, _ := cmd.Flags().GetBool("clear-duplicate-external-refs")
orphanHandling, _ := cmd.Flags().GetString("orphan-handling")
force, _ := cmd.Flags().GetBool("force")
// Open input
in := os.Stdin
@@ -309,7 +310,8 @@ NOTE: Import requires direct database access and does not work with daemon mode.
// Update last_import_hash metadata to enable content-based staleness detection (bd-khnb fix)
// This prevents git operations from resurrecting deleted issues by comparing content instead of mtime
if input != "" {
// When --force is true, ALWAYS update metadata even if no changes were made
if input != "" && (result.Created > 0 || result.Updated > 0 || len(result.IDMapping) > 0 || force) {
if currentHash, err := computeJSONLHash(input); err == nil {
if err := store.SetMetadata(ctx, "last_import_hash", currentHash); err != nil {
// Non-fatal warning: Metadata update failures are intentionally non-fatal to prevent blocking
@@ -358,6 +360,11 @@ NOTE: Import requires direct database access and does not work with daemon mode.
}
fmt.Fprintf(os.Stderr, "\n")
// Print force message if metadata was updated despite no changes
if force && result.Created == 0 && result.Updated == 0 && len(result.IDMapping) == 0 {
fmt.Fprintf(os.Stderr, "Metadata updated (database already in sync with JSONL)\n")
}
// Run duplicate detection if requested
if dedupeAfter {
fmt.Fprintf(os.Stderr, "\n=== Post-Import Duplicate Detection ===\n")
@@ -697,6 +704,7 @@ func init() {
importCmd.Flags().Bool("rename-on-import", false, "Rename imported issues to match database prefix (updates all references)")
importCmd.Flags().Bool("clear-duplicate-external-refs", false, "Clear duplicate external_ref values (keeps first occurrence)")
importCmd.Flags().String("orphan-handling", "", "How to handle missing parent issues: strict/resurrect/skip/allow (default: use config or 'allow')")
importCmd.Flags().Bool("force", false, "Force metadata update even when database is already in sync with JSONL")
importCmd.Flags().BoolVar(&jsonOutput, "json", false, "Output import statistics in JSON format")
rootCmd.AddCommand(importCmd)
}

View File

@@ -89,6 +89,7 @@ var (
noAutoFlush bool
noAutoImport bool
sandboxMode bool
allowStale bool // Use --allow-stale: skip staleness check (emergency escape hatch)
noDb bool // Use --no-db mode: load from JSONL, write back after each command
profileEnabled bool
profileFile *os.File
@@ -109,6 +110,7 @@ func init() {
rootCmd.PersistentFlags().BoolVar(&noAutoFlush, "no-auto-flush", false, "Disable automatic JSONL sync after CRUD operations")
rootCmd.PersistentFlags().BoolVar(&noAutoImport, "no-auto-import", false, "Disable automatic JSONL import when newer than DB")
rootCmd.PersistentFlags().BoolVar(&sandboxMode, "sandbox", false, "Sandbox mode: disables daemon and auto-sync")
rootCmd.PersistentFlags().BoolVar(&allowStale, "allow-stale", false, "Allow operations on potentially stale data (skip staleness check)")
rootCmd.PersistentFlags().BoolVar(&noDb, "no-db", false, "Use no-db mode: load from JSONL, no SQLite")
rootCmd.PersistentFlags().BoolVar(&profileEnabled, "profile", false, "Generate CPU profile for performance analysis")

View File

@@ -18,6 +18,11 @@ import (
// Implements bd-2q6d: All read operations should validate database freshness.
// Implements bd-c4rq: Daemon check moved to call sites to avoid function call overhead.
func ensureDatabaseFresh(ctx context.Context) error {
if allowStale {
fmt.Fprintf(os.Stderr, "⚠️ Staleness check skipped (--allow-stale), data may be out of sync\n")
return nil
}
// Skip check if no storage available (shouldn't happen in practice)
if store == nil {
return nil
@@ -43,7 +48,11 @@ func ensureDatabaseFresh(ctx context.Context) error {
"The JSONL file has been updated (e.g., after 'git pull') but the database\n"+
"hasn't been imported yet. This would cause you to see stale/incomplete data.\n\n"+
"To fix:\n"+
" bd import # Import JSONL updates to database\n\n"+
" bd import -i .beads/beads.jsonl # Import JSONL updates to database\n\n"+
"If in a sandboxed environment (e.g., Codex) where daemon can't be stopped:\n"+
" bd --sandbox ready # Use direct mode (no daemon)\n"+
" bd import --force # Force metadata update\n"+
" bd ready --allow-stale # Skip staleness check (use with caution)\n\n"+
"Or use daemon mode (auto-imports on every operation):\n"+
" bd daemon start\n"+
" bd <command> # Will auto-import before executing",