feat: Rename 'wisp' to 'ephemeral' in beads API (bd-o18s)

BREAKING CHANGE: API field and CLI command renamed

- types.Issue.Wisp → types.Issue.Ephemeral
- JSON field: "wisp" → "ephemeral"
- CLI: bd wisp → bd ephemeral
- Flags: --wisp → --ephemeral
- ID prefix: wisp → eph

The SQLite column already uses 'ephemeral' so no schema migration needed.

🤖 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-26 21:07:37 -08:00
parent 89be2a9d7f
commit ea8ae11002
34 changed files with 277 additions and 276 deletions

View File

@@ -671,7 +671,7 @@ func flushToJSONLWithState(state flushState) {
issues := make([]*types.Issue, 0, len(issueMap)) issues := make([]*types.Issue, 0, len(issueMap))
wispsSkipped := 0 wispsSkipped := 0
for _, issue := range issueMap { for _, issue := range issueMap {
if issue.Wisp { if issue.Ephemeral {
wispsSkipped++ wispsSkipped++
continue continue
} }

View File

@@ -15,7 +15,7 @@ type CleanupEmptyResponse struct {
DeletedCount int `json:"deleted_count"` DeletedCount int `json:"deleted_count"`
Message string `json:"message"` Message string `json:"message"`
Filter string `json:"filter,omitempty"` Filter string `json:"filter,omitempty"`
Wisp bool `json:"wisp,omitempty"` Ephemeral bool `json:"ephemeral,omitempty"`
} }
// Hard delete mode: bypass tombstone TTL safety, use --older-than days directly // Hard delete mode: bypass tombstone TTL safety, use --older-than days directly
@@ -56,7 +56,7 @@ Delete issues closed more than 30 days ago:
bd cleanup --older-than 30 --force bd cleanup --older-than 30 --force
Delete only closed wisps (transient molecules): Delete only closed wisps (transient molecules):
bd cleanup --wisp --force bd cleanup --ephemeral --force
Preview what would be deleted/pruned: Preview what would be deleted/pruned:
bd cleanup --dry-run bd cleanup --dry-run
@@ -80,7 +80,7 @@ SEE ALSO:
cascade, _ := cmd.Flags().GetBool("cascade") cascade, _ := cmd.Flags().GetBool("cascade")
olderThanDays, _ := cmd.Flags().GetInt("older-than") olderThanDays, _ := cmd.Flags().GetInt("older-than")
hardDelete, _ := cmd.Flags().GetBool("hard") hardDelete, _ := cmd.Flags().GetBool("hard")
wispOnly, _ := cmd.Flags().GetBool("wisp") wispOnly, _ := cmd.Flags().GetBool("ephemeral")
// Calculate custom TTL for --hard mode // Calculate custom TTL for --hard mode
// When --hard is set, use --older-than days as the tombstone TTL cutoff // When --hard is set, use --older-than days as the tombstone TTL cutoff
@@ -129,7 +129,7 @@ SEE ALSO:
// Add wisp filter if specified (bd-kwro.9) // Add wisp filter if specified (bd-kwro.9)
if wispOnly { if wispOnly {
wispTrue := true wispTrue := true
filter.Wisp = &wispTrue filter.Ephemeral = &wispTrue
} }
// Get all closed issues matching filter // Get all closed issues matching filter
@@ -165,7 +165,7 @@ SEE ALSO:
result.Filter = fmt.Sprintf("older than %d days", olderThanDays) result.Filter = fmt.Sprintf("older than %d days", olderThanDays)
} }
if wispOnly { if wispOnly {
result.Wisp = true result.Ephemeral = true
} }
outputJSON(result) outputJSON(result)
} else { } else {
@@ -270,6 +270,6 @@ func init() {
cleanupCmd.Flags().Bool("cascade", false, "Recursively delete all dependent issues") cleanupCmd.Flags().Bool("cascade", false, "Recursively delete all dependent issues")
cleanupCmd.Flags().Int("older-than", 0, "Only delete issues closed more than N days ago (0 = all closed issues)") cleanupCmd.Flags().Int("older-than", 0, "Only delete issues closed more than N days ago (0 = all closed issues)")
cleanupCmd.Flags().Bool("hard", false, "Bypass tombstone TTL safety; use --older-than days as cutoff") cleanupCmd.Flags().Bool("hard", false, "Bypass tombstone TTL safety; use --older-than days as cutoff")
cleanupCmd.Flags().Bool("wisp", false, "Only delete closed wisps (transient molecules)") cleanupCmd.Flags().Bool("ephemeral", false, "Only delete closed wisps (transient molecules)")
rootCmd.AddCommand(cleanupCmd) rootCmd.AddCommand(cleanupCmd)
} }

View File

@@ -107,7 +107,7 @@ var createCmd = &cobra.Command{
waitsForGate, _ := cmd.Flags().GetString("waits-for-gate") waitsForGate, _ := cmd.Flags().GetString("waits-for-gate")
forceCreate, _ := cmd.Flags().GetBool("force") forceCreate, _ := cmd.Flags().GetBool("force")
repoOverride, _ := cmd.Flags().GetString("repo") repoOverride, _ := cmd.Flags().GetString("repo")
wisp, _ := cmd.Flags().GetBool("wisp") wisp, _ := cmd.Flags().GetBool("ephemeral")
// Get estimate if provided // Get estimate if provided
var estimatedMinutes *int var estimatedMinutes *int
@@ -222,7 +222,7 @@ var createCmd = &cobra.Command{
Dependencies: deps, Dependencies: deps,
WaitsFor: waitsFor, WaitsFor: waitsFor,
WaitsForGate: waitsForGate, WaitsForGate: waitsForGate,
Wisp: wisp, Ephemeral: wisp,
CreatedBy: getActorWithGit(), CreatedBy: getActorWithGit(),
} }
@@ -268,7 +268,7 @@ var createCmd = &cobra.Command{
Assignee: assignee, Assignee: assignee,
ExternalRef: externalRefPtr, ExternalRef: externalRefPtr,
EstimatedMinutes: estimatedMinutes, EstimatedMinutes: estimatedMinutes,
Wisp: wisp, Ephemeral: wisp,
CreatedBy: getActorWithGit(), // GH#748: track who created the issue CreatedBy: getActorWithGit(), // GH#748: track who created the issue
} }
@@ -448,7 +448,7 @@ func init() {
createCmd.Flags().Bool("force", false, "Force creation even if prefix doesn't match database prefix") createCmd.Flags().Bool("force", false, "Force creation even if prefix doesn't match database prefix")
createCmd.Flags().String("repo", "", "Target repository for issue (overrides auto-routing)") createCmd.Flags().String("repo", "", "Target repository for issue (overrides auto-routing)")
createCmd.Flags().IntP("estimate", "e", 0, "Time estimate in minutes (e.g., 60 for 1 hour)") createCmd.Flags().IntP("estimate", "e", 0, "Time estimate in minutes (e.g., 60 for 1 hour)")
createCmd.Flags().Bool("wisp", false, "Create as wisp (ephemeral, not exported to JSONL)") createCmd.Flags().Bool("ephemeral", false, "Create as ephemeral (ephemeral, not exported to JSONL)")
// Note: --json flag is defined as a persistent flag in main.go, not here // Note: --json flag is defined as a persistent flag in main.go, not here
rootCmd.AddCommand(createCmd) rootCmd.AddCommand(createCmd)
} }

View File

