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 4fbf3e11d0
commit 707fc17583

View File

@@ -2,18 +2,14 @@ package cmd
import ( import (
"bytes" "bytes"
"crypto/rand"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/git" "github.com/steveyegge/gastown/internal/git"
"github.com/steveyegge/gastown/internal/mail" "github.com/steveyegge/gastown/internal/mail"
@@ -250,100 +246,54 @@ func runSpawn(cmd *cobra.Command, args []string) error {
// Handle molecule instantiation if specified // Handle molecule instantiation if specified
if spawnMolecule != "" { if spawnMolecule != "" {
// Use main beads to get the molecule template and parent issue // Use bd mol run to spawn the molecule - this handles everything:
mainBeads := beads.New(beadsPath) // - 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 cmd := exec.Command("bd", "--no-daemon", "mol", "run", spawnMolecule,
mol, err := mainBeads.Show(spawnMolecule) "--var", "issue="+spawnIssue, "--json")
if err != nil { cmd.Dir = beadsPath
return fmt.Errorf("getting molecule %s: %w", spawnMolecule, err)
}
if mol.Type != "molecule" { var stdout, stderr bytes.Buffer
return fmt.Errorf("%s is not a molecule (type: %s)", spawnMolecule, mol.Type) cmd.Stdout = &stdout
} cmd.Stderr = &stderr
// Validate the molecule if err := cmd.Run(); err != nil {
if err := beads.ValidateMolecule(mol); err != nil { errMsg := strings.TrimSpace(stderr.String())
return fmt.Errorf("invalid molecule: %w", err) if errMsg != "" {
} return fmt.Errorf("running molecule: %s", errMsg)
// 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
} }
return fmt.Errorf("running molecule: %w", err)
} }
if firstReadyStep == nil { // Parse mol run output
return fmt.Errorf("no ready step found in molecule (all steps have dependencies)") 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{ moleculeCtx = &MoleculeContext{
MoleculeID: spawnMolecule, MoleculeID: spawnMolecule,
RootIssueID: spawnIssue, // Original source issue in main beads RootIssueID: molResult.RootID,
TotalSteps: len(steps), TotalSteps: molResult.Created - 1, // -1 for root
StepNumber: stepNumber, StepNumber: 1, // Starting on first step
EphemeralInstanceID: ephInstanceID,
} }
// Switch to spawning on the first ready step // Update spawnIssue to be the molecule root (for assignment tracking)
// Note: The step is in ephemeral beads, but we still assign the source issue spawnIssue = molResult.RootID
// 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
} }
// Get or create issue // Get or create issue
@@ -630,11 +580,10 @@ func buildSpawnContext(issue *BeadsIssue, message string) string {
// MoleculeContext contains information about a molecule workflow assignment. // MoleculeContext contains information about a molecule workflow assignment.
type MoleculeContext struct { type MoleculeContext struct {
MoleculeID string // The molecule template ID MoleculeID string // The molecule template ID (proto)
RootIssueID string // The parent issue (molecule root) RootIssueID string // The spawned molecule root issue
TotalSteps int // Total number of steps in the molecule TotalSteps int // Total number of steps in the molecule
StepNumber int // Which step this is (1-indexed) StepNumber int // Which step this is (1-indexed)
EphemeralInstanceID string // Ephemeral instance ID (e.g., "eph-abc123")
} }
// buildWorkAssignmentMail creates a work assignment mail message for a polecat. // 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 issue != nil {
if moleculeCtx != nil { if moleculeCtx != nil {
if moleculeCtx.EphemeralInstanceID != "" { subject = fmt.Sprintf("🧬 Molecule: %s (step %d/%d)", issue.Title, moleculeCtx.StepNumber, moleculeCtx.TotalSteps)
// 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)
}
} else { } else {
subject = fmt.Sprintf("📋 Work Assignment: %s", issue.Title) subject = fmt.Sprintf("📋 Work Assignment: %s", issue.Title)
} }
@@ -677,21 +621,9 @@ func buildWorkAssignmentMail(issue *BeadsIssue, message, polecatAddress string,
if moleculeCtx != nil { if moleculeCtx != nil {
body.WriteString("\n## Molecule Workflow\n") 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("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)) body.WriteString(fmt.Sprintf("Molecule root: %s\n\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("After completing this step:\n") body.WriteString("After completing this step:\n")
if issue != nil { body.WriteString("1. Run `bd close <step-id>`\n")
body.WriteString("1. Run `bd close " + issue.ID + "`\n")
} else {
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("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("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") 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
}