Files
gastown/internal/doctor/workspace_check.go
splendid acd2565a5b fix: remove vestigial state.json files from agent directories
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/<name>/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 <noreply@anthropic.com>
2026-01-04 10:36:23 -08:00

386 lines
9.5 KiB
Go

package doctor
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
// TownConfigExistsCheck verifies mayor/town.json exists.
type TownConfigExistsCheck struct {
BaseCheck
}
// NewTownConfigExistsCheck creates a new town config exists check.
func NewTownConfigExistsCheck() *TownConfigExistsCheck {
return &TownConfigExistsCheck{
BaseCheck: BaseCheck{
CheckName: "town-config-exists",
CheckDescription: "Check that mayor/town.json exists",
},
}
}
// Run checks if mayor/town.json exists.
func (c *TownConfigExistsCheck) Run(ctx *CheckContext) *CheckResult {
configPath := filepath.Join(ctx.TownRoot, "mayor", "town.json")
if _, err := os.Stat(configPath); os.IsNotExist(err) {
return &CheckResult{
Name: c.Name(),
Status: StatusError,
Message: "mayor/town.json not found",
FixHint: "Run 'gt install' to initialize workspace",
}
}
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "mayor/town.json exists",
}
}
// TownConfigValidCheck verifies mayor/town.json is valid JSON with required fields.
type TownConfigValidCheck struct {
BaseCheck
}
// NewTownConfigValidCheck creates a new town config validation check.
func NewTownConfigValidCheck() *TownConfigValidCheck {
return &TownConfigValidCheck{
BaseCheck: BaseCheck{
CheckName: "town-config-valid",
CheckDescription: "Check that mayor/town.json is valid with required fields",
},
}
}
// townConfig represents the structure of mayor/town.json.
type townConfig struct {
Type string `json:"type"`
Version int `json:"version"`
Name string `json:"name"`
}
// Run validates mayor/town.json contents.
func (c *TownConfigValidCheck) Run(ctx *CheckContext) *CheckResult {
configPath := filepath.Join(ctx.TownRoot, "mayor", "town.json")
data, err := os.ReadFile(configPath)
if err != nil {
return &CheckResult{
Name: c.Name(),
Status: StatusError,
Message: "Cannot read mayor/town.json",
Details: []string{err.Error()},
}
}
var config townConfig
if err := json.Unmarshal(data, &config); err != nil {
return &CheckResult{
Name: c.Name(),
Status: StatusError,
Message: "mayor/town.json is not valid JSON",
Details: []string{err.Error()},
FixHint: "Fix JSON syntax in mayor/town.json",
}
}
var issues []string
if config.Type != "town" {
issues = append(issues, fmt.Sprintf("type should be 'town', got '%s'", config.Type))
}
if config.Version == 0 {
issues = append(issues, "version field is missing or zero")
}
if config.Name == "" {
issues = append(issues, "name field is missing or empty")
}
if len(issues) > 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusError,
Message: "mayor/town.json has invalid fields",
Details: issues,
FixHint: "Fix the field values in mayor/town.json",
}
}
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: fmt.Sprintf("mayor/town.json valid (name=%s, version=%d)", config.Name, config.Version),
}
}
// RigsRegistryExistsCheck verifies mayor/rigs.json exists.
type RigsRegistryExistsCheck struct {
FixableCheck
}
// NewRigsRegistryExistsCheck creates a new rigs registry exists check.
func NewRigsRegistryExistsCheck() *RigsRegistryExistsCheck {
return &RigsRegistryExistsCheck{
FixableCheck: FixableCheck{
BaseCheck: BaseCheck{
CheckName: "rigs-registry-exists",
CheckDescription: "Check that mayor/rigs.json exists",
},
},
}
}
// Run checks if mayor/rigs.json exists.
func (c *RigsRegistryExistsCheck) Run(ctx *CheckContext) *CheckResult {
rigsPath := filepath.Join(ctx.TownRoot, "mayor", "rigs.json")
if _, err := os.Stat(rigsPath); os.IsNotExist(err) {
return &CheckResult{
Name: c.Name(),
Status: StatusWarning,
Message: "mayor/rigs.json not found (no rigs registered)",
FixHint: "Run 'gt doctor --fix' to create empty rigs.json",
}
}
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "mayor/rigs.json exists",
}
}
// Fix creates an empty rigs.json file.
func (c *RigsRegistryExistsCheck) Fix(ctx *CheckContext) error {
rigsPath := filepath.Join(ctx.TownRoot, "mayor", "rigs.json")
emptyRigs := struct {
Version int `json:"version"`
Rigs map[string]interface{} `json:"rigs"`
}{
Version: 1,
Rigs: make(map[string]interface{}),
}
data, err := json.MarshalIndent(emptyRigs, "", " ")
if err != nil {
return fmt.Errorf("marshaling empty rigs.json: %w", err)
}
return os.WriteFile(rigsPath, data, 0644)
}
// RigsRegistryValidCheck verifies mayor/rigs.json is valid and rigs exist.
type RigsRegistryValidCheck struct {
FixableCheck
missingRigs []string // Cached for Fix
}
// NewRigsRegistryValidCheck creates a new rigs registry validation check.
func NewRigsRegistryValidCheck() *RigsRegistryValidCheck {
return &RigsRegistryValidCheck{
FixableCheck: FixableCheck{
BaseCheck: BaseCheck{
CheckName: "rigs-registry-valid",
CheckDescription: "Check that registered rigs exist on disk",
},
},
}
}
// rigsConfig represents the structure of mayor/rigs.json.
type rigsConfig struct {
Version int `json:"version"`
Rigs map[string]interface{} `json:"rigs"`
}
// Run validates mayor/rigs.json and checks that registered rigs exist.
func (c *RigsRegistryValidCheck) Run(ctx *CheckContext) *CheckResult {
rigsPath := filepath.Join(ctx.TownRoot, "mayor", "rigs.json")
data, err := os.ReadFile(rigsPath)
if err != nil {
if os.IsNotExist(err) {
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "No rigs.json (skipping validation)",
}
}
return &CheckResult{
Name: c.Name(),
Status: StatusError,
Message: "Cannot read mayor/rigs.json",
Details: []string{err.Error()},
}
}
var config rigsConfig
if err := json.Unmarshal(data, &config); err != nil {
return &CheckResult{
Name: c.Name(),
Status: StatusError,
Message: "mayor/rigs.json is not valid JSON",
Details: []string{err.Error()},
FixHint: "Fix JSON syntax in mayor/rigs.json",
}
}
if len(config.Rigs) == 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "No rigs registered",
}
}
// Check each registered rig exists
var missing []string
var found int
for rigName := range config.Rigs {
rigPath := filepath.Join(ctx.TownRoot, rigName)
if _, err := os.Stat(rigPath); os.IsNotExist(err) {
missing = append(missing, rigName)
} else {
found++
}
}
// Cache for Fix
c.missingRigs = missing
if len(missing) > 0 {
details := make([]string, len(missing))
for i, m := range missing {
details[i] = fmt.Sprintf("Missing rig directory: %s/", m)
}
return &CheckResult{
Name: c.Name(),
Status: StatusWarning,
Message: fmt.Sprintf("%d of %d registered rig(s) missing", len(missing), len(config.Rigs)),
Details: details,
FixHint: "Run 'gt doctor --fix' to remove missing rigs from registry",
}
}
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: fmt.Sprintf("All %d registered rig(s) exist", found),
}
}
// Fix removes missing rigs from the registry.
func (c *RigsRegistryValidCheck) Fix(ctx *CheckContext) error {
if len(c.missingRigs) == 0 {
return nil
}
rigsPath := filepath.Join(ctx.TownRoot, "mayor", "rigs.json")
data, err := os.ReadFile(rigsPath)
if err != nil {
return fmt.Errorf("reading rigs.json: %w", err)
}
var config rigsConfig
if err := json.Unmarshal(data, &config); err != nil {
return fmt.Errorf("parsing rigs.json: %w", err)
}
// Remove missing rigs
for _, rig := range c.missingRigs {
delete(config.Rigs, rig)
}
// Write back
newData, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("marshaling rigs.json: %w", err)
}
return os.WriteFile(rigsPath, newData, 0644)
}
// MayorExistsCheck verifies the mayor/ directory structure.
type MayorExistsCheck struct {
BaseCheck
}
// NewMayorExistsCheck creates a new mayor directory check.
func NewMayorExistsCheck() *MayorExistsCheck {
return &MayorExistsCheck{
BaseCheck: BaseCheck{
CheckName: "mayor-exists",
CheckDescription: "Check that mayor/ directory exists with required files",
},
}
}
// Run checks if mayor/ directory exists with expected contents.
func (c *MayorExistsCheck) Run(ctx *CheckContext) *CheckResult {
mayorPath := filepath.Join(ctx.TownRoot, "mayor")
info, err := os.Stat(mayorPath)
if os.IsNotExist(err) {
return &CheckResult{
Name: c.Name(),
Status: StatusError,
Message: "mayor/ directory not found",
FixHint: "Run 'gt install' to initialize workspace",
}
}
if !info.IsDir() {
return &CheckResult{
Name: c.Name(),
Status: StatusError,
Message: "mayor exists but is not a directory",
FixHint: "Remove mayor file and run 'gt install'",
}
}
// Check for expected files
var missing []string
expectedFiles := []string{"town.json"}
for _, f := range expectedFiles {
path := filepath.Join(mayorPath, f)
if _, err := os.Stat(path); os.IsNotExist(err) {
missing = append(missing, f)
}
}
if len(missing) > 0 {
return &CheckResult{
Name: c.Name(),
Status: StatusWarning,
Message: "mayor/ exists but missing expected files",
Details: missing,
}
}
return &CheckResult{
Name: c.Name(),
Status: StatusOK,
Message: "mayor/ directory exists with required files",
}
}
// WorkspaceChecks returns all workspace-level health checks.
func WorkspaceChecks() []Check {
return []Check{
NewTownConfigExistsCheck(),
NewTownConfigValidCheck(),
NewRigsRegistryExistsCheck(),
NewRigsRegistryValidCheck(),
NewMayorExistsCheck(),
}
}