Delete legacy Go-defined molecules (gt-ingm.3)

Molecules are now defined as formula files in .beads/formulas/ and
cooked into proto beads via `bd cook`. This removes:

- molecules_patrol.go (695 lines)
- molecules_session.go (544 lines)
- molecules_work.go (444 lines)
- builtin_molecules_test.go
- christmas_ornament_test.go

Updates:
- builtin_molecules.go: stub deprecated functions
- install.go: remove molecule seeding (formulas are cooked on-demand)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-25 02:25:43 -08:00
parent f443f4b83a
commit cd109e9db7
7 changed files with 12 additions and 2719 deletions

View File

@@ -2,6 +2,9 @@
package beads
// BuiltinMolecule defines a built-in molecule template.
// Deprecated: Molecules are now defined as formula files in .beads/formulas/
// and cooked into proto beads via `bd cook`. This type remains for backward
// compatibility but is no longer used.
type BuiltinMolecule struct {
ID string // Well-known ID (e.g., "mol-engineer-in-box")
Title string
@@ -9,70 +12,17 @@ type BuiltinMolecule struct {
}
// BuiltinMolecules returns all built-in molecule definitions.
// Molecules are defined in separate files by category:
// - molecules_work.go: EngineerInBox, QuickFix, Research, PolecatWork, ReadyWork
// - molecules_patrol.go: DeaconPatrol, WitnessPatrol, RefineryPatrol
// - molecules_session.go: CrewSession, PolecatSession, Bootstrap, VersionBump, InstallGoBinary
// Deprecated: Molecules are now defined as formula files (.beads/formulas/*.formula.json)
// and cooked into proto beads via `bd cook`. This function returns an empty slice.
// Use `bd cook` to create proto beads from formulas instead.
func BuiltinMolecules() []BuiltinMolecule {
return []BuiltinMolecule{
// Work molecules
EngineerInBoxMolecule(),
QuickFixMolecule(),
ResearchMolecule(),
PolecatWorkMolecule(),
ReadyWorkMolecule(),
// Patrol molecules
DeaconPatrolMolecule(),
RefineryPatrolMolecule(),
WitnessPatrolMolecule(),
PolecatArmMolecule(),
// Session and utility molecules
CrewSessionMolecule(),
PolecatSessionMolecule(),
BootstrapGasTownMolecule(),
VersionBumpMolecule(),
InstallGoBinaryMolecule(),
}
return []BuiltinMolecule{}
}
// 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.
// SeedBuiltinMolecules is deprecated and does nothing.
// Molecules are now created by cooking formula files with `bd cook`.
// This function remains for backward compatibility with existing installations.
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
// No-op: formulas are cooked on-demand, not seeded at install time
return 0, nil
}

View File

