// Package witness provides the polecat monitoring agent. package witness import ( "fmt" "regexp" "strings" "time" ) // Protocol message patterns for Witness inbox routing. var ( // POLECAT_DONE - polecat signaling work completion PatternPolecatDone = regexp.MustCompile(`^POLECAT_DONE\s+(\S+)`) // LIFECYCLE:Shutdown - daemon-triggered polecat shutdown PatternLifecycleShutdown = regexp.MustCompile(`^LIFECYCLE:Shutdown\s+(\S+)`) // HELP: - polecat requesting intervention PatternHelp = regexp.MustCompile(`^HELP:\s+(.+)`) // MERGED - refinery confirms branch merged PatternMerged = regexp.MustCompile(`^MERGED\s+(\S+)`) // MERGE_FAILED - refinery reporting merge failure PatternMergeFailed = regexp.MustCompile(`^MERGE_FAILED\s+(\S+)`) // HANDOFF - session continuity message PatternHandoff = regexp.MustCompile(`^🤝\s*HANDOFF`) // SWARM_START - mayor initiating batch work PatternSwarmStart = regexp.MustCompile(`^SWARM_START`) ) // ProtocolType identifies the type of protocol message. type ProtocolType string const ( ProtoPolecatDone ProtocolType = "polecat_done" ProtoLifecycleShutdown ProtocolType = "lifecycle_shutdown" ProtoHelp ProtocolType = "help" ProtoMerged ProtocolType = "merged" ProtoMergeFailed ProtocolType = "merge_failed" ProtoHandoff ProtocolType = "handoff" ProtoSwarmStart ProtocolType = "swarm_start" ProtoUnknown ProtocolType = "unknown" ) // PolecatDonePayload contains parsed data from a POLECAT_DONE message. type PolecatDonePayload struct { PolecatName string Exit string // COMPLETED, ESCALATED, DEFERRED, PHASE_COMPLETE IssueID string MRID string Branch string Gate string // Gate ID when Exit is PHASE_COMPLETE } // HelpPayload contains parsed data from a HELP message. type HelpPayload struct { Topic string Agent string IssueID string Problem string Tried string RequestedAt time.Time } // MergedPayload contains parsed data from a MERGED message. type MergedPayload struct { PolecatName string Branch string IssueID string MergedAt time.Time } // MergeFailedPayload contains parsed data from a MERGE_FAILED message. type MergeFailedPayload struct { PolecatName string Branch string IssueID string FailureType string // "build", "test", "lint", etc. Error string FailedAt time.Time } // SwarmStartPayload contains parsed data from a SWARM_START message. type SwarmStartPayload struct { SwarmID string BeadIDs []string Total int StartedAt time.Time } // ClassifyMessage determines the protocol type from a message subject. func ClassifyMessage(subject string) ProtocolType { switch { case PatternPolecatDone.MatchString(subject): return ProtoPolecatDone case PatternLifecycleShutdown.MatchString(subject): return ProtoLifecycleShutdown case PatternHelp.MatchString(subject): return ProtoHelp case PatternMerged.MatchString(subject): return ProtoMerged case PatternMergeFailed.MatchString(subject): return ProtoMergeFailed case PatternHandoff.MatchString(subject): return ProtoHandoff case PatternSwarmStart.MatchString(subject): return ProtoSwarmStart default: return ProtoUnknown } } // ParsePolecatDone extracts payload from a POLECAT_DONE message. // Subject format: POLECAT_DONE // Body format: // // Exit: COMPLETED|ESCALATED|DEFERRED|PHASE_COMPLETE // Issue: // MR: // Gate: // Branch: func ParsePolecatDone(subject, body string) (*PolecatDonePayload, error) { matches := PatternPolecatDone.FindStringSubmatch(subject) if len(matches) < 2 { return nil, fmt.Errorf("invalid POLECAT_DONE subject: %s", subject) } payload := &PolecatDonePayload{ PolecatName: matches[1], } // Parse body for structured fields for _, line := range strings.Split(body, "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "Exit:") { payload.Exit = strings.TrimSpace(strings.TrimPrefix(line, "Exit:")) } else if strings.HasPrefix(line, "Issue:") { payload.IssueID = strings.TrimSpace(strings.TrimPrefix(line, "Issue:")) } else if strings.HasPrefix(line, "MR:") { payload.MRID = strings.TrimSpace(strings.TrimPrefix(line, "MR:")) } else if strings.HasPrefix(line, "Gate:") { payload.Gate = strings.TrimSpace(strings.TrimPrefix(line, "Gate:")) } else if strings.HasPrefix(line, "Branch:") { payload.Branch = strings.TrimSpace(strings.TrimPrefix(line, "Branch:")) } } return payload, nil } // ParseHelp extracts payload from a HELP message. // Subject format: HELP: // Body format: // // Agent: // Issue: // Problem: // Tried: func ParseHelp(subject, body string) (*HelpPayload, error) { matches := PatternHelp.FindStringSubmatch(subject) if len(matches) < 2 { return nil, fmt.Errorf("invalid HELP subject: %s", subject) } payload := &HelpPayload{ Topic: matches[1], RequestedAt: time.Now(), } // Parse body for structured fields for _, line := range strings.Split(body, "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "Agent:") { payload.Agent = strings.TrimSpace(strings.TrimPrefix(line, "Agent:")) } else if strings.HasPrefix(line, "Issue:") { payload.IssueID = strings.TrimSpace(strings.TrimPrefix(line, "Issue:")) } else if strings.HasPrefix(line, "Problem:") { payload.Problem = strings.TrimSpace(strings.TrimPrefix(line, "Problem:")) } else if strings.HasPrefix(line, "Tried:") { payload.Tried = strings.TrimSpace(strings.TrimPrefix(line, "Tried:")) } } return payload, nil } // ParseMerged extracts payload from a MERGED message. // Subject format: MERGED // Body format: // // Branch: // Issue: // Merged-At: func ParseMerged(subject, body string) (*MergedPayload, error) { matches := PatternMerged.FindStringSubmatch(subject) if len(matches) < 2 { return nil, fmt.Errorf("invalid MERGED subject: %s", subject) } payload := &MergedPayload{ PolecatName: matches[1], } // Parse body for structured fields for _, line := range strings.Split(body, "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "Branch:") { payload.Branch = strings.TrimSpace(strings.TrimPrefix(line, "Branch:")) } else if strings.HasPrefix(line, "Issue:") { payload.IssueID = strings.TrimSpace(strings.TrimPrefix(line, "Issue:")) } else if strings.HasPrefix(line, "Merged-At:") { ts := strings.TrimSpace(strings.TrimPrefix(line, "Merged-At:")) if t, err := time.Parse(time.RFC3339, ts); err == nil { payload.MergedAt = t } } } return payload, nil } // ParseMergeFailed extracts payload from a MERGE_FAILED message. // Subject format: MERGE_FAILED // Body format: // // Branch: // Issue: // FailureType: // Error: func ParseMergeFailed(subject, body string) (*MergeFailedPayload, error) { matches := PatternMergeFailed.FindStringSubmatch(subject) if len(matches) < 2 { return nil, fmt.Errorf("invalid MERGE_FAILED subject: %s", subject) } payload := &MergeFailedPayload{ PolecatName: matches[1], FailedAt: time.Now(), } // Parse body for structured fields for _, line := range strings.Split(body, "\n") { line = strings.TrimSpace(line) switch { case strings.HasPrefix(line, "Branch:"): payload.Branch = strings.TrimSpace(strings.TrimPrefix(line, "Branch:")) case strings.HasPrefix(line, "Issue:"): payload.IssueID = strings.TrimSpace(strings.TrimPrefix(line, "Issue:")) case strings.HasPrefix(line, "FailureType:"): payload.FailureType = strings.TrimSpace(strings.TrimPrefix(line, "FailureType:")) case strings.HasPrefix(line, "Error:"): payload.Error = strings.TrimSpace(strings.TrimPrefix(line, "Error:")) } } return payload, nil } // ParseSwarmStart extracts payload from a SWARM_START message. // Body format is JSON: {"swarm_id": "batch-123", "beads": ["bd-a", "bd-b"]} func ParseSwarmStart(body string) (*SwarmStartPayload, error) { payload := &SwarmStartPayload{ StartedAt: time.Now(), } // Parse the JSON-like body (simplified parsing for key-value extraction) // Full JSON parsing would require encoding/json import for _, line := range strings.Split(body, "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "SwarmID:") || strings.HasPrefix(line, "swarm_id:") { payload.SwarmID = strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(line, "SwarmID:"), "swarm_id:")) } else if strings.HasPrefix(line, "Total:") { _, _ = fmt.Sscanf(line, "Total: %d", &payload.Total) } } return payload, nil } // CleanupWispLabels generates labels for a cleanup wisp. func CleanupWispLabels(polecatName, state string) []string { return []string{ "cleanup", fmt.Sprintf("polecat:%s", polecatName), fmt.Sprintf("state:%s", state), } } // SwarmWispLabels generates labels for a swarm tracking wisp. func SwarmWispLabels(swarmID string, total, completed int, startTime time.Time) []string { return []string{ "swarm", fmt.Sprintf("swarm_id:%s", swarmID), fmt.Sprintf("total:%d", total), fmt.Sprintf("completed:%d", completed), fmt.Sprintf("start:%s", startTime.Format(time.RFC3339)), } } // HelpAssessment represents the Witness's assessment of a help request. type HelpAssessment struct { CanHelp bool HelpAction string // What the Witness can do to help NeedsEscalation bool EscalationReason string } // AssessHelpRequest provides guidance for the Witness to assess a help request. // This is a template/guide - actual assessment is done by the Claude agent. func AssessHelpRequest(payload *HelpPayload) *HelpAssessment { assessment := &HelpAssessment{} // Heuristics for common help requests that Witness can handle topic := strings.ToLower(payload.Topic) problem := strings.ToLower(payload.Problem) // Git issues - Witness can often help if strings.Contains(topic, "git") || strings.Contains(problem, "git") { if strings.Contains(problem, "conflict") { assessment.CanHelp = false assessment.NeedsEscalation = true assessment.EscalationReason = "Git conflicts require human review" } else if strings.Contains(problem, "push") || strings.Contains(problem, "fetch") { assessment.CanHelp = true assessment.HelpAction = "Check git remote status and network connectivity" } } // Test failures - usually need escalation if strings.Contains(topic, "test") || strings.Contains(problem, "test fail") { assessment.CanHelp = false assessment.NeedsEscalation = true assessment.EscalationReason = "Test failures require investigation" } // Build issues - Witness can check basics if strings.Contains(topic, "build") || strings.Contains(problem, "compile") { assessment.CanHelp = true assessment.HelpAction = "Verify dependencies and build configuration" } // Requirements unclear - always escalate if strings.Contains(topic, "unclear") || strings.Contains(problem, "requirement") || strings.Contains(problem, "don't understand") { assessment.CanHelp = false assessment.NeedsEscalation = true assessment.EscalationReason = "Requirements clarification needed from Mayor" } // Default: escalate if we don't recognize the pattern if !assessment.CanHelp && !assessment.NeedsEscalation { assessment.NeedsEscalation = true assessment.EscalationReason = "Unknown help request type" } return assessment }