commit 4c782bc59de8cba4a1cf1632aa6adaaac47e50ca Author: Steve Yegge Date: Mon Dec 15 16:38:23 2025 -0800 Initial commit: Go port scaffolding - Go project structure (go.mod, cmd/gt/main.go) - Beads database initialized with gt- prefix - Town management design doc (docs/town-design.md) - Basic README and CLAUDE.md Epics tracked: - gt-u1j: Port Gas Town to Go - gt-f9x: Town & Rig Management 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 diff --git a/.beads/.gitignore b/.beads/.gitignore new file mode 100644 index 00000000..374adb81 --- /dev/null +++ b/.beads/.gitignore @@ -0,0 +1,32 @@ +# SQLite databases +*.db +*.db?* +*.db-journal +*.db-wal +*.db-shm + +# Daemon runtime files +daemon.lock +daemon.log +daemon.pid +bd.sock + +# Local version tracking (prevents upgrade notification spam after git ops) +.local_version + +# Legacy database files +db.sqlite +bd.db + +# Merge artifacts (temporary files from 3-way merge) +beads.base.jsonl +beads.base.meta.json +beads.left.jsonl +beads.left.meta.json +beads.right.jsonl +beads.right.meta.json + +# Keep JSONL exports and config (source of truth for git) +!issues.jsonl +!metadata.json +!config.json diff --git a/.beads/README.md b/.beads/README.md new file mode 100644 index 00000000..50f281f0 --- /dev/null +++ b/.beads/README.md @@ -0,0 +1,81 @@ +# Beads - AI-Native Issue Tracking + +Welcome to Beads! This repository uses **Beads** for issue tracking - a modern, AI-native tool designed to live directly in your codebase alongside your code. + +## What is Beads? + +Beads is issue tracking that lives in your repo, making it perfect for AI coding agents and developers who want their issues close to their code. No web UI required - everything works through the CLI and integrates seamlessly with git. + +**Learn more:** [github.com/steveyegge/beads](https://github.com/steveyegge/beads) + +## Quick Start + +### Essential Commands + +```bash +# Create new issues +bd create "Add user authentication" + +# View all issues +bd list + +# View issue details +bd show + +# Update issue status +bd update --status in_progress +bd update --status done + +# Sync with git remote +bd sync +``` + +### Working with Issues + +Issues in Beads are: +- **Git-native**: Stored in `.beads/issues.jsonl` and synced like code +- **AI-friendly**: CLI-first design works perfectly with AI coding agents +- **Branch-aware**: Issues can follow your branch workflow +- **Always in sync**: Auto-syncs with your commits + +## Why Beads? + +✨ **AI-Native Design** +- Built specifically for AI-assisted development workflows +- CLI-first interface works seamlessly with AI coding agents +- No context switching to web UIs + +🚀 **Developer Focused** +- Issues live in your repo, right next to your code +- Works offline, syncs when you push +- Fast, lightweight, and stays out of your way + +🔧 **Git Integration** +- Automatic sync with git commits +- Branch-aware issue tracking +- Intelligent JSONL merge resolution + +## Get Started with Beads + +Try Beads in your own projects: + +```bash +# Install Beads +curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash + +# Initialize in your repo +bd init + +# Create your first issue +bd create "Try out Beads" +``` + +## Learn More + +- **Documentation**: [github.com/steveyegge/beads/docs](https://github.com/steveyegge/beads/tree/main/docs) +- **Quick Start Guide**: Run `bd quickstart` +- **Examples**: [github.com/steveyegge/beads/examples](https://github.com/steveyegge/beads/tree/main/examples) + +--- + +*Beads: Issue tracking that moves at the speed of thought* ⚡ diff --git a/.beads/config.yaml b/.beads/config.yaml new file mode 100644 index 00000000..39dcf7c4 --- /dev/null +++ b/.beads/config.yaml @@ -0,0 +1,63 @@ +# Beads Configuration File +# This file configures default behavior for all bd commands in this repository +# All settings can also be set via environment variables (BD_* prefix) +# or overridden with command-line flags + +# Issue prefix for this repository (used by bd init) +# If not set, bd init will auto-detect from directory name +# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc. +# issue-prefix: "" + +# Use no-db mode: load from JSONL, no SQLite, write back after each command +# When true, bd will use .beads/issues.jsonl as the source of truth +# instead of SQLite database +# no-db: false + +# Disable daemon for RPC communication (forces direct database access) +# no-daemon: false + +# Disable auto-flush of database to JSONL after mutations +# no-auto-flush: false + +# Disable auto-import from JSONL when it's newer than database +# no-auto-import: false + +# Enable JSON output by default +# json: false + +# Default actor for audit trails (overridden by BD_ACTOR or --actor) +# actor: "" + +# Path to database (overridden by BEADS_DB or --db) +# db: "" + +# Auto-start daemon if not running (can also use BEADS_AUTO_START_DAEMON) +# auto-start-daemon: true + +# Debounce interval for auto-flush (can also use BEADS_FLUSH_DEBOUNCE) +# flush-debounce: "5s" + +# Git branch for beads commits (bd sync will commit to this branch) +# IMPORTANT: Set this for team projects so all clones use the same sync branch. +# This setting persists across clones (unlike database config which is gitignored). +# Can also use BEADS_SYNC_BRANCH env var for local override. +# If not set, bd sync will require you to run 'bd config set sync.branch '. +# sync-branch: "beads-sync" + +# Multi-repo configuration (experimental - bd-307) +# Allows hydrating from multiple repositories and routing writes to the correct JSONL +# repos: +# primary: "." # Primary repo (where this database lives) +# additional: # Additional repos to hydrate from (read-only) +# - ~/beads-planning # Personal planning repo +# - ~/work-planning # Work planning repo + +# Integration settings (access with 'bd config get/set') +# These are stored in the database, not in this file: +# - jira.url +# - jira.project +# - linear.url +# - linear.api-key +# - github.org +# - github.repo +sync-branch: beads-sync diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl new file mode 100644 index 00000000..561c7c76 --- /dev/null +++ b/.beads/issues.jsonl @@ -0,0 +1,13 @@ +{"id":"gt-f9x","title":"Town \u0026 Rig Management: install, doctor, federation","description":"Reify the Gas Town installation as a first-class concept.\n\n## Goals\n- Installable: gt install [path] creates complete installation\n- Diagnosable: gt doctor checks and fixes issues\n- Federable: Clone town to VMs with central control\n\n## Design Doc\nSee docs/town-design.md for full design.","status":"open","priority":1,"issue_type":"epic","created_at":"2025-12-15T16:36:37.344283-08:00","updated_at":"2025-12-15T16:36:37.344283-08:00","dependencies":[{"issue_id":"gt-f9x","depends_on_id":"gt-u1j.1","type":"blocks","created_at":"2025-12-15T16:37:32.3363-08:00","created_by":"daemon"}]} +{"id":"gt-f9x.1","title":"Config package: Config, State types and JSON serialization","description":"Define workspace and rig config structures","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-15T16:36:50.163851-08:00","updated_at":"2025-12-15T16:36:50.163851-08:00","dependencies":[{"issue_id":"gt-f9x.1","depends_on_id":"gt-f9x","type":"parent-child","created_at":"2025-12-15T16:36:50.164178-08:00","created_by":"daemon"}]} +{"id":"gt-f9x.10","title":"Extended addressing: Parse [machine:]rig/polecat","description":"Support machine-prefixed polecat addresses","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-15T16:37:23.426567-08:00","updated_at":"2025-12-15T16:37:23.426567-08:00","dependencies":[{"issue_id":"gt-f9x.10","depends_on_id":"gt-f9x","type":"parent-child","created_at":"2025-12-15T16:37:23.426926-08:00","created_by":"daemon"}]} +{"id":"gt-f9x.2","title":"Workspace detection: Find() walking up directory tree","description":"Find workspace root by looking for mayor/ or .gastown/","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-15T16:36:51.419316-08:00","updated_at":"2025-12-15T16:36:51.419316-08:00","dependencies":[{"issue_id":"gt-f9x.2","depends_on_id":"gt-f9x","type":"parent-child","created_at":"2025-12-15T16:36:51.419635-08:00","created_by":"daemon"},{"issue_id":"gt-f9x.2","depends_on_id":"gt-f9x.1","type":"blocks","created_at":"2025-12-15T16:37:32.426416-08:00","created_by":"daemon"}]} +{"id":"gt-f9x.3","title":"gt install command: Create workspace structure","description":"Create mayor/, mail/, rigs/, config.json, state.json","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-15T16:36:53.455589-08:00","updated_at":"2025-12-15T16:36:53.455589-08:00","dependencies":[{"issue_id":"gt-f9x.3","depends_on_id":"gt-f9x","type":"parent-child","created_at":"2025-12-15T16:36:53.455924-08:00","created_by":"daemon"},{"issue_id":"gt-f9x.3","depends_on_id":"gt-f9x.1","type":"blocks","created_at":"2025-12-15T16:37:32.513796-08:00","created_by":"daemon"},{"issue_id":"gt-f9x.3","depends_on_id":"gt-f9x.2","type":"blocks","created_at":"2025-12-15T16:37:32.597456-08:00","created_by":"daemon"}]} +{"id":"gt-f9x.4","title":"Doctor framework: Check interface, Result types, Report","description":"Framework for gt doctor health checks","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-15T16:37:03.81542-08:00","updated_at":"2025-12-15T16:37:03.81542-08:00","dependencies":[{"issue_id":"gt-f9x.4","depends_on_id":"gt-f9x","type":"parent-child","created_at":"2025-12-15T16:37:03.815763-08:00","created_by":"daemon"}]} +{"id":"gt-f9x.5","title":"Workspace doctor checks: Config, state, mail, boss, rigs","description":"Health checks for workspace-level components","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-15T16:37:05.267701-08:00","updated_at":"2025-12-15T16:37:05.267701-08:00","dependencies":[{"issue_id":"gt-f9x.5","depends_on_id":"gt-f9x","type":"parent-child","created_at":"2025-12-15T16:37:05.268035-08:00","created_by":"daemon"},{"issue_id":"gt-f9x.5","depends_on_id":"gt-f9x.4","type":"blocks","created_at":"2025-12-15T16:37:34.289236-08:00","created_by":"daemon"},{"issue_id":"gt-f9x.5","depends_on_id":"gt-f9x.2","type":"blocks","created_at":"2025-12-15T16:37:34.380374-08:00","created_by":"daemon"}]} +{"id":"gt-f9x.6","title":"Rig doctor checks: Refinery health, clones, gitignore","description":"Health checks for rig-level components","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-15T16:37:06.543281-08:00","updated_at":"2025-12-15T16:37:06.543281-08:00","dependencies":[{"issue_id":"gt-f9x.6","depends_on_id":"gt-f9x","type":"parent-child","created_at":"2025-12-15T16:37:06.543796-08:00","created_by":"daemon"},{"issue_id":"gt-f9x.6","depends_on_id":"gt-f9x.4","type":"blocks","created_at":"2025-12-15T16:37:34.46868-08:00","created_by":"daemon"}]} +{"id":"gt-f9x.7","title":"Connection interface: Protocol for local/remote ops","description":"Abstract interface for local vs SSH operations","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-15T16:37:07.764838-08:00","updated_at":"2025-12-15T16:37:07.764838-08:00","dependencies":[{"issue_id":"gt-f9x.7","depends_on_id":"gt-f9x","type":"parent-child","created_at":"2025-12-15T16:37:07.765169-08:00","created_by":"daemon"}]} +{"id":"gt-f9x.8","title":"LocalConnection: Local file/exec/tmux operations","description":"Implementation for local machine operations","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-15T16:37:19.879102-08:00","updated_at":"2025-12-15T16:37:19.879102-08:00","dependencies":[{"issue_id":"gt-f9x.8","depends_on_id":"gt-f9x","type":"parent-child","created_at":"2025-12-15T16:37:19.879451-08:00","created_by":"daemon"},{"issue_id":"gt-f9x.8","depends_on_id":"gt-f9x.7","type":"blocks","created_at":"2025-12-15T16:37:36.087392-08:00","created_by":"daemon"}]} +{"id":"gt-f9x.9","title":"Machine registry: Store and manage machine configs","description":"Registry for federation machine management","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-15T16:37:21.968099-08:00","updated_at":"2025-12-15T16:37:21.968099-08:00","dependencies":[{"issue_id":"gt-f9x.9","depends_on_id":"gt-f9x","type":"parent-child","created_at":"2025-12-15T16:37:21.968442-08:00","created_by":"daemon"},{"issue_id":"gt-f9x.9","depends_on_id":"gt-f9x.7","type":"blocks","created_at":"2025-12-15T16:37:36.174052-08:00","created_by":"daemon"}]} +{"id":"gt-u1j","title":"Port Gas Town to Go","description":"Complete rewrite of Gas Town in Go for improved performance and single-binary distribution.\n\n## Goals\n- Single installable binary (gt)\n- All Python functionality ported\n- Federation support built-in\n- Improved performance\n\n## Phases\n1. Core infrastructure (config, workspace, git wrapper)\n2. Rig \u0026 polecat management\n3. Session \u0026 tmux operations\n4. Mail system\n5. CLI commands\n6. TUI (optional)","status":"open","priority":0,"issue_type":"epic","created_at":"2025-12-15T16:36:28.769343-08:00","updated_at":"2025-12-15T16:36:28.769343-08:00"} +{"id":"gt-u1j.1","title":"Go scaffolding: cmd/gt, go.mod, Cobra setup","description":"Set up Go project structure with Cobra CLI framework","status":"open","priority":0,"issue_type":"task","created_at":"2025-12-15T16:36:48.376267-08:00","updated_at":"2025-12-15T16:36:48.376267-08:00","dependencies":[{"issue_id":"gt-u1j.1","depends_on_id":"gt-u1j","type":"parent-child","created_at":"2025-12-15T16:36:48.376622-08:00","created_by":"daemon"}]} diff --git a/.beads/metadata.json b/.beads/metadata.json new file mode 100644 index 00000000..c787975e --- /dev/null +++ b/.beads/metadata.json @@ -0,0 +1,4 @@ +{ + "database": "beads.db", + "jsonl_export": "issues.jsonl" +} \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..807d5983 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ + +# Use bd merge for beads JSONL files +.beads/issues.jsonl merge=beads diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..218c6e35 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Binaries +/gt +*.exe + +# Build +/dist/ +/build/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Test +coverage.out +*.test + +# Local config +config.toml +!config.example.toml diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..cab673a3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,23 @@ +# Claude: Gastown Go Port + +Run `bd prime` for beads context. + +## Project Info + +This is the **Go port** of Gas Town, a multi-agent workspace manager. + +- **Issue prefix**: `gt-` +- **Python version**: ~/ai/gastown-py (reference implementation) +- **Design docs**: docs/town-design.md + +## Development + +```bash +go build -o gt ./cmd/gt +go test ./... +``` + +## Key Epics + +- `gt-fqwd`: Port Gas Town to Go (main tracking epic) +- `gt-evp2`: Town & Rig Management (install, doctor, federation) diff --git a/README.md b/README.md new file mode 100644 index 00000000..e003b53f --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# Gastown (Go) + +Go port of [Gas Town](https://github.com/steveyegge/gastown-py) - a multi-agent workspace manager. + +## Status + +**Work in Progress** - This is the Go rewrite of the Python gastown tool. + +See the [Python version](https://github.com/steveyegge/gastown-py) for current functionality. + +## Goals + +- Single binary installation (`gt`) +- Self-diagnosing (`gt doctor`) +- Federation support (coordinate agents across VMs) +- Performance improvements over Python version + +## Development + +```bash +# Build +go build -o gt ./cmd/gt + +# Run +./gt --help +``` + +## Related + +- [gastown-py](https://github.com/steveyegge/gastown-py) - Python version (current) +- [beads](https://github.com/steveyegge/beads) - Issue tracking for agents diff --git a/cmd/gt/main.go b/cmd/gt/main.go new file mode 100644 index 00000000..7a7c9588 --- /dev/null +++ b/cmd/gt/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + "os" +) + +func main() { + fmt.Println("gt - Gas Town CLI") + fmt.Println("Version: 0.0.1 (development)") + if len(os.Args) > 1 { + fmt.Printf("Command: %s\n", os.Args[1]) + fmt.Println("Not yet implemented. See gastown-py for current functionality.") + } +} diff --git a/docs/town-design.md b/docs/town-design.md new file mode 100644 index 00000000..2134a017 --- /dev/null +++ b/docs/town-design.md @@ -0,0 +1,541 @@ +# 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: +- Workspace config (`.gastown/` or `mayor/`) +- Rigs (project workspaces) +- Polecats (worker clones) +- Mail system +- Beads integration + +## Config Files + +### Workspace Root + +A town is identified by a config directory (`mayor/` or `.gastown/`) containing `config.json` with `type: "workspace"`. + +``` +~/ai/ # Workspace root +├── mayor/ # or .gastown/ +│ ├── config.json # Workspace identity +│ ├── state.json # Workspace state +│ ├── mail/ # Mail system +│ │ └── inbox.jsonl # Mayor's inbox +│ ├── boss/ # Boss state +│ │ └── state.json +│ └── rigs/ # Mayor's rig clones (for beads access) +│ ├── gastown/ +│ └── beads/ +├── gastown/ # Rig +│ ├── .gastown/ # Rig config (type: "rig") +│ │ └── config.json +│ ├── refinery/ # Refinery infrastructure +│ │ ├── README.md +│ │ ├── state.json +│ │ ├── state.json.lock +│ │ └── rig/ # Refinery's git clone +│ └── / # Worker clones +└── beads/ # Another rig +``` + +### Workspace config.json + +```json +{ + "type": "workspace", + "version": 1, + "created_at": "2024-01-15T10:30:00Z" +} +``` + +### Workspace state.json + +```json +{ + "version": 1, + "projects": {} +} +``` + +### Rig config.json + +```json +{ + "type": "rig", + "git_url": "https://github.com/steveyegge/gastown", + "beads_path": "~/.gastown/rigs/gastown/.beads", + "federation": { + "machines": ["local", "gcp-west-1"], + "preferred_machine": "local" + } +} +``` + +### Refinery state.json + +```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 +``` diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..6c1fb795 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/steveyegge/gastown + +go 1.23