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))
wispsSkipped := 0
for _, issue := range issueMap {
if issue.Wisp {
if issue.Ephemeral {
wispsSkipped++
continue
}

View File

@@ -15,7 +15,7 @@ type CleanupEmptyResponse struct {
DeletedCount int `json:"deleted_count"`
Message string `json:"message"`
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
@@ -56,7 +56,7 @@ Delete issues closed more than 30 days ago:
bd cleanup --older-than 30 --force
Delete only closed wisps (transient molecules):
bd cleanup --wisp --force
bd cleanup --ephemeral --force
Preview what would be deleted/pruned:
bd cleanup --dry-run
@@ -80,7 +80,7 @@ SEE ALSO:
cascade, _ := cmd.Flags().GetBool("cascade")
olderThanDays, _ := cmd.Flags().GetInt("older-than")
hardDelete, _ := cmd.Flags().GetBool("hard")
wispOnly, _ := cmd.Flags().GetBool("wisp")
wispOnly, _ := cmd.Flags().GetBool("ephemeral")
// Calculate custom TTL for --hard mode
// 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)
if wispOnly {
wispTrue := true
filter.Wisp = &wispTrue
filter.Ephemeral = &wispTrue
}
// Get all closed issues matching filter
@@ -165,7 +165,7 @@ SEE ALSO:
result.Filter = fmt.Sprintf("older than %d days", olderThanDays)
}
if wispOnly {
result.Wisp = true
result.Ephemeral = true
}
outputJSON(result)
} else {
@@ -270,6 +270,6 @@ func init() {
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().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)
}

View File

@@ -107,7 +107,7 @@ var createCmd = &cobra.Command{
waitsForGate, _ := cmd.Flags().GetString("waits-for-gate")
forceCreate, _ := cmd.Flags().GetBool("force")
repoOverride, _ := cmd.Flags().GetString("repo")
wisp, _ := cmd.Flags().GetBool("wisp")
wisp, _ := cmd.Flags().GetBool("ephemeral")
// Get estimate if provided
var estimatedMinutes *int
@@ -222,7 +222,7 @@ var createCmd = &cobra.Command{
Dependencies: deps,
WaitsFor: waitsFor,
WaitsForGate: waitsForGate,
Wisp: wisp,
Ephemeral: wisp,
CreatedBy: getActorWithGit(),
}
@@ -268,7 +268,7 @@ var createCmd = &cobra.Command{
Assignee: assignee,
ExternalRef: externalRefPtr,
EstimatedMinutes: estimatedMinutes,
Wisp: wisp,
Ephemeral: wisp,
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().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().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
rootCmd.AddCommand(createCmd)
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -292,6 +292,7 @@ var versionChanges = []VersionChange{
Version: "0.37.0",
Date: "2025-12-26",
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 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",

View File

@@ -21,7 +21,7 @@ import (
// bd mol catalog # List available protos
// bd mol show <id> # Show proto/molecule structure
// 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)
// Molecules use the same label as templates - they ARE templates with workflow semantics
@@ -55,7 +55,7 @@ Commands:
See also:
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,
Assignee: assignee,
Actor: actorName,
Wisp: ephemeral,
Ephemeral: ephemeral,
Prefix: prefix,
}
return cloneSubgraph(ctx, s, subgraph, opts)

View File

@@ -40,12 +40,12 @@ Bond types:
Phase control:
By default, spawned protos follow the target's phase:
- Attaching to mol (Wisp=false) → spawns as persistent (Wisp=false)
- Attaching to wisp (Wisp=true) → spawns as ephemeral (Wisp=true)
- Attaching to mol (Ephemeral=false) → spawns as persistent (Ephemeral=false)
- Attaching to ephemeral issue (Ephemeral=true) → spawns as ephemeral (Ephemeral=true)
Override with:
--pour Force spawn as liquid (persistent, Wisp=false)
--wisp Force spawn as vapor (ephemeral, Wisp=true, excluded from JSONL export)
--pour Force spawn as liquid (persistent, Ephemeral=false)
--ephemeral Force spawn as vapor (ephemeral, Ephemeral=true, excluded from JSONL export)
Dynamic bonding (Christmas Ornament pattern):
Use --ref to specify a custom child reference with variable substitution.
@@ -57,7 +57,7 @@ Dynamic bonding (Christmas Ornament pattern):
Use cases:
- 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
Examples:
@@ -66,7 +66,7 @@ Examples:
bd mol bond mol-feature bd-abc123 # Attach proto to molecule
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-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`,
Args: cobra.ExactArgs(2),
Run: runMolBond,
@@ -102,20 +102,20 @@ func runMolBond(cmd *cobra.Command, args []string) {
customTitle, _ := cmd.Flags().GetString("as")
dryRun, _ := cmd.Flags().GetBool("dry-run")
varFlags, _ := cmd.Flags().GetStringSlice("var")
wisp, _ := cmd.Flags().GetBool("wisp")
ephemeral, _ := cmd.Flags().GetBool("ephemeral")
pour, _ := cmd.Flags().GetBool("pour")
childRef, _ := cmd.Flags().GetString("ref")
// Validate phase flags are not both set
if wisp && pour {
fmt.Fprintf(os.Stderr, "Error: cannot use both --wisp and --pour\n")
if ephemeral && pour {
fmt.Fprintf(os.Stderr, "Error: cannot use both --ephemeral and --pour\n")
os.Exit(1)
}
// All issues go in the main store; wisp vs pour determines the Wisp flag
// --wisp: create with Wisp=true (ephemeral, excluded from JSONL export)
// --pour: create with Wisp=false (persistent, exported to JSONL)
// Default: follow target's phase (wisp if target is wisp, otherwise persistent)
// All issues go in the main store; ephemeral vs pour determines the Wisp flag
// --ephemeral: create with Ephemeral=true (ephemeral, excluded from JSONL export)
// --pour: create with Ephemeral=false (persistent, exported to JSONL)
// Default: follow target's phase (ephemeral if target is ephemeral, otherwise persistent)
// Validate bond type
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(" Bond type: %s\n", bondType)
if wisp {
fmt.Printf(" Phase override: vapor (--wisp)\n")
if ephemeral {
fmt.Printf(" Phase override: vapor (--ephemeral)\n")
} else if pour {
fmt.Printf(" Phase override: liquid (--pour)\n")
}
@@ -240,16 +240,16 @@ func runMolBond(cmd *cobra.Command, args []string) {
case aIsProto && !bIsProto:
// Pass subgraph directly if cooked from formula
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 {
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:
// Pass subgraph directly if cooked from formula
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 {
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:
result, err = bondMolMol(ctx, store, issueA, issueB, bondType, actor)
@@ -273,10 +273,10 @@ func runMolBond(cmd *cobra.Command, args []string) {
if result.Spawned > 0 {
fmt.Printf(" Spawned: %d issues\n", result.Spawned)
}
if wisp {
fmt.Printf(" Phase: vapor (ephemeral, Wisp=true)\n")
if ephemeral {
fmt.Printf(" Phase: vapor (ephemeral, Ephemeral=true)\n")
} 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.
// 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.
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) {
return bondProtoMolWithSubgraph(ctx, s, nil, proto, mol, bondType, vars, childRef, actorName, wispFlag, pourFlag)
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, ephemeralFlag, pourFlag)
}
// 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
subgraph := protoSubgraph
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, ", "))
}
// Determine wisp flag based on explicit flags or target's phase
// --wisp: force wisp=true, --pour: force wisp=false, neither: follow target
makeWisp := mol.Wisp // Default: follow target's phase
if wispFlag {
makeWisp = true
// Determine ephemeral flag based on explicit flags or target's phase
// --ephemeral: force ephemeral=true, --pour: force ephemeral=false, neither: follow target
makeEphemeral := mol.Ephemeral // Default: follow target's phase
if ephemeralFlag {
makeEphemeral = true
} else if pourFlag {
makeWisp = false
makeEphemeral = false
}
// Build CloneOptions for spawning
opts := CloneOptions{
Vars: vars,
Actor: actorName,
Wisp: makeWisp,
Ephemeral: makeEphemeral,
}
// 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)
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
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
@@ -630,8 +630,8 @@ func init() {
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().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("pour", false, "Force spawn as liquid (persistent, Wisp=false)")
molBondCmd.Flags().Bool("ephemeral", false, "Force spawn as vapor (ephemeral, Ephemeral=true)")
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}})")
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
The burn operation:
1. Verifies the molecule has Wisp=true (is ephemeral)
2. Deletes the molecule and all its wisp children
1. Verifies the molecule has Ephemeral=true (is ephemeral)
2. Deletes the molecule and all its ephemeral children
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
@@ -81,8 +81,8 @@ func runMolBurn(cmd *cobra.Command, args []string) {
}
// Verify it's a wisp
if !rootIssue.Wisp {
fmt.Fprintf(os.Stderr, "Error: molecule %s is not a wisp (Wisp=false)\n", resolvedID)
if !rootIssue.Ephemeral {
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, " Use 'bd delete' to remove non-wisp issues\n")
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)
var wispIDs []string
for _, issue := range subgraph.Issues {
if issue.Wisp {
if issue.Ephemeral {
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("\nWisp issues to delete (%d total):\n", len(wispIDs))
for _, issue := range subgraph.Issues {
if !issue.Wisp {
if !issue.Ephemeral {
continue
}
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(" Wisp: %s\n", resolvedID)
fmt.Printf(" Ephemeral: %s\n", resolvedID)
fmt.Printf(" No digest created.\n")
}

View File

@@ -23,7 +23,7 @@ var molCatalogCmd = &cobra.Command{
Use: "catalog",
Aliases: []string{"list", "ls"},
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.
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("\nTo instantiate from formula:")
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
}
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
byType := make(map[string][]CatalogEntry)

View File

@@ -18,17 +18,17 @@ import (
var molSquashCmd = &cobra.Command{
Use: "squash <molecule-id>",
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
clearing their Wisp flag (or optionally deletes them).
The squash operation:
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)
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)
OR deletes them with --delete-children
@@ -95,13 +95,13 @@ func runMolSquash(cmd *cobra.Command, args []string) {
os.Exit(1)
}
// Filter to only wisp children (exclude root)
// Filter to only ephemeral children (exclude root)
var wispChildren []*types.Issue
for _, issue := range subgraph.Issues {
if issue.ID == subgraph.Root.ID {
continue // Skip root
}
if issue.Wisp {
if issue.Ephemeral {
wispChildren = append(wispChildren, issue)
}
}
@@ -113,13 +113,13 @@ func runMolSquash(cmd *cobra.Command, args []string) {
SquashedCount: 0,
})
} else {
fmt.Printf("No wisp children found for molecule %s\n", moleculeID)
fmt.Printf("No ephemeral children found for molecule %s\n", moleculeID)
}
return
}
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("\nWisp children to squash:\n")
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)),
Priority: root.Priority,
IssueType: types.TypeTask,
Wisp: false, // Digest is permanent, not a wisp
Ephemeral: false, // Digest is permanent, not a wisp
ClosedAt: &now,
}
@@ -283,7 +283,7 @@ func squashMolecule(ctx context.Context, s storage.Storage, root *types.Issue, c
return nil, err
}
// Delete wisp children (outside transaction for better error handling)
// Delete ephemeral children (outside transaction for better error handling)
if !keepChildren {
deleted, err := deleteWispChildren(ctx, s, childIDs)
if err != nil {
@@ -319,7 +319,7 @@ func deleteWispChildren(ctx context.Context, s storage.Storage, ids []string) (i
func init() {
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)")
molCmd.AddCommand(molSquashCmd)

View File

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

View File

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

View File

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

View File

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

View File

@@ -202,7 +202,7 @@ The 1-hour grace period ensures tombstones propagate even with minor clock drift
## 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

View File

@@ -129,7 +129,7 @@ For reusable workflows, beads uses a chemistry metaphor:
```bash
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 burn <id> # Wisp → nothing (discard)
```
@@ -227,10 +227,10 @@ bd close <id> --reason "Done"
Wisps accumulate if not squashed/burned:
```bash
bd wisp list # Check for orphans
bd ephemeral list # Check for orphans
bd mol squash <id> # Create digest
bd mol burn <id> # Or discard
bd wisp gc # Garbage collect old wisps
bd ephemeral gc # Garbage collect old wisps
```
## Layer Cake Architecture
@@ -273,7 +273,7 @@ bd dep tree <id> # Show dependency tree
```bash
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 squash <id> # Compress to digest
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
WaitsForGate string `json:"waits_for_gate,omitempty"` // Gate type: all-children or any-children
// Messaging fields (bd-kwro)
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
Sender string `json:"sender,omitempty"` // Who sent this (for messages)
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
// 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
}
@@ -115,8 +115,8 @@ type UpdateArgs struct {
RemoveLabels []string `json:"remove_labels,omitempty"`
SetLabels []string `json:"set_labels,omitempty"`
// Messaging fields (bd-kwro)
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
Sender *string `json:"sender,omitempty"` // Who sent this (for messages)
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
// Graph link fields (bd-fu83)
RelatesTo *string `json:"relates_to,omitempty"` // JSON array of related issue IDs
@@ -193,8 +193,8 @@ type ListArgs struct {
// Parent filtering (bd-yqhh)
ParentID string `json:"parent_id,omitempty"`
// Wisp filtering (bd-bkul)
Wisp *bool `json:"wisp,omitempty"`
// Ephemeral filtering (bd-bkul)
Ephemeral *bool `json:"ephemeral,omitempty"`
}
// CountArgs represents arguments for the count operation

View File

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

View File

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

View File

@@ -295,7 +295,7 @@ func TestRepliesTo(t *testing.T) {
IssueType: types.TypeMessage,
Sender: "alice",
Assignee: "bob",
Wisp: true,
Ephemeral: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
@@ -307,7 +307,7 @@ func TestRepliesTo(t *testing.T) {
IssueType: types.TypeMessage,
Sender: "bob",
Assignee: "alice",
Wisp: true,
Ephemeral: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
@@ -363,7 +363,7 @@ func TestRepliesTo_Chain(t *testing.T) {
IssueType: types.TypeMessage,
Sender: "user",
Assignee: "inbox",
Wisp: true,
Ephemeral: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
@@ -415,7 +415,7 @@ func TestWispField(t *testing.T) {
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeMessage,
Wisp: true,
Ephemeral: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
@@ -426,7 +426,7 @@ func TestWispField(t *testing.T) {
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
Wisp: false,
Ephemeral: false,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
@@ -443,7 +443,7 @@ func TestWispField(t *testing.T) {
if err != nil {
t.Fatalf("GetIssue failed: %v", err)
}
if !savedWisp.Wisp {
if !savedWisp.Ephemeral {
t.Error("Wisp issue should have Wisp=true")
}
@@ -451,7 +451,7 @@ func TestWispField(t *testing.T) {
if err != nil {
t.Fatalf("GetIssue failed: %v", err)
}
if savedPermanent.Wisp {
if savedPermanent.Ephemeral {
t.Error("Permanent issue should have Wisp=false")
}
}
@@ -468,7 +468,7 @@ func TestWispFilter(t *testing.T) {
Status: types.StatusClosed, // Closed for cleanup test
Priority: 2,
IssueType: types.TypeMessage,
Wisp: true,
Ephemeral: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
@@ -483,7 +483,7 @@ func TestWispFilter(t *testing.T) {
Status: types.StatusClosed,
Priority: 2,
IssueType: types.TypeTask,
Wisp: false,
Ephemeral: false,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
@@ -497,7 +497,7 @@ func TestWispFilter(t *testing.T) {
closedStatus := types.StatusClosed
wispFilter := types.IssueFilter{
Status: &closedStatus,
Wisp: &wispTrue,
Ephemeral: &wispTrue,
}
wispIssues, err := store.SearchIssues(ctx, "", wispFilter)
@@ -512,7 +512,7 @@ func TestWispFilter(t *testing.T) {
wispFalse := false
nonWispFilter := types.IssueFilter{
Status: &closedStatus,
Wisp: &wispFalse,
Ephemeral: &wispFalse,
}
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
if issue.Wisp {
if issue.Ephemeral {
wisp = 1
}
pinned := 0
@@ -94,7 +94,7 @@ func insertIssues(ctx context.Context, conn *sql.Conn, issues []*types.Issue) er
}
wisp := 0
if issue.Wisp {
if issue.Ephemeral {
wisp = 1
}
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)
wisp := 0
if issue.Wisp {
if issue.Ephemeral {
wisp = 1
}
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.
filtered := make([]*types.Issue, 0, len(allIssues))
for _, issue := range allIssues {
if !issue.Wisp {
if !issue.Ephemeral {
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
}
if wisp.Valid && wisp.Int64 != 0 {
issue.Wisp = true
issue.Ephemeral = true
}
// Pinned field (bd-7h5)
if pinned.Valid && pinned.Int64 != 0 {
@@ -562,7 +562,7 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
issue.Sender = sender.String
}
if wisp.Valid && wisp.Int64 != 0 {
issue.Wisp = true
issue.Ephemeral = true
}
// Pinned field (bd-7h5)
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)
if filter.Wisp != nil {
if *filter.Wisp {
if filter.Ephemeral != nil {
if *filter.Ephemeral {
whereClauses = append(whereClauses, "ephemeral = 1") // SQL column is still 'ephemeral'
} else {
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
}
if ephemeral.Valid && ephemeral.Int64 != 0 {
issue.Wisp = true
issue.Ephemeral = true
}
// Pinned field (bd-7h5)
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)
if filter.Wisp != nil {
if *filter.Wisp {
if filter.Ephemeral != nil {
if *filter.Ephemeral {
whereClauses = append(whereClauses, "ephemeral = 1") // SQL column is still 'ephemeral'
} else {
whereClauses = append(whereClauses, "(ephemeral = 0 OR ephemeral IS NULL)")
@@ -1244,7 +1244,7 @@ func scanIssueRow(row scanner) (*types.Issue, error) {
issue.Sender = sender.String
}
if wisp.Valid && wisp.Int64 != 0 {
issue.Wisp = true
issue.Ephemeral = true
}
// Pinned field (bd-7h5)
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)
// Messaging fields (bd-kwro): inter-agent communication support
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
Sender string `json:"sender,omitempty"` // Who sent this (for messages)
Ephemeral bool `json:"ephemeral,omitempty"` // If true, not exported to JSONL; bulk-deleted when closed
// NOTE: RepliesTo, RelatesTo, DuplicateOf, SupersededBy moved to dependencies table
// per Decision 004 (Edge Schema Consolidation). Use dependency API instead.
@@ -598,8 +598,8 @@ type IssueFilter struct {
// Tombstone filtering (bd-1bu)
IncludeTombstones bool // If false (default), exclude tombstones from results
// Wisp filtering (bd-kwro.9)
Wisp *bool // Filter by wisp flag (nil = any, true = only wisps, false = only non-wisps)
// Ephemeral filtering (bd-kwro.9)
Ephemeral *bool // Filter by ephemeral flag (nil = any, true = only ephemeral, false = only persistent)
// Pinned filtering (bd-7h5)
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:**
```bash
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
@@ -164,7 +164,7 @@ bd mol bond mol-feature mol-deploy --as "Feature with Deploy"
### Creating Wisps
```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-check --var target=db # With variables
```
@@ -172,8 +172,8 @@ bd mol spawn mol-check --var target=db # With variables
### Listing Wisps
```bash
bd wisp list # List all wisps
bd wisp list --json # Machine-readable
bd ephemeral list # List all wisps
bd ephemeral list --json # Machine-readable
```
### Ending Wisps
@@ -198,7 +198,7 @@ Use burn for routine work with no archival value.
### Garbage Collection
```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
# Patrol proto exists
bd wisp create mol-patrol
bd ephemeral create mol-patrol
# 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 burn <wisp>` | Delete wisp without trace |
| `bd pour <proto>` | Shortcut for `spawn --pour` |
| `bd wisp create <proto>` | Create ephemeral wisp |
| `bd wisp list` | List all wisps |
| `bd wisp gc` | Garbage collect orphaned wisps |
| `bd ephemeral create <proto>` | Create ephemeral wisp |
| `bd ephemeral list` | List all wisps |
| `bd ephemeral gc` | Garbage collect orphaned wisps |
| `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"**
- 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"**
- Target project must have closed issue with `provides:<capability>` label