feat: implement priming subsystem improvements
Phase 1 of dynamic priming subsystem: 1. PRIME.md provisioning for all workers (hq-5z76w, hq-ukjrr Part A) - Added ProvisionPrimeMD to beads package with Gas Town context template - Provision at rig level in AddRig() so all workers inherit it - Added fallback provisioning in crew and polecat managers - Created PRIME.md for existing rigs 2. Post-handoff detection to prevent handoff loop bug (hq-ukjrr Part B) - Added FileHandoffMarker constant (.runtime/handoff_to_successor) - gt handoff writes marker before respawn - gt prime detects marker and outputs "HANDOFF COMPLETE" warning - Marker cleared after detection to prevent duplicate warnings 3. Priming health checks for gt doctor (hq-5scnt) - New priming_check.go validates priming subsystem configuration - Checks: SessionStart hook, gt prime command, PRIME.md presence - Warns if CLAUDE.md is too large (should be bootstrap pointer) - Fixable: provisions missing PRIME.md files This ensures crew workers get Gas Town context (GUPP, hooks, propulsion) even if the gt prime hook fails, via bd prime fallback. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,344 @@
|
||||
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, agentType 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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
Reference in New Issue
Block a user