Files
gastown/internal/cmd/molecule.go
Steve Yegge 031a27c062 feat: add WaitsFor parsing and mol bond command (gt-odfr, gt-isje)
WaitsFor parsing:
- Add WaitsFor []string field to MoleculeStep struct
- Parse WaitsFor lines in molecule descriptions
- Enables fanout gate semantics (e.g., WaitsFor: all-children)
- Case-insensitive parsing like Needs/Tier

mol bond command:
- Add gt mol bond for dynamic child molecule creation
- Supports --parent, --ref, and --var flags
- Enables Christmas Ornament pattern for parallel child execution
- Creates child issue with expanded template variables
- Instantiates proto steps under the bonded child

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-23 21:44:25 -08:00

441 lines
15 KiB
Go

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
)
var moleculeCmd = &cobra.Command{
Use: "molecule",
Aliases: []string{"mol"},
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.
LIFECYCLE:
Proto (template)
▼ instantiate/bond
┌─────────────────┐
│ Mol (durable) │ ← tracked in .beads/
│ Wisp (ephemeral)│ ← tracked in .beads-wisp/
└────────┬────────┘
┌──────┴──────┐
▼ ▼
burn squash
(no record) (→ digest)
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`,
}
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 moleculeExportCmd = &cobra.Command{
Use: "export <path>",
Short: "Export built-in molecules to JSONL",
Long: `Export built-in molecule templates to a JSONL file.
This creates a molecules.jsonl file containing all built-in molecules.
You can place this in:
- <town>/.beads/molecules.jsonl (town-level)
- <rig>/.beads/molecules.jsonl (rig-level)
- .beads/molecules.jsonl (project-level)
The file can be edited to customize or add new molecules.`,
Args: cobra.ExactArgs(1),
RunE: runMoleculeExport,
}
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>",
Short: "Show progress through a molecule's steps",
Long: `Show the execution progress of an instantiated molecule.
Given a root issue (the parent of molecule steps), displays:
- Total steps and completion status
- Which steps are done, in-progress, ready, or blocked
- Overall progress percentage
This is useful for the Witness to monitor molecule execution.
Example:
gt molecule progress gt-abc`,
Args: cobra.ExactArgs(1),
RunE: runMoleculeProgress,
}
var moleculeAttachCmd = &cobra.Command{
Use: "attach <pinned-bead-id> <molecule-id>",
Short: "Attach a molecule to a pinned bead",
Long: `Attach a molecule to a pinned/handoff bead.
This records which molecule an agent is currently working on. The attachment
is stored in the pinned bead's description and visible via 'bd show'.
Example:
gt molecule attach gt-abc mol-xyz`,
Args: cobra.ExactArgs(2),
RunE: runMoleculeAttach,
}
var moleculeDetachCmd = &cobra.Command{
Use: "detach <pinned-bead-id>",
Short: "Detach molecule from a pinned bead",
Long: `Remove molecule attachment from a pinned/handoff bead.
This clears the attached_molecule and attached_at fields from the bead.
Example:
gt molecule detach gt-abc`,
Args: cobra.ExactArgs(1),
RunE: runMoleculeDetach,
}
var moleculeAttachmentCmd = &cobra.Command{
Use: "attachment <pinned-bead-id>",
Short: "Show attachment status of a pinned bead",
Long: `Show which molecule is attached to a pinned bead.
Example:
gt molecule attachment gt-abc`,
Args: cobra.ExactArgs(1),
RunE: runMoleculeAttachment,
}
var moleculeAttachFromMailCmd = &cobra.Command{
Use: "attach-from-mail <mail-id>",
Short: "Attach a molecule from a mail message",
Long: `Attach a molecule to the current agent's hook from a mail message.
This command reads a mail message, extracts the molecule ID from the body,
and attaches it to the agent's pinned bead (hook).
The mail body should contain an "attached_molecule:" field with the molecule ID.
Usage: gt mol attach-from-mail <mail-id>
Behavior:
1. Read mail body for attached_molecule field
2. Attach molecule to agent's hook
3. Mark mail as read
4. Return control for execution
Example:
gt mol attach-from-mail msg-abc123`,
Args: cobra.ExactArgs(1),
RunE: runMoleculeAttachFromMail,
}
var moleculeStatusCmd = &cobra.Command{
Use: "status [target]",
Short: "Show what's on an agent's hook",
Long: `Show what's slung on an agent's hook.
If no target is specified, shows the current agent's status based on
the working directory (polecat, crew member, witness, etc.).
Output includes:
- What's slung (molecule name, associated issue)
- Current phase and progress
- Whether it's a wisp
- Next action hint
Examples:
gt mol status # Show current agent's hook
gt mol status gastown/nux # Show specific polecat's hook
gt mol status gastown/witness # Show witness's hook`,
Args: cobra.MaximumNArgs(1),
RunE: runMoleculeStatus,
}
var moleculeCurrentCmd = &cobra.Command{
Use: "current [identity]",
Short: "Show what agent should be working on",
Long: `Query what an agent is supposed to be working on via breadcrumb trail.
Looks up the agent's handoff bead, checks for attached molecules, and
identifies the current/next step in the workflow.
If no identity is specified, uses the current agent based on working directory.
Output includes:
- Identity and handoff bead info
- Attached molecule (if any)
- Progress through steps
- Current step that should be worked on next
Examples:
gt molecule current # Current agent's work
gt molecule current gastown/furiosa
gt molecule current deacon
gt mol current gastown/witness`,
Args: cobra.MaximumNArgs(1),
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]",
Short: "Burn current molecule without creating a digest",
Long: `Burn (destroy) the current molecule attachment.
This discards the molecule without creating a permanent record. Use this
when abandoning work or when a molecule doesn't need an audit trail.
If no target is specified, burns the current agent's attached molecule.
For wisps, burning is the default completion action. For regular molecules,
consider using 'squash' instead to preserve an audit trail.`,
Args: cobra.MaximumNArgs(1),
RunE: runMoleculeBurn,
}
var moleculeSquashCmd = &cobra.Command{
Use: "squash [target]",
Short: "Compress molecule into a digest",
Long: `Squash the current molecule into a permanent digest.
This condenses a completed molecule's execution into a compact record.
The digest preserves:
- What molecule was executed
- When it ran
- Summary of results
Use this for patrol cycles and other operational work that should have
a permanent (but compact) record.`,
Args: cobra.MaximumNArgs(1),
RunE: runMoleculeSquash,
}
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")
// Attachment flags
moleculeAttachmentCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON")
// Status flags
moleculeStatusCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON")
// 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 subcommands
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(moleculeExportCmd)
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
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)
}