Implements structured escalation channel for Gas Town: - gt escalate command with CRITICAL/HIGH/MEDIUM severity levels - Mayor startup check for pending escalations - Escalation beads with tag for audit trail - Mail routing to overseer with priority mapping - Documentation in docs/escalation.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
255 lines
7.5 KiB
Go
255 lines
7.5 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/steveyegge/gastown/internal/events"
|
|
"github.com/steveyegge/gastown/internal/mail"
|
|
"github.com/steveyegge/gastown/internal/style"
|
|
"github.com/steveyegge/gastown/internal/workspace"
|
|
)
|
|
|
|
// Escalation severity levels.
|
|
// These map to mail priorities and indicate urgency for human attention.
|
|
const (
|
|
// SeverityCritical (P0) - System-threatening issues requiring immediate human attention.
|
|
// Examples: data corruption, security breach, complete system failure.
|
|
SeverityCritical = "CRITICAL"
|
|
|
|
// SeverityHigh (P1) - Important blockers that need human attention soon.
|
|
// Examples: unresolvable merge conflicts, critical blocking bugs, ambiguous requirements.
|
|
SeverityHigh = "HIGH"
|
|
|
|
// SeverityMedium (P2) - Standard escalations for human attention at convenience.
|
|
// Examples: unclear requirements, design decisions needed, non-blocking issues.
|
|
SeverityMedium = "MEDIUM"
|
|
)
|
|
|
|
var escalateCmd = &cobra.Command{
|
|
Use: "escalate <topic>",
|
|
GroupID: GroupComm,
|
|
Short: "Escalate an issue to the human overseer",
|
|
Long: `Escalate an issue to the human overseer for attention.
|
|
|
|
This is the structured escalation channel for Gas Town. Any agent can use this
|
|
to request human intervention when automated resolution isn't possible.
|
|
|
|
Severity levels:
|
|
CRITICAL (P0) - System-threatening, immediate attention required
|
|
Examples: data corruption, security breach, system down
|
|
HIGH (P1) - Important blocker, needs human soon
|
|
Examples: unresolvable conflict, critical bug, ambiguous spec
|
|
MEDIUM (P2) - Standard escalation, human attention at convenience
|
|
Examples: design decision needed, unclear requirements
|
|
|
|
The escalation creates an audit trail bead and sends mail to the overseer
|
|
with appropriate priority. All molecular algebra edge cases should escalate
|
|
here rather than failing silently.
|
|
|
|
Examples:
|
|
gt escalate "Database migration failed"
|
|
gt escalate -s CRITICAL "Data corruption detected in user table"
|
|
gt escalate -s HIGH "Merge conflict cannot be resolved automatically"
|
|
gt escalate -s MEDIUM "Need clarification on API design" -m "Details here..."`,
|
|
Args: cobra.MinimumNArgs(1),
|
|
RunE: runEscalate,
|
|
}
|
|
|
|
var (
|
|
escalateSeverity string
|
|
escalateMessage string
|
|
escalateDryRun bool
|
|
)
|
|
|
|
func init() {
|
|
escalateCmd.Flags().StringVarP(&escalateSeverity, "severity", "s", SeverityMedium,
|
|
"Severity level: CRITICAL, HIGH, or MEDIUM")
|
|
escalateCmd.Flags().StringVarP(&escalateMessage, "message", "m", "",
|
|
"Additional details about the escalation")
|
|
escalateCmd.Flags().BoolVarP(&escalateDryRun, "dry-run", "n", false,
|
|
"Show what would be done without executing")
|
|
rootCmd.AddCommand(escalateCmd)
|
|
}
|
|
|
|
func runEscalate(cmd *cobra.Command, args []string) error {
|
|
topic := strings.Join(args, " ")
|
|
|
|
// Validate severity
|
|
severity := strings.ToUpper(escalateSeverity)
|
|
if severity != SeverityCritical && severity != SeverityHigh && severity != SeverityMedium {
|
|
return fmt.Errorf("invalid severity '%s': must be CRITICAL, HIGH, or MEDIUM", escalateSeverity)
|
|
}
|
|
|
|
// Map severity to mail priority
|
|
var priority mail.Priority
|
|
switch severity {
|
|
case SeverityCritical:
|
|
priority = mail.PriorityUrgent
|
|
case SeverityHigh:
|
|
priority = mail.PriorityHigh
|
|
default:
|
|
priority = mail.PriorityNormal
|
|
}
|
|
|
|
// Find workspace
|
|
townRoot, err := workspace.FindFromCwdOrError()
|
|
if err != nil {
|
|
return fmt.Errorf("not in a Gas Town workspace: %w", err)
|
|
}
|
|
|
|
// Detect agent identity
|
|
agentID, err := detectAgentIdentity()
|
|
if err != nil {
|
|
agentID = "unknown"
|
|
}
|
|
|
|
// Build mail subject with severity tag
|
|
subject := fmt.Sprintf("[%s] %s", severity, topic)
|
|
|
|
// Build mail body
|
|
var bodyParts []string
|
|
bodyParts = append(bodyParts, fmt.Sprintf("Escalated by: %s", agentID))
|
|
bodyParts = append(bodyParts, fmt.Sprintf("Severity: %s", severity))
|
|
if escalateMessage != "" {
|
|
bodyParts = append(bodyParts, "")
|
|
bodyParts = append(bodyParts, escalateMessage)
|
|
}
|
|
body := strings.Join(bodyParts, "\n")
|
|
|
|
// Dry run mode
|
|
if escalateDryRun {
|
|
fmt.Printf("Would create escalation:\n")
|
|
fmt.Printf(" Severity: %s\n", severity)
|
|
fmt.Printf(" Priority: %s\n", priority)
|
|
fmt.Printf(" Subject: %s\n", subject)
|
|
fmt.Printf(" Body:\n%s\n", indentText(body, " "))
|
|
fmt.Printf("Would send mail to: overseer\n")
|
|
return nil
|
|
}
|
|
|
|
// Create escalation bead for audit trail
|
|
beadID, err := createEscalationBead(topic, severity, agentID, escalateMessage)
|
|
if err != nil {
|
|
// Non-fatal - escalation mail is more important
|
|
style.PrintWarning("could not create escalation bead: %v", err)
|
|
} else {
|
|
fmt.Printf("%s Created escalation bead: %s\n", style.Bold.Render("📋"), beadID)
|
|
}
|
|
|
|
// Send mail to overseer
|
|
router := mail.NewRouter(townRoot)
|
|
msg := &mail.Message{
|
|
From: agentID,
|
|
To: "overseer",
|
|
Subject: subject,
|
|
Body: body,
|
|
Priority: priority,
|
|
}
|
|
|
|
if err := router.Send(msg); err != nil {
|
|
return fmt.Errorf("sending escalation mail: %w", err)
|
|
}
|
|
|
|
// Log to activity feed
|
|
payload := events.EscalationPayload("", agentID, "overseer", topic)
|
|
payload["severity"] = severity
|
|
if beadID != "" {
|
|
payload["bead"] = beadID
|
|
}
|
|
_ = events.LogFeed(events.TypeEscalationSent, agentID, payload)
|
|
|
|
// Print confirmation with severity-appropriate styling
|
|
var emoji string
|
|
switch severity {
|
|
case SeverityCritical:
|
|
emoji = "🚨"
|
|
case SeverityHigh:
|
|
emoji = "⚠️"
|
|
default:
|
|
emoji = "📢"
|
|
}
|
|
|
|
fmt.Printf("%s Escalation sent to overseer [%s]\n", emoji, severity)
|
|
fmt.Printf(" Topic: %s\n", topic)
|
|
if beadID != "" {
|
|
fmt.Printf(" Bead: %s\n", beadID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// detectAgentIdentity returns the current agent's identity string.
|
|
func detectAgentIdentity() (string, error) {
|
|
// Try GT_ROLE first
|
|
if role := os.Getenv("GT_ROLE"); role != "" {
|
|
return role, nil
|
|
}
|
|
|
|
// Try to detect from cwd
|
|
agentID, _, _, err := resolveSelfTarget()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return agentID, nil
|
|
}
|
|
|
|
// createEscalationBead creates a bead to track the escalation.
|
|
func createEscalationBead(topic, severity, from, details string) (string, error) {
|
|
// Use bd create to make the escalation bead
|
|
args := []string{
|
|
"create",
|
|
"--title", fmt.Sprintf("[ESCALATION] %s", topic),
|
|
"--type", "task", // Use task type since escalation isn't a standard type
|
|
"--priority", severityToBeadsPriority(severity),
|
|
}
|
|
|
|
// Add description with escalation metadata
|
|
desc := fmt.Sprintf("Escalation from: %s\nSeverity: %s\n", from, severity)
|
|
if details != "" {
|
|
desc += "\n" + details
|
|
}
|
|
args = append(args, "--description", desc)
|
|
|
|
// Add tag for filtering
|
|
args = append(args, "--tag", "escalation")
|
|
|
|
cmd := exec.Command("bd", args...)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return "", fmt.Errorf("bd create: %w", err)
|
|
}
|
|
|
|
// Parse bead ID from output (bd create outputs: "Created bead: gt-xxxxx")
|
|
output := strings.TrimSpace(string(out))
|
|
parts := strings.Split(output, ": ")
|
|
if len(parts) >= 2 {
|
|
return strings.TrimSpace(parts[len(parts)-1]), nil
|
|
}
|
|
return "", fmt.Errorf("could not parse bead ID from: %s", output)
|
|
}
|
|
|
|
// severityToBeadsPriority converts severity to beads priority string.
|
|
func severityToBeadsPriority(severity string) string {
|
|
switch severity {
|
|
case SeverityCritical:
|
|
return "0" // P0
|
|
case SeverityHigh:
|
|
return "1" // P1
|
|
default:
|
|
return "2" // P2
|
|
}
|
|
}
|
|
|
|
// indentText indents each line of text with the given prefix.
|
|
func indentText(text, prefix string) string {
|
|
lines := strings.Split(text, "\n")
|
|
for i, line := range lines {
|
|
lines[i] = prefix + line
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|