Fix comments display: move outside dependents block, merge main

This commit is contained in:
Steve Yegge
2025-12-27 00:11:35 -08:00
47 changed files with 597 additions and 285 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

@@ -353,7 +353,7 @@ func runCook(cmd *cobra.Command, args []string) {
if len(bondPoints) > 0 {
fmt.Printf(" Bond points: %s\n", strings.Join(bondPoints, ", "))
}
fmt.Printf("\nTo use: bd pour %s --var <name>=<value>\n", result.ProtoID)
fmt.Printf("\nTo use: bd mol pour %s --var <name>=<value>\n", result.ProtoID)
}
// cookFormulaResult holds the result of cooking

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

@@ -5,9 +5,11 @@ import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/routing"
"github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/types"
@@ -15,6 +17,14 @@ import (
"github.com/steveyegge/beads/internal/utils"
)
// getBeadsDir returns the .beads directory path, derived from the global dbPath.
func getBeadsDir() string {
if dbPath != "" {
return filepath.Dir(dbPath)
}
return ""
}
// isChildOf returns true if childID is a hierarchical child of parentID.
// For example, "bd-abc.1" is a child of "bd-abc", and "bd-abc.1.2" is a child of "bd-abc.1".
func isChildOf(childID, parentID string) bool {
@@ -88,9 +98,15 @@ Examples:
resolveArgs = &rpc.ResolveIDArgs{ID: args[1]}
resp, err = daemonClient.ResolveID(resolveArgs)
if err != nil {
FatalErrorRespectJSON("resolving dependency ID %s: %v", args[1], err)
}
if err := json.Unmarshal(resp.Data, &toID); err != nil {
// Resolution failed - try auto-converting to external ref (bd-lfiu)
beadsDir := getBeadsDir()
if extRef := routing.ResolveToExternalRef(args[1], beadsDir); extRef != "" {
toID = extRef
isExternalRef = true
} else {
FatalErrorRespectJSON("resolving dependency ID %s: %v", args[1], err)
}
} else if err := json.Unmarshal(resp.Data, &toID); err != nil {
FatalErrorRespectJSON("unmarshaling resolved ID: %v", err)
}
}
@@ -111,7 +127,14 @@ Examples:
} else {
toID, err = utils.ResolvePartialID(ctx, store, args[1])
if err != nil {
FatalErrorRespectJSON("resolving dependency ID %s: %v", args[1], err)
// Resolution failed - try auto-converting to external ref (bd-lfiu)
beadsDir := getBeadsDir()
if extRef := routing.ResolveToExternalRef(args[1], beadsDir); extRef != "" {
toID = extRef
isExternalRef = true
} else {
FatalErrorRespectJSON("resolving dependency ID %s: %v", args[1], err)
}
}
}
}

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

