From a1de1c74d64a7aec2e7a96a6ba3e0ec038470a4a Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 16 Dec 2025 14:24:19 -0800 Subject: [PATCH] feat: add refinery mail notifications for conflict handling - notifyWorkerConflict: Mail worker with rebase instructions on conflict - notifyWorkerMerged: Mail worker on successful merge - findTownRoot: Walk up to find workspace for mail routing - High priority notification for conflicts - Integrates with mail system for agent communication This completes the semantic merge handling for MVP - conflicts are intelligently handled by notifying workers rather than silently failing. Closes gt-kmn.4 Generated with Claude Code Co-Authored-By: Claude Opus 4.5 --- internal/refinery/manager.go | 79 ++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/internal/refinery/manager.go b/internal/refinery/manager.go index 10afd820..ae3127e9 100644 --- a/internal/refinery/manager.go +++ b/internal/refinery/manager.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/steveyegge/gastown/internal/mail" "github.com/steveyegge/gastown/internal/rig" ) @@ -341,6 +342,8 @@ func (m *Manager) ProcessMR(mr *MergeRequest) MergeResult { // Abort the merge m.gitRun("merge", "--abort") m.completeMR(mr, MRFailed, "merge conflict - polecat must rebase") + // Notify worker about conflict + m.notifyWorkerConflict(mr) return result } result.Error = fmt.Sprintf("merge failed: %v", err) @@ -374,6 +377,9 @@ func (m *Manager) ProcessMR(mr *MergeRequest) MergeResult { result.Success = true m.completeMR(mr, MRMerged, "") + // Notify worker of success + m.notifyWorkerMerged(mr) + // Optionally delete the merged branch m.gitRun("push", "origin", "--delete", mr.Branch) @@ -488,3 +494,76 @@ func formatAge(t time.Time) string { } return fmt.Sprintf("%dd ago", int(d.Hours()/24)) } + +// notifyWorkerConflict sends a conflict notification to a polecat. +func (m *Manager) notifyWorkerConflict(mr *MergeRequest) { + // Find town root by walking up from rig path + townRoot := findTownRoot(m.workDir) + if townRoot == "" { + return + } + + router := mail.NewRouter(townRoot) + msg := mail.NewMessage( + fmt.Sprintf("%s/refinery", m.rig.Name), + fmt.Sprintf("%s/%s", m.rig.Name, mr.Worker), + "Merge conflict - rebase required", + fmt.Sprintf(`Your branch %s has conflicts with %s. + +Please rebase your changes: + git fetch origin + git rebase origin/%s + git push -f + +Then the Refinery will retry the merge.`, + mr.Branch, mr.TargetBranch, mr.TargetBranch), + ) + msg.Priority = mail.PriorityHigh + router.Send(msg) +} + +// notifyWorkerMerged sends a success notification to a polecat. +func (m *Manager) notifyWorkerMerged(mr *MergeRequest) { + townRoot := findTownRoot(m.workDir) + if townRoot == "" { + return + } + + router := mail.NewRouter(townRoot) + msg := mail.NewMessage( + fmt.Sprintf("%s/refinery", m.rig.Name), + fmt.Sprintf("%s/%s", m.rig.Name, mr.Worker), + "Work merged successfully", + fmt.Sprintf(`Your branch %s has been merged to %s. + +Issue: %s +Thank you for your contribution!`, + mr.Branch, mr.TargetBranch, mr.IssueID), + ) + router.Send(msg) +} + +// findTownRoot walks up directories to find the town root. +func findTownRoot(startPath string) string { + path := startPath + for { + // Check for mayor/ subdirectory (indicates town root) + if _, err := os.Stat(filepath.Join(path, "mayor")); err == nil { + return path + } + // Check for config.json with type: workspace + configPath := filepath.Join(path, "config.json") + if data, err := os.ReadFile(configPath); err == nil { + if strings.Contains(string(data), `"type": "workspace"`) { + return path + } + } + + parent := filepath.Dir(path) + if parent == path { + break // Reached root + } + path = parent + } + return "" +}