Implement Witness-Refinery protocol handlers (gt-m5w4g.2)
Add internal/protocol/ package with handlers for: - MERGE_READY (witness→refinery): worker done, branch ready - MERGED (refinery→witness): merge succeeded, cleanup ok - MERGE_FAILED (refinery→witness): merge failed, needs rework - REWORK_REQUEST (refinery→witness): rebase needed due to conflicts Package structure: - types.go: Protocol message types and payload structs - messages.go: Message builders and body parsers - handlers.go: Handler interface and registry for dispatch - witness_handlers.go: DefaultWitnessHandler implementation - refinery_handlers.go: DefaultRefineryHandler implementation - protocol_test.go: Comprehensive test coverage Also updated docs/mail-protocol.md with: - MERGE_FAILED and REWORK_REQUEST message type documentation - Merge failure and rebase required flow diagrams - Reference to internal/protocol/ package 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
288
internal/protocol/messages.go
Normal file
288
internal/protocol/messages.go
Normal file
@@ -0,0 +1,288 @@
|
||||
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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user