Simplify spawn.go to use bd mol run for molecules (gt-47tq)

Replace 95 lines of custom molecule instantiation with bd mol run:
- No more ephemeral beads layer
- No more manual molecule template handling
- bd mol run handles spawning, assignment, and pinning

🤖 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 21:59:47 -08:00
parent b8e8237dbc
commit bc334502cf

View File

@@ -2,18 +2,14 @@ package cmd
import (
"bytes"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/git"
"github.com/steveyegge/gastown/internal/mail"
@@ -250,100 +246,54 @@ func runSpawn(cmd *cobra.Command, args []string) error {
// Handle molecule instantiation if specified
if spawnMolecule != "" {
// Use main beads to get the molecule template and parent issue
mainBeads := beads.New(beadsPath)
// Use bd mol run to spawn the molecule - this handles everything:
// - Creates child issues from proto template
// - Assigns root to polecat
// - Sets root status to in_progress
// - Pins root for session recovery
fmt.Printf("Running molecule %s on %s...\n", spawnMolecule, spawnIssue)
// Get the molecule template from main beads
mol, err := mainBeads.Show(spawnMolecule)
if err != nil {
return fmt.Errorf("getting molecule %s: %w", spawnMolecule, err)
}
cmd := exec.Command("bd", "--no-daemon", "mol", "run", spawnMolecule,
"--var", "issue="+spawnIssue, "--json")
cmd.Dir = beadsPath
if mol.Type != "molecule" {
return fmt.Errorf("%s is not a molecule (type: %s)", spawnMolecule, mol.Type)
}
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
// Validate the molecule
if err := beads.ValidateMolecule(mol); err != nil {
return fmt.Errorf("invalid molecule: %w", err)
}
// Get the parent issue from main beads
parent, err := mainBeads.Show(spawnIssue)
if err != nil {
return fmt.Errorf("getting parent issue %s: %w", spawnIssue, err)
}
// Generate ephemeral instance ID
ephInstanceID, err := generateEphemeralInstanceID()
if err != nil {
return fmt.Errorf("generating ephemeral instance ID: %w", err)
}
// Ensure ephemeral beads directory exists
ephemeralPath, err := ensureEphemeralBeadsDir(r.Path)
if err != nil {
return fmt.Errorf("setting up ephemeral beads: %w", err)
}
// Create ephemeral beads instance for molecule instantiation
ephBeads := beads.New(ephemeralPath)
// Create an ephemeral parent issue to hold the molecule steps
// This links back to the source issue in main beads
ephParentDesc := fmt.Sprintf("Ephemeral molecule execution.\n\nsource_issue: %s\nmolecule: %s\ninstance: %s",
spawnIssue, spawnMolecule, ephInstanceID)
ephParent, err := ephBeads.Create(beads.CreateOptions{
Title: fmt.Sprintf("[%s] %s", ephInstanceID, parent.Title),
Type: "task",
Priority: parent.Priority,
Description: ephParentDesc,
})
if err != nil {
return fmt.Errorf("creating ephemeral parent issue: %w", err)
}
// Instantiate the molecule in ephemeral beads
fmt.Printf("Bonding molecule %s on %s (ephemeral: %s)...\n", spawnMolecule, spawnIssue, ephInstanceID)
steps, err := ephBeads.InstantiateMolecule(mol, ephParent, beads.InstantiateOptions{})
if err != nil {
return fmt.Errorf("instantiating molecule in ephemeral: %w", err)
}
fmt.Printf("%s Bonded %d steps in ephemeral\n", style.Bold.Render("✓"), len(steps))
for _, step := range steps {
fmt.Printf(" %s: %s\n", style.Dim.Render(step.ID), step.Title)
}
// Find the first ready step (one with no dependencies)
var firstReadyStep *beads.Issue
var stepNumber int
for i, step := range steps {
if len(step.DependsOn) == 0 {
firstReadyStep = step
stepNumber = i + 1
break
if err := cmd.Run(); err != nil {
errMsg := strings.TrimSpace(stderr.String())
if errMsg != "" {
return fmt.Errorf("running molecule: %s", errMsg)
}
return fmt.Errorf("running molecule: %w", err)
}
if firstReadyStep == nil {
return fmt.Errorf("no ready step found in molecule (all steps have dependencies)")
// Parse mol run output
var molResult struct {
RootID string `json:"root_id"`
IDMapping map[string]string `json:"id_mapping"`
Created int `json:"created"`
Assignee string `json:"assignee"`
Pinned bool `json:"pinned"`
}
if err := json.Unmarshal(stdout.Bytes(), &molResult); err != nil {
return fmt.Errorf("parsing molecule result: %w", err)
}
// Build molecule context for work assignment with ephemeral info
fmt.Printf("%s Molecule spawned: %s (%d steps)\n",
style.Bold.Render("✓"), molResult.RootID, molResult.Created-1) // -1 for root
// Build molecule context for work assignment
moleculeCtx = &MoleculeContext{
MoleculeID: spawnMolecule,
RootIssueID: spawnIssue, // Original source issue in main beads
TotalSteps: len(steps),
StepNumber: stepNumber,
EphemeralInstanceID: ephInstanceID,
MoleculeID: spawnMolecule,
RootIssueID: molResult.RootID,
TotalSteps: molResult.Created - 1, // -1 for root
StepNumber: 1, // Starting on first step
}
// Switch to spawning on the first ready step
// Note: The step is in ephemeral beads, but we still assign the source issue
// in main beads to the polecat for tracking
fmt.Printf("\nSpawning on first ready step: %s\n", firstReadyStep.ID)
// Keep spawnIssue as the source issue for assignment tracking
// Update spawnIssue to be the molecule root (for assignment tracking)
spawnIssue = molResult.RootID
}
// Get or create issue
@@ -630,11 +580,10 @@ func buildSpawnContext(issue *BeadsIssue, message string) string {
// MoleculeContext contains information about a molecule workflow assignment.
type MoleculeContext struct {
MoleculeID string // The molecule template ID
RootIssueID string // The parent issue (molecule root)
TotalSteps int // Total number of steps in the molecule
StepNumber int // Which step this is (1-indexed)
EphemeralInstanceID string // Ephemeral instance ID (e.g., "eph-abc123")
MoleculeID string // The molecule template ID (proto)
RootIssueID string // The spawned molecule root issue
TotalSteps int // Total number of steps in the molecule
StepNumber int // Which step this is (1-indexed)
}
// buildWorkAssignmentMail creates a work assignment mail message for a polecat.
@@ -646,12 +595,7 @@ func buildWorkAssignmentMail(issue *BeadsIssue, message, polecatAddress string,
if issue != nil {
if moleculeCtx != nil {
if moleculeCtx.EphemeralInstanceID != "" {
// Ephemeral molecule format per spec
subject = fmt.Sprintf("📋 Work Assignment: %s [MOLECULE]", issue.Title)
} else {
subject = fmt.Sprintf("🧬 Molecule Step %d/%d: %s", moleculeCtx.StepNumber, moleculeCtx.TotalSteps, issue.Title)
}
subject = fmt.Sprintf("🧬 Molecule: %s (step %d/%d)", issue.Title, moleculeCtx.StepNumber, moleculeCtx.TotalSteps)
} else {
subject = fmt.Sprintf("📋 Work Assignment: %s", issue.Title)
}
@@ -677,21 +621,9 @@ func buildWorkAssignmentMail(issue *BeadsIssue, message, polecatAddress string,
if moleculeCtx != nil {
body.WriteString("\n## Molecule Workflow\n")
body.WriteString(fmt.Sprintf("You are working on step %d of %d in molecule %s.\n", moleculeCtx.StepNumber, moleculeCtx.TotalSteps, moleculeCtx.MoleculeID))
body.WriteString(fmt.Sprintf("Source issue: %s\n", moleculeCtx.RootIssueID))
if moleculeCtx.EphemeralInstanceID != "" {
body.WriteString(fmt.Sprintf("Molecule instance: %s (ephemeral)\n\n", moleculeCtx.EphemeralInstanceID))
body.WriteString("**IMPORTANT**: This is an ephemeral molecule workflow.\n")
body.WriteString("The molecule steps are tracked in `.beads-ephemeral/` (not main beads).\n")
body.WriteString("When complete, generate a summary and squash the molecule.\n\n")
} else {
body.WriteString("\n")
}
body.WriteString(fmt.Sprintf("Molecule root: %s\n\n", moleculeCtx.RootIssueID))
body.WriteString("After completing this step:\n")
if issue != nil {
body.WriteString("1. Run `bd close " + issue.ID + "`\n")
} else {
body.WriteString("1. Run `bd close <step-id>`\n")
}
body.WriteString("1. Run `bd close <step-id>`\n")
body.WriteString("2. Run `bd ready --parent " + moleculeCtx.RootIssueID + "` to find next ready steps\n")
body.WriteString("3. If more steps are ready, continue working on them\n")
body.WriteString("4. When all steps are done, run `gt done` to signal completion\n\n")
@@ -729,43 +661,3 @@ func buildWorkAssignmentMail(issue *BeadsIssue, message, polecatAddress string,
}
}
// generateEphemeralInstanceID creates a unique ephemeral instance ID.
// Format: "eph-" followed by 6 hex characters (e.g., "eph-abc123").
func generateEphemeralInstanceID() (string, error) {
b := make([]byte, 3)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("generating random bytes: %w", err)
}
return "eph-" + hex.EncodeToString(b), nil
}
// ensureEphemeralBeadsDir ensures the ephemeral beads directory exists and is initialized.
// Returns the path to the ephemeral beads directory.
func ensureEphemeralBeadsDir(rigPath string) (string, error) {
ephemeralPath := filepath.Join(rigPath, ".beads-ephemeral")
// Create directory if it doesn't exist
if err := os.MkdirAll(ephemeralPath, 0755); err != nil {
return "", fmt.Errorf("creating ephemeral beads dir: %w", err)
}
// Check if it's initialized as a git repo
gitDir := filepath.Join(ephemeralPath, ".git")
if _, err := os.Stat(gitDir); os.IsNotExist(err) {
// Initialize git repo
cmd := exec.Command("git", "init")
cmd.Dir = ephemeralPath
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("initializing git in ephemeral dir: %w", err)
}
// Create ephemeral config
configPath := filepath.Join(ephemeralPath, "config.yaml")
configContent := "ephemeral: true\n# No sync-branch - ephemeral is local only\n"
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
return "", fmt.Errorf("creating ephemeral config: %w", err)
}
}
return ephemeralPath, nil
}