diff --git a/docs/reference.md b/docs/reference.md index e0c1f196..951d3a10 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -204,6 +204,88 @@ gt mol step done # Complete a molecule step | `GT_RIG` | Rig name for rig-level agents | | `GT_POLECAT` | Polecat name (for polecats only) | +## Agent Working Directories and Settings + +Each agent runs in a specific working directory and has its own Claude settings. +Understanding this hierarchy is essential for proper configuration. + +### Working Directories by Role + +| Role | Working Directory | Notes | +|------|-------------------|-------| +| **Mayor** | `~/gt/mayor/` | Isolated from child agents | +| **Deacon** | `~/gt/deacon/` | Background supervisor | +| **Witness** | `~/gt//witness/` | No git clone, monitors only | +| **Refinery** | `~/gt//refinery/rig/` | Worktree on main branch | +| **Crew** | `~/gt//crew//rig/` | Persistent clone | +| **Polecat** | `~/gt//polecats//rig/` | Ephemeral worktree | + +### Settings File Locations + +Claude Code searches for `.claude/settings.json` starting from the working +directory and traversing upward. Each agent has settings at its working directory: + +``` +~/gt/ +├── mayor/.claude/settings.json # Mayor settings +├── deacon/.claude/settings.json # Deacon settings +└── / + ├── witness/.claude/settings.json # Witness settings + ├── refinery/rig/.claude/settings.json + ├── crew//rig/.claude/settings.json + └── polecats//rig/.claude/settings.json +``` + +**Why this structure?** Child agents inherit the parent's working directory +when spawned. By keeping each role's files in separate directories, we prevent +the mayor's CLAUDE.md or settings from affecting polecat behavior. + +### Sparse Checkout (Source Repo Isolation) + +When agents work on source repositories that have their own `.claude/` directory, +Gas Town uses git sparse checkout to exclude it: + +```bash +# Automatically configured for worktrees +git sparse-checkout set --no-cone '/*' '!/.claude/' +``` + +This ensures the agent uses Gas Town's settings, not the source repo's. + +**Doctor check**: `gt doctor` verifies sparse checkout is configured correctly. + +### Settings Inheritance + +Claude Code's settings search order (first match wins): + +1. `.claude/settings.json` in current working directory +2. `.claude/settings.json` in parent directories (traversing up) +3. `~/.claude/settings.json` (user global settings) + +Gas Town places settings at each agent's working directory root, so agents +find their role-specific settings before reaching any parent or global config. + +### Settings Templates + +Gas Town uses two settings templates based on role type: + +| Type | Roles | Key Difference | +|------|-------|----------------| +| **Interactive** | Mayor, Crew | Mail injected on `UserPromptSubmit` hook | +| **Autonomous** | Polecat, Witness, Refinery, Deacon | Mail injected on `SessionStart` hook | + +Autonomous agents may start without user input, so they need mail checked +at session start. Interactive agents wait for user prompts. + +### Troubleshooting + +| Problem | Solution | +|---------|----------| +| Agent using wrong settings | Check `gt doctor`, verify sparse checkout | +| Settings not found | Ensure `.claude/settings.json` exists at role home | +| Source repo settings leaking | Run `gt doctor --fix` to configure sparse checkout | +| Mayor settings affecting polecats | Mayor should run in `mayor/`, not town root | + ## CLI Reference ### Town Management diff --git a/docs/understanding-gas-town.md b/docs/understanding-gas-town.md index e9fc0027..353fc924 100644 --- a/docs/understanding-gas-town.md +++ b/docs/understanding-gas-town.md @@ -26,7 +26,7 @@ These roles manage the Gas Town system itself: | Role | Description | Lifecycle | |------|-------------|-----------| -| **Mayor** | Global coordinator at town root | Singleton, persistent | +| **Mayor** | Global coordinator at mayor/ | Singleton, persistent | | **Deacon** | Background supervisor daemon ([watchdog chain](watchdog-chain.md)) | Singleton, persistent | | **Witness** | Per-rig polecat lifecycle manager | One per rig, persistent | | **Refinery** | Per-rig merge queue processor | One per rig, persistent | diff --git a/internal/cmd/hooks.go b/internal/cmd/hooks.go index 7c063ab4..53cec8b5 100644 --- a/internal/cmd/hooks.go +++ b/internal/cmd/hooks.go @@ -105,12 +105,14 @@ func discoverHooks(townRoot string) ([]HookInfo, error) { var hooks []HookInfo // Scan known locations for .claude/settings.json + // NOTE: Mayor settings are at ~/gt/mayor/.claude/, NOT ~/gt/.claude/ + // Settings at town root would pollute all child workspaces. locations := []struct { path string agent string }{ {filepath.Join(townRoot, "mayor", ".claude", "settings.json"), "mayor/"}, - {filepath.Join(townRoot, ".claude", "settings.json"), "town-root"}, + {filepath.Join(townRoot, "deacon", ".claude", "settings.json"), "deacon/"}, } // Scan rigs diff --git a/internal/cmd/mayor.go b/internal/cmd/mayor.go index 84c964bd..0dd538f6 100644 --- a/internal/cmd/mayor.go +++ b/internal/cmd/mayor.go @@ -3,6 +3,8 @@ package cmd import ( "errors" "fmt" + "os" + "path/filepath" "time" "github.com/spf13/cobra" @@ -126,9 +128,16 @@ func startMayorSession(t *tmux.Tmux, sessionName, agentOverride string) error { return fmt.Errorf("not in a Gas Town workspace: %w", err) } - // Create session in workspace root + // Mayor runs in mayor/ subdirectory to keep its files (CLAUDE.md, settings) + // separate from child agents that inherit the working directory + mayorDir := filepath.Join(townRoot, "mayor") + if err := os.MkdirAll(mayorDir, 0755); err != nil { + return fmt.Errorf("creating mayor directory: %w", err) + } + + // Create session in mayor directory fmt.Println("Starting Mayor session...") - if err := t.NewSession(sessionName, townRoot); err != nil { + if err := t.NewSession(sessionName, mayorDir); err != nil { return fmt.Errorf("creating session: %w", err) } @@ -170,7 +179,7 @@ func startMayorSession(t *tmux.Tmux, sessionName, agentOverride string) error { // Send the propulsion nudge to trigger autonomous coordination. // Wait for beacon to be fully processed (needs to be separate prompt) time.Sleep(2 * time.Second) - _ = t.NudgeSession(sessionName, session.PropulsionNudgeForRole("mayor", townRoot)) // Non-fatal + _ = t.NudgeSession(sessionName, session.PropulsionNudgeForRole("mayor", mayorDir)) // Non-fatal return nil } diff --git a/internal/cmd/prime.go b/internal/cmd/prime.go index 528371fd..1faaefc1 100644 --- a/internal/cmd/prime.go +++ b/internal/cmd/prime.go @@ -147,7 +147,10 @@ func runPrime(cmd *cobra.Command, args []string) error { } // Ensure beads redirect exists for worktree-based roles - ensureBeadsRedirect(ctx) + // Skip if there's a role/location mismatch to avoid creating bad redirects + if !roleInfo.Mismatch { + ensureBeadsRedirect(ctx) + } // NOTE: reportAgentState("running") removed (gt-zecmc) // Agent liveness is observable from tmux - no need to record it in bead. diff --git a/internal/cmd/role.go b/internal/cmd/role.go index 0a7a1d71..b744095e 100644 --- a/internal/cmd/role.go +++ b/internal/cmd/role.go @@ -270,7 +270,7 @@ func (info RoleInfo) ActorString() string { func getRoleHome(role Role, rig, polecat, townRoot string) string { switch role { case RoleMayor: - return townRoot + return filepath.Join(townRoot, "mayor") case RoleDeacon: return filepath.Join(townRoot, "deacon") case RoleWitness: @@ -423,7 +423,7 @@ func runRoleList(cmd *cobra.Command, args []string) error { name Role desc string }{ - {RoleMayor, "Global coordinator at town root"}, + {RoleMayor, "Global coordinator at mayor/"}, {RoleDeacon, "Background supervisor daemon"}, {RoleWitness, "Per-rig polecat lifecycle manager"}, {RoleRefinery, "Per-rig merge queue processor"}, diff --git a/internal/templates/templates.go b/internal/templates/templates.go index 3e07ee7e..ac9063ae 100644 --- a/internal/templates/templates.go +++ b/internal/templates/templates.go @@ -136,6 +136,32 @@ func (t *Templates) MessageNames() []string { return []string{"spawn", "nudge", "escalation", "handoff"} } +// CreateMayorCLAUDEmd creates the Mayor's CLAUDE.md file at the specified directory. +// This is used by both gt install and gt doctor --fix. +func CreateMayorCLAUDEmd(mayorDir, townRoot, townName, mayorSession, deaconSession string) error { + tmpl, err := New() + if err != nil { + return err + } + + data := RoleData{ + Role: "mayor", + TownRoot: townRoot, + TownName: townName, + WorkDir: mayorDir, + MayorSession: mayorSession, + DeaconSession: deaconSession, + } + + content, err := tmpl.RenderRole("mayor", data) + if err != nil { + return err + } + + claudePath := filepath.Join(mayorDir, "CLAUDE.md") + return os.WriteFile(claudePath, []byte(content), 0644) +} + // GetAllRoleTemplates returns all role templates as a map of filename to content. func GetAllRoleTemplates() (map[string][]byte, error) { entries, err := templateFS.ReadDir("roles")