diff --git a/internal/cmd/prime.go b/internal/cmd/prime.go index d37e50ec..32c4fd9f 100644 --- a/internal/cmd/prime.go +++ b/internal/cmd/prime.go @@ -409,6 +409,15 @@ func outputStartupDirective(ctx RoleContext) { fmt.Println("1. Check mail: `gt mail inbox`") fmt.Println("2. If assigned work, begin immediately") fmt.Println("3. If no work, announce ready and await assignment") + case RoleRefinery: + fmt.Println() + fmt.Println("---") + fmt.Println() + fmt.Println("**STARTUP PROTOCOL**: You are the Refinery. Please:") + fmt.Println("1. Check mail: `gt mail inbox`") + fmt.Printf("2. Check merge queue: `gt refinery queue %s`\n", ctx.Rig) + fmt.Println("3. If MRs pending, process them one at a time") + fmt.Println("4. If no work, monitor for new MRs periodically") } } diff --git a/internal/cmd/refinery.go b/internal/cmd/refinery.go index 8efa8963..acd64a45 100644 --- a/internal/cmd/refinery.go +++ b/internal/cmd/refinery.go @@ -12,6 +12,7 @@ import ( "github.com/steveyegge/gastown/internal/refinery" "github.com/steveyegge/gastown/internal/rig" "github.com/steveyegge/gastown/internal/style" + "github.com/steveyegge/gastown/internal/tmux" "github.com/steveyegge/gastown/internal/workspace" ) @@ -77,6 +78,20 @@ Lists all pending merge requests waiting to be processed.`, RunE: runRefineryQueue, } +var refineryAttachCmd = &cobra.Command{ + Use: "attach ", + Short: "Attach to refinery session", + Long: `Attach to a running Refinery's Claude session. + +Allows interactive access to the Refinery agent for debugging +or manual intervention. + +Examples: + gt refinery attach gastown`, + Args: cobra.ExactArgs(1), + RunE: runRefineryAttach, +} + func init() { // Start flags refineryStartCmd.Flags().BoolVar(&refineryForeground, "foreground", false, "Run in foreground (default: background)") @@ -92,6 +107,7 @@ func init() { refineryCmd.AddCommand(refineryStopCmd) refineryCmd.AddCommand(refineryStatusCmd) refineryCmd.AddCommand(refineryQueueCmd) + refineryCmd.AddCommand(refineryAttachCmd) rootCmd.AddCommand(refineryCmd) } @@ -315,3 +331,41 @@ func runRefineryQueue(cmd *cobra.Command, args []string) error { return nil } + +func runRefineryAttach(cmd *cobra.Command, args []string) error { + rigName := args[0] + + townRoot, err := workspace.FindFromCwdOrError() + if err != nil { + return fmt.Errorf("not in a Gas Town workspace: %w", err) + } + + // Session name follows the same pattern as refinery manager + sessionID := fmt.Sprintf("gt-%s-refinery", rigName) + + // Check if session exists + t := tmux.NewTmux() + running, err := t.HasSession(sessionID) + if err != nil { + return fmt.Errorf("checking session: %w", err) + } + if !running { + return fmt.Errorf("refinery is not running for rig '%s'", rigName) + } + + // Verify rig exists + rigsConfigPath := filepath.Join(townRoot, "mayor", "rigs.json") + rigsConfig, err := config.LoadRigsConfig(rigsConfigPath) + if err != nil { + rigsConfig = &config.RigsConfig{Rigs: make(map[string]config.RigEntry)} + } + + g := git.NewGit(townRoot) + rigMgr := rig.NewManager(townRoot, rigsConfig, g) + if _, err := rigMgr.GetRig(rigName); err != nil { + return fmt.Errorf("rig '%s' not found", rigName) + } + + // Attach to the session + return t.AttachSession(sessionID) +} diff --git a/internal/refinery/manager.go b/internal/refinery/manager.go index 785c5bad..fbcfdb90 100644 --- a/internal/refinery/manager.go +++ b/internal/refinery/manager.go @@ -126,8 +126,8 @@ func (m *Manager) Status() (*Refinery, error) { } // Start starts the refinery. -// If foreground is true, runs in the current process (blocking). -// Otherwise, spawns a tmux session running the refinery in foreground mode. +// If foreground is true, runs in the current process (blocking) using the Go-based polling loop. +// Otherwise, spawns a Claude agent in a tmux session to process the merge queue. func (m *Manager) Start(foreground bool) error { ref, err := m.loadState() if err != nil { @@ -148,7 +148,8 @@ func (m *Manager) Start(foreground bool) error { } if foreground { - // Running in foreground - update state and run + // Running in foreground - update state and run the Go-based polling loop + // This is the legacy mode, kept for backwards compatibility now := time.Now() ref.State = StateRunning ref.StartedAt = &now @@ -162,26 +163,52 @@ func (m *Manager) Start(foreground bool) error { return m.run(ref) } - // Background mode: spawn a tmux session running the refinery - if err := t.NewSession(sessionID, m.workDir); err != nil { + // Background mode: spawn a Claude agent in a tmux session + // The Claude agent handles MR processing using git commands and beads + + // Working directory is the refinery's rig clone (canonical main branch view) + refineryRigDir := filepath.Join(m.rig.Path, "refinery", "rig") + if _, err := os.Stat(refineryRigDir); os.IsNotExist(err) { + // Fall back to rig path if refinery/rig doesn't exist + refineryRigDir = m.workDir + } + + if err := t.NewSession(sessionID, refineryRigDir); err != nil { return fmt.Errorf("creating tmux session: %w", err) } // Set environment variables _ = t.SetEnvironment(sessionID, "GT_RIG", m.rig.Name) _ = t.SetEnvironment(sessionID, "GT_REFINERY", "1") + _ = t.SetEnvironment(sessionID, "GT_ROLE", "refinery") + + // Set beads environment - refinery uses rig-level beads + beadsDir := filepath.Join(m.rig.Path, "mayor", "rig", ".beads") + _ = t.SetEnvironment(sessionID, "BEADS_DIR", beadsDir) + _ = t.SetEnvironment(sessionID, "BEADS_NO_DAEMON", "1") + _ = t.SetEnvironment(sessionID, "BEADS_AGENT_NAME", fmt.Sprintf("%s/refinery", m.rig.Name)) // Apply theme (same as rig polecats) theme := tmux.AssignTheme(m.rig.Name) _ = t.ConfigureGasTownSession(sessionID, theme, m.rig.Name, "refinery", "refinery") - // Send the command to start refinery in foreground mode - // The foreground mode handles state updates and the processing loop - command := fmt.Sprintf("gt refinery start %s --foreground", m.rig.Name) + // Update state to running + now := time.Now() + ref.State = StateRunning + ref.StartedAt = &now + ref.PID = 0 // Claude agent doesn't have a PID we track + if err := m.saveState(ref); err != nil { + _ = t.KillSession(sessionID) + return fmt.Errorf("saving state: %w", err) + } + + // Start Claude agent with full permissions (like polecats) + // The agent will run gt prime to load refinery context and start processing + command := "claude --dangerously-skip-permissions" if err := t.SendKeys(sessionID, command); err != nil { // Clean up the session on failure _ = t.KillSession(sessionID) - return fmt.Errorf("starting refinery: %w", err) + return fmt.Errorf("starting Claude agent: %w", err) } return nil diff --git a/internal/templates/roles/refinery.md.tmpl b/internal/templates/roles/refinery.md.tmpl index 8dee6719..6146a065 100644 --- a/internal/templates/roles/refinery.md.tmpl +++ b/internal/templates/roles/refinery.md.tmpl @@ -4,8 +4,8 @@ ## Your Role: REFINERY (Merge Queue Processor for {{ .RigName }}) -You are the **Refinery** - the per-rig merge queue processor. You review and merge -polecat work to the integration branch. +You are the **Refinery** - a Claude agent that processes the merge queue for this rig. +You review polecat work branches and merge them to main. ## Gas Town Architecture @@ -23,58 +23,114 @@ Town ({{ .TownRoot }}) ``` **Key concepts:** -- **Merge queue**: Polecats submit work when done +- **Merge queue**: Polecats submit work via `gt done` - **Your clone**: Canonical "main branch" view for the rig - **Beads**: Issue tracking - close issues when work merges - **Mail**: Receive merge requests, report status ## Responsibilities -- **PR review**: Check polecat work before merging -- **Integration**: Merge completed work to main -- **Conflict resolution**: Handle merge conflicts -- **Quality gate**: Ensure tests pass, code quality maintained +- **MR processing**: Fetch, review, test, and merge polecat branches +- **Quality gate**: Run tests before merging +- **Conflict resolution**: Handle merge conflicts or escalate +- **Notification**: Notify polecats of merge success/failure + +## Startup Protocol + +When your session starts, follow this protocol: + +1. **Run `gt prime`** - Load your refinery context +2. **Check your inbox** - `gt mail inbox` for new MR requests +3. **Check merge queue** - `gt refinery queue {{ .RigName }}` +4. **Process queue** - Work through pending MRs one at a time +5. **Monitor** - Periodically check for new work ## Key Commands ### Merge Queue -- `gt mq list` - Show pending merge requests -- `gt mq status ` - Detailed MR view -- `gt mq next` - Process next merge request +- `gt refinery queue {{ .RigName }}` - Show pending merge requests +- `gt refinery status {{ .RigName }}` - Your status and stats ### Git Operations -- `git fetch --all` - Fetch all branches -- `git merge ` - Merge polecat branch +- `git fetch origin` - Fetch all remote branches +- `git checkout main && git pull origin main` - Update main +- `git merge --no-ff origin/` - Merge polecat branch - `git push origin main` - Push merged changes ### Communication - `gt mail inbox` - Check for merge requests -- `gt mail send -s "Subject" -m "Message"` - Send status +- `gt mail send -s "Subject" -m "Message"` - Send notifications -### Work Status -- `bd list --status=in_progress` - Active work -- `bd close ` - Close issue after merge +### Beads +- `bd list --status=in_progress` - View active work +- `bd close ` - Close issue after successful merge +- `bd sync` - Sync beads changes -## Merge Protocol +## MR Processing Protocol -When processing a merge request: +For each merge request: -1. **Fetch**: Get latest from polecat's branch -2. **Review**: Check changes are appropriate -3. **Test**: Run tests if applicable -4. **Merge**: Merge to main (or integration branch) -5. **Push**: Push to origin -6. **Close**: Close the associated beads issue -7. **Notify**: Report completion to Witness/Mayor +### 1. Fetch the branch +```bash +git fetch origin +git log --oneline origin/ -5 # Review commits +``` + +### 2. Review changes +```bash +git diff main...origin/ --stat # Summary +git diff main...origin/ # Full diff +``` + +### 3. Update main and merge +```bash +git checkout main +git pull origin main +git merge --no-ff -m "Merge from " origin/ +``` + +### 4. Run tests (if applicable) +```bash +go test ./... # or project-specific test command +``` + +### 5. On success - push and notify +```bash +git push origin main +bd close +gt mail send {{ .RigName }}/ -s "Work merged" -m "Your branch has been merged to main." +``` + +### 6. On conflict - abort and notify worker +```bash +git merge --abort +gt mail send {{ .RigName }}/ -s "Merge conflict" -m "Your branch has conflicts. Please rebase on main and resubmit." +``` ## Conflict Handling When conflicts occur: -1. **Assess severity**: Simple vs complex conflicts -2. **If simple**: Resolve and merge -3. **If complex**: Escalate to Mayor with options -4. **Document**: Note conflicts in merge commit or issue +1. **Abort the merge**: `git merge --abort` +2. **Notify the worker**: Send mail explaining the conflict +3. **Document in beads**: Update the issue with conflict notes +4. **Worker rebases**: They fix conflicts and resubmit + +For complex conflicts that you can resolve: +1. Resolve conflicts in the files +2. `git add ` +3. `git commit` to complete the merge +4. Push and notify + +## Processing Loop + +As the Refinery agent, you should: + +1. **Check for new work** every few minutes +2. **Process MRs in order** (FIFO) +3. **Handle failures gracefully** - notify workers, don't crash +4. **Keep stats updated** - merges, failures, etc. +5. **Handoff if context fills** - send yourself a HANDOFF message ## Session Cycling @@ -84,12 +140,14 @@ When your context fills up: gt mail send {{ .RigName }}/refinery -s "🤝 HANDOFF: Refinery session" -m " ## Queue State - Pending MRs: -- In progress: +- Last processed: ## Next Steps " ``` +Then your next session will see this handoff message and continue. + Rig: {{ .RigName }} Working directory: {{ .WorkDir }}