refactor: Remove duplicate mol commands from gt (gt-w91xz)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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: <town>/.beads/molecules.jsonl
|
||||
- Rig-level: <rig>/.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 <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,
|
||||
}
|
||||
|
||||
var moleculeProgressCmd = &cobra.Command{
|
||||
Use: "progress <root-issue-id>",
|
||||
@@ -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: <town>/.beads/molecules.jsonl
|
||||
- Rig-level: <rig>/.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 <proto-id>",
|
||||
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 <rig> --names); do
|
||||
gt mol bond mol-polecat-arm --parent=$PATROL_WISP_ID \
|
||||
--ref=arm-$polecat \
|
||||
--var polecat_name=$polecat \
|
||||
--var rig=<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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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: <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
|
||||
}
|
||||
Reference in New Issue
Block a user