refactor(cmd): split molecule.go into focused files
Split the 1929-line molecule.go into 5 focused files: - molecule.go (376): command definitions, init(), loadMoleculeCatalog - molecule_status.go (673): status, progress, current commands - molecule_list.go (432): list, show, export, parse, instances - molecule_lifecycle.go (359): instantiate, catalog, burn, squash - molecule_attach.go (128): attach, detach, attachment No functional changes - pure refactoring for maintainability. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
359
internal/cmd/molecule_lifecycle.go
Normal file
359
internal/cmd/molecule_lifecycle.go
Normal file
@@ -0,0 +1,359 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
)
|
||||
|
||||
func runMoleculeInstantiate(cmd *cobra.Command, args []string) error {
|
||||
molID := args[0]
|
||||
|
||||
workDir, err := findLocalBeadsDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
b := beads.New(workDir)
|
||||
|
||||
// Try catalog first
|
||||
catalog, err := loadMoleculeCatalog(workDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading catalog: %w", err)
|
||||
}
|
||||
|
||||
var mol *beads.Issue
|
||||
|
||||
if catalogMol := catalog.Get(molID); catalogMol != nil {
|
||||
mol = catalogMol.ToIssue()
|
||||
} else {
|
||||
// Fall back to database
|
||||
mol, err = b.Show(molID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting molecule: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if mol.Type != "molecule" {
|
||||
return fmt.Errorf("%s is not a molecule (type: %s)", molID, mol.Type)
|
||||
}
|
||||
|
||||
// Validate molecule
|
||||
if err := beads.ValidateMolecule(mol); err != nil {
|
||||
return fmt.Errorf("invalid molecule: %w", err)
|
||||
}
|
||||
|
||||
// Get the parent issue
|
||||
parent, err := b.Show(moleculeInstParent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting parent issue: %w", err)
|
||||
}
|
||||
|
||||
// Parse context variables
|
||||
ctx := make(map[string]string)
|
||||
for _, kv := range moleculeInstContext {
|
||||
parts := strings.SplitN(kv, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid context format %q (expected key=value)", kv)
|
||||
}
|
||||
ctx[parts[0]] = parts[1]
|
||||
}
|
||||
|
||||
// Instantiate the molecule
|
||||
opts := beads.InstantiateOptions{Context: ctx}
|
||||
steps, err := b.InstantiateMolecule(mol, parent, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("instantiating molecule: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Created %d steps from %s on %s\n\n",
|
||||
style.Bold.Render("✓"), len(steps), molID, moleculeInstParent)
|
||||
|
||||
for _, step := range steps {
|
||||
fmt.Printf(" %s: %s\n", style.Dim.Render(step.ID), step.Title)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// runMoleculeCatalog lists available molecule protos.
|
||||
func runMoleculeCatalog(cmd *cobra.Command, args []string) error {
|
||||
workDir, err := findLocalBeadsDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
// Load catalog
|
||||
catalog, err := loadMoleculeCatalog(workDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading catalog: %w", err)
|
||||
}
|
||||
|
||||
molecules := catalog.List()
|
||||
|
||||
if moleculeJSON {
|
||||
type catalogEntry struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Source string `json:"source"`
|
||||
StepCount int `json:"step_count"`
|
||||
}
|
||||
|
||||
var entries []catalogEntry
|
||||
for _, mol := range molecules {
|
||||
steps, _ := beads.ParseMoleculeSteps(mol.Description)
|
||||
entries = append(entries, catalogEntry{
|
||||
ID: mol.ID,
|
||||
Title: mol.Title,
|
||||
Source: mol.Source,
|
||||
StepCount: len(steps),
|
||||
})
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(entries)
|
||||
}
|
||||
|
||||
// Human-readable output
|
||||
fmt.Printf("%s Molecule Catalog (%d protos)\n\n", style.Bold.Render("🧬"), len(molecules))
|
||||
|
||||
if len(molecules) == 0 {
|
||||
fmt.Printf(" %s\n", style.Dim.Render("(no protos available)"))
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, mol := range molecules {
|
||||
steps, _ := beads.ParseMoleculeSteps(mol.Description)
|
||||
stepCount := len(steps)
|
||||
|
||||
sourceMarker := style.Dim.Render(fmt.Sprintf("[%s]", mol.Source))
|
||||
fmt.Printf(" %s: %s (%d steps) %s\n",
|
||||
style.Bold.Render(mol.ID), mol.Title, stepCount, sourceMarker)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// runMoleculeBurn burns (destroys) the current molecule attachment.
|
||||
func runMoleculeBurn(cmd *cobra.Command, args []string) error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting current directory: %w", err)
|
||||
}
|
||||
|
||||
// Find town root
|
||||
townRoot, err := workspace.FindFromCwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding workspace: %w", err)
|
||||
}
|
||||
if townRoot == "" {
|
||||
return fmt.Errorf("not in a Gas Town workspace")
|
||||
}
|
||||
|
||||
// Determine target agent
|
||||
var target string
|
||||
if len(args) > 0 {
|
||||
target = args[0]
|
||||
} else {
|
||||
// Auto-detect from current directory
|
||||
roleCtx := detectRole(cwd, townRoot)
|
||||
target = buildAgentIdentity(roleCtx)
|
||||
if target == "" {
|
||||
return fmt.Errorf("cannot determine agent identity from current directory")
|
||||
}
|
||||
}
|
||||
|
||||
// Find beads directory
|
||||
workDir, err := findLocalBeadsDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
b := beads.New(workDir)
|
||||
|
||||
// Find agent's pinned bead (handoff bead)
|
||||
parts := strings.Split(target, "/")
|
||||
role := parts[len(parts)-1]
|
||||
|
||||
handoff, err := b.FindHandoffBead(role)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding handoff bead: %w", err)
|
||||
}
|
||||
if handoff == nil {
|
||||
return fmt.Errorf("no handoff bead found for %s", target)
|
||||
}
|
||||
|
||||
// Check for attached molecule
|
||||
attachment := beads.ParseAttachmentFields(handoff)
|
||||
if attachment == nil || attachment.AttachedMolecule == "" {
|
||||
fmt.Printf("%s No molecule attached to %s - nothing to burn\n",
|
||||
style.Dim.Render("ℹ"), target)
|
||||
return nil
|
||||
}
|
||||
|
||||
moleculeID := attachment.AttachedMolecule
|
||||
|
||||
// Detach the molecule (this "burns" it by removing the attachment)
|
||||
_, err = b.DetachMolecule(handoff.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("detaching molecule: %w", err)
|
||||
}
|
||||
|
||||
if moleculeJSON {
|
||||
result := map[string]interface{}{
|
||||
"burned": moleculeID,
|
||||
"from": target,
|
||||
"handoff_id": handoff.ID,
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(result)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Burned molecule %s from %s\n",
|
||||
style.Bold.Render("🔥"), moleculeID, target)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// runMoleculeSquash squashes the current molecule into a digest.
|
||||
func runMoleculeSquash(cmd *cobra.Command, args []string) error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting current directory: %w", err)
|
||||
}
|
||||
|
||||
// Find town root
|
||||
townRoot, err := workspace.FindFromCwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding workspace: %w", err)
|
||||
}
|
||||
if townRoot == "" {
|
||||
return fmt.Errorf("not in a Gas Town workspace")
|
||||
}
|
||||
|
||||
// Determine target agent
|
||||
var target string
|
||||
if len(args) > 0 {
|
||||
target = args[0]
|
||||
} else {
|
||||
// Auto-detect from current directory
|
||||
roleCtx := detectRole(cwd, townRoot)
|
||||
target = buildAgentIdentity(roleCtx)
|
||||
if target == "" {
|
||||
return fmt.Errorf("cannot determine agent identity from current directory")
|
||||
}
|
||||
}
|
||||
|
||||
// Find beads directory
|
||||
workDir, err := findLocalBeadsDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
b := beads.New(workDir)
|
||||
|
||||
// Find agent's pinned bead (handoff bead)
|
||||
parts := strings.Split(target, "/")
|
||||
role := parts[len(parts)-1]
|
||||
|
||||
handoff, err := b.FindHandoffBead(role)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding handoff bead: %w", err)
|
||||
}
|
||||
if handoff == nil {
|
||||
return fmt.Errorf("no handoff bead found for %s", target)
|
||||
}
|
||||
|
||||
// Check for attached molecule
|
||||
attachment := beads.ParseAttachmentFields(handoff)
|
||||
if attachment == nil || attachment.AttachedMolecule == "" {
|
||||
fmt.Printf("%s No molecule attached to %s - nothing to squash\n",
|
||||
style.Dim.Render("ℹ"), target)
|
||||
return nil
|
||||
}
|
||||
|
||||
moleculeID := attachment.AttachedMolecule
|
||||
|
||||
// Get progress info for the digest
|
||||
progress, _ := getMoleculeProgressInfo(b, moleculeID)
|
||||
|
||||
// Create a digest issue
|
||||
digestTitle := fmt.Sprintf("Digest: %s", moleculeID)
|
||||
digestDesc := fmt.Sprintf(`Squashed molecule execution.
|
||||
|
||||
molecule: %s
|
||||
agent: %s
|
||||
squashed_at: %s
|
||||
`, moleculeID, target, time.Now().UTC().Format(time.RFC3339))
|
||||
|
||||
if progress != nil {
|
||||
digestDesc += fmt.Sprintf(`
|
||||
## Execution Summary
|
||||
- Steps: %d/%d completed
|
||||
- Status: %s
|
||||
`, progress.DoneSteps, progress.TotalSteps, func() string {
|
||||
if progress.Complete {
|
||||
return "complete"
|
||||
}
|
||||
return "partial"
|
||||
}())
|
||||
}
|
||||
|
||||
// Create the digest bead
|
||||
digestIssue, err := b.Create(beads.CreateOptions{
|
||||
Title: digestTitle,
|
||||
Description: digestDesc,
|
||||
Type: "task",
|
||||
Priority: 4, // P4 - backlog priority for digests
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating digest: %w", err)
|
||||
}
|
||||
|
||||
// Add the digest label
|
||||
_ = b.Update(digestIssue.ID, beads.UpdateOptions{
|
||||
AddLabels: []string{"digest"},
|
||||
})
|
||||
|
||||
// Close the digest immediately
|
||||
closedStatus := "closed"
|
||||
err = b.Update(digestIssue.ID, beads.UpdateOptions{
|
||||
Status: &closedStatus,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("%s Created digest but couldn't close it: %v\n",
|
||||
style.Dim.Render("Warning:"), err)
|
||||
}
|
||||
|
||||
// Detach the molecule from the handoff bead
|
||||
_, err = b.DetachMolecule(handoff.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("detaching molecule: %w", err)
|
||||
}
|
||||
|
||||
if moleculeJSON {
|
||||
result := map[string]interface{}{
|
||||
"squashed": moleculeID,
|
||||
"digest_id": digestIssue.ID,
|
||||
"from": target,
|
||||
"handoff_id": handoff.ID,
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(result)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Squashed molecule %s → digest %s\n",
|
||||
style.Bold.Render("📦"), moleculeID, digestIssue.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user