feat(molecule): add gt molecule seed command (gt-dd8s)
Adds the gt molecule seed subcommand that creates built-in molecule definitions (engineer-in-box, quick-fix, research) in the beads database. - Brings molecule.go from main (with list, show, parse, instantiate, instances) - Adds builtin_molecules.go with 3 built-in workflow templates - SeedBuiltinMolecules() writes directly to JSONL to bypass bd CLI type validation - Molecules use well-known IDs (mol-engineer-in-box, mol-quick-fix, mol-research) - Command is idempotent - skips molecules that already exist Note: bd CLI does not yet support molecule as a valid issue type. Filed beads-1 to add molecule type support. Until then, use bd --no-db. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
224
internal/beads/builtin_molecules.go
Normal file
224
internal/beads/builtin_molecules.go
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
// Package beads provides a wrapper for the bd (beads) CLI.
|
||||||
|
package beads
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BuiltinMolecule defines a built-in molecule template.
|
||||||
|
type BuiltinMolecule struct {
|
||||||
|
ID string // Well-known ID (e.g., "mol-engineer-in-box")
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuiltinMolecules returns all built-in molecule definitions.
|
||||||
|
func BuiltinMolecules() []BuiltinMolecule {
|
||||||
|
return []BuiltinMolecule{
|
||||||
|
EngineerInBoxMolecule(),
|
||||||
|
QuickFixMolecule(),
|
||||||
|
ResearchMolecule(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EngineerInBoxMolecule returns the engineer-in-box molecule definition.
|
||||||
|
// This is a full workflow from design to merge.
|
||||||
|
func EngineerInBoxMolecule() BuiltinMolecule {
|
||||||
|
return BuiltinMolecule{
|
||||||
|
ID: "mol-engineer-in-box",
|
||||||
|
Title: "Engineer in a Box",
|
||||||
|
Description: `Full workflow from design to merge.
|
||||||
|
|
||||||
|
## Step: design
|
||||||
|
Think carefully about architecture. Consider:
|
||||||
|
- Existing patterns in the codebase
|
||||||
|
- Trade-offs between approaches
|
||||||
|
- Testability and maintainability
|
||||||
|
|
||||||
|
Write a brief design summary before proceeding.
|
||||||
|
|
||||||
|
## Step: implement
|
||||||
|
Write the code. Follow codebase conventions.
|
||||||
|
Needs: design
|
||||||
|
|
||||||
|
## Step: review
|
||||||
|
Self-review the changes. Look for:
|
||||||
|
- Bugs and edge cases
|
||||||
|
- Style issues
|
||||||
|
- Missing error handling
|
||||||
|
Needs: implement
|
||||||
|
|
||||||
|
## Step: test
|
||||||
|
Write and run tests. Cover happy path and edge cases.
|
||||||
|
Fix any failures before proceeding.
|
||||||
|
Needs: implement
|
||||||
|
|
||||||
|
## Step: submit
|
||||||
|
Submit for merge via refinery.
|
||||||
|
Needs: review, test`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuickFixMolecule returns the quick-fix molecule definition.
|
||||||
|
// This is a fast path for small changes.
|
||||||
|
func QuickFixMolecule() BuiltinMolecule {
|
||||||
|
return BuiltinMolecule{
|
||||||
|
ID: "mol-quick-fix",
|
||||||
|
Title: "Quick Fix",
|
||||||
|
Description: `Fast path for small changes.
|
||||||
|
|
||||||
|
## Step: implement
|
||||||
|
Make the fix. Keep it focused.
|
||||||
|
|
||||||
|
## Step: test
|
||||||
|
Run relevant tests. Fix any regressions.
|
||||||
|
Needs: implement
|
||||||
|
|
||||||
|
## Step: submit
|
||||||
|
Submit for merge.
|
||||||
|
Needs: test`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResearchMolecule returns the research molecule definition.
|
||||||
|
// This is an investigation workflow.
|
||||||
|
func ResearchMolecule() BuiltinMolecule {
|
||||||
|
return BuiltinMolecule{
|
||||||
|
ID: "mol-research",
|
||||||
|
Title: "Research",
|
||||||
|
Description: `Investigation workflow.
|
||||||
|
|
||||||
|
## Step: investigate
|
||||||
|
Explore the question. Search code, read docs,
|
||||||
|
understand context. Take notes.
|
||||||
|
|
||||||
|
## Step: document
|
||||||
|
Write up findings. Include:
|
||||||
|
- What you learned
|
||||||
|
- Recommendations
|
||||||
|
- Open questions
|
||||||
|
Needs: investigate`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// jsonlIssue represents an issue in the JSONL format.
|
||||||
|
// This struct matches the beads JSONL schema for direct file writes.
|
||||||
|
type jsonlIssue struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Priority int `json:"priority"`
|
||||||
|
IssueType string `json:"issue_type"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SeedBuiltinMolecules creates all built-in molecules in the beads database.
|
||||||
|
// It skips molecules that already exist (by ID match).
|
||||||
|
// Returns the number of molecules created.
|
||||||
|
//
|
||||||
|
// Note: Since the bd CLI doesn't support the "molecule" type, this function
|
||||||
|
// writes directly to the JSONL file to create molecules with the proper type.
|
||||||
|
func (b *Beads) SeedBuiltinMolecules() (int, error) {
|
||||||
|
molecules := BuiltinMolecules()
|
||||||
|
created := 0
|
||||||
|
|
||||||
|
// Find the JSONL file
|
||||||
|
jsonlPath := filepath.Join(b.workDir, ".beads", "issues.jsonl")
|
||||||
|
if _, err := os.Stat(jsonlPath); os.IsNotExist(err) {
|
||||||
|
return 0, fmt.Errorf("beads JSONL not found: %s", jsonlPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read existing issues to check for duplicates
|
||||||
|
existingIDs, err := readExistingIDs(jsonlPath)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("reading existing issues: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare new molecules to add
|
||||||
|
var newMolecules []jsonlIssue
|
||||||
|
now := time.Now().Format(time.RFC3339Nano)
|
||||||
|
|
||||||
|
for _, mol := range molecules {
|
||||||
|
if existingIDs[mol.ID] {
|
||||||
|
continue // Already exists
|
||||||
|
}
|
||||||
|
|
||||||
|
newMolecules = append(newMolecules, jsonlIssue{
|
||||||
|
ID: mol.ID,
|
||||||
|
Title: mol.Title,
|
||||||
|
Description: mol.Description,
|
||||||
|
Status: "open",
|
||||||
|
Priority: 2, // Medium priority
|
||||||
|
IssueType: "molecule",
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
})
|
||||||
|
created++
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newMolecules) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append new molecules to the JSONL file
|
||||||
|
f, err := os.OpenFile(jsonlPath, os.O_APPEND|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("opening JSONL for append: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
for _, mol := range newMolecules {
|
||||||
|
line, err := json.Marshal(mol)
|
||||||
|
if err != nil {
|
||||||
|
return created, fmt.Errorf("marshaling molecule %s: %w", mol.ID, err)
|
||||||
|
}
|
||||||
|
if _, err := f.Write(append(line, '\n')); err != nil {
|
||||||
|
return created, fmt.Errorf("writing molecule %s: %w", mol.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return created, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readExistingIDs reads the JSONL file and returns a set of existing issue IDs.
|
||||||
|
func readExistingIDs(jsonlPath string) (map[string]bool, error) {
|
||||||
|
ids := make(map[string]bool)
|
||||||
|
|
||||||
|
f, err := os.Open(jsonlPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
// Increase buffer size for long lines
|
||||||
|
buf := make([]byte, 0, 64*1024)
|
||||||
|
scanner.Buffer(buf, 1024*1024)
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Bytes()
|
||||||
|
if len(line) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just extract the ID field - we don't need to parse the full issue
|
||||||
|
var partial struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(line, &partial); err != nil {
|
||||||
|
continue // Skip malformed lines
|
||||||
|
}
|
||||||
|
if partial.ID != "" {
|
||||||
|
ids[partial.ID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids, scanner.Err()
|
||||||
|
}
|
||||||
533
internal/cmd/molecule.go
Normal file
533
internal/cmd/molecule.go
Normal file
@@ -0,0 +1,533 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
var moleculeSeedCmd = &cobra.Command{
|
||||||
|
Use: "seed",
|
||||||
|
Short: "Create built-in molecules",
|
||||||
|
Long: `Seed the beads database with built-in molecule definitions.
|
||||||
|
|
||||||
|
Creates the following molecules if they don't already exist:
|
||||||
|
- Engineer in a Box: Full workflow from design to merge
|
||||||
|
- Quick Fix: Fast path for small changes
|
||||||
|
- Research: Investigation workflow
|
||||||
|
|
||||||
|
This command is idempotent - running it multiple times is safe.`,
|
||||||
|
RunE: runMoleculeSeed,
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
moleculeCmd.AddCommand(moleculeSeedCmd)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMoleculeSeed(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)
|
||||||
|
created, err := b.SeedBuiltinMolecules()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("seeding molecules: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if created == 0 {
|
||||||
|
fmt.Printf("%s All built-in molecules already exist\n", style.Dim.Render("✓"))
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s Seeded %d built-in molecule(s)\n", style.Bold.Render("✓"), created)
|
||||||
|
fmt.Printf("\n%s Molecules added to JSONL. Use 'bd --no-db' until beads-cli supports molecule type.\n",
|
||||||
|
style.Dim.Render("Note:"))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user