# Town Management Design Design for `gt install`, `gt doctor`, and federation in the Gas Town Go port. ## Overview A **Town** is a complete Gas Town installation containing: - Town config (`config/` directory - VISIBLE, not hidden) - Mayor's home (`mayor/` directory) - Rigs (managed project clones) - Per-rig agents (witness/, refinery/, polecats/, mayor/) - Mail system - Beads integration ## Architecture Decision: Decentralized Agents (gt-iib) Each rig contains ALL its agents rather than centralizing at town level: - Mayor's clone lives at `/mayor/rig/` (not `mayor/rigs//`) - Witness (pit boss) at `/witness/rig/` - NEW in GGT - Refinery at `/refinery/rig/` - Polecats at `/polecats//` ## Directory Structure ### Town Level ``` ~/ai/ # Town root (e.g., stevey-gastown repo) ├── config/ # Town config (VISIBLE, not hidden!) │ ├── town.json # {"type": "town", "name": "..."} │ ├── rigs.json # Registry of managed rigs │ └── federation.json # Wasteland config (future) │ ├── mayor/ # Mayor's HOME at town level │ ├── CLAUDE.md # Mayor role context │ ├── mail/ │ │ └── inbox.jsonl # Mayor's inbox │ └── state.json │ ├── wyvern/ # Rig (see below) └── beads/ # Another rig ``` ### Rig Level (e.g., wyvern) ``` wyvern/ # Rig = clone of project repo ├── .git/ │ └── info/exclude # Gas Town adds: polecats/ refinery/ witness/ mayor/ ├── .beads/ # Beads (if project uses it) ├── [project files] # Clean project code on main branch │ ├── polecats/ # Worker clones (gitignored via exclude) │ ├── Nux/ # git clone of wyvern │ └── Toast/ │ ├── refinery/ # Refinery agent │ ├── rig/ # Refinery's clone │ ├── state.json │ └── mail/inbox.jsonl │ ├── witness/ # Witness agent (per-rig pit boss) - NEW │ ├── rig/ # Witness's clone │ ├── state.json │ └── mail/inbox.jsonl │ └── mayor/ # Mayor's presence in this rig ├── rig/ # Mayor's clone for rig-specific edits └── state.json ``` ### Minimal Rig Invasiveness Gas Town is a harness OVER projects. When adding a rig: 1. Clone project to town: `gt rig add ` 2. Add to `.git/info/exclude`: `polecats/`, `refinery/`, `witness/`, `mayor/` 3. Create agent directories **The project repo is NEVER modified.** No commits needed. ## Config Files ### config/town.json ```json { "type": "town", "version": 1, "name": "stevey-gastown", "created_at": "2024-01-15T10:30:00Z" } ``` ### config/rigs.json ```json { "version": 1, "rigs": { "wyvern": { "git_url": "https://github.com/steveyegge/wyvern", "added_at": "2024-01-15T10:30:00Z" }, "beads": { "git_url": "https://github.com/steveyegge/beads", "added_at": "2024-01-15T10:30:00Z" } } } ``` ### config/federation.json (future) ```json { "version": 1, "wasteland": null, "peers": [] } ``` ### Agent state.json (refinery/, witness/, mayor/) ```json { "version": 1, "state": "stopped", "awake": false, "created_at": "2024-01-15T10:30:00Z", "last_started": null, "last_stopped": null, "last_wake": null, "last_sleep": null } ``` ## gt install ### Command ```bash gt install [path] # Default: current directory gt install ~/ai ``` ### Behavior 1. **Check if already installed**: Look for `mayor/` or `.gastown/` with `type: "workspace"` 2. **Create workspace structure**: - `mayor/config.json` - workspace identity - `mayor/state.json` - workspace state - `mayor/mail/` - mail directory - `mayor/boss/state.json` - boss state - `mayor/rigs/` - empty, populated when rigs are added 3. **Create .gitignore** - ignore ephemeral state, polecat clones 4. **Create CLAUDE.md** - Mayor instructions 5. **Initialize git** if not present ### Implementation (Go) ```go // pkg/workspace/install.go package workspace type InstallOptions struct { Path string Force bool // Overwrite existing } func Install(opts InstallOptions) (*Workspace, error) { path := opts.Path if path == "" { path = "." } // Resolve and validate absPath, err := filepath.Abs(path) if err != nil { return nil, fmt.Errorf("invalid path: %w", err) } // Check existing if ws, _ := Find(absPath); ws != nil && !opts.Force { return nil, ErrAlreadyInstalled } // Create structure mayorDir := filepath.Join(absPath, "mayor") if err := os.MkdirAll(mayorDir, 0755); err != nil { return nil, err } // Write config config := Config{ Type: "workspace", Version: 1, CreatedAt: time.Now().UTC(), } if err := writeJSON(filepath.Join(mayorDir, "config.json"), config); err != nil { return nil, err } // ... create state, mail, boss, rigs return &Workspace{Path: absPath, Config: config}, nil } ``` ## gt doctor ### Command ```bash gt doctor # Check workspace health gt doctor --fix # Auto-fix issues gt doctor # Check specific rig ``` ### Checks #### Workspace Level 1. **Workspace exists**: `mayor/` or `.gastown/` directory 2. **Valid config**: `config.json` has `type: "workspace"` 3. **State file**: `state.json` exists and is valid JSON 4. **Mail directory**: `mail/` exists 5. **Boss state**: `boss/state.json` exists 6. **Rigs directory**: `rigs/` exists #### Per-Rig Checks 1. **Refinery directory**: `/refinery/` exists 2. **Refinery README**: `refinery/README.md` exists 3. **Refinery state**: `refinery/state.json` exists 4. **Refinery lock**: `refinery/state.json.lock` exists 5. **Refinery clone**: `refinery/rig/` has valid `.git` 6. **Boss rig clone**: `mayor/rigs//` has valid `.git` 7. **Gitignore entries**: workspace `.gitignore` has rig patterns ### Output Format ``` $ gt doctor Workspace: ~/ai ✓ Workspace config valid ✓ Workspace state valid ✓ Mail directory exists ✓ Boss state valid Rig: gastown ✓ Refinery directory exists ✓ Refinery README exists ✓ Refinery state valid ✗ Missing refinery/rig/ clone ✓ Mayor rig clone exists ✓ Gitignore entries present Rig: beads ✓ Refinery directory exists ✓ Refinery README exists ✓ Refinery state valid ✓ Refinery clone valid ✓ Mayor rig clone exists ✓ Gitignore entries present Issues: 1 found, 0 fixed Run with --fix to auto-repair ``` ### Implementation (Go) ```go // pkg/doctor/doctor.go package doctor type CheckResult struct { Name string Status Status // Pass, Fail, Warn Message string Fixable bool } type DoctorOptions struct { Fix bool Rig string // Empty = all rigs Verbose bool } func Run(ws *workspace.Workspace, opts DoctorOptions) (*Report, error) { report := &Report{} // Workspace checks report.Add(checkWorkspaceConfig(ws)) report.Add(checkWorkspaceState(ws)) report.Add(checkMailDir(ws)) report.Add(checkBossState(ws)) report.Add(checkRigsDir(ws)) // Per-rig checks rigs, _ := ws.ListRigs() for _, rig := range rigs { if opts.Rig != "" && rig.Name != opts.Rig { continue } report.AddRig(rig.Name, checkRig(rig, ws, opts.Fix)) } return report, nil } func checkRefineryHealth(rig *rig.Rig, fix bool) []CheckResult { var results []CheckResult refineryDir := filepath.Join(rig.Path, "refinery") // Check refinery directory if !dirExists(refineryDir) { r := CheckResult{Name: "Refinery directory", Status: Fail, Fixable: true} if fix { if err := os.MkdirAll(refineryDir, 0755); err == nil { r.Status = Fixed } } results = append(results, r) } // Check README.md readmePath := filepath.Join(refineryDir, "README.md") if !fileExists(readmePath) { r := CheckResult{Name: "Refinery README", Status: Fail, Fixable: true} if fix { if err := writeRefineryReadme(readmePath); err == nil { r.Status = Fixed } } results = append(results, r) } // ... more checks return results } ``` ## Workspace Detection Find workspace root by walking up from current directory: ```go // pkg/workspace/find.go func Find(startPath string) (*Workspace, error) { current, _ := filepath.Abs(startPath) for current != filepath.Dir(current) { // Check both "mayor" and ".gastown" directories for _, dirName := range []string{"mayor", ".gastown"} { configDir := filepath.Join(current, dirName) configPath := filepath.Join(configDir, "config.json") if fileExists(configPath) { var config Config if err := readJSON(configPath, &config); err != nil { continue } if config.Type == "workspace" { return &Workspace{ Path: current, ConfigDir: dirName, Config: config, }, nil } } } current = filepath.Dir(current) } return nil, ErrNotFound } ``` ## Minimal Federation Protocol Federation enables work distribution across multiple machines via SSH. ### Core Abstractions #### Connection Interface ```go // pkg/connection/connection.go type Connection interface { // Command execution Execute(ctx context.Context, cmd string, opts ExecOpts) (*Result, error) // File operations ReadFile(path string) ([]byte, error) WriteFile(path string, data []byte) error AppendFile(path string, data []byte) error FileExists(path string) (bool, error) ListDir(path string) ([]string, error) MkdirAll(path string) error // Tmux operations TmuxSend(session string, text string) error TmuxCapture(session string, lines int) (string, error) TmuxHasSession(session string) (bool, error) // Health IsHealthy() bool } ``` #### LocalConnection ```go // pkg/connection/local.go type LocalConnection struct{} func (c *LocalConnection) Execute(ctx context.Context, cmd string, opts ExecOpts) (*Result, error) { // Direct exec.Command } func (c *LocalConnection) ReadFile(path string) ([]byte, error) { return os.ReadFile(path) } ``` #### SSHConnection ```go // pkg/connection/ssh.go type SSHConnection struct { Host string User string KeyPath string client *ssh.Client } func (c *SSHConnection) Execute(ctx context.Context, cmd string, opts ExecOpts) (*Result, error) { session, err := c.client.NewSession() if err != nil { return nil, err } defer session.Close() // Run command via SSH } ``` ### Machine Registry ```go // pkg/federation/registry.go type Machine struct { Name string Type string // "local", "ssh", "gcp" Workspace string // Remote workspace path SSHHost string SSHUser string SSHKeyPath string GCPProject string GCPZone string GCPInstance string } type Registry struct { machines map[string]*Machine conns map[string]Connection } func (r *Registry) GetConnection(name string) (Connection, error) { if conn, ok := r.conns[name]; ok { return conn, nil } machine, ok := r.machines[name] if !ok { return nil, ErrMachineNotFound } var conn Connection switch machine.Type { case "local": conn = &LocalConnection{} case "ssh", "gcp": conn = NewSSHConnection(machine.SSHHost, machine.SSHUser, machine.SSHKeyPath) } r.conns[name] = conn return conn, nil } ``` ### Extended Addressing Polecat addresses support optional machine prefix: ``` [machine:]rig/polecat Examples: beads/happy # Local machine (default) gcp-west:beads/happy # Remote machine ``` ```go // pkg/identity/address.go type PolecatAddress struct { Machine string // Default: "local" Rig string Polecat string } func ParseAddress(addr string) (*PolecatAddress, error) { parts := strings.SplitN(addr, ":", 2) if len(parts) == 2 { // machine:rig/polecat machine := parts[0] rigPolecat := strings.SplitN(parts[1], "/", 2) return &PolecatAddress{Machine: machine, Rig: rigPolecat[0], Polecat: rigPolecat[1]}, nil } // rig/polecat (local default) rigPolecat := strings.SplitN(addr, "/", 2) return &PolecatAddress{Machine: "local", Rig: rigPolecat[0], Polecat: rigPolecat[1]}, nil } ``` ### Mail Routing For federation < 50 agents, use centralized mail through Mayor's machine: ```go // pkg/mail/router.go type MailRouter struct { registry *federation.Registry } func (r *MailRouter) Deliver(msg *Message) error { addr, _ := identity.ParseAddress(msg.Recipient) conn, err := r.registry.GetConnection(addr.Machine) if err != nil { return err } mailboxPath := filepath.Join(addr.Rig, addr.Polecat, "mail", "inbox.jsonl") return conn.AppendFile(mailboxPath, msg.ToJSONL()) } ``` ## Implementation Plan ### Subtasks for gt-evp2 1. **Config package** - Config, State types and JSON serialization 2. **Workspace detection** - Find() walking up directory tree 3. **gt install command** - Create workspace structure 4. **Doctor framework** - Check interface, Result types, Report 5. **Workspace doctor checks** - Config, state, mail, boss, rigs 6. **Rig doctor checks** - Refinery health, clones, gitignore 7. **Connection interface** - Define protocol for local/remote ops 8. **LocalConnection** - Local file/exec/tmux operations 9. **Machine registry** - Store and manage machine configs 10. **Extended addressing** - Parse `[machine:]rig/polecat` ### Deferred (Federation Phase 2) - SSHConnection implementation - GCPConnection with gcloud integration - Cross-machine mail routing - Remote session management - Worker pool across machines ## CLI Commands Summary ```bash # Installation gt install [path] # Install workspace at path gt install --force # Overwrite existing # Diagnostics gt doctor # Check workspace health gt doctor --fix # Auto-fix issues gt doctor # Check specific rig gt doctor --verbose # Show all checks (not just failures) # Future (federation) gt machine list # List machines gt machine add # Add machine gt machine status # Check all machine health ```