refactor: rename ephemeral → wisp throughout (bd-ldb0)

Standardize terminology for ephemeral molecule storage:
- .beads-ephemeral/ → .beads-wisps/
- --ephemeral flag → --wisp flag
- All Ephemeral* functions → Wisp*

Wisps are the "steam" in Gas Town's engine metaphor - ephemeral
molecules that evaporate after squash.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-22 00:32:18 -08:00
parent 22eee81f42
commit a49b924794
3 changed files with 107 additions and 106 deletions

View File

@@ -32,9 +32,9 @@ Bond types:
parallel - B runs alongside A parallel - B runs alongside A
conditional - B runs only if A fails conditional - B runs only if A fails
Ephemeral storage (wisps): Wisp storage (ephemeral molecules):
Use --ephemeral to create molecules in .beads-ephemeral/ instead of .beads/. Use --wisp to create molecules in .beads-wisps/ instead of .beads/.
Ephemeral molecules (wisps) are local-only, gitignored, and not synced. Wisps are local-only, gitignored, and not synced - the "steam" of Gas Town.
Use bd mol squash to convert a wisp to a digest in permanent storage. Use bd mol squash to convert a wisp to a digest in permanent storage.
Use bd mol burn to delete a wisp without creating a digest. Use bd mol burn to delete a wisp without creating a digest.
@@ -43,7 +43,7 @@ Examples:
bd mol bond mol-feature mol-deploy --type parallel # Run in parallel bd mol bond mol-feature mol-deploy --type parallel # Run in parallel
bd mol bond mol-feature bd-abc123 # Attach proto to molecule bd mol bond mol-feature bd-abc123 # Attach proto to molecule
bd mol bond bd-abc123 bd-def456 # Join two molecules bd mol bond bd-abc123 bd-def456 # Join two molecules
bd mol bond mol-patrol --ephemeral # Create wisp for patrol cycle`, bd mol bond mol-patrol --wisp # Create wisp for patrol cycle`,
Args: cobra.ExactArgs(2), Args: cobra.ExactArgs(2),
Run: runMolBond, Run: runMolBond,
} }
@@ -78,22 +78,22 @@ func runMolBond(cmd *cobra.Command, args []string) {
customTitle, _ := cmd.Flags().GetString("as") customTitle, _ := cmd.Flags().GetString("as")
dryRun, _ := cmd.Flags().GetBool("dry-run") dryRun, _ := cmd.Flags().GetBool("dry-run")
varFlags, _ := cmd.Flags().GetStringSlice("var") varFlags, _ := cmd.Flags().GetStringSlice("var")
ephemeral, _ := cmd.Flags().GetBool("ephemeral") wisp, _ := cmd.Flags().GetBool("wisp")
// Determine which store to use for spawning // Determine which store to use for spawning
targetStore := store targetStore := store
if ephemeral { if wisp {
// Open ephemeral storage for wisp creation // Open wisp storage for ephemeral molecule creation
ephStore, err := beads.NewEphemeralStorage(ctx) wispStore, err := beads.NewWispStorage(ctx)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to open ephemeral storage: %v\n", err) fmt.Fprintf(os.Stderr, "Error: failed to open wisp storage: %v\n", err)
os.Exit(1) os.Exit(1)
} }
defer ephStore.Close() defer wispStore.Close()
targetStore = ephStore targetStore = wispStore
// Ensure ephemeral directory is gitignored // Ensure wisp directory is gitignored
if err := beads.EnsureEphemeralGitignore(); err != nil { if err := beads.EnsureWispGitignore(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: could not update .gitignore: %v\n", err) fmt.Fprintf(os.Stderr, "Warning: could not update .gitignore: %v\n", err)
} }
} }
@@ -148,16 +148,16 @@ func runMolBond(cmd *cobra.Command, args []string) {
fmt.Printf(" A: %s (%s)\n", issueA.Title, operandType(aIsProto)) fmt.Printf(" A: %s (%s)\n", issueA.Title, operandType(aIsProto))
fmt.Printf(" B: %s (%s)\n", issueB.Title, operandType(bIsProto)) fmt.Printf(" B: %s (%s)\n", issueB.Title, operandType(bIsProto))
fmt.Printf(" Bond type: %s\n", bondType) fmt.Printf(" Bond type: %s\n", bondType)
if ephemeral { if wisp {
fmt.Printf(" Storage: ephemeral (.beads-ephemeral/)\n") fmt.Printf(" Storage: wisp (.beads-wisps/)\n")
} }
if aIsProto && bIsProto { if aIsProto && bIsProto {
fmt.Printf(" Result: compound proto\n") fmt.Printf(" Result: compound proto\n")
if customTitle != "" { if customTitle != "" {
fmt.Printf(" Custom title: %s\n", customTitle) fmt.Printf(" Custom title: %s\n", customTitle)
} }
if ephemeral { if wisp {
fmt.Printf(" Note: --ephemeral ignored for proto+proto (templates stay in permanent storage)\n") fmt.Printf(" Note: --wisp ignored for proto+proto (templates stay in permanent storage)\n")
} }
} else if aIsProto || bIsProto { } else if aIsProto || bIsProto {
fmt.Printf(" Result: spawn proto, attach to molecule\n") fmt.Printf(" Result: spawn proto, attach to molecule\n")
@@ -187,8 +187,8 @@ func runMolBond(cmd *cobra.Command, args []string) {
os.Exit(1) os.Exit(1)
} }
// Schedule auto-flush (only for non-ephemeral, ephemeral doesn't sync) // Schedule auto-flush (only for non-wisp, wisps don't sync)
if !ephemeral { if !wisp {
markDirtyAndScheduleFlush() markDirtyAndScheduleFlush()
} }
@@ -202,8 +202,8 @@ func runMolBond(cmd *cobra.Command, args []string) {
if result.Spawned > 0 { if result.Spawned > 0 {
fmt.Printf(" Spawned: %d issues\n", result.Spawned) fmt.Printf(" Spawned: %d issues\n", result.Spawned)
} }
if ephemeral { if wisp {
fmt.Printf(" Storage: ephemeral (wisp)\n") fmt.Printf(" Storage: wisp (.beads-wisps/)\n")
} }
} }
@@ -418,7 +418,7 @@ func init() {
molBondCmd.Flags().String("as", "", "Custom title for compound proto (proto+proto only)") molBondCmd.Flags().String("as", "", "Custom title for compound proto (proto+proto only)")
molBondCmd.Flags().Bool("dry-run", false, "Preview what would be created") molBondCmd.Flags().Bool("dry-run", false, "Preview what would be created")
molBondCmd.Flags().StringSlice("var", []string{}, "Variable substitution for spawned protos (key=value)") molBondCmd.Flags().StringSlice("var", []string{}, "Variable substitution for spawned protos (key=value)")
molBondCmd.Flags().Bool("ephemeral", false, "Create molecule in ephemeral storage (wisp)") molBondCmd.Flags().Bool("wisp", false, "Create molecule in wisp storage (.beads-wisps/)")
molCmd.AddCommand(molBondCmd) molCmd.AddCommand(molBondCmd)
} }

