feat(mol): spawn molecules as ephemeral by default (bd-2vh3.2)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -62,8 +62,9 @@ Commands:
|
|||||||
// spawnMolecule creates new issues from the proto with variable substitution.
|
// spawnMolecule creates new issues from the proto with variable substitution.
|
||||||
// This instantiates a proto (template) into a molecule (real issues).
|
// This instantiates a proto (template) into a molecule (real issues).
|
||||||
// Wraps cloneSubgraph from template.go and returns InstantiateResult.
|
// 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) {
|
// If ephemeral is true, spawned issues are marked for bulk deletion when closed.
|
||||||
return cloneSubgraph(ctx, s, subgraph, vars, assignee, actorName)
|
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
|
// printMoleculeTree prints the molecule structure as a tree
|
||||||
|
|||||||
@@ -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, ", "))
|
return nil, fmt.Errorf("missing required variables: %s (use --var)", strings.Join(missingVars, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawn the proto
|
// Spawn the proto (ephemeral by default for molecule execution - bd-2vh3)
|
||||||
spawnResult, err := spawnMolecule(ctx, s, subgraph, vars, "", actorName)
|
spawnResult, err := spawnMolecule(ctx, s, subgraph, vars, "", actorName, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("spawning proto: %w", err)
|
return nil, fmt.Errorf("spawning proto: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,8 +89,8 @@ func runMolRun(cmd *cobra.Command, args []string) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawn the molecule with actor as assignee
|
// Spawn the molecule with actor as assignee (ephemeral for cleanup - bd-2vh3)
|
||||||
result, err := spawnMolecule(ctx, store, subgraph, vars, actor, actor)
|
result, err := spawnMolecule(ctx, store, subgraph, vars, actor, actor, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error spawning molecule: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error spawning molecule: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ func runMolSpawn(cmd *cobra.Command, args []string) {
|
|||||||
assignee, _ := cmd.Flags().GetString("assignee")
|
assignee, _ := cmd.Flags().GetString("assignee")
|
||||||
attachFlags, _ := cmd.Flags().GetStringSlice("attach")
|
attachFlags, _ := cmd.Flags().GetStringSlice("attach")
|
||||||
attachType, _ := cmd.Flags().GetString("attach-type")
|
attachType, _ := cmd.Flags().GetString("attach-type")
|
||||||
|
persistent, _ := cmd.Flags().GetBool("persistent")
|
||||||
|
|
||||||
// Parse variables
|
// Parse variables
|
||||||
vars := make(map[string]string)
|
vars := make(map[string]string)
|
||||||
@@ -181,7 +182,9 @@ func runMolSpawn(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clone the subgraph (spawn the molecule)
|
// 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 {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error spawning molecule: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error spawning molecule: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -233,6 +236,7 @@ func init() {
|
|||||||
molSpawnCmd.Flags().String("assignee", "", "Assign the root issue to this agent/user")
|
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().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().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)
|
molCmd.AddCommand(molSpawnCmd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -292,8 +292,8 @@ Example:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone the subgraph
|
// Clone the subgraph (deprecated command, non-ephemeral for backwards compatibility)
|
||||||
result, err := cloneSubgraph(ctx, store, subgraph, vars, assignee, actor)
|
result, err := cloneSubgraph(ctx, store, subgraph, vars, assignee, actor, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error instantiating template: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error instantiating template: %v\n", err)
|
||||||
os.Exit(1)
|
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
|
// cloneSubgraph creates new issues from the template with variable substitution
|
||||||
// If assignee is non-empty, it will be set on the root epic
|
// 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 {
|
if s == nil {
|
||||||
return nil, fmt.Errorf("no database connection")
|
return nil, fmt.Errorf("no database connection")
|
||||||
}
|
}
|
||||||
@@ -483,6 +484,7 @@ func cloneSubgraph(ctx context.Context, s storage.Storage, subgraph *TemplateSub
|
|||||||
IssueType: oldIssue.IssueType,
|
IssueType: oldIssue.IssueType,
|
||||||
Assignee: issueAssignee,
|
Assignee: issueAssignee,
|
||||||
EstimatedMinutes: oldIssue.EstimatedMinutes,
|
EstimatedMinutes: oldIssue.EstimatedMinutes,
|
||||||
|
Ephemeral: ephemeral, // bd-2vh3: mark for cleanup when closed
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ func TestCloneSubgraph(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
vars := map[string]string{"version": "2.0.0"}
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("cloneSubgraph failed: %v", err)
|
t.Fatalf("cloneSubgraph failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -308,7 +308,7 @@ func TestCloneSubgraph(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
vars := map[string]string{"service": "api-gateway"}
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("cloneSubgraph failed: %v", err)
|
t.Fatalf("cloneSubgraph failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -367,7 +367,7 @@ func TestCloneSubgraph(t *testing.T) {
|
|||||||
t.Fatalf("loadTemplateSubgraph failed: %v", err)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("cloneSubgraph failed: %v", err)
|
t.Fatalf("cloneSubgraph failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -402,7 +402,7 @@ func TestCloneSubgraph(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clone with assignee override
|
// 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 {
|
if err != nil {
|
||||||
t.Fatalf("cloneSubgraph failed: %v", err)
|
t.Fatalf("cloneSubgraph failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user