From 888bc3ea741650070059eb0e18d35999b7d836e0 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 27 Dec 2025 14:36:12 -0800 Subject: [PATCH] refactor: Remove duplicate mol commands from gt (gt-w91xz) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove beads data operations from gt mol, delegating to bd: - catalog → bd formula list - list → bd mol list - show → bd mol show - parse → bd mol show - instantiate → bd mol pour - instances → bd queries - bond → bd mol bond Keep agent-specific operations: - status, current, progress (agent context queries) - attach, detach, attachment, attach-from-mail (hook management) - step (agent step operations) - burn, squash (agent-aware lifecycle) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/cmd/molecule.go | 230 ++-------------- internal/cmd/molecule_lifecycle.go | 255 ------------------ internal/cmd/molecule_list.go | 419 ----------------------------- 3 files changed, 19 insertions(+), 885 deletions(-) delete mode 100644 internal/cmd/molecule_list.go diff --git a/internal/cmd/molecule.go b/internal/cmd/molecule.go index 25ba5f70..a456aa22 100644 --- a/internal/cmd/molecule.go +++ b/internal/cmd/molecule.go @@ -1,133 +1,40 @@ package cmd import ( - "path/filepath" - "github.com/spf13/cobra" - "github.com/steveyegge/gastown/internal/beads" - "github.com/steveyegge/gastown/internal/workspace" ) // Molecule command flags var ( - moleculeJSON bool - moleculeInstParent string - moleculeInstContext []string - moleculeCatalogOnly bool // List only catalog templates - moleculeDBOnly bool // List only database molecules - moleculeBondParent string - moleculeBondRef string - moleculeBondVars []string + moleculeJSON bool ) var moleculeCmd = &cobra.Command{ Use: "mol", Aliases: []string{"molecule"}, GroupID: GroupWork, - Short: "Molecule workflow commands", - Long: `Manage molecule workflow templates. + Short: "Agent molecule workflow commands", + Long: `Agent-specific molecule workflow operations. -Molecules are composable workflow patterns stored as beads issues. -When instantiated on a parent issue, they create child beads forming a DAG. +These commands operate on the current agent's hook and attached molecules. +For beads data operations (listing, showing, creating molecules), use bd: -LIFECYCLE: - Proto (template) - │ - ▼ instantiate/bond - ┌─────────────────┐ - │ Mol (durable) │ ← tracked in .beads/ - │ Wisp (ephemeral)│ ← tracked in .beads/ with Wisp=true - └────────┬────────┘ - │ - ┌──────┴──────┐ - ▼ ▼ - burn squash - (no record) (→ digest) + bd formula list List molecule protos (replaces gt mol catalog) + bd mol show Show molecule details (replaces gt mol show) + bd mol pour Instantiate molecule (replaces gt mol instantiate) + bd mol bond Bond molecules together (replaces gt mol bond) -PHASE TRANSITIONS (for pluggable molecules): - ┌─────────────┬─────────────┬─────────────┬─────────────────────┐ - │ Phase │ Parallelism │ Blocks │ Purpose │ - ├─────────────┼─────────────┼─────────────┼─────────────────────┤ - │ discovery │ full │ (nothing) │ Inventory, gather │ - │ structural │ sequential │ discovery │ Big-picture review │ - │ tactical │ parallel │ structural │ Detailed work │ - │ synthesis │ single │ tactical │ Aggregate results │ - └─────────────┴─────────────┴─────────────┴─────────────────────┘ - -COMMANDS: - catalog List available molecule protos - instantiate Create steps from a molecule template - progress Show execution progress of an instantiated molecule - status Show what's on an agent's hook - burn Discard molecule without creating a digest - squash Complete molecule and create a digest`, +AGENT COMMANDS: + status Show what's on current agent's hook + current Show what agent should be working on + progress Show execution progress of attached molecule + attach Attach molecule to agent's hook + detach Detach molecule from agent's hook + burn Burn attached molecule (no record) + squash Squash attached molecule (→ digest) + step Step operations within a molecule`, } -var moleculeListCmd = &cobra.Command{ - Use: "list", - Short: "List molecules", - Long: `List all molecule definitions. - -By default, lists molecules from all sources: -- Built-in molecules (shipped with gt) -- Town-level: /.beads/molecules.jsonl -- Rig-level: /.beads/molecules.jsonl -- Project-level: .beads/molecules.jsonl -- Database: molecules stored as issues - -Use --catalog to show only template molecules (not instantiated). -Use --db to show only database molecules.`, - RunE: runMoleculeList, -} - -var moleculeShowCmd = &cobra.Command{ - Use: "show ", - 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 ", - 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 ", - 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 ", - 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, -} var moleculeProgressCmd = &cobra.Command{ Use: "progress ", @@ -256,21 +163,6 @@ Examples: RunE: runMoleculeCurrent, } -var moleculeCatalogCmd = &cobra.Command{ - Use: "catalog", - Short: "List available molecule protos", - Long: `List molecule protos available for slinging. - -This is a convenience alias for 'gt mol list --catalog' that shows only -reusable templates, not instantiated molecules. - -Protos come from: -- Built-in molecules (shipped with gt) -- Town-level: /.beads/molecules.jsonl -- Rig-level: /.beads/molecules.jsonl -- Project-level: .beads/molecules.jsonl`, - RunE: runMoleculeCatalog, -} var moleculeBurnCmd = &cobra.Command{ Use: "burn [target]", @@ -322,54 +214,8 @@ IMPORTANT: Always use 'gt mol step done' to complete steps. Do not manually close steps with 'bd close' - that skips the auto-continuation logic.`, } -var moleculeBondCmd = &cobra.Command{ - Use: "bond ", - Short: "Dynamically bond a child molecule to a running parent", - Long: `Bond a child molecule to a running parent molecule/wisp. - -This creates a new child molecule instance under the specified parent, -enabling the Christmas Ornament pattern where a step can dynamically -spawn children for parallel execution. - -Examples: - # Bond a polecat inspection arm to current patrol wisp - gt mol bond mol-polecat-arm --parent=patrol-x7k --ref=arm-toast \ - --var polecat_name=toast --var rig=gastown - - # The child will have ID: patrol-x7k.arm-toast - # And template variables {{polecat_name}} and {{rig}} expanded - -Usage in mol-witness-patrol's survey-workers step: - for polecat in $(gt polecat list --names); do - gt mol bond mol-polecat-arm --parent=$PATROL_WISP_ID \ - --ref=arm-$polecat \ - --var polecat_name=$polecat \ - --var rig= - done`, - Args: cobra.ExactArgs(1), - RunE: runMoleculeBond, -} func init() { - // List flags - moleculeListCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON") - moleculeListCmd.Flags().BoolVar(&moleculeCatalogOnly, "catalog", false, "Show only catalog templates") - moleculeListCmd.Flags().BoolVar(&moleculeDBOnly, "db", false, "Show only database molecules") - - // 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") - // Progress flags moleculeProgressCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON") @@ -382,64 +228,26 @@ func init() { // Current flags moleculeCurrentCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON") - // Catalog flags - moleculeCatalogCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON") - // Burn flags moleculeBurnCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON") // Squash flags moleculeSquashCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON") - // Bond flags - moleculeBondCmd.Flags().StringVar(&moleculeBondParent, "parent", "", "Parent molecule/wisp ID (required)") - moleculeBondCmd.Flags().StringVar(&moleculeBondRef, "ref", "", "Child reference suffix (e.g., arm-toast)") - moleculeBondCmd.Flags().StringArrayVar(&moleculeBondVars, "var", nil, "Template variable (key=value)") - moleculeBondCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON") - moleculeBondCmd.MarkFlagRequired("parent") - // Add step subcommand with its children moleculeStepCmd.AddCommand(moleculeStepDoneCmd) moleculeCmd.AddCommand(moleculeStepCmd) - // Add subcommands + // Add subcommands (agent-specific operations only) moleculeCmd.AddCommand(moleculeStatusCmd) moleculeCmd.AddCommand(moleculeCurrentCmd) - moleculeCmd.AddCommand(moleculeCatalogCmd) moleculeCmd.AddCommand(moleculeBurnCmd) moleculeCmd.AddCommand(moleculeSquashCmd) - moleculeCmd.AddCommand(moleculeListCmd) - moleculeCmd.AddCommand(moleculeShowCmd) - moleculeCmd.AddCommand(moleculeParseCmd) - moleculeCmd.AddCommand(moleculeInstantiateCmd) - moleculeCmd.AddCommand(moleculeInstancesCmd) moleculeCmd.AddCommand(moleculeProgressCmd) moleculeCmd.AddCommand(moleculeAttachCmd) moleculeCmd.AddCommand(moleculeDetachCmd) moleculeCmd.AddCommand(moleculeAttachmentCmd) moleculeCmd.AddCommand(moleculeAttachFromMailCmd) - moleculeCmd.AddCommand(moleculeBondCmd) rootCmd.AddCommand(moleculeCmd) } - -// loadMoleculeCatalog loads the molecule catalog with hierarchical sources. -func loadMoleculeCatalog(workDir string) (*beads.MoleculeCatalog, error) { - var townRoot, rigPath, projectPath string - - // Try to find town root (non-fatal: falls back to local formulas) - townRoot, _ = workspace.FindFromCwd() - - // Try to find rig path - if townRoot != "" { - rigName, _, err := findCurrentRig(townRoot) - if err == nil && rigName != "" { - rigPath = filepath.Join(townRoot, rigName) - } - } - - // Project path is the work directory - projectPath = workDir - - return beads.LoadCatalog(townRoot, rigPath, projectPath) -} diff --git a/internal/cmd/molecule_lifecycle.go b/internal/cmd/molecule_lifecycle.go index 3cbf284c..70d6f640 100644 --- a/internal/cmd/molecule_lifecycle.go +++ b/internal/cmd/molecule_lifecycle.go @@ -13,261 +13,6 @@ import ( "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 -} - -// runMoleculeBond dynamically bonds a child molecule to a running parent. -// This enables the Christmas Ornament pattern for parallel child execution. -func runMoleculeBond(cmd *cobra.Command, args []string) error { - protoID := args[0] - - workDir, err := findLocalBeadsDir() - if err != nil { - return fmt.Errorf("not in a beads workspace: %w", err) - } - - b := beads.New(workDir) - - // Load the molecule proto from catalog - catalog, err := loadMoleculeCatalog(workDir) - if err != nil { - return fmt.Errorf("loading catalog: %w", err) - } - - var proto *beads.Issue - - if catalogMol := catalog.Get(protoID); catalogMol != nil { - proto = catalogMol.ToIssue() - } else { - // Fall back to database - proto, err = b.Show(protoID) - if err != nil { - return fmt.Errorf("getting molecule proto: %w", err) - } - } - - if proto.Type != "molecule" { - return fmt.Errorf("%s is not a molecule (type: %s)", protoID, proto.Type) - } - - // Validate molecule - if err := beads.ValidateMolecule(proto); err != nil { - return fmt.Errorf("invalid molecule: %w", err) - } - - // Get the parent issue (the running molecule/wisp) - parent, err := b.Show(moleculeBondParent) - if err != nil { - return fmt.Errorf("getting parent: %w", err) - } - - // Parse template variables from --var flags - ctx := make(map[string]string) - for _, kv := range moleculeBondVars { - parts := strings.SplitN(kv, "=", 2) - if len(parts) != 2 { - return fmt.Errorf("invalid var format %q (expected key=value)", kv) - } - ctx[parts[0]] = parts[1] - } - - // Create the bonded child as an issue under the parent - // First, create a container issue for the bonded molecule - childTitle := proto.Title - if moleculeBondRef != "" { - childTitle = fmt.Sprintf("%s (%s)", proto.Title, moleculeBondRef) - } - - // Expand template variables in the proto description - expandedDesc := beads.ExpandTemplateVars(proto.Description, ctx) - - // Add bonding metadata - bondingMeta := fmt.Sprintf(` ---- -bonded_from: %s -bonded_to: %s -bonded_ref: %s -bonded_at: %s -`, protoID, moleculeBondParent, moleculeBondRef, time.Now().UTC().Format(time.RFC3339)) - - childDesc := expandedDesc + bondingMeta - - // Create the child molecule container - childOpts := beads.CreateOptions{ - Title: childTitle, - Description: childDesc, - Type: "task", // Bonded children are tasks, not molecules - Priority: parent.Priority, - Parent: moleculeBondParent, - } - - child, err := b.Create(childOpts) - if err != nil { - return fmt.Errorf("creating bonded child: %w", err) - } - - // Now instantiate the proto's steps under this child - opts := beads.InstantiateOptions{Context: ctx} - steps, err := b.InstantiateMolecule(proto, child, opts) - if err != nil { - // Clean up the child container on failure (best-effort cleanup) - _ = b.Close(child.ID) - return fmt.Errorf("instantiating bonded molecule: %w", err) - } - - if moleculeJSON { - result := map[string]interface{}{ - "proto": protoID, - "parent": moleculeBondParent, - "ref": moleculeBondRef, - "child_id": child.ID, - "steps": len(steps), - "variables": ctx, - } - enc := json.NewEncoder(os.Stdout) - enc.SetIndent("", " ") - return enc.Encode(result) - } - - fmt.Printf("%s Bonded %s to %s\n", - style.Bold.Render("🔗"), protoID, moleculeBondParent) - fmt.Printf(" Child: %s (%d steps)\n", child.ID, len(steps)) - if moleculeBondRef != "" { - fmt.Printf(" Ref: %s\n", moleculeBondRef) - } - if len(ctx) > 0 { - fmt.Printf(" Variables: %v\n", ctx) - } - - 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() diff --git a/internal/cmd/molecule_list.go b/internal/cmd/molecule_list.go deleted file mode 100644 index 8fff90ee..00000000 --- a/internal/cmd/molecule_list.go +++ /dev/null @@ -1,419 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - "os" - "strings" - - "github.com/spf13/cobra" - "github.com/steveyegge/gastown/internal/beads" - "github.com/steveyegge/gastown/internal/style" -) - -func runMoleculeList(cmd *cobra.Command, args []string) error { - workDir, err := findLocalBeadsDir() - if err != nil { - return fmt.Errorf("not in a beads workspace: %w", err) - } - - // Collect molecules from requested sources - type moleculeEntry struct { - ID string `json:"id"` - Title string `json:"title"` - Source string `json:"source"` - StepCount int `json:"step_count,omitempty"` - Status string `json:"status,omitempty"` - Description string `json:"description,omitempty"` - } - - var entries []moleculeEntry - - // Load from catalog (unless --db only) - if !moleculeDBOnly { - catalog, err := loadMoleculeCatalog(workDir) - if err != nil { - return fmt.Errorf("loading catalog: %w", err) - } - - for _, mol := range catalog.List() { - steps, _ := beads.ParseMoleculeSteps(mol.Description) - entries = append(entries, moleculeEntry{ - ID: mol.ID, - Title: mol.Title, - Source: mol.Source, - StepCount: len(steps), - Description: mol.Description, - }) - } - } - - // Load from database (unless --catalog only) - if !moleculeCatalogOnly { - 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) - } - - // Track catalog IDs to avoid duplicates - catalogIDs := make(map[string]bool) - for _, e := range entries { - catalogIDs[e.ID] = true - } - - for _, mol := range issues { - // Skip if already in catalog (catalog takes precedence) - if catalogIDs[mol.ID] { - continue - } - - steps, _ := beads.ParseMoleculeSteps(mol.Description) - entries = append(entries, moleculeEntry{ - ID: mol.ID, - Title: mol.Title, - Source: "database", - StepCount: len(steps), - Status: mol.Status, - Description: mol.Description, - }) - } - } - - if moleculeJSON { - enc := json.NewEncoder(os.Stdout) - enc.SetIndent("", " ") - return enc.Encode(entries) - } - - // Human-readable output - fmt.Printf("%s Molecules (%d)\n\n", style.Bold.Render("🧬"), len(entries)) - - if len(entries) == 0 { - fmt.Printf(" %s\n", style.Dim.Render("(no molecules defined)")) - return nil - } - - // Create styled table - table := style.NewTable( - style.Column{Name: "ID", Width: 20}, - style.Column{Name: "TITLE", Width: 35}, - style.Column{Name: "STEPS", Width: 5, Align: style.AlignRight}, - style.Column{Name: "SOURCE", Width: 10}, - ) - - for _, mol := range entries { - // Format steps count - stepStr := "" - if mol.StepCount > 0 { - stepStr = fmt.Sprintf("%d", mol.StepCount) - } - - // Format title with status - title := mol.Title - if mol.Status == "closed" { - title = style.Dim.Render(mol.Title + " [closed]") - } - - // Format source - source := style.Dim.Render(mol.Source) - - table.AddRow(mol.ID, title, stepStr, source) - } - - fmt.Print(table.Render()) - - return nil -} - -func runMoleculeShow(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) - } - - // Try catalog first - catalog, err := loadMoleculeCatalog(workDir) - if err != nil { - return fmt.Errorf("loading catalog: %w", err) - } - - var mol *beads.Issue - var source string - - if catalogMol := catalog.Get(molID); catalogMol != nil { - mol = catalogMol.ToIssue() - source = catalogMol.Source - } else { - // Fall back to database - b := beads.New(workDir) - mol, err = b.Show(molID) - if err != nil { - return fmt.Errorf("getting molecule: %w", err) - } - source = "database" - } - - 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) - _ = source // silence unused warning; used in output formatting below - - // For JSON, include parsed steps - if moleculeJSON { - type moleculeOutput struct { - *beads.Issue - Source string `json:"source"` - Steps []beads.MoleculeStep `json:"steps,omitempty"` - ParseError string `json:"parse_error,omitempty"` - } - out := moleculeOutput{Issue: mol, Source: source, 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 %s\n", style.Bold.Render(mol.ID), mol.Title, style.Dim.Render(fmt.Sprintf("[%s]", source))) - 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 (need beads client for this) - b := beads.New(workDir) - 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 := findLocalBeadsDir() - 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 runMoleculeInstances(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) - - // 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 -} - -// 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: " - 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 -}