From dfc796589f118846d78403f9ea05b9ffbb079aac Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 28 Dec 2025 01:34:01 -0800 Subject: [PATCH] feat: add pour warning for vapor-phase formulas, improve help text, add doctor check - Add Phase field to Formula type to indicate recommended instantiation phase - Add warning in 'bd mol pour' when formula has phase="vapor" - Improve pour/wisp help text with clear comparison of when to use each - Add CheckPersistentMolIssues doctor check to detect mol- issues in JSONL - Update beads-release.formula.json with phase="vapor" This helps prevent accidental persistence of ephemeral workflow issues. --- cmd/bd/cook.go | 2 + cmd/bd/doctor.go | 5 +++ cmd/bd/doctor/maintenance.go | 75 ++++++++++++++++++++++++++++++++++++ cmd/bd/pour.go | 37 ++++++++++++++---- cmd/bd/template.go | 1 + cmd/bd/wisp.go | 26 ++++++++++--- internal/formula/types.go | 5 +++ 7 files changed, 137 insertions(+), 14 deletions(-) diff --git a/cmd/bd/cook.go b/cmd/bd/cook.go index 7d72003e..ec7ad670 100644 --- a/cmd/bd/cook.go +++ b/cmd/bd/cook.go @@ -649,6 +649,8 @@ func cookFormulaToSubgraphWithVars(f *formula.Formula, protoID string, vars map[ } } } + // Attach recommended phase from formula (bd-mol cleanup: warn on pour of vapor formulas) + subgraph.Phase = f.Phase return subgraph, nil } diff --git a/cmd/bd/doctor.go b/cmd/bd/doctor.go index c06b2039..c5444dee 100644 --- a/cmd/bd/doctor.go +++ b/cmd/bd/doctor.go @@ -911,6 +911,11 @@ func runDiagnostics(path string) doctorResult { result.Checks = append(result.Checks, staleMoleculesCheck) // Don't fail overall check for stale molecules, just warn + // Check 26b: Persistent mol- issues (should have been ephemeral) + persistentMolCheck := convertDoctorCheck(doctor.CheckPersistentMolIssues(path)) + result.Checks = append(result.Checks, persistentMolCheck) + // Don't fail overall check for persistent mol issues, just warn + // Check 27: Expired tombstones (maintenance, bd-bqcc) tombstonesExpiredCheck := convertDoctorCheck(doctor.CheckExpiredTombstones(path)) result.Checks = append(result.Checks, tombstonesExpiredCheck) diff --git a/cmd/bd/doctor/maintenance.go b/cmd/bd/doctor/maintenance.go index 4debe2ff..ddc51790 100644 --- a/cmd/bd/doctor/maintenance.go +++ b/cmd/bd/doctor/maintenance.go @@ -350,3 +350,78 @@ func resolveBeadsDir(beadsDir string) string { return target } + +// CheckPersistentMolIssues detects mol- prefixed issues that should have been ephemeral. +// When users run "bd mol pour" on formulas that should use "bd mol wisp", the resulting +// issues get the "mol-" prefix but persist in JSONL. These should be cleaned up. +func CheckPersistentMolIssues(path string) DoctorCheck { + beadsDir := resolveBeadsDir(filepath.Join(path, ".beads")) + jsonlPath := filepath.Join(beadsDir, "issues.jsonl") + + if _, err := os.Stat(jsonlPath); os.IsNotExist(err) { + return DoctorCheck{ + Name: "Persistent Mol Issues", + Status: StatusOK, + Message: "N/A (no JSONL file)", + Category: CategoryMaintenance, + } + } + + // Read JSONL and count mol- prefixed issues that are not ephemeral + file, err := os.Open(jsonlPath) // #nosec G304 - path constructed safely + if err != nil { + return DoctorCheck{ + Name: "Persistent Mol Issues", + Status: StatusOK, + Message: "N/A (unable to read JSONL)", + Category: CategoryMaintenance, + } + } + defer file.Close() + + var molCount int + var molIDs []string + decoder := json.NewDecoder(file) + + for { + var issue types.Issue + if err := decoder.Decode(&issue); err != nil { + break + } + // Skip deleted issues (tombstones) + if issue.DeletedAt != nil { + continue + } + // Look for mol- prefix that shouldn't be in JSONL + // (ephemeral issues have Ephemeral=true and don't get exported) + if strings.HasPrefix(issue.ID, "mol-") && !issue.Ephemeral { + molCount++ + if len(molIDs) < 3 { + molIDs = append(molIDs, issue.ID) + } + } + } + + if molCount == 0 { + return DoctorCheck{ + Name: "Persistent Mol Issues", + Status: StatusOK, + Message: "No persistent mol- issues", + Category: CategoryMaintenance, + } + } + + detail := fmt.Sprintf("Example: %v", molIDs) + if molCount > 3 { + detail += fmt.Sprintf(" (+%d more)", molCount-3) + } + + return DoctorCheck{ + Name: "Persistent Mol Issues", + Status: StatusWarning, + Message: fmt.Sprintf("%d mol- issue(s) in JSONL should be ephemeral", molCount), + Detail: detail, + Fix: "Run 'bd delete --force' to remove, or use 'bd mol wisp' instead of 'bd mol pour'", + Category: CategoryMaintenance, + } +} diff --git a/cmd/bd/pour.go b/cmd/bd/pour.go index 06b88305..fd48a528 100644 --- a/cmd/bd/pour.go +++ b/cmd/bd/pour.go @@ -21,20 +21,29 @@ var pourCmd = &cobra.Command{ Short: "Instantiate a proto as a persistent mol (solid -> liquid)", Long: `Pour a proto into a persistent mol - like pouring molten metal into a mold. -This is the chemistry-inspired command for creating persistent work from templates. +This is the chemistry-inspired command for creating PERSISTENT work from templates. The resulting mol lives in .beads/ (permanent storage) and is synced with git. Phase transition: Proto (solid) -> pour -> Mol (liquid) -Use pour for: - - Feature work that spans sessions - - Important work needing audit trail - - Anything you might need to reference later +WHEN TO USE POUR vs WISP: + pour (liquid): Persistent work that needs audit trail + - Feature implementations spanning multiple sessions + - Work you may need to reference later + - Anything worth preserving in git history + + wisp (vapor): Ephemeral work that auto-cleans up + - Release workflows (one-time execution) + - Patrol cycles (deacon, witness, refinery) + - Health checks and diagnostics + - Any operational workflow without audit value + +TIP: Formulas can specify phase:"vapor" to recommend wisp usage. + If you pour a vapor-phase formula, you'll get a warning. Examples: - bd mol pour mol-feature --var name=auth # Create persistent mol from proto - bd mol pour mol-release --var version=1.0 # Release workflow - bd mol pour mol-review --var pr=123 # Code review workflow`, + bd mol pour mol-feature --var name=auth # Persistent feature work + bd mol pour mol-review --var pr=123 # Persistent code review`, Args: cobra.ExactArgs(1), Run: runPour, } @@ -85,6 +94,18 @@ func runPour(cmd *cobra.Command, args []string) { subgraph = sg protoID = sg.Root.ID isFormula = true + + // Warn if formula recommends vapor phase (bd-mol cleanup) + if sg.Phase == "vapor" { + fmt.Fprintf(os.Stderr, "%s Formula %q recommends vapor phase (ephemeral)\n", ui.RenderWarn("⚠"), args[0]) + fmt.Fprintf(os.Stderr, " Consider using: bd mol wisp %s", args[0]) + for _, v := range varFlags { + fmt.Fprintf(os.Stderr, " --var %s", v) + } + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " Pour creates persistent issues that sync to git.\n") + fmt.Fprintf(os.Stderr, " Wisp creates ephemeral issues that auto-cleanup.\n\n") + } } if subgraph == nil { diff --git a/cmd/bd/template.go b/cmd/bd/template.go index 9ed899e0..f6ba8dea 100644 --- a/cmd/bd/template.go +++ b/cmd/bd/template.go @@ -31,6 +31,7 @@ type TemplateSubgraph struct { Dependencies []*types.Dependency // All dependencies within the subgraph IssueMap map[string]*types.Issue // ID -> Issue for quick lookup VarDefs map[string]formula.VarDef // Variable definitions from formula (for defaults) + Phase string // Recommended phase: "liquid" (pour) or "vapor" (wisp) } // InstantiateResult holds the result of template instantiation diff --git a/cmd/bd/wisp.go b/cmd/bd/wisp.go index 2c20ef6e..5fa5bf08 100644 --- a/cmd/bd/wisp.go +++ b/cmd/bd/wisp.go @@ -29,15 +29,28 @@ import ( var wispCmd = &cobra.Command{ Use: "wisp [proto-id]", Short: "Create or manage wisps (ephemeral molecules)", - Long: `Create or manage wisps - ephemeral molecules for operational workflows. + Long: `Create or manage wisps - EPHEMERAL molecules for operational workflows. When called with a proto-id argument, creates a wisp from that proto. When called with a subcommand (list, gc), manages existing wisps. Wisps are issues with Ephemeral=true in the main database. They're stored locally but NOT exported to JSONL (and thus not synced via git). -They're used for patrol cycles, operational loops, and other workflows -that shouldn't accumulate in the shared issue database. + +WHEN TO USE WISP vs POUR: + wisp (vapor): Ephemeral work that auto-cleans up + - Release workflows (one-time execution) + - Patrol cycles (deacon, witness, refinery) + - Health checks and diagnostics + - Any operational workflow without audit value + + pour (liquid): Persistent work that needs audit trail + - Feature implementations spanning multiple sessions + - Work you may need to reference later + - Anything worth preserving in git history + +TIP: Formulas can specify phase:"vapor" to recommend wisp usage. + If you use pour on a vapor-phase formula, you'll get a warning. The wisp lifecycle: 1. Create: bd mol wisp or bd create --ephemeral @@ -46,9 +59,10 @@ The wisp lifecycle: 4. Or burn: bd mol burn (deletes without creating digest) Examples: - bd mol wisp mol-patrol # Create wisp from proto - bd mol wisp list # List all wisps - bd mol wisp gc # Garbage collect old wisps + bd mol wisp beads-release --var version=1.0 # Release workflow + bd mol wisp mol-patrol # Ephemeral patrol cycle + bd mol wisp list # List all wisps + bd mol wisp gc # Garbage collect old wisps Subcommands: list List all wisps in current context diff --git a/internal/formula/types.go b/internal/formula/types.go index d7821595..02c8d47b 100644 --- a/internal/formula/types.go +++ b/internal/formula/types.go @@ -99,6 +99,11 @@ type Formula struct { // Used with TypeAspect to specify which steps the aspect applies to. Pointcuts []*Pointcut `json:"pointcuts,omitempty"` + // Phase indicates the recommended instantiation phase: "liquid" (pour) or "vapor" (wisp). + // If "vapor", bd pour will warn and suggest using bd mol wisp instead. + // Patrol and release workflows should typically use "vapor" since they're operational. + Phase string `json:"phase,omitempty"` + // Source tracks where this formula was loaded from (set by parser). Source string `json:"source,omitempty"` }