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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -275,7 +275,7 @@ open ──▶ in_progress ──▶ closed
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ bd wisp create │───▶│ Wisp Issues │───▶│ bd mol squash │
|
||||
│ bd ephemeral create │───▶│ Wisp Issues │───▶│ bd mol squash │
|
||||
│ (from template) │ │ (local-only) │ │ (→ digest) │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
@@ -351,7 +351,7 @@ Beads uses a chemistry metaphor for template-based workflows. See [MOLECULES.md]
|
||||
|-------|-------|---------|---------|
|
||||
| Solid | Proto | `.beads/` | `bd mol catalog` |
|
||||
| Liquid | Mol | `.beads/` | `bd pour` |
|
||||
| Vapor | Wisp | `.beads/` (Wisp=true, not exported) | `bd wisp create` |
|
||||
| Vapor | Wisp | `.beads/` (Wisp=true, not exported) | `bd ephemeral create` |
|
||||
|
||||
### Proto/Template Commands
|
||||
|
||||
@@ -385,17 +385,17 @@ bd pour <proto-id> --attach <other-proto> --json
|
||||
### Wisp Commands
|
||||
|
||||
```bash
|
||||
# Instantiate proto as ephemeral wisp (solid → vapor)
|
||||
bd wisp create <proto-id> --var key=value --json
|
||||
# Instantiate proto as ephemeral issue (solid → vapor)
|
||||
bd ephemeral create <proto-id> --var key=value --json
|
||||
|
||||
# List all wisps
|
||||
bd wisp list --json
|
||||
bd wisp list --all --json # Include closed
|
||||
bd ephemeral list --json
|
||||
bd ephemeral list --all --json # Include closed
|
||||
|
||||
# Garbage collect orphaned wisps
|
||||
bd wisp gc --json
|
||||
bd wisp gc --age 24h --json # Custom age threshold
|
||||
bd wisp gc --dry-run # Preview what would be cleaned
|
||||
bd ephemeral gc --json
|
||||
bd ephemeral gc --age 24h --json # Custom age threshold
|
||||
bd ephemeral gc --dry-run # Preview what would be cleaned
|
||||
```
|
||||
|
||||
### Bonding (Combining Work)
|
||||
@@ -424,29 +424,29 @@ bd mol bond <A> <B> --dry-run
|
||||
|
||||
```bash
|
||||
# Compress wisp to permanent digest
|
||||
bd mol squash <wisp-id> --json
|
||||
bd mol squash <ephemeral-id> --json
|
||||
|
||||
# With agent-provided summary
|
||||
bd mol squash <wisp-id> --summary "Work completed" --json
|
||||
bd mol squash <ephemeral-id> --summary "Work completed" --json
|
||||
|
||||
# Preview
|
||||
bd mol squash <wisp-id> --dry-run
|
||||
bd mol squash <ephemeral-id> --dry-run
|
||||
|
||||
# Keep wisp children after squash
|
||||
bd mol squash <wisp-id> --keep-children --json
|
||||
bd mol squash <ephemeral-id> --keep-children --json
|
||||
```
|
||||
|
||||
### Burn (Discard Wisp)
|
||||
|
||||
```bash
|
||||
# Delete wisp without digest (destructive)
|
||||
bd mol burn <wisp-id> --json
|
||||
bd mol burn <ephemeral-id> --json
|
||||
|
||||
# Preview
|
||||
bd mol burn <wisp-id> --dry-run
|
||||
bd mol burn <ephemeral-id> --dry-run
|
||||
|
||||
# Skip confirmation
|
||||
bd mol burn <wisp-id> --force --json
|
||||
bd mol burn <ephemeral-id> --force --json
|
||||
```
|
||||
|
||||
**Note:** Most mol commands require `--no-daemon` flag when daemon is running.
|
||||
|
||||
@@ -202,7 +202,7 @@ The 1-hour grace period ensures tombstones propagate even with minor clock drift
|
||||
|
||||
## Wisps: Intentional Tombstone Bypass
|
||||
|
||||
**Wisps** (ephemeral issues created by `bd wisp create`) are intentionally excluded from tombstone tracking.
|
||||
**Wisps** (ephemeral issues created by `bd ephemeral create`) are intentionally excluded from tombstone tracking.
|
||||
|
||||
### Why Wisps Don't Need Tombstones
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ For reusable workflows, beads uses a chemistry metaphor:
|
||||
|
||||
```bash
|
||||
bd pour <proto> # Proto → Mol (persistent instance)
|
||||
bd wisp create <proto> # Proto → Wisp (ephemeral instance)
|
||||
bd ephemeral create <proto> # Proto → Wisp (ephemeral instance)
|
||||
bd mol squash <id> # Mol/Wisp → Digest (permanent record)
|
||||
bd mol burn <id> # Wisp → nothing (discard)
|
||||
```
|
||||
@@ -227,10 +227,10 @@ bd close <id> --reason "Done"
|
||||
Wisps accumulate if not squashed/burned:
|
||||
|
||||
```bash
|
||||
bd wisp list # Check for orphans
|
||||
bd ephemeral list # Check for orphans
|
||||
bd mol squash <id> # Create digest
|
||||
bd mol burn <id> # Or discard
|
||||
bd wisp gc # Garbage collect old wisps
|
||||
bd ephemeral gc # Garbage collect old wisps
|
||||
```
|
||||
|
||||
## Layer Cake Architecture
|
||||
@@ -273,7 +273,7 @@ bd dep tree <id> # Show dependency tree
|
||||
|
||||
```bash
|
||||
bd pour <proto> --var k=v # Template → persistent mol
|
||||
bd wisp create <proto> # Template → ephemeral wisp
|
||||
bd ephemeral create <proto> # Template → ephemeral wisp
|
||||
bd mol bond A B # Connect work graphs
|
||||
bd mol squash <id> # Compress to digest
|
||||
bd mol burn <id> # Discard without record
|
||||
|
||||
@@ -89,11 +89,11 @@ type CreateArgs struct {
|
||||
WaitsFor string `json:"waits_for,omitempty"` // Spawner issue ID to wait for
|
||||
WaitsForGate string `json:"waits_for_gate,omitempty"` // Gate type: all-children or any-children
|
||||
// Messaging fields (bd-kwro)
|
||||
Sender string `json:"sender,omitempty"` // Who sent this (for messages)
|
||||
Wisp bool `json:"wisp,omitempty"` // Wisp = ephemeral vapor from the Steam Engine; bulk-deleted when closed
|
||||
Sender string `json:"sender,omitempty"` // Who sent this (for messages)
|
||||
Ephemeral bool `json:"ephemeral,omitempty"` // If true, not exported to JSONL; bulk-deleted when closed
|
||||
RepliesTo string `json:"replies_to,omitempty"` // Issue ID for conversation threading
|
||||
// ID generation (bd-hobo)
|
||||
IDPrefix string `json:"id_prefix,omitempty"` // Override prefix for ID generation (mol, wisp, etc.)
|
||||
IDPrefix string `json:"id_prefix,omitempty"` // Override prefix for ID generation (mol, eph, etc.)
|
||||
CreatedBy string `json:"created_by,omitempty"` // Who created the issue
|
||||
}
|
||||
|
||||
@@ -115,8 +115,8 @@ type UpdateArgs struct {
|
||||
RemoveLabels []string `json:"remove_labels,omitempty"`
|
||||
SetLabels []string `json:"set_labels,omitempty"`
|
||||
// Messaging fields (bd-kwro)
|
||||
Sender *string `json:"sender,omitempty"` // Who sent this (for messages)
|
||||
Wisp *bool `json:"wisp,omitempty"` // Wisp = ephemeral vapor from the Steam Engine; bulk-deleted when closed
|
||||
Sender *string `json:"sender,omitempty"` // Who sent this (for messages)
|
||||
Ephemeral *bool `json:"ephemeral,omitempty"` // If true, not exported to JSONL; bulk-deleted when closed
|
||||
RepliesTo *string `json:"replies_to,omitempty"` // Issue ID for conversation threading
|
||||
// Graph link fields (bd-fu83)
|
||||
RelatesTo *string `json:"relates_to,omitempty"` // JSON array of related issue IDs
|
||||
@@ -193,8 +193,8 @@ type ListArgs struct {
|
||||
// Parent filtering (bd-yqhh)
|
||||
ParentID string `json:"parent_id,omitempty"`
|
||||
|
||||
// Wisp filtering (bd-bkul)
|
||||
Wisp *bool `json:"wisp,omitempty"`
|
||||
// Ephemeral filtering (bd-bkul)
|
||||
Ephemeral *bool `json:"ephemeral,omitempty"`
|
||||
}
|
||||
|
||||
// CountArgs represents arguments for the count operation
|
||||
|
||||
@@ -81,8 +81,8 @@ func updatesFromArgs(a UpdateArgs) map[string]interface{} {
|
||||
if a.Sender != nil {
|
||||
u["sender"] = *a.Sender
|
||||
}
|
||||
if a.Wisp != nil {
|
||||
u["wisp"] = *a.Wisp
|
||||
if a.Ephemeral != nil {
|
||||
u["ephemeral"] = *a.Ephemeral
|
||||
}
|
||||
if a.RepliesTo != nil {
|
||||
u["replies_to"] = *a.RepliesTo
|
||||
@@ -176,8 +176,8 @@ func (s *Server) handleCreate(req *Request) Response {
|
||||
EstimatedMinutes: createArgs.EstimatedMinutes,
|
||||
Status: types.StatusOpen,
|
||||
// Messaging fields (bd-kwro)
|
||||
Sender: createArgs.Sender,
|
||||
Wisp: createArgs.Wisp,
|
||||
Sender: createArgs.Sender,
|
||||
Ephemeral: createArgs.Ephemeral,
|
||||
// NOTE: RepliesTo now handled via replies-to dependency (Decision 004)
|
||||
// ID generation (bd-hobo)
|
||||
IDPrefix: createArgs.IDPrefix,
|
||||
@@ -844,8 +844,8 @@ func (s *Server) handleList(req *Request) Response {
|
||||
filter.ParentID = &listArgs.ParentID
|
||||
}
|
||||
|
||||
// Wisp filtering (bd-bkul)
|
||||
filter.Wisp = listArgs.Wisp
|
||||
// Ephemeral filtering (bd-bkul)
|
||||
filter.Ephemeral = listArgs.Ephemeral
|
||||
|
||||
// Guard against excessive ID lists to avoid SQLite parameter limits
|
||||
const maxIDs = 1000
|
||||
@@ -1475,7 +1475,7 @@ func (s *Server) handleGateCreate(req *Request) Response {
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1, // Gates are typically high priority
|
||||
Assignee: "deacon/",
|
||||
Wisp: true, // Gates are wisps (ephemeral)
|
||||
Ephemeral: true, // Gates are wisps (ephemeral)
|
||||
AwaitType: args.AwaitType,
|
||||
AwaitID: args.AwaitID,
|
||||
Timeout: args.Timeout,
|
||||
|
||||
@@ -885,7 +885,7 @@ func (s *SQLiteStorage) scanIssues(ctx context.Context, rows *sql.Rows) ([]*type
|
||||
issue.Sender = sender.String
|
||||
}
|
||||
if wisp.Valid && wisp.Int64 != 0 {
|
||||
issue.Wisp = true
|
||||
issue.Ephemeral = true
|
||||
}
|
||||
// Pinned field (bd-7h5)
|
||||
if pinned.Valid && pinned.Int64 != 0 {
|
||||
@@ -1006,7 +1006,7 @@ func (s *SQLiteStorage) scanIssuesWithDependencyType(ctx context.Context, rows *
|
||||
issue.Sender = sender.String
|
||||
}
|
||||
if wisp.Valid && wisp.Int64 != 0 {
|
||||
issue.Wisp = true
|
||||
issue.Ephemeral = true
|
||||
}
|
||||
// Pinned field (bd-7h5)
|
||||
if pinned.Valid && pinned.Int64 != 0 {
|
||||
|
||||
@@ -295,7 +295,7 @@ func TestRepliesTo(t *testing.T) {
|
||||
IssueType: types.TypeMessage,
|
||||
Sender: "alice",
|
||||
Assignee: "bob",
|
||||
Wisp: true,
|
||||
Ephemeral: true,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
@@ -307,7 +307,7 @@ func TestRepliesTo(t *testing.T) {
|
||||
IssueType: types.TypeMessage,
|
||||
Sender: "bob",
|
||||
Assignee: "alice",
|
||||
Wisp: true,
|
||||
Ephemeral: true,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
@@ -363,7 +363,7 @@ func TestRepliesTo_Chain(t *testing.T) {
|
||||
IssueType: types.TypeMessage,
|
||||
Sender: "user",
|
||||
Assignee: "inbox",
|
||||
Wisp: true,
|
||||
Ephemeral: true,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
@@ -415,7 +415,7 @@ func TestWispField(t *testing.T) {
|
||||
Status: types.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: types.TypeMessage,
|
||||
Wisp: true,
|
||||
Ephemeral: true,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
@@ -426,7 +426,7 @@ func TestWispField(t *testing.T) {
|
||||
Status: types.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: types.TypeTask,
|
||||
Wisp: false,
|
||||
Ephemeral: false,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
@@ -443,7 +443,7 @@ func TestWispField(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssue failed: %v", err)
|
||||
}
|
||||
if !savedWisp.Wisp {
|
||||
if !savedWisp.Ephemeral {
|
||||
t.Error("Wisp issue should have Wisp=true")
|
||||
}
|
||||
|
||||
@@ -451,7 +451,7 @@ func TestWispField(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssue failed: %v", err)
|
||||
}
|
||||
if savedPermanent.Wisp {
|
||||
if savedPermanent.Ephemeral {
|
||||
t.Error("Permanent issue should have Wisp=false")
|
||||
}
|
||||
}
|
||||
@@ -468,7 +468,7 @@ func TestWispFilter(t *testing.T) {
|
||||
Status: types.StatusClosed, // Closed for cleanup test
|
||||
Priority: 2,
|
||||
IssueType: types.TypeMessage,
|
||||
Wisp: true,
|
||||
Ephemeral: true,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
@@ -483,7 +483,7 @@ func TestWispFilter(t *testing.T) {
|
||||
Status: types.StatusClosed,
|
||||
Priority: 2,
|
||||
IssueType: types.TypeTask,
|
||||
Wisp: false,
|
||||
Ephemeral: false,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
@@ -497,7 +497,7 @@ func TestWispFilter(t *testing.T) {
|
||||
closedStatus := types.StatusClosed
|
||||
wispFilter := types.IssueFilter{
|
||||
Status: &closedStatus,
|
||||
Wisp: &wispTrue,
|
||||
Ephemeral: &wispTrue,
|
||||
}
|
||||
|
||||
wispIssues, err := store.SearchIssues(ctx, "", wispFilter)
|
||||
@@ -512,7 +512,7 @@ func TestWispFilter(t *testing.T) {
|
||||
wispFalse := false
|
||||
nonWispFilter := types.IssueFilter{
|
||||
Status: &closedStatus,
|
||||
Wisp: &wispFalse,
|
||||
Ephemeral: &wispFalse,
|
||||
}
|
||||
|
||||
permanentIssues, err := store.SearchIssues(ctx, "", nonWispFilter)
|
||||
|
||||
@@ -28,7 +28,7 @@ func insertIssue(ctx context.Context, conn *sql.Conn, issue *types.Issue) error
|
||||
}
|
||||
|
||||
wisp := 0
|
||||
if issue.Wisp {
|
||||
if issue.Ephemeral {
|
||||
wisp = 1
|
||||
}
|
||||
pinned := 0
|
||||
@@ -94,7 +94,7 @@ func insertIssues(ctx context.Context, conn *sql.Conn, issues []*types.Issue) er
|
||||
}
|
||||
|
||||
wisp := 0
|
||||
if issue.Wisp {
|
||||
if issue.Ephemeral {
|
||||
wisp = 1
|
||||
}
|
||||
pinned := 0
|
||||
|
||||
@@ -282,7 +282,7 @@ func (s *SQLiteStorage) upsertIssueInTx(ctx context.Context, tx *sql.Tx, issue *
|
||||
err := tx.QueryRowContext(ctx, `SELECT id FROM issues WHERE id = ?`, issue.ID).Scan(&existingID)
|
||||
|
||||
wisp := 0
|
||||
if issue.Wisp {
|
||||
if issue.Ephemeral {
|
||||
wisp = 1
|
||||
}
|
||||
pinned := 0
|
||||
|
||||
@@ -54,7 +54,7 @@ func (s *SQLiteStorage) ExportToMultiRepo(ctx context.Context) (map[string]int,
|
||||
// Wisps exist only in SQLite and are shared via .beads/redirect, not JSONL.
|
||||
filtered := make([]*types.Issue, 0, len(allIssues))
|
||||
for _, issue := range allIssues {
|
||||
if !issue.Wisp {
|
||||
if !issue.Ephemeral {
|
||||
filtered = append(filtered, issue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,7 +349,7 @@ func (s *SQLiteStorage) GetIssue(ctx context.Context, id string) (*types.Issue,
|
||||
issue.Sender = sender.String
|
||||
}
|
||||
if wisp.Valid && wisp.Int64 != 0 {
|
||||
issue.Wisp = true
|
||||
issue.Ephemeral = true
|
||||
}
|
||||
// Pinned field (bd-7h5)
|
||||
if pinned.Valid && pinned.Int64 != 0 {
|
||||
@@ -562,7 +562,7 @@ func (s *SQLiteStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
|
||||
issue.Sender = sender.String
|
||||
}
|
||||
if wisp.Valid && wisp.Int64 != 0 {
|
||||
issue.Wisp = true
|
||||
issue.Ephemeral = true
|
||||
}
|
||||
// Pinned field (bd-7h5)
|
||||
if pinned.Valid && pinned.Int64 != 0 {
|
||||
@@ -1652,8 +1652,8 @@ func (s *SQLiteStorage) SearchIssues(ctx context.Context, query string, filter t
|
||||
}
|
||||
|
||||
// Wisp filtering (bd-kwro.9)
|
||||
if filter.Wisp != nil {
|
||||
if *filter.Wisp {
|
||||
if filter.Ephemeral != nil {
|
||||
if *filter.Ephemeral {
|
||||
whereClauses = append(whereClauses, "ephemeral = 1") // SQL column is still 'ephemeral'
|
||||
} else {
|
||||
whereClauses = append(whereClauses, "(ephemeral = 0 OR ephemeral IS NULL)")
|
||||
|
||||
@@ -400,7 +400,7 @@ func (s *SQLiteStorage) GetStaleIssues(ctx context.Context, filter types.StaleFi
|
||||
issue.Sender = sender.String
|
||||
}
|
||||
if ephemeral.Valid && ephemeral.Int64 != 0 {
|
||||
issue.Wisp = true
|
||||
issue.Ephemeral = true
|
||||
}
|
||||
// Pinned field (bd-7h5)
|
||||
if pinned.Valid && pinned.Int64 != 0 {
|
||||
|
||||
@@ -1089,8 +1089,8 @@ func (t *sqliteTxStorage) SearchIssues(ctx context.Context, query string, filter
|
||||
}
|
||||
|
||||
// Wisp filtering (bd-kwro.9)
|
||||
if filter.Wisp != nil {
|
||||
if *filter.Wisp {
|
||||
if filter.Ephemeral != nil {
|
||||
if *filter.Ephemeral {
|
||||
whereClauses = append(whereClauses, "ephemeral = 1") // SQL column is still 'ephemeral'
|
||||
} else {
|
||||
whereClauses = append(whereClauses, "(ephemeral = 0 OR ephemeral IS NULL)")
|
||||
@@ -1244,7 +1244,7 @@ func scanIssueRow(row scanner) (*types.Issue, error) {
|
||||
issue.Sender = sender.String
|
||||
}
|
||||
if wisp.Valid && wisp.Int64 != 0 {
|
||||
issue.Wisp = true
|
||||
issue.Ephemeral = true
|
||||
}
|
||||
// Pinned field (bd-7h5)
|
||||
if pinned.Valid && pinned.Int64 != 0 {
|
||||
|
||||
@@ -44,8 +44,8 @@ type Issue struct {
|
||||
OriginalType string `json:"original_type,omitempty"` // Issue type before deletion (for tombstones)
|
||||
|
||||
// Messaging fields (bd-kwro): inter-agent communication support
|
||||
Sender string `json:"sender,omitempty"` // Who sent this (for messages)
|
||||
Wisp bool `json:"wisp,omitempty"` // Wisp = ephemeral vapor from the Steam Engine; bulk-deleted when closed
|
||||
Sender string `json:"sender,omitempty"` // Who sent this (for messages)
|
||||
Ephemeral bool `json:"ephemeral,omitempty"` // If true, not exported to JSONL; bulk-deleted when closed
|
||||
// NOTE: RepliesTo, RelatesTo, DuplicateOf, SupersededBy moved to dependencies table
|
||||
// per Decision 004 (Edge Schema Consolidation). Use dependency API instead.
|
||||
|
||||
@@ -598,8 +598,8 @@ type IssueFilter struct {
|
||||
// Tombstone filtering (bd-1bu)
|
||||
IncludeTombstones bool // If false (default), exclude tombstones from results
|
||||
|
||||
// Wisp filtering (bd-kwro.9)
|
||||
Wisp *bool // Filter by wisp flag (nil = any, true = only wisps, false = only non-wisps)
|
||||
// Ephemeral filtering (bd-kwro.9)
|
||||
Ephemeral *bool // Filter by ephemeral flag (nil = any, true = only ephemeral, false = only persistent)
|
||||
|
||||
// Pinned filtering (bd-7h5)
|
||||
Pinned *bool // Filter by pinned flag (nil = any, true = only pinned, false = only non-pinned)
|
||||
|
||||
@@ -84,7 +84,7 @@ bd mol spawn mol-release --var version=2.0 # With variable substitution
|
||||
**Chemistry shortcuts:**
|
||||
```bash
|
||||
bd pour mol-feature # Shortcut for spawn --pour
|
||||
bd wisp create mol-patrol # Explicit wisp creation
|
||||
bd ephemeral create mol-patrol # Explicit wisp creation
|
||||
```
|
||||
|
||||
### Spawn with Immediate Execution
|
||||
@@ -164,7 +164,7 @@ bd mol bond mol-feature mol-deploy --as "Feature with Deploy"
|
||||
### Creating Wisps
|
||||
|
||||
```bash
|
||||
bd wisp create mol-patrol # From proto
|
||||
bd ephemeral create mol-patrol # From proto
|
||||
bd mol spawn mol-patrol # Same (spawn defaults to wisp)
|
||||
bd mol spawn mol-check --var target=db # With variables
|
||||
```
|
||||
@@ -172,8 +172,8 @@ bd mol spawn mol-check --var target=db # With variables
|
||||
### Listing Wisps
|
||||
|
||||
```bash
|
||||
bd wisp list # List all wisps
|
||||
bd wisp list --json # Machine-readable
|
||||
bd ephemeral list # List all wisps
|
||||
bd ephemeral list --json # Machine-readable
|
||||
```
|
||||
|
||||
### Ending Wisps
|
||||
@@ -198,7 +198,7 @@ Use burn for routine work with no archival value.
|
||||
### Garbage Collection
|
||||
|
||||
```bash
|
||||
bd wisp gc # Clean up orphaned wisps
|
||||
bd ephemeral gc # Clean up orphaned wisps
|
||||
```
|
||||
|
||||
---
|
||||
@@ -289,7 +289,7 @@ bd mol spawn mol-weekly-review --pour
|
||||
|
||||
```bash
|
||||
# Patrol proto exists
|
||||
bd wisp create mol-patrol
|
||||
bd ephemeral create mol-patrol
|
||||
|
||||
# Execute patrol work...
|
||||
|
||||
@@ -328,9 +328,9 @@ bd mol distill bd-release-epic --as "Release Process" --var version=X.Y.Z
|
||||
| `bd mol squash <mol>` | Compress wisp children to digest |
|
||||
| `bd mol burn <wisp>` | Delete wisp without trace |
|
||||
| `bd pour <proto>` | Shortcut for `spawn --pour` |
|
||||
| `bd wisp create <proto>` | Create ephemeral wisp |
|
||||
| `bd wisp list` | List all wisps |
|
||||
| `bd wisp gc` | Garbage collect orphaned wisps |
|
||||
| `bd ephemeral create <proto>` | Create ephemeral wisp |
|
||||
| `bd ephemeral list` | List all wisps |
|
||||
| `bd ephemeral gc` | Garbage collect orphaned wisps |
|
||||
| `bd ship <capability>` | Publish capability for cross-project deps |
|
||||
|
||||
---
|
||||
@@ -347,7 +347,7 @@ bd mol distill bd-release-epic --as "Release Process" --var version=X.Y.Z
|
||||
|
||||
**"Wisp commands fail"**
|
||||
- Wisps stored in `.beads-wisp/` (separate from `.beads/`)
|
||||
- Check `bd wisp list` for active wisps
|
||||
- Check `bd ephemeral list` for active wisps
|
||||
|
||||
**"External dependency not satisfied"**
|
||||
- Target project must have closed issue with `provides:<capability>` label
|
||||
|
||||
Reference in New Issue
Block a user