@@ -20,8 +20,8 @@ import (
// Usage:
// 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 mol pour <id> --var key=value # Instantiate proto → persistent mol
// bd mol wisp <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
@@ -48,14 +48,14 @@ The molecule metaphor:
- Distilling extracts a proto from an ad-hoc epic
Commands:
catalog List available protos
show Show proto/molecule structure and variables
bond Polymorphic combine: proto+proto, proto+mol, mol+mol
distill Extract proto from ad-hoc epic
See also:
bd pour <proto> # Instantiate as persistent mol (liquid phase)
bd wisp create <proto> # Instantiate as ephemeral wisp (vapor phase)`,
catalog List available protos
show Show proto/molecule structure and variables
pour Instantiate proto as persistent mol (liquid phase)
wisp Instantiate proto as ephemeral wisp (vapor phase)
bond Polymorphic combine: proto+proto, proto+mol, mol+mol
squash Condense molecule to digest
burn Discard wisp
distill Extract proto from ad-hoc epic`,
}
// =============================================================================
@@ -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 mol pour / bd mol wisp.
Formulas are ephemeral proto definitions stored as .formula.json files.
They are cooked inline when pouring, never stored as database beads.
@@ -92,12 +92,12 @@ Search paths (in priority order):
fmt.Println("\nOr distill from existing work:")
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 mol pour <formula-name> --var key=value # persistent mol")
fmt.Println(" bd mol wisp <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 mol pour / bd mol wisp):"))
// Group by type for display
byType := make(map[string][]CatalogEntry)

View File

@@ -100,7 +100,7 @@ The output shows all steps with status indicators:
}
fmt.Println(".")
fmt.Println("\nTo start work on a molecule:")
fmt.Println(" bd pour <proto-id> # Instantiate a molecule from template")
fmt.Println(" bd mol pour <proto-id> # Instantiate a molecule from template")
fmt.Println(" bd update <step-id> --status in_progress # Claim a step")
return
}

View File

@@ -225,7 +225,7 @@ func runMolDistill(cmd *cobra.Command, args []string) {
fmt.Printf(" Variables: %s\n", strings.Join(result.Variables, ", "))
}
fmt.Printf("\nTo instantiate:\n")
fmt.Printf(" bd pour %s", result.FormulaName)
fmt.Printf(" bd mol pour %s", result.FormulaName)
for _, v := range result.Variables {
fmt.Printf(" --var %s=<value>", v)
}

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

@@ -72,8 +72,11 @@ func initializeNoDbMode() error {
debug.Logf("using prefix '%s'", prefix)
// Set global store
// Set global store and mark as active (fixes bd comment --no-db)
storeMutex.Lock()
store = memStore
storeActive = true
storeMutex.Unlock()
return nil
}
@@ -218,7 +221,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

@@ -158,6 +158,90 @@ func TestDetectPrefix(t *testing.T) {
})
}
func TestInitializeNoDbMode_SetsStoreActive(t *testing.T) {
// This test verifies the fix for bd comment --no-db not working.
// The bug was that initializeNoDbMode() set `store` but not `storeActive`,
// so ensureStoreActive() would try to find a SQLite database.
tempDir := t.TempDir()
beadsDir := filepath.Join(tempDir, ".beads")
if err := os.MkdirAll(beadsDir, 0o755); err != nil {
t.Fatalf("Failed to create .beads dir: %v", err)
}
// Create a minimal JSONL file with one issue
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
content := `{"id":"bd-1","title":"Test Issue","status":"open"}
`
if err := os.WriteFile(jsonlPath, []byte(content), 0o600); err != nil {
t.Fatalf("Failed to write JSONL: %v", err)
}
// Save and restore global state
oldStore := store
oldStoreActive := storeActive
oldCwd, _ := os.Getwd()
defer func() {
storeMutex.Lock()
store = oldStore
storeActive = oldStoreActive
storeMutex.Unlock()
_ = os.Chdir(oldCwd)
}()
// Change to temp dir so initializeNoDbMode finds .beads
if err := os.Chdir(tempDir); err != nil {
t.Fatalf("Failed to chdir: %v", err)
}
// Reset global state
storeMutex.Lock()
store = nil
storeActive = false
storeMutex.Unlock()
// Initialize no-db mode
if err := initializeNoDbMode(); err != nil {
t.Fatalf("initializeNoDbMode failed: %v", err)
}
// Verify storeActive is now true
storeMutex.Lock()
active := storeActive
s := store
storeMutex.Unlock()
if !active {
t.Error("storeActive should be true after initializeNoDbMode")
}
if s == nil {
t.Fatal("store should not be nil after initializeNoDbMode")
}
// ensureStoreActive should now return immediately without error
if err := ensureStoreActive(); err != nil {
t.Errorf("ensureStoreActive should succeed after initializeNoDbMode: %v", err)
}
// Verify comments work (this was the failing case)
ctx := rootCtx
comment, err := s.AddIssueComment(ctx, "bd-1", "testuser", "Test comment")
if err != nil {
t.Fatalf("AddIssueComment failed: %v", err)
}
if comment.Text != "Test comment" {
t.Errorf("Expected 'Test comment', got %s", comment.Text)
}
comments, err := s.GetIssueComments(ctx, "bd-1")
if err != nil {
t.Fatalf("GetIssueComments failed: %v", err)
}
if len(comments) != 1 {
t.Errorf("Expected 1 comment, got %d", len(comments))
}
}
func TestWriteIssuesToJSONL(t *testing.T) {
tempDir := t.TempDir()
beadsDir := filepath.Join(tempDir, ".beads")

View File

@@ -32,9 +32,9 @@ Use pour for:
- Anything you might need to reference later
Examples:
bd pour mol-feature --var name=auth # Create persistent mol from proto
bd pour mol-release --var version=1.0 # Release workflow
bd pour mol-review --var pr=123 # Code review workflow`,
bd mol pour mol-feature --var name=auth # Create persistent mol from proto
bd mol pour mol-release --var version=1.0 # Release workflow
bd mol pour mol-review --var pr=123 # Code review workflow`,
Args: cobra.ExactArgs(1),
Run: runPour,
}
@@ -260,5 +260,5 @@ func init() {
pourCmd.Flags().StringSlice("attach", []string{}, "Proto to attach after spawning (repeatable)")
pourCmd.Flags().String("attach-type", types.BondTypeSequential, "Bond type for attachments: sequential, parallel, or conditional")
rootCmd.AddCommand(pourCmd)
molCmd.AddCommand(pourCmd)
}

