feat(doctor): add safeguards for town root branch protection

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>
This commit is contained in:
slit
2026-01-09 13:16:37 -08:00
committed by beads/crew/giles
parent 692d6819f2
commit ba76bf1232
5 changed files with 460 additions and 1 deletions

View File

@@ -3,9 +3,13 @@ package cmd
import (
"fmt"
"os"
"os/exec"
"strings"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/workspace"
)
var rootCmd = &cobra.Command{
@@ -16,7 +20,7 @@ var rootCmd = &cobra.Command{
It coordinates agent spawning, work distribution, and communication
across distributed teams of AI agents working on shared codebases.`,
PersistentPreRunE: checkBeadsDependency,
PersistentPreRunE: persistentPreRun,
}
// Commands that don't require beads to be installed/checked.
@@ -27,8 +31,74 @@ var beadsExemptCommands = map[string]bool{
"completion": true,
}
// Commands exempt from the town root branch warning.
// These are commands that help fix the problem or are diagnostic.
var branchCheckExemptCommands = map[string]bool{
"version": true,
"help": true,
"completion": true,
"doctor": true, // Used to fix the problem
"install": true, // Initial setup
"git-init": true, // Git setup
}
// persistentPreRun runs before every command.
func persistentPreRun(cmd *cobra.Command, args []string) error {
// Get the root command name being run
cmdName := cmd.Name()
// Check town root branch (warning only, non-blocking)
if !branchCheckExemptCommands[cmdName] {
warnIfTownRootOffMain()
}
// Skip beads check for exempt commands
if beadsExemptCommands[cmdName] {
return nil
}
// Check beads version
return CheckBeadsVersion()
}
// warnIfTownRootOffMain prints a warning if the town root is not on main branch.
// This is a non-blocking warning to help catch accidental branch switches.
func warnIfTownRootOffMain() {
// Find town root (silently - don't error if not in workspace)
townRoot, err := workspace.FindFromCwd()
if err != nil || townRoot == "" {
return
}
// Check if it's a git repo
gitDir := townRoot + "/.git"
if _, err := os.Stat(gitDir); os.IsNotExist(err) {
return
}
// Get current branch
gitCmd := exec.Command("git", "branch", "--show-current")
gitCmd.Dir = townRoot
out, err := gitCmd.Output()
if err != nil {
return
}
branch := strings.TrimSpace(string(out))
if branch == "" || branch == "main" || branch == "master" {
return
}
// Town root is on wrong branch - warn the user
fmt.Fprintf(os.Stderr, "\n%s Town root is on branch '%s' (should be 'main')\n",
style.Bold.Render("⚠️ WARNING:"), branch)
fmt.Fprintf(os.Stderr, " This can cause gt commands to fail. Run: %s\n\n",
style.Dim.Render("gt doctor --fix"))
}
// checkBeadsDependency verifies beads meets minimum version requirements.
// Skips check for exempt commands (version, help, completion).
// Deprecated: Use persistentPreRun instead, which calls CheckBeadsVersion.
func checkBeadsDependency(cmd *cobra.Command, args []string) error {
// Get the root command name being run
cmdName := cmd.Name()