From db353c247b2dcc7dd10611394c3a0b8cc9662f6c Mon Sep 17 00:00:00 2001 From: mayor Date: Fri, 9 Jan 2026 23:56:38 -0800 Subject: [PATCH] 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 --- .beads/PRIME.md | 43 ++++ internal/beads/beads.go | 84 ++++++++ internal/cmd/doctor.go | 3 + internal/cmd/handoff.go | 11 + internal/cmd/prime.go | 52 ++++- internal/constants/constants.go | 5 + internal/crew/manager.go | 8 + internal/doctor/priming_check.go | 344 +++++++++++++++++++++++++++++++ internal/polecat/manager.go | 15 +- internal/rig/manager.go | 9 + 10 files changed, 567 insertions(+), 7 deletions(-) create mode 100644 .beads/PRIME.md create mode 100644 internal/doctor/priming_check.go diff --git a/.beads/PRIME.md b/.beads/PRIME.md new file mode 100644 index 00000000..64b684da --- /dev/null +++ b/.beads/PRIME.md @@ -0,0 +1,43 @@ +# Gas Town Worker Context + +> **Context Recovery**: Run `gt prime` for full context after compaction or new session. + +## The Propulsion Principle (GUPP) + +**If you find work on your hook, YOU RUN IT.** + +No confirmation. No waiting. No announcements. The hook having work IS the assignment. +This is physics, not politeness. Gas Town is a steam engine - you are a piston. + +**Failure mode we're preventing:** +- Agent starts with work on hook +- Agent announces itself and waits for human to say "ok go" +- Human is AFK / trusting the engine to run +- Work sits idle. The whole system stalls. + +## Startup Protocol + +1. Check your hook: `gt mol status` +2. If work is hooked → EXECUTE (no announcement, no waiting) +3. If hook empty → Check mail: `gt mail inbox` +4. Still nothing? Wait for user instructions + +## Key Commands + +- `gt prime` - Get full role context (run after compaction) +- `gt mol status` - Check your hooked work +- `gt mail inbox` - Check for messages +- `bd ready` - Find available work (no blockers) +- `bd sync` - Sync beads changes + +## Session Close Protocol + +Before saying "done": +1. git status (check what changed) +2. git add (stage code changes) +3. bd sync (commit beads changes) +4. git commit -m "..." (commit code) +5. bd sync (commit any new beads changes) +6. git push (push to remote) + +**Work is not done until pushed.** diff --git a/internal/beads/beads.go b/internal/beads/beads.go index b000aebc..e1246f01 100644 --- a/internal/beads/beads.go +++ b/internal/beads/beads.go @@ -599,3 +599,87 @@ func (b *Beads) IsBeadsRepo() bool { info, err := os.Stat(beadsDir) return err == nil && info.IsDir() } + +// primeContent is the Gas Town PRIME.md content that provides essential context +// for crew workers. This is the fallback if the SessionStart hook fails. +const primeContent = `# Gas Town Worker Context + +> **Context Recovery**: Run ` + "`gt prime`" + ` for full context after compaction or new session. + +## The Propulsion Principle (GUPP) + +**If you find work on your hook, YOU RUN IT.** + +No confirmation. No waiting. No announcements. The hook having work IS the assignment. +This is physics, not politeness. Gas Town is a steam engine - you are a piston. + +**Failure mode we're preventing:** +- Agent starts with work on hook +- Agent announces itself and waits for human to say "ok go" +- Human is AFK / trusting the engine to run +- Work sits idle. The whole system stalls. + +## Startup Protocol + +1. Check your hook: ` + "`gt mol status`" + ` +2. If work is hooked → EXECUTE (no announcement, no waiting) +3. If hook empty → Check mail: ` + "`gt mail inbox`" + ` +4. Still nothing? Wait for user instructions + +## Key Commands + +- ` + "`gt prime`" + ` - Get full role context (run after compaction) +- ` + "`gt mol status`" + ` - Check your hooked work +- ` + "`gt mail inbox`" + ` - Check for messages +- ` + "`bd ready`" + ` - Find available work (no blockers) +- ` + "`bd sync`" + ` - Sync beads changes + +## Session Close Protocol + +Before saying "done": +1. git status (check what changed) +2. git add (stage code changes) +3. bd sync (commit beads changes) +4. git commit -m "..." (commit code) +5. bd sync (commit any new beads changes) +6. git push (push to remote) + +**Work is not done until pushed.** +` + +// ProvisionPrimeMD writes the Gas Town PRIME.md file to the specified beads directory. +// This provides essential Gas Town context (GUPP, startup protocol) as a fallback +// if the SessionStart hook fails. The PRIME.md is read by bd prime. +// +// The beadsDir should be the actual beads directory (after following any redirect). +// Returns nil if PRIME.md already exists (idempotent). +func ProvisionPrimeMD(beadsDir string) error { + primePath := filepath.Join(beadsDir, "PRIME.md") + + // Check if already exists - don't overwrite customizations + if _, err := os.Stat(primePath); err == nil { + return nil // Already exists, don't overwrite + } + + // Create .beads directory if it doesn't exist + if err := os.MkdirAll(beadsDir, 0755); err != nil { + return fmt.Errorf("creating beads dir: %w", err) + } + + // Write PRIME.md + if err := os.WriteFile(primePath, []byte(primeContent), 0644); err != nil { + return fmt.Errorf("writing PRIME.md: %w", err) + } + + return nil +} + +// ProvisionPrimeMDForWorktree provisions PRIME.md for a worktree by following its redirect. +// This is the main entry point for crew/polecat provisioning. +func ProvisionPrimeMDForWorktree(worktreePath string) error { + // Resolve the beads directory (follows redirect chain) + beadsDir := ResolveBeadsDir(worktreePath) + + // Provision PRIME.md in the target directory + return ProvisionPrimeMD(beadsDir) +} diff --git a/internal/cmd/doctor.go b/internal/cmd/doctor.go index 52c99baf..1ab77148 100644 --- a/internal/cmd/doctor.go +++ b/internal/cmd/doctor.go @@ -161,6 +161,9 @@ func runDoctor(cmd *cobra.Command, args []string) error { d.Register(doctor.NewLegacyGastownCheck()) d.Register(doctor.NewClaudeSettingsCheck()) + // Priming subsystem check + d.Register(doctor.NewPrimingCheck()) + // Crew workspace checks d.Register(doctor.NewCrewStateCheck()) d.Register(doctor.NewCrewWorktreeCheck()) diff --git a/internal/cmd/handoff.go b/internal/cmd/handoff.go index 233b1ac2..bde0c400 100644 --- a/internal/cmd/handoff.go +++ b/internal/cmd/handoff.go @@ -9,6 +9,7 @@ import ( "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/config" + "github.com/steveyegge/gastown/internal/constants" "github.com/steveyegge/gastown/internal/events" "github.com/steveyegge/gastown/internal/session" "github.com/steveyegge/gastown/internal/style" @@ -192,6 +193,16 @@ func runHandoff(cmd *cobra.Command, args []string) error { style.PrintWarning("could not clear history: %v", err) } + // Write handoff marker for successor detection (prevents handoff loop bug). + // The marker is cleared by gt prime after it outputs the warning. + // This tells the new session "you're post-handoff, don't re-run /handoff" + if cwd, err := os.Getwd(); err == nil { + runtimeDir := filepath.Join(cwd, constants.DirRuntime) + _ = os.MkdirAll(runtimeDir, 0755) + markerPath := filepath.Join(runtimeDir, constants.FileHandoffMarker) + _ = os.WriteFile(markerPath, []byte(currentSession), 0644) + } + // Use exec to respawn the pane - this kills us and restarts return t.RespawnPane(pane, restartCmd) } diff --git a/internal/cmd/prime.go b/internal/cmd/prime.go index 4ba9ed4c..f941d272 100644 --- a/internal/cmd/prime.go +++ b/internal/cmd/prime.go @@ -83,10 +83,6 @@ func init() { type RoleContext = RoleInfo func runPrime(cmd *cobra.Command, args []string) error { - if !state.IsEnabled() { - return nil - } - cwd, err := os.Getwd() if err != nil { return fmt.Errorf("getting current directory: %w", err) @@ -96,7 +92,17 @@ func runPrime(cmd *cobra.Command, args []string) error { if err != nil { return fmt.Errorf("finding workspace: %w", err) } + + // "Discover, Don't Track" principle: + // - If we're in a workspace, proceed - the workspace's existence IS the enable signal + // - If we're NOT in a workspace, check the global enabled state + // This ensures a missing/stale state file doesn't break workspace users if townRoot == "" { + // Not in a workspace - check global enabled state + // (This matters for hooks that might run from random directories) + if !state.IsEnabled() { + return nil // Silent exit - not in workspace and not enabled + } return fmt.Errorf("not in a Gas Town workspace") } @@ -117,6 +123,9 @@ func runPrime(cmd *cobra.Command, args []string) error { } } + // Check for handoff marker (prevents handoff loop bug) + checkHandoffMarker(cwd) + // Get role using env-aware detection roleInfo, err := GetRoleWithContext(cwd, townRoot) if err != nil { @@ -1632,3 +1641,38 @@ func readSessionFile(dir string) string { } return "" } + +// checkHandoffMarker checks for a handoff marker file and outputs a warning if found. +// This prevents the "handoff loop" bug where a new session sees /handoff in context +// and incorrectly runs it again. The marker tells the new session: "handoff is DONE, +// the /handoff you see in context was from YOUR PREDECESSOR, not a request for you." +func checkHandoffMarker(workDir string) { + markerPath := filepath.Join(workDir, constants.DirRuntime, constants.FileHandoffMarker) + data, err := os.ReadFile(markerPath) + if err != nil { + // No marker = not post-handoff, normal startup + return + } + + // Marker found - this is a post-handoff session + prevSession := strings.TrimSpace(string(data)) + + // Remove the marker FIRST so we don't warn twice + _ = os.Remove(markerPath) + + // Output prominent warning + fmt.Println() + fmt.Println(style.Bold.Render("╔══════════════════════════════════════════════════════════════════╗")) + fmt.Println(style.Bold.Render("║ ✅ HANDOFF COMPLETE - You are the NEW session ║")) + fmt.Println(style.Bold.Render("╚══════════════════════════════════════════════════════════════════╝")) + fmt.Println() + if prevSession != "" { + fmt.Printf("Your predecessor (%s) handed off to you.\n", prevSession) + } + fmt.Println() + fmt.Println(style.Bold.Render("⚠️ DO NOT run /handoff - that was your predecessor's action.")) + fmt.Println(" The /handoff you see in context is NOT a request for you.") + fmt.Println() + fmt.Println("Instead: Check your hook (`gt mol status`) and mail (`gt mail inbox`).") + fmt.Println() +} diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 597584d7..86c4e0b6 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -71,6 +71,11 @@ const ( // FileAccountsJSON is the accounts configuration file in mayor/. FileAccountsJSON = "accounts.json" + + // FileHandoffMarker is the marker file indicating a handoff just occurred. + // Written by gt handoff before respawn, cleared by gt prime after detection. + // This prevents the handoff loop bug where agents re-run /handoff from context. + FileHandoffMarker = "handoff_to_successor" ) // Beads configuration constants. diff --git a/internal/crew/manager.go b/internal/crew/manager.go index e2cb6a25..b2432938 100644 --- a/internal/crew/manager.go +++ b/internal/crew/manager.go @@ -173,6 +173,14 @@ func (m *Manager) Add(name string, createBranch bool) (*CrewWorker, error) { fmt.Printf("Warning: could not set up shared beads: %v\n", err) } + // Provision PRIME.md with Gas Town context for this worker. + // This is the fallback if SessionStart hook fails - ensures crew workers + // always have GUPP and essential Gas Town context. + if err := beads.ProvisionPrimeMDForWorktree(crewPath); err != nil { + // Non-fatal - crew can still work via hook, warn but don't fail + fmt.Printf("Warning: could not provision PRIME.md: %v\n", err) + } + // Copy overlay files from .runtime/overlay/ to crew root. // This allows services to have .env and other config files at their root. if err := rig.CopyOverlay(m.rig.Path, crewPath); err != nil { diff --git a/internal/doctor/priming_check.go b/internal/doctor/priming_check.go new file mode 100644 index 00000000..7d138524 --- /dev/null +++ b/internal/doctor/priming_check.go @@ -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 +} diff --git a/internal/polecat/manager.go b/internal/polecat/manager.go index 385352cc..e09e2864 100644 --- a/internal/polecat/manager.go +++ b/internal/polecat/manager.go @@ -281,6 +281,14 @@ func (m *Manager) AddWithOptions(name string, opts AddOptions) (*Polecat, error) fmt.Printf("Warning: could not set up shared beads: %v\n", err) } + // Provision PRIME.md with Gas Town context for this worker. + // This is the fallback if SessionStart hook fails - ensures polecats + // always have GUPP and essential Gas Town context. + if err := beads.ProvisionPrimeMDForWorktree(clonePath); err != nil { + // Non-fatal - polecat can still work via hook, warn but don't fail + fmt.Printf("Warning: could not provision PRIME.md: %v\n", err) + } + // Copy overlay files from .runtime/overlay/ to polecat root. // This allows services to have .env and other config files at their root. if err := rig.CopyOverlay(m.rig.Path, clonePath); err != nil { @@ -572,8 +580,9 @@ func (m *Manager) RepairWorktreeWithOptions(name string, force bool, opts AddOpt }, nil } -// ReconcilePool syncs pool state with existing polecat directories. -// This should be called to recover from crashes or stale state. +// ReconcilePool derives pool InUse state from existing polecat directories. +// This implements ZFC: InUse is discovered from filesystem, not tracked separately. +// Called before each allocation to ensure InUse reflects reality. func (m *Manager) ReconcilePool() { polecats, err := m.List() if err != nil { @@ -586,7 +595,7 @@ func (m *Manager) ReconcilePool() { } m.namePool.Reconcile(names) - _ = m.namePool.Save() // non-fatal: state file update + // Note: No Save() needed - InUse is transient state, only OverflowNext is persisted } // PoolStatus returns information about the name pool. diff --git a/internal/rig/manager.go b/internal/rig/manager.go index 31c7e378..8199f31d 100644 --- a/internal/rig/manager.go +++ b/internal/rig/manager.go @@ -386,6 +386,15 @@ func (m *Manager) AddRig(opts AddRigOptions) (*Rig, error) { } fmt.Printf(" ✓ Initialized beads (prefix: %s)\n", opts.BeadsPrefix) + // Provision PRIME.md with Gas Town context for all workers in this rig. + // This is the fallback if SessionStart hook fails - ensures ALL workers + // (crew, polecats, refinery, witness) have GUPP and essential Gas Town context. + // PRIME.md is read by bd prime and output to the agent. + rigBeadsPath := filepath.Join(rigPath, ".beads") + if err := beads.ProvisionPrimeMD(rigBeadsPath); err != nil { + fmt.Printf(" Warning: Could not provision PRIME.md: %v\n", err) + } + // Create refinery as worktree from bare repo on default branch. // Refinery needs to see polecat branches (shared .repo.git) and merges them. // Being on the default branch allows direct merge workflow.