From 9ad70cce64a317597ee89480ddf2c01a3b36ce4e Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Wed, 24 Dec 2025 12:52:47 -0800 Subject: [PATCH] refactor(mol): remove bd mol spawn - use pour/wisp only (bd-8y9t) Remove the spawn command from bd mol. Proto instantiation now uses: - bd pour - Instantiate as persistent mol (liquid phase) - bd wisp create - Instantiate as ephemeral wisp (vapor phase) Rationale: - 'spawn' doesn't fit the chemistry metaphor - Two phase transitions (pour/wisp) are clearer than one command with flags - Avoids confusion about defaults Changes: - Delete mol_spawn.go - Update mol.go, mol_catalog.go, mol_distill.go to reference pour/wisp - Update pour.go and wisp.go to remove 'Equivalent to spawn' comments - Update info.go changelog entries - Update CHANGELOG.md, ARCHITECTURE.md, DELETIONS.md Closes bd-8y9t --- CHANGELOG.md | 6 +- cmd/bd/info.go | 4 +- cmd/bd/mol.go | 12 +- cmd/bd/mol_catalog.go | 7 +- cmd/bd/mol_distill.go | 4 +- cmd/bd/mol_spawn.go | 262 ------------------------------------------ cmd/bd/pour.go | 3 - cmd/bd/wisp.go | 2 - docs/ARCHITECTURE.md | 6 +- docs/DELETIONS.md | 2 +- 10 files changed, 23 insertions(+), 285 deletions(-) delete mode 100644 cmd/bd/mol_spawn.go diff --git a/CHANGELOG.md b/CHANGELOG.md index e5440a13..8494d363 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -156,10 +156,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **Wisp molecules** (bd-2vh3) - Spawn molecules as wisps by default - - `bd mol spawn` creates wisp issues that live only in SQLite +- **Wisp molecules** (bd-2vh3) - Support for ephemeral wisps + - `bd wisp create` creates wisp issues that live only in SQLite - Wisp issues never export to JSONL (prevents zombie resurrection) - - Use `--persistent` flag to opt out of wisp spawning + - Use `bd pour` for persistent mols, `bd wisp create` for ephemeral wisps - `bd mol squash` compresses wisp children into a digest issue - `--summary` flag allows agents to provide AI-generated summaries diff --git a/cmd/bd/info.go b/cmd/bd/info.go index 9cf027fc..e4071cea 100644 --- a/cmd/bd/info.go +++ b/cmd/bd/info.go @@ -341,9 +341,9 @@ var versionChanges = []VersionChange{ Version: "0.33.0", Date: "2025-12-21", Changes: []string{ - "NEW: Wisp molecules (bd-2vh3) - bd mol spawn creates wisp issues by default", + "NEW: Wisp molecules (bd-2vh3) - use 'bd wisp create' for ephemeral wisps", "NEW: Wisp issues live only in SQLite, never export to JSONL (prevents zombie resurrection)", - "NEW: --persistent flag on bd mol spawn to opt out of wisp spawning", + "NEW: Use 'bd pour' for persistent mols, 'bd wisp create' for ephemeral wisps", "NEW: bd mol squash compresses wisp children into digest issue", "NEW: --summary flag on bd mol squash for agent-provided AI summaries", "FIX: DeleteIssue now cascades to comments table (bd-687g)", diff --git a/cmd/bd/mol.go b/cmd/bd/mol.go index 3a237f5c..315f8b89 100644 --- a/cmd/bd/mol.go +++ b/cmd/bd/mol.go @@ -20,7 +20,8 @@ import ( // Usage: // bd mol catalog # List available protos // bd mol show # Show proto/molecule structure -// bd mol spawn --var key=value # Instantiate proto → molecule +// bd pour --var key=value # Instantiate proto → persistent mol +// bd wisp create --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 @@ -49,10 +50,13 @@ The molecule metaphor: Commands: catalog List available protos show Show proto/molecule structure and variables - spawn Instantiate a proto → molecule bond Polymorphic combine: proto+proto, proto+mol, mol+mol - run Spawn + assign + pin for durable execution - distill Extract proto from ad-hoc epic (reverse of spawn)`, + run Pour + assign + pin for durable execution + distill Extract proto from ad-hoc epic + +See also: + bd pour # Instantiate as persistent mol (liquid phase) + bd wisp create # Instantiate as ephemeral wisp (vapor phase)`, } // ============================================================================= diff --git a/cmd/bd/mol_catalog.go b/cmd/bd/mol_catalog.go index 7b115225..8044b0d5 100644 --- a/cmd/bd/mol_catalog.go +++ b/cmd/bd/mol_catalog.go @@ -60,12 +60,13 @@ var molCatalogCmd = &cobra.Command{ fmt.Println(" 1. Create an epic with child issues") fmt.Println(" 2. Add the 'template' label: bd label add template") fmt.Println(" 3. Use {{variable}} placeholders in titles/descriptions") - fmt.Println("\nTo spawn (instantiate) a molecule from a proto:") - fmt.Println(" bd mol spawn --var key=value") + fmt.Println("\nTo instantiate a molecule from a proto:") + fmt.Println(" bd pour --var key=value # persistent mol") + fmt.Println(" bd wisp create --var key=value # ephemeral wisp") return } - fmt.Printf("%s\n", ui.RenderPass("Protos (for bd mol spawn):")) + fmt.Printf("%s\n", ui.RenderPass("Protos (for bd pour / bd wisp create):")) for _, mol := range molecules { vars := extractVariables(mol.Title + " " + mol.Description) varStr := "" diff --git a/cmd/bd/mol_distill.go b/cmd/bd/mol_distill.go index 5b6d5ce3..ca2beb17 100644 --- a/cmd/bd/mol_distill.go +++ b/cmd/bd/mol_distill.go @@ -190,8 +190,8 @@ func runMolDistill(cmd *cobra.Command, args []string) { if len(result.Variables) > 0 { fmt.Printf(" Variables: %s\n", strings.Join(result.Variables, ", ")) } - fmt.Printf("\nTo spawn this proto:\n") - fmt.Printf(" bd mol spawn %s", result.ProtoID[:8]) + fmt.Printf("\nTo instantiate this proto:\n") + fmt.Printf(" bd pour %s", result.ProtoID[:8]) for _, v := range result.Variables { fmt.Printf(" --var %s=", v) } diff --git a/cmd/bd/mol_spawn.go b/cmd/bd/mol_spawn.go deleted file mode 100644 index ae5a53e5..00000000 --- a/cmd/bd/mol_spawn.go +++ /dev/null @@ -1,262 +0,0 @@ -package main - -import ( - "fmt" - "os" - "strings" - - "github.com/spf13/cobra" - "github.com/steveyegge/beads/internal/types" - "github.com/steveyegge/beads/internal/ui" - "github.com/steveyegge/beads/internal/utils" -) - -var molSpawnCmd = &cobra.Command{ - Use: "spawn ", - Short: "Instantiate a proto into a molecule", - Long: `Spawn a molecule by instantiating a proto template into real issues. - -Variables are specified with --var key=value flags. The proto's {{key}} -placeholders will be replaced with the corresponding values. - -Phase behavior: - - By default, spawned molecules are WISPS (ephemeral, in .beads-wisp/) - - Use --pour to create a persistent MOL (in .beads/) - - Wisps are local-only, gitignored, and not synced - - Mols are permanent, synced, and auditable - -Chemistry shortcuts: - bd pour # Equivalent to: bd mol spawn --pour - bd wisp # Equivalent to: bd mol spawn - -Use --attach to bond additional protos to the spawned molecule in a single -command. Each attached proto is spawned and bonded using the --attach-type -(default: sequential). This is equivalent to running spawn + multiple bond -commands, but more convenient for composing workflows. - -Example: - bd mol spawn mol-patrol # Creates wisp (default) - bd mol spawn mol-feature --pour --var name=auth # Creates persistent mol - bd mol spawn bd-abc123 --pour --var version=1.2.0 # Persistent with vars - bd mol spawn mol-feature --attach mol-testing --var name=auth`, - Args: cobra.ExactArgs(1), - Run: runMolSpawn, -} - -func runMolSpawn(cmd *cobra.Command, args []string) { - CheckReadonly("mol spawn") - - ctx := rootCtx - - // mol spawn requires direct store access for subgraph loading and cloning - if store == nil { - if daemonClient != nil { - fmt.Fprintf(os.Stderr, "Error: mol spawn requires direct database access\n") - fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon mol spawn %s ...\n", args[0]) - } else { - fmt.Fprintf(os.Stderr, "Error: no database connection\n") - } - os.Exit(1) - } - - dryRun, _ := cmd.Flags().GetBool("dry-run") - varFlags, _ := cmd.Flags().GetStringSlice("var") - assignee, _ := cmd.Flags().GetString("assignee") - attachFlags, _ := cmd.Flags().GetStringSlice("attach") - attachType, _ := cmd.Flags().GetString("attach-type") - pour, _ := cmd.Flags().GetBool("pour") - persistent, _ := cmd.Flags().GetBool("persistent") - - // Handle deprecated --persistent flag - if persistent { - fmt.Fprintf(os.Stderr, "Warning: --persistent is deprecated, use --pour instead\n") - pour = true - } - - // Parse variables - vars := make(map[string]string) - for _, v := range varFlags { - parts := strings.SplitN(v, "=", 2) - if len(parts) != 2 { - fmt.Fprintf(os.Stderr, "Error: invalid variable format '%s', expected 'key=value'\n", v) - os.Exit(1) - } - vars[parts[0]] = parts[1] - } - - // Resolve molecule ID - moleculeID, err := utils.ResolvePartialID(ctx, store, args[0]) - if err != nil { - fmt.Fprintf(os.Stderr, "Error resolving molecule ID %s: %v\n", args[0], err) - os.Exit(1) - } - - // Load the molecule subgraph - subgraph, err := loadTemplateSubgraph(ctx, store, moleculeID) - if err != nil { - fmt.Fprintf(os.Stderr, "Error loading molecule: %v\n", err) - os.Exit(1) - } - - // Resolve and load attached protos - type attachmentInfo struct { - id string - issue *types.Issue - subgraph *MoleculeSubgraph - } - var attachments []attachmentInfo - for _, attachArg := range attachFlags { - attachID, err := utils.ResolvePartialID(ctx, store, attachArg) - if err != nil { - fmt.Fprintf(os.Stderr, "Error resolving attachment ID %s: %v\n", attachArg, err) - os.Exit(1) - } - attachIssue, err := store.GetIssue(ctx, attachID) - if err != nil { - fmt.Fprintf(os.Stderr, "Error loading attachment %s: %v\n", attachID, err) - os.Exit(1) - } - // Verify it's a proto (has template label) - isProtoIssue := false - for _, label := range attachIssue.Labels { - if label == MoleculeLabel { - isProtoIssue = true - break - } - } - if !isProtoIssue { - fmt.Fprintf(os.Stderr, "Error: %s is not a proto (missing '%s' label)\n", attachID, MoleculeLabel) - os.Exit(1) - } - attachSubgraph, err := loadTemplateSubgraph(ctx, store, attachID) - if err != nil { - fmt.Fprintf(os.Stderr, "Error loading attachment subgraph %s: %v\n", attachID, err) - os.Exit(1) - } - attachments = append(attachments, attachmentInfo{ - id: attachID, - issue: attachIssue, - subgraph: attachSubgraph, - }) - } - - // Check for missing variables (primary + all attachments) - requiredVars := extractAllVariables(subgraph) - for _, attach := range attachments { - attachVars := extractAllVariables(attach.subgraph) - for _, v := range attachVars { - // Dedupe: only add if not already in requiredVars - found := false - for _, rv := range requiredVars { - if rv == v { - found = true - break - } - } - if !found { - requiredVars = append(requiredVars, v) - } - } - } - var missingVars []string - for _, v := range requiredVars { - if _, ok := vars[v]; !ok { - missingVars = append(missingVars, v) - } - } - if len(missingVars) > 0 { - fmt.Fprintf(os.Stderr, "Error: missing required variables: %s\n", strings.Join(missingVars, ", ")) - fmt.Fprintf(os.Stderr, "Provide them with: --var %s=\n", missingVars[0]) - os.Exit(1) - } - - if dryRun { - fmt.Printf("\nDry run: would create %d issues from molecule %s\n\n", len(subgraph.Issues), moleculeID) - for _, issue := range subgraph.Issues { - newTitle := substituteVariables(issue.Title, vars) - suffix := "" - if issue.ID == subgraph.Root.ID && assignee != "" { - suffix = fmt.Sprintf(" (assignee: %s)", assignee) - } - fmt.Printf(" - %s (from %s)%s\n", newTitle, issue.ID, suffix) - } - if len(attachments) > 0 { - fmt.Printf("\nAttachments (%s bonding):\n", attachType) - for _, attach := range attachments { - fmt.Printf(" + %s (%d issues)\n", attach.issue.Title, len(attach.subgraph.Issues)) - for _, issue := range attach.subgraph.Issues { - newTitle := substituteVariables(issue.Title, vars) - fmt.Printf(" - %s (from %s)\n", newTitle, issue.ID) - } - } - } - if len(vars) > 0 { - fmt.Printf("\nVariables:\n") - for k, v := range vars { - fmt.Printf(" {{%s}} = %s\n", k, v) - } - } - return - } - - // Clone the subgraph (spawn the molecule) - // Spawned molecules are wisps by default (vapor phase) - use --pour for persistent mol (liquid phase) - wisp := !pour - result, err := spawnMolecule(ctx, store, subgraph, vars, assignee, actor, wisp) - if err != nil { - fmt.Fprintf(os.Stderr, "Error spawning molecule: %v\n", err) - os.Exit(1) - } - - // Attach bonded protos to the spawned molecule - totalAttached := 0 - if len(attachments) > 0 { - // Get the spawned molecule issue for bonding - spawnedMol, err := store.GetIssue(ctx, result.NewEpicID) - if err != nil { - fmt.Fprintf(os.Stderr, "Error loading spawned molecule: %v\n", err) - os.Exit(1) - } - - for _, attach := range attachments { - bondResult, err := bondProtoMol(ctx, store, attach.issue, spawnedMol, attachType, vars, "", actor, pour) - if err != nil { - fmt.Fprintf(os.Stderr, "Error attaching %s: %v\n", attach.id, err) - os.Exit(1) - } - totalAttached += bondResult.Spawned - } - } - - // Schedule auto-flush - markDirtyAndScheduleFlush() - - if jsonOutput { - // Enhance result with attachment info - type spawnWithAttach struct { - *InstantiateResult - Attached int `json:"attached"` - } - outputJSON(spawnWithAttach{result, totalAttached}) - return - } - - fmt.Printf("%s Spawned molecule: created %d issues\n", ui.RenderPass("✓"), result.Created) - fmt.Printf(" Root issue: %s\n", result.NewEpicID) - if totalAttached > 0 { - fmt.Printf(" Attached: %d issues from %d protos\n", totalAttached, len(attachments)) - } -} - -func init() { - molSpawnCmd.Flags().StringSlice("var", []string{}, "Variable substitution (key=value)") - molSpawnCmd.Flags().Bool("dry-run", false, "Preview what would be created") - molSpawnCmd.Flags().String("assignee", "", "Assign the root issue to this agent/user") - molSpawnCmd.Flags().StringSlice("attach", []string{}, "Proto to attach after spawning (repeatable)") - molSpawnCmd.Flags().String("attach-type", types.BondTypeSequential, "Bond type for attachments: sequential, parallel, or conditional") - molSpawnCmd.Flags().Bool("pour", false, "Create persistent mol in .beads/ (default: wisp in .beads-wisp/)") - molSpawnCmd.Flags().Bool("persistent", false, "Deprecated: use --pour instead") - _ = molSpawnCmd.Flags().MarkDeprecated("persistent", "use --pour instead") // Only fails if flag missing - - molCmd.AddCommand(molSpawnCmd) -} diff --git a/cmd/bd/pour.go b/cmd/bd/pour.go index d4684eb3..a0ce6938 100644 --- a/cmd/bd/pour.go +++ b/cmd/bd/pour.go @@ -12,7 +12,6 @@ import ( ) // pourCmd is a top-level command for instantiating protos as persistent mols. -// It's the "chemistry" alias for: bd mol spawn --pour // // In the molecular chemistry metaphor: // - Proto (solid) -> pour -> Mol (liquid) @@ -32,8 +31,6 @@ Use pour for: - Important work needing audit trail - Anything you might need to reference later -Equivalent to: bd mol spawn --pour - Examples: bd pour mol-feature --var name=auth # Create persistent mol from proto bd pour mol-release --var version=1.0 # Release workflow diff --git a/cmd/bd/wisp.go b/cmd/bd/wisp.go index 173b4d65..ccd83a97 100644 --- a/cmd/bd/wisp.go +++ b/cmd/bd/wisp.go @@ -93,8 +93,6 @@ The wisp will: - NOT sync to remote - Either evaporate (burn) or condense to digest (squash) -Equivalent to: bd mol spawn - Examples: bd wisp create mol-patrol # Ephemeral patrol cycle bd wisp create mol-health-check # One-time health check diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 514e5223..7a46ebd2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -273,12 +273,12 @@ open ──▶ in_progress ──▶ closed ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ bd mol spawn │───▶│ Wisp Issues │───▶│ bd mol squash │ -│ (from template)│ │ (local-only) │ │ (→ digest) │ +│ bd wisp create │───▶│ Wisp Issues │───▶│ bd mol squash │ +│ (from template) │ │ (local-only) │ │ (→ digest) │ └─────────────────┘ └─────────────────┘ └─────────────────┘ ``` -1. **Spawn:** Create wisps from a molecule template +1. **Create:** Create wisps from a molecule template 2. **Execute:** Agent works through wisp steps (local SQLite only) 3. **Squash:** Compress wisps into a permanent digest issue diff --git a/docs/DELETIONS.md b/docs/DELETIONS.md index 7a564d00..1b9ac8a3 100644 --- a/docs/DELETIONS.md +++ b/docs/DELETIONS.md @@ -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 mol spawn`) are intentionally excluded from tombstone tracking. +**Wisps** (ephemeral issues created by `bd wisp create`) are intentionally excluded from tombstone tracking. ### Why Wisps Don't Need Tombstones