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:
@@ -2,6 +2,9 @@
|
|||||||
package beads
|
package beads
|
||||||
|
|
||||||
// BuiltinMolecule defines a built-in molecule template.
|
// 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 {
|
type BuiltinMolecule struct {
|
||||||
ID string // Well-known ID (e.g., "mol-engineer-in-box")
|
ID string // Well-known ID (e.g., "mol-engineer-in-box")
|
||||||
Title string
|
Title string
|
||||||
@@ -9,70 +12,17 @@ type BuiltinMolecule struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BuiltinMolecules returns all built-in molecule definitions.
|
// BuiltinMolecules returns all built-in molecule definitions.
|
||||||
// Molecules are defined in separate files by category:
|
// Deprecated: Molecules are now defined as formula files (.beads/formulas/*.formula.json)
|
||||||
// - molecules_work.go: EngineerInBox, QuickFix, Research, PolecatWork, ReadyWork
|
// and cooked into proto beads via `bd cook`. This function returns an empty slice.
|
||||||
// - molecules_patrol.go: DeaconPatrol, WitnessPatrol, RefineryPatrol
|
// Use `bd cook` to create proto beads from formulas instead.
|
||||||
// - molecules_session.go: CrewSession, PolecatSession, Bootstrap, VersionBump, InstallGoBinary
|
|
||||||
func BuiltinMolecules() []BuiltinMolecule {
|
func BuiltinMolecules() []BuiltinMolecule {
|
||||||
return []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(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SeedBuiltinMolecules creates all built-in molecules in the beads database.
|
// SeedBuiltinMolecules is deprecated and does nothing.
|
||||||
// It skips molecules that already exist (by title match).
|
// Molecules are now created by cooking formula files with `bd cook`.
|
||||||
// Returns the number of molecules created.
|
// This function remains for backward compatibility with existing installations.
|
||||||
func (b *Beads) SeedBuiltinMolecules() (int, error) {
|
func (b *Beads) SeedBuiltinMolecules() (int, error) {
|
||||||
molecules := BuiltinMolecules()
|
// No-op: formulas are cooked on-demand, not seeded at install time
|
||||||
created := 0
|
return 0, nil
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,6 @@ 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"
|
||||||
@@ -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)
|
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,10 +245,3 @@ 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