feat(mol): rename bond→spawn, add BondRef data model

Molecule evolution:
- Rename `bd mol bond` to `bd mol spawn` for instantiation
- Add BondRef type for tracking compound lineage
- Add `bonded_from` field to Issue for compound molecules
- Add BondType constants (sequential, parallel, conditional, root)
- Add IsCompound() and GetConstituents() helpers
- Add 'protomolecule' easter egg alias

Closes: bd-mh4w, bd-rnnr
Part of: bd-o5xe (Molecule bonding epic)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-21 01:19:59 -08:00
parent 9336d2dd1a
commit 466c606eb9
2 changed files with 103 additions and 54 deletions

View File

@@ -18,14 +18,17 @@ import (
// Molecule commands - work templates for agent workflows // Molecule commands - work templates for agent workflows
// //
// Terminology: // Terminology:
// - Molecule: A template epic with child issues forming a DAG workflow // - Proto: Uninstantiated template (easter egg: 'protomolecule' alias)
// - Bond: Instantiate a molecule, creating real issues from the template // - Molecule: A spawned instance of a proto
// - Catalog: List available molecules // - Spawn: Instantiate a proto, creating real issues from the template
// - Bond: Polymorphic combine operation (proto+proto, proto+mol, mol+mol)
// - Distill: Extract ad-hoc epic → reusable proto
// - Compound: Result of bonding
// //
// Usage: // Usage:
// bd mol catalog # List available molecules // bd mol catalog # List available protos
// bd mol show <id> # Show molecule structure // bd mol show <id> # Show proto/molecule structure
// bd mol bond <id> --var key=value # Create issues from molecule // bd mol spawn <id> --var key=value # Instantiate proto → molecule
// MoleculeLabel is the label used to identify molecules (templates) // MoleculeLabel is the label used to identify molecules (templates)
// Molecules use the same label as templates - they ARE templates with workflow semantics // Molecules use the same label as templates - they ARE templates with workflow semantics
@@ -36,22 +39,26 @@ const MoleculeLabel = BeadsTemplateLabel
type MoleculeSubgraph = TemplateSubgraph type MoleculeSubgraph = TemplateSubgraph
var molCmd = &cobra.Command{ var molCmd = &cobra.Command{
Use: "mol", Use: "mol",
Short: "Molecule commands (work templates)", Aliases: []string{"protomolecule"}, // Easter egg for The Expanse fans
Short: "Molecule commands (work templates)",
Long: `Manage molecules - work templates for agent workflows. Long: `Manage molecules - work templates for agent workflows.
Molecules are epics with the "template" label. They define a DAG of work Protos are template epics with the "template" label. They define a DAG of work
that can be instantiated ("bonded") to create real issues. that can be spawned to create real issues (molecules).
The molecule metaphor: The molecule metaphor:
- A molecule is a template (reusable work pattern) - A proto is an uninstantiated template (reusable work pattern)
- Bonding creates new issues from the template - Spawning creates a molecule (real issues) from the proto
- Variables ({{key}}) are substituted during bonding - Variables ({{key}}) are substituted during spawning
- Bonding combines protos or molecules into compounds
- Distilling extracts a proto from an ad-hoc epic
Commands: Commands:
catalog List available molecules catalog List available protos
show Show molecule structure and variables show Show proto/molecule structure and variables
bond Create issues from a molecule template`, spawn Instantiate a proto → molecule
run Spawn + assign + pin for durable execution`,
} }
var molCatalogCmd = &cobra.Command{ var molCatalogCmd = &cobra.Command{
@@ -97,17 +104,17 @@ var molCatalogCmd = &cobra.Command{
} }
if len(molecules) == 0 { if len(molecules) == 0 {
fmt.Println("No molecules available.") fmt.Println("No protos available.")
fmt.Println("\nTo create a molecule:") fmt.Println("\nTo create a proto:")
fmt.Println(" 1. Create an epic with child issues") fmt.Println(" 1. Create an epic with child issues")
fmt.Println(" 2. Add the 'template' label: bd label add <epic-id> template") fmt.Println(" 2. Add the 'template' label: bd label add <epic-id> template")
fmt.Println(" 3. Use {{variable}} placeholders in titles/descriptions") fmt.Println(" 3. Use {{variable}} placeholders in titles/descriptions")
fmt.Println("\nTo bond (instantiate) a molecule:") fmt.Println("\nTo spawn (instantiate) a molecule from a proto:")
fmt.Println(" bd mol bond <id> --var key=value") fmt.Println(" bd mol spawn <id> --var key=value")
return return
} }
fmt.Printf("%s\n", ui.RenderPass("Molecules (for bd mol bond):")) fmt.Printf("%s\n", ui.RenderPass("Protos (for bd mol spawn):"))
for _, mol := range molecules { for _, mol := range molecules {
vars := extractVariables(mol.Title + " " + mol.Description) vars := extractVariables(mol.Title + " " + mol.Description)
varStr := "" varStr := ""
@@ -182,28 +189,28 @@ func showMolecule(subgraph *MoleculeSubgraph) {
fmt.Println() fmt.Println()
} }
var molBondCmd = &cobra.Command{ var molSpawnCmd = &cobra.Command{
Use: "bond <molecule-id>", Use: "spawn <proto-id>",
Short: "Create issues from a molecule template", Short: "Instantiate a proto into a molecule",
Long: `Bond (instantiate) a molecule by creating real issues from its template. Long: `Spawn a molecule by instantiating a proto template into real issues.
Variables are specified with --var key=value flags. The molecule's {{key}} Variables are specified with --var key=value flags. The proto's {{key}}
placeholders will be replaced with the corresponding values. placeholders will be replaced with the corresponding values.
Example: Example:
bd mol bond mol-code-review --var pr=123 --var repo=myproject bd mol spawn mol-code-review --var pr=123 --var repo=myproject
bd mol bond bd-abc123 --var version=1.2.0 --assignee=worker-1`, bd mol spawn bd-abc123 --var version=1.2.0 --assignee=worker-1`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
CheckReadonly("mol bond") CheckReadonly("mol spawn")
ctx := rootCtx ctx := rootCtx
// mol bond requires direct store access for subgraph loading and cloning // mol spawn requires direct store access for subgraph loading and cloning
if store == nil { if store == nil {
if daemonClient != nil { if daemonClient != nil {
fmt.Fprintf(os.Stderr, "Error: mol bond requires direct database access\n") 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 bond %s ...\n", args[0]) fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon mol spawn %s ...\n", args[0])
} else { } else {
fmt.Fprintf(os.Stderr, "Error: no database connection\n") fmt.Fprintf(os.Stderr, "Error: no database connection\n")
} }
@@ -272,10 +279,10 @@ Example:
return return
} }
// Clone the subgraph (bond the molecule) // Clone the subgraph (spawn the molecule)
result, err := bondMolecule(ctx, store, subgraph, vars, assignee, actor) result, err := spawnMolecule(ctx, store, subgraph, vars, assignee, actor)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Error bonding molecule: %v\n", err) fmt.Fprintf(os.Stderr, "Error spawning molecule: %v\n", err)
os.Exit(1) os.Exit(1)
} }
@@ -287,18 +294,18 @@ Example:
return return
} }
fmt.Printf("%s Bonded molecule: created %d issues\n", ui.RenderPass("✓"), result.Created) fmt.Printf("%s Spawned molecule: created %d issues\n", ui.RenderPass("✓"), result.Created)
fmt.Printf(" Root issue: %s\n", result.NewEpicID) fmt.Printf(" Root issue: %s\n", result.NewEpicID)
}, },
} }
var molRunCmd = &cobra.Command{ var molRunCmd = &cobra.Command{
Use: "run <molecule-id>", Use: "run <proto-id>",
Short: "Bond molecule and start execution (bond + assign + pin)", Short: "Spawn proto and start execution (spawn + assign + pin)",
Long: `Run a molecule by bonding it and setting up for durable execution. Long: `Run a molecule by spawning a proto and setting up for durable execution.
This command: This command:
1. Bonds the molecule (creates issues from template) 1. Spawns the molecule (creates issues from proto template)
2. Assigns the root issue to the caller 2. Assigns the root issue to the caller
3. Sets root status to in_progress 3. Sets root status to in_progress
4. Pins the root issue for session recovery 4. Pins the root issue for session recovery
@@ -367,10 +374,10 @@ Example:
os.Exit(1) os.Exit(1)
} }
// Bond the molecule with actor as assignee // Spawn the molecule with actor as assignee
result, err := bondMolecule(ctx, store, subgraph, vars, actor, actor) result, err := spawnMolecule(ctx, store, subgraph, vars, actor, actor)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Error bonding molecule: %v\n", err) fmt.Fprintf(os.Stderr, "Error spawning molecule: %v\n", err)
os.Exit(1) os.Exit(1)
} }
@@ -410,15 +417,15 @@ Example:
} }
func init() { func init() {
molBondCmd.Flags().StringSlice("var", []string{}, "Variable substitution (key=value)") molSpawnCmd.Flags().StringSlice("var", []string{}, "Variable substitution (key=value)")
molBondCmd.Flags().Bool("dry-run", false, "Preview what would be created") molSpawnCmd.Flags().Bool("dry-run", false, "Preview what would be created")
molBondCmd.Flags().String("assignee", "", "Assign the root issue to this agent/user") molSpawnCmd.Flags().String("assignee", "", "Assign the root issue to this agent/user")
molRunCmd.Flags().StringSlice("var", []string{}, "Variable substitution (key=value)") molRunCmd.Flags().StringSlice("var", []string{}, "Variable substitution (key=value)")
molCmd.AddCommand(molCatalogCmd) molCmd.AddCommand(molCatalogCmd)
molCmd.AddCommand(molShowCmd) molCmd.AddCommand(molShowCmd)
molCmd.AddCommand(molBondCmd) molCmd.AddCommand(molSpawnCmd)
molCmd.AddCommand(molRunCmd) molCmd.AddCommand(molRunCmd)
rootCmd.AddCommand(molCmd) rootCmd.AddCommand(molCmd)
} }
@@ -427,9 +434,10 @@ func init() {
// Molecule Helper Functions // Molecule Helper Functions
// ============================================================================= // =============================================================================
// bondMolecule creates new issues from the molecule with variable substitution // spawnMolecule creates new issues from the proto with variable substitution.
// Wraps cloneSubgraph from template.go and returns BondResult // This instantiates a proto (template) into a molecule (real issues).
func bondMolecule(ctx context.Context, s storage.Storage, subgraph *MoleculeSubgraph, vars map[string]string, assignee string, actorName string) (*InstantiateResult, error) { // Wraps cloneSubgraph from template.go and returns SpawnResult.
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) return cloneSubgraph(ctx, s, subgraph, vars, assignee, actorName)
} }

