From d6ab9ab62c2575a9c17cdca15e4e7be5ef82ed16 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 21 Dec 2025 13:44:25 -0800 Subject: [PATCH] feat(mol): spawn molecules as ephemeral by default (bd-2vh3.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Molecule spawning now creates ephemeral issues that can be bulk-deleted when closed using `bd cleanup --ephemeral`. This supports the ephemeral molecule workflow pattern where execution traces are cleaned up while outcomes persist. Changes: - Add `ephemeral` parameter to cloneSubgraph() and spawnMolecule() - mol spawn: ephemeral=true by default, add --persistent flag to opt out - mol run: ephemeral=true (molecule execution) - mol bond: ephemeral=true (bonded protos) - template instantiate: ephemeral=false (deprecated, backwards compat) This is Tier 1 of the ephemeral molecule workflow epic (bd-2vh3). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/bd/mol.go | 5 +++-- cmd/bd/mol_bond.go | 4 ++-- cmd/bd/mol_run.go | 4 ++-- cmd/bd/mol_spawn.go | 6 +++++- cmd/bd/template.go | 8 +++++--- cmd/bd/template_test.go | 8 ++++---- 6 files changed, 21 insertions(+), 14 deletions(-) diff --git a/cmd/bd/mol.go b/cmd/bd/mol.go index c7697a8f..55e5afc7 100644 --- a/cmd/bd/mol.go +++ b/cmd/bd/mol.go @@ -62,8 +62,9 @@ Commands: // spawnMolecule creates new issues from the proto with variable substitution. // This instantiates a proto (template) into a molecule (real issues). // Wraps cloneSubgraph from template.go and returns InstantiateResult. -func spawnMolecule(ctx context.Context, s storage.Storage, subgraph *MoleculeSubgraph, vars map[string]string, assignee string, actorName string) (*InstantiateResult, error) { - return cloneSubgraph(ctx, s, subgraph, vars, assignee, actorName) +// If ephemeral is true, spawned issues are marked for bulk deletion when closed. +func spawnMolecule(ctx context.Context, s storage.Storage, subgraph *MoleculeSubgraph, vars map[string]string, assignee string, actorName string, ephemeral bool) (*InstantiateResult, error) { + return cloneSubgraph(ctx, s, subgraph, vars, assignee, actorName, ephemeral) } // printMoleculeTree prints the molecule structure as a tree diff --git a/cmd/bd/mol_bond.go b/cmd/bd/mol_bond.go index 9b2728e1..1f56ff5b 100644 --- a/cmd/bd/mol_bond.go +++ b/cmd/bd/mol_bond.go @@ -283,8 +283,8 @@ func bondProtoMol(ctx context.Context, s storage.Storage, proto, mol *types.Issu return nil, fmt.Errorf("missing required variables: %s (use --var)", strings.Join(missingVars, ", ")) } - // Spawn the proto - spawnResult, err := spawnMolecule(ctx, s, subgraph, vars, "", actorName) + // Spawn the proto (ephemeral by default for molecule execution - bd-2vh3) + spawnResult, err := spawnMolecule(ctx, s, subgraph, vars, "", actorName, true) if err != nil { return nil, fmt.Errorf("spawning proto: %w", err) } diff --git a/cmd/bd/mol_run.go b/cmd/bd/mol_run.go index 438da634..cf76deb8 100644 --- a/cmd/bd/mol_run.go +++ b/cmd/bd/mol_run.go @@ -89,8 +89,8 @@ func runMolRun(cmd *cobra.Command, args []string) { os.Exit(1) } - // Spawn the molecule with actor as assignee - result, err := spawnMolecule(ctx, store, subgraph, vars, actor, actor) + // Spawn the molecule with actor as assignee (ephemeral for cleanup - bd-2vh3) + result, err := spawnMolecule(ctx, store, subgraph, vars, actor, actor, true) if err != nil { fmt.Fprintf(os.Stderr, "Error spawning molecule: %v\n", err) os.Exit(1) diff --git a/cmd/bd/mol_spawn.go b/cmd/bd/mol_spawn.go index 05909ab3..0aed479e 100644 --- a/cmd/bd/mol_spawn.go +++ b/cmd/bd/mol_spawn.go @@ -53,6 +53,7 @@ func runMolSpawn(cmd *cobra.Command, args []string) { assignee, _ := cmd.Flags().GetString("assignee") attachFlags, _ := cmd.Flags().GetStringSlice("attach") attachType, _ := cmd.Flags().GetString("attach-type") + persistent, _ := cmd.Flags().GetBool("persistent") // Parse variables vars := make(map[string]string) @@ -181,7 +182,9 @@ func runMolSpawn(cmd *cobra.Command, args []string) { } // Clone the subgraph (spawn the molecule) - result, err := spawnMolecule(ctx, store, subgraph, vars, assignee, actor) + // Spawned molecules are ephemeral by default (bd-2vh3) - use --persistent to opt out + ephemeral := !persistent + result, err := spawnMolecule(ctx, store, subgraph, vars, assignee, actor, ephemeral) if err != nil { fmt.Fprintf(os.Stderr, "Error spawning molecule: %v\n", err) os.Exit(1) @@ -233,6 +236,7 @@ func init() { 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("persistent", false, "Create non-ephemeral issues (default: ephemeral for cleanup)") molCmd.AddCommand(molSpawnCmd) } diff --git a/cmd/bd/template.go b/cmd/bd/template.go index 9d7ecfef..327355cb 100644 --- a/cmd/bd/template.go +++ b/cmd/bd/template.go @@ -292,8 +292,8 @@ Example: return } - // Clone the subgraph - result, err := cloneSubgraph(ctx, store, subgraph, vars, assignee, actor) + // Clone the subgraph (deprecated command, non-ephemeral for backwards compatibility) + result, err := cloneSubgraph(ctx, store, subgraph, vars, assignee, actor, false) if err != nil { fmt.Fprintf(os.Stderr, "Error instantiating template: %v\n", err) os.Exit(1) @@ -453,7 +453,8 @@ func substituteVariables(text string, vars map[string]string) string { // cloneSubgraph creates new issues from the template with variable substitution // If assignee is non-empty, it will be set on the root epic -func cloneSubgraph(ctx context.Context, s storage.Storage, subgraph *TemplateSubgraph, vars map[string]string, assignee string, actorName string) (*InstantiateResult, error) { +// If ephemeral is true, spawned issues are marked for bulk deletion when closed (bd-2vh3) +func cloneSubgraph(ctx context.Context, s storage.Storage, subgraph *TemplateSubgraph, vars map[string]string, assignee string, actorName string, ephemeral bool) (*InstantiateResult, error) { if s == nil { return nil, fmt.Errorf("no database connection") } @@ -483,6 +484,7 @@ func cloneSubgraph(ctx context.Context, s storage.Storage, subgraph *TemplateSub IssueType: oldIssue.IssueType, Assignee: issueAssignee, EstimatedMinutes: oldIssue.EstimatedMinutes, + Ephemeral: ephemeral, // bd-2vh3: mark for cleanup when closed CreatedAt: time.Now(), UpdatedAt: time.Now(), } diff --git a/cmd/bd/template_test.go b/cmd/bd/template_test.go index d984c3ab..1c154602 100644 --- a/cmd/bd/template_test.go +++ b/cmd/bd/template_test.go @@ -268,7 +268,7 @@ func TestCloneSubgraph(t *testing.T) { } vars := map[string]string{"version": "2.0.0"} - result, err := cloneSubgraph(ctx, s, subgraph, vars, "", "test-user") + result, err := cloneSubgraph(ctx, s, subgraph, vars, "", "test-user", false) if err != nil { t.Fatalf("cloneSubgraph failed: %v", err) } @@ -308,7 +308,7 @@ func TestCloneSubgraph(t *testing.T) { } vars := map[string]string{"service": "api-gateway"} - result, err := cloneSubgraph(ctx, s, subgraph, vars, "", "test-user") + result, err := cloneSubgraph(ctx, s, subgraph, vars, "", "test-user", false) if err != nil { t.Fatalf("cloneSubgraph failed: %v", err) } @@ -367,7 +367,7 @@ func TestCloneSubgraph(t *testing.T) { t.Fatalf("loadTemplateSubgraph failed: %v", err) } - result, err := cloneSubgraph(ctx, s, subgraph, nil, "", "test-user") + result, err := cloneSubgraph(ctx, s, subgraph, nil, "", "test-user", false) if err != nil { t.Fatalf("cloneSubgraph failed: %v", err) } @@ -402,7 +402,7 @@ func TestCloneSubgraph(t *testing.T) { } // Clone with assignee override - result, err := cloneSubgraph(ctx, s, subgraph, nil, "new-assignee", "test-user") + result, err := cloneSubgraph(ctx, s, subgraph, nil, "new-assignee", "test-user", false) if err != nil { t.Fatalf("cloneSubgraph failed: %v", err) }