feat(molecule): add molecule CLI commands and spawn integration
Add `gt molecule` subcommands for managing workflow templates: - list: Show all molecules (type=molecule issues) - show: Display molecule with parsed step structure - parse: Validate molecule and show parsed details - instantiate: Create child beads from molecule template - instances: Show all instantiations of a molecule Also add `--molecule` flag to `gt spawn` for molecule-based workflows. When specified, the molecule is instantiated on the parent issue first, then the polecat is spawned on the first ready step. 🤝 Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
495
internal/cmd/molecule.go
Normal file
495
internal/cmd/molecule.go
Normal file
@@ -0,0 +1,495 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
)
|
||||
|
||||
// Molecule command flags
|
||||
var (
|
||||
moleculeJSON bool
|
||||
moleculeInstParent string
|
||||
moleculeInstContext []string
|
||||
)
|
||||
|
||||
var moleculeCmd = &cobra.Command{
|
||||
Use: "molecule",
|
||||
Short: "Molecule workflow commands",
|
||||
Long: `Manage molecule workflow templates.
|
||||
|
||||
Molecules are composable workflow patterns stored as beads issues.
|
||||
When instantiated on a parent issue, they create child beads forming a DAG.`,
|
||||
}
|
||||
|
||||
var moleculeListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List molecules",
|
||||
Long: `List all molecule definitions.
|
||||
|
||||
Molecules are issues with type=molecule.`,
|
||||
RunE: runMoleculeList,
|
||||
}
|
||||
|
||||
var moleculeShowCmd = &cobra.Command{
|
||||
Use: "show <id>",
|
||||
Short: "Show molecule with parsed steps",
|
||||
Long: `Show a molecule definition with its parsed steps.
|
||||
|
||||
Displays the molecule's title, description structure, and all defined steps
|
||||
with their dependencies.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runMoleculeShow,
|
||||
}
|
||||
|
||||
var moleculeParseCmd = &cobra.Command{
|
||||
Use: "parse <id>",
|
||||
Short: "Validate and show parsed structure",
|
||||
Long: `Parse and validate a molecule definition.
|
||||
|
||||
This command parses the molecule's step definitions and reports any errors.
|
||||
Useful for debugging molecule definitions before instantiation.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runMoleculeParse,
|
||||
}
|
||||
|
||||
var moleculeInstantiateCmd = &cobra.Command{
|
||||
Use: "instantiate <mol-id>",
|
||||
Short: "Create steps from molecule template",
|
||||
Long: `Instantiate a molecule on a parent issue.
|
||||
|
||||
Creates child issues for each step defined in the molecule, wiring up
|
||||
dependencies according to the Needs: declarations.
|
||||
|
||||
Template variables ({{variable}}) can be substituted using --context flags.
|
||||
|
||||
Examples:
|
||||
gt molecule instantiate mol-xyz --parent=gt-abc
|
||||
gt molecule instantiate mol-xyz --parent=gt-abc --context feature=auth --context file=login.go`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runMoleculeInstantiate,
|
||||
}
|
||||
|
||||
var moleculeInstancesCmd = &cobra.Command{
|
||||
Use: "instances <mol-id>",
|
||||
Short: "Show all instantiations of a molecule",
|
||||
Long: `Show all parent issues that have instantiated this molecule.
|
||||
|
||||
Lists each instantiation with its status and progress.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runMoleculeInstances,
|
||||
}
|
||||
|
||||
func init() {
|
||||
// List flags
|
||||
moleculeListCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON")
|
||||
|
||||
// Show flags
|
||||
moleculeShowCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON")
|
||||
|
||||
// Parse flags
|
||||
moleculeParseCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON")
|
||||
|
||||
// Instantiate flags
|
||||
moleculeInstantiateCmd.Flags().StringVar(&moleculeInstParent, "parent", "", "Parent issue ID (required)")
|
||||
moleculeInstantiateCmd.Flags().StringArrayVar(&moleculeInstContext, "context", nil, "Context variable (key=value)")
|
||||
moleculeInstantiateCmd.MarkFlagRequired("parent")
|
||||
|
||||
// Instances flags
|
||||
moleculeInstancesCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON")
|
||||
|
||||
// Add subcommands
|
||||
moleculeCmd.AddCommand(moleculeListCmd)
|
||||
moleculeCmd.AddCommand(moleculeShowCmd)
|
||||
moleculeCmd.AddCommand(moleculeParseCmd)
|
||||
moleculeCmd.AddCommand(moleculeInstantiateCmd)
|
||||
moleculeCmd.AddCommand(moleculeInstancesCmd)
|
||||
|
||||
rootCmd.AddCommand(moleculeCmd)
|
||||
}
|
||||
|
||||
func runMoleculeList(cmd *cobra.Command, args []string) error {
|
||||
workDir, err := findBeadsWorkDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
b := beads.New(workDir)
|
||||
issues, err := b.List(beads.ListOptions{
|
||||
Type: "molecule",
|
||||
Status: "all",
|
||||
Priority: -1,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing molecules: %w", err)
|
||||
}
|
||||
|
||||
if moleculeJSON {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(issues)
|
||||
}
|
||||
|
||||
// Human-readable output
|
||||
fmt.Printf("%s Molecules (%d)\n\n", style.Bold.Render("🧬"), len(issues))
|
||||
|
||||
if len(issues) == 0 {
|
||||
fmt.Printf(" %s\n", style.Dim.Render("(no molecules defined)"))
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, mol := range issues {
|
||||
statusMarker := ""
|
||||
if mol.Status == "closed" {
|
||||
statusMarker = " " + style.Dim.Render("[closed]")
|
||||
}
|
||||
|
||||
// Parse steps to show count
|
||||
steps, _ := beads.ParseMoleculeSteps(mol.Description)
|
||||
stepCount := ""
|
||||
if len(steps) > 0 {
|
||||
stepCount = fmt.Sprintf(" (%d steps)", len(steps))
|
||||
}
|
||||
|
||||
fmt.Printf(" %s: %s%s%s\n", style.Bold.Render(mol.ID), mol.Title, stepCount, statusMarker)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMoleculeShow(cmd *cobra.Command, args []string) error {
|
||||
molID := args[0]
|
||||
|
||||
workDir, err := findBeadsWorkDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
b := beads.New(workDir)
|
||||
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)
|
||||
}
|
||||
|
||||
// Parse steps
|
||||
steps, parseErr := beads.ParseMoleculeSteps(mol.Description)
|
||||
|
||||
// For JSON, include parsed steps
|
||||
if moleculeJSON {
|
||||
type moleculeOutput struct {
|
||||
*beads.Issue
|
||||
Steps []beads.MoleculeStep `json:"steps,omitempty"`
|
||||
ParseError string `json:"parse_error,omitempty"`
|
||||
}
|
||||
out := moleculeOutput{Issue: mol, Steps: steps}
|
||||
if parseErr != nil {
|
||||
out.ParseError = parseErr.Error()
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(out)
|
||||
}
|
||||
|
||||
// Human-readable output
|
||||
fmt.Printf("\n%s: %s\n", style.Bold.Render(mol.ID), mol.Title)
|
||||
fmt.Printf("Type: %s\n", mol.Type)
|
||||
|
||||
if parseErr != nil {
|
||||
fmt.Printf("\n%s Parse error: %s\n", style.Bold.Render("⚠"), parseErr)
|
||||
}
|
||||
|
||||
// Show steps
|
||||
fmt.Printf("\nSteps (%d):\n", len(steps))
|
||||
if len(steps) == 0 {
|
||||
fmt.Printf(" %s\n", style.Dim.Render("(no steps defined)"))
|
||||
} else {
|
||||
// Find which steps are ready (no dependencies)
|
||||
for _, step := range steps {
|
||||
needsStr := ""
|
||||
if len(step.Needs) == 0 {
|
||||
needsStr = style.Dim.Render("(ready first)")
|
||||
} else {
|
||||
needsStr = fmt.Sprintf("Needs: %s", strings.Join(step.Needs, ", "))
|
||||
}
|
||||
|
||||
tierStr := ""
|
||||
if step.Tier != "" {
|
||||
tierStr = fmt.Sprintf(" [%s]", step.Tier)
|
||||
}
|
||||
|
||||
fmt.Printf(" %-12s → %s%s\n", step.Ref, needsStr, tierStr)
|
||||
}
|
||||
}
|
||||
|
||||
// Count instances
|
||||
instances, _ := findMoleculeInstances(b, molID)
|
||||
fmt.Printf("\nInstances: %d\n", len(instances))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMoleculeParse(cmd *cobra.Command, args []string) error {
|
||||
molID := args[0]
|
||||
|
||||
workDir, err := findBeadsWorkDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
b := beads.New(workDir)
|
||||
mol, err := b.Show(molID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting molecule: %w", err)
|
||||
}
|
||||
|
||||
// Validate the molecule
|
||||
validationErr := beads.ValidateMolecule(mol)
|
||||
|
||||
// Parse steps regardless of validation
|
||||
steps, parseErr := beads.ParseMoleculeSteps(mol.Description)
|
||||
|
||||
if moleculeJSON {
|
||||
type parseOutput struct {
|
||||
Valid bool `json:"valid"`
|
||||
ValidationError string `json:"validation_error,omitempty"`
|
||||
ParseError string `json:"parse_error,omitempty"`
|
||||
Steps []beads.MoleculeStep `json:"steps"`
|
||||
}
|
||||
out := parseOutput{
|
||||
Valid: validationErr == nil,
|
||||
Steps: steps,
|
||||
}
|
||||
if validationErr != nil {
|
||||
out.ValidationError = validationErr.Error()
|
||||
}
|
||||
if parseErr != nil {
|
||||
out.ParseError = parseErr.Error()
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(out)
|
||||
}
|
||||
|
||||
// Human-readable output
|
||||
fmt.Printf("\n%s: %s\n\n", style.Bold.Render(mol.ID), mol.Title)
|
||||
|
||||
if validationErr != nil {
|
||||
fmt.Printf("%s Validation failed: %s\n\n", style.Bold.Render("✗"), validationErr)
|
||||
} else {
|
||||
fmt.Printf("%s Valid molecule\n\n", style.Bold.Render("✓"))
|
||||
}
|
||||
|
||||
if parseErr != nil {
|
||||
fmt.Printf("Parse error: %s\n\n", parseErr)
|
||||
}
|
||||
|
||||
fmt.Printf("Parsed Steps (%d):\n", len(steps))
|
||||
for i, step := range steps {
|
||||
fmt.Printf("\n [%d] %s\n", i+1, style.Bold.Render(step.Ref))
|
||||
if step.Title != step.Ref {
|
||||
fmt.Printf(" Title: %s\n", step.Title)
|
||||
}
|
||||
if len(step.Needs) > 0 {
|
||||
fmt.Printf(" Needs: %s\n", strings.Join(step.Needs, ", "))
|
||||
}
|
||||
if step.Tier != "" {
|
||||
fmt.Printf(" Tier: %s\n", step.Tier)
|
||||
}
|
||||
if step.Instructions != "" {
|
||||
// Show first line of instructions
|
||||
firstLine := strings.SplitN(step.Instructions, "\n", 2)[0]
|
||||
if len(firstLine) > 60 {
|
||||
firstLine = firstLine[:57] + "..."
|
||||
}
|
||||
fmt.Printf(" Instructions: %s\n", style.Dim.Render(firstLine))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMoleculeInstantiate(cmd *cobra.Command, args []string) error {
|
||||
molID := args[0]
|
||||
|
||||
workDir, err := findBeadsWorkDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
b := beads.New(workDir)
|
||||
|
||||
// Get the molecule
|
||||
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
|
||||
}
|
||||
|
||||
func runMoleculeInstances(cmd *cobra.Command, args []string) error {
|
||||
molID := args[0]
|
||||
|
||||
workDir, err := findBeadsWorkDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
b := beads.New(workDir)
|
||||
|
||||
// Verify the molecule exists
|
||||
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)
|
||||
}
|
||||
|
||||
// Find all instances
|
||||
instances, err := findMoleculeInstances(b, molID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding instances: %w", err)
|
||||
}
|
||||
|
||||
if moleculeJSON {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(instances)
|
||||
}
|
||||
|
||||
// Human-readable output
|
||||
fmt.Printf("\n%s Instances of %s (%d)\n\n",
|
||||
style.Bold.Render("📋"), molID, len(instances))
|
||||
|
||||
if len(instances) == 0 {
|
||||
fmt.Printf(" %s\n", style.Dim.Render("(no instantiations found)"))
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%-16s %-12s %s\n",
|
||||
style.Bold.Render("Parent"),
|
||||
style.Bold.Render("Status"),
|
||||
style.Bold.Render("Created"))
|
||||
fmt.Println(strings.Repeat("-", 50))
|
||||
|
||||
for _, inst := range instances {
|
||||
// Calculate progress from children
|
||||
progress := ""
|
||||
if len(inst.Children) > 0 {
|
||||
closed := 0
|
||||
for _, childID := range inst.Children {
|
||||
child, err := b.Show(childID)
|
||||
if err == nil && child.Status == "closed" {
|
||||
closed++
|
||||
}
|
||||
}
|
||||
progress = fmt.Sprintf(" (%d/%d complete)", closed, len(inst.Children))
|
||||
}
|
||||
|
||||
statusStr := inst.Status
|
||||
if inst.Status == "closed" {
|
||||
statusStr = style.Dim.Render("done")
|
||||
} else if inst.Status == "in_progress" {
|
||||
statusStr = "active"
|
||||
}
|
||||
|
||||
created := ""
|
||||
if inst.CreatedAt != "" {
|
||||
// Parse and format date
|
||||
created = inst.CreatedAt[:10] // Just the date portion
|
||||
}
|
||||
|
||||
fmt.Printf("%-16s %-12s %s%s\n", inst.ID, statusStr, created, progress)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// moleculeInstance represents an instantiation of a molecule.
|
||||
type moleculeInstance struct {
|
||||
*beads.Issue
|
||||
}
|
||||
|
||||
// findMoleculeInstances finds all parent issues that have steps instantiated from the given molecule.
|
||||
func findMoleculeInstances(b *beads.Beads, molID string) ([]*beads.Issue, error) {
|
||||
// Get all issues and look for ones with children that have instantiated_from metadata
|
||||
// This is a brute-force approach - could be optimized with better queries
|
||||
|
||||
// Strategy: search for issues whose descriptions contain "instantiated_from: <molID>"
|
||||
allIssues, err := b.List(beads.ListOptions{Status: "all", Priority: -1})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Find issues that reference this molecule
|
||||
parentIDs := make(map[string]bool)
|
||||
for _, issue := range allIssues {
|
||||
if strings.Contains(issue.Description, fmt.Sprintf("instantiated_from: %s", molID)) {
|
||||
// This is a step - find its parent
|
||||
if issue.Parent != "" {
|
||||
parentIDs[issue.Parent] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch the parent issues
|
||||
var parents []*beads.Issue
|
||||
for parentID := range parentIDs {
|
||||
parent, err := b.Show(parentID)
|
||||
if err == nil {
|
||||
parents = append(parents, parent)
|
||||
}
|
||||
}
|
||||
|
||||
return parents, nil
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"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/polecat"
|
||||
@@ -30,12 +31,13 @@ var polecatNames = []string{
|
||||
|
||||
// Spawn command flags
|
||||
var (
|
||||
spawnIssue string
|
||||
spawnMessage string
|
||||
spawnCreate bool
|
||||
spawnNoStart bool
|
||||
spawnPolecat string
|
||||
spawnRig string
|
||||
spawnIssue string
|
||||
spawnMessage string
|
||||
spawnCreate bool
|
||||
spawnNoStart bool
|
||||
spawnPolecat string
|
||||
spawnRig string
|
||||
spawnMolecule string
|
||||
)
|
||||
|
||||
var spawnCmd = &cobra.Command{
|
||||
@@ -47,6 +49,9 @@ var spawnCmd = &cobra.Command{
|
||||
Assigns an issue or task to a polecat and starts a session. If no polecat
|
||||
is specified, auto-selects an idle polecat in the rig.
|
||||
|
||||
When --molecule is specified, the molecule is first instantiated on the parent
|
||||
issue (creating child steps), then the polecat is spawned on the first ready step.
|
||||
|
||||
Examples:
|
||||
gt spawn gastown/Toast --issue gt-abc
|
||||
gt spawn gastown --issue gt-def # auto-select polecat
|
||||
@@ -55,7 +60,10 @@ Examples:
|
||||
|
||||
# Flag-based selection (rig inferred from current directory):
|
||||
gt spawn --issue gt-xyz --polecat Angharad
|
||||
gt spawn --issue gt-abc --rig gastown --polecat Toast`,
|
||||
gt spawn --issue gt-abc --rig gastown --polecat Toast
|
||||
|
||||
# With molecule workflow:
|
||||
gt spawn --issue gt-abc --molecule mol-engineer-box`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: runSpawn,
|
||||
}
|
||||
@@ -67,6 +75,7 @@ func init() {
|
||||
spawnCmd.Flags().BoolVar(&spawnNoStart, "no-start", false, "Assign work but don't start session")
|
||||
spawnCmd.Flags().StringVar(&spawnPolecat, "polecat", "", "Polecat name (alternative to positional arg)")
|
||||
spawnCmd.Flags().StringVar(&spawnRig, "rig", "", "Rig name (defaults to current directory's rig)")
|
||||
spawnCmd.Flags().StringVar(&spawnMolecule, "molecule", "", "Molecule ID to instantiate on the issue")
|
||||
|
||||
rootCmd.AddCommand(spawnCmd)
|
||||
}
|
||||
@@ -86,6 +95,11 @@ func runSpawn(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("must specify --issue or -m/--message")
|
||||
}
|
||||
|
||||
// --molecule requires --issue
|
||||
if spawnMolecule != "" && spawnIssue == "" {
|
||||
return fmt.Errorf("--molecule requires --issue to be specified")
|
||||
}
|
||||
|
||||
// Find workspace first (needed for rig inference)
|
||||
townRoot, err := workspace.FindFromCwdOrError()
|
||||
if err != nil {
|
||||
@@ -170,6 +184,61 @@ func runSpawn(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("polecat '%s' is already working on %s", polecatName, pc.Issue)
|
||||
}
|
||||
|
||||
// Handle molecule instantiation if specified
|
||||
if spawnMolecule != "" {
|
||||
b := beads.New(r.Path)
|
||||
|
||||
// Get the molecule
|
||||
mol, err := b.Show(spawnMolecule)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting molecule %s: %w", spawnMolecule, err)
|
||||
}
|
||||
|
||||
if mol.Type != "molecule" {
|
||||
return fmt.Errorf("%s is not a molecule (type: %s)", spawnMolecule, mol.Type)
|
||||
}
|
||||
|
||||
// Validate the molecule
|
||||
if err := beads.ValidateMolecule(mol); err != nil {
|
||||
return fmt.Errorf("invalid molecule: %w", err)
|
||||
}
|
||||
|
||||
// Get the parent issue
|
||||
parent, err := b.Show(spawnIssue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting parent issue %s: %w", spawnIssue, err)
|
||||
}
|
||||
|
||||
// Instantiate the molecule
|
||||
fmt.Printf("Instantiating molecule %s on %s...\n", spawnMolecule, spawnIssue)
|
||||
steps, err := b.InstantiateMolecule(mol, parent, beads.InstantiateOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("instantiating molecule: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Created %d steps\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
|
||||
for _, step := range steps {
|
||||
if len(step.DependsOn) == 0 {
|
||||
firstReadyStep = step
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if firstReadyStep == nil {
|
||||
return fmt.Errorf("no ready step found in molecule (all steps have dependencies)")
|
||||
}
|
||||
|
||||
// Switch to spawning on the first ready step
|
||||
fmt.Printf("\nSpawning on first ready step: %s\n", firstReadyStep.ID)
|
||||
spawnIssue = firstReadyStep.ID
|
||||
}
|
||||
|
||||
// Get issue details if specified
|
||||
var issue *BeadsIssue
|
||||
if spawnIssue != "" {
|
||||
|
||||
Reference in New Issue
Block a user