Merge remote-tracking branch 'origin/polecat/rictus'
This commit is contained in:
@@ -23,6 +23,13 @@ var doctorCmd = &cobra.Command{
|
||||
Doctor checks for common configuration issues, missing files,
|
||||
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 --rig to check a specific rig instead of the entire workspace.`,
|
||||
RunE: runDoctor,
|
||||
@@ -71,6 +78,13 @@ func runDoctor(cmd *cobra.Command, args []string) error {
|
||||
d.Register(doctor.NewWispSizeCheck())
|
||||
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
|
||||
d.Register(doctor.NewSettingsCheck())
|
||||
d.Register(doctor.NewRuntimeGitignoreCheck())
|
||||
|
||||
@@ -45,12 +45,18 @@ This creates a rig container with:
|
||||
- config.json Rig configuration
|
||||
- .beads/ Rig-level issue tracking (initialized)
|
||||
- .beads-wisp/ Local wisp/molecule tracking (gitignored)
|
||||
- plugins/ Rig-level plugin directory
|
||||
- refinery/rig/ Canonical main clone
|
||||
- mayor/rig/ Mayor's working clone
|
||||
- crew/main/ Default human workspace
|
||||
- witness/ Witness agent directory
|
||||
- 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:
|
||||
gt rig add gastown https://github.com/steveyegge/gastown
|
||||
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(" ├── .beads/ (prefix: %s)\n", newRig.Config.Prefix)
|
||||
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(" ├── mayor/rig/ (mayor's clone)\n")
|
||||
fmt.Printf(" ├── crew/%s/ (your workspace)\n", rigAddCrew)
|
||||
|
||||
459
internal/doctor/patrol_check.go
Normal file
459
internal/doctor/patrol_check.go
Normal 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",
|
||||
}
|
||||
}
|
||||
@@ -283,6 +283,18 @@ func (m *Manager) AddRig(opts AddRigOptions) (*Rig, error) {
|
||||
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
|
||||
m.config.Rigs[opts.Name] = config.RigEntry{
|
||||
GitURL: opts.GitURL,
|
||||
@@ -491,3 +503,112 @@ func (m *Manager) createRoleCLAUDEmd(workspacePath string, role string, rigName
|
||||
claudePath := filepath.Join(workspacePath, "CLAUDE.md")
|
||||
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/")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user