@@ -1,414 +0,0 @@
package beads
import "testing"
func TestBuiltinMolecules(t *testing.T) {
molecules := BuiltinMolecules()
if len(molecules) != 14 {
t.Errorf("expected 14 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)
}
}
func TestInstallGoBinaryMolecule(t *testing.T) {
mol := InstallGoBinaryMolecule()
if mol.ID != "mol-install-go-binary" {
t.Errorf("expected ID 'mol-install-go-binary', got %q", mol.ID)
}
if mol.Title != "Install Go Binary" {
t.Errorf("expected Title 'Install Go Binary', got %q", mol.Title)
}
steps, err := ParseMoleculeSteps(mol.Description)
if err != nil {
t.Fatalf("failed to parse: %v", err)
}
// Should have 1 step: install
if len(steps) != 1 {
t.Errorf("expected 1 step, got %d", len(steps))
}
if steps[0].Ref != "install" {
t.Errorf("expected ref 'install', got %q", steps[0].Ref)
}
// install has no deps
if len(steps[0].Needs) != 0 {
t.Errorf("install should have no deps, got %v", steps[0].Needs)
}
}
func TestPolecatWorkMolecule(t *testing.T) {
mol := PolecatWorkMolecule()
if mol.ID != "mol-polecat-work" {
t.Errorf("expected ID 'mol-polecat-work', got %q", mol.ID)
}
if mol.Title != "Polecat Work" {
t.Errorf("expected Title 'Polecat Work', got %q", mol.Title)
}
steps, err := ParseMoleculeSteps(mol.Description)
if err != nil {
t.Fatalf("failed to parse: %v", err)
}
// Should have 8 steps: load-context, implement, self-review, verify-tests,
// rebase-main, submit-merge, exit-decision, request-shutdown
if len(steps) != 8 {
t.Errorf("expected 8 steps, got %d", len(steps))
}
expectedRefs := []string{
"load-context", "implement", "self-review", "verify-tests",
"rebase-main", "submit-merge", "exit-decision", "request-shutdown",
}
for i, expected := range expectedRefs {
if i >= len(steps) {
t.Errorf("missing step %d: expected %q", i, expected)
continue
}
if steps[i].Ref != expected {
t.Errorf("step %d: expected ref %q, got %q", i, expected, steps[i].Ref)
}
}
// Verify key dependencies
// load-context has no deps
if len(steps[0].Needs) != 0 {
t.Errorf("load-context should have no deps, got %v", steps[0].Needs)
}
// implement needs load-context
if len(steps[1].Needs) != 1 || steps[1].Needs[0] != "load-context" {
t.Errorf("implement should need load-context, got %v", steps[1].Needs)
}
// rebase-main needs self-review and verify-tests
if len(steps[4].Needs) != 2 {
t.Errorf("rebase-main should need 2 deps, got %v", steps[4].Needs)
}
// request-shutdown needs exit-decision
if len(steps[7].Needs) != 1 || steps[7].Needs[0] != "exit-decision" {
t.Errorf("request-shutdown should need exit-decision, got %v", steps[7].Needs)
}
}
func TestDeaconPatrolMolecule(t *testing.T) {
mol := DeaconPatrolMolecule()
if mol.ID != "mol-deacon-patrol" {
t.Errorf("expected ID 'mol-deacon-patrol', got %q", mol.ID)
}
if mol.Title != "Deacon Patrol" {
t.Errorf("expected Title 'Deacon Patrol', got %q", mol.Title)
}
steps, err := ParseMoleculeSteps(mol.Description)
if err != nil {
t.Fatalf("failed to parse: %v", err)
}
// Should have 8 steps: inbox-check, trigger-pending-spawns, health-scan, plugin-run,
// orphan-check, session-gc, context-check, loop-or-exit
if len(steps) != 8 {
t.Errorf("expected 8 steps, got %d", len(steps))
}
expectedRefs := []string{
"inbox-check", "trigger-pending-spawns", "health-scan", "plugin-run",
"orphan-check", "session-gc", "context-check", "loop-or-exit",
}
for i, expected := range expectedRefs {
if i >= len(steps) {
t.Errorf("missing step %d: expected %q", i, expected)
continue
}
if steps[i].Ref != expected {
t.Errorf("step %d: expected ref %q, got %q", i, expected, steps[i].Ref)
}
}
// Verify key dependencies
// inbox-check has no deps (first step)
if len(steps[0].Needs) != 0 {
t.Errorf("inbox-check should have no deps, got %v", steps[0].Needs)
}
// trigger-pending-spawns needs inbox-check
if len(steps[1].Needs) != 1 || steps[1].Needs[0] != "inbox-check" {
t.Errorf("trigger-pending-spawns should need inbox-check, got %v", steps[1].Needs)
}
// loop-or-exit needs context-check
if len(steps[7].Needs) != 1 || steps[7].Needs[0] != "context-check" {
t.Errorf("loop-or-exit should need context-check, got %v", steps[7].Needs)
}
}
func TestWitnessPatrolMolecule(t *testing.T) {
mol := WitnessPatrolMolecule()
if mol.ID != "mol-witness-patrol" {
t.Errorf("expected ID 'mol-witness-patrol', got %q", mol.ID)
}
if mol.Title != "Witness Patrol" {
t.Errorf("expected Title 'Witness Patrol', got %q", mol.Title)
}
steps, err := ParseMoleculeSteps(mol.Description)
if err != nil {
t.Fatalf("failed to parse: %v", err)
}
// Should have 9 steps using Christmas Ornament pattern:
// PREFLIGHT: inbox-check, check-refinery, load-state
// DISCOVERY: survey-workers (bonds mol-polecat-arm dynamically)
// CLEANUP: aggregate, save-state, generate-summary, context-check, burn-or-loop
if len(steps) != 9 {
t.Errorf("expected 9 steps, got %d", len(steps))
}
expectedRefs := []string{
"inbox-check", "check-refinery", "load-state", "survey-workers",
"aggregate", "save-state", "generate-summary", "context-check", "burn-or-loop",
}
for i, expected := range expectedRefs {
if i >= len(steps) {
t.Errorf("missing step %d: expected %q", i, expected)
continue
}
if steps[i].Ref != expected {
t.Errorf("step %d: expected ref %q, got %q", i, expected, steps[i].Ref)
}
}
// Verify key dependencies
// inbox-check has no deps (first step)
if len(steps[0].Needs) != 0 {
t.Errorf("inbox-check should have no deps, got %v", steps[0].Needs)
}
// check-refinery needs inbox-check
if len(steps[1].Needs) != 1 || steps[1].Needs[0] != "inbox-check" {
t.Errorf("check-refinery should need inbox-check, got %v", steps[1].Needs)
}
// load-state needs check-refinery
if len(steps[2].Needs) != 1 || steps[2].Needs[0] != "check-refinery" {
t.Errorf("load-state should need check-refinery, got %v", steps[2].Needs)
}
// aggregate needs survey-workers (fanout gate)
if len(steps[4].Needs) != 1 || steps[4].Needs[0] != "survey-workers" {
t.Errorf("aggregate should need survey-workers, got %v", steps[4].Needs)
}
// aggregate should have WaitsFor: all-children
if len(steps[4].WaitsFor) != 1 || steps[4].WaitsFor[0] != "all-children" {
t.Errorf("aggregate should WaitsFor all-children, got %v", steps[4].WaitsFor)
}
// burn-or-loop needs context-check
if len(steps[8].Needs) != 1 || steps[8].Needs[0] != "context-check" {
t.Errorf("burn-or-loop should need context-check, got %v", steps[8].Needs)
}
}
func TestPolecatArmMolecule(t *testing.T) {
mol := PolecatArmMolecule()
if mol.ID != "mol-polecat-arm" {
t.Errorf("expected ID 'mol-polecat-arm', got %q", mol.ID)
}
if mol.Title != "Polecat Arm" {
t.Errorf("expected Title 'Polecat Arm', got %q", mol.Title)
}
steps, err := ParseMoleculeSteps(mol.Description)
if err != nil {
t.Fatalf("failed to parse: %v", err)
}
// Should have 5 steps: capture, assess, load-history, decide, execute
if len(steps) != 5 {
t.Errorf("expected 5 steps, got %d", len(steps))
}
expectedRefs := []string{"capture", "assess", "load-history", "decide", "execute"}
for i, expected := range expectedRefs {
if i >= len(steps) {
t.Errorf("missing step %d: expected %q", i, expected)
continue
}
if steps[i].Ref != expected {
t.Errorf("step %d: expected ref %q, got %q", i, expected, steps[i].Ref)
}
}
// Verify dependencies form a chain
// capture has no deps (first step)
if len(steps[0].Needs) != 0 {
t.Errorf("capture should have no deps, got %v", steps[0].Needs)
}
// assess needs capture
if len(steps[1].Needs) != 1 || steps[1].Needs[0] != "capture" {
t.Errorf("assess should need capture, got %v", steps[1].Needs)
}
// load-history needs assess
if len(steps[2].Needs) != 1 || steps[2].Needs[0] != "assess" {
t.Errorf("load-history should need assess, got %v", steps[2].Needs)
}
// decide needs load-history
if len(steps[3].Needs) != 1 || steps[3].Needs[0] != "load-history" {
t.Errorf("decide should need load-history, got %v", steps[3].Needs)
}
// execute needs decide
if len(steps[4].Needs) != 1 || steps[4].Needs[0] != "decide" {
t.Errorf("execute should need decide, got %v", steps[4].Needs)
}
}

View File

@@ -1,545 +0,0 @@
package beads
import (
"os"
"path/filepath"
"strings"
"testing"
)
// TestChristmasOrnamentPattern tests the dynamic bonding pattern used by mol-witness-patrol.
// This pattern allows a parent molecule step to dynamically create child molecules
// at runtime, with a fanout gate (WaitsFor: all-children) for aggregation.
func TestChristmasOrnamentPattern(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
// Find beads repo
workDir := findBeadsDir(t)
if workDir == "" {
t.Skip("no .beads directory found")
}
b := New(workDir)
// Test 1: Verify mol-witness-patrol has correct structure
t.Run("WitnessPatrolStructure", func(t *testing.T) {
mol := WitnessPatrolMolecule()
steps, err := ParseMoleculeSteps(mol.Description)
if err != nil {
t.Fatalf("failed to parse mol-witness-patrol: %v", err)
}
// Find aggregate step
var aggregateStep *MoleculeStep
for i := range steps {
if steps[i].Ref == "aggregate" {
aggregateStep = &steps[i]
break
}
}
if aggregateStep == nil {
t.Fatal("aggregate step not found in mol-witness-patrol")
}
// Verify WaitsFor: all-children
hasAllChildren := false
for _, cond := range aggregateStep.WaitsFor {
if strings.ToLower(cond) == "all-children" {
hasAllChildren = true
break
}
}
if !hasAllChildren {
t.Errorf("aggregate step should have WaitsFor: all-children, got %v", aggregateStep.WaitsFor)
}
// Verify aggregate needs survey-workers
needsSurvey := false
for _, dep := range aggregateStep.Needs {
if dep == "survey-workers" {
needsSurvey = true
break
}
}
if !needsSurvey {
t.Errorf("aggregate step should need survey-workers, got %v", aggregateStep.Needs)
}
})
// Test 2: Verify mol-polecat-arm has correct structure
t.Run("PolecatArmStructure", func(t *testing.T) {
mol := PolecatArmMolecule()
steps, err := ParseMoleculeSteps(mol.Description)
if err != nil {
t.Fatalf("failed to parse mol-polecat-arm: %v", err)
}
// Should have 5 steps in order: capture, assess, load-history, decide, execute
expectedRefs := []string{"capture", "assess", "load-history", "decide", "execute"}
if len(steps) != len(expectedRefs) {
t.Errorf("expected %d steps, got %d", len(expectedRefs), len(steps))
}
for i, expected := range expectedRefs {
if i < len(steps) && steps[i].Ref != expected {
t.Errorf("step %d: expected %q, got %q", i, expected, steps[i].Ref)
}
}
// Verify template variables are present in description
if !strings.Contains(mol.Description, "{{polecat_name}}") {
t.Error("mol-polecat-arm should have {{polecat_name}} template variable")
}
if !strings.Contains(mol.Description, "{{rig}}") {
t.Error("mol-polecat-arm should have {{rig}} template variable")
}
})
// Test 3: Template variable expansion
t.Run("TemplateVariableExpansion", func(t *testing.T) {
mol := PolecatArmMolecule()
ctx := map[string]string{
"polecat_name": "toast",
"rig": "gastown",
}
expanded := ExpandTemplateVars(mol.Description, ctx)
// Template variables should be expanded
if strings.Contains(expanded, "{{polecat_name}}") {
t.Error("{{polecat_name}} was not expanded")
}
if strings.Contains(expanded, "{{rig}}") {
t.Error("{{rig}} was not expanded")
}
// Values should be present
if !strings.Contains(expanded, "toast") {
t.Error("polecat_name value 'toast' not found in expanded description")
}
if !strings.Contains(expanded, "gastown") {
t.Error("rig value 'gastown' not found in expanded description")
}
})
// Test 4: Create parent and verify bonding metadata parsing
t.Run("BondingMetadataParsing", func(t *testing.T) {
// Create a test issue with bonding metadata (simulating what mol bond creates)
bondingDesc := `Polecat Arm (arm-toast)
---
bonded_from: mol-polecat-arm
bonded_to: patrol-x7k
bonded_ref: arm-toast
bonded_at: 2025-12-23T10:00:00Z
`
// Verify we can parse the bonding metadata
if !strings.Contains(bondingDesc, "bonded_from:") {
t.Error("bonding metadata should contain bonded_from")
}
if !strings.Contains(bondingDesc, "bonded_to:") {
t.Error("bonding metadata should contain bonded_to")
}
if !strings.Contains(bondingDesc, "bonded_ref:") {
t.Error("bonding metadata should contain bonded_ref")
}
})
// Test 5: Verify issue creation with parent relationship works
t.Run("ParentChildRelationship", func(t *testing.T) {
// Create a test parent issue
parent, err := b.Create(CreateOptions{
Title: "Test Patrol Parent",
Type: "task",
Priority: 2,
Description: "Test parent for Christmas Ornament pattern",
})
if err != nil {
t.Fatalf("failed to create parent issue: %v", err)
}
defer func() {
_ = b.Close(parent.ID)
}()
// Create a child issue under the parent
child, err := b.Create(CreateOptions{
Title: "Test Polecat Arm",
Type: "task",
Priority: parent.Priority,
Parent: parent.ID,
Description: "Test child for bonding pattern",
})
if err != nil {
t.Fatalf("failed to create child issue: %v", err)
}
defer func() {
_ = b.Close(child.ID)
}()
// Verify parent-child relationship exists
// The child should have a dependency on the parent
t.Logf("Created parent %s and child %s", parent.ID, child.ID)
})
}
// TestEmptyPatrol tests the scenario where witness patrol runs with 0 polecats.
func TestEmptyPatrol(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
mol := WitnessPatrolMolecule()
steps, err := ParseMoleculeSteps(mol.Description)
if err != nil {
t.Fatalf("failed to parse mol-witness-patrol: %v", err)
}
// Find survey-workers step
var surveyStep *MoleculeStep
for i := range steps {
if steps[i].Ref == "survey-workers" {
surveyStep = &steps[i]
break
}
}
if surveyStep == nil {
t.Fatal("survey-workers step not found")
}
// Verify the step description mentions handling 0 polecats
if !strings.Contains(surveyStep.Instructions, "no polecats") &&
!strings.Contains(surveyStep.Instructions, "If no polecats") {
t.Log("Note: survey-workers step should document handling of 0 polecats case")
}
// With 0 polecats:
// - survey-workers bonds no children
// - aggregate step with WaitsFor: all-children should complete immediately
// This is correct behavior - an empty set of children is vacuously complete
}
// TestNudgeProgression tests the nudge matrix logic documented in mol-polecat-arm.
func TestNudgeProgression(t *testing.T) {
mol := PolecatArmMolecule()
steps, err := ParseMoleculeSteps(mol.Description)
if err != nil {
t.Fatalf("failed to parse mol-polecat-arm: %v", err)
}
// Find decide step (contains the nudge matrix)
var decideStep *MoleculeStep
for i := range steps {
if steps[i].Ref == "decide" {
decideStep = &steps[i]
break
}
}
if decideStep == nil {
t.Fatal("decide step not found in mol-polecat-arm")
}
// Verify the nudge matrix is documented
nudgeKeywords := []string{"nudge-1", "nudge-2", "nudge-3", "escalate"}
for _, keyword := range nudgeKeywords {
if !strings.Contains(decideStep.Instructions, keyword) {
t.Errorf("decide step should document %s action", keyword)
}
}
// Verify idle time thresholds are documented
timeThresholds := []string{"10-15min", "15-20min", "20+min"}
for _, threshold := range timeThresholds {
if !strings.Contains(decideStep.Instructions, threshold) &&
!strings.Contains(decideStep.Instructions, strings.ReplaceAll(threshold, "-", " ")) {
t.Logf("Note: decide step should document %s threshold", threshold)
}
}
}
// TestPreKillVerification tests that the execute step documents pre-kill verification.
func TestPreKillVerification(t *testing.T) {
mol := PolecatArmMolecule()
steps, err := ParseMoleculeSteps(mol.Description)
if err != nil {
t.Fatalf("failed to parse mol-polecat-arm: %v", err)
}
// Find execute step
var executeStep *MoleculeStep
for i := range steps {
if steps[i].Ref == "execute" {
executeStep = &steps[i]
break
}
}
if executeStep == nil {
t.Fatal("execute step not found in mol-polecat-arm")
}
// Verify pre-kill verification is documented
if !strings.Contains(executeStep.Instructions, "pre-kill") &&
!strings.Contains(executeStep.Instructions, "git status") {
t.Error("execute step should document pre-kill verification")
}
// Verify clean git state check
if !strings.Contains(executeStep.Instructions, "clean") {
t.Error("execute step should check for clean git state")
}
// Verify unpushed commits check
if !strings.Contains(executeStep.Instructions, "unpushed") {
t.Log("Note: execute step should document unpushed commits check")
}
}
// TestWaitsForAllChildren tests the fanout gate semantics.
func TestWaitsForAllChildren(t *testing.T) {
// Test the WaitsFor parsing
desc := `## Step: survey
Discover items.
## Step: aggregate
Collect results.
WaitsFor: all-children
Needs: survey`
steps, err := ParseMoleculeSteps(desc)
if err != nil {
t.Fatalf("failed to parse: %v", err)
}
if len(steps) != 2 {
t.Fatalf("expected 2 steps, got %d", len(steps))
}
aggregate := steps[1]
if aggregate.Ref != "aggregate" {
t.Errorf("expected aggregate step, got %s", aggregate.Ref)
}
if len(aggregate.WaitsFor) != 1 || aggregate.WaitsFor[0] != "all-children" {
t.Errorf("expected WaitsFor: [all-children], got %v", aggregate.WaitsFor)
}
}
// TestMultipleWaitsForConditions tests parsing multiple WaitsFor conditions.
func TestMultipleWaitsForConditions(t *testing.T) {
desc := `## Step: finalize
Complete the process.
WaitsFor: all-children, external-signal, timeout`
steps, err := ParseMoleculeSteps(desc)
if err != nil {
t.Fatalf("failed to parse: %v", err)
}
if len(steps) != 1 {
t.Fatalf("expected 1 step, got %d", len(steps))
}
expected := []string{"all-children", "external-signal", "timeout"}
if len(steps[0].WaitsFor) != len(expected) {
t.Errorf("expected %d WaitsFor conditions, got %d", len(expected), len(steps[0].WaitsFor))
}
for i, exp := range expected {
if i < len(steps[0].WaitsFor) && steps[0].WaitsFor[i] != exp {
t.Errorf("WaitsFor[%d]: expected %q, got %q", i, exp, steps[0].WaitsFor[i])
}
}
}
// TestMolBondCLI tests the mol bond command via CLI integration.
// This test requires the gt binary to be built and in PATH.
func TestMolBondCLI(t *testing.T) {
if testing.Short() {
t.Skip("skipping CLI integration test in short mode")
}
workDir := findBeadsDir(t)
if workDir == "" {
t.Skip("no .beads directory found")
}
b := New(workDir)
// Create a parent issue to bond to
parent, err := b.Create(CreateOptions{
Title: "Test Patrol for Bonding",
Type: "task",
Priority: 2,
Description: "Parent issue for mol bond CLI test",
})
if err != nil {
t.Fatalf("failed to create parent issue: %v", err)
}
defer func() {
// Clean up: close parent (children are auto-closed as children)
_ = b.Close(parent.ID)
}()
// Test bonding mol-polecat-arm to the parent
t.Run("BondPolecatArm", func(t *testing.T) {
// Use bd mol bond command (the underlying command)
// gt mol bond mol-polecat-arm --parent=<id> --ref=arm-test --var polecat_name=toast --var rig=gastown
args := []string{
"mol", "bond", "mol-polecat-arm",
"--parent", parent.ID,
"--ref", "arm-toast",
"--var", "polecat_name=toast",
"--var", "rig=gastown",
}
// Execute via the beads wrapper
// Since we can't easily call the cmd package from here,
// we verify the bonding logic works by testing the building blocks
// 1. Verify mol-polecat-arm exists in catalog
catalog := BuiltinMolecules()
var polecatArm *BuiltinMolecule
for i := range catalog {
if catalog[i].ID == "mol-polecat-arm" {
polecatArm = &catalog[i]
break
}
}
if polecatArm == nil {
t.Fatal("mol-polecat-arm not found in catalog")
}
// 2. Verify template expansion works
ctx := map[string]string{
"polecat_name": "toast",
"rig": "gastown",
}
expanded := ExpandTemplateVars(polecatArm.Description, ctx)
if strings.Contains(expanded, "{{polecat_name}}") {
t.Error("template variable polecat_name was not expanded")
}
if strings.Contains(expanded, "{{rig}}") {
t.Error("template variable rig was not expanded")
}
// 3. Create a child issue manually to simulate what mol bond does
childTitle := "Polecat Arm (arm-toast)"
bondingMeta := `
---
bonded_from: mol-polecat-arm
bonded_to: ` + parent.ID + `
bonded_ref: arm-toast
bonded_at: 2025-12-23T10:00:00Z
`
childDesc := expanded + bondingMeta
child, err := b.Create(CreateOptions{
Title: childTitle,
Type: "task",
Priority: parent.Priority,
Parent: parent.ID,
Description: childDesc,
})
if err != nil {
t.Fatalf("failed to create bonded child: %v", err)
}
defer func() {
_ = b.Close(child.ID)
}()
// 4. Verify the child was created with correct properties
fetched, err := b.Show(child.ID)
if err != nil {
t.Fatalf("failed to fetch child: %v", err)
}
if !strings.Contains(fetched.Title, "arm-toast") {
t.Errorf("child title should contain arm-toast, got %s", fetched.Title)
}
if !strings.Contains(fetched.Description, "bonded_from: mol-polecat-arm") {
t.Error("child description should contain bonding metadata")
}
if !strings.Contains(fetched.Description, "toast") {
t.Error("child description should have expanded polecat_name")
}
t.Logf("Created bonded child: %s (%s)", child.ID, childTitle)
t.Logf("Args that would be used: %v", args)
})
}
// TestActivityFeed tests the activity feed output from witness patrol.
func TestActivityFeed(t *testing.T) {
// The activity feed should show:
// - Polecats inspected
// - Nudges sent
// - Sessions killed
// - Escalations
mol := WitnessPatrolMolecule()
steps, err := ParseMoleculeSteps(mol.Description)
if err != nil {
t.Fatalf("failed to parse mol-witness-patrol: %v", err)
}
// Find generate-summary step (produces activity feed)
var summaryStep *MoleculeStep
for i := range steps {
if steps[i].Ref == "generate-summary" {
summaryStep = &steps[i]
break
}
}
if summaryStep == nil {
t.Fatal("generate-summary step not found in mol-witness-patrol")
}
// Verify the step documents key metrics
expectedMetrics := []string{
"Workers inspected",
"Nudges sent",
"Sessions killed",
"Escalations",
}
for _, metric := range expectedMetrics {
if !strings.Contains(strings.ToLower(summaryStep.Instructions), strings.ToLower(metric)) {
t.Logf("Note: generate-summary should document %q metric", metric)
}
}
// Verify it mentions digests (for squashing)
if !strings.Contains(summaryStep.Instructions, "digest") {
t.Log("Note: generate-summary should mention digest creation")
}
}
// findBeadsDir walks up from current directory to find .beads
func findBeadsDir(t *testing.T) string {
t.Helper()
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("failed to get cwd: %v", err)
}
dir := cwd
for {
if _, err := os.Stat(filepath.Join(dir, ".beads")); err == nil {
return dir
}
parent := filepath.Dir(dir)
if parent == dir {
return ""
}
dir = parent
}
}

