ZFC #5: Move merge/conflict decisions from Go to Refinery agent

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 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-30 01:53:42 -08:00
parent e72882cdcb
commit ede5406d36
2 changed files with 74 additions and 149 deletions

View File

@@ -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/<worker>
// - 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