From ede5406d36f3b2d7e33ec761ec429345ebaa3d33 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 30 Dec 2025 01:53:42 -0800 Subject: [PATCH] ZFC #5: Move merge/conflict decisions from Go to Refinery agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove decision-making logic from Go code and document agent responsibility: - ProcessMR() now returns error indicating agent handles processing - Foreground mode deprecated with message directing to background mode - Retry() no longer calls ProcessMR (agent picks up retried MRs) - Added ZFC compliance section to refinery role template - Marked unused helper functions as deprecated (runTests, getMergeConfig, pushWithRetry) The Refinery agent (Claude) now: - Runs git commands directly (fetch, checkout, merge, push) - Detects conflicts and decides: retry, notify polecat, escalate - Runs tests and decides: proceed, rollback, retry - Makes all engineering decisions based on command output Go code provides only primitives: queue listing, status, mail notifications. (gt-sxa64) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/refinery/manager.go | 185 +++++----------------- internal/templates/roles/refinery.md.tmpl | 38 +++++ 2 files changed, 74 insertions(+), 149 deletions(-) diff --git a/internal/refinery/manager.go b/internal/refinery/manager.go index 098d918c..fae1ae50 100644 --- a/internal/refinery/manager.go +++ b/internal/refinery/manager.go @@ -332,45 +332,18 @@ func (m *Manager) branchToMR(branch string) *MergeRequest { } } -// run is the main processing loop (for foreground mode). +// run is deprecated - foreground mode now just prints a message. +// The Refinery agent (Claude) handles all merge processing. +// See: ZFC #5 - Move merge/conflict decisions from Go to Refinery agent func (m *Manager) run(ref *Refinery) error { - fmt.Fprintln(m.output, "Refinery running...") - fmt.Fprintln(m.output, "Press Ctrl+C to stop") - - ticker := time.NewTicker(10 * time.Second) - defer ticker.Stop() - - for range ticker.C { - // Process queue - if err := m.ProcessQueue(); err != nil { - fmt.Fprintf(m.output, "Queue processing error: %v\n", err) - } - } - return nil -} - -// ProcessQueue processes all pending merge requests. -func (m *Manager) ProcessQueue() error { - queue, err := m.Queue() - if err != nil { - return err - } - - for _, item := range queue { - if !item.MR.IsOpen() { - continue - } - - fmt.Fprintf(m.output, "Processing: %s (%s)\n", item.MR.Branch, item.MR.Worker) - - result := m.ProcessMR(item.MR) - if result.Success { - fmt.Fprintln(m.output, " ✓ Merged successfully") - } else { - fmt.Fprintf(m.output, " ✗ Failed: %s\n", result.Error) - } - } - + fmt.Fprintln(m.output, "") + fmt.Fprintln(m.output, "╔══════════════════════════════════════════════════════════════╗") + fmt.Fprintln(m.output, "║ Foreground mode is deprecated. ║") + fmt.Fprintln(m.output, "║ ║") + fmt.Fprintln(m.output, "║ The Refinery agent (Claude) handles all merge decisions. ║") + fmt.Fprintln(m.output, "║ Use 'gt refinery start' to run in background mode. ║") + fmt.Fprintln(m.output, "╚══════════════════════════════════════════════════════════════╝") + fmt.Fprintln(m.output, "") return nil } @@ -383,113 +356,24 @@ type MergeResult struct { TestsFailed bool } -// ProcessMR processes a single merge request. +// ProcessMR is deprecated - the Refinery agent now handles all merge processing. +// +// ZFC #5: Move merge/conflict decisions from Go to Refinery agent +// +// The agent runs git commands directly and makes decisions based on output: +// - Agent attempts merge: git checkout -b temp origin/polecat/ +// - Agent detects conflict and decides: retry, notify polecat, escalate +// - Agent runs tests and decides: proceed, rollback, retry +// - Agent pushes: git push origin main +// +// This function is kept for backwards compatibility but always returns an error +// indicating that the agent should handle merge processing. +// +// Deprecated: Use the Refinery agent (Claude) for merge processing. func (m *Manager) ProcessMR(mr *MergeRequest) MergeResult { - ref, _ := m.loadState() - config := m.getMergeConfig() - - // Claim the MR (open → in_progress) - if err := mr.Claim(); err != nil { - return MergeResult{Error: fmt.Sprintf("cannot claim MR: %v", err)} + return MergeResult{ + Error: "ProcessMR is deprecated - the Refinery agent handles merge processing (ZFC #5)", } - ref.CurrentMR = mr - _ = m.saveState(ref) // non-fatal: state file update - - // Emit merge_started event - actor := fmt.Sprintf("%s/refinery", m.rig.Name) - _ = events.LogFeed(events.TypeMergeStarted, actor, events.MergePayload(mr.ID, mr.Worker, mr.Branch, "")) - - result := MergeResult{} - - // 1. Fetch the branch - if err := m.gitRun("fetch", "origin", mr.Branch); err != nil { - result.Error = fmt.Sprintf("fetch failed: %v", err) - _ = events.LogFeed(events.TypeMergeFailed, actor, events.MergePayload(mr.ID, mr.Worker, mr.Branch, result.Error)) - m.completeMR(mr, "", result.Error) // Reopen for retry - return result - } - - // 2. Checkout target branch - if err := m.gitRun("checkout", mr.TargetBranch); err != nil { - result.Error = fmt.Sprintf("checkout target failed: %v", err) - _ = events.LogFeed(events.TypeMergeFailed, actor, events.MergePayload(mr.ID, mr.Worker, mr.Branch, result.Error)) - m.completeMR(mr, "", result.Error) // Reopen for retry - return result - } - - // Pull latest (non-fatal: may fail if remote unreachable) - _ = m.gitRun("pull", "origin", mr.TargetBranch) - - // 3. Merge - err := m.gitRun("merge", "--no-ff", "-m", - fmt.Sprintf("Merge %s from %s", mr.Branch, mr.Worker), - "origin/"+mr.Branch) - - if err != nil { - errStr := err.Error() - if strings.Contains(errStr, "CONFLICT") || strings.Contains(errStr, "conflict") { - result.Conflict = true - result.Error = "merge conflict" - // Abort the merge (best-effort cleanup) - _ = m.gitRun("merge", "--abort") - _ = events.LogFeed(events.TypeMergeFailed, actor, events.MergePayload(mr.ID, mr.Worker, mr.Branch, "merge conflict")) - m.completeMR(mr, "", "merge conflict - polecat must rebase") // Reopen for rebase - // Notify worker about conflict - m.notifyWorkerConflict(mr) - return result - } - result.Error = fmt.Sprintf("merge failed: %v", err) - _ = events.LogFeed(events.TypeMergeFailed, actor, events.MergePayload(mr.ID, mr.Worker, mr.Branch, result.Error)) - m.completeMR(mr, "", result.Error) // Reopen for retry - return result - } - - // 4. Run tests if configured - if config.RunTests && config.TestCommand != "" { - if err := m.runTests(config.TestCommand); err != nil { - result.TestsFailed = true - result.Error = fmt.Sprintf("tests failed: %v", err) - // Reset to before merge (best-effort rollback) - _ = m.gitRun("reset", "--hard", "HEAD~1") - _ = events.LogFeed(events.TypeMergeFailed, actor, events.MergePayload(mr.ID, mr.Worker, mr.Branch, result.Error)) - m.completeMR(mr, "", result.Error) // Reopen for fixes - return result - } - } - - // 5. Push with retry logic - if err := m.pushWithRetry(mr.TargetBranch, config); err != nil { - result.Error = fmt.Sprintf("push failed: %v", err) - // Reset to before merge (best-effort rollback) - _ = m.gitRun("reset", "--hard", "HEAD~1") - _ = events.LogFeed(events.TypeMergeFailed, actor, events.MergePayload(mr.ID, mr.Worker, mr.Branch, result.Error)) - m.completeMR(mr, "", result.Error) // Reopen for retry - return result - } - - // 6. Get merge commit SHA - mergeCommit, err := m.gitOutput("rev-parse", "HEAD") - if err != nil { - mergeCommit = "" // Non-fatal, continue - } - - // Success! - result.Success = true - result.MergeCommit = mergeCommit - m.completeMR(mr, CloseReasonMerged, "") - - // Emit merged event - _ = events.LogFeed(events.TypeMerged, actor, events.MergePayload(mr.ID, mr.Worker, mr.Branch, "")) - - // Notify worker of success - m.notifyWorkerMerged(mr) - - // Optionally delete the merged branch (non-fatal: cleanup only) - if config.DeleteMergedBranches { - _ = m.gitRun("branch", "-D", mr.Branch) - } - - return result } // completeMR marks an MR as complete. @@ -528,6 +412,7 @@ func (m *Manager) completeMR(mr *MergeRequest, closeReason CloseReason, errMsg s } // runTests executes the test command. +// Deprecated: The Refinery agent runs tests directly via shell commands (ZFC #5). func (m *Manager) runTests(testCmd string) error { parts := strings.Fields(testCmd) if len(parts) == 0 { @@ -588,6 +473,7 @@ func (m *Manager) gitOutput(args ...string) (string, error) { // getMergeConfig loads the merge configuration from disk. // Returns default config if not configured. +// Deprecated: Configuration is read by the agent from settings (ZFC #5). func (m *Manager) getMergeConfig() MergeConfig { mergeConfig := DefaultMergeConfig() @@ -611,6 +497,7 @@ func (m *Manager) getMergeConfig() MergeConfig { } // pushWithRetry pushes to the target branch with exponential backoff retry. +// Deprecated: The Refinery agent decides retry strategy (ZFC #5). func (m *Manager) pushWithRetry(targetBranch string, config MergeConfig) error { var lastErr error delay := time.Duration(config.PushRetryDelayMs) * time.Millisecond @@ -743,7 +630,8 @@ func (m *Manager) FindMR(idOrBranch string) (*MergeRequest, error) { } // Retry resets a failed merge request so it can be processed again. -// If processNow is true, immediately processes the MR instead of waiting for the loop. +// The processNow parameter is deprecated - the Refinery agent handles processing. +// Clearing the error is sufficient; the agent will pick up the MR in its next patrol cycle. func (m *Manager) Retry(id string, processNow bool) error { ref, err := m.loadState() if err != nil { @@ -772,12 +660,11 @@ func (m *Manager) Retry(id string, processNow bool) error { return err } - // If --now flag, process immediately + // Note: processNow is deprecated (ZFC #5). + // The Refinery agent handles merge processing. + // It will pick up this MR in its next patrol cycle. if processNow { - result := m.ProcessMR(mr) - if !result.Success { - return fmt.Errorf("retry failed: %s", result.Error) - } + fmt.Fprintln(m.output, "Note: --now is deprecated. The Refinery agent will process this MR in its next patrol cycle.") } return nil diff --git a/internal/templates/roles/refinery.md.tmpl b/internal/templates/roles/refinery.md.tmpl index ec40dc1c..5feaf266 100644 --- a/internal/templates/roles/refinery.md.tmpl +++ b/internal/templates/roles/refinery.md.tmpl @@ -80,6 +80,44 @@ queue for your rig, merging polecat work to main one at a time with sequential r **The Scotty Test**: Before proceeding past any failure, ask yourself: "Would Scotty walk past a warp core leak because it existed before his shift?" +## 🔧 ZFC Compliance: Agent-Driven Decisions + +**You are the decision maker.** All merge/conflict decisions are made by you, the agent, +not by Go code. This follows the Zero Friction Control (ZFC) principle. + +**Your Decision Domain:** + +| Situation | Your Decision | +|-----------|---------------| +| Merge conflict detected | Abort, notify polecat, or attempt resolution | +| Tests fail after merge | Rollback, notify polecat, investigate cause | +| Push fails | Retry with backoff, or abort and investigate | +| Pre-existing test failure | Fix it yourself or file bead for tracking | +| Uncertain merge order | Choose based on priority, dependencies, timing | + +**Why This Matters:** +- Go code provides git operations (fetch, checkout, merge, push) +- You run those commands and interpret the results +- You decide what to do when things go wrong +- This makes the system auditable - your decisions are logged + +**Anti-patterns to Avoid:** +- DON'T rely on Go code to decide conflict handling +- DON'T expect automated rollback - you decide when to rollback +- DON'T assume retry logic - you decide retry strategy + +**Example: Handling a Conflict** +```bash +git checkout -b temp origin/polecat/rictus-12345 +git rebase origin/main +# If conflict: +git status # See what conflicted +# DECISION: Can I resolve it? Is it trivial? +# - If trivial: fix, git add, git rebase --continue +# - If complex: git rebase --abort, notify polecat +gt mail send gastown/polecats/rictus -s "Rebase needed" -m "..." +``` + ## Patrol Molecule: mol-refinery-patrol Your work is defined by the `mol-refinery-patrol` molecule with these steps: