Refinery as worktree: local MR integration (gt-4u5z)

Architecture changes:
- Refinery created as worktree of mayor clone (shares .git)
- Polecat branches stay local (never pushed to origin)
- MRs stored as wisps in .beads-wisp/mq/ (ephemeral)
- Only main gets pushed to origin after merge

New mrqueue package for wisp-based MR storage.
Updated spawn, done, mq_submit, refinery, molecule templates.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-23 21:24:42 -08:00
parent e895e51ee4
commit b685879b63
11 changed files with 378 additions and 142 deletions

View File

@@ -1,8 +1,39 @@
# Merge Queue Design
The merge queue is the coordination mechanism for landing completed work. It's implemented entirely in Beads - merge requests are just another issue type with dependencies.
The merge queue coordinates landing completed work. MRs are ephemeral wisps (not synced beads), and polecat branches stay local (never pushed to origin).
**Key insight**: Git is already a ledger. Beads is already federated. The merge queue is just a query pattern over beads issues.
**Key insight**: Git is already a ledger. Beads track durable state (issues). MRs are transient operational state - perfect for wisps.
## Architecture (Current)
```
┌─────────────────────────────────────────────────────────────────┐
│ LOCAL MERGE QUEUE │
│ │
│ Polecat worktree ──commit──► local branch (polecat/nux) │
│ │ │ │
│ │ │ (same .git) │
│ ▼ ▼ │
│ .beads-wisp/mq/mr-xxx.json Refinery worktree │
│ │ │ │
│ └──────────────────────────┘ │
│ │ │
│ Refinery reads MR, merges branch │
│ │ │
│ ▼ │
│ git push origin main │
│ │ │
│ Delete local branch │
│ Delete MR wisp file │
└─────────────────────────────────────────────────────────────────┘
```
**Key points:**
- Refinery is a worktree of the same git repo as polecats (shared .git)
- Polecat branches are local only - never pushed to origin
- MRs stored in `.beads-wisp/mq/` (ephemeral, not synced)
- Only `main` branch gets pushed to origin after merge
- Source issues tracked in beads (durable), closed after merge
## Overview
@@ -30,61 +61,47 @@ The merge queue is the coordination mechanism for landing completed work. It's i
## Merge Request Schema
A merge request is a beads issue with `type: merge-request`:
A merge request is a JSON file in `.beads-wisp/mq/`:
```yaml
id: gt-mr-abc123
type: merge-request
status: open # open, in_progress, closed
priority: P1 # Inherited from source issue
title: "Merge: Fix login timeout (gt-xyz)"
# MR-specific fields (in description or structured)
branch: polecat/Nux/gt-xyz # Source branch
target: main # Target branch (or integration/epic-id)
source_issue: gt-xyz # The work being merged
worker: Nux # Who did the work
rig: gastown # Which rig
# Set on completion
merge_commit: abc123def # SHA of merge commit (on success)
close_reason: merged # merged, rejected, conflict, superseded
# Standard beads fields
created: 2025-12-17T10:00:00Z
updated: 2025-12-17T10:30:00Z
assignee: engineer # The Engineer processing it
depends_on: [gt-mr-earlier] # Ordering dependencies
```json
// .beads-wisp/mq/mr-1703372400-a1b2c3d4.json
{
"id": "mr-1703372400-a1b2c3d4",
"branch": "polecat/nux",
"target": "main",
"source_issue": "gt-xyz",
"worker": "nux",
"rig": "gastown",
"title": "Merge: gt-xyz",
"priority": 1,
"created_at": "2025-12-23T20:00:00Z"
}
```
### ID Convention
Merge request IDs follow the pattern: `<prefix>-mr-<hash>`
MR IDs follow the pattern: `mr-<timestamp>-<random>`
Example: `gt-mr-abc123` for a gastown merge request.
Example: `mr-1703372400-a1b2c3d4`
This distinguishes them from regular issues while keeping them in the same namespace.
These are ephemeral - deleted after merge. Source issues in beads provide the durable record.
### Creating Merge Requests
Workers submit to the queue via:
```bash
# Worker signals work is ready
# Worker signals work is ready (preferred)
gt done # Auto-detects branch, issue, creates MR
# Or explicit submission
gt mq submit # Auto-detects branch, issue, worker
gt mq submit --issue gt-xyz # Explicit issue
# Explicit submission
gt mq submit --branch polecat/Nux/gt-xyz --issue gt-xyz
# Under the hood, this creates:
bd create --type=merge-request \
--title="Merge: Fix login timeout (gt-xyz)" \
--priority=P1 \
--body="branch: polecat/Nux/gt-xyz
target: main
source_issue: gt-xyz
worker: Nux
rig: gastown"
# Under the hood, this writes to .beads-wisp/mq/:
# - Creates mr-<timestamp>-<random>.json
# - No beads issue created (MRs are ephemeral wisps)
# - Branch stays local (never pushed to origin)
```
## Queue Ordering
@@ -175,8 +192,8 @@ The Engineer (formerly Refinery) processes the merge queue continuously:
```python
def process_merge(mr):
# 1. Fetch the branch
git fetch origin {mr.branch}
# 1. Branch is already local (shared .git with polecats)
# No fetch needed - refinery worktree sees polecat branches directly
# 2. Check for conflicts with target
conflicts = git_check_conflicts(mr.branch, mr.target)
@@ -194,12 +211,15 @@ def process_merge(mr):
git reset --hard HEAD~1 # Undo merge
return Failure(reason="tests_failed", output=result.output)
# 5. Push to origin
# 5. Push to origin (only main goes to origin)
git push origin {mr.target}
# 6. Clean up source branch (optional)
# 6. Clean up source branch (local delete only)
if config.delete_merged_branches:
git push origin --delete {mr.branch}
git branch -D {mr.branch} # Local delete, not remote
# 7. Remove MR wisp file
os.remove(.beads-wisp/mq/{mr.id}.json)
return Success(merge_commit=git_rev_parse("HEAD"))
```

View File

@@ -601,7 +601,7 @@ git checkout main
git merge --ff-only temp
git push origin main
git branch -d temp
git push origin --delete <polecat-branch>
git branch -D <polecat-branch> # Local delete (branches never go to origin)
` + "```" + `
Main has moved. Any remaining branches need rebasing on new baseline.

View File

@@ -409,9 +409,9 @@ End session with proper handoff.
1. Sync all state:
` + "```" + `bash
git add -A && git commit -m "WIP: <summary>" || true
git push origin HEAD
bd sync
` + "```" + `
Note: Branch stays local (commits saved in shared .git).
2. Write handoff to successor (yourself):
` + "```" + `bash
@@ -522,8 +522,8 @@ Finalize session and request termination.
1. Sync all state:
` + "```" + `bash
bd sync
git push origin HEAD
` + "```" + `
Note: Branch stays local (commits saved in shared .git).
2. Update work mol based on exit type:
- COMPLETED: ` + "`bd close <work-mol-root>`" + `

View File

@@ -163,13 +163,12 @@ Submit to merge queue via beads.
**IMPORTANT**: Do NOT use gh pr create or GitHub PRs.
The Refinery processes merges via beads merge-request issues.
Branch stays local (refinery sees it via shared worktree).
1. Push your branch to origin
2. Create a beads merge-request: bd create --type=merge-request --title="Merge: <summary>"
3. Signal ready: gt done
1. Create a beads merge-request: bd create --type=merge-request --title="Merge: <summary>"
2. Signal ready: gt done
` + "```" + `bash
git push origin HEAD
bd create --type=merge-request --title="Merge: <issue-summary>"
gt done # Signal work ready for merge queue
` + "```" + `

View File

@@ -3,12 +3,14 @@ package cmd
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/git"
"github.com/steveyegge/gastown/internal/mail"
"github.com/steveyegge/gastown/internal/mrqueue"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/workspace"
)
@@ -144,41 +146,31 @@ func runDone(cmd *cobra.Command, args []string) error {
// Build title
title := fmt.Sprintf("Merge: %s", issueID)
// CRITICAL: Push branch to origin BEFORE creating MR
// Without this, the worktree can be deleted and the branch lost forever
fmt.Printf("Pushing branch to origin...\n")
if err := g.Push("origin", branch, false); err != nil {
return fmt.Errorf("pushing branch to origin: %w", err)
}
fmt.Printf("%s Branch pushed to origin/%s\n", style.Bold.Render("✓"), branch)
// Note: Branch stays local. Refinery sees it via shared .git (worktree).
// Only main gets pushed to origin after merge.
// Build description with MR fields
mrFields := &beads.MRFields{
// Submit to MR queue (wisp storage - ephemeral, not synced)
rigPath := filepath.Join(townRoot, rigName)
queue := mrqueue.New(rigPath)
mr := &mrqueue.MR{
Branch: branch,
Target: target,
SourceIssue: issueID,
Worker: worker,
Rig: rigName,
}
description := beads.FormatMRFields(mrFields)
// Create the merge-request issue
createOpts := beads.CreateOptions{
Title: title,
Type: "merge-request",
Priority: priority,
Description: description,
}
issue, err := bd.Create(createOpts)
if err != nil {
return fmt.Errorf("creating merge request: %w", err)
if err := queue.Submit(mr); err != nil {
return fmt.Errorf("submitting to merge queue: %w", err)
}
mrID = issue.ID
mrID = mr.ID
// Success output
fmt.Printf("%s Work submitted to merge queue\n", style.Bold.Render("✓"))
fmt.Printf(" MR ID: %s\n", style.Bold.Render(issue.ID))
fmt.Printf(" MR ID: %s\n", style.Bold.Render(mr.ID))
fmt.Printf(" Source: %s\n", branch)
fmt.Printf(" Target: %s\n", target)
fmt.Printf(" Issue: %s\n", issueID)

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
@@ -11,6 +12,7 @@ import (
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/git"
"github.com/steveyegge/gastown/internal/mrqueue"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/workspace"
)
@@ -133,40 +135,30 @@ func runMqSubmit(cmd *cobra.Command, args []string) error {
// Build title
title := fmt.Sprintf("Merge: %s", issueID)
// CRITICAL: Push branch to origin BEFORE creating MR
// Without this, the worktree can be deleted and the branch lost forever
fmt.Printf("Pushing branch to origin...\n")
if err := g.Push("origin", branch, false); err != nil {
return fmt.Errorf("pushing branch to origin: %w", err)
}
fmt.Printf("%s Branch pushed to origin/%s\n", style.Bold.Render("✓"), branch)
// Note: Branch stays local. Refinery sees it via shared .git (worktree).
// Only main gets pushed to origin after merge.
// Build description with MR fields
mrFields := &beads.MRFields{
// Submit to MR queue (wisp storage - ephemeral, not synced)
rigPath := filepath.Join(townRoot, rigName)
queue := mrqueue.New(rigPath)
mr := &mrqueue.MR{
Branch: branch,
Target: target,
SourceIssue: issueID,
Worker: worker,
Rig: rigName,
}
description := beads.FormatMRFields(mrFields)
// Create the merge-request issue
createOpts := beads.CreateOptions{
Title: title,
Type: "merge-request",
Priority: priority,
Description: description,
}
issue, err := bd.Create(createOpts)
if err != nil {
return fmt.Errorf("creating merge request: %w", err)
if err := queue.Submit(mr); err != nil {
return fmt.Errorf("submitting to merge queue: %w", err)
}
// Success output
fmt.Printf("%s Created merge request\n", style.Bold.Render("✓"))
fmt.Printf(" MR ID: %s\n", style.Bold.Render(issue.ID))
fmt.Printf("%s Submitted to merge queue\n", style.Bold.Render("✓"))
fmt.Printf(" MR ID: %s\n", style.Bold.Render(mr.ID))
fmt.Printf(" Source: %s\n", branch)
fmt.Printf(" Target: %s\n", target)
fmt.Printf(" Issue: %s\n", issueID)

View File

@@ -608,8 +608,7 @@ func buildSpawnContext(issue *BeadsIssue, message string) string {
sb.WriteString("2. Work on your task, commit changes regularly\n")
sb.WriteString("3. Run `bd close <issue-id>` when done\n")
sb.WriteString("4. Run `bd sync` to push beads changes\n")
sb.WriteString("5. Push code: `git push origin HEAD`\n")
sb.WriteString("6. Run `gt done` to signal completion\n")
sb.WriteString("5. Run `gt done` to signal completion (branch stays local)\n")
return sb.String()
}
@@ -673,18 +672,16 @@ func buildWorkAssignmentMail(issue *BeadsIssue, message, polecatAddress string,
if moleculeCtx != nil {
body.WriteString("4. Check `bd ready --parent " + moleculeCtx.RootIssueID + "` for more steps\n")
body.WriteString("5. Repeat steps 2-4 for each ready step\n")
body.WriteString("6. When all steps done: run `bd sync`, push code, run `gt done`\n")
body.WriteString("6. When all steps done: run `bd sync`, then `gt done`\n")
} else {
body.WriteString("4. Run `bd sync` to push beads changes\n")
body.WriteString("5. Push code: `git push origin HEAD`\n")
body.WriteString("6. Run `gt done` to signal completion\n")
body.WriteString("5. Run `gt done` to signal completion (branch stays local)\n")
}
body.WriteString("\n## Handoff Protocol\n")
body.WriteString("Before signaling done, ensure:\n")
body.WriteString("- Git status is clean (no uncommitted changes)\n")
body.WriteString("- Issue is closed with `bd close`\n")
body.WriteString("- Beads are synced with `bd sync`\n")
body.WriteString("- Code is pushed to origin\n")
body.WriteString("\nThe `gt done` command verifies these and signals the Witness.\n")
return &mail.Message{

183
internal/mrqueue/mrqueue.go Normal file
View File

@@ -0,0 +1,183 @@
// Package mrqueue provides wisp-based merge request queue storage.
// MRs are ephemeral - stored locally in .beads-wisp/mq/ and deleted after merge.
// This avoids sync overhead for transient MR state.
package mrqueue
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
// MR represents a merge request in the queue.
type MR struct {
ID string `json:"id"`
Branch string `json:"branch"` // Source branch (e.g., "polecat/nux")
Target string `json:"target"` // Target branch (e.g., "main")
SourceIssue string `json:"source_issue"` // The work item being merged
Worker string `json:"worker"` // Who did the work
Rig string `json:"rig"` // Which rig
Title string `json:"title"` // MR title
Priority int `json:"priority"` // Priority (lower = higher priority)
CreatedAt time.Time `json:"created_at"`
}
// Queue manages the MR wisp storage.
type Queue struct {
dir string // .beads-wisp/mq/ directory
}
// New creates a new MR queue for the given rig path.
func New(rigPath string) *Queue {
return &Queue{
dir: filepath.Join(rigPath, ".beads-wisp", "mq"),
}
}
// NewFromWorkdir creates a queue by finding the rig root from a working directory.
func NewFromWorkdir(workdir string) (*Queue, error) {
// Walk up to find .beads-wisp or rig root
dir := workdir
for {
wispDir := filepath.Join(dir, ".beads-wisp")
if info, err := os.Stat(wispDir); err == nil && info.IsDir() {
return &Queue{dir: filepath.Join(wispDir, "mq")}, nil
}
parent := filepath.Dir(dir)
if parent == dir {
return nil, fmt.Errorf("could not find .beads-wisp directory from %s", workdir)
}
dir = parent
}
}
// EnsureDir creates the MQ directory if it doesn't exist.
func (q *Queue) EnsureDir() error {
return os.MkdirAll(q.dir, 0755)
}
// generateID creates a unique MR ID.
func generateID() string {
b := make([]byte, 4)
rand.Read(b)
return fmt.Sprintf("mr-%d-%s", time.Now().Unix(), hex.EncodeToString(b))
}
// Submit adds a new MR to the queue.
func (q *Queue) Submit(mr *MR) error {
if err := q.EnsureDir(); err != nil {
return fmt.Errorf("creating mq directory: %w", err)
}
if mr.ID == "" {
mr.ID = generateID()
}
if mr.CreatedAt.IsZero() {
mr.CreatedAt = time.Now()
}
data, err := json.MarshalIndent(mr, "", " ")
if err != nil {
return fmt.Errorf("marshaling MR: %w", err)
}
path := filepath.Join(q.dir, mr.ID+".json")
if err := os.WriteFile(path, data, 0644); err != nil {
return fmt.Errorf("writing MR file: %w", err)
}
return nil
}
// List returns all pending MRs, sorted by priority then creation time.
func (q *Queue) List() ([]*MR, error) {
entries, err := os.ReadDir(q.dir)
if err != nil {
if os.IsNotExist(err) {
return nil, nil // Empty queue
}
return nil, fmt.Errorf("reading mq directory: %w", err)
}
var mrs []*MR
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
continue
}
mr, err := q.load(filepath.Join(q.dir, entry.Name()))
if err != nil {
continue // Skip malformed files
}
mrs = append(mrs, mr)
}
// Sort by priority (lower first), then by creation time (older first)
sort.Slice(mrs, func(i, j int) bool {
if mrs[i].Priority != mrs[j].Priority {
return mrs[i].Priority < mrs[j].Priority
}
return mrs[i].CreatedAt.Before(mrs[j].CreatedAt)
})
return mrs, nil
}
// Get retrieves a specific MR by ID.
func (q *Queue) Get(id string) (*MR, error) {
path := filepath.Join(q.dir, id+".json")
return q.load(path)
}
// load reads an MR from a file path.
func (q *Queue) load(path string) (*MR, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var mr MR
if err := json.Unmarshal(data, &mr); err != nil {
return nil, err
}
return &mr, nil
}
// Remove deletes an MR from the queue (after successful merge).
func (q *Queue) Remove(id string) error {
path := filepath.Join(q.dir, id+".json")
err := os.Remove(path)
if os.IsNotExist(err) {
return nil // Already removed
}
return err
}
// Count returns the number of pending MRs.
func (q *Queue) Count() int {
entries, err := os.ReadDir(q.dir)
if err != nil {
return 0
}
count := 0
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".json") {
count++
}
}
return count
}
// Dir returns the queue directory path.
func (q *Queue) Dir() string {
return q.dir
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/git"
"github.com/steveyegge/gastown/internal/mrqueue"
"github.com/steveyegge/gastown/internal/rig"
)
@@ -69,6 +70,7 @@ func DefaultMergeQueueConfig() *MergeQueueConfig {
type Engineer struct {
rig *rig.Rig
beads *beads.Beads
mrQueue *mrqueue.Queue
git *git.Git
config *MergeQueueConfig
workDir string
@@ -83,6 +85,7 @@ func NewEngineer(r *rig.Rig) *Engineer {
return &Engineer{
rig: r,
beads: beads.New(r.Path),
mrQueue: mrqueue.New(r.Path),
git: git.NewGit(r.Path),
config: DefaultMergeQueueConfig(),
workDir: r.Path,
@@ -229,7 +232,7 @@ func (e *Engineer) Stop() {
}
// processOnce performs one iteration of the Engineer loop:
// 1. Query for ready merge-requests
// 1. Query for ready merge-requests from wisp storage
// 2. If none, return (will try again on next tick)
// 3. Process the highest priority, oldest MR
func (e *Engineer) processOnce(ctx context.Context) error {
@@ -240,38 +243,30 @@ func (e *Engineer) processOnce(ctx context.Context) error {
default:
}
// 1. Query: bd ready --type=merge-request (filtered client-side)
readyMRs, err := e.beads.ReadyWithType("merge-request")
// 1. Query MR queue (wisp storage - already sorted by priority/age)
pendingMRs, err := e.mrQueue.List()
if err != nil {
return fmt.Errorf("querying ready merge-requests: %w", err)
return fmt.Errorf("querying merge queue: %w", err)
}
// 2. If empty, return
if len(readyMRs) == 0 {
if len(pendingMRs) == 0 {
return nil
}
// 3. Select highest priority, oldest MR
// bd ready already returns sorted by priority then age, so first is best
mr := readyMRs[0]
// 3. Select highest priority, oldest MR (List already returns sorted)
mr := pendingMRs[0]
fmt.Fprintf(e.output, "[Engineer] Processing: %s (%s)\n", mr.ID, mr.Title)
// 4. Claim: bd update <id> --status=in_progress
inProgress := "in_progress"
if err := e.beads.Update(mr.ID, beads.UpdateOptions{Status: &inProgress}); err != nil {
return fmt.Errorf("claiming MR %s: %w", mr.ID, err)
}
// 4. Process MR
result := e.ProcessMRFromQueue(ctx, mr)
// 5. Process (delegate to ProcessMR - implementation in separate issue gt-3x1.2)
result := e.ProcessMR(ctx, mr)
// 6. Handle result
// 5. Handle result
if result.Success {
e.handleSuccess(mr, result)
e.handleSuccessFromQueue(mr, result)
} else {
// Failure handling (detailed implementation in gt-3x1.4)
e.handleFailure(mr, result)
e.handleFailureFromQueue(mr, result)
}
return nil
@@ -349,12 +344,12 @@ func (e *Engineer) handleSuccess(mr *beads.Issue, result ProcessResult) {
}
}
// 4. Delete source branch if configured
// 4. Delete source branch if configured (local only - branches never go to origin)
if e.config.DeleteMergedBranches && mrFields.Branch != "" {
if err := e.git.DeleteRemoteBranch("origin", mrFields.Branch); err != nil {
if err := e.git.DeleteBranch(mrFields.Branch, true); err != nil {
fmt.Fprintf(e.output, "[Engineer] Warning: failed to delete branch %s: %v\n", mrFields.Branch, err)
} else {
fmt.Fprintf(e.output, "[Engineer] Deleted branch: %s\n", mrFields.Branch)
fmt.Fprintf(e.output, "[Engineer] Deleted local branch: %s\n", mrFields.Branch)
}
}
@@ -376,3 +371,58 @@ func (e *Engineer) handleFailure(mr *beads.Issue, result ProcessResult) {
// Full failure handling (assign back to worker, labels) in gt-3x1.4
}
// ProcessMRFromQueue processes a merge request from wisp queue.
func (e *Engineer) ProcessMRFromQueue(ctx context.Context, mr *mrqueue.MR) ProcessResult {
// MR fields are directly on the struct (no parsing needed)
fmt.Fprintln(e.output, "[Engineer] Processing MR from queue:")
fmt.Fprintf(e.output, " Branch: %s\n", mr.Branch)
fmt.Fprintf(e.output, " Target: %s\n", mr.Target)
fmt.Fprintf(e.output, " Worker: %s\n", mr.Worker)
fmt.Fprintf(e.output, " Source: %s\n", mr.SourceIssue)
// TODO: Actual merge implementation
// For now, return failure - actual implementation in gt-3x1.2
return ProcessResult{
Success: false,
Error: "ProcessMRFromQueue not fully implemented (see gt-3x1.2)",
}
}
// handleSuccessFromQueue handles a successful merge from wisp queue.
func (e *Engineer) handleSuccessFromQueue(mr *mrqueue.MR, result ProcessResult) {
// 1. Close source issue with reference to MR
if mr.SourceIssue != "" {
closeReason := fmt.Sprintf("Merged in %s", mr.ID)
if err := e.beads.CloseWithReason(closeReason, mr.SourceIssue); err != nil {
fmt.Fprintf(e.output, "[Engineer] Warning: failed to close source issue %s: %v\n", mr.SourceIssue, err)
} else {
fmt.Fprintf(e.output, "[Engineer] Closed source issue: %s\n", mr.SourceIssue)
}
}
// 2. Delete source branch if configured (local only)
if e.config.DeleteMergedBranches && mr.Branch != "" {
if err := e.git.DeleteBranch(mr.Branch, true); err != nil {
fmt.Fprintf(e.output, "[Engineer] Warning: failed to delete branch %s: %v\n", mr.Branch, err)
} else {
fmt.Fprintf(e.output, "[Engineer] Deleted local branch: %s\n", mr.Branch)
}
}
// 3. Remove MR from queue (ephemeral - just delete the file)
if err := e.mrQueue.Remove(mr.ID); err != nil {
fmt.Fprintf(e.output, "[Engineer] Warning: failed to remove MR from queue: %v\n", err)
}
// 4. Log success
fmt.Fprintf(e.output, "[Engineer] ✓ Merged: %s (commit: %s)\n", mr.ID, result.MergeCommit)
}
// handleFailureFromQueue handles a failed merge from wisp queue.
func (e *Engineer) handleFailureFromQueue(mr *mrqueue.MR, result ProcessResult) {
// MR stays in queue for retry - no action needed on the file
// Log the failure
fmt.Fprintf(e.output, "[Engineer] ✗ Failed: %s - %s\n", mr.ID, result.Error)
fmt.Fprintln(e.output, "[Engineer] MR remains in queue for retry")
}

View File

@@ -183,7 +183,7 @@ func (m *Manager) Start(foreground bool) error {
// 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)
// Working directory is the refinery worktree (shares .git with mayor/polecats)
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
@@ -506,9 +506,9 @@ func (m *Manager) ProcessMR(mr *MergeRequest) MergeResult {
// Notify worker of success
m.notifyWorkerMerged(mr)
// Optionally delete the merged branch
// Optionally delete the merged branch (local only - branches never go to origin)
if config.DeleteMergedBranches {
_ = m.gitRun("push", "origin", "--delete", mr.Branch)
_ = m.gitRun("branch", "-D", mr.Branch)
}
return result

View File

@@ -217,20 +217,7 @@ func (m *Manager) AddRig(opts AddRigOptions) (*Rig, error) {
return nil, fmt.Errorf("saving rig config: %w", err)
}
// Clone repository for refinery (canonical main)
refineryRigPath := filepath.Join(rigPath, "refinery", "rig")
if err := os.MkdirAll(filepath.Dir(refineryRigPath), 0755); err != nil {
return nil, fmt.Errorf("creating refinery dir: %w", err)
}
if err := m.git.Clone(opts.GitURL, refineryRigPath); err != nil {
return nil, fmt.Errorf("cloning for refinery: %w", err)
}
// Create refinery CLAUDE.md (overrides any from cloned repo)
if err := m.createRoleCLAUDEmd(refineryRigPath, "refinery", opts.Name, ""); err != nil {
return nil, fmt.Errorf("creating refinery CLAUDE.md: %w", err)
}
// Clone repository for mayor
// Clone repository for mayor (must be first - serves as base for worktrees)
mayorRigPath := filepath.Join(rigPath, "mayor", "rig")
if err := os.MkdirAll(filepath.Dir(mayorRigPath), 0755); err != nil {
return nil, fmt.Errorf("creating mayor dir: %w", err)
@@ -243,6 +230,22 @@ func (m *Manager) AddRig(opts AddRigOptions) (*Rig, error) {
return nil, fmt.Errorf("creating mayor CLAUDE.md: %w", err)
}
// Create refinery as a worktree of mayor's clone.
// This allows refinery to see polecat branches locally (shared .git).
// Refinery uses the "refinery" branch which tracks main.
refineryRigPath := filepath.Join(rigPath, "refinery", "rig")
if err := os.MkdirAll(filepath.Dir(refineryRigPath), 0755); err != nil {
return nil, fmt.Errorf("creating refinery dir: %w", err)
}
mayorGit := git.NewGit(mayorRigPath)
if err := mayorGit.WorktreeAdd(refineryRigPath, "refinery"); err != nil {
return nil, fmt.Errorf("creating refinery worktree: %w", err)
}
// Create refinery CLAUDE.md (overrides any from cloned repo)
if err := m.createRoleCLAUDEmd(refineryRigPath, "refinery", opts.Name, ""); err != nil {
return nil, fmt.Errorf("creating refinery CLAUDE.md: %w", err)
}
// Clone repository for default crew workspace
crewPath := filepath.Join(rigPath, "crew", opts.CrewName)
if err := os.MkdirAll(filepath.Dir(crewPath), 0755); err != nil {