Fixes CI lint failures by handling unchecked error returns and marking unused parameters with blank identifiers. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
373 lines
10 KiB
Go
373 lines
10 KiB
Go
package doctor
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/steveyegge/gastown/internal/beads"
|
|
"github.com/steveyegge/gastown/internal/constants"
|
|
)
|
|
|
|
// PrimingCheck verifies the priming subsystem is correctly configured.
|
|
// This ensures agents receive proper context on startup via the gt prime chain.
|
|
type PrimingCheck struct {
|
|
FixableCheck
|
|
issues []primingIssue
|
|
}
|
|
|
|
type primingIssue struct {
|
|
location string // e.g., "mayor", "gastown/crew/max", "gastown/witness"
|
|
issueType string // e.g., "no_hook", "no_prime", "large_claude_md", "missing_prime_md"
|
|
description string
|
|
fixable bool
|
|
}
|
|
|
|
// NewPrimingCheck creates a new priming subsystem check.
|
|
func NewPrimingCheck() *PrimingCheck {
|
|
return &PrimingCheck{
|
|
FixableCheck: FixableCheck{
|
|
BaseCheck: BaseCheck{
|
|
CheckName: "priming",
|
|
CheckDescription: "Verify priming subsystem is correctly configured",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Run checks the priming configuration across all agent locations.
|
|
func (c *PrimingCheck) Run(ctx *CheckContext) *CheckResult {
|
|
c.issues = nil
|
|
|
|
var details []string
|
|
|
|
// Check 1: gt binary in PATH
|
|
if err := exec.Command("which", "gt").Run(); err != nil {
|
|
c.issues = append(c.issues, primingIssue{
|
|
location: "system",
|
|
issueType: "gt_not_in_path",
|
|
description: "gt binary not found in PATH",
|
|
fixable: false,
|
|
})
|
|
details = append(details, "gt binary not found in PATH")
|
|
}
|
|
|
|
// Check 2: Mayor priming (town-level)
|
|
mayorIssues := c.checkAgentPriming(ctx.TownRoot, "mayor", "mayor")
|
|
for _, issue := range mayorIssues {
|
|
details = append(details, fmt.Sprintf("%s: %s", issue.location, issue.description))
|
|
}
|
|
c.issues = append(c.issues, mayorIssues...)
|
|
|
|
// Check 3: Deacon priming
|
|
deaconPath := filepath.Join(ctx.TownRoot, "deacon")
|
|
if dirExists(deaconPath) {
|
|
deaconIssues := c.checkAgentPriming(ctx.TownRoot, "deacon", "deacon")
|
|
for _, issue := range deaconIssues {
|
|
details = append(details, fmt.Sprintf("%s: %s", issue.location, issue.description))
|
|
}
|
|
c.issues = append(c.issues, deaconIssues...)
|
|
}
|
|
|
|
// Check 4: Rig-level agents (witness, refinery, crew, polecats)
|
|
rigIssues := c.checkRigPriming(ctx.TownRoot)
|
|
for _, issue := range rigIssues {
|
|
details = append(details, fmt.Sprintf("%s: %s", issue.location, issue.description))
|
|
}
|
|
c.issues = append(c.issues, rigIssues...)
|
|
|
|
if len(c.issues) == 0 {
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusOK,
|
|
Message: "Priming subsystem is correctly configured",
|
|
}
|
|
}
|
|
|
|
// Count fixable issues
|
|
fixableCount := 0
|
|
for _, issue := range c.issues {
|
|
if issue.fixable {
|
|
fixableCount++
|
|
}
|
|
}
|
|
|
|
fixHint := ""
|
|
if fixableCount > 0 {
|
|
fixHint = fmt.Sprintf("Run 'gt doctor --fix' to fix %d issue(s)", fixableCount)
|
|
}
|
|
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusError,
|
|
Message: fmt.Sprintf("Found %d priming issue(s)", len(c.issues)),
|
|
Details: details,
|
|
FixHint: fixHint,
|
|
}
|
|
}
|
|
|
|
// checkAgentPriming checks priming configuration for a specific agent.
|
|
func (c *PrimingCheck) checkAgentPriming(townRoot, agentDir, _ string) []primingIssue {
|
|
var issues []primingIssue
|
|
|
|
agentPath := filepath.Join(townRoot, agentDir)
|
|
settingsPath := filepath.Join(agentPath, ".claude", "settings.json")
|
|
|
|
// Check for SessionStart hook with gt prime
|
|
if fileExists(settingsPath) {
|
|
data, err := os.ReadFile(settingsPath)
|
|
if err == nil {
|
|
var settings map[string]any
|
|
if err := json.Unmarshal(data, &settings); err == nil {
|
|
if !c.hasGtPrimeHook(settings) {
|
|
issues = append(issues, primingIssue{
|
|
location: agentDir,
|
|
issueType: "no_prime_hook",
|
|
description: "SessionStart hook missing 'gt prime'",
|
|
fixable: false, // Requires template regeneration
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check CLAUDE.md is minimal (bootstrap pointer, not full context)
|
|
claudeMdPath := filepath.Join(agentPath, "CLAUDE.md")
|
|
if fileExists(claudeMdPath) {
|
|
lines := c.countLines(claudeMdPath)
|
|
if lines > 30 {
|
|
issues = append(issues, primingIssue{
|
|
location: agentDir,
|
|
issueType: "large_claude_md",
|
|
description: fmt.Sprintf("CLAUDE.md has %d lines (should be <30 for bootstrap pointer)", lines),
|
|
fixable: false, // Requires manual review
|
|
})
|
|
}
|
|
}
|
|
|
|
// Check AGENTS.md is minimal (bootstrap pointer, not full context)
|
|
agentsMdPath := filepath.Join(agentPath, "AGENTS.md")
|
|
if fileExists(agentsMdPath) {
|
|
lines := c.countLines(agentsMdPath)
|
|
if lines > 20 {
|
|
issues = append(issues, primingIssue{
|
|
location: agentDir,
|
|
issueType: "large_agents_md",
|
|
description: fmt.Sprintf("AGENTS.md has %d lines (should be <20 for bootstrap pointer)", lines),
|
|
fixable: false, // Full context should come from gt prime templates
|
|
})
|
|
}
|
|
}
|
|
|
|
return issues
|
|
}
|
|
|
|
// checkRigPriming checks priming for all rigs.
|
|
func (c *PrimingCheck) checkRigPriming(townRoot string) []primingIssue {
|
|
var issues []primingIssue
|
|
|
|
entries, err := os.ReadDir(townRoot)
|
|
if err != nil {
|
|
return issues
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() {
|
|
continue
|
|
}
|
|
|
|
rigName := entry.Name()
|
|
rigPath := filepath.Join(townRoot, rigName)
|
|
|
|
// Skip non-rig directories
|
|
if rigName == "mayor" || rigName == "deacon" || rigName == "daemon" ||
|
|
rigName == "docs" || rigName[0] == '.' {
|
|
continue
|
|
}
|
|
|
|
// Check if this is actually a rig (has .beads directory)
|
|
if !dirExists(filepath.Join(rigPath, ".beads")) {
|
|
continue
|
|
}
|
|
|
|
// Check PRIME.md exists at rig level
|
|
primeMdPath := filepath.Join(rigPath, ".beads", "PRIME.md")
|
|
if !fileExists(primeMdPath) {
|
|
issues = append(issues, primingIssue{
|
|
location: rigName,
|
|
issueType: "missing_prime_md",
|
|
description: "Missing .beads/PRIME.md (Gas Town context fallback)",
|
|
fixable: true,
|
|
})
|
|
}
|
|
|
|
// Check AGENTS.md is minimal at rig level (bootstrap pointer, not full context)
|
|
agentsMdPath := filepath.Join(rigPath, "AGENTS.md")
|
|
if fileExists(agentsMdPath) {
|
|
lines := c.countLines(agentsMdPath)
|
|
if lines > 20 {
|
|
issues = append(issues, primingIssue{
|
|
location: rigName,
|
|
issueType: "large_agents_md",
|
|
description: fmt.Sprintf("AGENTS.md has %d lines (should be <20 for bootstrap pointer)", lines),
|
|
fixable: false, // Requires manual review
|
|
})
|
|
}
|
|
}
|
|
|
|
// Check witness priming
|
|
witnessPath := filepath.Join(rigPath, "witness")
|
|
if dirExists(witnessPath) {
|
|
witnessIssues := c.checkAgentPriming(townRoot, filepath.Join(rigName, "witness"), "witness")
|
|
issues = append(issues, witnessIssues...)
|
|
}
|
|
|
|
// Check refinery priming
|
|
refineryPath := filepath.Join(rigPath, "refinery")
|
|
if dirExists(refineryPath) {
|
|
refineryIssues := c.checkAgentPriming(townRoot, filepath.Join(rigName, "refinery"), "refinery")
|
|
issues = append(issues, refineryIssues...)
|
|
}
|
|
|
|
// Check crew PRIME.md (shared settings, individual worktrees)
|
|
crewDir := filepath.Join(rigPath, "crew")
|
|
if dirExists(crewDir) {
|
|
crewEntries, _ := os.ReadDir(crewDir)
|
|
for _, crewEntry := range crewEntries {
|
|
if !crewEntry.IsDir() || crewEntry.Name() == ".claude" {
|
|
continue
|
|
}
|
|
crewPath := filepath.Join(crewDir, crewEntry.Name())
|
|
// Check if beads redirect is set up (crew should redirect to rig)
|
|
beadsDir := beads.ResolveBeadsDir(crewPath)
|
|
primeMdPath := filepath.Join(beadsDir, "PRIME.md")
|
|
if !fileExists(primeMdPath) {
|
|
issues = append(issues, primingIssue{
|
|
location: fmt.Sprintf("%s/crew/%s", rigName, crewEntry.Name()),
|
|
issueType: "missing_prime_md",
|
|
description: "Missing PRIME.md (Gas Town context fallback)",
|
|
fixable: true,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check polecat PRIME.md
|
|
polecatsDir := filepath.Join(rigPath, "polecats")
|
|
if dirExists(polecatsDir) {
|
|
pcEntries, _ := os.ReadDir(polecatsDir)
|
|
for _, pcEntry := range pcEntries {
|
|
if !pcEntry.IsDir() || pcEntry.Name() == ".claude" {
|
|
continue
|
|
}
|
|
polecatPath := filepath.Join(polecatsDir, pcEntry.Name())
|
|
// Check if beads redirect is set up
|
|
beadsDir := beads.ResolveBeadsDir(polecatPath)
|
|
primeMdPath := filepath.Join(beadsDir, "PRIME.md")
|
|
if !fileExists(primeMdPath) {
|
|
issues = append(issues, primingIssue{
|
|
location: fmt.Sprintf("%s/polecats/%s", rigName, pcEntry.Name()),
|
|
issueType: "missing_prime_md",
|
|
description: "Missing PRIME.md (Gas Town context fallback)",
|
|
fixable: true,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return issues
|
|
}
|
|
|
|
// hasGtPrimeHook checks if settings have a SessionStart hook that calls gt prime.
|
|
func (c *PrimingCheck) hasGtPrimeHook(settings map[string]any) bool {
|
|
hooks, ok := settings["hooks"].(map[string]any)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
hookList, ok := hooks["SessionStart"].([]any)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
for _, hook := range hookList {
|
|
hookMap, ok := hook.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
innerHooks, ok := hookMap["hooks"].([]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, inner := range innerHooks {
|
|
innerMap, ok := inner.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
cmd, ok := innerMap["command"].(string)
|
|
if ok && strings.Contains(cmd, "gt prime") {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// countLines counts the number of lines in a file.
|
|
func (c *PrimingCheck) countLines(path string) int {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
count := 0
|
|
for scanner.Scan() {
|
|
count++
|
|
}
|
|
return count
|
|
}
|
|
|
|
// Fix attempts to fix priming issues.
|
|
func (c *PrimingCheck) Fix(ctx *CheckContext) error {
|
|
var errors []string
|
|
|
|
for _, issue := range c.issues {
|
|
if !issue.fixable {
|
|
continue
|
|
}
|
|
|
|
switch issue.issueType {
|
|
case "missing_prime_md":
|
|
// Provision PRIME.md at the appropriate location
|
|
var targetPath string
|
|
|
|
// Parse the location to determine where to provision
|
|
if strings.Contains(issue.location, "/crew/") || strings.Contains(issue.location, "/polecats/") {
|
|
// Worker location - use beads.ProvisionPrimeMDForWorktree
|
|
worktreePath := filepath.Join(ctx.TownRoot, issue.location)
|
|
if err := beads.ProvisionPrimeMDForWorktree(worktreePath); err != nil {
|
|
errors = append(errors, fmt.Sprintf("%s: %v", issue.location, err))
|
|
}
|
|
} else {
|
|
// Rig location - provision directly
|
|
targetPath = filepath.Join(ctx.TownRoot, issue.location, constants.DirBeads)
|
|
if err := beads.ProvisionPrimeMD(targetPath); err != nil {
|
|
errors = append(errors, fmt.Sprintf("%s: %v", issue.location, err))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(errors) > 0 {
|
|
return fmt.Errorf("%s", strings.Join(errors, "; "))
|
|
}
|
|
return nil
|
|
}
|