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.
This commit is contained in:
Steve Yegge
2025-12-28 01:34:01 -08:00
parent b5ab4f2a3b
commit dfc796589f
7 changed files with 137 additions and 14 deletions

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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 <id> --force' to remove, or use 'bd mol wisp' instead of 'bd mol pour'",
Category: CategoryMaintenance,
}
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 <proto> or bd create --ephemeral
@@ -46,9 +59,10 @@ The wisp lifecycle:
4. Or burn: bd mol burn <id> (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