From 432d14d9df7ccf1ae0f25dd66d454ea363b4bcee Mon Sep 17 00:00:00 2001 From: julianknutsen Date: Tue, 6 Jan 2026 15:18:08 -0800 Subject: [PATCH] Install Claude settings during rig and HQ creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creates settings.json automatically during initial setup, so Claude settings are available immediately on launch. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- docs/reference.md | 99 ++++++++++++++++++++++++--------- internal/cmd/install.go | 68 +++++++++++----------- internal/templates/templates.go | 26 --------- 3 files changed, 108 insertions(+), 85 deletions(-) diff --git a/docs/reference.md b/docs/reference.md index 951d3a10..bad44f05 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -7,24 +7,38 @@ Technical reference for Gas Town internals. Read the README first. ``` ~/gt/ Town root β”œβ”€β”€ .beads/ Town-level beads (hq-* prefix) -β”œβ”€β”€ mayor/ Mayor config -β”‚ └── town.json +β”œβ”€β”€ mayor/ Mayor agent home (town coordinator) +β”‚ β”œβ”€β”€ town.json Town configuration +β”‚ β”œβ”€β”€ CLAUDE.md Mayor context (on disk) +β”‚ └── .claude/settings.json Mayor Claude settings +β”œβ”€β”€ deacon/ Deacon agent home (background supervisor) +β”‚ └── .claude/settings.json Deacon settings (context via gt prime) └── / Project container (NOT a git clone) β”œβ”€β”€ config.json Rig identity β”œβ”€β”€ .beads/ β†’ mayor/rig/.beads β”œβ”€β”€ .repo.git/ Bare repo (shared by worktrees) β”œβ”€β”€ mayor/rig/ Mayor's clone (canonical beads) - β”œβ”€β”€ refinery/rig/ Worktree on main - β”œβ”€β”€ witness/ No clone (monitors only) - β”œβ”€β”€ crew// Human workspaces - └── polecats// Worker worktrees + β”‚ └── CLAUDE.md Per-rig mayor context (on disk) + β”œβ”€β”€ witness/ Witness agent home (monitors only) + β”‚ └── .claude/settings.json (context via gt prime) + β”œβ”€β”€ refinery/ Refinery settings parent + β”‚ β”œβ”€β”€ .claude/settings.json + β”‚ └── rig/ Worktree on main + β”‚ └── CLAUDE.md Refinery context (on disk) + β”œβ”€β”€ crew/ Crew settings parent (shared) + β”‚ β”œβ”€β”€ .claude/settings.json (context via gt prime) + β”‚ └── /rig/ Human workspaces + └── polecats/ Polecat settings parent (shared) + β”œβ”€β”€ .claude/settings.json (context via gt prime) + └── /rig/ Worker worktrees ``` **Key points:** - Rig root is a container, not a clone - `.repo.git/` is bare - refinery and polecats are worktrees -- Mayor clone holds canonical `.beads/`, others inherit via redirect +- Per-rig `mayor/rig/` holds canonical `.beads/`, others inherit via redirect +- Settings placed in parent dirs (not git clones) for upward traversal ## Beads Routing @@ -213,46 +227,81 @@ Understanding this hierarchy is essential for proper configuration. | Role | Working Directory | Notes | |------|-------------------|-------| -| **Mayor** | `~/gt/mayor/` | Isolated from child agents | -| **Deacon** | `~/gt/deacon/` | Background supervisor | -| **Witness** | `~/gt//witness/` | No git clone, monitors only | +| **Mayor** | `~/gt/mayor/` | Town-level coordinator, isolated from rigs | +| **Deacon** | `~/gt/deacon/` | Background supervisor daemon | +| **Witness** | `~/gt//witness/` | No git clone, monitors polecats only | | **Refinery** | `~/gt//refinery/rig/` | Worktree on main branch | -| **Crew** | `~/gt//crew//rig/` | Persistent clone | -| **Polecat** | `~/gt//polecats//rig/` | Ephemeral worktree | +| **Crew** | `~/gt//crew//rig/` | Persistent human workspace clone | +| **Polecat** | `~/gt//polecats//rig/` | Ephemeral worker worktree | + +Note: The per-rig `/mayor/rig/` directory is NOT a working directoryβ€”it's +a git clone that holds the canonical `.beads/` database for that rig. ### 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: +directory and traversing upward. Settings are placed in **parent directories** +(not inside git clones) so they're found via directory traversal without +polluting source repositories: ``` ~/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 + β”œβ”€β”€ witness/.claude/settings.json # Witness settings (no rig/ subdir) + β”œβ”€β”€ refinery/.claude/settings.json # Found by refinery/rig/ via traversal + β”œβ”€β”€ crew/.claude/settings.json # Shared by all crew//rig/ + └── polecats/.claude/settings.json # Shared by all polecats//rig/ ``` -**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. +**Why parent directories?** Agents working in git clones (like `refinery/rig/`) +would pollute the source repo if settings were placed there. By putting settings +one level up, Claude finds them via upward traversal, and all workers of the +same type share the same settings. + +### CLAUDE.md Locations + +Role context is delivered via CLAUDE.md files or ephemeral injection: + +| Role | CLAUDE.md Location | Method | +|------|-------------------|--------| +| **Mayor** | `~/gt/mayor/CLAUDE.md` | On disk | +| **Deacon** | (none) | Injected via `gt prime` at SessionStart | +| **Witness** | (none) | Injected via `gt prime` at SessionStart | +| **Refinery** | `/refinery/rig/CLAUDE.md` | On disk (inside worktree) | +| **Crew** | (none) | Injected via `gt prime` at SessionStart | +| **Polecat** | (none) | Injected via `gt prime` at SessionStart | + +Additionally, each rig has `/mayor/rig/CLAUDE.md` for the per-rig mayor clone +(used for beads operations, not a running agent). + +**Why ephemeral injection?** Writing CLAUDE.md into git clones would: +1. Pollute source repos when agents commit/push +2. Leak Gas Town internals into project history +3. Conflict with project-specific CLAUDE.md files + +The `gt prime` command runs at SessionStart hook and injects context without +persisting it to disk. ### 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: +When agents work on source repositories that have their own Claude Code configuration, +Gas Town uses git sparse checkout to exclude all context files: ```bash -# Automatically configured for worktrees -git sparse-checkout set --no-cone '/*' '!/.claude/' +# Automatically configured for worktrees - excludes: +# - .claude/ : settings, rules, agents, commands +# - CLAUDE.md : primary context file +# - CLAUDE.local.md: personal context file +# - .mcp.json : MCP server configuration +git sparse-checkout set --no-cone '/*' '!/.claude/' '!/CLAUDE.md' '!/CLAUDE.local.md' '!/.mcp.json' ``` -This ensures the agent uses Gas Town's settings, not the source repo's. +This ensures agents use Gas Town's context, not the source repo's instructions. **Doctor check**: `gt doctor` verifies sparse checkout is configured correctly. +Run `gt doctor --fix` to update legacy configurations missing the newer patterns. ### Settings Inheritance diff --git a/internal/cmd/install.go b/internal/cmd/install.go index 72c79794..57343187 100644 --- a/internal/cmd/install.go +++ b/internal/cmd/install.go @@ -172,20 +172,37 @@ func runInstall(cmd *cobra.Command, args []string) error { } fmt.Printf(" βœ“ Created mayor/rigs.json\n") - // Create Mayor CLAUDE.md at HQ root (Mayor runs from there) - if err := createMayorCLAUDEmd(absPath, absPath); err != nil { + // Create Mayor CLAUDE.md at mayor/ (Mayor's canonical home) + // IMPORTANT: CLAUDE.md must be in ~/gt/mayor/, NOT ~/gt/ + // CLAUDE.md at town root would be inherited by ALL agents via directory traversal, + // causing crew/polecat/etc to receive Mayor-specific instructions. + if err := createMayorCLAUDEmd(mayorDir, absPath); err != nil { fmt.Printf(" %s Could not create CLAUDE.md: %v\n", style.Dim.Render("⚠"), err) } else { - fmt.Printf(" βœ“ Created CLAUDE.md\n") + fmt.Printf(" βœ“ Created mayor/CLAUDE.md\n") } - // Ensure Mayor has Claude settings with SessionStart hooks. - // This ensures gt prime runs on Claude startup, which outputs the Mayor - // delegation protocol - critical for preventing direct implementation. - if err := claude.EnsureSettingsForRole(absPath, "mayor"); err != nil { - fmt.Printf(" %s Could not create .claude/settings.json: %v\n", style.Dim.Render("⚠"), err) + // Create mayor settings (mayor runs from ~/gt/mayor/) + // IMPORTANT: Settings must be in ~/gt/mayor/.claude/, NOT ~/gt/.claude/ + // Settings at town root would be found by ALL agents via directory traversal, + // causing crew/polecat/etc to cd to town root before running commands. + // mayorDir already defined above + if err := os.MkdirAll(mayorDir, 0755); err != nil { + fmt.Printf(" %s Could not create mayor directory: %v\n", style.Dim.Render("⚠"), err) + } else if err := claude.EnsureSettingsForRole(mayorDir, "mayor"); err != nil { + fmt.Printf(" %s Could not create mayor settings: %v\n", style.Dim.Render("⚠"), err) } else { - fmt.Printf(" βœ“ Created .claude/settings.json\n") + fmt.Printf(" βœ“ Created mayor/.claude/settings.json\n") + } + + // Create deacon directory and settings (deacon runs from ~/gt/deacon/) + deaconDir := filepath.Join(absPath, "deacon") + if err := os.MkdirAll(deaconDir, 0755); err != nil { + fmt.Printf(" %s Could not create deacon directory: %v\n", style.Dim.Render("⚠"), err) + } else if err := claude.EnsureSettingsForRole(deaconDir, "deacon"); err != nil { + fmt.Printf(" %s Could not create deacon settings: %v\n", style.Dim.Render("⚠"), err) + } else { + fmt.Printf(" βœ“ Created deacon/.claude/settings.json\n") } // Initialize git BEFORE beads so that bd can compute repository fingerprint. @@ -260,32 +277,15 @@ func runInstall(cmd *cobra.Command, args []string) error { return nil } -func createMayorCLAUDEmd(hqRoot, townRoot string) error { - tmpl, err := templates.New() - if err != nil { - return err - } - - // Get town name for session names +func createMayorCLAUDEmd(mayorDir, townRoot string) error { townName, _ := workspace.GetTownName(townRoot) - - data := templates.RoleData{ - Role: "mayor", - TownRoot: townRoot, - TownName: townName, - WorkDir: hqRoot, - DefaultBranch: "main", // Mayor doesn't merge, but field required - MayorSession: session.MayorSessionName(), - DeaconSession: session.DeaconSessionName(), - } - - content, err := tmpl.RenderRole("mayor", data) - if err != nil { - return err - } - - claudePath := filepath.Join(hqRoot, "CLAUDE.md") - return os.WriteFile(claudePath, []byte(content), 0644) + return templates.CreateMayorCLAUDEmd( + mayorDir, + townRoot, + townName, + session.MayorSessionName(), + session.DeaconSessionName(), + ) } func writeJSON(path string, data interface{}) error { diff --git a/internal/templates/templates.go b/internal/templates/templates.go index ac9063ae..294c6143 100644 --- a/internal/templates/templates.go +++ b/internal/templates/templates.go @@ -241,32 +241,6 @@ func CommandNames() ([]string, error) { return names, nil } -// 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) -} - // HasCommands checks if a workspace has the .claude/commands/ directory provisioned. func HasCommands(workspacePath string) bool { commandsDir := filepath.Join(workspacePath, ".claude", "commands")