feat(beads): add built-in molecules for standard workflows
Add three built-in molecule definitions that are automatically seeded during `gt install`: - engineer-in-box: Full workflow from design to merge (5 steps) - quick-fix: Fast path for small changes (3 steps) - research: Investigation workflow (2 steps) These molecules provide reusable workflow templates that polecats can instantiate to execute multi-step procedures with proper dependency tracking between steps. Closes gt-4nn.4 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/steveyegge/gastown/internal/beads"
|
||||||
"github.com/steveyegge/gastown/internal/config"
|
"github.com/steveyegge/gastown/internal/config"
|
||||||
"github.com/steveyegge/gastown/internal/style"
|
"github.com/steveyegge/gastown/internal/style"
|
||||||
"github.com/steveyegge/gastown/internal/templates"
|
"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)
|
fmt.Printf(" %s Could not initialize town beads: %v\n", style.Dim.Render("⚠"), err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" ✓ Initialized .beads/ (town-level beads with gm- prefix)\n")
|
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
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user