fix(doctor): check patrol formulas instead of placeholder beads (#715)

Changes patrol-molecules-exist check to verify that patrol formulas
are accessible via `bd formula list` instead of looking for placeholder
molecule beads created by `bd create --type=molecule`.

## Problem

The check was looking for molecule-type beads with titles "Deacon Patrol",
"Witness Patrol", and "Refinery Patrol". These placeholder beads serve no
functional purpose because:

1. Patrols actually use `bd mol wisp mol-deacon-patrol` which cooks
   formulas inline (on-the-fly)
2. The formulas already exist at the town level in .beads/formulas/
3. The placeholder beads are never referenced by any patrol code

## Solution

- Check for formula names (mol-deacon-patrol, mol-witness-patrol,
  mol-refinery-patrol) instead of bead titles
- Use `bd formula list` instead of `bd list --type=molecule`
- Remove Fix() method that created unnecessary placeholder beads
- Update messages to reflect that formulas should exist in search paths

## Impact

- Check now verifies what patrols actually need (formulas)
- Eliminates creation of unnecessary placeholder beads
- More accurate health check for patrol system

Co-authored-by: Roland Tritsch <roland@ailtir.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Roland Tritsch
2026-01-22 03:50:41 +00:00
committed by GitHub
parent 2514507a49
commit 2119841d57

View File

@@ -15,35 +15,35 @@ import (
"github.com/steveyegge/gastown/internal/templates" "github.com/steveyegge/gastown/internal/templates"
) )
// PatrolMoleculesExistCheck verifies that patrol molecules exist for each rig. // PatrolMoleculesExistCheck verifies that patrol formulas are accessible.
// Patrols use `bd mol wisp <formula-name>` to spawn workflows, so the formulas
// must exist in the formula search path (.beads/formulas/, ~/.beads/formulas/, or $GT_ROOT/.beads/formulas/).
type PatrolMoleculesExistCheck struct { type PatrolMoleculesExistCheck struct {
FixableCheck BaseCheck
missingMols map[string][]string // rig -> missing molecule titles missingFormulas map[string][]string // rig -> missing formula names
} }
// NewPatrolMoleculesExistCheck creates a new patrol molecules exist check. // NewPatrolMoleculesExistCheck creates a new patrol formulas exist check.
func NewPatrolMoleculesExistCheck() *PatrolMoleculesExistCheck { func NewPatrolMoleculesExistCheck() *PatrolMoleculesExistCheck {
return &PatrolMoleculesExistCheck{ return &PatrolMoleculesExistCheck{
FixableCheck: FixableCheck{ BaseCheck: BaseCheck{
BaseCheck: BaseCheck{ CheckName: "patrol-molecules-exist",
CheckName: "patrol-molecules-exist", CheckDescription: "Check if patrol formulas are accessible",
CheckDescription: "Check if patrol molecules exist for each rig", CheckCategory: CategoryPatrol,
CheckCategory: CategoryPatrol,
},
}, },
} }
} }
// patrolMolecules are the required patrol molecule titles. // patrolFormulas are the required patrol formula names.
var patrolMolecules = []string{ var patrolFormulas = []string{
"Deacon Patrol", "mol-deacon-patrol",
"Witness Patrol", "mol-witness-patrol",
"Refinery Patrol", "mol-refinery-patrol",
} }
// Run checks if patrol molecules exist. // Run checks if patrol formulas are accessible.
func (c *PatrolMoleculesExistCheck) Run(ctx *CheckContext) *CheckResult { func (c *PatrolMoleculesExistCheck) Run(ctx *CheckContext) *CheckResult {
c.missingMols = make(map[string][]string) c.missingFormulas = make(map[string][]string)
rigs, err := discoverRigs(ctx.TownRoot) rigs, err := discoverRigs(ctx.TownRoot)
if err != nil { if err != nil {
@@ -66,9 +66,9 @@ func (c *PatrolMoleculesExistCheck) Run(ctx *CheckContext) *CheckResult {
var details []string var details []string
for _, rigName := range rigs { for _, rigName := range rigs {
rigPath := filepath.Join(ctx.TownRoot, rigName) rigPath := filepath.Join(ctx.TownRoot, rigName)
missing := c.checkPatrolMolecules(rigPath) missing := c.checkPatrolFormulas(rigPath)
if len(missing) > 0 { if len(missing) > 0 {
c.missingMols[rigName] = missing c.missingFormulas[rigName] = missing
details = append(details, fmt.Sprintf("%s: missing %v", rigName, missing)) details = append(details, fmt.Sprintf("%s: missing %v", rigName, missing))
} }
} }
@@ -77,73 +77,42 @@ func (c *PatrolMoleculesExistCheck) Run(ctx *CheckContext) *CheckResult {
return &CheckResult{ return &CheckResult{
Name: c.Name(), Name: c.Name(),
Status: StatusWarning, Status: StatusWarning,
Message: fmt.Sprintf("%d rig(s) missing patrol molecules", len(c.missingMols)), Message: fmt.Sprintf("%d rig(s) missing patrol formulas", len(c.missingFormulas)),
Details: details, Details: details,
FixHint: "Run 'gt doctor --fix' to create missing patrol molecules", FixHint: "Formulas should exist in .beads/formulas/ at town or rig level, or in ~/.beads/formulas/",
} }
} }
return &CheckResult{ return &CheckResult{
Name: c.Name(), Name: c.Name(),
Status: StatusOK, Status: StatusOK,
Message: fmt.Sprintf("All %d rig(s) have patrol molecules", len(rigs)), Message: fmt.Sprintf("All %d rig(s) have patrol formulas accessible", len(rigs)),
} }
} }
// checkPatrolMolecules returns missing patrol molecule titles for a rig. // checkPatrolFormulas returns missing patrol formula names for a rig.
func (c *PatrolMoleculesExistCheck) checkPatrolMolecules(rigPath string) []string { func (c *PatrolMoleculesExistCheck) checkPatrolFormulas(rigPath string) []string {
// List molecules using bd // List formulas accessible from this rig using bd formula list
cmd := exec.Command("bd", "list", "--type=molecule") // This checks .beads/formulas/, ~/.beads/formulas/, and $GT_ROOT/.beads/formulas/
cmd := exec.Command("bd", "formula", "list")
cmd.Dir = rigPath cmd.Dir = rigPath
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
return patrolMolecules // Can't check, assume all missing // Can't check formulas, assume all missing
return patrolFormulas
} }
outputStr := string(output) outputStr := string(output)
var missing []string var missing []string
for _, mol := range patrolMolecules { for _, formulaName := range patrolFormulas {
if !strings.Contains(outputStr, mol) { // Formula list output includes the formula name without extension
missing = append(missing, mol) if !strings.Contains(outputStr, formulaName) {
missing = append(missing, formulaName)
} }
} }
return missing return missing
} }
// Fix creates missing patrol molecules.
func (c *PatrolMoleculesExistCheck) Fix(ctx *CheckContext) error {
for rigName, missing := range c.missingMols {
rigPath := filepath.Join(ctx.TownRoot, rigName)
for _, mol := range missing {
desc := getPatrolMoleculeDesc(mol)
cmd := exec.Command("bd", "create", //nolint:gosec // G204: args are constructed internally
"--type=molecule",
"--title="+mol,
"--description="+desc,
"--priority=2",
)
cmd.Dir = rigPath
if err := cmd.Run(); err != nil {
return fmt.Errorf("creating %s in %s: %w", mol, rigName, err)
}
}
}
return nil
}
func getPatrolMoleculeDesc(title string) string {
switch title {
case "Deacon Patrol":
return "Mayor's daemon patrol loop for handling callbacks, health checks, and cleanup."
case "Witness Patrol":
return "Per-rig worker monitor patrol loop with progressive nudging."
case "Refinery Patrol":
return "Merge queue processor patrol loop with verification gates."
default:
return "Patrol molecule"
}
}
// PatrolHooksWiredCheck verifies that hooks trigger patrol execution. // PatrolHooksWiredCheck verifies that hooks trigger patrol execution.
type PatrolHooksWiredCheck struct { type PatrolHooksWiredCheck struct {
FixableCheck FixableCheck