Install Claude settings during rig and HQ creation

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 <noreply@anthropic.com>
This commit is contained in:
julianknutsen
2026-01-06 15:18:08 -08:00
parent b7b8e141b1
commit 432d14d9df
3 changed files with 108 additions and 85 deletions

View File

@@ -7,24 +7,38 @@ Technical reference for Gas Town internals. Read the README first.
``` ```
~/gt/ Town root ~/gt/ Town root
├── .beads/ Town-level beads (hq-* prefix) ├── .beads/ Town-level beads (hq-* prefix)
├── mayor/ Mayor config ├── mayor/ Mayor agent home (town coordinator)
── town.json ── 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)
└── <rig>/ Project container (NOT a git clone) └── <rig>/ Project container (NOT a git clone)
├── config.json Rig identity ├── config.json Rig identity
├── .beads/ → mayor/rig/.beads ├── .beads/ → mayor/rig/.beads
├── .repo.git/ Bare repo (shared by worktrees) ├── .repo.git/ Bare repo (shared by worktrees)
├── mayor/rig/ Mayor's clone (canonical beads) ├── mayor/rig/ Mayor's clone (canonical beads)
├── refinery/rig/ Worktree on main │ └── CLAUDE.md Per-rig mayor context (on disk)
├── witness/ No clone (monitors only) ├── witness/ Witness agent home (monitors only)
├── crew/<name>/ Human workspaces │ └── .claude/settings.json (context via gt prime)
── polecats/<name>/ Worker worktrees ── 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)
│ └── <name>/rig/ Human workspaces
└── polecats/ Polecat settings parent (shared)
├── .claude/settings.json (context via gt prime)
└── <name>/rig/ Worker worktrees
``` ```
**Key points:** **Key points:**
- Rig root is a container, not a clone - Rig root is a container, not a clone
- `.repo.git/` is bare - refinery and polecats are worktrees - `.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 ## Beads Routing
@@ -213,46 +227,81 @@ Understanding this hierarchy is essential for proper configuration.
| Role | Working Directory | Notes | | Role | Working Directory | Notes |
|------|-------------------|-------| |------|-------------------|-------|
| **Mayor** | `~/gt/mayor/` | Isolated from child agents | | **Mayor** | `~/gt/mayor/` | Town-level coordinator, isolated from rigs |
| **Deacon** | `~/gt/deacon/` | Background supervisor | | **Deacon** | `~/gt/deacon/` | Background supervisor daemon |
| **Witness** | `~/gt/<rig>/witness/` | No git clone, monitors only | | **Witness** | `~/gt/<rig>/witness/` | No git clone, monitors polecats only |
| **Refinery** | `~/gt/<rig>/refinery/rig/` | Worktree on main branch | | **Refinery** | `~/gt/<rig>/refinery/rig/` | Worktree on main branch |
| **Crew** | `~/gt/<rig>/crew/<name>/rig/` | Persistent clone | | **Crew** | `~/gt/<rig>/crew/<name>/rig/` | Persistent human workspace clone |
| **Polecat** | `~/gt/<rig>/polecats/<name>/rig/` | Ephemeral worktree | | **Polecat** | `~/gt/<rig>/polecats/<name>/rig/` | Ephemeral worker worktree |
Note: The per-rig `<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 ### Settings File Locations
Claude Code searches for `.claude/settings.json` starting from the working 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/ ~/gt/
├── mayor/.claude/settings.json # Mayor settings ├── mayor/.claude/settings.json # Mayor settings
├── deacon/.claude/settings.json # Deacon settings ├── deacon/.claude/settings.json # Deacon settings
└── <rig>/ └── <rig>/
├── witness/.claude/settings.json # Witness settings ├── witness/.claude/settings.json # Witness settings (no rig/ subdir)
├── refinery/rig/.claude/settings.json ├── refinery/.claude/settings.json # Found by refinery/rig/ via traversal
├── crew/<name>/rig/.claude/settings.json ├── crew/.claude/settings.json # Shared by all crew/<name>/rig/
└── polecats/<name>/rig/.claude/settings.json └── polecats/.claude/settings.json # Shared by all polecats/<name>/rig/
``` ```
**Why this structure?** Child agents inherit the parent's working directory **Why parent directories?** Agents working in git clones (like `refinery/rig/`)
when spawned. By keeping each role's files in separate directories, we prevent would pollute the source repo if settings were placed there. By putting settings
the mayor's CLAUDE.md or settings from affecting polecat behavior. 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** | `<rig>/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 `<rig>/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) ### Sparse Checkout (Source Repo Isolation)
When agents work on source repositories that have their own `.claude/` directory, When agents work on source repositories that have their own Claude Code configuration,
Gas Town uses git sparse checkout to exclude it: Gas Town uses git sparse checkout to exclude all context files:
```bash ```bash
# Automatically configured for worktrees # Automatically configured for worktrees - excludes:
git sparse-checkout set --no-cone '/*' '!/.claude/' # - .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. **Doctor check**: `gt doctor` verifies sparse checkout is configured correctly.
Run `gt doctor --fix` to update legacy configurations missing the newer patterns.
### Settings Inheritance ### Settings Inheritance