View File

@@ -1,695 +0,0 @@
// Package beads provides a wrapper for the bd (beads) CLI.
package beads
// DeaconPatrolMolecule returns the deacon-patrol molecule definition.
// This is the Mayor's daemon loop for handling callbacks, health checks, and cleanup.
func DeaconPatrolMolecule() BuiltinMolecule {
return BuiltinMolecule{
ID: "mol-deacon-patrol",
Title: "Deacon Patrol",
Description: `Mayor's daemon patrol loop.
The Deacon is the Mayor's background process that runs continuously,
handling callbacks, monitoring rig health, and performing cleanup.
Each patrol cycle runs these steps in sequence, then loops or exits.
## Step: inbox-check
Handle callbacks from agents.
Check the Mayor's inbox for messages from:
- Witnesses reporting polecat status
- Refineries reporting merge results
- Polecats requesting help or escalation
- External triggers (webhooks, timers)
Process each message:
` + "```" + `bash
gt mail inbox
# For each message:
gt mail read <id>
# Handle based on message type
` + "```" + `
Callbacks may spawn new polecats, update issue state, or trigger other actions.
## Step: trigger-pending-spawns
Nudge newly spawned polecats that are ready for input.
When polecats are spawned, their Claude session takes 10-20 seconds to initialize.
The spawn command returns immediately without waiting. This step finds spawned
polecats that are now ready and sends them a trigger to start working.
` + "```" + `bash
# For each rig with polecats
for rig in gastown beads; do
gt polecats $rig
# For each working polecat, check if Claude is ready
# Use tmux capture-pane to look for "> " prompt
done
` + "```" + `
For each ready polecat that hasn't been triggered yet:
1. Send "Begin." to trigger UserPromptSubmit hook
2. The hook injects mail, polecat sees its assignment
3. Mark polecat as triggered in state
Use WaitForClaudeReady from tmux package (polls for "> " prompt).
Timeout: 60 seconds per polecat. If not ready, try again next cycle.
Needs: inbox-check
## Step: health-scan
Check Witness and Refinery health for each rig.
**ZFC Principle**: You (Claude) make the judgment call about what is "stuck" or
"unresponsive" - there are no hardcoded thresholds in Go. Read the signals,
consider context, and decide.
For each rig, run:
` + "```" + `bash
gt witness status <rig>
gt refinery status <rig>
` + "```" + `
**Signals to assess:**
| Component | Healthy Signals | Concerning Signals |
|-----------|-----------------|-------------------|
| Witness | State: running, recent activity | State: not running, no heartbeat |
| Refinery | State: running, queue processing | Queue stuck, merge failures |
**Tracking unresponsive cycles:**
Maintain in your patrol state (persisted across cycles):
` + "```" + `
health_state:
<rig>:
witness:
unresponsive_cycles: 0
last_seen_healthy: <timestamp>
refinery:
unresponsive_cycles: 0
last_seen_healthy: <timestamp>
` + "```" + `
**Decision matrix** (you decide the thresholds based on context):
| Cycles Unresponsive | Suggested Action |
|---------------------|------------------|
| 1-2 | Note it, check again next cycle |
| 3-4 | Attempt restart: gt witness restart <rig> |
| 5+ | Escalate to Mayor with context |
**Restart commands:**
` + "```" + `bash
gt witness restart <rig>
gt refinery restart <rig>
` + "```" + `
**Escalation:**
` + "```" + `bash
gt mail send mayor/ -s "Health: <rig> <component> unresponsive" \
-m "Component has been unresponsive for N cycles. Restart attempts failed.
Last healthy: <timestamp>
Error signals: <details>"
` + "```" + `
Reset unresponsive_cycles to 0 when component responds normally.
Needs: trigger-pending-spawns
## Step: plugin-run
Execute registered plugins.
Scan ~/gt/plugins/ for plugin directories. Each plugin has a plugin.md with
YAML frontmatter defining its gate (when to run) and instructions (what to do).
See docs/deacon-plugins.md for full documentation.
Gate types:
- cooldown: Time since last run (e.g., 24h)
- cron: Schedule-based (e.g., "0 9 * * *")
- condition: Metric threshold (e.g., wisp count > 50)
- event: Trigger-based (e.g., startup, heartbeat)
For each plugin:
1. Read plugin.md frontmatter to check gate
2. Compare against state.json (last run, etc.)
3. If gate is open, execute the plugin
Plugins marked parallel: true can run concurrently using Task tool subagents.
Sequential plugins run one at a time in directory order.
Skip this step if ~/gt/plugins/ does not exist or is empty.
Needs: health-scan
## Step: orphan-check
Find abandoned work.
Scan for orphaned state:
- Issues marked in_progress with no active polecat
- Polecats that stopped responding mid-work
- Merge queue entries with no polecat owner
- Wisp sessions that outlived their spawner
` + "```" + `bash
bd list --status=in_progress
gt polecats --all --orphan
` + "```" + `
For each orphan:
- Check if polecat session still exists
- If not, mark issue for reassignment or retry
- File incident beads if data loss occurred
Needs: health-scan
## Step: session-gc
Clean dead sessions.
Garbage collect terminated sessions:
- Remove stale polecat directories
- Clean up wisp session artifacts
- Prune old logs and temp files
- Archive completed molecule state
` + "```" + `bash
gt gc --sessions
gt gc --wisps --age=1h
` + "```" + `
Preserve audit trail. Only clean sessions confirmed dead.
Needs: orphan-check
## Step: context-check
Check own context limit.
The Deacon runs in a Claude session with finite context.
Check if approaching the limit:
` + "```" + `bash
gt context --usage
` + "```" + `
If context is high (>80%), prepare for handoff:
- Summarize current state
- Note any pending work
- Write handoff to molecule state
This enables the Deacon to burn and respawn cleanly.
Needs: session-gc
## Step: loop-or-exit
Burn and let daemon respawn, or exit if context high.
Decision point at end of patrol cycle:
If context is LOW:
- Sleep briefly (avoid tight loop)
- Return to inbox-check step
If context is HIGH:
- Write state to persistent storage
- Exit cleanly
- Let the daemon orchestrator respawn a fresh Deacon
The daemon ensures Deacon is always running:
` + "```" + `bash
# Daemon respawns on exit
gt daemon status
` + "```" + `
This enables infinite patrol duration via context-aware respawning.
Needs: context-check`,
}
}
// WitnessPatrolMolecule returns the witness-patrol molecule definition.
// This is the per-rig worker monitor's patrol loop using the Christmas Ornament pattern.
func WitnessPatrolMolecule() BuiltinMolecule {
return BuiltinMolecule{
ID: "mol-witness-patrol",
Title: "Witness Patrol",
Description: `Per-rig worker monitor patrol loop using the Christmas Ornament pattern.
The Witness is the Pit Boss for your rig. You watch polecats, nudge them toward
completion, verify clean git state before kills, and escalate stuck workers.
**You do NOT do implementation work.** Your job is oversight, not coding.
This molecule uses dynamic bonding to create mol-polecat-arm for each worker,
enabling parallel inspection with a fanout gate for aggregation.
## The Christmas Ornament Shape
` + "```" + `
★ mol-witness-patrol (trunk)
/|\
┌────────┘ │ └────────┐
PREFLIGHT DISCOVERY CLEANUP
│ │ │
inbox-check survey aggregate (WaitsFor: all-children)
check-refnry │ save-state
load-state │ generate-summary
↓ context-check
┌───────┼───────┐ burn-or-loop
● ● ● mol-polecat-arm (dynamic)
ace nux toast
` + "```" + `
---
# PREFLIGHT PHASE
---
## Step: inbox-check
Process witness mail: lifecycle requests, help requests.
` + "```" + `bash
gt mail inbox
` + "```" + `
Handle by message type:
- **LIFECYCLE/Shutdown**: Queue for pre-kill verification
- **Blocked/Help**: Assess if resolvable or escalate
- **HANDOFF**: Load predecessor state
- **Work complete**: Verify issue closed, proceed to pre-kill
Record any pending actions for later steps.
Mark messages as processed when complete.
## Step: check-refinery
Ensure the refinery is alive and processing merge requests.
**Redundant system**: This check runs in both gt spawn and Witness patrol
to ensure the merge queue processor stays operational.
` + "```" + `bash
# Check if refinery session is running
gt session status <rig>/refinery
# Check for merge requests in queue
bd list --type=merge-request --status=open
` + "```" + `
If merge requests are waiting AND refinery is not running:
` + "```" + `bash
gt session start <rig>/refinery
gt mail send <rig>/refinery -s "PATROL: Wake up" -m "Merge requests in queue. Please process."
` + "```" + `
If refinery is running but queue is non-empty for >30 min, send nudge.
This ensures polecats don't wait forever for their branches to merge.
Needs: inbox-check
## Step: load-state
Read handoff bead and get nudge counts.
Load persistent state from the witness handoff bead:
- Active workers and their status from last cycle
- Nudge counts per worker per issue
- Last nudge timestamps
- Pending escalations
` + "```" + `bash
bd show <handoff-bead-id>
` + "```" + `
If no handoff exists (fresh start), initialize empty state.
This state persists across wisp burns and session cycles.
Needs: check-refinery
---
# DISCOVERY PHASE (Dynamic Bonding)
---
## Step: survey-workers
List polecats and bond mol-polecat-arm for each one.
` + "```" + `bash
# Get list of polecats
gt polecat list <rig>
` + "```" + `
For each polecat discovered, dynamically bond an inspection arm:
` + "```" + `bash
# Bond mol-polecat-arm for each polecat
for polecat in $(gt polecat list <rig> --names); do
bd mol bond mol-polecat-arm $PATROL_WISP_ID \
--ref arm-$polecat \
--var polecat_name=$polecat \
--var rig=<rig>
done
` + "```" + `
This creates child wisps like:
- patrol-x7k.arm-ace (5 steps)
- patrol-x7k.arm-nux (5 steps)
- patrol-x7k.arm-toast (5 steps)
Each arm runs in PARALLEL. The aggregate step will wait for all to complete.
If no polecats are found, this step completes immediately with no children.
Needs: load-state
---
# CLEANUP PHASE (Gate + Fixed Steps)
---
## Step: aggregate
Collect outcomes from all polecat inspection arms.
WaitsFor: all-children
This is a **fanout gate** - it cannot proceed until ALL dynamically-bonded
polecat arms have completed their inspection cycles.
Once all arms complete, collect their outcomes:
- Actions taken per polecat (nudge, kill, escalate, none)
- Updated nudge counts
- Any errors or issues discovered
Build the consolidated state for save-state.
Needs: survey-workers
## Step: save-state
Update handoff bead with new states.
Persist state to the witness handoff bead:
- Updated worker statuses from all arms
- Current nudge counts per worker
- Nudge timestamps
- Actions taken this cycle
- Pending items for next cycle
` + "```" + `bash
bd update <handoff-bead-id> --description="<serialized state>"
` + "```" + `
This state survives wisp burns and session cycles.
Needs: aggregate
## Step: generate-summary
Summarize this patrol cycle for digest.
Include:
- Workers inspected (count, names)
- Nudges sent (count, to whom)
- Sessions killed (count, names)
- Escalations (count, issues)
- Issues found (brief descriptions)
- Actions pending for next cycle
This becomes the digest when the patrol wisp is squashed.
Needs: save-state
## Step: context-check
Check own context usage.
If context is HIGH (>80%):
- Ensure state is saved to handoff bead
- Prepare for burn/respawn
If context is LOW:
- Can continue patrolling
Needs: generate-summary
## Step: burn-or-loop
End of patrol cycle decision.
If context is LOW:
- Burn this wisp (no audit trail needed for patrol cycles)
- Sleep briefly to avoid tight loop (30-60 seconds)
- Return to inbox-check step
If context is HIGH:
- Burn wisp with summary digest
- Exit cleanly (daemon will respawn fresh Witness)
` + "```" + `bash
bd mol burn # Destroy ephemeral wisp
` + "```" + `
The daemon ensures Witness is always running.
Needs: context-check`,
}
}
// PolecatArmMolecule returns the polecat-arm molecule definition.
// This is dynamically bonded by mol-witness-patrol for each polecat being monitored.
func PolecatArmMolecule() BuiltinMolecule {
return BuiltinMolecule{
ID: "mol-polecat-arm",
Title: "Polecat Arm",
Description: `Single polecat inspection and action cycle.
This molecule is bonded dynamically by mol-witness-patrol's survey-workers step.
Each polecat being monitored gets one arm that runs in parallel with other arms.
## Variables
| Variable | Required | Description |
|----------|----------|-------------|
| polecat_name | Yes | Name of the polecat to inspect |
| rig | Yes | Rig containing the polecat |
## Step: capture
Capture recent tmux output for {{polecat_name}}.
` + "```" + `bash
tmux capture-pane -t gt-{{rig}}-{{polecat_name}} -p | tail -50
` + "```" + `
Record:
- Last activity timestamp (when was last tool call?)
- Visible errors or stack traces
- Completion indicators ("Done", "Finished", etc.)
## Step: assess
Categorize polecat state based on captured output.
States:
- **working**: Recent tool calls, active processing
- **idle**: At prompt, no recent activity
- **error**: Showing errors or stack traces
- **requesting_shutdown**: Sent LIFECYCLE/Shutdown mail
- **done**: Showing completion indicators
Calculate: minutes since last activity.
Needs: capture
## Step: load-history
Read nudge history for {{polecat_name}} from patrol state.
` + "```" + `
nudge_count = state.nudges[{{polecat_name}}].count
last_nudge_time = state.nudges[{{polecat_name}}].timestamp
` + "```" + `
This data was loaded by the parent patrol's load-state step and passed
to the arm via the bonding context.
Needs: assess
## Step: decide
Apply the nudge matrix to determine action for {{polecat_name}}.
| State | Idle Time | Nudge Count | Action |
|-------|-----------|-------------|--------|
| working | any | any | none |
| idle | <10min | any | none |
| idle | 10-15min | 0 | nudge-1 (gentle) |
| idle | 15-20min | 1 | nudge-2 (direct) |
| idle | 20+min | 2 | nudge-3 (final) |
| idle | any | 3 | escalate |
| error | any | any | assess-severity |
| requesting_shutdown | any | any | pre-kill-verify |
| done | any | any | pre-kill-verify |
Nudge text:
1. "How's progress? Need any help?"
2. "Please wrap up soon. What's blocking you?"
3. "Final check. Will escalate in 5 min if no response."
Record decision and rationale.
Needs: load-history
## Step: execute
Take the decided action for {{polecat_name}}.
**nudge-N**:
` + "```" + `bash
tmux send-keys -t gt-{{rig}}-{{polecat_name}} "{{nudge_text}}" Enter
` + "```" + `
**pre-kill-verify**:
` + "```" + `bash
cd polecats/{{polecat_name}}
git status # Must be clean
git log origin/main..HEAD # Check for unpushed
bd show <assigned-issue> # Verify closed/deferred
` + "```" + `
If clean: kill session, remove worktree, delete branch
If dirty: record failure, retry next cycle
**escalate**:
` + "```" + `bash
gt mail send mayor/ -s "Escalation: {{polecat_name}} stuck" -m "..."
` + "```" + `
**none**: No action needed.
Record: action taken, result, updated nudge count.
Needs: decide
## Output
The arm completes with:
- action_taken: none | nudge-1 | nudge-2 | nudge-3 | killed | escalated
- result: success | failed | pending
- updated_state: New nudge count and timestamp for {{polecat_name}}
This data feeds back to the parent patrol's aggregate step.`,
}
}
// RefineryPatrolMolecule returns the refinery-patrol molecule definition.
// This is the merge queue processor's patrol loop with verification gates.
func RefineryPatrolMolecule() BuiltinMolecule {
return BuiltinMolecule{
ID: "mol-refinery-patrol",
Title: "Refinery Patrol",
Description: `Merge queue processor patrol loop.
The Refinery is the Engineer in the engine room. You process polecat branches,
merging them to main one at a time with sequential rebasing.
**The Scotty Test**: Before proceeding past any failure, ask yourself:
"Would Scotty walk past a warp core leak because it existed before his shift?"
## Step: inbox-check
Check mail for MR submissions, escalations, messages.
` + "```" + `bash
gt mail inbox
# Process any urgent items
` + "```" + `
Handle shutdown requests, escalations, and status queries.
## Step: queue-scan
Fetch remote and identify polecat branches waiting.
` + "```" + `bash
git fetch origin
git branch -r | grep polecat
gt refinery queue <rig>
` + "```" + `
If queue empty, skip to context-check step.
Track branch list for this cycle.
Needs: inbox-check
## Step: process-branch
Pick next branch. Rebase on current main.
` + "```" + `bash
git checkout -b temp origin/<polecat-branch>
git rebase origin/main
` + "```" + `
If rebase conflicts and unresolvable:
- git rebase --abort
- Notify polecat to fix and resubmit
- Skip to loop-check for next branch
Needs: queue-scan
## Step: run-tests
Run the test suite.
` + "```" + `bash
go test ./...
` + "```" + `
Track results: pass count, fail count, specific failures.
Needs: process-branch
## Step: handle-failures
**VERIFICATION GATE**: This step enforces the Beads Promise.
If tests PASSED: This step auto-completes. Proceed to merge.
If tests FAILED:
1. Diagnose: Is this a branch regression or pre-existing on main?
2. If branch caused it:
- Abort merge
- Notify polecat: "Tests failing. Please fix and resubmit."
- Skip to loop-check
3. If pre-existing on main:
- Option A: Fix it yourself (you're the Engineer!)
- Option B: File a bead: bd create --type=bug --priority=1 --title="..."
**GATE REQUIREMENT**: You CANNOT proceed to merge-push without:
- Tests passing, OR
- Fix committed, OR
- Bead filed for the failure
This is non-negotiable. Never disavow. Never "note and proceed."
Needs: run-tests
## Step: merge-push
Merge to main and push immediately.
` + "```" + `bash
git checkout main
git merge --ff-only temp
git push origin main
git branch -d temp
git branch -D <polecat-branch> # Local delete (branches never go to origin)
` + "```" + `
Main has moved. Any remaining branches need rebasing on new baseline.
Needs: handle-failures
## Step: loop-check
More branches to process?
If yes: Return to process-branch with next branch.
If no: Continue to generate-summary.
Track: branches processed, branches skipped (with reasons).
Needs: merge-push
## Step: generate-summary
Summarize this patrol cycle.
Include:
- Branches processed (count, names)
- Test results (pass/fail)
- Issues filed (if any)
- Branches skipped (with reasons)
- Any escalations sent
This becomes the digest when the patrol is squashed.
Needs: loop-check
## Step: context-check
Check own context usage.
If context is HIGH (>80%):
- Write handoff summary
- Prepare for burn/respawn
If context is LOW:
- Can continue processing
Needs: generate-summary
## Step: burn-or-loop
End of patrol cycle decision.
If queue non-empty AND context LOW:
- Burn this wisp, start fresh patrol
- Return to inbox-check
If queue empty OR context HIGH:
- Burn wisp with summary digest
- Exit (daemon will respawn if needed)
Needs: context-check`,
}
}

View File

@@ -1,544 +0,0 @@
// Package beads provides a wrapper for the bd (beads) CLI.
package beads
// InstallGoBinaryMolecule returns the install-go-binary molecule definition.
// This is a single step to rebuild and install the gt binary after code changes.
func InstallGoBinaryMolecule() BuiltinMolecule {
return BuiltinMolecule{
ID: "mol-install-go-binary",
Title: "Install Go Binary",
Description: `Single step to rebuild and install the gt binary after code changes.
## Step: install
Build and install the gt binary locally.
Run from the rig directory:
` + "```" + `
go build -o gt ./cmd/gt
go install ./cmd/gt
` + "```" + `
Verify the installed binary is updated:
` + "```" + `
which gt
gt --version # if version command exists
` + "```",
}
}
// BootstrapGasTownMolecule returns the bootstrap molecule for new Gas Town installations.
// This walks a user through setting up Gas Town from scratch after brew install.
func BootstrapGasTownMolecule() BuiltinMolecule {
return BuiltinMolecule{
ID: "mol-bootstrap",
Title: "Bootstrap Gas Town",
Description: `Complete setup of a new Gas Town installation.
Run this after installing gt and bd via Homebrew. This molecule guides you through
creating an HQ, setting up rigs, and configuring your environment.
## Step: locate-hq
Determine where to install the Gas Town HQ.
Ask the user for their preferred location. Common choices:
- ~/gt (recommended - short, easy to type)
- ~/gastown
- ~/workspace/gt
Validate the path:
- Must not already exist (or be empty)
- Parent directory must be writable
- Avoid paths with spaces
Store the chosen path for subsequent steps.
## Step: create-hq
Create the HQ directory structure.
` + "```" + `bash
mkdir -p {{hq_path}}
cd {{hq_path}}
gt install . --name {{hq_name}}
` + "```" + `
If the user wants to track the HQ in git:
` + "```" + `bash
gt git-init --github={{github_repo}} --private
` + "```" + `
The HQ now has:
- mayor/ directory
- .beads/ for town-level tracking
- CLAUDE.md for mayor context
Needs: locate-hq
## Step: setup-rigs
Configure which rigs to add to the HQ.
Default rigs for Gas Town development:
- gastown (git@github.com:steveyegge/gastown.git)
- beads (git@github.com:steveyegge/beads.git)
For each rig, run:
` + "```" + `bash
gt rig add <name> <git-url> --prefix <prefix>
` + "```" + `
This creates the full rig structure:
- refinery/rig/ (canonical main clone)
- mayor/rig/ (mayor's working clone)
- crew/main/ (default human workspace)
- witness/ (polecat monitor)
- polecats/ (worker directory)
Needs: create-hq
## Step: build-gt
Build the gt binary from source.
` + "```" + `bash
cd {{hq_path}}/gastown/mayor/rig
go build -o gt ./cmd/gt
` + "```" + `
Verify the build succeeded:
` + "```" + `bash
./gt version
` + "```" + `
Needs: setup-rigs
Tier: haiku
## Step: install-paths
Install gt to a location in PATH.
Check if ~/bin or ~/.local/bin is in PATH:
` + "```" + `bash
echo $PATH | tr ':' '\n' | grep -E '(~/bin|~/.local/bin|/home/.*/bin)'
` + "```" + `
Copy the binary:
` + "```" + `bash
mkdir -p ~/bin
cp {{hq_path}}/gastown/mayor/rig/gt ~/bin/gt
` + "```" + `
If ~/bin is not in PATH, add to shell config:
` + "```" + `bash
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.zshrc
# or ~/.bashrc for bash users
` + "```" + `
Verify:
` + "```" + `bash
which gt
gt version
` + "```" + `
Needs: build-gt
Tier: haiku
## Step: init-beads
Initialize beads databases in all clones.
For each rig's mayor clone:
` + "```" + `bash
cd {{hq_path}}/<rig>/mayor/rig
bd init --prefix <rig-prefix>
` + "```" + `
For the town-level beads:
` + "```" + `bash
cd {{hq_path}}
bd init --prefix hq
` + "```" + `
Configure sync-branch for multi-clone setups:
` + "```" + `bash
echo "sync-branch: beads-sync" >> .beads/config.yaml
` + "```" + `
Needs: setup-rigs
Tier: haiku
## Step: sync-beads
Sync beads from remotes and fix any issues.
For each initialized beads database:
` + "```" + `bash
bd sync
bd doctor --fix
` + "```" + `
This imports existing issues from JSONL and sets up git hooks.
Needs: init-beads
Tier: haiku
## Step: verify
Verify the installation is complete and working.
Run health checks:
` + "```" + `bash
gt status # Should show rigs with crew/refinery/mayor
gt doctor # Check for issues
bd list # Should show issues from synced beads
` + "```" + `
Test spawning capability (dry run):
` + "```" + `bash
gt spawn --help
` + "```" + `
Print summary:
- HQ location
- Installed rigs
- gt version
- bd version
Needs: sync-beads, install-paths`,
}
}
// VersionBumpMolecule returns the version-bump molecule definition.
// This is the release checklist for Gas Town versions.
func VersionBumpMolecule() BuiltinMolecule {
return BuiltinMolecule{
ID: "mol-version-bump",
Title: "Version Bump",
Description: `Release checklist for Gas Town version {{version}}.
This molecule ensures all release steps are completed properly.
Replace {{version}} with the target version (e.g., 0.1.0).
## Step: update-version
Update version string in internal/cmd/version.go.
Change the Version variable to the new version:
` + "```" + `go
var (
Version = "{{version}}"
BuildTime = "unknown"
GitCommit = "unknown"
)
` + "```" + `
## Step: rebuild-binary
Rebuild the gt binary with version info.
` + "```" + `bash
go build -ldflags="-X github.com/steveyegge/gastown/internal/cmd.Version={{version}} \
-X github.com/steveyegge/gastown/internal/cmd.GitCommit=$(git rev-parse --short HEAD) \
-X github.com/steveyegge/gastown/internal/cmd.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-o gt ./cmd/gt
` + "```" + `
Verify the version:
` + "```" + `bash
./gt version
` + "```" + `
Needs: update-version
## Step: run-tests
Run the full test suite.
` + "```" + `bash
go test ./...
` + "```" + `
Fix any failures before proceeding.
Needs: rebuild-binary
## Step: update-changelog
Update CHANGELOG.md with release notes.
Add a new section at the top:
` + "```" + `markdown
## [{{version}}] - YYYY-MM-DD
### Added
- Feature descriptions
### Changed
- Change descriptions
### Fixed
- Bug fix descriptions
` + "```" + `
Needs: run-tests
## Step: commit-release
Commit the release changes.
` + "```" + `bash
git add -A
git commit -m "release: v{{version}}"
` + "```" + `
Needs: update-changelog
## Step: tag-release
Create and push the release tag.
` + "```" + `bash
git tag -a v{{version}} -m "Release v{{version}}"
git push origin main
git push origin v{{version}}
` + "```" + `
Needs: commit-release
## Step: verify-release
Verify the release is complete.
- Check that the tag exists on GitHub
- Verify CI/CD (if configured) completed successfully
- Test installation from the new tag:
` + "```" + `bash
go install github.com/steveyegge/gastown/cmd/gt@v{{version}}
gt version
` + "```" + `
Needs: tag-release
## Step: update-installations
Update local installations and restart daemons.
` + "```" + `bash
# Rebuild and install
go install ./cmd/gt
# Restart any running daemons
pkill -f "gt daemon" || true
gt daemon start
` + "```" + `
Needs: verify-release`,
}
}
// CrewSessionMolecule returns the crew-session molecule definition.
// This is a light harness for crew workers that enables autonomous overnight work.
// Key insight: if there's an attached mol, continue working without awaiting input.
func CrewSessionMolecule() BuiltinMolecule {
return BuiltinMolecule{
ID: "mol-crew-session",
Title: "Crew Session",
Description: `Light session harness for crew workers.
This molecule enables autonomous work on long-lived molecules. The key insight:
**If there's an attached mol, continue working without awaiting input.**
This transforms crew workers from interactive assistants to autonomous workers
that can churn through long molecules overnight.
## Step: orient
Load context and identify self.
` + "```" + `bash
gt prime # Load Gas Town context
` + "```" + `
Identify yourself:
- Read crew.md for role context
- Note your rig and crew member name
- Understand the session wisp model
## Step: handoff-read
Check inbox for predecessor handoff.
` + "```" + `bash
gt mail inbox
` + "```" + `
Look for 🤝 HANDOFF messages from your previous session.
If found:
- Read the handoff carefully
- Load predecessor's context and state
- Note where they left off
If no handoff found, this is a fresh start.
Needs: orient
## Step: check-attachment
Look for pinned work to continue.
` + "```" + `bash
bd list --pinned --assignee=$(gt whoami) --status=in_progress
gt mol status
` + "```" + `
**DECISION POINT:**
If attachment found:
- This is autonomous continuation mode
- Proceed directly to execute step
- NO human input needed
If no attachment found:
- This is interactive mode
- Await user instruction before proceeding
- Mark this step complete when user provides direction
Needs: handoff-read
## Step: execute
Work the attached molecule.
Find next ready step in the attached mol:
` + "```" + `bash
bd ready --parent=<work-mol-root>
bd update <step> --status=in_progress
` + "```" + `
Work until one of:
- All steps in mol completed
- Context approaching limit (>80%)
- Natural stopping point reached
- Blocked by external dependency
Track progress in the mol itself (close completed steps).
File discovered work as new issues.
Needs: check-attachment
## Step: cleanup
End session with proper handoff.
1. Sync all state:
` + "```" + `bash
git add -A && git commit -m "WIP: <summary>" || true
bd sync
` + "```" + `
Note: Branch stays local (commits saved in shared .git).
2. Write handoff to successor (yourself):
` + "```" + `bash
gt mail send <self-addr> -s "🤝 HANDOFF: <brief context>" -m "
## Progress
- Completed: <what was done>
- Next: <what to do next>
## State
- Current step: <step-id>
- Blockers: <any blockers>
## Notes
<any context successor needs>
"
` + "```" + `
3. Session ends. Successor will pick up from handoff.
Needs: execute`,
}
}
// PolecatSessionMolecule returns the polecat-session molecule definition.
// This is a one-shot session wisp that wraps polecat work.
// Unlike patrol wisps (which loop), this wisp terminates with the session.
func PolecatSessionMolecule() BuiltinMolecule {
return BuiltinMolecule{
ID: "mol-polecat-session",
Title: "Polecat Session",
Description: `One-shot session wisp for polecat workers.
This molecule wraps the polecat's work assignment. It handles:
1. Onboarding - read polecat.md, load context
2. Execution - run the attached work molecule
3. Cleanup - sync, burn, request shutdown
Unlike patrol wisps (which loop), this wisp terminates when work is done.
The attached work molecule is permanent and auditable.
## Step: orient
Read polecat.md protocol and initialize context.
` + "```" + `bash
gt prime # Load Gas Town context
gt mail inbox # Check for work assignment
` + "```" + `
Understand:
- Your identity (rig/polecat-name)
- The beads system
- Exit strategies (COMPLETED, BLOCKED, REFACTOR, ESCALATE)
- Handoff protocols
## Step: handoff-read
Check for predecessor session handoff.
If this polecat was respawned after a crash or context cycle:
- Check mail for 🤝 HANDOFF from previous session
- Load state from the attached work mol
- Resume from last completed step
` + "```" + `bash
gt mail inbox | grep HANDOFF
bd show <work-mol-id> # Check step completion state
` + "```" + `
Needs: orient
## Step: find-work
Locate attached work molecule.
` + "```" + `bash
gt mol status # Shows what's on your hook
` + "```" + `
The work mol should already be attached (done by spawn).
If not attached, check mail for work assignment.
Verify you have:
- A work mol ID
- Understanding of the work scope
- No blockers to starting
Needs: handoff-read
## Step: execute
Run the attached work molecule to completion.
For each ready step in the work mol:
` + "```" + `bash
bd ready --parent=<work-mol-root>
bd update <step> --status=in_progress
# ... do the work ...
bd close <step>
` + "```" + `
Continue until reaching the exit-decision step in the work mol.
All exit types (COMPLETED, BLOCKED, REFACTOR, ESCALATE) proceed to cleanup.
**Dynamic modifications allowed**:
- Add review or test steps if needed
- File discovered blockers as issues
- Request session refresh if context filling
Needs: find-work
## Step: cleanup
Finalize session and request termination.
1. Sync all state:
` + "```" + `bash
bd sync
` + "```" + `
Note: Branch stays local (commits saved in shared .git).
2. Update work mol based on exit type:
- COMPLETED: ` + "`bd close <work-mol-root>`" + `
- BLOCKED/REFACTOR/ESCALATE: ` + "`bd update <work-mol-root> --status=deferred`" + `
3. Burn this session wisp (no audit needed):
` + "```" + `bash
bd mol burn
` + "```" + `
4. Request shutdown from Witness:
` + "```" + `bash
gt mail send <rig>/witness -s "SHUTDOWN: <polecat-name>" -m "Session complete. Exit: <type>"
` + "```" + `
5. Wait for Witness to terminate session. Do not exit directly.
Needs: execute`,
}
}

