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