View File

@@ -172,20 +172,37 @@ func runInstall(cmd *cobra.Command, args []string) error {
} }
fmt.Printf(" ✓ Created mayor/rigs.json\n") fmt.Printf(" ✓ Created mayor/rigs.json\n")
// Create Mayor CLAUDE.md at HQ root (Mayor runs from there) // Create Mayor CLAUDE.md at mayor/ (Mayor's canonical home)
if err := createMayorCLAUDEmd(absPath, absPath); err != nil { // 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) fmt.Printf(" %s Could not create CLAUDE.md: %v\n", style.Dim.Render("⚠"), err)
} else { } else {
fmt.Printf(" ✓ Created CLAUDE.md\n") fmt.Printf(" ✓ Created mayor/CLAUDE.md\n")
} }
// Ensure Mayor has Claude settings with SessionStart hooks. // Create mayor settings (mayor runs from ~/gt/mayor/)
// This ensures gt prime runs on Claude startup, which outputs the Mayor // IMPORTANT: Settings must be in ~/gt/mayor/.claude/, NOT ~/gt/.claude/
// delegation protocol - critical for preventing direct implementation. // Settings at town root would be found by ALL agents via directory traversal,
if err := claude.EnsureSettingsForRole(absPath, "mayor"); err != nil { // causing crew/polecat/etc to cd to town root before running commands.
fmt.Printf(" %s Could not create .claude/settings.json: %v\n", style.Dim.Render("⚠"), err) // 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 { } 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. // 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 return nil
} }
func createMayorCLAUDEmd(hqRoot, townRoot string) error { func createMayorCLAUDEmd(mayorDir, townRoot string) error {
tmpl, err := templates.New()
if err != nil {
return err
}
// Get town name for session names
townName, _ := workspace.GetTownName(townRoot) townName, _ := workspace.GetTownName(townRoot)
return templates.CreateMayorCLAUDEmd(
data := templates.RoleData{ mayorDir,
Role: "mayor", townRoot,
TownRoot: townRoot, townName,
TownName: townName, session.MayorSessionName(),
WorkDir: hqRoot, session.DeaconSessionName(),
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)
} }
func writeJSON(path string, data interface{}) error { func writeJSON(path string, data interface{}) error {

View File

@@ -241,32 +241,6 @@ func CommandNames() ([]string, error) {
return names, nil 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. // HasCommands checks if a workspace has the .claude/commands/ directory provisioned.
func HasCommands(workspacePath string) bool { func HasCommands(workspacePath string) bool {
commandsDir := filepath.Join(workspacePath, ".claude", "commands") commandsDir := filepath.Join(workspacePath, ".claude", "commands")