diff --git a/internal/beads/builtin_molecules.go b/internal/beads/builtin_molecules.go new file mode 100644 index 00000000..1aa48812 --- /dev/null +++ b/internal/beads/builtin_molecules.go @@ -0,0 +1,138 @@ +// Package beads provides a wrapper for the bd (beads) CLI. +package beads + +// 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`, + } +} + +// SeedBuiltinMolecules creates all built-in molecules in the beads database. +// It skips molecules that already exist (by title match). +// Returns the number of molecules created. +func (b *Beads) SeedBuiltinMolecules() (int, error) { + molecules := BuiltinMolecules() + created := 0 + + // Get existing molecules to avoid duplicates + existing, err := b.List(ListOptions{Type: "molecule", Priority: -1}) + if err != nil { + return 0, err + } + + // Build map of existing molecule titles + existingTitles := make(map[string]bool) + for _, issue := range existing { + existingTitles[issue.Title] = true + } + + // Create each molecule if it doesn't exist + for _, mol := range molecules { + if existingTitles[mol.Title] { + continue // Already exists + } + + _, err := b.Create(CreateOptions{ + Title: mol.Title, + Type: "molecule", + Priority: 2, // Medium priority + Description: mol.Description, + }) + if err != nil { + return created, err + } + created++ + } + + return created, nil +} diff --git a/internal/beads/builtin_molecules_test.go b/internal/beads/builtin_molecules_test.go new file mode 100644 index 00000000..e8235204 --- /dev/null +++ b/internal/beads/builtin_molecules_test.go @@ -0,0 +1,143 @@ +package beads + +import "testing" + +func TestBuiltinMolecules(t *testing.T) { + molecules := BuiltinMolecules() + + if len(molecules) != 3 { + t.Errorf("expected 3 built-in molecules, got %d", len(molecules)) + } + + // Verify each molecule can be parsed and validated + for _, mol := range molecules { + t.Run(mol.Title, func(t *testing.T) { + // Check required fields + if mol.ID == "" { + t.Error("molecule missing ID") + } + if mol.Title == "" { + t.Error("molecule missing Title") + } + if mol.Description == "" { + t.Error("molecule missing Description") + } + + // Parse the molecule steps + steps, err := ParseMoleculeSteps(mol.Description) + if err != nil { + t.Fatalf("failed to parse molecule steps: %v", err) + } + + if len(steps) == 0 { + t.Error("molecule has no steps") + } + + // Validate the molecule as if it were an issue + issue := &Issue{ + Type: "molecule", + Title: mol.Title, + Description: mol.Description, + } + + if err := ValidateMolecule(issue); err != nil { + t.Errorf("molecule validation failed: %v", err) + } + }) + } +} + +func TestEngineerInBoxMolecule(t *testing.T) { + mol := EngineerInBoxMolecule() + + steps, err := ParseMoleculeSteps(mol.Description) + if err != nil { + t.Fatalf("failed to parse: %v", err) + } + + // Should have 5 steps: design, implement, review, test, submit + if len(steps) != 5 { + t.Errorf("expected 5 steps, got %d", len(steps)) + } + + // Verify step refs + expectedRefs := []string{"design", "implement", "review", "test", "submit"} + for i, expected := range expectedRefs { + if steps[i].Ref != expected { + t.Errorf("step %d: expected ref %q, got %q", i, expected, steps[i].Ref) + } + } + + // Verify dependencies + // design has no deps + if len(steps[0].Needs) != 0 { + t.Errorf("design should have no deps, got %v", steps[0].Needs) + } + + // implement needs design + if len(steps[1].Needs) != 1 || steps[1].Needs[0] != "design" { + t.Errorf("implement should need design, got %v", steps[1].Needs) + } + + // review needs implement + if len(steps[2].Needs) != 1 || steps[2].Needs[0] != "implement" { + t.Errorf("review should need implement, got %v", steps[2].Needs) + } + + // test needs implement + if len(steps[3].Needs) != 1 || steps[3].Needs[0] != "implement" { + t.Errorf("test should need implement, got %v", steps[3].Needs) + } + + // submit needs review and test + if len(steps[4].Needs) != 2 { + t.Errorf("submit should need 2 deps, got %v", steps[4].Needs) + } +} + +func TestQuickFixMolecule(t *testing.T) { + mol := QuickFixMolecule() + + steps, err := ParseMoleculeSteps(mol.Description) + if err != nil { + t.Fatalf("failed to parse: %v", err) + } + + // Should have 3 steps: implement, test, submit + if len(steps) != 3 { + t.Errorf("expected 3 steps, got %d", len(steps)) + } + + expectedRefs := []string{"implement", "test", "submit"} + for i, expected := range expectedRefs { + if steps[i].Ref != expected { + t.Errorf("step %d: expected ref %q, got %q", i, expected, steps[i].Ref) + } + } +} + +func TestResearchMolecule(t *testing.T) { + mol := ResearchMolecule() + + steps, err := ParseMoleculeSteps(mol.Description) + if err != nil { + t.Fatalf("failed to parse: %v", err) + } + + // Should have 2 steps: investigate, document + if len(steps) != 2 { + t.Errorf("expected 2 steps, got %d", len(steps)) + } + + expectedRefs := []string{"investigate", "document"} + for i, expected := range expectedRefs { + if steps[i].Ref != expected { + t.Errorf("step %d: expected ref %q, got %q", i, expected, steps[i].Ref) + } + } + + // document needs investigate + if len(steps[1].Needs) != 1 || steps[1].Needs[0] != "investigate" { + t.Errorf("document should need investigate, got %v", steps[1].Needs) + } +} diff --git a/internal/cmd/install.go b/internal/cmd/install.go index 13cf701a..7cff9532 100644 --- a/internal/cmd/install.go +++ b/internal/cmd/install.go @@ -10,6 +10,7 @@ import ( "time" "github.com/spf13/cobra" + "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/templates" @@ -168,6 +169,13 @@ func runInstall(cmd *cobra.Command, args []string) error { fmt.Printf(" %s Could not initialize town beads: %v\n", style.Dim.Render("⚠"), err) } else { fmt.Printf(" ✓ Initialized .beads/ (town-level beads with gm- prefix)\n") + + // Seed built-in molecules + if err := seedBuiltinMolecules(absPath); err != nil { + fmt.Printf(" %s Could not seed built-in molecules: %v\n", style.Dim.Render("⚠"), err) + } else { + fmt.Printf(" ✓ Seeded built-in molecules\n") + } } } @@ -240,3 +248,11 @@ func initTownBeads(townPath string) error { } return nil } + +// seedBuiltinMolecules creates built-in molecule definitions in the beads database. +// These molecules provide standard workflows like engineer-in-box, quick-fix, and research. +func seedBuiltinMolecules(townPath string) error { + b := beads.New(townPath) + _, err := b.SeedBuiltinMolecules() + return err +}