View File

@@ -622,49 +622,50 @@ func FindAllDatabases() []DatabaseInfo {
return databases return databases
} }
// EphemeralDirName is the default name for the ephemeral storage directory. // WispDirName is the default name for the wisp storage directory.
// This directory is a sibling to .beads/ and should be gitignored. // This directory is a sibling to .beads/ and should be gitignored.
const EphemeralDirName = ".beads-ephemeral" // Wisps are ephemeral molecules - the "steam" in Gas Town's engine metaphor.
const WispDirName = ".beads-wisps"
// FindEphemeralDir locates or determines the ephemeral storage directory. // FindWispDir locates or determines the wisp storage directory.
// The ephemeral directory is a sibling to the .beads directory. // The wisp directory is a sibling to the .beads directory.
// //
// Returns the path to the ephemeral directory (which may not exist yet). // Returns the path to the wisp directory (which may not exist yet).
// Returns empty string if no .beads directory can be found. // Returns empty string if no .beads directory can be found.
func FindEphemeralDir() string { func FindWispDir() string {
beadsDir := FindBeadsDir() beadsDir := FindBeadsDir()
if beadsDir == "" { if beadsDir == "" {
return "" return ""
} }
// Ephemeral dir is a sibling to .beads // Wisp dir is a sibling to .beads
// e.g., /project/.beads -> /project/.beads-ephemeral // e.g., /project/.beads -> /project/.beads-wisps
projectRoot := filepath.Dir(beadsDir) projectRoot := filepath.Dir(beadsDir)
return filepath.Join(projectRoot, EphemeralDirName) return filepath.Join(projectRoot, WispDirName)
} }
// FindEphemeralDatabasePath returns the path to the ephemeral database file. // FindWispDatabasePath returns the path to the wisp database file.
// Creates the ephemeral directory if it doesn't exist. // Creates the wisp directory if it doesn't exist.
// Returns empty string if no .beads directory can be found. // Returns empty string if no .beads directory can be found.
func FindEphemeralDatabasePath() (string, error) { func FindWispDatabasePath() (string, error) {
ephemeralDir := FindEphemeralDir() wispDir := FindWispDir()
if ephemeralDir == "" { if wispDir == "" {
return "", fmt.Errorf("no .beads directory found") return "", fmt.Errorf("no .beads directory found")
} }
// Create ephemeral directory if it doesn't exist // Create wisp directory if it doesn't exist
if err := os.MkdirAll(ephemeralDir, 0755); err != nil { if err := os.MkdirAll(wispDir, 0755); err != nil {
return "", fmt.Errorf("creating ephemeral directory: %w", err) return "", fmt.Errorf("creating wisp directory: %w", err)
} }
return filepath.Join(ephemeralDir, CanonicalDatabaseName), nil return filepath.Join(wispDir, CanonicalDatabaseName), nil
} }
// NewEphemeralStorage opens the ephemeral database for wisp storage. // NewWispStorage opens the wisp database for ephemeral molecule storage.
// Creates the database and directory if they don't exist. // Creates the database and directory if they don't exist.
// The ephemeral database uses the same schema as the main database. // The wisp database uses the same schema as the main database.
func NewEphemeralStorage(ctx context.Context) (Storage, error) { func NewWispStorage(ctx context.Context) (Storage, error) {
dbPath, err := FindEphemeralDatabasePath() dbPath, err := FindWispDatabasePath()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -672,9 +673,9 @@ func NewEphemeralStorage(ctx context.Context) (Storage, error) {
return sqlite.New(ctx, dbPath) return sqlite.New(ctx, dbPath)
} }
// EnsureEphemeralGitignore ensures the ephemeral directory is gitignored. // EnsureWispGitignore ensures the wisp directory is gitignored.
// This should be called after creating the ephemeral directory. // This should be called after creating the wisp directory.
func EnsureEphemeralGitignore() error { func EnsureWispGitignore() error {
beadsDir := FindBeadsDir() beadsDir := FindBeadsDir()
if beadsDir == "" { if beadsDir == "" {
return fmt.Errorf("no .beads directory found") return fmt.Errorf("no .beads directory found")
@@ -683,14 +684,14 @@ func EnsureEphemeralGitignore() error {
projectRoot := filepath.Dir(beadsDir) projectRoot := filepath.Dir(beadsDir)
gitignorePath := filepath.Join(projectRoot, ".gitignore") gitignorePath := filepath.Join(projectRoot, ".gitignore")
// Check if .gitignore exists and already contains the ephemeral dir // Check if .gitignore exists and already contains the wisp dir
content, err := os.ReadFile(gitignorePath) content, err := os.ReadFile(gitignorePath)
if err == nil { if err == nil {
// File exists, check if already gitignored // File exists, check if already gitignored
lines := strings.Split(string(content), "\n") lines := strings.Split(string(content), "\n")
for _, line := range lines { for _, line := range lines {
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
if line == EphemeralDirName || line == EphemeralDirName+"/" { if line == WispDirName || line == WispDirName+"/" {
return nil // Already gitignored return nil // Already gitignored
} }
} }
@@ -710,20 +711,20 @@ func EnsureEphemeralGitignore() error {
} }
} }
// Add the ephemeral directory // Add the wisp directory
if _, err := f.WriteString(EphemeralDirName + "/\n"); err != nil { if _, err := f.WriteString(WispDirName + "/\n"); err != nil {
return fmt.Errorf("writing to .gitignore: %w", err) return fmt.Errorf("writing to .gitignore: %w", err)
} }
return nil return nil
} }
// IsEphemeralDatabase checks if a database path is an ephemeral database. // IsWispDatabase checks if a database path is a wisp database.
// Returns true if the database is in a .beads-ephemeral directory. // Returns true if the database is in a .beads-wisps directory.
func IsEphemeralDatabase(dbPath string) bool { func IsWispDatabase(dbPath string) bool {
if dbPath == "" { if dbPath == "" {
return false return false
} }
dir := filepath.Dir(dbPath) dir := filepath.Dir(dbPath)
return filepath.Base(dir) == EphemeralDirName return filepath.Base(dir) == WispDirName
} }

View File

@@ -1255,8 +1255,8 @@ func TestFindDatabasePath_WorktreeNoLocalDB(t *testing.T) {
} }
} }
// TestFindEphemeralDir tests that FindEphemeralDir returns the correct path // TestFindWispDir tests that FindWispDir returns the correct path
func TestFindEphemeralDir(t *testing.T) { func TestFindWispDir(t *testing.T) {
// Save original state // Save original state
originalEnv := os.Getenv("BEADS_DIR") originalEnv := os.Getenv("BEADS_DIR")
defer func() { defer func() {
@@ -1268,7 +1268,7 @@ func TestFindEphemeralDir(t *testing.T) {
}() }()
// Create temporary directory with .beads // Create temporary directory with .beads
tmpDir, err := os.MkdirTemp("", "beads-ephemeral-test-*") tmpDir, err := os.MkdirTemp("", "beads-wisp-test-*")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -1286,22 +1286,22 @@ func TestFindEphemeralDir(t *testing.T) {
// Set BEADS_DIR // Set BEADS_DIR
os.Setenv("BEADS_DIR", beadsDir) os.Setenv("BEADS_DIR", beadsDir)
// FindEphemeralDir should return sibling directory // FindWispDir should return sibling directory
result := FindEphemeralDir() result := FindWispDir()
expected := filepath.Join(tmpDir, EphemeralDirName) expected := filepath.Join(tmpDir, WispDirName)
// Resolve symlinks for comparison // Resolve symlinks for comparison
resultResolved, _ := filepath.EvalSymlinks(result) resultResolved, _ := filepath.EvalSymlinks(result)
expectedResolved, _ := filepath.EvalSymlinks(expected) expectedResolved, _ := filepath.EvalSymlinks(expected)
if resultResolved != expectedResolved { if resultResolved != expectedResolved {
t.Errorf("FindEphemeralDir() = %q, want %q", result, expected) t.Errorf("FindWispDir() = %q, want %q", result, expected)
} }
} }
// TestFindEphemeralDir_NoBeadsDir tests that FindEphemeralDir returns empty string // TestFindWispDir_NoBeadsDir tests that FindWispDir returns empty string
// when no .beads directory exists // when no .beads directory exists
func TestFindEphemeralDir_NoBeadsDir(t *testing.T) { func TestFindWispDir_NoBeadsDir(t *testing.T) {
// Save original state // Save original state
originalEnv := os.Getenv("BEADS_DIR") originalEnv := os.Getenv("BEADS_DIR")
defer func() { defer func() {
@@ -1314,7 +1314,7 @@ func TestFindEphemeralDir_NoBeadsDir(t *testing.T) {
os.Unsetenv("BEADS_DIR") os.Unsetenv("BEADS_DIR")
// Create temporary directory without .beads // Create temporary directory without .beads
tmpDir, err := os.MkdirTemp("", "beads-no-ephemeral-test-*") tmpDir, err := os.MkdirTemp("", "beads-no-wisp-test-*")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -1322,16 +1322,16 @@ func TestFindEphemeralDir_NoBeadsDir(t *testing.T) {
t.Chdir(tmpDir) t.Chdir(tmpDir)
// FindEphemeralDir should return empty string // FindWispDir should return empty string
result := FindEphemeralDir() result := FindWispDir()
if result != "" { if result != "" {
t.Errorf("FindEphemeralDir() = %q, want empty string", result) t.Errorf("FindWispDir() = %q, want empty string", result)
} }
} }
// TestFindEphemeralDatabasePath tests that FindEphemeralDatabasePath creates // TestFindWispDatabasePath tests that FindWispDatabasePath creates
// the ephemeral directory and returns the correct database path // the wisp directory and returns the correct database path
func TestFindEphemeralDatabasePath(t *testing.T) { func TestFindWispDatabasePath(t *testing.T) {
// Save original state // Save original state
originalEnv := os.Getenv("BEADS_DIR") originalEnv := os.Getenv("BEADS_DIR")
defer func() { defer func() {
@@ -1343,7 +1343,7 @@ func TestFindEphemeralDatabasePath(t *testing.T) {
}() }()
// Create temporary directory with .beads // Create temporary directory with .beads
tmpDir, err := os.MkdirTemp("", "beads-ephdb-test-*") tmpDir, err := os.MkdirTemp("", "beads-wispdb-test-*")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -1359,32 +1359,32 @@ func TestFindEphemeralDatabasePath(t *testing.T) {
os.Setenv("BEADS_DIR", beadsDir) os.Setenv("BEADS_DIR", beadsDir)
// FindEphemeralDatabasePath should create directory and return path // FindWispDatabasePath should create directory and return path
result, err := FindEphemeralDatabasePath() result, err := FindWispDatabasePath()
if err != nil { if err != nil {
t.Fatalf("FindEphemeralDatabasePath() error = %v", err) t.Fatalf("FindWispDatabasePath() error = %v", err)
} }
expected := filepath.Join(tmpDir, EphemeralDirName, CanonicalDatabaseName) expected := filepath.Join(tmpDir, WispDirName, CanonicalDatabaseName)
// Resolve symlinks for comparison // Resolve symlinks for comparison
resultResolved, _ := filepath.EvalSymlinks(result) resultResolved, _ := filepath.EvalSymlinks(result)
expectedResolved, _ := filepath.EvalSymlinks(expected) expectedResolved, _ := filepath.EvalSymlinks(expected)
if resultResolved != expectedResolved { if resultResolved != expectedResolved {
t.Errorf("FindEphemeralDatabasePath() = %q, want %q", result, expected) t.Errorf("FindWispDatabasePath() = %q, want %q", result, expected)
} }
// Verify the directory was created // Verify the directory was created
ephemeralDir := filepath.Join(tmpDir, EphemeralDirName) wispDir := filepath.Join(tmpDir, WispDirName)
if _, err := os.Stat(ephemeralDir); os.IsNotExist(err) { if _, err := os.Stat(wispDir); os.IsNotExist(err) {
t.Errorf("Ephemeral directory was not created: %q", ephemeralDir) t.Errorf("Wisp directory was not created: %q", wispDir)
} }
} }
// TestIsEphemeralDatabase tests that IsEphemeralDatabase correctly identifies // TestIsWispDatabase tests that IsWispDatabase correctly identifies
// ephemeral database paths // wisp database paths
func TestIsEphemeralDatabase(t *testing.T) { func TestIsWispDatabase(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
dbPath string dbPath string
@@ -1401,35 +1401,35 @@ func TestIsEphemeralDatabase(t *testing.T) {
expected: false, expected: false,
}, },
{ {
name: "ephemeral database", name: "wisp database",
dbPath: "/project/.beads-ephemeral/beads.db", dbPath: "/project/.beads-wisps/beads.db",
expected: true, expected: true,
}, },
{ {
name: "nested ephemeral", name: "nested wisp",
dbPath: "/some/deep/path/.beads-ephemeral/beads.db", dbPath: "/some/deep/path/.beads-wisps/beads.db",
expected: true, expected: true,
}, },
{ {
name: "similar but not ephemeral", name: "similar but not wisp",
dbPath: "/project/.beads-ephemeral-backup/beads.db", dbPath: "/project/.beads-wisps-backup/beads.db",
expected: false, expected: false,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result := IsEphemeralDatabase(tt.dbPath) result := IsWispDatabase(tt.dbPath)
if result != tt.expected { if result != tt.expected {
t.Errorf("IsEphemeralDatabase(%q) = %v, want %v", tt.dbPath, result, tt.expected) t.Errorf("IsWispDatabase(%q) = %v, want %v", tt.dbPath, result, tt.expected)
} }
}) })
} }
} }
// TestEnsureEphemeralGitignore tests that EnsureEphemeralGitignore correctly // TestEnsureWispGitignore tests that EnsureWispGitignore correctly
// adds the ephemeral directory to .gitignore // adds the wisp directory to .gitignore
func TestEnsureEphemeralGitignore(t *testing.T) { func TestEnsureWispGitignore(t *testing.T) {
// Save original state // Save original state
originalEnv := os.Getenv("BEADS_DIR") originalEnv := os.Getenv("BEADS_DIR")
defer func() { defer func() {
@@ -1452,12 +1452,12 @@ func TestEnsureEphemeralGitignore(t *testing.T) {
}, },
{ {
name: "already gitignored", name: "already gitignored",
existingContent: ".beads-ephemeral/\n", existingContent: ".beads-wisps/\n",
expectAppend: false, expectAppend: false,
}, },
{ {
name: "already gitignored without slash", name: "already gitignored without slash",
existingContent: ".beads-ephemeral\n", existingContent: ".beads-wisps\n",
expectAppend: false, expectAppend: false,
}, },
{ {
@@ -1499,9 +1499,9 @@ func TestEnsureEphemeralGitignore(t *testing.T) {
} }
} }
// Call EnsureEphemeralGitignore // Call EnsureWispGitignore
if err := EnsureEphemeralGitignore(); err != nil { if err := EnsureWispGitignore(); err != nil {
t.Fatalf("EnsureEphemeralGitignore() error = %v", err) t.Fatalf("EnsureWispGitignore() error = %v", err)
} }
// Read result // Read result
@@ -1510,30 +1510,30 @@ func TestEnsureEphemeralGitignore(t *testing.T) {
t.Fatalf("Failed to read .gitignore: %v", err) t.Fatalf("Failed to read .gitignore: %v", err)
} }
// Check if ephemeral dir is in gitignore // Check if wisp dir is in gitignore
hasEntry := false hasEntry := false
lines := strings.Split(string(content), "\n") lines := strings.Split(string(content), "\n")
for _, line := range lines { for _, line := range lines {
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
if line == EphemeralDirName || line == EphemeralDirName+"/" { if line == WispDirName || line == WispDirName+"/" {
hasEntry = true hasEntry = true
break break
} }
} }
if !hasEntry { if !hasEntry {
t.Errorf("EnsureEphemeralGitignore() did not add %s to .gitignore", EphemeralDirName) t.Errorf("EnsureWispGitignore() did not add %s to .gitignore", WispDirName)
} }
// Verify idempotent: calling again should not duplicate // Verify idempotent: calling again should not duplicate
if err := EnsureEphemeralGitignore(); err != nil { if err := EnsureWispGitignore(); err != nil {
t.Fatalf("EnsureEphemeralGitignore() second call error = %v", err) t.Fatalf("EnsureWispGitignore() second call error = %v", err)
} }
content2, _ := os.ReadFile(gitignorePath) content2, _ := os.ReadFile(gitignorePath)
count := strings.Count(string(content2), EphemeralDirName) count := strings.Count(string(content2), WispDirName)
if count > 1 { if count > 1 {
t.Errorf("EnsureEphemeralGitignore() added duplicate entry (count=%d)", count) t.Errorf("EnsureWispGitignore() added duplicate entry (count=%d)", count)
} }
}) })
} }