View File

@@ -1,444 +0,0 @@
// Package beads provides a wrapper for the bd (beads) CLI.
package beads
// 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`,
}
}
// PolecatWorkMolecule returns the polecat-work molecule definition.
// This is the full polecat lifecycle from assignment to decommission.
// It's an operational molecule that enables crash recovery and context survival.
func PolecatWorkMolecule() BuiltinMolecule {
return BuiltinMolecule{
ID: "mol-polecat-work",
Title: "Polecat Work",
Description: `Full polecat lifecycle from assignment to decommission.
This molecule is your contract. Follow it to one of its defined exits.
The Witness doesn't care which exit you take, only that you exit properly.
**State Machine**: A polecat that crashes can restart, read its molecule state,
and continue from the last completed step. No work is lost.
**Non-Linear Exits**: If blocked at any step, skip to exit-decision directly.
## Step: load-context
Run gt prime and bd prime. Verify issue assignment.
Check inbox for any relevant messages.
Read the assigned issue and understand the requirements.
Identify any blockers or missing information.
**If blocked here**: Missing requirements? Unclear scope? Jump to exit-decision
with exit_type=escalate.
## Step: implement
Implement the solution. Follow codebase conventions.
File discovered work as new issues with bd create.
Make regular commits with clear messages.
Keep changes focused on the assigned issue.
**Dynamic modifications allowed**:
- Add extra review or test steps if needed
- File discovered blockers as issues
- Request session refresh if context is filling up
**If blocked here**: Dependency missing? Work too large? Jump to exit-decision.
Needs: load-context
## Step: self-review
Review your own changes. Look for:
- Bugs and edge cases
- Style issues
- Missing error handling
- Security concerns
Fix any issues found before proceeding.
Needs: implement
## Step: verify-tests
Run existing tests. Add new tests for new functionality.
Ensure adequate coverage.
` + "```" + `bash
go test ./...
` + "```" + `
Fix any test failures before proceeding.
Needs: implement
## Step: rebase-main
Rebase against main to incorporate any changes.
Resolve conflicts if needed.
` + "```" + `bash
git fetch origin main
git rebase origin/main
` + "```" + `
If there are conflicts, resolve them carefully and
continue the rebase. If conflicts are unresolvable, jump to exit-decision
with exit_type=escalate.
Needs: self-review, verify-tests
## Step: submit-merge
Submit to merge queue via beads.
**IMPORTANT**: Do NOT use gh pr create or GitHub PRs.
The Refinery processes merges via beads merge-request issues.
Branch stays local (refinery sees it via shared worktree).
1. Create a beads merge-request: bd create --type=merge-request --title="Merge: <summary>"
2. Signal ready: gt done
` + "```" + `bash
bd create --type=merge-request --title="Merge: <issue-summary>"
gt done # Signal work ready for merge queue
` + "```" + `
If there are CI failures, fix them before proceeding.
Needs: rebase-main
## Step: exit-decision
**CONVERGENCE POINT**: All exits pass through here.
Determine your exit type and take appropriate action:
### Exit Type: COMPLETED (normal)
Work finished successfully. Submit-merge done.
` + "```" + `bash
# Document completion
bd update <step-id> --status=closed
` + "```" + `
### Exit Type: BLOCKED
External dependency prevents progress.
` + "```" + `bash
# 1. File the blocker
bd create --type=task --title="Blocker: <description>" --priority=1
# 2. Link dependency
bd dep add <your-issue> <blocker-id>
# 3. Defer your issue
bd update <your-issue> --status=deferred
# 4. Notify witness
gt mail send <rig>/witness -s "Blocked: <issue-id>" -m "Blocked by <blocker-id>. Deferring."
` + "```" + `
### Exit Type: REFACTOR
Work is too large for one polecat session.
` + "```" + `bash
# Option A: Self-refactor
# 1. Break into sub-issues
bd create --type=task --title="Sub: part 1" --parent=<your-issue>
bd create --type=task --title="Sub: part 2" --parent=<your-issue>
# 2. Close what you completed, defer the rest
bd close <completed-sub-issues>
bd update <your-issue> --status=deferred
# Option B: Request refactor
gt mail send mayor/ -s "Refactor needed: <issue-id>" -m "
Issue too large. Completed X, remaining Y needs breakdown.
Recommend splitting into: ...
"
bd update <your-issue> --status=deferred
` + "```" + `
### Exit Type: ESCALATE
Need human judgment or authority.
` + "```" + `bash
# 1. Document what you know
bd comment <your-issue> "Escalating because: <reason>. Context: <details>"
# 2. Mail human
gt mail send --human -s "Escalation: <issue-id>" -m "
Need human decision on: <specific question>
Context: <what you've tried>
Options I see: <A, B, C>
"
# 3. Defer the issue
bd update <your-issue> --status=deferred
` + "```" + `
**Record your exit**: Update this step with your exit type and actions taken.
Needs: load-context
## Step: request-shutdown
Wait for termination.
All exit paths converge here. Your work is either:
- Merged (COMPLETED)
- Deferred with proper handoff (BLOCKED/REFACTOR/ESCALATE)
The polecat is now ready to be cleaned up.
Do not exit directly - wait for Witness to kill the session.
Needs: exit-decision`,
}
}
// ReadyWorkMolecule returns the ready-work molecule definition.
// This is an autonomous backlog processing patrol for crew workers.
// It's a vapor-phase molecule (wisp) that scans backlogs, selects work,
// and processes items until context is low.
func ReadyWorkMolecule() BuiltinMolecule {
return BuiltinMolecule{
ID: "mol-ready-work",
Title: "Ready Work",
Description: `Autonomous backlog processing patrol for crew workers.
**Phase**: vapor (wisp) - ephemeral patrol cycles
**Squash**: after each work item or context threshold
This molecule enables crew workers to autonomously process backlog items
using an ROI heuristic. It scans multiple backlogs, selects the highest-value
achievable item, executes it, and loops until context is low.
## Variables
| Variable | Default | Description |
|----------|---------|-------------|
| backlog_priority | (see scan order) | Override backlog scan order |
| context_threshold | 20 | Percentage at which to handoff |
| max_items | unlimited | Maximum items to process per session |
## Step: orient
Load context and check for interrupts.
` + "```" + `bash
gt prime # Load Gas Town context
` + "```" + `
Check for:
- Mail with overseer instructions: ` + "`gt mail inbox`" + `
- Predecessor handoff: Look for 🤝 HANDOFF messages
- Current context state
If overseer mail directs specific work, attach that instead of autonomous scan.
If handoff exists, resume from handoff state.
## Step: scan-backlogs
Survey all backlogs in priority order.
Scan order (highest to lowest priority):
1. ` + "`gh pr list --state open`" + ` - PRs need review/merge
2. ` + "`gh issue list --state open --label untriaged`" + ` - Untriaged issues
3. ` + "`bd ready`" + ` - Beads issues ready to work
4. ` + "`gh issue list --state open --label triaged`" + ` - Triaged GitHub issues
For each backlog, capture:
- Total count of items
- Top 3-5 candidates with brief descriptions
- Estimated size category (small/medium/large)
` + "```" + `bash
# Example scan
echo "=== PRs ===" && gh pr list --state open --limit 10
echo "=== Untriaged ===" && gh issue list --state open --label untriaged --limit 10
echo "=== Beads Ready ===" && bd ready
echo "=== Triaged ===" && gh issue list --state open --label triaged --limit 10
` + "```" + `
If all backlogs empty: exit patrol (nothing to do).
Needs: orient
## Step: select-work
Apply ROI heuristic to select best work item.
**ROI Formula**: Impact / Effort, constrained by remaining context
Evaluation criteria:
1. **Estimate size** - Tokens needed (small=1k, medium=5k, large=20k+)
2. **Check context capacity** - Can this item fit in remaining context?
3. **Weight by impact**:
- PRs: High (blocking others) → weight 3x
- Untriaged: Medium (needs triage) → weight 2x
- Beads ready: Medium (concrete work) → weight 2x
- Triaged GH: Lower (already processed) → weight 1x
4. **Adjust by priority** - P0/P1 issues get 2x multiplier
Selection algorithm:
1. Filter to items that fit in remaining context
2. Score each: (impact_weight × priority_multiplier) / estimated_effort
3. Select highest scoring item
4. If tie: prefer PRs > untriaged > beads > triaged
If no achievable items (all too large): goto handoff step.
Record selection rationale for audit.
Needs: scan-backlogs
## Step: execute-work
Work the selected item based on its type.
**For PRs (gh pr)**:
- Review the changes
- If good: approve and merge
- If issues: request changes with specific feedback
- Close or comment as appropriate
**For untriaged issues (gh issue, no label)**:
- Read and understand the issue
- Add appropriate labels (bug, feature, enhancement, etc.)
- Set priority if determinable
- Convert to beads if actionable: ` + "`bd create --title=\"...\" --type=...`" + `
- Close if duplicate/invalid/wontfix
**For beads ready (bd)**:
- Claim: ` + "`bd update <id> --status=in_progress`" + `
- Implement the fix/feature
- Test changes
- Commit and push
- Close: ` + "`bd close <id>`" + `
**For triaged GitHub issues**:
- Implement the fix/feature
- Create PR or push directly
- Link to issue: ` + "`Fixes #<num>`" + `
- Close issue when merged
Commit regularly. Push changes. Update issue state.
Needs: select-work
## Step: check-context
Assess context state after completing work item.
` + "```" + `bash
# Estimate context usage (if tool available)
gt context --usage
` + "```" + `
Decision matrix:
| Context Used | Action |
|--------------|--------|
| < 60% | Loop to scan-backlogs (continue working) |
| 60-80% | One more small item, then handoff |
| > 80% | Goto handoff immediately |
Also check:
- Items processed this session vs max_items limit
- Time elapsed (soft limit for long sessions)
- Any new high-priority mail that should interrupt
If continuing: return to scan-backlogs step.
Needs: execute-work
## Step: handoff
Prepare for session transition.
1. **Summarize work completed**:
- Items processed (count and types)
- PRs reviewed/merged
- Issues triaged
- Beads closed
- Any issues filed
2. **Note in-progress items**:
- If interrupted mid-work, record state
- File continuation issue if needed
3. **Send handoff mail**:
` + "```" + `bash
gt mail send <self-addr> -s "🤝 HANDOFF: Ready-work patrol" -m "
## Completed This Session
- <item counts and summaries>
## Backlog State
- PRs remaining: <count>
- Untriaged: <count>
- Beads ready: <count>
- Triaged: <count>
## Notes
<any context for successor>
"
` + "```" + `
4. **Squash wisp to digest**:
` + "```" + `bash
bd mol squash <wisp-id> --summary="Processed N items: X PRs, Y issues, Z beads"
` + "```" + `
5. **Exit for fresh session** - Successor picks up from handoff.
Needs: check-context`,
}
}

View File

@@ -10,7 +10,6 @@ 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"
@@ -173,13 +172,6 @@ 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")
}
}
}
@@ -253,10 +245,3 @@ 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
}