@@ -18,37 +18,37 @@ import (
// Wisp commands - manage ephemeral molecules // Wisp commands - manage ephemeral molecules
// //
// Wisps are ephemeral issues with Wisp=true in the main database. // Ephemeral issues are ephemeral issues with Wisp=true in the main database.
// They're used for patrol cycles and operational loops that shouldn't // They're used for patrol cycles and operational loops that shouldn't
// be exported to JSONL (and thus not synced via git). // be exported to JSONL (and thus not synced via git).
// //
// Commands: // Commands:
// bd wisp list - List all wisps in current context // bd ephemeral list - List all ephemeral issues in current context
// bd wisp gc - Garbage collect orphaned wisps // bd ephemeral gc - Garbage collect orphaned ephemeral issues
var wispCmd = &cobra.Command{ var ephemeralCmd = &cobra.Command{
Use: "wisp", Use: "ephemeral",
Short: "Manage ephemeral molecules (wisps)", Short: "Manage ephemeral molecules",
Long: `Manage wisps - ephemeral molecules for operational workflows. Long: `Manage ephemeral issues - ephemeral molecules for operational workflows.
Wisps are issues with Wisp=true in the main database. They're stored Ephemeral issues are issues with Wisp=true in the main database. They're stored
locally but NOT exported to JSONL (and thus not synced via git). locally but NOT exported to JSONL (and thus not synced via git).
They're used for patrol cycles, operational loops, and other workflows They're used for patrol cycles, operational loops, and other workflows
that shouldn't accumulate in the shared issue database. that shouldn't accumulate in the shared issue database.
The wisp lifecycle: The wisp lifecycle:
1. Create: bd wisp create <proto> or bd create --wisp 1. Create: bd ephemeral create <proto> or bd create --ephemeral
2. Execute: Normal bd operations work on wisps 2. Execute: Normal bd operations work on ephemeral issues
3. Squash: bd mol squash <id> (clears Wisp flag, promotes to persistent) 3. Squash: bd mol squash <id> (clears Wisp flag, promotes to persistent)
4. Or burn: bd mol burn <id> (deletes wisp without creating digest) 4. Or burn: bd mol burn <id> (deletes wisp without creating digest)
Commands: Commands:
list List all wisps in current context list List all ephemeral issues in current context
gc Garbage collect orphaned wisps`, gc Garbage collect orphaned ephemeral issues`,
} }
// WispListItem represents a wisp in list output // EphemeralListItem represents a wisp in list output
type WispListItem struct { type EphemeralListItem struct {
ID string `json:"id"` ID string `json:"id"`
Title string `json:"title"` Title string `json:"title"`
Status string `json:"status"` Status string `json:"status"`
@@ -58,9 +58,9 @@ type WispListItem struct {
Old bool `json:"old,omitempty"` // Not updated in 24+ hours Old bool `json:"old,omitempty"` // Not updated in 24+ hours
} }
// WispListResult is the JSON output for wisp list // EphemeralListResult is the JSON output for wisp list
type WispListResult struct { type EphemeralListResult struct {
Wisps []WispListItem `json:"wisps"` Wisps []EphemeralListItem `json:"ephemeral_items"`
Count int `json:"count"` Count int `json:"count"`
OldCount int `json:"old_count,omitempty"` OldCount int `json:"old_count,omitempty"`
} }
@@ -68,8 +68,8 @@ type WispListResult struct {
// OldThreshold is how old a wisp must be to be flagged as old (time-based, for ephemeral cleanup) // OldThreshold is how old a wisp must be to be flagged as old (time-based, for ephemeral cleanup)
const OldThreshold = 24 * time.Hour const OldThreshold = 24 * time.Hour
// wispCreateCmd instantiates a proto as an ephemeral wisp // ephemeralCreateCmd instantiates a proto as an ephemeral wisp
var wispCreateCmd = &cobra.Command{ var ephemeralCreateCmd = &cobra.Command{
Use: "create <proto-id>", Use: "create <proto-id>",
Short: "Instantiate a proto as an ephemeral wisp (solid -> vapor)", Short: "Instantiate a proto as an ephemeral wisp (solid -> vapor)",
Long: `Create a wisp from a proto - sublimation from solid to vapor. Long: `Create a wisp from a proto - sublimation from solid to vapor.
@@ -91,14 +91,14 @@ The wisp will:
- Either evaporate (burn) or condense to digest (squash) - Either evaporate (burn) or condense to digest (squash)
Examples: Examples:
bd wisp create mol-patrol # Ephemeral patrol cycle bd ephemeral create mol-patrol # Ephemeral patrol cycle
bd wisp create mol-health-check # One-time health check bd ephemeral create mol-health-check # One-time health check
bd wisp create mol-diagnostics --var target=db # Diagnostic run`, bd ephemeral create mol-diagnostics --var target=db # Diagnostic run`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
Run: runWispCreate, Run: runEphemeralCreate,
} }
func runWispCreate(cmd *cobra.Command, args []string) { func runEphemeralCreate(cmd *cobra.Command, args []string) {
CheckReadonly("wisp create") CheckReadonly("wisp create")
ctx := rootCtx ctx := rootCtx
@@ -215,7 +215,7 @@ func runWispCreate(cmd *cobra.Command, args []string) {
if dryRun { if dryRun {
fmt.Printf("\nDry run: would create wisp with %d issues from proto %s\n\n", len(subgraph.Issues), protoID) fmt.Printf("\nDry run: would create wisp with %d issues from proto %s\n\n", len(subgraph.Issues), protoID)
fmt.Printf("Storage: main database (wisp=true, not exported to JSONL)\n\n") fmt.Printf("Storage: main database (ephemeral=true, not exported to JSONL)\n\n")
for _, issue := range subgraph.Issues { for _, issue := range subgraph.Issues {
newTitle := substituteVariables(issue.Title, vars) newTitle := substituteVariables(issue.Title, vars)
fmt.Printf(" - %s (from %s)\n", newTitle, issue.ID) fmt.Printf(" - %s (from %s)\n", newTitle, issue.ID)
@@ -223,22 +223,22 @@ func runWispCreate(cmd *cobra.Command, args []string) {
return return
} }
// Spawn as wisp in main database (ephemeral=true sets Wisp flag, skips JSONL export) // Spawn as ephemeral in main database (Ephemeral=true, skips JSONL export)
// bd-hobo: Use "wisp" prefix for distinct visual recognition // bd-hobo: Use "eph" prefix for distinct visual recognition
result, err := spawnMolecule(ctx, store, subgraph, vars, "", actor, true, "wisp") result, err := spawnMolecule(ctx, store, subgraph, vars, "", actor, true, "eph")
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Error creating wisp: %v\n", err) fmt.Fprintf(os.Stderr, "Error creating ephemeral: %v\n", err)
os.Exit(1) os.Exit(1)
} }
// Wisps are in main db but don't trigger JSONL export (Wisp flag excludes them) // Ephemeral issues are in main db but don't trigger JSONL export (Wisp flag excludes them)
if jsonOutput { if jsonOutput {
type wispCreateResult struct { type ephemeralCreateResult struct {
*InstantiateResult *InstantiateResult
Phase string `json:"phase"` Phase string `json:"phase"`
} }
outputJSON(wispCreateResult{result, "vapor"}) outputJSON(ephemeralCreateResult{result, "vapor"})
return return
} }
@@ -283,12 +283,12 @@ func resolvePartialIDDirect(ctx context.Context, partial string) (string, error)
return "", fmt.Errorf("not found: %s", partial) return "", fmt.Errorf("not found: %s", partial)
} }
var wispListCmd = &cobra.Command{ var ephemeralListCmd = &cobra.Command{
Use: "list", Use: "list",
Short: "List all wisps in current context", Short: "List all ephemeral issues in current context",
Long: `List all ephemeral molecules (wisps) in the current context. Long: `List all ephemeral molecules (ephemeral issues) in the current context.
Wisps are issues with Wisp=true in the main database. They are stored Ephemeral issues are issues with Wisp=true in the main database. They are stored
locally but not exported to JSONL (and thus not synced via git). locally but not exported to JSONL (and thus not synced via git).
The list shows: The list shows:
@@ -298,18 +298,18 @@ The list shows:
- Started: When the wisp was created - Started: When the wisp was created
- Updated: Last modification time - Updated: Last modification time
Old wisp detection: Old ephemeral issue detection:
- Old wisps haven't been updated in 24+ hours - Old ephemeral issues haven't been updated in 24+ hours
- Use 'bd wisp gc' to clean up old/abandoned wisps - Use 'bd ephemeral gc' to clean up old/abandoned ephemeral issues
Examples: Examples:
bd wisp list # List all wisps bd ephemeral list # List all ephemeral issues
bd wisp list --json # JSON output for programmatic use bd ephemeral list --json # JSON output for programmatic use
bd wisp list --all # Include closed wisps`, bd ephemeral list --all # Include closed ephemeral issues`,
Run: runWispList, Run: runEphemeralList,
} }
func runWispList(cmd *cobra.Command, args []string) { func runEphemeralList(cmd *cobra.Command, args []string) {
ctx := rootCtx ctx := rootCtx
showAll, _ := cmd.Flags().GetBool("all") showAll, _ := cmd.Flags().GetBool("all")
@@ -317,8 +317,8 @@ func runWispList(cmd *cobra.Command, args []string) {
// Check for database connection // Check for database connection
if store == nil && daemonClient == nil { if store == nil && daemonClient == nil {
if jsonOutput { if jsonOutput {
outputJSON(WispListResult{ outputJSON(EphemeralListResult{
Wisps: []WispListItem{}, Wisps: []EphemeralListItem{},
Count: 0, Count: 0,
}) })
} else { } else {
@@ -327,15 +327,15 @@ func runWispList(cmd *cobra.Command, args []string) {
return return
} }
// Query wisps from main database using Wisp filter // Query ephemeral issues from main database using Wisp filter
wispFlag := true ephemeralFlag := true
var issues []*types.Issue var issues []*types.Issue
var err error var err error
if daemonClient != nil { if daemonClient != nil {
// Use daemon RPC // Use daemon RPC
resp, rpcErr := daemonClient.List(&rpc.ListArgs{ resp, rpcErr := daemonClient.List(&rpc.ListArgs{
Wisp: &wispFlag, Ephemeral: &ephemeralFlag,
}) })
if rpcErr != nil { if rpcErr != nil {
err = rpcErr err = rpcErr
@@ -347,12 +347,12 @@ func runWispList(cmd *cobra.Command, args []string) {
} else { } else {
// Direct database access // Direct database access
filter := types.IssueFilter{ filter := types.IssueFilter{
Wisp: &wispFlag, Ephemeral: &ephemeralFlag,
} }
issues, err = store.SearchIssues(ctx, "", filter) issues, err = store.SearchIssues(ctx, "", filter)
} }
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Error listing wisps: %v\n", err) fmt.Fprintf(os.Stderr, "Error listing ephemeral issues: %v\n", err)
os.Exit(1) os.Exit(1)
} }
@@ -367,13 +367,13 @@ func runWispList(cmd *cobra.Command, args []string) {
issues = filtered issues = filtered
} }
// Convert to list items and detect old wisps // Convert to list items and detect old ephemeral issues
now := time.Now() now := time.Now()
items := make([]WispListItem, 0, len(issues)) items := make([]EphemeralListItem, 0, len(issues))
oldCount := 0 oldCount := 0
for _, issue := range issues { for _, issue := range issues {
item := WispListItem{ item := EphemeralListItem{
ID: issue.ID, ID: issue.ID,
Title: issue.Title, Title: issue.Title,
Status: string(issue.Status), Status: string(issue.Status),
@@ -392,11 +392,11 @@ func runWispList(cmd *cobra.Command, args []string) {
} }
// Sort by updated_at descending (most recent first) // Sort by updated_at descending (most recent first)
slices.SortFunc(items, func(a, b WispListItem) int { slices.SortFunc(items, func(a, b EphemeralListItem) int {
return b.UpdatedAt.Compare(a.UpdatedAt) // descending order return b.UpdatedAt.Compare(a.UpdatedAt) // descending order
}) })
result := WispListResult{ result := EphemeralListResult{
Wisps: items, Wisps: items,
Count: len(items), Count: len(items),
OldCount: oldCount, OldCount: oldCount,
@@ -409,11 +409,11 @@ func runWispList(cmd *cobra.Command, args []string) {
// Human-readable output // Human-readable output
if len(items) == 0 { if len(items) == 0 {
fmt.Println("No wisps found") fmt.Println("No ephemeral issues found")
return return
} }
fmt.Printf("Wisps (%d):\n\n", len(items)) fmt.Printf("Ephemeral issues (%d):\n\n", len(items))
// Print header // Print header
fmt.Printf("%-12s %-10s %-4s %-46s %s\n", fmt.Printf("%-12s %-10s %-4s %-46s %s\n",
@@ -442,9 +442,9 @@ func runWispList(cmd *cobra.Command, args []string) {
// Print warnings // Print warnings
if oldCount > 0 { if oldCount > 0 {
fmt.Printf("\n%s %d old wisp(s) (not updated in 24+ hours)\n", fmt.Printf("\n%s %d old ephemeral issue(s) (not updated in 24+ hours)\n",
ui.RenderWarn("⚠"), oldCount) ui.RenderWarn("⚠"), oldCount)
fmt.Println(" Hint: Use 'bd wisp gc' to clean up old wisps") fmt.Println(" Hint: Use 'bd ephemeral gc' to clean up old ephemeral issues")
} }
} }
@@ -478,38 +478,38 @@ func formatTimeAgo(t time.Time) string {
} }
} }
var wispGCCmd = &cobra.Command{ var ephemeralGCCmd = &cobra.Command{
Use: "gc", Use: "gc",
Short: "Garbage collect old/abandoned wisps", Short: "Garbage collect old/abandoned ephemeral issues",
Long: `Garbage collect old or abandoned wisps from the database. Long: `Garbage collect old or abandoned ephemeral issues from the database.
A wisp is considered abandoned if: A wisp is considered abandoned if:
- It hasn't been updated in --age duration and is not closed - It hasn't been updated in --age duration and is not closed
Abandoned wisps are deleted without creating a digest. Use 'bd mol squash' Abandoned ephemeral issues are deleted without creating a digest. Use 'bd mol squash'
if you want to preserve a summary before garbage collection. if you want to preserve a summary before garbage collection.
Note: This uses time-based cleanup, appropriate for ephemeral wisps. Note: This uses time-based cleanup, appropriate for ephemeral ephemeral issues.
For graph-pressure staleness detection (blocking other work), see 'bd mol stale'. For graph-pressure staleness detection (blocking other work), see 'bd mol stale'.
Examples: Examples:
bd wisp gc # Clean abandoned wisps (default: 1h threshold) bd ephemeral gc # Clean abandoned ephemeral issues (default: 1h threshold)
bd wisp gc --dry-run # Preview what would be cleaned bd ephemeral gc --dry-run # Preview what would be cleaned
bd wisp gc --age 24h # Custom age threshold bd ephemeral gc --age 24h # Custom age threshold
bd wisp gc --all # Also clean closed wisps older than threshold`, bd ephemeral gc --all # Also clean closed ephemeral issues older than threshold`,
Run: runWispGC, Run: runEphemeralGC,
} }
// WispGCResult is the JSON output for wisp gc // EphemeralGCResult is the JSON output for ephemeral gc
type WispGCResult struct { type EphemeralGCResult struct {
CleanedIDs []string `json:"cleaned_ids"` CleanedIDs []string `json:"cleaned_ids"`
CleanedCount int `json:"cleaned_count"` CleanedCount int `json:"cleaned_count"`
Candidates int `json:"candidates,omitempty"` Candidates int `json:"candidates,omitempty"`
DryRun bool `json:"dry_run,omitempty"` DryRun bool `json:"dry_run,omitempty"`
} }
func runWispGC(cmd *cobra.Command, args []string) { func runEphemeralGC(cmd *cobra.Command, args []string) {
CheckReadonly("wisp gc") CheckReadonly("ephemeral gc")
ctx := rootCtx ctx := rootCtx
@@ -531,26 +531,26 @@ func runWispGC(cmd *cobra.Command, args []string) {
// Wisp gc requires direct store access for deletion // Wisp gc requires direct store access for deletion
if store == nil { if store == nil {
if daemonClient != nil { if daemonClient != nil {
fmt.Fprintf(os.Stderr, "Error: wisp gc requires direct database access\n") fmt.Fprintf(os.Stderr, "Error: ephemeral gc requires direct database access\n")
fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon wisp gc\n") fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon ephemeral gc\n")
} else { } else {
fmt.Fprintf(os.Stderr, "Error: no database connection\n") fmt.Fprintf(os.Stderr, "Error: no database connection\n")
} }
os.Exit(1) os.Exit(1)
} }
// Query wisps from main database using Wisp filter // Query ephemeral issues from main database using Wisp filter
wispFlag := true ephemeralFlag := true
filter := types.IssueFilter{ filter := types.IssueFilter{
Wisp: &wispFlag, Ephemeral: &ephemeralFlag,
} }
issues, err := store.SearchIssues(ctx, "", filter) issues, err := store.SearchIssues(ctx, "", filter)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Error listing wisps: %v\n", err) fmt.Fprintf(os.Stderr, "Error listing ephemeral issues: %v\n", err)
os.Exit(1) os.Exit(1)
} }
// Find old/abandoned wisps // Find old/abandoned ephemeral issues
now := time.Now() now := time.Now()
var abandoned []*types.Issue var abandoned []*types.Issue
for _, issue := range issues { for _, issue := range issues {
@@ -567,13 +567,13 @@ func runWispGC(cmd *cobra.Command, args []string) {
if len(abandoned) == 0 { if len(abandoned) == 0 {
if jsonOutput { if jsonOutput {
outputJSON(WispGCResult{ outputJSON(EphemeralGCResult{
CleanedIDs: []string{}, CleanedIDs: []string{},
CleanedCount: 0, CleanedCount: 0,
DryRun: dryRun, DryRun: dryRun,
}) })
} else { } else {
fmt.Println("No abandoned wisps found") fmt.Println("No abandoned ephemeral issues found")
} }
return return
} }
@@ -584,28 +584,28 @@ func runWispGC(cmd *cobra.Command, args []string) {
for i, o := range abandoned { for i, o := range abandoned {
ids[i] = o.ID ids[i] = o.ID
} }
outputJSON(WispGCResult{ outputJSON(EphemeralGCResult{
CleanedIDs: ids, CleanedIDs: ids,
Candidates: len(abandoned), Candidates: len(abandoned),
CleanedCount: 0, CleanedCount: 0,
DryRun: true, DryRun: true,
}) })
} else { } else {
fmt.Printf("Dry run: would clean %d abandoned wisp(s):\n\n", len(abandoned)) fmt.Printf("Dry run: would clean %d abandoned ephemeral issue(s):\n\n", len(abandoned))
for _, issue := range abandoned { for _, issue := range abandoned {
age := formatTimeAgo(issue.UpdatedAt) age := formatTimeAgo(issue.UpdatedAt)
fmt.Printf(" %s: %s (last updated: %s)\n", issue.ID, issue.Title, age) fmt.Printf(" %s: %s (last updated: %s)\n", issue.ID, issue.Title, age)
} }
fmt.Printf("\nRun without --dry-run to delete these wisps.\n") fmt.Printf("\nRun without --dry-run to delete these ephemeral issues.\n")
} }
return return
} }
// Delete abandoned wisps // Delete abandoned ephemeral issues
var cleanedIDs []string var cleanedIDs []string
sqliteStore, ok := store.(*sqlite.SQLiteStorage) sqliteStore, ok := store.(*sqlite.SQLiteStorage)
if !ok { if !ok {
fmt.Fprintf(os.Stderr, "Error: wisp gc requires SQLite storage backend\n") fmt.Fprintf(os.Stderr, "Error: ephemeral gc requires SQLite storage backend\n")
os.Exit(1) os.Exit(1)
} }
@@ -617,7 +617,7 @@ func runWispGC(cmd *cobra.Command, args []string) {
cleanedIDs = append(cleanedIDs, issue.ID) cleanedIDs = append(cleanedIDs, issue.ID)
} }
result := WispGCResult{ result := EphemeralGCResult{
CleanedIDs: cleanedIDs, CleanedIDs: cleanedIDs,
CleanedCount: len(cleanedIDs), CleanedCount: len(cleanedIDs),
} }
@@ -627,25 +627,25 @@ func runWispGC(cmd *cobra.Command, args []string) {
return return
} }
fmt.Printf("%s Cleaned %d abandoned wisp(s)\n", ui.RenderPass("✓"), result.CleanedCount) fmt.Printf("%s Cleaned %d abandoned ephemeral issue(s)\n", ui.RenderPass("✓"), result.CleanedCount)
for _, id := range cleanedIDs { for _, id := range cleanedIDs {
fmt.Printf(" - %s\n", id) fmt.Printf(" - %s\n", id)
} }
} }
func init() { func init() {
// Wisp create command flags // Ephemeral create command flags
wispCreateCmd.Flags().StringSlice("var", []string{}, "Variable substitution (key=value)") ephemeralCreateCmd.Flags().StringSlice("var", []string{}, "Variable substitution (key=value)")
wispCreateCmd.Flags().Bool("dry-run", false, "Preview what would be created") ephemeralCreateCmd.Flags().Bool("dry-run", false, "Preview what would be created")
wispListCmd.Flags().Bool("all", false, "Include closed wisps") ephemeralListCmd.Flags().Bool("all", false, "Include closed ephemeral issues")
wispGCCmd.Flags().Bool("dry-run", false, "Preview what would be cleaned") ephemeralGCCmd.Flags().Bool("dry-run", false, "Preview what would be cleaned")
wispGCCmd.Flags().String("age", "1h", "Age threshold for abandoned wisp detection") ephemeralGCCmd.Flags().String("age", "1h", "Age threshold for abandoned ephemeral issue detection")
wispGCCmd.Flags().Bool("all", false, "Also clean closed wisps older than threshold") ephemeralGCCmd.Flags().Bool("all", false, "Also clean closed ephemeral issues older than threshold")
wispCmd.AddCommand(wispCreateCmd) ephemeralCmd.AddCommand(ephemeralCreateCmd)
wispCmd.AddCommand(wispListCmd) ephemeralCmd.AddCommand(ephemeralListCmd)
wispCmd.AddCommand(wispGCCmd) ephemeralCmd.AddCommand(ephemeralGCCmd)
rootCmd.AddCommand(wispCmd) rootCmd.AddCommand(ephemeralCmd)
} }

View File

@@ -362,7 +362,7 @@ Examples:
// Wisps exist only in SQLite and are shared via .beads/redirect, not JSONL. // Wisps exist only in SQLite and are shared via .beads/redirect, not JSONL.
filtered := make([]*types.Issue, 0, len(issues)) filtered := make([]*types.Issue, 0, len(issues))
for _, issue := range issues { for _, issue := range issues {
if !issue.Wisp { if !issue.Ephemeral {
filtered = append(filtered, issue) filtered = append(filtered, issue)
} }
} }

View File

@@ -157,7 +157,7 @@ Examples:
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 1, // Gates are typically high priority Priority: 1, // Gates are typically high priority
// Assignee left empty - orchestrator decides who processes gates // Assignee left empty - orchestrator decides who processes gates
Wisp: true, // Gates are wisps (ephemeral) Ephemeral: true, // Gates are wisps (ephemeral)
AwaitType: awaitType, AwaitType: awaitType,
AwaitID: awaitID, AwaitID: awaitID,
Timeout: timeout, Timeout: timeout,

View File

@@ -87,8 +87,8 @@ func runHook(cmd *cobra.Command, args []string) {
for _, issue := range issues { for _, issue := range issues {
phase := "mol" phase := "mol"
if issue.Wisp { if issue.Ephemeral {
phase = "wisp" phase = "ephemeral"
} }
fmt.Printf(" 📌 %s (%s) - %s\n", issue.ID, phase, issue.Status) fmt.Printf(" 📌 %s (%s) - %s\n", issue.ID, phase, issue.Status)
fmt.Printf(" %s\n", issue.Title) fmt.Printf(" %s\n", issue.Title)

View File

@@ -292,6 +292,7 @@ var versionChanges = []VersionChange{
Version: "0.37.0", Version: "0.37.0",
Date: "2025-12-26", Date: "2025-12-26",
Changes: []string{ Changes: []string{
"BREAKING: Ephemeral API rename (bd-o18s) - Wisp→Ephemeral: JSON 'wisp'→'ephemeral', bd wisp→bd ephemeral",
"NEW: bd gate create/show/list/close/wait (bd-udsi) - Async coordination primitives for agent workflows", "NEW: bd gate create/show/list/close/wait (bd-udsi) - Async coordination primitives for agent workflows",
"NEW: bd gate eval (gt-twjr5.2) - Evaluate timer gates and GitHub gates (gh:run, gh:pr, mail)", "NEW: bd gate eval (gt-twjr5.2) - Evaluate timer gates and GitHub gates (gh:run, gh:pr, mail)",
"NEW: bd gate approve (gt-twjr5.4) - Human gate approval command", "NEW: bd gate approve (gt-twjr5.4) - Human gate approval command",

View File

@@ -21,7 +21,7 @@ import (
// bd mol catalog # List available protos // bd mol catalog # List available protos
// bd mol show <id> # Show proto/molecule structure // bd mol show <id> # Show proto/molecule structure
// bd pour <id> --var key=value # Instantiate proto → persistent mol // bd pour <id> --var key=value # Instantiate proto → persistent mol
// bd wisp create <id> --var key=value # Instantiate proto → ephemeral wisp // bd ephemeral create <id> --var key=value # Instantiate proto → ephemeral wisp
// MoleculeLabel is the label used to identify molecules (templates) // MoleculeLabel is the label used to identify molecules (templates)
// Molecules use the same label as templates - they ARE templates with workflow semantics // Molecules use the same label as templates - they ARE templates with workflow semantics
@@ -55,7 +55,7 @@ Commands:
See also: See also:
bd pour <proto> # Instantiate as persistent mol (liquid phase) bd pour <proto> # Instantiate as persistent mol (liquid phase)
bd wisp create <proto> # Instantiate as ephemeral wisp (vapor phase)`, bd ephemeral create <proto> # Instantiate as ephemeral wisp (vapor phase)`,
} }
// ============================================================================= // =============================================================================
@@ -72,7 +72,7 @@ func spawnMolecule(ctx context.Context, s storage.Storage, subgraph *MoleculeSub
Vars: vars, Vars: vars,
Assignee: assignee, Assignee: assignee,
Actor: actorName, Actor: actorName,
Wisp: ephemeral, Ephemeral: ephemeral,
Prefix: prefix, Prefix: prefix,
} }
return cloneSubgraph(ctx, s, subgraph, opts) return cloneSubgraph(ctx, s, subgraph, opts)

View File

@@ -40,12 +40,12 @@ Bond types:
Phase control: Phase control:
By default, spawned protos follow the target's phase: By default, spawned protos follow the target's phase:
- Attaching to mol (Wisp=false) → spawns as persistent (Wisp=false) - Attaching to mol (Ephemeral=false) → spawns as persistent (Ephemeral=false)
- Attaching to wisp (Wisp=true) → spawns as ephemeral (Wisp=true) - Attaching to ephemeral issue (Ephemeral=true) → spawns as ephemeral (Ephemeral=true)
Override with: Override with:
--pour Force spawn as liquid (persistent, Wisp=false) --pour Force spawn as liquid (persistent, Ephemeral=false)
--wisp Force spawn as vapor (ephemeral, Wisp=true, excluded from JSONL export) --ephemeral Force spawn as vapor (ephemeral, Ephemeral=true, excluded from JSONL export)
Dynamic bonding (Christmas Ornament pattern): Dynamic bonding (Christmas Ornament pattern):
Use --ref to specify a custom child reference with variable substitution. Use --ref to specify a custom child reference with variable substitution.
@@ -57,7 +57,7 @@ Dynamic bonding (Christmas Ornament pattern):
Use cases: Use cases:
- Found important bug during patrol? Use --pour to persist it - Found important bug during patrol? Use --pour to persist it
- Need ephemeral diagnostic on persistent feature? Use --wisp - Need ephemeral diagnostic on persistent feature? Use --ephemeral
- Spawning per-worker arms on a patrol? Use --ref for readable IDs - Spawning per-worker arms on a patrol? Use --ref for readable IDs
Examples: Examples:
@@ -66,7 +66,7 @@ Examples:
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-critical-bug wisp-patrol --pour # Persist found bug bd mol bond mol-critical-bug wisp-patrol --pour # Persist found bug
bd mol bond mol-temp-check bd-feature --wisp # Ephemeral diagnostic bd mol bond mol-temp-check bd-feature --ephemeral # Ephemeral diagnostic
bd mol bond mol-arm bd-patrol --ref arm-{{name}} --var name=ace # Dynamic child ID`, bd mol bond mol-arm bd-patrol --ref arm-{{name}} --var name=ace # Dynamic child ID`,
Args: cobra.ExactArgs(2), Args: cobra.ExactArgs(2),
Run: runMolBond, Run: runMolBond,
@@ -102,20 +102,20 @@ 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")
wisp, _ := cmd.Flags().GetBool("wisp") ephemeral, _ := cmd.Flags().GetBool("ephemeral")
pour, _ := cmd.Flags().GetBool("pour") pour, _ := cmd.Flags().GetBool("pour")
childRef, _ := cmd.Flags().GetString("ref") childRef, _ := cmd.Flags().GetString("ref")
// Validate phase flags are not both set // Validate phase flags are not both set
if wisp && pour { if ephemeral && pour {
fmt.Fprintf(os.Stderr, "Error: cannot use both --wisp and --pour\n") fmt.Fprintf(os.Stderr, "Error: cannot use both --ephemeral and --pour\n")
os.Exit(1) os.Exit(1)
} }
// All issues go in the main store; wisp vs pour determines the Wisp flag // All issues go in the main store; ephemeral vs pour determines the Wisp flag
// --wisp: create with Wisp=true (ephemeral, excluded from JSONL export) // --ephemeral: create with Ephemeral=true (ephemeral, excluded from JSONL export)
// --pour: create with Wisp=false (persistent, exported to JSONL) // --pour: create with Ephemeral=false (persistent, exported to JSONL)
// Default: follow target's phase (wisp if target is wisp, otherwise persistent) // Default: follow target's phase (ephemeral if target is ephemeral, otherwise persistent)
// Validate bond type // Validate bond type
if bondType != types.BondTypeSequential && bondType != types.BondTypeParallel && bondType != types.BondTypeConditional { if bondType != types.BondTypeSequential && bondType != types.BondTypeParallel && bondType != types.BondTypeConditional {
@@ -181,8 +181,8 @@ func runMolBond(cmd *cobra.Command, args []string) {
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 wisp { if ephemeral {
fmt.Printf(" Phase override: vapor (--wisp)\n") fmt.Printf(" Phase override: vapor (--ephemeral)\n")
} else if pour { } else if pour {
fmt.Printf(" Phase override: liquid (--pour)\n") fmt.Printf(" Phase override: liquid (--pour)\n")
} }
@@ -240,16 +240,16 @@ func runMolBond(cmd *cobra.Command, args []string) {
case aIsProto && !bIsProto: case aIsProto && !bIsProto:
// Pass subgraph directly if cooked from formula // Pass subgraph directly if cooked from formula
if cookedA { if cookedA {
result, err = bondProtoMolWithSubgraph(ctx, store, subgraphA, issueA, issueB, bondType, vars, childRef, actor, wisp, pour) result, err = bondProtoMolWithSubgraph(ctx, store, subgraphA, issueA, issueB, bondType, vars, childRef, actor, ephemeral, pour)
} else { } else {
result, err = bondProtoMol(ctx, store, issueA, issueB, bondType, vars, childRef, actor, wisp, pour) result, err = bondProtoMol(ctx, store, issueA, issueB, bondType, vars, childRef, actor, ephemeral, pour)
} }
case !aIsProto && bIsProto: case !aIsProto && bIsProto:
// Pass subgraph directly if cooked from formula // Pass subgraph directly if cooked from formula
if cookedB { if cookedB {
result, err = bondProtoMolWithSubgraph(ctx, store, subgraphB, issueB, issueA, bondType, vars, childRef, actor, wisp, pour) result, err = bondProtoMolWithSubgraph(ctx, store, subgraphB, issueB, issueA, bondType, vars, childRef, actor, ephemeral, pour)
} else { } else {
result, err = bondMolProto(ctx, store, issueA, issueB, bondType, vars, childRef, actor, wisp, pour) result, err = bondMolProto(ctx, store, issueA, issueB, bondType, vars, childRef, actor, ephemeral, pour)
} }
default: default:
result, err = bondMolMol(ctx, store, issueA, issueB, bondType, actor) result, err = bondMolMol(ctx, store, issueA, issueB, bondType, actor)
@@ -273,10 +273,10 @@ 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 wisp { if ephemeral {
fmt.Printf(" Phase: vapor (ephemeral, Wisp=true)\n") fmt.Printf(" Phase: vapor (ephemeral, Ephemeral=true)\n")
} else if pour { } else if pour {
fmt.Printf(" Phase: liquid (persistent, Wisp=false)\n") fmt.Printf(" Phase: liquid (persistent, Ephemeral=false)\n")
} }
} }
@@ -386,12 +386,12 @@ func bondProtoProto(ctx context.Context, s storage.Storage, protoA, protoB *type
// bondProtoMol bonds a proto to an existing molecule by spawning the proto. // bondProtoMol bonds a proto to an existing molecule by spawning the proto.
// If childRef is provided, generates custom IDs like "parent.childref" (dynamic bonding). // If childRef is provided, generates custom IDs like "parent.childref" (dynamic bonding).
// protoSubgraph can be nil if proto is from DB (will be loaded), or pre-loaded for formulas. // protoSubgraph can be nil if proto is from DB (will be loaded), or pre-loaded for formulas.
func bondProtoMol(ctx context.Context, s storage.Storage, proto, mol *types.Issue, bondType string, vars map[string]string, childRef string, actorName string, wispFlag, pourFlag bool) (*BondResult, error) { func bondProtoMol(ctx context.Context, s storage.Storage, proto, mol *types.Issue, bondType string, vars map[string]string, childRef string, actorName string, ephemeralFlag, pourFlag bool) (*BondResult, error) {
return bondProtoMolWithSubgraph(ctx, s, nil, proto, mol, bondType, vars, childRef, actorName, wispFlag, pourFlag) return bondProtoMolWithSubgraph(ctx, s, nil, proto, mol, bondType, vars, childRef, actorName, ephemeralFlag, pourFlag)
} }
// bondProtoMolWithSubgraph is the internal implementation that accepts a pre-loaded subgraph. // bondProtoMolWithSubgraph is the internal implementation that accepts a pre-loaded subgraph.
func bondProtoMolWithSubgraph(ctx context.Context, s storage.Storage, protoSubgraph *TemplateSubgraph, proto, mol *types.Issue, bondType string, vars map[string]string, childRef string, actorName string, wispFlag, pourFlag bool) (*BondResult, error) { func bondProtoMolWithSubgraph(ctx context.Context, s storage.Storage, protoSubgraph *TemplateSubgraph, proto, mol *types.Issue, bondType string, vars map[string]string, childRef string, actorName string, ephemeralFlag, pourFlag bool) (*BondResult, error) {
// Use provided subgraph or load from DB // Use provided subgraph or load from DB
subgraph := protoSubgraph subgraph := protoSubgraph
if subgraph == nil { if subgraph == nil {
@@ -414,20 +414,20 @@ func bondProtoMolWithSubgraph(ctx context.Context, s storage.Storage, protoSubgr
return nil, fmt.Errorf("missing required variables: %s (use --var)", strings.Join(missingVars, ", ")) return nil, fmt.Errorf("missing required variables: %s (use --var)", strings.Join(missingVars, ", "))
} }
// Determine wisp flag based on explicit flags or target's phase // Determine ephemeral flag based on explicit flags or target's phase
// --wisp: force wisp=true, --pour: force wisp=false, neither: follow target // --ephemeral: force ephemeral=true, --pour: force ephemeral=false, neither: follow target
makeWisp := mol.Wisp // Default: follow target's phase makeEphemeral := mol.Ephemeral // Default: follow target's phase
if wispFlag { if ephemeralFlag {
makeWisp = true makeEphemeral = true
} else if pourFlag { } else if pourFlag {
makeWisp = false makeEphemeral = false
} }
// Build CloneOptions for spawning // Build CloneOptions for spawning
opts := CloneOptions{ opts := CloneOptions{
Vars: vars, Vars: vars,
Actor: actorName, Actor: actorName,
Wisp: makeWisp, Ephemeral: makeEphemeral,
} }
// Dynamic bonding: use custom IDs if childRef is provided // Dynamic bonding: use custom IDs if childRef is provided
@@ -482,9 +482,9 @@ func bondProtoMolWithSubgraph(ctx context.Context, s storage.Storage, protoSubgr
} }
// bondMolProto bonds a molecule to a proto (symmetric with bondProtoMol) // bondMolProto bonds a molecule to a proto (symmetric with bondProtoMol)
func bondMolProto(ctx context.Context, s storage.Storage, mol, proto *types.Issue, bondType string, vars map[string]string, childRef string, actorName string, wispFlag, pourFlag bool) (*BondResult, error) { func bondMolProto(ctx context.Context, s storage.Storage, mol, proto *types.Issue, bondType string, vars map[string]string, childRef string, actorName string, ephemeralFlag, pourFlag bool) (*BondResult, error) {
// Same as bondProtoMol but with arguments swapped // Same as bondProtoMol but with arguments swapped
return bondProtoMol(ctx, s, proto, mol, bondType, vars, childRef, actorName, wispFlag, pourFlag) return bondProtoMol(ctx, s, proto, mol, bondType, vars, childRef, actorName, ephemeralFlag, pourFlag)
} }
// bondMolMol bonds two molecules together // bondMolMol bonds two molecules together
@@ -630,8 +630,8 @@ 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("wisp", false, "Force spawn as vapor (ephemeral, Wisp=true)") molBondCmd.Flags().Bool("ephemeral", false, "Force spawn as vapor (ephemeral, Ephemeral=true)")
molBondCmd.Flags().Bool("pour", false, "Force spawn as liquid (persistent, Wisp=false)") molBondCmd.Flags().Bool("pour", false, "Force spawn as liquid (persistent, Ephemeral=false)")
molBondCmd.Flags().String("ref", "", "Custom child reference with {{var}} substitution (e.g., arm-{{polecat_name}})") molBondCmd.Flags().String("ref", "", "Custom child reference with {{var}} substitution (e.g., arm-{{polecat_name}})")
molCmd.AddCommand(molBondCmd) molCmd.AddCommand(molBondCmd)

View File

@@ -23,8 +23,8 @@ completely removes the wisp with no trace. Use this for:
- Test/debug wisps you don't want to preserve - Test/debug wisps you don't want to preserve
The burn operation: The burn operation:
1. Verifies the molecule has Wisp=true (is ephemeral) 1. Verifies the molecule has Ephemeral=true (is ephemeral)
2. Deletes the molecule and all its wisp children 2. Deletes the molecule and all its ephemeral children
3. No digest is created (use 'bd mol squash' if you want a digest) 3. No digest is created (use 'bd mol squash' if you want a digest)
CAUTION: This is a destructive operation. The wisp's data will be CAUTION: This is a destructive operation. The wisp's data will be
@@ -81,8 +81,8 @@ func runMolBurn(cmd *cobra.Command, args []string) {
} }
// Verify it's a wisp // Verify it's a wisp
if !rootIssue.Wisp { if !rootIssue.Ephemeral {
fmt.Fprintf(os.Stderr, "Error: molecule %s is not a wisp (Wisp=false)\n", resolvedID) fmt.Fprintf(os.Stderr, "Error: molecule %s is not a wisp (Ephemeral=false)\n", resolvedID)
fmt.Fprintf(os.Stderr, "Hint: mol burn only works with wisp molecules\n") fmt.Fprintf(os.Stderr, "Hint: mol burn only works with wisp molecules\n")
fmt.Fprintf(os.Stderr, " Use 'bd delete' to remove non-wisp issues\n") fmt.Fprintf(os.Stderr, " Use 'bd delete' to remove non-wisp issues\n")
os.Exit(1) os.Exit(1)
@@ -98,7 +98,7 @@ func runMolBurn(cmd *cobra.Command, args []string) {
// Collect wisp issue IDs to delete (only delete wisps, not regular children) // Collect wisp issue IDs to delete (only delete wisps, not regular children)
var wispIDs []string var wispIDs []string
for _, issue := range subgraph.Issues { for _, issue := range subgraph.Issues {
if issue.Wisp { if issue.Ephemeral {
wispIDs = append(wispIDs, issue.ID) wispIDs = append(wispIDs, issue.ID)
} }
} }
@@ -120,7 +120,7 @@ func runMolBurn(cmd *cobra.Command, args []string) {
fmt.Printf("Root: %s\n", subgraph.Root.Title) fmt.Printf("Root: %s\n", subgraph.Root.Title)
fmt.Printf("\nWisp issues to delete (%d total):\n", len(wispIDs)) fmt.Printf("\nWisp issues to delete (%d total):\n", len(wispIDs))
for _, issue := range subgraph.Issues { for _, issue := range subgraph.Issues {
if !issue.Wisp { if !issue.Ephemeral {
continue continue
} }
status := string(issue.Status) status := string(issue.Status)
@@ -166,7 +166,7 @@ func runMolBurn(cmd *cobra.Command, args []string) {
} }
fmt.Printf("%s Burned wisp: %d issues deleted\n", ui.RenderPass("✓"), result.DeletedCount) fmt.Printf("%s Burned wisp: %d issues deleted\n", ui.RenderPass("✓"), result.DeletedCount)
fmt.Printf(" Wisp: %s\n", resolvedID) fmt.Printf(" Ephemeral: %s\n", resolvedID)
fmt.Printf(" No digest created.\n") fmt.Printf(" No digest created.\n")
} }

View File

@@ -23,7 +23,7 @@ var molCatalogCmd = &cobra.Command{
Use: "catalog", Use: "catalog",
Aliases: []string{"list", "ls"}, Aliases: []string{"list", "ls"},
Short: "List available molecule formulas", Short: "List available molecule formulas",
Long: `List formulas available for bd pour / bd wisp create. Long: `List formulas available for bd pour / bd ephemeral create.
Formulas are ephemeral proto definitions stored as .formula.json files. Formulas are ephemeral proto definitions stored as .formula.json files.
They are cooked inline when pouring, never stored as database beads. They are cooked inline when pouring, never stored as database beads.
@@ -93,11 +93,11 @@ Search paths (in priority order):
fmt.Println(" bd mol distill <epic-id> my-workflow") fmt.Println(" bd mol distill <epic-id> my-workflow")
fmt.Println("\nTo instantiate from formula:") fmt.Println("\nTo instantiate from formula:")
fmt.Println(" bd pour <formula-name> --var key=value # persistent mol") fmt.Println(" bd pour <formula-name> --var key=value # persistent mol")
fmt.Println(" bd wisp create <formula-name> --var key=value # ephemeral wisp") fmt.Println(" bd ephemeral create <formula-name> --var key=value # ephemeral wisp")
return return
} }
fmt.Printf("%s\n\n", ui.RenderPass("Formulas (for bd pour / bd wisp create):")) fmt.Printf("%s\n\n", ui.RenderPass("Formulas (for bd pour / bd ephemeral create):"))
// Group by type for display // Group by type for display
byType := make(map[string][]CatalogEntry) byType := make(map[string][]CatalogEntry)

View File

@@ -18,17 +18,17 @@ import (
var molSquashCmd = &cobra.Command{ var molSquashCmd = &cobra.Command{
Use: "squash <molecule-id>", Use: "squash <molecule-id>",
Short: "Compress molecule execution into a digest", Short: "Compress molecule execution into a digest",
Long: `Squash a molecule's wisp children into a single digest issue. Long: `Squash a molecule's ephemeral children into a single digest issue.
This command collects all wisp child issues of a molecule (Wisp=true), This command collects all ephemeral child issues of a molecule (Ephemeral=true),
generates a summary digest, and promotes the wisps to persistent by generates a summary digest, and promotes the wisps to persistent by
clearing their Wisp flag (or optionally deletes them). clearing their Wisp flag (or optionally deletes them).
The squash operation: The squash operation:
1. Loads the molecule and all its children 1. Loads the molecule and all its children
2. Filters to only wisps (ephemeral issues with Wisp=true) 2. Filters to only wisps (ephemeral issues with Ephemeral=true)
3. Generates a digest (summary of work done) 3. Generates a digest (summary of work done)
4. Creates a permanent digest issue (Wisp=false) 4. Creates a permanent digest issue (Ephemeral=false)
5. Clears Wisp flag on children (promotes to persistent) 5. Clears Wisp flag on children (promotes to persistent)
OR deletes them with --delete-children OR deletes them with --delete-children
@@ -95,13 +95,13 @@ func runMolSquash(cmd *cobra.Command, args []string) {
os.Exit(1) os.Exit(1)
} }
// Filter to only wisp children (exclude root) // Filter to only ephemeral children (exclude root)
var wispChildren []*types.Issue var wispChildren []*types.Issue
for _, issue := range subgraph.Issues { for _, issue := range subgraph.Issues {
if issue.ID == subgraph.Root.ID { if issue.ID == subgraph.Root.ID {
continue // Skip root continue // Skip root
} }
if issue.Wisp { if issue.Ephemeral {
wispChildren = append(wispChildren, issue) wispChildren = append(wispChildren, issue)
} }
} }
@@ -113,13 +113,13 @@ func runMolSquash(cmd *cobra.Command, args []string) {
SquashedCount: 0, SquashedCount: 0,
}) })
} else { } else {
fmt.Printf("No wisp children found for molecule %s\n", moleculeID) fmt.Printf("No ephemeral children found for molecule %s\n", moleculeID)
} }
return return
} }
if dryRun { if dryRun {
fmt.Printf("\nDry run: would squash %d wisp children of %s\n\n", len(wispChildren), moleculeID) fmt.Printf("\nDry run: would squash %d ephemeral children of %s\n\n", len(wispChildren), moleculeID)
fmt.Printf("Root: %s\n", subgraph.Root.Title) fmt.Printf("Root: %s\n", subgraph.Root.Title)
fmt.Printf("\nWisp children to squash:\n") fmt.Printf("\nWisp children to squash:\n")
for _, issue := range wispChildren { for _, issue := range wispChildren {
@@ -247,7 +247,7 @@ func squashMolecule(ctx context.Context, s storage.Storage, root *types.Issue, c
CloseReason: fmt.Sprintf("Squashed from %d wisps", len(children)), CloseReason: fmt.Sprintf("Squashed from %d wisps", len(children)),
Priority: root.Priority, Priority: root.Priority,
IssueType: types.TypeTask, IssueType: types.TypeTask,
Wisp: false, // Digest is permanent, not a wisp Ephemeral: false, // Digest is permanent, not a wisp
ClosedAt: &now, ClosedAt: &now,
} }
@@ -283,7 +283,7 @@ func squashMolecule(ctx context.Context, s storage.Storage, root *types.Issue, c
return nil, err return nil, err
} }
// Delete wisp children (outside transaction for better error handling) // Delete ephemeral children (outside transaction for better error handling)
if !keepChildren { if !keepChildren {
deleted, err := deleteWispChildren(ctx, s, childIDs) deleted, err := deleteWispChildren(ctx, s, childIDs)
if err != nil { if err != nil {
@@ -319,7 +319,7 @@ func deleteWispChildren(ctx context.Context, s storage.Storage, ids []string) (i
func init() { func init() {
molSquashCmd.Flags().Bool("dry-run", false, "Preview what would be squashed") molSquashCmd.Flags().Bool("dry-run", false, "Preview what would be squashed")
molSquashCmd.Flags().Bool("keep-children", false, "Don't delete wisp children after squash") molSquashCmd.Flags().Bool("keep-children", false, "Don't delete ephemeral children after squash")
molSquashCmd.Flags().String("summary", "", "Agent-provided summary (bypasses auto-generation)") molSquashCmd.Flags().String("summary", "", "Agent-provided summary (bypasses auto-generation)")
molCmd.AddCommand(molSquashCmd) molCmd.AddCommand(molSquashCmd)

View File

@@ -489,7 +489,7 @@ func TestSquashMolecule(t *testing.T) {
Status: types.StatusClosed, Status: types.StatusClosed,
Priority: 2, Priority: 2,
IssueType: types.TypeTask, IssueType: types.TypeTask,
Wisp: true, Ephemeral: true,
CloseReason: "Completed design", CloseReason: "Completed design",
} }
child2 := &types.Issue{ child2 := &types.Issue{
@@ -498,7 +498,7 @@ func TestSquashMolecule(t *testing.T) {
Status: types.StatusClosed, Status: types.StatusClosed,
Priority: 2, Priority: 2,
IssueType: types.TypeTask, IssueType: types.TypeTask,
Wisp: true, Ephemeral: true,
CloseReason: "Code merged", CloseReason: "Code merged",
} }
@@ -547,7 +547,7 @@ func TestSquashMolecule(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Failed to get digest: %v", err) t.Fatalf("Failed to get digest: %v", err)
} }
if digest.Wisp { if digest.Ephemeral {
t.Error("Digest should NOT be ephemeral") t.Error("Digest should NOT be ephemeral")
} }
if digest.Status != types.StatusClosed { if digest.Status != types.StatusClosed {
@@ -595,7 +595,7 @@ func TestSquashMoleculeWithDelete(t *testing.T) {
Status: types.StatusClosed, Status: types.StatusClosed,
Priority: 2, Priority: 2,
IssueType: types.TypeTask, IssueType: types.TypeTask,
Wisp: true, Ephemeral: true,
} }
if err := s.CreateIssue(ctx, child, "test"); err != nil { if err := s.CreateIssue(ctx, child, "test"); err != nil {
t.Fatalf("Failed to create child: %v", err) t.Fatalf("Failed to create child: %v", err)
@@ -705,7 +705,7 @@ func TestSquashMoleculeWithAgentSummary(t *testing.T) {
Status: types.StatusClosed, Status: types.StatusClosed,
Priority: 2, Priority: 2,
IssueType: types.TypeTask, IssueType: types.TypeTask,
Wisp: true, Ephemeral: true,
CloseReason: "Done", CloseReason: "Done",
} }
if err := s.CreateIssue(ctx, child, "test"); err != nil { if err := s.CreateIssue(ctx, child, "test"); err != nil {
@@ -1304,14 +1304,14 @@ func TestWispFilteringFromExport(t *testing.T) {
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 1, Priority: 1,
IssueType: types.TypeTask, IssueType: types.TypeTask,
Wisp: false, Ephemeral: false,
} }
wispIssue := &types.Issue{ wispIssue := &types.Issue{
Title: "Wisp Issue", Title: "Wisp Issue",
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 2, Priority: 2,
IssueType: types.TypeTask, IssueType: types.TypeTask,
Wisp: true, Ephemeral: true,
} }
if err := s.CreateIssue(ctx, normalIssue, "test"); err != nil { if err := s.CreateIssue(ctx, normalIssue, "test"); err != nil {
@@ -1333,7 +1333,7 @@ func TestWispFilteringFromExport(t *testing.T) {
// Filter wisp issues (simulating export behavior) // Filter wisp issues (simulating export behavior)
exportableIssues := make([]*types.Issue, 0) exportableIssues := make([]*types.Issue, 0)
for _, issue := range allIssues { for _, issue := range allIssues {
if !issue.Wisp { if !issue.Ephemeral {
exportableIssues = append(exportableIssues, issue) exportableIssues = append(exportableIssues, issue)
} }
} }

View File

@@ -218,7 +218,7 @@ func writeIssuesToJSONL(memStore *memory.MemoryStorage, beadsDir string) error {
// Wisps exist only in SQLite and are shared via .beads/redirect, not JSONL. // Wisps exist only in SQLite and are shared via .beads/redirect, not JSONL.
filtered := make([]*types.Issue, 0, len(issues)) filtered := make([]*types.Issue, 0, len(issues))
for _, issue := range issues { for _, issue := range issues {
if !issue.Wisp { if !issue.Ephemeral {
filtered = append(filtered, issue) filtered = append(filtered, issue)
} }
} }

View File

@@ -65,7 +65,7 @@ func exportToJSONL(ctx context.Context, jsonlPath string) error {
// This prevents "zombie" issues that resurrect after mol squash deletes them. // This prevents "zombie" issues that resurrect after mol squash deletes them.
filteredIssues := make([]*types.Issue, 0, len(issues)) filteredIssues := make([]*types.Issue, 0, len(issues))
for _, issue := range issues { for _, issue := range issues {
if issue.Wisp { if issue.Ephemeral {
continue continue
} }
filteredIssues = append(filteredIssues, issue) filteredIssues = append(filteredIssues, issue)

View File

@@ -42,10 +42,10 @@ type InstantiateResult struct {
// CloneOptions controls how the subgraph is cloned during spawn/bond // CloneOptions controls how the subgraph is cloned during spawn/bond
type CloneOptions struct { type CloneOptions struct {
Vars map[string]string // Variable substitutions for {{key}} placeholders Vars map[string]string // Variable substitutions for {{key}} placeholders
Assignee string // Assign the root epic to this agent/user Assignee string // Assign the root epic to this agent/user
Actor string // Actor performing the operation Actor string // Actor performing the operation
Wisp bool // If true, spawned issues are marked for bulk deletion Ephemeral bool // If true, spawned issues are marked for bulk deletion
Prefix string // Override prefix for ID generation (bd-hobo: distinct prefixes) Prefix string // Override prefix for ID generation (bd-hobo: distinct prefixes)
// Dynamic bonding fields (for Christmas Ornament pattern) // Dynamic bonding fields (for Christmas Ornament pattern)
@@ -327,7 +327,7 @@ Example:
Vars: vars, Vars: vars,
Assignee: assignee, Assignee: assignee,
Actor: actor, Actor: actor,
Wisp: false, Ephemeral: false,
} }
var result *InstantiateResult var result *InstantiateResult
if daemonClient != nil { if daemonClient != nil {
@@ -713,7 +713,7 @@ func cloneSubgraphViaDaemon(client *rpc.Client, subgraph *TemplateSubgraph, opts
AcceptanceCriteria: substituteVariables(oldIssue.AcceptanceCriteria, opts.Vars), AcceptanceCriteria: substituteVariables(oldIssue.AcceptanceCriteria, opts.Vars),
Assignee: issueAssignee, Assignee: issueAssignee,
EstimatedMinutes: oldIssue.EstimatedMinutes, EstimatedMinutes: oldIssue.EstimatedMinutes,
Wisp: opts.Wisp, Ephemeral: opts.Ephemeral,
IDPrefix: opts.Prefix, // bd-hobo: distinct prefixes for mols/wisps IDPrefix: opts.Prefix, // bd-hobo: distinct prefixes for mols/wisps
} }
@@ -960,7 +960,7 @@ func cloneSubgraph(ctx context.Context, s storage.Storage, subgraph *TemplateSub
IssueType: oldIssue.IssueType, IssueType: oldIssue.IssueType,
Assignee: issueAssignee, Assignee: issueAssignee,
EstimatedMinutes: oldIssue.EstimatedMinutes, EstimatedMinutes: oldIssue.EstimatedMinutes,
Wisp: opts.Wisp, // bd-2vh3: mark for cleanup when closed Ephemeral: opts.Ephemeral, // bd-2vh3: mark for cleanup when closed
IDPrefix: opts.Prefix, // bd-hobo: distinct prefixes for mols/wisps IDPrefix: opts.Prefix, // bd-hobo: distinct prefixes for mols/wisps
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),

View File

@@ -27,7 +27,7 @@ func TestThreadTraversal(t *testing.T) {
IssueType: types.TypeMessage, IssueType: types.TypeMessage,
Assignee: "worker", Assignee: "worker",
Sender: "manager", Sender: "manager",
Wisp: true, Ephemeral: true,
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
} }
@@ -43,7 +43,7 @@ func TestThreadTraversal(t *testing.T) {
IssueType: types.TypeMessage, IssueType: types.TypeMessage,
Assignee: "manager", Assignee: "manager",
Sender: "worker", Sender: "worker",
Wisp: true, Ephemeral: true,
CreatedAt: now.Add(time.Minute), CreatedAt: now.Add(time.Minute),
UpdatedAt: now.Add(time.Minute), UpdatedAt: now.Add(time.Minute),
} }
@@ -59,7 +59,7 @@ func TestThreadTraversal(t *testing.T) {
IssueType: types.TypeMessage, IssueType: types.TypeMessage,
Assignee: "worker", Assignee: "worker",
Sender: "manager", Sender: "manager",
Wisp: true, Ephemeral: true,
CreatedAt: now.Add(2 * time.Minute), CreatedAt: now.Add(2 * time.Minute),
UpdatedAt: now.Add(2 * time.Minute), UpdatedAt: now.Add(2 * time.Minute),
} }
@@ -190,7 +190,7 @@ func TestThreadTraversalEmptyThread(t *testing.T) {
IssueType: types.TypeMessage, IssueType: types.TypeMessage,
Assignee: "user", Assignee: "user",
Sender: "sender", Sender: "sender",
Wisp: true, Ephemeral: true,
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
} }
@@ -228,7 +228,7 @@ func TestThreadTraversalBranching(t *testing.T) {
IssueType: types.TypeMessage, IssueType: types.TypeMessage,
Assignee: "user", Assignee: "user",
Sender: "sender", Sender: "sender",
Wisp: true, Ephemeral: true,
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
} }
@@ -245,7 +245,7 @@ func TestThreadTraversalBranching(t *testing.T) {
IssueType: types.TypeMessage, IssueType: types.TypeMessage,
Assignee: "sender", Assignee: "sender",
Sender: "user", Sender: "user",
Wisp: true, Ephemeral: true,
CreatedAt: now.Add(time.Minute), CreatedAt: now.Add(time.Minute),
UpdatedAt: now.Add(time.Minute), UpdatedAt: now.Add(time.Minute),
} }
@@ -261,7 +261,7 @@ func TestThreadTraversalBranching(t *testing.T) {
IssueType: types.TypeMessage, IssueType: types.TypeMessage,
Assignee: "sender", Assignee: "sender",
Sender: "another-user", Sender: "another-user",
Wisp: true, Ephemeral: true,
CreatedAt: now.Add(2 * time.Minute), CreatedAt: now.Add(2 * time.Minute),
UpdatedAt: now.Add(2 * time.Minute), UpdatedAt: now.Add(2 * time.Minute),
} }
@@ -364,7 +364,7 @@ func TestThreadTraversalOnlyRepliesTo(t *testing.T) {
IssueType: types.TypeMessage, IssueType: types.TypeMessage,
Assignee: "user", Assignee: "user",
Sender: "sender", Sender: "sender",
Wisp: true, Ephemeral: true,
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
} }
@@ -380,7 +380,7 @@ func TestThreadTraversalOnlyRepliesTo(t *testing.T) {
IssueType: types.TypeMessage, IssueType: types.TypeMessage,
Assignee: "user", Assignee: "user",
Sender: "sender", Sender: "sender",
Wisp: true, Ephemeral: true,
CreatedAt: now.Add(time.Minute), CreatedAt: now.Add(time.Minute),
UpdatedAt: now.Add(time.Minute), UpdatedAt: now.Add(time.Minute),
} }

View File

@@ -275,7 +275,7 @@ open ──▶ in_progress ──▶ closed
``` ```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ bd wisp create │───▶│ Wisp Issues │───▶│ bd mol squash │ │ bd ephemeral create │───▶│ Wisp Issues │───▶│ bd mol squash │
│ (from template) │ │ (local-only) │ │ (→ digest) │ │ (from template) │ │ (local-only) │ │ (→ digest) │
└─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘
``` ```

View File

@@ -351,7 +351,7 @@ Beads uses a chemistry metaphor for template-based workflows. See [MOLECULES.md]
|-------|-------|---------|---------| |-------|-------|---------|---------|
| Solid | Proto | `.beads/` | `bd mol catalog` | | Solid | Proto | `.beads/` | `bd mol catalog` |
| Liquid | Mol | `.beads/` | `bd pour` | | Liquid | Mol | `.beads/` | `bd pour` |
| Vapor | Wisp | `.beads/` (Wisp=true, not exported) | `bd wisp create` | | Vapor | Wisp | `.beads/` (Wisp=true, not exported) | `bd ephemeral create` |
### Proto/Template Commands ### Proto/Template Commands
@@ -385,17 +385,17 @@ bd pour <proto-id> --attach <other-proto> --json
### Wisp Commands ### Wisp Commands
```bash ```bash
# Instantiate proto as ephemeral wisp (solid → vapor) # Instantiate proto as ephemeral issue (solid → vapor)
bd wisp create <proto-id> --var key=value --json bd ephemeral create <proto-id> --var key=value --json
# List all wisps # List all wisps
bd wisp list --json bd ephemeral list --json
bd wisp list --all --json # Include closed bd ephemeral list --all --json # Include closed
# Garbage collect orphaned wisps # Garbage collect orphaned wisps
bd wisp gc --json bd ephemeral gc --json
bd wisp gc --age 24h --json # Custom age threshold bd ephemeral gc --age 24h --json # Custom age threshold
bd wisp gc --dry-run # Preview what would be cleaned bd ephemeral gc --dry-run # Preview what would be cleaned
``` ```
### Bonding (Combining Work) ### Bonding (Combining Work)
@@ -424,29 +424,29 @@ bd mol bond <A> <B> --dry-run
```bash ```bash
# Compress wisp to permanent digest # Compress wisp to permanent digest
bd mol squash <wisp-id> --json bd mol squash <ephemeral-id> --json
# With agent-provided summary # With agent-provided summary
bd mol squash <wisp-id> --summary "Work completed" --json bd mol squash <ephemeral-id> --summary "Work completed" --json
# Preview # Preview
bd mol squash <wisp-id> --dry-run bd mol squash <ephemeral-id> --dry-run
# Keep wisp children after squash # Keep wisp children after squash
bd mol squash <wisp-id> --keep-children --json bd mol squash <ephemeral-id> --keep-children --json
``` ```
### Burn (Discard Wisp) ### Burn (Discard Wisp)
```bash ```bash
# Delete wisp without digest (destructive) # Delete wisp without digest (destructive)
bd mol burn <wisp-id> --json bd mol burn <ephemeral-id> --json
# Preview # Preview
bd mol burn <wisp-id> --dry-run bd mol burn <ephemeral-id> --dry-run
# Skip confirmation # Skip confirmation
bd mol burn <wisp-id> --force --json bd mol burn <ephemeral-id> --force --json
``` ```
**Note:** Most mol commands require `--no-daemon` flag when daemon is running. **Note:** Most mol commands require `--no-daemon` flag when daemon is running.

View File

@@ -202,7 +202,7 @@ The 1-hour grace period ensures tombstones propagate even with minor clock drift
## Wisps: Intentional Tombstone Bypass ## Wisps: Intentional Tombstone Bypass
**Wisps** (ephemeral issues created by `bd wisp create`) are intentionally excluded from tombstone tracking. **Wisps** (ephemeral issues created by `bd ephemeral create`) are intentionally excluded from tombstone tracking.
### Why Wisps Don't Need Tombstones ### Why Wisps Don't Need Tombstones

View File

@@ -129,7 +129,7 @@ For reusable workflows, beads uses a chemistry metaphor:
```bash ```bash
bd pour <proto> # Proto → Mol (persistent instance) bd pour <proto> # Proto → Mol (persistent instance)
bd wisp create <proto> # Proto → Wisp (ephemeral instance) bd ephemeral create <proto> # Proto → Wisp (ephemeral instance)
bd mol squash <id> # Mol/Wisp → Digest (permanent record) bd mol squash <id> # Mol/Wisp → Digest (permanent record)
bd mol burn <id> # Wisp → nothing (discard) bd mol burn <id> # Wisp → nothing (discard)
``` ```
@@ -227,10 +227,10 @@ bd close <id> --reason "Done"
Wisps accumulate if not squashed/burned: Wisps accumulate if not squashed/burned:
```bash ```bash
bd wisp list # Check for orphans bd ephemeral list # Check for orphans
bd mol squash <id> # Create digest bd mol squash <id> # Create digest
bd mol burn <id> # Or discard bd mol burn <id> # Or discard
bd wisp gc # Garbage collect old wisps bd ephemeral gc # Garbage collect old wisps
``` ```
## Layer Cake Architecture ## Layer Cake Architecture
@@ -273,7 +273,7 @@ bd dep tree <id> # Show dependency tree
```bash ```bash
bd pour <proto> --var k=v # Template → persistent mol bd pour <proto> --var k=v # Template → persistent mol
bd wisp create <proto> # Template → ephemeral wisp bd ephemeral create <proto> # Template → ephemeral wisp
bd mol bond A B # Connect work graphs bd mol bond A B # Connect work graphs
bd mol squash <id> # Compress to digest bd mol squash <id> # Compress to digest
bd mol burn <id> # Discard without record bd mol burn <id> # Discard without record

View File

@@ -89,11 +89,11 @@ type CreateArgs struct {
WaitsFor string `json:"waits_for,omitempty"` // Spawner issue ID to wait for WaitsFor string `json:"waits_for,omitempty"` // Spawner issue ID to wait for
WaitsForGate string `json:"waits_for_gate,omitempty"` // Gate type: all-children or any-children WaitsForGate string `json:"waits_for_gate,omitempty"` // Gate type: all-children or any-children
// Messaging fields (bd-kwro) // Messaging fields (bd-kwro)
Sender string `json:"sender,omitempty"` // Who sent this (for messages) Sender string `json:"sender,omitempty"` // Who sent this (for messages)
Wisp bool `json:"wisp,omitempty"` // Wisp = ephemeral vapor from the Steam Engine; bulk-deleted when closed Ephemeral bool `json:"ephemeral,omitempty"` // If true, not exported to JSONL; bulk-deleted when closed
RepliesTo string `json:"replies_to,omitempty"` // Issue ID for conversation threading RepliesTo string `json:"replies_to,omitempty"` // Issue ID for conversation threading
// ID generation (bd-hobo) // ID generation (bd-hobo)
IDPrefix string `json:"id_prefix,omitempty"` // Override prefix for ID generation (mol, wisp, etc.) IDPrefix string `json:"id_prefix,omitempty"` // Override prefix for ID generation (mol, eph, etc.)
CreatedBy string `json:"created_by,omitempty"` // Who created the issue CreatedBy string `json:"created_by,omitempty"` // Who created the issue
} }
@@ -115,8 +115,8 @@ type UpdateArgs struct {
RemoveLabels []string `json:"remove_labels,omitempty"` RemoveLabels []string `json:"remove_labels,omitempty"`
SetLabels []string `json:"set_labels,omitempty"` SetLabels []string `json:"set_labels,omitempty"`
// Messaging fields (bd-kwro) // Messaging fields (bd-kwro)
Sender *string `json:"sender,omitempty"` // Who sent this (for messages) Sender *string `json:"sender,omitempty"` // Who sent this (for messages)
Wisp *bool `json:"wisp,omitempty"` // Wisp = ephemeral vapor from the Steam Engine; bulk-deleted when closed Ephemeral *bool `json:"ephemeral,omitempty"` // If true, not exported to JSONL; bulk-deleted when closed
RepliesTo *string `json:"replies_to,omitempty"` // Issue ID for conversation threading RepliesTo *string `json:"replies_to,omitempty"` // Issue ID for conversation threading
// Graph link fields (bd-fu83) // Graph link fields (bd-fu83)
RelatesTo *string `json:"relates_to,omitempty"` // JSON array of related issue IDs RelatesTo *string `json:"relates_to,omitempty"` // JSON array of related issue IDs
@@ -193,8 +193,8 @@ type ListArgs struct {
// Parent filtering (bd-yqhh) // Parent filtering (bd-yqhh)
ParentID string `json:"parent_id,omitempty"` ParentID string `json:"parent_id,omitempty"`
// Wisp filtering (bd-bkul) // Ephemeral filtering (bd-bkul)
Wisp *bool `json:"wisp,omitempty"` Ephemeral *bool `json:"ephemeral,omitempty"`
} }
// CountArgs represents arguments for the count operation // CountArgs represents arguments for the count operation

View File

@@ -81,8 +81,8 @@ func updatesFromArgs(a UpdateArgs) map[string]interface{} {
if a.Sender != nil { if a.Sender != nil {
u["sender"] = *a.Sender u["sender"] = *a.Sender
} }
if a.Wisp != nil { if a.Ephemeral != nil {
u["wisp"] = *a.Wisp u["ephemeral"] = *a.Ephemeral
} }
if a.RepliesTo != nil { if a.RepliesTo != nil {
u["replies_to"] = *a.RepliesTo u["replies_to"] = *a.RepliesTo
@@ -176,8 +176,8 @@ func (s *Server) handleCreate(req *Request) Response {
EstimatedMinutes: createArgs.EstimatedMinutes, EstimatedMinutes: createArgs.EstimatedMinutes,
Status: types.StatusOpen, Status: types.StatusOpen,
// Messaging fields (bd-kwro) // Messaging fields (bd-kwro)
Sender: createArgs.Sender, Sender: createArgs.Sender,
Wisp: createArgs.Wisp, Ephemeral: createArgs.Ephemeral,
// NOTE: RepliesTo now handled via replies-to dependency (Decision 004) // NOTE: RepliesTo now handled via replies-to dependency (Decision 004)
// ID generation (bd-hobo) // ID generation (bd-hobo)
IDPrefix: createArgs.IDPrefix, IDPrefix: createArgs.IDPrefix,
@@ -844,8 +844,8 @@ func (s *Server) handleList(req *Request) Response {
filter.ParentID = &listArgs.ParentID filter.ParentID = &listArgs.ParentID
} }
// Wisp filtering (bd-bkul) // Ephemeral filtering (bd-bkul)
filter.Wisp = listArgs.Wisp filter.Ephemeral = listArgs.Ephemeral
// Guard against excessive ID lists to avoid SQLite parameter limits // Guard against excessive ID lists to avoid SQLite parameter limits
const maxIDs = 1000 const maxIDs = 1000
@@ -1475,7 +1475,7 @@ func (s *Server) handleGateCreate(req *Request) Response {
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 1, // Gates are typically high priority Priority: 1, // Gates are typically high priority
Assignee: "deacon/", Assignee: "deacon/",
Wisp: true, // Gates are wisps (ephemeral) Ephemeral: true, // Gates are wisps (ephemeral)
AwaitType: args.AwaitType, AwaitType: args.AwaitType,
AwaitID: args.AwaitID, AwaitID: args.AwaitID,
Timeout: args.Timeout, Timeout: args.Timeout,

View File

@@ -885,7 +885,7 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type
issue.Sender = sender.String issue.Sender = sender.String
} }
if wisp.Valid && wisp.Int64 != 0 { if wisp.Valid && wisp.Int64 != 0 {
issue.Wisp = true issue.Ephemeral = true
} }
// Pinned field (bd-7h5) // Pinned field (bd-7h5)
if pinned.Valid && pinned.Int64 != 0 { if pinned.Valid && pinned.Int64 != 0 {
@@ -1006,7 +1006,7 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows *
issue.Sender = sender.String issue.Sender = sender.String
} }
if wisp.Valid && wisp.Int64 != 0 { if wisp.Valid && wisp.Int64 != 0 {
issue.Wisp = true issue.Ephemeral = true
} }
// Pinned field (bd-7h5) // Pinned field (bd-7h5)
if pinned.Valid && pinned.Int64 != 0 { if pinned.Valid && pinned.Int64 != 0 {

View File

@@ -295,7 +295,7 @@ func TestRepliesTo(t *testing.T) {
IssueType: types.TypeMessage, IssueType: types.TypeMessage,
Sender: "alice", Sender: "alice",
Assignee: "bob", Assignee: "bob",
Wisp: true, Ephemeral: true,
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
@@ -307,7 +307,7 @@ func TestRepliesTo(t *testing.T) {
IssueType: types.TypeMessage, IssueType: types.TypeMessage,
Sender: "bob", Sender: "bob",
Assignee: "alice", Assignee: "alice",
Wisp: true, Ephemeral: true,
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
@@ -363,7 +363,7 @@ func TestRepliesTo_Chain(t *testing.T) {
IssueType: types.TypeMessage, IssueType: types.TypeMessage,
Sender: "user", Sender: "user",
Assignee: "inbox", Assignee: "inbox",
Wisp: true, Ephemeral: true,
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
@@ -415,7 +415,7 @@ func TestWispField(t *testing.T) {
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 2, Priority: 2,
IssueType: types.TypeMessage, IssueType: types.TypeMessage,
Wisp: true, Ephemeral: true,
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
@@ -426,7 +426,7 @@ func TestWispField(t *testing.T) {
Status: types.StatusOpen, Status: types.StatusOpen,
Priority: 2, Priority: 2,
IssueType: types.TypeTask, IssueType: types.TypeTask,
Wisp: false, Ephemeral: false,
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
@@ -443,7 +443,7 @@ func TestWispField(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("GetIssue failed: %v", err) t.Fatalf("GetIssue failed: %v", err)
} }
if !savedWisp.Wisp { if !savedWisp.Ephemeral {
t.Error("Wisp issue should have Wisp=true") t.Error("Wisp issue should have Wisp=true")
} }
@@ -451,7 +451,7 @@ func TestWispField(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("GetIssue failed: %v", err) t.Fatalf("GetIssue failed: %v", err)
} }
if savedPermanent.Wisp { if savedPermanent.Ephemeral {
t.Error("Permanent issue should have Wisp=false") t.Error("Permanent issue should have Wisp=false")
} }
} }
@@ -468,7 +468,7 @@ func TestWispFilter(t *testing.T) {
Status: types.StatusClosed, // Closed for cleanup test Status: types.StatusClosed, // Closed for cleanup test
Priority: 2, Priority: 2,
IssueType: types.TypeMessage, IssueType: types.TypeMessage,
Wisp: true, Ephemeral: true,
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
@@ -483,7 +483,7 @@ func TestWispFilter(t *testing.T) {
Status: types.StatusClosed, Status: types.StatusClosed,
Priority: 2, Priority: 2,
IssueType: types.TypeTask, IssueType: types.TypeTask,
Wisp: false, Ephemeral: false,
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
@@ -497,7 +497,7 @@ func TestWispFilter(t *testing.T) {
closedStatus := types.StatusClosed closedStatus := types.StatusClosed
wispFilter := types.IssueFilter{ wispFilter := types.IssueFilter{
Status: &closedStatus, Status: &closedStatus,
Wisp: &wispTrue, Ephemeral: &wispTrue,
} }
wispIssues, err := store.SearchIssues(ctx, "", wispFilter) wispIssues, err := store.SearchIssues(ctx, "", wispFilter)
@@ -512,7 +512,7 @@ func TestWispFilter(t *testing.T) {
wispFalse := false wispFalse := false
nonWispFilter := types.IssueFilter{ nonWispFilter := types.IssueFilter{
Status: &closedStatus, Status: &closedStatus,
Wisp: &wispFalse, Ephemeral: &wispFalse,
} }
permanentIssues, err := store.SearchIssues(ctx, "", nonWispFilter) permanentIssues, err := store.SearchIssues(ctx, "", nonWispFilter)

View File

@@ -28,7 +28,7 @@ func insertIssue(ctx context.Context, conn *sql.Conn, issue *types.Issue) error
} }
wisp := 0 wisp := 0
if issue.Wisp { if issue.Ephemeral {
wisp = 1 wisp = 1
} }
pinned := 0 pinned := 0
@@ -94,7 +94,7 @@ func insertIssues(ctx context.Context, conn *sql.Conn, issues []*types.Issue) er
} }
wisp := 0 wisp := 0
if issue.Wisp { if issue.Ephemeral {
wisp = 1 wisp = 1
} }
pinned := 0 pinned := 0

View File

@@ -282,7 +282,7 @@ func (s *SQLiteStorage) upsertIssueInTx(ctx context.Context, tx *sql.Tx, issue *
err := tx.QueryRowContext(ctx, `SELECT id FROM issues WHERE id = ?`, issue.ID).Scan(&existingID) err := tx.QueryRowContext(ctx, `SELECT id FROM issues WHERE id = ?`, issue.ID).Scan(&existingID)
wisp := 0 wisp := 0
if issue.Wisp { if issue.Ephemeral {
wisp = 1 wisp = 1
} }
pinned := 0 pinned := 0

View File

@@ -54,7 +54,7 @@ func (s *SQLiteStorage) ExportToMultiRepo(ctx context.Context) (map[string]int,
// Wisps exist only in SQLite and are shared via .beads/redirect, not JSONL. // Wisps exist only in SQLite and are shared via .beads/redirect, not JSONL.
filtered := make([]*types.Issue, 0, len(allIssues)) filtered := make([]*types.Issue, 0, len(allIssues))
for _, issue := range allIssues { for _, issue := range allIssues {
if !issue.Wisp { if !issue.Ephemeral {
filtered = append(filtered, issue) filtered = append(filtered, issue)
} }
} }

View File

@@ -349,7 +349,7 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
issue.Sender = sender.String issue.Sender = sender.String
} }
if wisp.Valid && wisp.Int64 != 0 { if wisp.Valid && wisp.Int64 != 0 {
issue.Wisp = true issue.Ephemeral = true
} }
// Pinned field (bd-7h5) // Pinned field (bd-7h5)
if pinned.Valid && pinned.Int64 != 0 { if pinned.Valid && pinned.Int64 != 0 {
@@ -562,7 +562,7 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
issue.Sender = sender.String issue.Sender = sender.String
} }
if wisp.Valid && wisp.Int64 != 0 { if wisp.Valid && wisp.Int64 != 0 {
issue.Wisp = true issue.Ephemeral = true
} }
// Pinned field (bd-7h5) // Pinned field (bd-7h5)
if pinned.Valid && pinned.Int64 != 0 { if pinned.Valid && pinned.Int64 != 0 {
@@ -1652,8 +1652,8 @@ func (s *SQLiteStorage) SearchIssues(ctx context.Context, query string, filter t
} }
// Wisp filtering (bd-kwro.9) // Wisp filtering (bd-kwro.9)
if filter.Wisp != nil { if filter.Ephemeral != nil {
if *filter.Wisp { if *filter.Ephemeral {
whereClauses = append(whereClauses, "ephemeral = 1") // SQL column is still 'ephemeral' whereClauses = append(whereClauses, "ephemeral = 1") // SQL column is still 'ephemeral'
} else { } else {
whereClauses = append(whereClauses, "(ephemeral = 0 OR ephemeral IS NULL)") whereClauses = append(whereClauses, "(ephemeral = 0 OR ephemeral IS NULL)")

View File

@@ -400,7 +400,7 @@ func (s *SQLiteStorage) GetStaleIssues(ctx context.Context, filter types.StaleFi
issue.Sender = sender.String issue.Sender = sender.String
} }
if ephemeral.Valid && ephemeral.Int64 != 0 { if ephemeral.Valid && ephemeral.Int64 != 0 {
issue.Wisp = true issue.Ephemeral = true
} }
// Pinned field (bd-7h5) // Pinned field (bd-7h5)
if pinned.Valid && pinned.Int64 != 0 { if pinned.Valid && pinned.Int64 != 0 {

View File

@@ -1089,8 +1089,8 @@ func (t *sqliteTxStorage) SearchIssues(ctx context.Context, query string, filter
} }
// Wisp filtering (bd-kwro.9) // Wisp filtering (bd-kwro.9)
if filter.Wisp != nil { if filter.Ephemeral != nil {
if *filter.Wisp { if *filter.Ephemeral {
whereClauses = append(whereClauses, "ephemeral = 1") // SQL column is still 'ephemeral' whereClauses = append(whereClauses, "ephemeral = 1") // SQL column is still 'ephemeral'
} else { } else {
whereClauses = append(whereClauses, "(ephemeral = 0 OR ephemeral IS NULL)") whereClauses = append(whereClauses, "(ephemeral = 0 OR ephemeral IS NULL)")
@@ -1244,7 +1244,7 @@ func scanIssueRow(row scanner) (*types.Issue, error) {
issue.Sender = sender.String issue.Sender = sender.String
} }
if wisp.Valid && wisp.Int64 != 0 { if wisp.Valid && wisp.Int64 != 0 {
issue.Wisp = true issue.Ephemeral = true
} }
// Pinned field (bd-7h5) // Pinned field (bd-7h5)
if pinned.Valid && pinned.Int64 != 0 { if pinned.Valid && pinned.Int64 != 0 {

View File

@@ -44,8 +44,8 @@ type Issue struct {
OriginalType string `json:"original_type,omitempty"` // Issue type before deletion (for tombstones) OriginalType string `json:"original_type,omitempty"` // Issue type before deletion (for tombstones)
// Messaging fields (bd-kwro): inter-agent communication support // Messaging fields (bd-kwro): inter-agent communication support
Sender string `json:"sender,omitempty"` // Who sent this (for messages) Sender string `json:"sender,omitempty"` // Who sent this (for messages)
Wisp bool `json:"wisp,omitempty"` // Wisp = ephemeral vapor from the Steam Engine; bulk-deleted when closed Ephemeral bool `json:"ephemeral,omitempty"` // If true, not exported to JSONL; bulk-deleted when closed
// NOTE: RepliesTo, RelatesTo, DuplicateOf, SupersededBy moved to dependencies table // NOTE: RepliesTo, RelatesTo, DuplicateOf, SupersededBy moved to dependencies table
// per Decision 004 (Edge Schema Consolidation). Use dependency API instead. // per Decision 004 (Edge Schema Consolidation). Use dependency API instead.
@@ -598,8 +598,8 @@ type IssueFilter struct {
// Tombstone filtering (bd-1bu) // Tombstone filtering (bd-1bu)
IncludeTombstones bool // If false (default), exclude tombstones from results IncludeTombstones bool // If false (default), exclude tombstones from results
// Wisp filtering (bd-kwro.9) // Ephemeral filtering (bd-kwro.9)
Wisp *bool // Filter by wisp flag (nil = any, true = only wisps, false = only non-wisps) Ephemeral *bool // Filter by ephemeral flag (nil = any, true = only ephemeral, false = only persistent)
// Pinned filtering (bd-7h5) // Pinned filtering (bd-7h5)
Pinned *bool // Filter by pinned flag (nil = any, true = only pinned, false = only non-pinned) Pinned *bool // Filter by pinned flag (nil = any, true = only pinned, false = only non-pinned)

View File

@@ -84,7 +84,7 @@ bd mol spawn mol-release --var version=2.0 # With variable substitution
**Chemistry shortcuts:** **Chemistry shortcuts:**
```bash ```bash
bd pour mol-feature # Shortcut for spawn --pour bd pour mol-feature # Shortcut for spawn --pour
bd wisp create mol-patrol # Explicit wisp creation bd ephemeral create mol-patrol # Explicit wisp creation
``` ```
### Spawn with Immediate Execution ### Spawn with Immediate Execution
@@ -164,7 +164,7 @@ bd mol bond mol-feature mol-deploy --as "Feature with Deploy"
### Creating Wisps ### Creating Wisps
```bash ```bash
bd wisp create mol-patrol # From proto bd ephemeral create mol-patrol # From proto
bd mol spawn mol-patrol # Same (spawn defaults to wisp) bd mol spawn mol-patrol # Same (spawn defaults to wisp)
bd mol spawn mol-check --var target=db # With variables bd mol spawn mol-check --var target=db # With variables
``` ```
@@ -172,8 +172,8 @@ bd mol spawn mol-check --var target=db # With variables
### Listing Wisps ### Listing Wisps
```bash ```bash
bd wisp list # List all wisps bd ephemeral list # List all wisps
bd wisp list --json # Machine-readable bd ephemeral list --json # Machine-readable
``` ```
### Ending Wisps ### Ending Wisps
@@ -198,7 +198,7 @@ Use burn for routine work with no archival value.
### Garbage Collection ### Garbage Collection
```bash ```bash
bd wisp gc # Clean up orphaned wisps bd ephemeral gc # Clean up orphaned wisps
``` ```
--- ---
@@ -289,7 +289,7 @@ bd mol spawn mol-weekly-review --pour
```bash ```bash
# Patrol proto exists # Patrol proto exists
bd wisp create mol-patrol bd ephemeral create mol-patrol
# Execute patrol work... # Execute patrol work...
@@ -328,9 +328,9 @@ bd mol distill bd-release-epic --as "Release Process" --var version=X.Y.Z
| `bd mol squash <mol>` | Compress wisp children to digest | | `bd mol squash <mol>` | Compress wisp children to digest |
| `bd mol burn <wisp>` | Delete wisp without trace | | `bd mol burn <wisp>` | Delete wisp without trace |
| `bd pour <proto>` | Shortcut for `spawn --pour` | | `bd pour <proto>` | Shortcut for `spawn --pour` |
| `bd wisp create <proto>` | Create ephemeral wisp | | `bd ephemeral create <proto>` | Create ephemeral wisp |
| `bd wisp list` | List all wisps | | `bd ephemeral list` | List all wisps |
| `bd wisp gc` | Garbage collect orphaned wisps | | `bd ephemeral gc` | Garbage collect orphaned wisps |
| `bd ship <capability>` | Publish capability for cross-project deps | | `bd ship <capability>` | Publish capability for cross-project deps |
--- ---
@@ -347,7 +347,7 @@ bd mol distill bd-release-epic --as "Release Process" --var version=X.Y.Z
**"Wisp commands fail"** **"Wisp commands fail"**
- Wisps stored in `.beads-wisp/` (separate from `.beads/`) - Wisps stored in `.beads-wisp/` (separate from `.beads/`)
- Check `bd wisp list` for active wisps - Check `bd ephemeral list` for active wisps
**"External dependency not satisfied"** **"External dependency not satisfied"**
- Target project must have closed issue with `provides:<capability>` label - Target project must have closed issue with `provides:<capability>` label