diff --git a/docs/escalation.md b/docs/escalation.md new file mode 100644 index 00000000..58b97a13 --- /dev/null +++ b/docs/escalation.md @@ -0,0 +1,149 @@ +# Gas Town Escalation Protocol + +> Reference for human escalation path in Gas Town + +## Overview + +Gas Town agents can escalate issues to the human overseer when automated +resolution isn't possible. This provides a structured channel for: + +- System-threatening errors (data corruption, security issues) +- Unresolvable conflicts (merge conflicts, ambiguous requirements) +- Design decisions requiring human judgment +- Edge cases in molecular algebra that can't proceed without guidance + +## Severity Levels + +| Level | Priority | Description | Examples | +|-------|----------|-------------|----------| +| **CRITICAL** | P0 (urgent) | System-threatening, immediate attention | Data corruption, security breach, system down | +| **HIGH** | P1 (high) | Important blocker, needs human soon | Unresolvable merge conflict, critical bug, ambiguous spec | +| **MEDIUM** | P2 (normal) | Standard escalation, human at convenience | Design decision needed, unclear requirements | + +## Escalation Command + +Any agent can escalate directly using `gt escalate`: + +```bash +# Basic escalation (default: MEDIUM severity) +gt escalate "Database migration failed" + +# Critical escalation - immediate attention +gt escalate -s CRITICAL "Data corruption detected in user table" + +# High priority escalation +gt escalate -s HIGH "Merge conflict cannot be resolved automatically" + +# With additional details +gt escalate -s MEDIUM "Need clarification on API design" -m "The spec mentions both REST and GraphQL..." +``` + +### What happens on escalation + +1. **Bead created**: An escalation bead (tagged `escalation`) is created for audit trail +2. **Mail sent**: Mail is sent to the `overseer` (human operator) with appropriate priority +3. **Activity logged**: Event logged to the activity feed for visibility + +## Escalation Flow + +``` +Any Agent Overseer (Human) + | | + | gt escalate -s HIGH "msg" | + |----------------------------->| + | | + | [ESCALATION] msg (P1 mail) | + |----------------------------->| + | | + | Reviews & resolves | + | | + | bd close | + |<-----------------------------| +``` + +## Mayor Startup Check + +On `gt prime`, the Mayor automatically checks for pending escalations: + +``` +## PENDING ESCALATIONS + +There are 3 escalation(s) awaiting human attention: + + CRITICAL: 1 + HIGH: 1 + MEDIUM: 1 + + [CRITICAL] Data corruption detected (gt-abc) + [HIGH] Merge conflict in auth module (gt-def) + [MEDIUM] API design clarification needed (gt-ghi) + +**Action required:** Review escalations with `bd list --tag=escalation` +Close resolved ones with `bd close --reason "resolution"` +``` + +## When to Escalate + +### Agents SHOULD escalate when: + +- **System errors**: Database corruption, disk full, network failures +- **Security issues**: Unauthorized access attempts, credential exposure +- **Unresolvable conflicts**: Merge conflicts that can't be auto-resolved +- **Ambiguous requirements**: Spec is unclear, multiple valid interpretations +- **Design decisions**: Architectural choices that need human judgment +- **Stuck loops**: Agent is stuck and can't make progress + +### Agents should NOT escalate for: + +- **Normal workflow**: Regular work that can proceed without human input +- **Recoverable errors**: Transient failures that will auto-retry +- **Information queries**: Questions that can be answered from context + +## Molecular Algebra Edge Cases + +All edge cases in molecular algebra should escalate rather than fail silently: + +```go +// Example: Molecule step has conflicting dependencies +if hasConflictingDeps { + // Don't fail silently - escalate + exec.Command("gt", "escalate", "-s", "HIGH", + "Molecule step has conflicting dependencies: "+stepID).Run() +} +``` + +This ensures: +1. Issues are visible to humans +2. Audit trail exists for debugging +3. System doesn't silently break + +## Viewing Escalations + +```bash +# List all open escalations +bd list --status=open --tag=escalation + +# View specific escalation +bd show + +# Close resolved escalation +bd close --reason "Resolved by fixing X" +``` + +## Integration with Existing Flow + +The escalation system integrates with the existing polecat exit flow: + +| Exit Type | When to Use | Escalation? | +|-----------|-------------|-------------| +| `COMPLETED` | Work done successfully | No | +| `ESCALATED` | Hit blocker, needs human | Yes (automatic via `gt done --exit ESCALATED`) | +| `DEFERRED` | Work paused, will resume | No | + +When a polecat uses `gt done --exit ESCALATED`: +1. Witness receives the notification +2. Witness can forward to Mayor with `ESCALATION:` subject +3. Mayor callback handler forwards to overseer + +The new `gt escalate` command provides a more direct path that any agent can use, +with structured severity levels and audit trail. diff --git a/docs/reference.md b/docs/reference.md index dcd7caaf..99b18467 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -222,6 +222,16 @@ gt mail send -s "Subject" -m "Body" gt mail send --human -s "..." # To overseer ``` +### Escalation +```bash +gt escalate "topic" # Default: MEDIUM severity +gt escalate -s CRITICAL "msg" # Urgent, immediate attention +gt escalate -s HIGH "msg" # Important blocker +gt escalate -s MEDIUM "msg" -m "Details..." +``` + +See [escalation.md](escalation.md) for full protocol. + ### Sessions ```bash gt handoff # Request cycle (context-aware) diff --git a/internal/cmd/escalate.go b/internal/cmd/escalate.go new file mode 100644 index 00000000..2d895032 --- /dev/null +++ b/internal/cmd/escalate.go @@ -0,0 +1,254 @@ +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 ", + 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") +} diff --git a/internal/cmd/prime.go b/internal/cmd/prime.go index 150dd234..f6d64eac 100644 --- a/internal/cmd/prime.go +++ b/internal/cmd/prime.go @@ -135,6 +135,11 @@ func runPrime(cmd *cobra.Command, args []string) error { // Run gt mail check --inject to inject any pending mail runMailCheckInject(cwd) + // For Mayor, check for pending escalations + if ctx.Role == RoleMayor { + checkPendingEscalations(ctx) + } + // Output startup directive for roles that should announce themselves // Skip if in autonomous mode (slung work provides its own directive) if !hasSlungWork { @@ -1298,3 +1303,90 @@ func ensureBeadsRedirect(ctx RoleContext) { // Note: We don't print a message here to avoid cluttering prime output // The redirect is silently restored } + +// checkPendingEscalations queries for open escalation beads and displays them prominently. +// This is called on Mayor startup to surface issues needing human attention. +func checkPendingEscalations(ctx RoleContext) { + // Query for open escalations using bd list with tag filter + cmd := exec.Command("bd", "list", "--status=open", "--tag=escalation", "--json") + cmd.Dir = ctx.WorkDir + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + // Silently skip - escalation check is best-effort + return + } + + // Parse JSON output + var escalations []struct { + ID string `json:"id"` + Title string `json:"title"` + Priority int `json:"priority"` + Description string `json:"description"` + Created string `json:"created"` + } + + if err := json.Unmarshal(stdout.Bytes(), &escalations); err != nil || len(escalations) == 0 { + // No escalations or parse error + return + } + + // Count by severity + critical := 0 + high := 0 + medium := 0 + for _, e := range escalations { + switch e.Priority { + case 0: + critical++ + case 1: + high++ + default: + medium++ + } + } + + // Display prominently + fmt.Println() + fmt.Printf("%s\n\n", style.Bold.Render("## 🚨 PENDING ESCALATIONS")) + fmt.Printf("There are %d escalation(s) awaiting human attention:\n\n", len(escalations)) + + if critical > 0 { + fmt.Printf(" 🔴 CRITICAL: %d\n", critical) + } + if high > 0 { + fmt.Printf(" 🟠 HIGH: %d\n", high) + } + if medium > 0 { + fmt.Printf(" 🟡 MEDIUM: %d\n", medium) + } + fmt.Println() + + // Show first few escalations + maxShow := 5 + if len(escalations) < maxShow { + maxShow = len(escalations) + } + for i := 0; i < maxShow; i++ { + e := escalations[i] + severity := "MEDIUM" + switch e.Priority { + case 0: + severity = "CRITICAL" + case 1: + severity = "HIGH" + } + fmt.Printf(" • [%s] %s (%s)\n", severity, e.Title, e.ID) + } + if len(escalations) > maxShow { + fmt.Printf(" ... and %d more\n", len(escalations)-maxShow) + } + fmt.Println() + + fmt.Println("**Action required:** Review escalations with `bd list --tag=escalation`") + fmt.Println("Close resolved ones with `bd close --reason \"resolution\"`") + fmt.Println() +}