diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 1acba30a..fd1b6007 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,6 +1,7 @@ {"id":"gt-01u","title":"Design: Collapse mail into beads","description":"## Proposal\n\nReplace the separate mail system (JSONL inboxes) with beads issues using a naming convention.\n\n## Rationale\n\nIf all state should be in beads (work items, swarm state, dependencies), why have a separate system for messages? Mail is just:\n- Handoffs (agent → self)\n- Commands (Mayor → Refinery)\n- Escalations (Witness → Mayor)\n\nThese can all be beads issues with a special prefix.\n\n## Design\n\n### Convention\n- Messages use `@-` prefix: `@-witness-1734012345`\n- Assignee = recipient\n- Status: open = unread, closed = read/acknowledged\n- Priority 0 = urgent\n\n### Commands (thin wrappers)\n```bash\ngt mail send witness -s \"Subject\" -m \"Body\"\n → bd create --prefix=@ --title=\"Subject\" --assignee=witness --description=\"Body\"\n\ngt mail inbox\n → bd list --prefix=@ --assignee=$(gt whoami) --status=open\n\ngt mail read @-abc\n → bd show @-abc \u0026\u0026 bd close @-abc\n```\n\n### Notification\nDaemon watches for new `@-` issues and pokes relevant sessions.\nOr: agents poll on heartbeat (simpler).\n\n## What We Remove\n- `mail/inbox.jsonl` files\n- Mail JSONL read/write code\n- Separate delivery mechanism\n\n## What We Keep\n- `gt mail` CLI (as wrapper)\n- Handoff semantics\n- Notification (via daemon or polling)\n\n## Benefits\n- One system, one sync, one query interface\n- All communication in git history\n- Simpler architecture\n\n## Risks\n- Beads prefix filtering must be efficient\n- Namespace collision with user prefixes\n- Performance for high-frequency messages (probably fine for handoffs)\n\n## Decision Point\nDo we need first-class mail support in beads (`bd mail` commands) or is convention sufficient?","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-16T02:10:18.32879-08:00","updated_at":"2025-12-16T13:12:16.46526-08:00","closed_at":"2025-12-16T13:12:16.46526-08:00"} {"id":"gt-082","title":"Worker cleanup: Beads sync on shutdown","description":"Add beads sync verification to worker cleanup checklist and Witness verification.\n\n## Update to Decommission Checklist (gt-sd6)\n\nAdd to pre-done verification:\n- bd sync --status must show 'Up to date'\n- git status .beads/ must show no changes\n\n## Beads Edge Cases\n\nUncommitted beads changes:\n bd sync\n git add .beads/\n git commit -m 'beads: final sync'\n\nBeads sync conflict (rare):\n git fetch origin main\n git checkout main -- .beads/\n bd sync --force\n git add .beads/\n git commit -m 'beads: resolve sync conflict'\n\n## Update to Witness Verification (gt-f8v)\n\nWhen capturing worker state:\n town capture \u003cpolecat\u003e \"bd sync --status \u0026\u0026 git status .beads/\"\n\nCheck for:\n- bd sync --status shows 'Up to date'\n- git status .beads/ shows no changes\n\nIf beads not synced, nudge:\n WITNESS CHECK: Beads not synced. Run 'bd sync' then commit .beads/. Signal done when complete.","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-15T19:47:21.757756-08:00","updated_at":"2025-12-15T20:48:37.663168-08:00","dependencies":[{"issue_id":"gt-082","depends_on_id":"gt-l3c","type":"blocks","created_at":"2025-12-15T19:47:35.977804-08:00","created_by":"daemon"}]} {"id":"gt-0asj","title":"Merge: gt-5af.5","description":"branch: polecat/Scabrous\ntarget: main\nsource_issue: gt-5af.5\nrig: gastown","status":"closed","priority":1,"issue_type":"merge-request","created_at":"2025-12-19T17:50:25.227909-08:00","updated_at":"2025-12-19T17:52:57.683445-08:00","closed_at":"2025-12-19T17:52:57.683445-08:00","close_reason":"Superseded - source issue gt-5af.5 already closed, branch has extensive conflicts with main"} +{"id":"gt-0ei3","title":"Add molecules.jsonl as separate catalog file for template molecules","description":"","design":"Template molecules should live in a separate molecules.jsonl file, distinct from work items in issues.jsonl.\n\n## Rationale\n- Templates are read-only, work items are read-write\n- Clean separation makes bd list show only work, not templates\n- Enables sharing/versioning molecule catalogs independently\n- Organizations can publish/share molecule catalogs\n- Built-in molecules ship with bd, not seeded per-project\n\n## File Location\n.beads/\n├── beads.db # Runtime database\n├── issues.jsonl # Work items (synced)\n├── molecules.jsonl # Template catalog (read-only)\n└── config.yaml\n\n## Hierarchical Loading\n1. Built-in molecules (shipped with bd binary)\n2. Town-level: ~/gt/.beads/molecules.jsonl\n3. Rig-level: ~/gt/\u003crig\u003e/.beads/molecules.jsonl\n4. Project-level: committed in repo\n\n## Instantiation\nbd molecule instantiate mol-polecat-work --parent gt-123\nCreates child issues in issues.jsonl with instantiated_from reference.\n\n## Key Properties\n- Templates marked is_template: true in graph\n- Templates are read-only (mutations rejected)\n- bd list excludes templates by default\n- bd molecule list shows catalog","status":"open","priority":1,"issue_type":"feature","created_at":"2025-12-19T20:16:10.763471-08:00","updated_at":"2025-12-19T20:16:19.682508-08:00"} {"id":"gt-0iy3","title":"Merge: gt-3x1.3","description":"branch: polecat/Doof\ntarget: main\nsource_issue: gt-3x1.3\nrig: gastown","status":"closed","priority":1,"issue_type":"merge-request","created_at":"2025-12-19T14:53:52.741123-08:00","updated_at":"2025-12-19T19:13:27.737052-08:00","closed_at":"2025-12-19T17:47:03.618858-08:00"} {"id":"gt-0ol","title":"Update prompts.md: Engineer role and templates","description":"Update docs/prompts.md with Engineer role:\n\n1. Role Prompts table: Change Refinery to Engineer\n2. Add Engineer-specific prompts:\n - Session restart request template\n - Subtask filing template\n - Handoff mail template\n3. Update refinery.md template name to engineer.md\n4. Ensure consistency with architecture.md","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-16T23:12:05.279233-08:00","updated_at":"2025-12-16T23:12:05.279233-08:00","dependencies":[{"issue_id":"gt-0ol","depends_on_id":"gt-h5n","type":"blocks","created_at":"2025-12-16T23:12:15.013747-08:00","created_by":"daemon"}]} {"id":"gt-0pc","title":"Document Overseer role (human operator)","description":"Document the Overseer role in Gas Town architecture.\n\n## The Overseer\n\nThe **Overseer** is the human operator of Gas Town. Not an agent - a person.\n\n## Responsibilities\n\n| Area | Overseer Does | Mayor/Agents Do |\n|------|---------------|-----------------|\n| Strategy | Define project goals | Execute toward goals |\n| Priorities | Set priority order | Work in priority order |\n| Escalations | Final decision on stuck work | Escalate to Overseer |\n| Resources | Provision machines | Use allocated resources |\n| Quality | Review \u0026 approve swarm output | Produce output |\n| Operations | Run gt commands, monitor dashboards | Do the work |\n\n## Key Interactions\n\n### Overseer → Mayor\n- Start/stop Mayor sessions\n- Direct Mayor via conversation\n- Review Mayor recommendations\n- Approve cross-rig decisions\n\n### Mayor → Overseer (Escalations)\n- Stuck workers after retries\n- Resource decisions (add machines, polecats)\n- Ambiguous requirements\n- Architecture decisions\n\n## Operating Cadence\n\nTypical Overseer workflow:\n1. Morning: Check status, review overnight work\n2. During day: Monitor, respond to escalations, adjust priorities\n3. End of day: Review progress, plan next batch\n\n## Commands for Overseers\n\n```bash\ngt status # Quick health check\ngt doctor # Detailed diagnostics \ngt doctor --fix # Auto-repair issues\ngt inbox # Messages from agents\ngt stop --all # Emergency halt\n```\n\n## Documentation Updates\n\nAdd to docs/architecture.md:\n- Overseer section under Agent Roles\n- Clarify Mayor reports to Overseer\n- Add Overseer to workflow diagrams","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-15T23:18:03.177633-08:00","updated_at":"2025-12-15T23:22:51.477786-08:00","closed_at":"2025-12-15T23:22:51.477786-08:00"} diff --git a/internal/cmd/crew.go b/internal/cmd/crew.go index 1cd86821..59b328d3 100644 --- a/internal/cmd/crew.go +++ b/internal/cmd/crew.go @@ -506,6 +506,10 @@ func runCrewAt(cmd *cobra.Command, args []string) error { _ = t.SetEnvironment(sessionID, "GT_RIG", r.Name) _ = t.SetEnvironment(sessionID, "GT_CREW", name) + // Apply rig-based theming (uses config if set, falls back to hash) + theme := getThemeForRig(r.Name) + _ = t.ConfigureGasTownSession(sessionID, theme, r.Name, name, "crew") + // Wait for shell to be ready after session creation if err := t.WaitForShellReady(sessionID, 5*time.Second); err != nil { return fmt.Errorf("waiting for shell: %w", err) @@ -847,6 +851,10 @@ func runCrewRestart(cmd *cobra.Command, args []string) error { t.SetEnvironment(sessionID, "GT_RIG", r.Name) t.SetEnvironment(sessionID, "GT_CREW", name) + // Apply rig-based theming (uses config if set, falls back to hash) + theme := getThemeForRig(r.Name) + _ = t.ConfigureGasTownSession(sessionID, theme, r.Name, name, "crew") + // Wait for shell to be ready if err := t.WaitForShellReady(sessionID, 5*time.Second); err != nil { return fmt.Errorf("waiting for shell: %w", err) diff --git a/internal/cmd/theme.go b/internal/cmd/theme.go index 64df0f25..9147dafc 100644 --- a/internal/cmd/theme.go +++ b/internal/cmd/theme.go @@ -2,10 +2,14 @@ package cmd import ( "fmt" + "os" + "path/filepath" "strings" "github.com/spf13/cobra" + "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/tmux" + "github.com/steveyegge/gastown/internal/workspace" ) var ( @@ -63,9 +67,15 @@ func runTheme(cmd *cobra.Command, args []string) error { // Show current theme assignment if len(args) == 0 { - theme := tmux.AssignTheme(rigName) + theme := getThemeForRig(rigName) fmt.Printf("Rig: %s\n", rigName) fmt.Printf("Theme: %s (%s)\n", theme.Name, theme.Style()) + // Show if it's configured vs default + if configured := loadRigTheme(rigName); configured != "" { + fmt.Printf("(configured in .gastown/config.json)\n") + } else { + fmt.Printf("(default, based on rig name hash)\n") + } return nil } @@ -76,10 +86,13 @@ func runTheme(cmd *cobra.Command, args []string) error { return fmt.Errorf("unknown theme: %s (use --list to see available themes)", themeName) } - // TODO: Save to rig config.json - fmt.Printf("Theme '%s' selected for rig '%s'\n", themeName, rigName) - fmt.Println("Note: Run 'gt theme apply' to apply to running sessions") - fmt.Println("(Persistent config not yet implemented)") + // Save to rig config + if err := saveRigTheme(rigName, themeName); err != nil { + return fmt.Errorf("saving theme config: %w", err) + } + + fmt.Printf("Theme '%s' saved for rig '%s'\n", themeName, rigName) + fmt.Println("Run 'gt theme apply' to apply to running sessions") return nil } @@ -133,7 +146,8 @@ func runThemeApply(cmd *cobra.Command, args []string) error { role = "polecat" } - theme = tmux.AssignTheme(rig) + // Use configured theme, fall back to hash-based assignment + theme = getThemeForRig(rig) } // Apply theme and status format @@ -165,29 +179,115 @@ func runThemeApply(cmd *cobra.Command, args []string) error { // detectCurrentRig determines the rig from environment or cwd. func detectCurrentRig() string { - // Try environment first - if rig := detectCurrentSession(); rig != "" { - // Extract rig from session name - parts := strings.SplitN(rig, "-", 3) - if len(parts) >= 2 && parts[0] == "gt" { + // Try environment first (GT_RIG is set in tmux sessions) + if rig := os.Getenv("GT_RIG"); rig != "" { + return rig + } + + // Try to extract from tmux session name + if session := detectCurrentSession(); session != "" { + // Extract rig from session name: gt--... + parts := strings.SplitN(session, "-", 3) + if len(parts) >= 2 && parts[0] == "gt" && parts[1] != "mayor" && parts[1] != "deacon" { return parts[1] } } - // Try to detect from cwd - cwd, err := findBeadsWorkDir() + // Try to detect from actual cwd path + cwd, err := os.Getwd() if err != nil { return "" } - // Extract rig name from path - // Typical paths: /Users/stevey/gt//... - parts := strings.Split(cwd, "/") - for i, p := range parts { - if p == "gt" && i+1 < len(parts) { - return parts[i+1] - } + // Find town root to extract rig name + townRoot, err := workspace.FindFromCwd() + if err != nil || townRoot == "" { + return "" + } + + // Get path relative to town root + rel, err := filepath.Rel(townRoot, cwd) + if err != nil { + return "" + } + + // Extract first path component (rig name) + // Patterns: /..., mayor/..., deacon/... + parts := strings.Split(rel, string(filepath.Separator)) + if len(parts) > 0 && parts[0] != "." && parts[0] != "mayor" && parts[0] != "deacon" { + return parts[0] } return "" } + +// getThemeForRig returns the theme for a rig, checking config first. +func getThemeForRig(rigName string) tmux.Theme { + // Try to load configured theme + if themeName := loadRigTheme(rigName); themeName != "" { + if theme := tmux.GetThemeByName(themeName); theme != nil { + return *theme + } + } + // Fall back to hash-based assignment + return tmux.AssignTheme(rigName) +} + +// loadRigTheme loads the theme name from rig config. +func loadRigTheme(rigName string) string { + townRoot, err := workspace.FindFromCwd() + if err != nil || townRoot == "" { + return "" + } + + configPath := filepath.Join(townRoot, rigName, ".gastown", "config.json") + cfg, err := config.LoadRigConfig(configPath) + if err != nil { + return "" + } + + if cfg.Theme != nil && cfg.Theme.Name != "" { + return cfg.Theme.Name + } + return "" +} + +// saveRigTheme saves the theme name to rig config. +func saveRigTheme(rigName, themeName string) error { + townRoot, err := workspace.FindFromCwd() + if err != nil { + return fmt.Errorf("finding workspace: %w", err) + } + if townRoot == "" { + return fmt.Errorf("not in a Gas Town workspace") + } + + configPath := filepath.Join(townRoot, rigName, ".gastown", "config.json") + + // Load existing config or create new + var cfg *config.RigConfig + cfg, err = config.LoadRigConfig(configPath) + if err != nil { + // Create new config if not found + if os.IsNotExist(err) || strings.Contains(err.Error(), "not found") { + cfg = &config.RigConfig{ + Type: "rig", + Version: config.CurrentRigConfigVersion, + } + } else { + return fmt.Errorf("loading config: %w", err) + } + } + + // Set theme + cfg.Theme = &config.ThemeConfig{ + Name: themeName, + } + + // Save + if err := config.SaveRigConfig(configPath, cfg); err != nil { + return fmt.Errorf("saving config: %w", err) + } + + return nil +}