Merge remote-tracking branch 'origin/polecat/rictus'

This commit is contained in:
Steve Yegge
2025-12-23 01:15:51 -08:00
4 changed files with 601 additions and 0 deletions
+14
View File
@@ -23,6 +23,13 @@ var doctorCmd = &cobra.Command{
Doctor checks for common configuration issues, missing files, Doctor checks for common configuration issues, missing files,
and other problems that could affect workspace operation. and other problems that could affect workspace operation.
Patrol checks:
- patrol-molecules-exist Verify patrol molecules exist
- patrol-hooks-wired Verify daemon triggers patrols
- patrol-not-stuck Detect stale wisps (>1h)
- patrol-plugins-accessible Verify plugin directories
- patrol-roles-have-prompts Verify role prompts exist
Use --fix to attempt automatic fixes for issues that support it. Use --fix to attempt automatic fixes for issues that support it.
Use --rig to check a specific rig instead of the entire workspace.`, Use --rig to check a specific rig instead of the entire workspace.`,
RunE: runDoctor, RunE: runDoctor,
@@ -71,6 +78,13 @@ func runDoctor(cmd *cobra.Command, args []string) error {
d.Register(doctor.NewWispSizeCheck()) d.Register(doctor.NewWispSizeCheck())
d.Register(doctor.NewWispStaleCheck()) d.Register(doctor.NewWispStaleCheck())
// Patrol system checks
d.Register(doctor.NewPatrolMoleculesExistCheck())
d.Register(doctor.NewPatrolHooksWiredCheck())
d.Register(doctor.NewPatrolNotStuckCheck())
d.Register(doctor.NewPatrolPluginsAccessibleCheck())
d.Register(doctor.NewPatrolRolesHavePromptsCheck())
// Config architecture checks // Config architecture checks
d.Register(doctor.NewSettingsCheck()) d.Register(doctor.NewSettingsCheck())
d.Register(doctor.NewRuntimeGitignoreCheck()) d.Register(doctor.NewRuntimeGitignoreCheck())
+7
View File
@@ -45,12 +45,18 @@ This creates a rig container with:
- config.json Rig configuration - config.json Rig configuration
- .beads/ Rig-level issue tracking (initialized) - .beads/ Rig-level issue tracking (initialized)
- .beads-wisp/ Local wisp/molecule tracking (gitignored) - .beads-wisp/ Local wisp/molecule tracking (gitignored)
- plugins/ Rig-level plugin directory
- refinery/rig/ Canonical main clone - refinery/rig/ Canonical main clone
- mayor/rig/ Mayor's working clone - mayor/rig/ Mayor's working clone
- crew/main/ Default human workspace - crew/main/ Default human workspace
- witness/ Witness agent directory - witness/ Witness agent directory
- polecats/ Worker directory (empty) - polecats/ Worker directory (empty)
The command also:
- Seeds patrol molecules (Deacon, Witness, Refinery)
- Creates ~/gt/plugins/ (town-level) if it doesn't exist
- Creates <rig>/plugins/ (rig-level)
Example: Example:
gt rig add gastown https://github.com/steveyegge/gastown gt rig add gastown https://github.com/steveyegge/gastown
gt rig add my-project git@github.com:user/repo.git --prefix mp`, gt rig add my-project git@github.com:user/repo.git --prefix mp`,
@@ -201,6 +207,7 @@ func runRigAdd(cmd *cobra.Command, args []string) error {
fmt.Printf(" ├── config.json\n") fmt.Printf(" ├── config.json\n")
fmt.Printf(" ├── .beads/ (prefix: %s)\n", newRig.Config.Prefix) fmt.Printf(" ├── .beads/ (prefix: %s)\n", newRig.Config.Prefix)
fmt.Printf(" ├── .beads-wisp/ (local wisp/molecule tracking)\n") fmt.Printf(" ├── .beads-wisp/ (local wisp/molecule tracking)\n")
fmt.Printf(" ├── plugins/ (rig-level plugins)\n")
fmt.Printf(" ├── refinery/rig/ (canonical main)\n") fmt.Printf(" ├── refinery/rig/ (canonical main)\n")
fmt.Printf(" ├── mayor/rig/ (mayor's clone)\n") fmt.Printf(" ├── mayor/rig/ (mayor's clone)\n")
fmt.Printf(" ├── crew/%s/ (your workspace)\n", rigAddCrew) fmt.Printf(" ├── crew/%s/ (your workspace)\n", rigAddCrew)
+459
View File
@@ -0,0 +1,459 @@
package doctor
import (
"bufio"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
// PatrolMoleculesExistCheck verifies that patrol molecules exist for each rig.
type PatrolMoleculesExistCheck struct {
FixableCheck
missingMols map[string][]string // rig -> missing molecule titles
}
// NewPatrolMoleculesExistCheck creates a new patrol molecules exist check.
func NewPatrolMoleculesExistCheck() *PatrolMoleculesExistCheck {
return &PatrolMoleculesExistCheck{
FixableCheck: FixableCheck{
BaseCheck: BaseCheck{
CheckName: "patrol-molecules-exist",
CheckDescription: "Check if patrol molecules exist for each rig",
},
},
}
}
// patrolMolecules are the required patrol molecule titles.
var patrolMolecules = []string{
"Deacon Patrol",
"Witness Patrol",
"Refinery Patrol",
}
// Run checks if patrol molecules exist.
func (c *PatrolMoleculesExistCheck) Run(ctx *CheckContext) *CheckResult {
c.missingMols = make(map[string][]string)
rigs, err := discoverRigs(ctx.TownRoot)
if err != nil {
return &CheckResult{
Name: c.Name(),
Status: StatusError,
Message: "Failed to discover rigs",
Details: []string{err.Error()},
}
}
if len(rigs) == 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "No rigs configured",
}
}
var details []string
for _, rigName := range rigs {
rigPath := filepath.Join(ctx.TownRoot, rigName)
missing := c.checkPatrolMolecules(rigPath)
if len(missing) > 0 {
c.missingMols[rigName] = missing
details = append(details, fmt.Sprintf("%s: missing %v", rigName, missing))
}
}
if len(details) > 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusWarning,
Message: fmt.Sprintf("%d rig(s) missing patrol molecules", len(c.missingMols)),
Details: details,
FixHint: "Run 'gt doctor --fix' to create missing patrol molecules",
}
}
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: fmt.Sprintf("All %d rig(s) have patrol molecules", len(rigs)),
}
}
// checkPatrolMolecules returns missing patrol molecule titles for a rig.
func (c *PatrolMoleculesExistCheck) checkPatrolMolecules(rigPath string) []string {
// List molecules using bd
cmd := exec.Command("bd", "list", "--type=molecule")
cmd.Dir = rigPath
output, err := cmd.Output()
if err != nil {
return patrolMolecules // Can't check, assume all missing
}
outputStr := string(output)
var missing []string
for _, mol := range patrolMolecules {
if !strings.Contains(outputStr, mol) {
missing = append(missing, mol)
}
}
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",
"--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.
type PatrolHooksWiredCheck struct {
BaseCheck
}
// NewPatrolHooksWiredCheck creates a new patrol hooks wired check.
func NewPatrolHooksWiredCheck() *PatrolHooksWiredCheck {
return &PatrolHooksWiredCheck{
BaseCheck: BaseCheck{
CheckName: "patrol-hooks-wired",
CheckDescription: "Check if hooks trigger patrol execution",
},
}
}
// Run checks if patrol hooks are wired.
func (c *PatrolHooksWiredCheck) Run(ctx *CheckContext) *CheckResult {
// Check for daemon config which manages patrols
daemonConfigPath := filepath.Join(ctx.TownRoot, "mayor", "daemon.json")
if _, err := os.Stat(daemonConfigPath); os.IsNotExist(err) {
return &CheckResult{
Name: c.Name(),
Status: StatusWarning,
Message: "Daemon config not found",
FixHint: "Run 'gt daemon init' to configure daemon",
}
}
// Check daemon config for patrol configuration
data, err := os.ReadFile(daemonConfigPath)
if err != nil {
return &CheckResult{
Name: c.Name(),
Status: StatusError,
Message: "Failed to read daemon config",
Details: []string{err.Error()},
}
}
var config map[string]interface{}
if err := json.Unmarshal(data, &config); err != nil {
return &CheckResult{
Name: c.Name(),
Status: StatusWarning,
Message: "Invalid daemon config format",
Details: []string{err.Error()},
}
}
// Check for patrol entries
if patrols, ok := config["patrols"]; ok {
if patrolMap, ok := patrols.(map[string]interface{}); ok && len(patrolMap) > 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: fmt.Sprintf("Daemon configured with %d patrol(s)", len(patrolMap)),
}
}
}
// Check if heartbeat is enabled (triggers deacon patrol)
if heartbeat, ok := config["heartbeat"]; ok {
if hb, ok := heartbeat.(map[string]interface{}); ok {
if enabled, ok := hb["enabled"].(bool); ok && enabled {
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "Daemon heartbeat enabled (triggers patrols)",
}
}
}
}
return &CheckResult{
Name: c.Name(),
Status: StatusWarning,
Message: "Patrol hooks not configured in daemon",
FixHint: "Configure patrols in mayor/daemon.json or run 'gt daemon init'",
}
}
// PatrolNotStuckCheck detects wisps that have been in_progress too long.
type PatrolNotStuckCheck struct {
BaseCheck
stuckThreshold time.Duration
}
// NewPatrolNotStuckCheck creates a new patrol not stuck check.
func NewPatrolNotStuckCheck() *PatrolNotStuckCheck {
return &PatrolNotStuckCheck{
BaseCheck: BaseCheck{
CheckName: "patrol-not-stuck",
CheckDescription: "Check for stuck patrol wisps (>1h in_progress)",
},
stuckThreshold: 1 * time.Hour,
}
}
// Run checks for stuck patrol wisps.
func (c *PatrolNotStuckCheck) Run(ctx *CheckContext) *CheckResult {
rigs, err := discoverRigs(ctx.TownRoot)
if err != nil {
return &CheckResult{
Name: c.Name(),
Status: StatusError,
Message: "Failed to discover rigs",
Details: []string{err.Error()},
}
}
if len(rigs) == 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "No rigs configured",
}
}
var stuckWisps []string
for _, rigName := range rigs {
wispPath := filepath.Join(ctx.TownRoot, rigName, ".beads-wisp", "issues.jsonl")
stuck := c.checkStuckWisps(wispPath, rigName)
stuckWisps = append(stuckWisps, stuck...)
}
if len(stuckWisps) > 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusWarning,
Message: fmt.Sprintf("%d stuck patrol wisp(s) found (>1h)", len(stuckWisps)),
Details: stuckWisps,
FixHint: "Manual review required - wisps may need to be burned or sessions restarted",
}
}
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "No stuck patrol wisps found",
}
}
// checkStuckWisps returns descriptions of stuck wisps in a rig.
func (c *PatrolNotStuckCheck) checkStuckWisps(issuesPath string, rigName string) []string {
file, err := os.Open(issuesPath)
if err != nil {
return nil // No issues file
}
defer file.Close()
var stuck []string
cutoff := time.Now().Add(-c.stuckThreshold)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if line == "" {
continue
}
var issue struct {
ID string `json:"id"`
Title string `json:"title"`
Status string `json:"status"`
UpdatedAt time.Time `json:"updated_at"`
}
if err := json.Unmarshal([]byte(line), &issue); err != nil {
continue
}
// Check for in_progress issues older than threshold
if issue.Status == "in_progress" && !issue.UpdatedAt.IsZero() && issue.UpdatedAt.Before(cutoff) {
stuck = append(stuck, fmt.Sprintf("%s: %s (%s) - stale since %s",
rigName, issue.ID, issue.Title, issue.UpdatedAt.Format("2006-01-02 15:04")))
}
}
return stuck
}
// PatrolPluginsAccessibleCheck verifies plugin directories exist and are readable.
type PatrolPluginsAccessibleCheck struct {
FixableCheck
missingDirs []string
}
// NewPatrolPluginsAccessibleCheck creates a new patrol plugins accessible check.
func NewPatrolPluginsAccessibleCheck() *PatrolPluginsAccessibleCheck {
return &PatrolPluginsAccessibleCheck{
FixableCheck: FixableCheck{
BaseCheck: BaseCheck{
CheckName: "patrol-plugins-accessible",
CheckDescription: "Check if plugin directories exist and are readable",
},
},
}
}
// Run checks if plugin directories are accessible.
func (c *PatrolPluginsAccessibleCheck) Run(ctx *CheckContext) *CheckResult {
c.missingDirs = nil
// Check town-level plugins directory
townPluginsDir := filepath.Join(ctx.TownRoot, "plugins")
if _, err := os.Stat(townPluginsDir); os.IsNotExist(err) {
c.missingDirs = append(c.missingDirs, townPluginsDir)
}
// Check rig-level plugins directories
rigs, err := discoverRigs(ctx.TownRoot)
if err == nil {
for _, rigName := range rigs {
rigPluginsDir := filepath.Join(ctx.TownRoot, rigName, "plugins")
if _, err := os.Stat(rigPluginsDir); os.IsNotExist(err) {
c.missingDirs = append(c.missingDirs, rigPluginsDir)
}
}
}
if len(c.missingDirs) > 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusWarning,
Message: fmt.Sprintf("%d plugin directory(ies) missing", len(c.missingDirs)),
Details: c.missingDirs,
FixHint: "Run 'gt doctor --fix' to create missing directories",
}
}
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "All plugin directories accessible",
}
}
// Fix creates missing plugin directories.
func (c *PatrolPluginsAccessibleCheck) Fix(ctx *CheckContext) error {
for _, dir := range c.missingDirs {
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("creating %s: %w", dir, err)
}
}
return nil
}
// PatrolRolesHavePromptsCheck verifies that prompts/roles/*.md exist for each role.
type PatrolRolesHavePromptsCheck struct {
BaseCheck
}
// NewPatrolRolesHavePromptsCheck creates a new patrol roles have prompts check.
func NewPatrolRolesHavePromptsCheck() *PatrolRolesHavePromptsCheck {
return &PatrolRolesHavePromptsCheck{
BaseCheck: BaseCheck{
CheckName: "patrol-roles-have-prompts",
CheckDescription: "Check if prompts/roles/*.md exist for each patrol role",
},
}
}
// requiredRolePrompts are the required role prompt files.
var requiredRolePrompts = []string{
"deacon.md",
"witness.md",
"refinery.md",
}
// Run checks if role prompts exist.
func (c *PatrolRolesHavePromptsCheck) Run(ctx *CheckContext) *CheckResult {
rigs, err := discoverRigs(ctx.TownRoot)
if err != nil {
return &CheckResult{
Name: c.Name(),
Status: StatusError,
Message: "Failed to discover rigs",
Details: []string{err.Error()},
}
}
if len(rigs) == 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "No rigs configured",
}
}
var missingPrompts []string
for _, rigName := range rigs {
// Check in mayor's clone (canonical for the rig)
mayorRig := filepath.Join(ctx.TownRoot, rigName, "mayor", "rig")
promptsDir := filepath.Join(mayorRig, "prompts", "roles")
for _, roleFile := range requiredRolePrompts {
promptPath := filepath.Join(promptsDir, roleFile)
if _, err := os.Stat(promptPath); os.IsNotExist(err) {
missingPrompts = append(missingPrompts, fmt.Sprintf("%s: %s", rigName, roleFile))
}
}
}
if len(missingPrompts) > 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusWarning,
Message: fmt.Sprintf("%d role prompt(s) missing", len(missingPrompts)),
Details: missingPrompts,
FixHint: "Role prompts should be in the project repository under prompts/roles/",
}
}
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "All patrol role prompts found",
}
}
+121
View File
@@ -283,6 +283,18 @@ func (m *Manager) AddRig(opts AddRigOptions) (*Rig, error) {
return nil, fmt.Errorf("initializing wisp beads: %w", err) return nil, fmt.Errorf("initializing wisp beads: %w", err)
} }
// Seed patrol molecules for this rig
if err := m.seedPatrolMolecules(rigPath); err != nil {
// Non-fatal: log warning but continue
fmt.Printf(" Warning: Could not seed patrol molecules: %v\n", err)
}
// Create plugin directories
if err := m.createPluginDirectories(rigPath); err != nil {
// Non-fatal: log warning but continue
fmt.Printf(" Warning: Could not create plugin directories: %v\n", err)
}
// Register in town config // Register in town config
m.config.Rigs[opts.Name] = config.RigEntry{ m.config.Rigs[opts.Name] = config.RigEntry{
GitURL: opts.GitURL, GitURL: opts.GitURL,
@@ -491,3 +503,112 @@ func (m *Manager) createRoleCLAUDEmd(workspacePath string, role string, rigName
claudePath := filepath.Join(workspacePath, "CLAUDE.md") claudePath := filepath.Join(workspacePath, "CLAUDE.md")
return os.WriteFile(claudePath, []byte(content), 0644) return os.WriteFile(claudePath, []byte(content), 0644)
} }
// seedPatrolMolecules creates patrol molecule prototypes in the rig's beads database.
// These molecules define the work loops for Deacon, Witness, and Refinery roles.
func (m *Manager) seedPatrolMolecules(rigPath string) error {
// Use bd command to seed molecules (more reliable than internal API)
// The bd mol seed command creates built-in molecules if they don't exist
cmd := exec.Command("bd", "mol", "seed", "--patrol")
cmd.Dir = rigPath
if err := cmd.Run(); err != nil {
// Fallback: bd mol seed might not support --patrol yet
// Try creating them individually via bd create
return m.seedPatrolMoleculesManually(rigPath)
}
return nil
}
// seedPatrolMoleculesManually creates patrol molecules using bd create commands.
func (m *Manager) seedPatrolMoleculesManually(rigPath string) error {
// Patrol molecule definitions (subset of builtin_molecules.go for seeding)
patrolMols := []struct {
title string
desc string
}{
{
title: "Deacon Patrol",
desc: "Mayor's daemon patrol loop for handling callbacks, health checks, and cleanup.",
},
{
title: "Witness Patrol",
desc: "Per-rig worker monitor patrol loop with progressive nudging.",
},
{
title: "Refinery Patrol",
desc: "Merge queue processor patrol loop with verification gates.",
},
}
for _, mol := range patrolMols {
// Check if already exists by title
checkCmd := exec.Command("bd", "list", "--type=molecule", "--format=json")
checkCmd.Dir = rigPath
output, _ := checkCmd.Output()
if strings.Contains(string(output), mol.title) {
continue // Already exists
}
// Create the molecule
cmd := exec.Command("bd", "create",
"--type=molecule",
"--title="+mol.title,
"--description="+mol.desc,
"--priority=2",
)
cmd.Dir = rigPath
if err := cmd.Run(); err != nil {
// Non-fatal, continue with others
continue
}
}
return nil
}
// createPluginDirectories creates plugin directories at town and rig levels.
// - ~/gt/plugins/ (town-level, shared across all rigs)
// - <rig>/plugins/ (rig-level, rig-specific plugins)
func (m *Manager) createPluginDirectories(rigPath string) error {
// Town-level plugins directory
townPluginsDir := filepath.Join(m.townRoot, "plugins")
if err := os.MkdirAll(townPluginsDir, 0755); err != nil {
return fmt.Errorf("creating town plugins directory: %w", err)
}
// Create a README in town plugins if it doesn't exist
townReadme := filepath.Join(townPluginsDir, "README.md")
if _, err := os.Stat(townReadme); os.IsNotExist(err) {
content := `# Gas Town Plugins
This directory contains town-level plugins that run during Deacon patrol cycles.
## Plugin Structure
Each plugin is a directory containing:
- plugin.md - Plugin definition with YAML frontmatter
## Gate Types
- cooldown: Time since last run (e.g., 24h)
- cron: Schedule-based (e.g., "0 9 * * *")
- condition: Metric threshold
- event: Trigger-based (startup, heartbeat)
See docs/deacon-plugins.md for full documentation.
`
if writeErr := os.WriteFile(townReadme, []byte(content), 0644); writeErr != nil {
// Non-fatal
return nil
}
}
// Rig-level plugins directory
rigPluginsDir := filepath.Join(rigPath, "plugins")
if err := os.MkdirAll(rigPluginsDir, 0755); err != nil {
return fmt.Errorf("creating rig plugins directory: %w", err)
}
// Add plugins/ to rig .gitignore
gitignorePath := filepath.Join(rigPath, ".gitignore")
return m.ensureGitignoreEntry(gitignorePath, "plugins/")
}