feat: gt doctor checks agent beads exist (gt-bq4wo)
Adds agent-beads-exist check to gt doctor: - Verifies witness/refinery beads exist for each rig - Verifies deacon/mayor beads exist in first rig - Supports --fix to create missing agent beads - Skips non-gt prefixes (beads library limitation) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
272
internal/doctor/agent_beads_check.go
Normal file
272
internal/doctor/agent_beads_check.go
Normal file
@@ -0,0 +1,272 @@
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
)
|
||||
|
||||
// AgentBeadsCheck verifies that agent beads exist for all agents.
|
||||
// This includes:
|
||||
// - Global agents (deacon, mayor) - stored in first rig's beads
|
||||
// - Per-rig agents (witness, refinery) - stored in each rig's beads
|
||||
//
|
||||
// Agent beads are created by gt rig add (see gt-h3hak, gt-pinkq).
|
||||
//
|
||||
// NOTE: Currently, the beads library validates that agent IDs must start
|
||||
// with 'gt-'. Rigs with different prefixes (like 'bd-') cannot have agent
|
||||
// beads created until that validation is fixed in the beads repo.
|
||||
type AgentBeadsCheck struct {
|
||||
FixableCheck
|
||||
}
|
||||
|
||||
// NewAgentBeadsCheck creates a new agent beads check.
|
||||
func NewAgentBeadsCheck() *AgentBeadsCheck {
|
||||
return &AgentBeadsCheck{
|
||||
FixableCheck: FixableCheck{
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "agent-beads-exist",
|
||||
CheckDescription: "Verify agent beads exist for all agents",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Run checks if agent beads exist for all expected agents.
|
||||
func (c *AgentBeadsCheck) Run(ctx *CheckContext) *CheckResult {
|
||||
// Load routes to get prefixes (routes.jsonl is source of truth for prefixes)
|
||||
beadsDir := filepath.Join(ctx.TownRoot, ".beads")
|
||||
routes, err := beads.LoadRoutes(beadsDir)
|
||||
if err != nil {
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusWarning,
|
||||
Message: "Could not load routes.jsonl",
|
||||
}
|
||||
}
|
||||
|
||||
// Build prefix -> rigName map from routes
|
||||
// Routes have format: prefix "gt-" -> path "gastown/mayor/rig"
|
||||
prefixToRig := make(map[string]string) // prefix (without hyphen) -> rigName
|
||||
for _, r := range routes {
|
||||
// Extract rig name from path like "gastown/mayor/rig"
|
||||
parts := strings.Split(r.Path, "/")
|
||||
if len(parts) >= 1 && parts[0] != "." {
|
||||
rigName := parts[0]
|
||||
prefix := strings.TrimSuffix(r.Prefix, "-")
|
||||
prefixToRig[prefix] = rigName
|
||||
}
|
||||
}
|
||||
|
||||
if len(prefixToRig) == 0 {
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusOK,
|
||||
Message: "No rigs with beads routes (agent beads created on rig add)",
|
||||
}
|
||||
}
|
||||
|
||||
var missing []string
|
||||
var checked int
|
||||
|
||||
// Find the first rig (by name, alphabetically) for global agents
|
||||
// Only consider gt-prefix rigs since other prefixes can't have agent beads yet
|
||||
var firstRigName string
|
||||
var firstPrefix string
|
||||
for prefix, rigName := range prefixToRig {
|
||||
if prefix != "gt" {
|
||||
continue // Skip non-gt prefixes for first rig selection
|
||||
}
|
||||
if firstRigName == "" || rigName < firstRigName {
|
||||
firstRigName = rigName
|
||||
firstPrefix = prefix
|
||||
}
|
||||
}
|
||||
|
||||
// Check each rig for its agents
|
||||
var skipped []string
|
||||
for prefix, rigName := range prefixToRig {
|
||||
// Skip non-gt prefixes - beads library currently requires gt- prefix for agents
|
||||
// TODO: Remove this once beads validation is fixed to accept any prefix
|
||||
if prefix != "gt" {
|
||||
skipped = append(skipped, fmt.Sprintf("%s (%s-*)", rigName, prefix))
|
||||
continue
|
||||
}
|
||||
|
||||
// Get beads client for this rig
|
||||
rigBeadsPath := filepath.Join(ctx.TownRoot, rigName, "mayor", "rig")
|
||||
bd := beads.New(rigBeadsPath)
|
||||
|
||||
// Check rig-specific agents
|
||||
witnessID := fmt.Sprintf("%s-witness-%s", prefix, rigName)
|
||||
refineryID := fmt.Sprintf("%s-refinery-%s", prefix, rigName)
|
||||
|
||||
if _, err := bd.Show(witnessID); err != nil {
|
||||
missing = append(missing, witnessID)
|
||||
}
|
||||
checked++
|
||||
|
||||
if _, err := bd.Show(refineryID); err != nil {
|
||||
missing = append(missing, refineryID)
|
||||
}
|
||||
checked++
|
||||
|
||||
// Check global agents in first rig
|
||||
if rigName == firstRigName {
|
||||
deaconID := firstPrefix + "-deacon"
|
||||
mayorID := firstPrefix + "-mayor"
|
||||
|
||||
if _, err := bd.Show(deaconID); err != nil {
|
||||
missing = append(missing, deaconID)
|
||||
}
|
||||
checked++
|
||||
|
||||
if _, err := bd.Show(mayorID); err != nil {
|
||||
missing = append(missing, mayorID)
|
||||
}
|
||||
checked++
|
||||
}
|
||||
}
|
||||
|
||||
if len(missing) == 0 {
|
||||
msg := fmt.Sprintf("All %d agent beads exist", checked)
|
||||
var details []string
|
||||
if len(skipped) > 0 {
|
||||
details = append(details, fmt.Sprintf("Skipped %d rig(s) with non-gt prefix (beads library limitation): %s",
|
||||
len(skipped), strings.Join(skipped, ", ")))
|
||||
}
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusOK,
|
||||
Message: msg,
|
||||
Details: details,
|
||||
}
|
||||
}
|
||||
|
||||
details := missing
|
||||
if len(skipped) > 0 {
|
||||
details = append(details, fmt.Sprintf("Skipped %d rig(s) with non-gt prefix: %s",
|
||||
len(skipped), strings.Join(skipped, ", ")))
|
||||
}
|
||||
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusError,
|
||||
Message: fmt.Sprintf("%d agent bead(s) missing", len(missing)),
|
||||
Details: details,
|
||||
FixHint: "Run 'gt doctor --fix' to create missing agent beads",
|
||||
}
|
||||
}
|
||||
|
||||
// Fix creates missing agent beads.
|
||||
func (c *AgentBeadsCheck) Fix(ctx *CheckContext) error {
|
||||
// Load routes to get prefixes (same as Run)
|
||||
beadsDir := filepath.Join(ctx.TownRoot, ".beads")
|
||||
routes, err := beads.LoadRoutes(beadsDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading routes.jsonl: %w", err)
|
||||
}
|
||||
|
||||
// Build prefix -> rigName map from routes
|
||||
prefixToRig := make(map[string]string)
|
||||
for _, r := range routes {
|
||||
parts := strings.Split(r.Path, "/")
|
||||
if len(parts) >= 1 && parts[0] != "." {
|
||||
rigName := parts[0]
|
||||
prefix := strings.TrimSuffix(r.Prefix, "-")
|
||||
prefixToRig[prefix] = rigName
|
||||
}
|
||||
}
|
||||
|
||||
if len(prefixToRig) == 0 {
|
||||
return nil // Nothing to fix
|
||||
}
|
||||
|
||||
// Find the first rig for global agents (only gt-prefix rigs)
|
||||
var firstRigName string
|
||||
var firstPrefix string
|
||||
for prefix, rigName := range prefixToRig {
|
||||
if prefix != "gt" {
|
||||
continue
|
||||
}
|
||||
if firstRigName == "" || rigName < firstRigName {
|
||||
firstRigName = rigName
|
||||
firstPrefix = prefix
|
||||
}
|
||||
}
|
||||
|
||||
// Create missing agents for each rig
|
||||
for prefix, rigName := range prefixToRig {
|
||||
// Skip non-gt prefixes - beads library currently requires gt- prefix for agents
|
||||
if prefix != "gt" {
|
||||
continue
|
||||
}
|
||||
|
||||
rigBeadsPath := filepath.Join(ctx.TownRoot, rigName, "mayor", "rig")
|
||||
bd := beads.New(rigBeadsPath)
|
||||
|
||||
// Create rig-specific agents if missing
|
||||
witnessID := fmt.Sprintf("%s-witness-%s", prefix, rigName)
|
||||
if _, err := bd.Show(witnessID); err != nil {
|
||||
fields := &beads.AgentFields{
|
||||
RoleType: "witness",
|
||||
Rig: rigName,
|
||||
AgentState: "idle",
|
||||
RoleBead: witnessID + "-role",
|
||||
}
|
||||
desc := fmt.Sprintf("Witness for %s - monitors polecat health and progress.", rigName)
|
||||
if _, err := bd.CreateAgentBead(witnessID, desc, fields); err != nil {
|
||||
return fmt.Errorf("creating %s: %w", witnessID, err)
|
||||
}
|
||||
}
|
||||
|
||||
refineryID := fmt.Sprintf("%s-refinery-%s", prefix, rigName)
|
||||
if _, err := bd.Show(refineryID); err != nil {
|
||||
fields := &beads.AgentFields{
|
||||
RoleType: "refinery",
|
||||
Rig: rigName,
|
||||
AgentState: "idle",
|
||||
RoleBead: refineryID + "-role",
|
||||
}
|
||||
desc := fmt.Sprintf("Refinery for %s - processes merge queue.", rigName)
|
||||
if _, err := bd.CreateAgentBead(refineryID, desc, fields); err != nil {
|
||||
return fmt.Errorf("creating %s: %w", refineryID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create global agents in first rig if missing
|
||||
if rigName == firstRigName {
|
||||
deaconID := firstPrefix + "-deacon"
|
||||
if _, err := bd.Show(deaconID); err != nil {
|
||||
fields := &beads.AgentFields{
|
||||
RoleType: "deacon",
|
||||
Rig: "",
|
||||
AgentState: "idle",
|
||||
RoleBead: deaconID + "-role",
|
||||
}
|
||||
desc := "Deacon (daemon beacon) - receives mechanical heartbeats, runs town plugins and monitoring."
|
||||
if _, err := bd.CreateAgentBead(deaconID, desc, fields); err != nil {
|
||||
return fmt.Errorf("creating %s: %w", deaconID, err)
|
||||
}
|
||||
}
|
||||
|
||||
mayorID := firstPrefix + "-mayor"
|
||||
if _, err := bd.Show(mayorID); err != nil {
|
||||
fields := &beads.AgentFields{
|
||||
RoleType: "mayor",
|
||||
Rig: "",
|
||||
AgentState: "idle",
|
||||
RoleBead: mayorID + "-role",
|
||||
}
|
||||
desc := "Mayor - global coordinator, handles cross-rig communication and escalations."
|
||||
if _, err := bd.CreateAgentBead(mayorID, desc, fields); err != nil {
|
||||
return fmt.Errorf("creating %s: %w", mayorID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user