View File

@@ -51,6 +51,9 @@ type Issue struct {
// Template field (beads-1ra): template molecule support // Template field (beads-1ra): template molecule support
IsTemplate bool `json:"is_template,omitempty"` // If true, issue is a read-only template molecule IsTemplate bool `json:"is_template,omitempty"` // If true, issue is a read-only template molecule
// Bonding fields (bd-rnnr): compound molecule lineage
BondedFrom []BondRef `json:"bonded_from,omitempty"` // For compounds: constituent protos
} }
// ComputeContentHash creates a deterministic hash of the issue's content. // ComputeContentHash creates a deterministic hash of the issue's content.
@@ -90,6 +93,16 @@ func (i *Issue) ComputeContentHash() string {
if i.IsTemplate { if i.IsTemplate {
h.Write([]byte("template")) h.Write([]byte("template"))
} }
h.Write([]byte{0})
// Hash bonded_from for compound molecules (bd-rnnr)
for _, br := range i.BondedFrom {
h.Write([]byte(br.ProtoID))
h.Write([]byte{0})
h.Write([]byte(br.BondType))
h.Write([]byte{0})
h.Write([]byte(br.BondPoint))
h.Write([]byte{0})
}
return fmt.Sprintf("%x", h.Sum(nil)) return fmt.Sprintf("%x", h.Sum(nil))
} }
@@ -531,8 +544,36 @@ type StaleFilter struct {
// EpicStatus represents an epic with its completion status // EpicStatus represents an epic with its completion status
type EpicStatus struct { type EpicStatus struct {
Epic *Issue `json:"epic"` Epic *Issue `json:"epic"`
TotalChildren int `json:"total_children"` TotalChildren int `json:"total_children"`
ClosedChildren int `json:"closed_children"` ClosedChildren int `json:"closed_children"`
EligibleForClose bool `json:"eligible_for_close"` EligibleForClose bool `json:"eligible_for_close"`
}
// BondRef tracks compound molecule lineage (bd-rnnr).
// When protos or molecules are bonded together, BondRefs record
// which sources were combined and how they were attached.
type BondRef struct {
ProtoID string `json:"proto_id"` // Source proto/molecule ID
BondType string `json:"bond_type"` // sequential, parallel, conditional
BondPoint string `json:"bond_point,omitempty"` // Attachment site (issue ID or empty for root)
}
// Bond type constants for compound molecules
const (
BondTypeSequential = "sequential" // B runs after A completes
BondTypeParallel = "parallel" // B runs alongside A
BondTypeConditional = "conditional" // B runs only if A fails
BondTypeRoot = "root" // Marks the primary/root component
)
// IsCompound returns true if this issue is a compound (bonded from multiple sources).
func (i *Issue) IsCompound() bool {
return len(i.BondedFrom) > 0
}
// GetConstituents returns the BondRefs for this compound's constituent protos.
// Returns nil for non-compound issues.
func (i *Issue) GetConstituents() []BondRef {
return i.BondedFrom
} }