package protocol import ( "fmt" "strings" "time" "github.com/steveyegge/gastown/internal/mail" ) // NewMergeReadyMessage creates a MERGE_READY protocol message. // Sent by Witness to Refinery when a polecat's work is verified and ready. func NewMergeReadyMessage(rig, polecat, branch, issue string) *mail.Message { payload := MergeReadyPayload{ Branch: branch, Issue: issue, Polecat: polecat, Rig: rig, Verified: "clean git state, issue closed", Timestamp: time.Now(), } body := formatMergeReadyBody(payload) msg := mail.NewMessage( fmt.Sprintf("%s/witness", rig), fmt.Sprintf("%s/refinery", rig), fmt.Sprintf("MERGE_READY %s", polecat), body, ) msg.Priority = mail.PriorityHigh msg.Type = mail.TypeTask return msg } // formatMergeReadyBody formats the body of a MERGE_READY message. func formatMergeReadyBody(p MergeReadyPayload) string { var sb strings.Builder sb.WriteString(fmt.Sprintf("Branch: %s\n", p.Branch)) sb.WriteString(fmt.Sprintf("Issue: %s\n", p.Issue)) sb.WriteString(fmt.Sprintf("Polecat: %s\n", p.Polecat)) sb.WriteString(fmt.Sprintf("Rig: %s\n", p.Rig)) if p.Verified != "" { sb.WriteString(fmt.Sprintf("Verified: %s\n", p.Verified)) } return sb.String() } // NewMergedMessage creates a MERGED protocol message. // Sent by Refinery to Witness when a branch is successfully merged. func NewMergedMessage(rig, polecat, branch, issue, targetBranch, mergeCommit string) *mail.Message { payload := MergedPayload{ Branch: branch, Issue: issue, Polecat: polecat, Rig: rig, MergedAt: time.Now(), MergeCommit: mergeCommit, TargetBranch: targetBranch, } body := formatMergedBody(payload) msg := mail.NewMessage( fmt.Sprintf("%s/refinery", rig), fmt.Sprintf("%s/witness", rig), fmt.Sprintf("MERGED %s", polecat), body, ) msg.Priority = mail.PriorityHigh msg.Type = mail.TypeNotification return msg } // formatMergedBody formats the body of a MERGED message. func formatMergedBody(p MergedPayload) string { var sb strings.Builder sb.WriteString(fmt.Sprintf("Branch: %s\n", p.Branch)) sb.WriteString(fmt.Sprintf("Issue: %s\n", p.Issue)) sb.WriteString(fmt.Sprintf("Polecat: %s\n", p.Polecat)) sb.WriteString(fmt.Sprintf("Rig: %s\n", p.Rig)) sb.WriteString(fmt.Sprintf("Target: %s\n", p.TargetBranch)) sb.WriteString(fmt.Sprintf("Merged-At: %s\n", p.MergedAt.Format(time.RFC3339))) if p.MergeCommit != "" { sb.WriteString(fmt.Sprintf("Merge-Commit: %s\n", p.MergeCommit)) } return sb.String() } // NewMergeFailedMessage creates a MERGE_FAILED protocol message. // Sent by Refinery to Witness when merge fails (tests, build, etc.). func NewMergeFailedMessage(rig, polecat, branch, issue, targetBranch, failureType, errorMsg string) *mail.Message { payload := MergeFailedPayload{ Branch: branch, Issue: issue, Polecat: polecat, Rig: rig, FailedAt: time.Now(), FailureType: failureType, Error: errorMsg, TargetBranch: targetBranch, } body := formatMergeFailedBody(payload) msg := mail.NewMessage( fmt.Sprintf("%s/refinery", rig), fmt.Sprintf("%s/witness", rig), fmt.Sprintf("MERGE_FAILED %s", polecat), body, ) msg.Priority = mail.PriorityHigh msg.Type = mail.TypeTask return msg } // formatMergeFailedBody formats the body of a MERGE_FAILED message. func formatMergeFailedBody(p MergeFailedPayload) string { var sb strings.Builder sb.WriteString(fmt.Sprintf("Branch: %s\n", p.Branch)) sb.WriteString(fmt.Sprintf("Issue: %s\n", p.Issue)) sb.WriteString(fmt.Sprintf("Polecat: %s\n", p.Polecat)) sb.WriteString(fmt.Sprintf("Rig: %s\n", p.Rig)) sb.WriteString(fmt.Sprintf("Target: %s\n", p.TargetBranch)) sb.WriteString(fmt.Sprintf("Failed-At: %s\n", p.FailedAt.Format(time.RFC3339))) sb.WriteString(fmt.Sprintf("Failure-Type: %s\n", p.FailureType)) sb.WriteString(fmt.Sprintf("Error: %s\n", p.Error)) return sb.String() } // NewReworkRequestMessage creates a REWORK_REQUEST protocol message. // Sent by Refinery to Witness when a branch needs rebasing due to conflicts. func NewReworkRequestMessage(rig, polecat, branch, issue, targetBranch string, conflictFiles []string) *mail.Message { payload := ReworkRequestPayload{ Branch: branch, Issue: issue, Polecat: polecat, Rig: rig, RequestedAt: time.Now(), TargetBranch: targetBranch, ConflictFiles: conflictFiles, Instructions: formatRebaseInstructions(targetBranch), } body := formatReworkRequestBody(payload) msg := mail.NewMessage( fmt.Sprintf("%s/refinery", rig), fmt.Sprintf("%s/witness", rig), fmt.Sprintf("REWORK_REQUEST %s", polecat), body, ) msg.Priority = mail.PriorityHigh msg.Type = mail.TypeTask return msg } // formatReworkRequestBody formats the body of a REWORK_REQUEST message. func formatReworkRequestBody(p ReworkRequestPayload) string { var sb strings.Builder sb.WriteString(fmt.Sprintf("Branch: %s\n", p.Branch)) sb.WriteString(fmt.Sprintf("Issue: %s\n", p.Issue)) sb.WriteString(fmt.Sprintf("Polecat: %s\n", p.Polecat)) sb.WriteString(fmt.Sprintf("Rig: %s\n", p.Rig)) sb.WriteString(fmt.Sprintf("Target: %s\n", p.TargetBranch)) sb.WriteString(fmt.Sprintf("Requested-At: %s\n", p.RequestedAt.Format(time.RFC3339))) if len(p.ConflictFiles) > 0 { sb.WriteString(fmt.Sprintf("Conflict-Files: %s\n", strings.Join(p.ConflictFiles, ", "))) } sb.WriteString("\n") sb.WriteString(p.Instructions) return sb.String() } // formatRebaseInstructions returns standard rebase instructions. func formatRebaseInstructions(targetBranch string) string { return fmt.Sprintf(`Please rebase your changes onto %s: git fetch origin git rebase origin/%s # Resolve any conflicts git push -f The Refinery will retry the merge after rebase is complete.`, targetBranch, targetBranch) } // ParseMergeReadyPayload parses a MERGE_READY message body into a payload. func ParseMergeReadyPayload(body string) *MergeReadyPayload { return &MergeReadyPayload{ Branch: parseField(body, "Branch"), Issue: parseField(body, "Issue"), Polecat: parseField(body, "Polecat"), Rig: parseField(body, "Rig"), Verified: parseField(body, "Verified"), Timestamp: time.Now(), // Use current time if not parseable } } // ParseMergedPayload parses a MERGED message body into a payload. func ParseMergedPayload(body string) *MergedPayload { payload := &MergedPayload{ Branch: parseField(body, "Branch"), Issue: parseField(body, "Issue"), Polecat: parseField(body, "Polecat"), Rig: parseField(body, "Rig"), TargetBranch: parseField(body, "Target"), MergeCommit: parseField(body, "Merge-Commit"), } // Parse timestamp if ts := parseField(body, "Merged-At"); ts != "" { if t, err := time.Parse(time.RFC3339, ts); err == nil { payload.MergedAt = t } } return payload } // ParseMergeFailedPayload parses a MERGE_FAILED message body into a payload. func ParseMergeFailedPayload(body string) *MergeFailedPayload { payload := &MergeFailedPayload{ Branch: parseField(body, "Branch"), Issue: parseField(body, "Issue"), Polecat: parseField(body, "Polecat"), Rig: parseField(body, "Rig"), TargetBranch: parseField(body, "Target"), FailureType: parseField(body, "Failure-Type"), Error: parseField(body, "Error"), } // Parse timestamp if ts := parseField(body, "Failed-At"); ts != "" { if t, err := time.Parse(time.RFC3339, ts); err == nil { payload.FailedAt = t } } return payload } // ParseReworkRequestPayload parses a REWORK_REQUEST message body into a payload. func ParseReworkRequestPayload(body string) *ReworkRequestPayload { payload := &ReworkRequestPayload{ Branch: parseField(body, "Branch"), Issue: parseField(body, "Issue"), Polecat: parseField(body, "Polecat"), Rig: parseField(body, "Rig"), TargetBranch: parseField(body, "Target"), } // Parse timestamp if ts := parseField(body, "Requested-At"); ts != "" { if t, err := time.Parse(time.RFC3339, ts); err == nil { payload.RequestedAt = t } } // Parse conflict files if files := parseField(body, "Conflict-Files"); files != "" { payload.ConflictFiles = strings.Split(files, ", ") } return payload } // parseField extracts a field value from a key-value body format. // Format: "Key: value" func parseField(body, key string) string { lines := strings.Split(body, "\n") prefix := key + ": " for _, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, prefix) { return strings.TrimPrefix(line, prefix) } } return "" }