Files
gastown/internal/doctor/priming_check.go
gastown/crew/dennis b9e8be4352 fix(lint): resolve errcheck and unparam violations
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>
2026-01-11 18:06:09 -08:00

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
}