Add human escalation path with severity levels (gt-1z3z)

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>
This commit is contained in:
Steve Yegge
2025-12-30 09:56:20 -08:00
parent ab30b974a4
commit a7831ba11d
4 changed files with 505 additions and 0 deletions

149
docs/escalation.md Normal file
View File

@@ -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 <esc-id> |
|<-----------------------------|
```
## 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 <id> --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 <escalation-id>
# Close resolved escalation
bd close <id> --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.

View File

@@ -222,6 +222,16 @@ gt mail send <addr> -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)

254
internal/cmd/escalate.go Normal file
View File

@@ -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 <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")
}

View File

@@ -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 <id> --reason \"resolution\"`")
fmt.Println()
}