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:
149
docs/escalation.md
Normal file
149
docs/escalation.md
Normal 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.
|
||||
@@ -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
254
internal/cmd/escalate.go
Normal 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")
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user