View File

@@ -305,20 +305,20 @@ var showCmd = &cobra.Command{
fmt.Printf(" ◊ %s: %s [P%d - %s]\n", dep.ID, dep.Title, dep.Priority, dep.Status)
}
}
}
if len(details.Comments) > 0 {
fmt.Printf("\nComments (%d):\n", len(details.Comments))
for _, comment := range details.Comments {
fmt.Printf(" [%s] %s\n", comment.Author, comment.CreatedAt.Format("2006-01-02 15:04"))
commentLines := strings.Split(comment.Text, "\n")
for _, line := range commentLines {
fmt.Printf(" %s\n", line)
}
if len(details.Comments) > 0 {
fmt.Printf("\nComments (%d):\n", len(details.Comments))
for _, comment := range details.Comments {
fmt.Printf(" [%s] %s\n", comment.Author, comment.CreatedAt.Format("2006-01-02 15:04"))
commentLines := strings.Split(comment.Text, "\n")
for _, line := range commentLines {
fmt.Printf(" %s\n", line)
}
}
}
}
fmt.Println()
fmt.Println()
}
}

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

@@ -18,33 +18,43 @@ import (
// Wisp commands - manage ephemeral molecules
//
// Wisps are ephemeral issues with Wisp=true in the main database.
// Wisps are ephemeral issues with Ephemeral=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 mol wisp list - List all wisps in current context
// bd mol wisp gc - Garbage collect orphaned wisps
var wispCmd = &cobra.Command{
Use: "wisp",
Short: "Manage ephemeral molecules (wisps)",
Long: `Manage wisps - ephemeral molecules for operational workflows.
Use: "wisp [proto-id]",
Short: "Create or manage wisps (ephemeral molecules)",
Long: `Create or manage wisps - ephemeral molecules for operational workflows.
Wisps are issues with Wisp=true in the main database. They're stored
When called with a proto-id argument, creates a wisp from that proto.
When called with a subcommand (list, gc), manages existing wisps.
Wisps are issues with Ephemeral=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
3. Squash: bd mol squash <id> (clears Wisp flag, promotes to persistent)
4. Or burn: bd mol burn <id> (deletes wisp without creating digest)
1. Create: bd mol wisp <proto> or bd create --ephemeral
2. Execute: Normal bd operations work on wisp issues
3. Squash: bd mol squash <id> (clears Ephemeral flag, promotes to persistent)
4. Or burn: bd mol burn <id> (deletes without creating digest)
Commands:
Examples:
bd mol wisp mol-patrol # Create wisp from proto
bd mol wisp list # List all wisps
bd mol wisp gc # Garbage collect old wisps
Subcommands:
list List all wisps in current context
gc Garbage collect orphaned wisps`,
Args: cobra.MaximumNArgs(1),
Run: runWisp,
}
// WispListItem represents a wisp in list output
@@ -68,32 +78,44 @@ 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
// runWisp handles the wisp command when called directly with a proto-id
// It delegates to runWispCreate for the actual work
func runWisp(cmd *cobra.Command, args []string) {
if len(args) == 0 {
// No proto-id provided, show help
cmd.Help()
return
}
// Delegate to the create logic
runWispCreate(cmd, args)
}
// wispCreateCmd instantiates a proto as an ephemeral wisp (kept for backwards compat)
var wispCreateCmd = &cobra.Command{
Use: "create <proto-id>",
Short: "Instantiate a proto as an ephemeral wisp (solid -> vapor)",
Short: "Instantiate a proto as a wisp (solid -> vapor)",
Long: `Create a wisp from a proto - sublimation from solid to vapor.
This is the chemistry-inspired command for creating ephemeral work from templates.
The resulting wisp is stored in the main database with Wisp=true and NOT exported to JSONL.
The resulting wisp is stored in the main database with Ephemeral=true and NOT exported to JSONL.
Phase transition: Proto (solid) -> Wisp (vapor)
Use wisp create for:
Use wisp for:
- Patrol cycles (deacon, witness)
- Health checks and monitoring
- One-shot orchestration runs
- Routine operations with no audit value
The wisp will:
- Be stored in main database with Wisp=true flag
- Be stored in main database with Ephemeral=true flag
- NOT be exported to JSONL (and thus not synced via git)
- 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 mol wisp create mol-patrol # Ephemeral patrol cycle
bd mol wisp create mol-health-check # One-time health check
bd mol wisp create mol-diagnostics --var target=db # Diagnostic run`,
Args: cobra.ExactArgs(1),
Run: runWispCreate,
}
@@ -107,7 +129,7 @@ func runWispCreate(cmd *cobra.Command, args []string) {
if store == nil {
if daemonClient != nil {
fmt.Fprintf(os.Stderr, "Error: wisp create requires direct database access\n")
fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon wisp create %s ...\n", args[0])
fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon mol wisp %s ...\n", args[0])
} else {
fmt.Fprintf(os.Stderr, "Error: no database connection\n")
}
@@ -215,7 +237,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,15 +245,15 @@ 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)
os.Exit(1)
}
// Wisps are in main db but don't trigger JSONL export (Wisp flag excludes them)
// Wisp issues are in main db but don't trigger JSONL export (Ephemeral flag excludes them)
if jsonOutput {
type wispCreateResult struct {
@@ -286,9 +308,9 @@ func resolvePartialIDDirect(ctx context.Context, partial string) (string, error)
var wispListCmd = &cobra.Command{
Use: "list",
Short: "List all wisps in current context",
Long: `List all ephemeral molecules (wisps) in the current context.
Long: `List all wisps (ephemeral molecules) in the current context.
Wisps are issues with Wisp=true in the main database. They are stored
Wisps are issues with Ephemeral=true in the main database. They are stored
locally but not exported to JSONL (and thus not synced via git).
The list shows:
@@ -300,12 +322,12 @@ The list shows:
Old wisp detection:
- Old wisps haven't been updated in 24+ hours
- Use 'bd wisp gc' to clean up old/abandoned wisps
- Use 'bd mol wisp gc' to clean up old/abandoned wisps
Examples:
bd wisp list # List all wisps
bd wisp list --json # JSON output for programmatic use
bd wisp list --all # Include closed wisps`,
bd mol wisp list # List all wisps
bd mol wisp list --json # JSON output for programmatic use
bd mol wisp list --all # Include closed wisps`,
Run: runWispList,
}
@@ -327,15 +349,15 @@ func runWispList(cmd *cobra.Command, args []string) {
return
}
// Query wisps from main database using Wisp filter
wispFlag := true
// Query wisps from main database using Ephemeral 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,7 +369,7 @@ func runWispList(cmd *cobra.Command, args []string) {
} else {
// Direct database access
filter := types.IssueFilter{
Wisp: &wispFlag,
Ephemeral: &ephemeralFlag,
}
issues, err = store.SearchIssues(ctx, "", filter)
}
@@ -444,7 +466,7 @@ func runWispList(cmd *cobra.Command, args []string) {
if oldCount > 0 {
fmt.Printf("\n%s %d old wisp(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 mol wisp gc' to clean up old wisps")
}
}
@@ -493,10 +515,10 @@ Note: This uses time-based cleanup, appropriate for ephemeral wisps.
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`,
bd mol wisp gc # Clean abandoned wisps (default: 1h threshold)
bd mol wisp gc --dry-run # Preview what would be cleaned
bd mol wisp gc --age 24h # Custom age threshold
bd mol wisp gc --all # Also clean closed wisps older than threshold`,
Run: runWispGC,
}
@@ -532,17 +554,17 @@ func runWispGC(cmd *cobra.Command, args []string) {
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, "Hint: use --no-daemon flag: bd --no-daemon mol wisp gc\n")
} else {
fmt.Fprintf(os.Stderr, "Error: no database connection\n")
}
os.Exit(1)
}
// Query wisps from main database using Wisp filter
wispFlag := true
// Query wisps from main database using Ephemeral filter
ephemeralFlag := true
filter := types.IssueFilter{
Wisp: &wispFlag,
Ephemeral: &ephemeralFlag,
}
issues, err := store.SearchIssues(ctx, "", filter)
if err != nil {
@@ -634,7 +656,11 @@ func runWispGC(cmd *cobra.Command, args []string) {
}
func init() {
// Wisp create command flags
// Wisp command flags (for direct create: bd mol wisp <proto>)
wispCmd.Flags().StringSlice("var", []string{}, "Variable substitution (key=value)")
wispCmd.Flags().Bool("dry-run", false, "Preview what would be created")
// Wisp create command flags (kept for backwards compat: bd mol wisp create <proto>)
wispCreateCmd.Flags().StringSlice("var", []string{}, "Variable substitution (key=value)")
wispCreateCmd.Flags().Bool("dry-run", false, "Preview what would be created")
@@ -647,5 +673,5 @@ func init() {
wispCmd.AddCommand(wispCreateCmd)
wispCmd.AddCommand(wispListCmd)
wispCmd.AddCommand(wispGCCmd)
rootCmd.AddCommand(wispCmd)
molCmd.AddCommand(wispCmd)
}