Add three layers of protection to prevent accidental branch switches in the town root (~/gt), which should always stay on main: 1. Doctor check `town-root-branch`: Verifies town root is on main/master. Fixable via `gt doctor --fix` to switch back to main. 2. Doctor check `pre-checkout-hook`: Verifies git pre-checkout hook is installed. The hook blocks checkout from main to any other branch. Fixable via `gt doctor --fix` or `gt git-init`. 3. Runtime warning in all gt commands: Non-blocking warning if town root is on wrong branch, with fix instructions. The root cause of this issue was git commands running in the wrong directory, switching the town root to a polecat branch. This broke gt commands because rigs.json and other configs were on main, not the polecat branch. Closes: hq-1kwuj Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
198 lines
6.8 KiB
Go
198 lines
6.8 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/steveyegge/gastown/internal/doctor"
|
|
"github.com/steveyegge/gastown/internal/workspace"
|
|
)
|
|
|
|
var (
|
|
doctorFix bool
|
|
doctorVerbose bool
|
|
doctorRig string
|
|
doctorRestartSessions bool
|
|
)
|
|
|
|
var doctorCmd = &cobra.Command{
|
|
Use: "doctor",
|
|
GroupID: GroupDiag,
|
|
Short: "Run health checks on the workspace",
|
|
Long: `Run diagnostic checks on the Gas Town workspace.
|
|
|
|
Doctor checks for common configuration issues, missing files,
|
|
and other problems that could affect workspace operation.
|
|
|
|
Workspace checks:
|
|
- town-config-exists Check mayor/town.json exists
|
|
- town-config-valid Check mayor/town.json is valid
|
|
- rigs-registry-exists Check mayor/rigs.json exists (fixable)
|
|
- rigs-registry-valid Check registered rigs exist (fixable)
|
|
- mayor-exists Check mayor/ directory structure
|
|
|
|
Town root protection:
|
|
- town-git Verify town root is under version control
|
|
- town-root-branch Verify town root is on main branch (fixable)
|
|
- pre-checkout-hook Verify pre-checkout hook prevents branch switches (fixable)
|
|
|
|
Infrastructure checks:
|
|
- daemon Check if daemon is running (fixable)
|
|
- repo-fingerprint Check database has valid repo fingerprint (fixable)
|
|
- boot-health Check Boot watchdog health (vet mode)
|
|
|
|
Cleanup checks (fixable):
|
|
- orphan-sessions Detect orphaned tmux sessions
|
|
- orphan-processes Detect orphaned Claude processes
|
|
- wisp-gc Detect and clean abandoned wisps (>1h)
|
|
|
|
Clone divergence checks:
|
|
- persistent-role-branches Detect crew/witness/refinery not on main
|
|
- clone-divergence Detect clones significantly behind origin/main
|
|
|
|
Crew workspace checks:
|
|
- crew-state Validate crew worker state.json files (fixable)
|
|
- crew-worktrees Detect stale cross-rig worktrees (fixable)
|
|
|
|
Rig checks (with --rig flag):
|
|
- rig-is-git-repo Verify rig is a valid git repository
|
|
- git-exclude-configured Check .git/info/exclude has Gas Town dirs (fixable)
|
|
- witness-exists Verify witness/ structure exists (fixable)
|
|
- refinery-exists Verify refinery/ structure exists (fixable)
|
|
- mayor-clone-exists Verify mayor/rig/ clone exists (fixable)
|
|
- polecat-clones-valid Verify polecat directories are valid clones
|
|
- beads-config-valid Verify beads configuration (fixable)
|
|
|
|
Routing checks (fixable):
|
|
- routes-config Check beads routing configuration
|
|
- prefix-mismatch Detect rigs.json vs routes.jsonl prefix mismatches (fixable)
|
|
|
|
Session hook checks:
|
|
- session-hooks Check settings.json use session-start.sh
|
|
- claude-settings Check Claude settings.json match templates (fixable)
|
|
|
|
Patrol checks:
|
|
- patrol-molecules-exist Verify patrol molecules exist
|
|
- patrol-hooks-wired Verify daemon triggers patrols
|
|
- patrol-not-stuck Detect stale wisps (>1h)
|
|
- patrol-plugins-accessible Verify plugin directories
|
|
- patrol-roles-have-prompts Verify role prompts exist
|
|
|
|
Use --fix to attempt automatic fixes for issues that support it.
|
|
Use --rig to check a specific rig instead of the entire workspace.`,
|
|
RunE: runDoctor,
|
|
}
|
|
|
|
func init() {
|
|
doctorCmd.Flags().BoolVar(&doctorFix, "fix", false, "Attempt to automatically fix issues")
|
|
doctorCmd.Flags().BoolVarP(&doctorVerbose, "verbose", "v", false, "Show detailed output")
|
|
doctorCmd.Flags().StringVar(&doctorRig, "rig", "", "Check specific rig only")
|
|
doctorCmd.Flags().BoolVar(&doctorRestartSessions, "restart-sessions", false, "Restart patrol sessions when fixing stale settings (use with --fix)")
|
|
rootCmd.AddCommand(doctorCmd)
|
|
}
|
|
|
|
func runDoctor(cmd *cobra.Command, args []string) error {
|
|
// Find town root
|
|
townRoot, err := workspace.FindFromCwdOrError()
|
|
if err != nil {
|
|
return fmt.Errorf("not in a Gas Town workspace: %w", err)
|
|
}
|
|
|
|
// Create check context
|
|
ctx := &doctor.CheckContext{
|
|
TownRoot: townRoot,
|
|
RigName: doctorRig,
|
|
Verbose: doctorVerbose,
|
|
RestartSessions: doctorRestartSessions,
|
|
}
|
|
|
|
// Create doctor and register checks
|
|
d := doctor.NewDoctor()
|
|
|
|
// Register workspace-level checks first (fundamental)
|
|
d.RegisterAll(doctor.WorkspaceChecks()...)
|
|
|
|
d.Register(doctor.NewGlobalStateCheck())
|
|
|
|
// Register built-in checks
|
|
d.Register(doctor.NewTownGitCheck())
|
|
d.Register(doctor.NewTownRootBranchCheck())
|
|
d.Register(doctor.NewPreCheckoutHookCheck())
|
|
d.Register(doctor.NewDaemonCheck())
|
|
d.Register(doctor.NewRepoFingerprintCheck())
|
|
d.Register(doctor.NewBootHealthCheck())
|
|
d.Register(doctor.NewBeadsDatabaseCheck())
|
|
d.Register(doctor.NewCustomTypesCheck())
|
|
d.Register(doctor.NewFormulaCheck())
|
|
d.Register(doctor.NewBdDaemonCheck())
|
|
d.Register(doctor.NewPrefixConflictCheck())
|
|
d.Register(doctor.NewPrefixMismatchCheck())
|
|
d.Register(doctor.NewRoutesCheck())
|
|
d.Register(doctor.NewOrphanSessionCheck())
|
|
d.Register(doctor.NewOrphanProcessCheck())
|
|
d.Register(doctor.NewGTRootCheck())
|
|
d.Register(doctor.NewWispGCCheck())
|
|
d.Register(doctor.NewBranchCheck())
|
|
d.Register(doctor.NewBeadsSyncOrphanCheck())
|
|
d.Register(doctor.NewCloneDivergenceCheck())
|
|
d.Register(doctor.NewIdentityCollisionCheck())
|
|
d.Register(doctor.NewLinkedPaneCheck())
|
|
d.Register(doctor.NewThemeCheck())
|
|
d.Register(doctor.NewCrashReportCheck())
|
|
|
|
// Patrol system checks
|
|
d.Register(doctor.NewPatrolMoleculesExistCheck())
|
|
d.Register(doctor.NewPatrolHooksWiredCheck())
|
|
d.Register(doctor.NewPatrolNotStuckCheck())
|
|
d.Register(doctor.NewPatrolPluginsAccessibleCheck())
|
|
d.Register(doctor.NewPatrolRolesHavePromptsCheck())
|
|
d.Register(doctor.NewAgentBeadsCheck())
|
|
d.Register(doctor.NewRigBeadsCheck())
|
|
|
|
// NOTE: StaleAttachmentsCheck removed - staleness detection belongs in Deacon molecule
|
|
|
|
// Config architecture checks
|
|
d.Register(doctor.NewSettingsCheck())
|
|
d.Register(doctor.NewSessionHookCheck())
|
|
d.Register(doctor.NewRuntimeGitignoreCheck())
|
|
d.Register(doctor.NewLegacyGastownCheck())
|
|
d.Register(doctor.NewClaudeSettingsCheck())
|
|
|
|
// Crew workspace checks
|
|
d.Register(doctor.NewCrewStateCheck())
|
|
d.Register(doctor.NewCrewWorktreeCheck())
|
|
d.Register(doctor.NewCommandsCheck())
|
|
|
|
// Lifecycle hygiene checks
|
|
d.Register(doctor.NewLifecycleHygieneCheck())
|
|
|
|
// Hook attachment checks
|
|
d.Register(doctor.NewHookAttachmentValidCheck())
|
|
d.Register(doctor.NewHookSingletonCheck())
|
|
d.Register(doctor.NewOrphanedAttachmentsCheck())
|
|
|
|
// Rig-specific checks (only when --rig is specified)
|
|
if doctorRig != "" {
|
|
d.RegisterAll(doctor.RigChecks()...)
|
|
}
|
|
|
|
// Run checks
|
|
var report *doctor.Report
|
|
if doctorFix {
|
|
report = d.Fix(ctx)
|
|
} else {
|
|
report = d.Run(ctx)
|
|
}
|
|
|
|
// Print report
|
|
report.Print(os.Stdout, doctorVerbose)
|
|
|
|
// Exit with error code if there are errors
|
|
if report.HasErrors() {
|
|
return fmt.Errorf("doctor found %d error(s)", report.Summary.Errors)
|
|
}
|
|
|
|
return nil
|
|
}
|