From acd2565a5bf678071e2bdd323430f8807c77ba18 Mon Sep 17 00:00:00 2001 From: splendid Date: Sat, 3 Jan 2026 21:20:11 -0800 Subject: [PATCH] fix: remove vestigial state.json files from agent directories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent directories (witness/, refinery/, mayor/) contained state.json files with last_active timestamps that were never updated, making them stale and misleading. This change removes: - initAgentStates function that created vestigial state.json files - AgentState type and related Load/Save functions from config package - MayorStateValidCheck from doctor checks - requesting_* lifecycle verification (dead code - flags were never set) - FileStateJSON constant and MayorStatePath function Kept intact: - daemon/state.json (actively used for daemon runtime state) - crew//state.json (operational CrewWorker metadata) - Agent state tracking via beads (the ZFC-compliant approach) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/cmd/doctor.go | 1 - internal/cmd/install.go | 11 -- internal/cmd/install_integration_test.go | 17 -- internal/cmd/rig_integration_test.go | 21 ++- internal/config/loader.go | 52 ------ internal/config/loader_test.go | 36 ---- internal/config/types.go | 8 - internal/constants/constants.go | 8 - internal/daemon/lifecycle.go | 120 ------------- internal/doctor/lifecycle_check.go | 206 +---------------------- internal/doctor/workspace_check.go | 73 -------- internal/rig/manager.go | 38 +---- internal/rig/manager_test.go | 8 +- 13 files changed, 23 insertions(+), 576 deletions(-) diff --git a/internal/cmd/doctor.go b/internal/cmd/doctor.go index b15f5a5c..74cf9246 100644 --- a/internal/cmd/doctor.go +++ b/internal/cmd/doctor.go @@ -30,7 +30,6 @@ Workspace checks: - rigs-registry-exists Check mayor/rigs.json exists (fixable) - rigs-registry-valid Check registered rigs exist (fixable) - mayor-exists Check mayor/ directory structure - - mayor-state-valid Check mayor/state.json is valid (fixable) Infrastructure checks: - daemon Check if daemon is running (fixable) diff --git a/internal/cmd/install.go b/internal/cmd/install.go index d4c63985..5e5ecd70 100644 --- a/internal/cmd/install.go +++ b/internal/cmd/install.go @@ -171,17 +171,6 @@ func runInstall(cmd *cobra.Command, args []string) error { } fmt.Printf(" ✓ Created mayor/rigs.json\n") - // Create mayor state.json - mayorState := &config.AgentState{ - Role: "mayor", - LastActive: time.Now(), - } - statePath := filepath.Join(mayorDir, "state.json") - if err := config.SaveAgentState(statePath, mayorState); err != nil { - return fmt.Errorf("writing mayor state: %w", err) - } - fmt.Printf(" ✓ Created mayor/state.json\n") - // Create Mayor CLAUDE.md at HQ root (Mayor runs from there) if err := createMayorCLAUDEmd(absPath, absPath); err != nil { fmt.Printf(" %s Could not create CLAUDE.md: %v\n", style.Dim.Render("⚠"), err) diff --git a/internal/cmd/install_integration_test.go b/internal/cmd/install_integration_test.go index 778dadfd..83495e5e 100644 --- a/internal/cmd/install_integration_test.go +++ b/internal/cmd/install_integration_test.go @@ -3,7 +3,6 @@ package cmd import ( - "encoding/json" "os" "os/exec" "path/filepath" @@ -61,22 +60,6 @@ func TestInstallCreatesCorrectStructure(t *testing.T) { t.Errorf("rigs.json should be empty, got %d rigs", len(rigsConfig.Rigs)) } - // Verify mayor/state.json - statePath := filepath.Join(hqPath, "mayor", "state.json") - assertFileExists(t, statePath, "mayor/state.json") - - stateData, err := os.ReadFile(statePath) - if err != nil { - t.Fatalf("failed to read state.json: %v", err) - } - var state map[string]interface{} - if err := json.Unmarshal(stateData, &state); err != nil { - t.Fatalf("failed to parse state.json: %v", err) - } - if state["role"] != "mayor" { - t.Errorf("state.json role = %q, want %q", state["role"], "mayor") - } - // Verify CLAUDE.md exists claudePath := filepath.Join(hqPath, "CLAUDE.md") assertFileExists(t, claudePath, "CLAUDE.md") diff --git a/internal/cmd/rig_integration_test.go b/internal/cmd/rig_integration_test.go index 721ff49c..40f470cf 100644 --- a/internal/cmd/rig_integration_test.go +++ b/internal/cmd/rig_integration_test.go @@ -542,17 +542,20 @@ func TestRigAddCreatesAgentDirs(t *testing.T) { rigPath := filepath.Join(townRoot, "agenttest") - // Verify agent state files exist - expectedStateFiles := []string{ - "witness/state.json", - "refinery/state.json", - "mayor/state.json", + // Verify agent directories exist (state.json files are no longer created) + expectedDirs := []string{ + "witness", + "refinery", + "mayor", } - for _, stateFile := range expectedStateFiles { - path := filepath.Join(rigPath, stateFile) - if _, err := os.Stat(path); err != nil { - t.Errorf("expected state file %s to exist: %v", stateFile, err) + for _, dir := range expectedDirs { + path := filepath.Join(rigPath, dir) + info, err := os.Stat(path) + if err != nil { + t.Errorf("expected directory %s to exist: %v", dir, err) + } else if !info.IsDir() { + t.Errorf("expected %s to be a directory", dir) } } } diff --git a/internal/config/loader.go b/internal/config/loader.go index c6a0808d..6f86e64a 100644 --- a/internal/config/loader.go +++ b/internal/config/loader.go @@ -113,50 +113,6 @@ func SaveRigsConfig(path string, config *RigsConfig) error { return nil } -// LoadAgentState loads an agent state file. -func LoadAgentState(path string) (*AgentState, error) { - data, err := os.ReadFile(path) //nolint:gosec // G304: path is constructed internally, not from user input - if err != nil { - if os.IsNotExist(err) { - return nil, fmt.Errorf("%w: %s", ErrNotFound, path) - } - return nil, fmt.Errorf("reading state: %w", err) - } - - var state AgentState - if err := json.Unmarshal(data, &state); err != nil { - return nil, fmt.Errorf("parsing state: %w", err) - } - - if err := validateAgentState(&state); err != nil { - return nil, err - } - - return &state, nil -} - -// SaveAgentState saves an agent state to a file. -func SaveAgentState(path string, state *AgentState) error { - if err := validateAgentState(state); err != nil { - return err - } - - if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { - return fmt.Errorf("creating directory: %w", err) - } - - data, err := json.MarshalIndent(state, "", " ") - if err != nil { - return fmt.Errorf("encoding state: %w", err) - } - - if err := os.WriteFile(path, data, 0644); err != nil { //nolint:gosec // G306: state files don't contain secrets - return fmt.Errorf("writing state: %w", err) - } - - return nil -} - // validateTownConfig validates a TownConfig. func validateTownConfig(c *TownConfig) error { if c.Type != "town" && c.Type != "" { @@ -182,14 +138,6 @@ func validateRigsConfig(c *RigsConfig) error { return nil } -// validateAgentState validates an AgentState. -func validateAgentState(s *AgentState) error { - if s.Role == "" { - return fmt.Errorf("%w: role", ErrMissingField) - } - return nil -} - // LoadRigConfig loads and validates a rig configuration file. func LoadRigConfig(path string) (*RigConfig, error) { data, err := os.ReadFile(path) //nolint:gosec // G304: path is constructed internally, not from user input diff --git a/internal/config/loader_test.go b/internal/config/loader_test.go index 6e632fd5..548b6d69 100644 --- a/internal/config/loader_test.go +++ b/internal/config/loader_test.go @@ -80,36 +80,6 @@ func TestRigsConfigRoundTrip(t *testing.T) { } } -func TestAgentStateRoundTrip(t *testing.T) { - dir := t.TempDir() - path := filepath.Join(dir, "state.json") - - original := &AgentState{ - Role: "mayor", - LastActive: time.Now().Truncate(time.Second), - Session: "abc123", - Extra: map[string]any{ - "custom": "value", - }, - } - - if err := SaveAgentState(path, original); err != nil { - t.Fatalf("SaveAgentState: %v", err) - } - - loaded, err := LoadAgentState(path) - if err != nil { - t.Fatalf("LoadAgentState: %v", err) - } - - if loaded.Role != original.Role { - t.Errorf("Role = %q, want %q", loaded.Role, original.Role) - } - if loaded.Session != original.Session { - t.Errorf("Session = %q, want %q", loaded.Session, original.Session) - } -} - func TestLoadTownConfigNotFound(t *testing.T) { _, err := LoadTownConfig("/nonexistent/path.json") if err == nil { @@ -129,12 +99,6 @@ func TestValidationErrors(t *testing.T) { if err := validateTownConfig(tc); err == nil { t.Error("expected error for wrong type") } - - // Missing role - as := &AgentState{} - if err := validateAgentState(as); err == nil { - t.Error("expected error for missing role") - } } func TestRigConfigRoundTrip(t *testing.T) { diff --git a/internal/config/types.go b/internal/config/types.go index ff1e5e37..28751f38 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -65,14 +65,6 @@ type BeadsConfig struct { Prefix string `json:"prefix"` // issue prefix } -// AgentState represents an agent's current state (*/state.json). -type AgentState struct { - Role string `json:"role"` // "mayor", "witness", etc. - LastActive time.Time `json:"last_active"` - Session string `json:"session,omitempty"` - Extra map[string]any `json:"extra,omitempty"` -} - // CurrentTownVersion is the current schema version for TownConfig. // Version 2: Added Owner and PublicName fields for federation identity. const CurrentTownVersion = 2 diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 85d99186..a777e1f4 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -64,9 +64,6 @@ const ( // FileTownJSON is the town configuration file in mayor/. FileTownJSON = "town.json" - // FileStateJSON is the agent state file. - FileStateJSON = "state.json" - // FileConfigJSON is the general config file. FileConfigJSON = "config.json" @@ -176,11 +173,6 @@ func MayorTownPath(townRoot string) string { return townRoot + "/" + DirMayor + "/" + FileTownJSON } -// MayorStatePath returns the path to mayor state.json within a town root. -func MayorStatePath(townRoot string) string { - return townRoot + "/" + DirMayor + "/" + FileStateJSON -} - // RigMayorPath returns the path to mayor/rig within a rig. func RigMayorPath(rigPath string) string { return rigPath + "/" + DirMayor + "/" + DirRig diff --git a/internal/daemon/lifecycle.go b/internal/daemon/lifecycle.go index 27cdc9b5..0be31759 100644 --- a/internal/daemon/lifecycle.go +++ b/internal/daemon/lifecycle.go @@ -3,7 +3,6 @@ package daemon import ( "encoding/json" "fmt" - "os" "os/exec" "path/filepath" "strings" @@ -160,11 +159,6 @@ func (d *Daemon) executeLifecycleAction(request *LifecycleRequest) error { d.logger.Printf("Executing %s for session %s", request.Action, sessionName) - // Verify agent state shows requesting_=true before killing - if err := d.verifyAgentRequestingState(request.From, request.Action); err != nil { - return fmt.Errorf("state verification failed: %w", err) - } - // Check agent bead state (ZFC: trust what agent reports) - gt-39ttg agentBeadID := d.identityToAgentBeadID(request.From) if agentBeadID != "" { @@ -206,11 +200,6 @@ func (d *Daemon) executeLifecycleAction(request *LifecycleRequest) error { return fmt.Errorf("restarting session: %w", err) } d.logger.Printf("Restarted session %s", sessionName) - - // Clear the requesting state so we don't cycle again - if err := d.clearAgentRequestingState(request.From, request.Action); err != nil { - d.logger.Printf("Warning: failed to clear agent state: %v", err) - } return nil default: @@ -517,115 +506,6 @@ func (d *Daemon) closeMessage(id string) error { return nil } -// verifyAgentRequestingState verifies that the agent has set requesting_=true -// in its state.json before we kill its session. This ensures the agent is actually -// ready to be killed and has completed its pre-shutdown tasks (git clean, handoff mail, etc). -func (d *Daemon) verifyAgentRequestingState(identity string, action LifecycleAction) error { - stateFile := d.identityToStateFile(identity) - if stateFile == "" { - // If we can't determine state file, log warning but allow action - // This maintains backwards compatibility with agents that don't support state files yet - d.logger.Printf("Warning: cannot determine state file for %s, skipping verification", identity) - return nil - } - - data, err := os.ReadFile(stateFile) - if err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("agent state file not found: %s (agent must set requesting_%s=true before lifecycle request)", stateFile, action) - } - return fmt.Errorf("reading agent state: %w", err) - } - - var state map[string]interface{} - if err := json.Unmarshal(data, &state); err != nil { - return fmt.Errorf("parsing agent state: %w", err) - } - - // Check for requesting_=true - key := "requesting_" + string(action) - val, ok := state[key] - if !ok { - return fmt.Errorf("agent state missing %s field (agent must set this before lifecycle request)", key) - } - - requesting, ok := val.(bool) - if !ok || !requesting { - return fmt.Errorf("agent state %s is not true (got: %v)", key, val) - } - - d.logger.Printf("Verified agent %s has %s=true", identity, key) - return nil -} - -// clearAgentRequestingState clears the requesting_=true flag after -// successfully completing a lifecycle action. This prevents the daemon from -// repeatedly cycling the same session. -func (d *Daemon) clearAgentRequestingState(identity string, action LifecycleAction) error { - stateFile := d.identityToStateFile(identity) - if stateFile == "" { - return fmt.Errorf("cannot determine state file for %s", identity) - } - - data, err := os.ReadFile(stateFile) - if err != nil { - return fmt.Errorf("reading state file: %w", err) - } - - var state map[string]interface{} - if err := json.Unmarshal(data, &state); err != nil { - return fmt.Errorf("parsing state: %w", err) - } - - // Remove the requesting_ key - key := "requesting_" + string(action) - delete(state, key) - delete(state, "requesting_time") // Also clean up the timestamp - - // Write back - newData, err := json.MarshalIndent(state, "", " ") - if err != nil { - return fmt.Errorf("marshaling state: %w", err) - } - - if err := os.WriteFile(stateFile, newData, 0644); err != nil { - return fmt.Errorf("writing state file: %w", err) - } - - d.logger.Printf("Cleared %s from agent %s state", key, identity) - return nil -} - -// identityToStateFile maps an agent identity to its state.json file path. -// Uses parseIdentity to extract components, then derives state file location. -func (d *Daemon) identityToStateFile(identity string) string { - parsed, err := parseIdentity(identity) - if err != nil { - return "" - } - - // Derive state file path based on working directory - workDir := d.getWorkDir(nil, parsed) // Use defaults, not role bead config - if workDir == "" { - return "" - } - - // For mayor and deacon, state file is in a subdirectory - switch parsed.RoleType { - case "mayor": - return filepath.Join(d.config.TownRoot, "mayor", "state.json") - case "deacon": - return filepath.Join(d.config.TownRoot, "deacon", "state.json") - case "witness": - return filepath.Join(d.config.TownRoot, parsed.RigName, "witness", "state.json") - case "refinery": - return filepath.Join(d.config.TownRoot, parsed.RigName, "refinery", "state.json") - default: - // For crew and polecat, state file is in their working directory - return filepath.Join(workDir, "state.json") - } -} - // AgentBeadInfo represents the parsed fields from an agent bead. type AgentBeadInfo struct { ID string `json:"id"` diff --git a/internal/doctor/lifecycle_check.go b/internal/doctor/lifecycle_check.go index eef5a10a..912f544e 100644 --- a/internal/doctor/lifecycle_check.go +++ b/internal/doctor/lifecycle_check.go @@ -3,23 +3,15 @@ package doctor import ( "encoding/json" "fmt" - "os" "os/exec" - "path/filepath" "strings" - - "github.com/steveyegge/gastown/internal/session" ) // LifecycleHygieneCheck detects and cleans up stale lifecycle state. -// This can happen when: -// - Lifecycle messages weren't properly deleted after processing -// - Agent state.json has stuck requesting_* flags -// - Session was manually killed without clearing state +// This can happen when lifecycle messages weren't properly deleted after processing. type LifecycleHygieneCheck struct { FixableCheck - staleMessages []staleMessage - stuckStateFiles []stuckState + staleMessages []staleMessage } type staleMessage struct { @@ -28,19 +20,13 @@ type staleMessage struct { From string } -type stuckState struct { - stateFile string - identity string - flag string -} - // NewLifecycleHygieneCheck creates a new lifecycle hygiene check. func NewLifecycleHygieneCheck() *LifecycleHygieneCheck { return &LifecycleHygieneCheck{ FixableCheck: FixableCheck{ BaseCheck: BaseCheck{ CheckName: "lifecycle-hygiene", - CheckDescription: "Check for stale lifecycle messages and stuck state flags", + CheckDescription: "Check for stale lifecycle messages", }, }, } @@ -49,36 +35,21 @@ func NewLifecycleHygieneCheck() *LifecycleHygieneCheck { // Run checks for stale lifecycle state. func (c *LifecycleHygieneCheck) Run(ctx *CheckContext) *CheckResult { c.staleMessages = nil - c.stuckStateFiles = nil - - var details []string // Check for stale lifecycle messages in deacon inbox staleCount := c.checkDeaconInbox(ctx) - if staleCount > 0 { - details = append(details, fmt.Sprintf("%d stale lifecycle message(s) in deacon inbox", staleCount)) - } - - // Check for stuck requesting_* flags in state files - stuckCount := c.checkStateFiles(ctx) - if stuckCount > 0 { - details = append(details, fmt.Sprintf("%d agent(s) with stuck requesting_* flags", stuckCount)) - } - - total := staleCount + stuckCount - if total == 0 { + if staleCount == 0 { return &CheckResult{ Name: c.Name(), Status: StatusOK, - Message: "No stale lifecycle state found", + Message: "No stale lifecycle messages found", } } return &CheckResult{ Name: c.Name(), Status: StatusWarning, - Message: fmt.Sprintf("Found %d lifecycle hygiene issue(s)", total), - Details: details, + Message: fmt.Sprintf("Found %d stale lifecycle message(s) in deacon inbox", staleCount), FixHint: "Run 'gt doctor --fix' to clean up", } } @@ -121,139 +92,7 @@ func (c *LifecycleHygieneCheck) checkDeaconInbox(ctx *CheckContext) int { return len(c.staleMessages) } -// checkStateFiles looks for stuck requesting_* flags in state.json files. -func (c *LifecycleHygieneCheck) checkStateFiles(ctx *CheckContext) int { - stateFiles := c.findStateFiles(ctx.TownRoot) - - for _, sf := range stateFiles { - data, err := os.ReadFile(sf.path) - if err != nil { - continue - } - - var state map[string]interface{} - if err := json.Unmarshal(data, &state); err != nil { - continue - } - - // Check for any requesting_* flags - for key, val := range state { - if strings.HasPrefix(key, "requesting_") { - if boolVal, ok := val.(bool); ok && boolVal { - // Found a stuck flag - verify session is actually healthy - if c.isSessionHealthy(sf.identity, ctx.TownRoot) { - c.stuckStateFiles = append(c.stuckStateFiles, stuckState{ - stateFile: sf.path, - identity: sf.identity, - flag: key, - }) - } - } - } - } - } - - return len(c.stuckStateFiles) -} - -type stateFileInfo struct { - path string - identity string -} - -// findStateFiles locates all state.json files for agents. -func (c *LifecycleHygieneCheck) findStateFiles(townRoot string) []stateFileInfo { - var files []stateFileInfo - - // Mayor state - mayorState := filepath.Join(townRoot, "mayor", "state.json") - if _, err := os.Stat(mayorState); err == nil { - files = append(files, stateFileInfo{path: mayorState, identity: "mayor"}) - } - - // Scan rigs for witness, refinery, and crew state files - entries, err := os.ReadDir(townRoot) - if err != nil { - return files - } - - for _, entry := range entries { - if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") || entry.Name() == "mayor" { - continue - } - - rigName := entry.Name() - rigPath := filepath.Join(townRoot, rigName) - - // Witness state - witnessState := filepath.Join(rigPath, "witness", "state.json") - if _, err := os.Stat(witnessState); err == nil { - files = append(files, stateFileInfo{ - path: witnessState, - identity: rigName + "-witness", - }) - } - - // Refinery state - refineryState := filepath.Join(rigPath, "refinery", "state.json") - if _, err := os.Stat(refineryState); err == nil { - files = append(files, stateFileInfo{ - path: refineryState, - identity: rigName + "-refinery", - }) - } - - // Crew state files - crewPath := filepath.Join(rigPath, "crew") - crewEntries, err := os.ReadDir(crewPath) - if err != nil { - continue - } - for _, crew := range crewEntries { - if !crew.IsDir() || strings.HasPrefix(crew.Name(), ".") { - continue - } - crewState := filepath.Join(crewPath, crew.Name(), "state.json") - if _, err := os.Stat(crewState); err == nil { - files = append(files, stateFileInfo{ - path: crewState, - identity: rigName + "-crew-" + crew.Name(), - }) - } - } - } - - return files -} - -// isSessionHealthy checks if the tmux session for this identity exists and is running. -func (c *LifecycleHygieneCheck) isSessionHealthy(identity, _ string) bool { - sessionName := identityToSessionName(identity) - if sessionName == "" { - return false - } - - // Check if session exists - cmd := exec.Command("tmux", "has-session", "-t", sessionName) - return cmd.Run() == nil -} - -// identityToSessionName converts an identity to its tmux session name. -func identityToSessionName(identity string) string { - switch identity { - case "mayor": - return session.MayorSessionName() - default: - if strings.HasSuffix(identity, "-witness") || - strings.HasSuffix(identity, "-refinery") || - strings.Contains(identity, "-crew-") { - return "gt-" + identity - } - return "" - } -} - -// Fix cleans up stale lifecycle state. +// Fix cleans up stale lifecycle messages. func (c *LifecycleHygieneCheck) Fix(ctx *CheckContext) error { var errors []string @@ -266,39 +105,8 @@ func (c *LifecycleHygieneCheck) Fix(ctx *CheckContext) error { } } - // Clear stuck requesting_* flags - for _, stuck := range c.stuckStateFiles { - if err := c.clearRequestingFlag(stuck); err != nil { - errors = append(errors, fmt.Sprintf("failed to clear %s in %s: %v", stuck.flag, stuck.identity, err)) - } - } - if len(errors) > 0 { return fmt.Errorf("%s", strings.Join(errors, "; ")) } return nil } - -// clearRequestingFlag removes the stuck requesting_* flag from a state file. -func (c *LifecycleHygieneCheck) clearRequestingFlag(stuck stuckState) error { - data, err := os.ReadFile(stuck.stateFile) - if err != nil { - return err - } - - var state map[string]interface{} - if err := json.Unmarshal(data, &state); err != nil { - return err - } - - // Remove the requesting flag and any associated timestamp - delete(state, stuck.flag) - delete(state, "requesting_time") - - newData, err := json.MarshalIndent(state, "", " ") - if err != nil { - return err - } - - return os.WriteFile(stuck.stateFile, newData, 0644) -} diff --git a/internal/doctor/workspace_check.go b/internal/doctor/workspace_check.go index 500c5368..b926e86c 100644 --- a/internal/doctor/workspace_check.go +++ b/internal/doctor/workspace_check.go @@ -373,78 +373,6 @@ func (c *MayorExistsCheck) Run(ctx *CheckContext) *CheckResult { } } -// MayorStateValidCheck verifies mayor/state.json is valid JSON if it exists. -type MayorStateValidCheck struct { - FixableCheck -} - -// NewMayorStateValidCheck creates a new mayor state validation check. -func NewMayorStateValidCheck() *MayorStateValidCheck { - return &MayorStateValidCheck{ - FixableCheck: FixableCheck{ - BaseCheck: BaseCheck{ - CheckName: "mayor-state-valid", - CheckDescription: "Check that mayor/state.json is valid if it exists", - }, - }, - } -} - -// Run validates mayor/state.json if it exists. -func (c *MayorStateValidCheck) Run(ctx *CheckContext) *CheckResult { - statePath := filepath.Join(ctx.TownRoot, "mayor", "state.json") - - data, err := os.ReadFile(statePath) - if err != nil { - if os.IsNotExist(err) { - return &CheckResult{ - Name: c.Name(), - Status: StatusOK, - Message: "mayor/state.json not present (optional)", - } - } - return &CheckResult{ - Name: c.Name(), - Status: StatusError, - Message: "Cannot read mayor/state.json", - Details: []string{err.Error()}, - } - } - - // Just verify it's valid JSON - var state interface{} - if err := json.Unmarshal(data, &state); err != nil { - return &CheckResult{ - Name: c.Name(), - Status: StatusError, - Message: "mayor/state.json is not valid JSON", - Details: []string{err.Error()}, - FixHint: "Run 'gt doctor --fix' to reset to default state", - } - } - - return &CheckResult{ - Name: c.Name(), - Status: StatusOK, - Message: "mayor/state.json is valid JSON", - } -} - -// Fix resets mayor/state.json to default empty state. -func (c *MayorStateValidCheck) Fix(ctx *CheckContext) error { - statePath := filepath.Join(ctx.TownRoot, "mayor", "state.json") - - // Default empty state - defaultState := map[string]interface{}{} - - data, err := json.MarshalIndent(defaultState, "", " ") - if err != nil { - return fmt.Errorf("marshaling default state: %w", err) - } - - return os.WriteFile(statePath, data, 0644) -} - // WorkspaceChecks returns all workspace-level health checks. func WorkspaceChecks() []Check { return []Check{ @@ -453,6 +381,5 @@ func WorkspaceChecks() []Check { NewRigsRegistryExistsCheck(), NewRigsRegistryValidCheck(), NewMayorExistsCheck(), - NewMayorStateValidCheck(), } } diff --git a/internal/rig/manager.go b/internal/rig/manager.go index 94b13de9..83f30550 100644 --- a/internal/rig/manager.go +++ b/internal/rig/manager.go @@ -133,9 +133,9 @@ func (m *Manager) loadRig(name string, entry config.RigEntry) (*Rig, error) { } } - // Check for witness (witnesses don't have clones, just state.json) - witnessStatePath := filepath.Join(rigPath, "witness", "state.json") - if _, err := os.Stat(witnessStatePath); err == nil { + // Check for witness (witnesses don't have clones, just the witness directory) + witnessPath := filepath.Join(rigPath, "witness") + if info, err := os.Stat(witnessPath); err == nil && info.IsDir() { rig.HasWitness = true } @@ -414,11 +414,6 @@ Use crew for your own workspace. Polecats are for batch work dispatch. return nil, fmt.Errorf("creating polecats dir: %w", err) } - // Initialize agent state files - if err := m.initAgentStates(rigPath); err != nil { - return nil, fmt.Errorf("initializing agent states: %w", err) - } - // Initialize beads at rig level fmt.Printf(" Initializing beads database...\n") if err := m.initBeads(rigPath, opts.BeadsPrefix); err != nil { @@ -484,33 +479,6 @@ func LoadRigConfig(rigPath string) (*RigConfig, error) { return &cfg, nil } -// initAgentStates creates initial state.json files for agents. -func (m *Manager) initAgentStates(rigPath string) error { - agents := []struct { - path string - role string - }{ - {filepath.Join(rigPath, "refinery", "state.json"), "refinery"}, - {filepath.Join(rigPath, "witness", "state.json"), "witness"}, - {filepath.Join(rigPath, "mayor", "state.json"), "mayor"}, - } - - for _, agent := range agents { - state := &config.AgentState{ - Role: agent.role, - LastActive: time.Now(), - } - data, err := json.MarshalIndent(state, "", " ") - if err != nil { - return err - } - if err := os.WriteFile(agent.path, data, 0644); err != nil { - return err - } - } - return nil -} - // initBeads initializes the beads database at rig level. // The project's .beads/config.yaml determines sync-branch settings. // Use `bd doctor --fix` in the project to configure sync-branch if needed. diff --git a/internal/rig/manager_test.go b/internal/rig/manager_test.go index edd7d1e6..ca378922 100644 --- a/internal/rig/manager_test.go +++ b/internal/rig/manager_test.go @@ -40,7 +40,7 @@ func createTestRig(t *testing.T, root, name string) { t.Fatalf("mkdir rig: %v", err) } - // Create agent dirs + // Create agent dirs (witness, refinery, mayor) for _, dir := range AgentDirs { dirPath := filepath.Join(rigPath, dir) if err := os.MkdirAll(dirPath, 0755); err != nil { @@ -48,12 +48,6 @@ func createTestRig(t *testing.T, root, name string) { } } - // Create witness state.json (witnesses don't have clones, just state) - witnessState := filepath.Join(rigPath, "witness", "state.json") - if err := os.WriteFile(witnessState, []byte(`{"role":"witness"}`), 0644); err != nil { - t.Fatalf("write witness state: %v", err) - } - // Create some polecats polecatsDir := filepath.Join(rigPath, "polecats") for _, polecat := range []string{"Toast", "Cheedo"} {