Fix comments display: move outside dependents block, merge main
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
120
cmd/bd/wisp.go
120
cmd/bd/wisp.go
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user