Files
gastown/internal/formula/formulas/mol-shutdown-dance.formula.toml
organic b649635f48 feat(formula): add shutdown-dance molecule for death warrant execution
Defines the state machine that Dogs execute for death warrants:
- 3-attempt interrogation with escalating timeouts (60s, 120s, 240s)
- PARDON path when session responds with ALIVE
- EXECUTE path after all attempts exhausted
- EPITAPH step for audit logging

Key design decisions documented:
- Dogs are goroutines, not Claude sessions
- Timeout gates close on timer OR early response detection
- State persisted to ~/gt/deacon/dogs/active/ for crash recovery

Implements specification for gt-cd404.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 20:44:30 -08:00

520 lines
15 KiB
TOML

description = """
Death warrant execution state machine for Dogs.
Dogs execute this molecule to process death warrants. Each Dog is a lightweight
goroutine (NOT a Claude session) that runs the interrogation state machine.
## Architecture Context
Dogs are lightweight workers in Boot's pool (see dog-pool-architecture.md):
- Fixed pool of 5 goroutines (configurable via GT_DOG_POOL_SIZE)
- State persisted to ~/gt/deacon/dogs/active/<id>.json
- Recovery on Boot restart via orphan state files
## State Machine
```
┌─────────────────────────────────────────┐
│ │
▼ │
┌───────────────────────────┐ │
│ INTERROGATING │ │
│ │ │
│ 1. Send health check │ │
│ 2. Open timeout gate │ │
└───────────┬───────────────┘ │
│ │
│ gate closes (timeout or response) │
▼ │
┌───────────────────────────┐ │
│ EVALUATING │ │
│ │ │
│ Check tmux output for │ │
│ ALIVE keyword │ │
└───────────┬───────────────┘ │
│ │
┌───────┴───────┐ │
│ │ │
▼ ▼ │
[ALIVE found] [No ALIVE] │
│ │ │
│ │ attempt < 3? │
│ ├──────────────────────────────────→─┘
│ │ yes: attempt++, longer timeout
│ │
│ │ no: attempt == 3
▼ ▼
┌─────────┐ ┌─────────────┐
│ PARDONED│ │ EXECUTING │
│ │ │ │
│ Cancel │ │ Kill tmux │
│ warrant │ │ session │
└────┬────┘ └──────┬──────┘
│ │
└────────┬───────┘
┌────────────────┐
│ EPITAPH │
│ │
│ Log outcome │
│ Release dog │
└────────────────┘
```
## Timeout Gates
| Attempt | Timeout | Cumulative Wait |
|---------|---------|-----------------|
| 1 | 60s | 60s |
| 2 | 120s | 180s (3 min) |
| 3 | 240s | 420s (7 min) |
Timeout gates work like this:
- Gate opens when interrogation message is sent
- Gate closes when EITHER:
a) Timeout expires (proceed to evaluate)
b) Response detected (early close, proceed to evaluate)
- The gate state determines the evaluation outcome
## Interrogation Message Format
```
[DOG] HEALTH CHECK: Session {target}, respond ALIVE within {timeout}s or face termination.
Warrant reason: {reason}
Filed by: {requester}
Attempt: {attempt}/3
```
## Response Detection
The Dog checks tmux output for:
1. The ALIVE keyword (explicit response)
2. Any Claude output after the health check (implicit activity)
```go
func (d *Dog) CheckForResponse() bool {
output := tmux.CapturePane(d.Warrant.Target, 50) // Last 50 lines
return strings.Contains(output, "ALIVE")
}
```
## Variables
| Variable | Source | Description |
|-------------|-------------|-----------------------------------------------|
| warrant_id | hook_bead | Bead ID of the death warrant |
| target | warrant | Session name to interrogate |
| reason | warrant | Why warrant was issued |
| requester | warrant | Who filed the warrant (e.g., deacon, witness) |
## Integration
Dogs are NOT Claude sessions. This molecule is:
1. A specification document (defines the state machine)
2. A reference for Go implementation in internal/shutdown/
3. A template for creating warrant-tracking beads
The Go implementation follows this spec exactly."""
formula = "mol-shutdown-dance"
version = 1
[squash]
trigger = "on_complete"
template_type = "operational"
include_metrics = true
# ============================================================================
# STEP 1: WARRANT_RECEIVED
# ============================================================================
[[steps]]
id = "warrant-received"
title = "Receive and validate death warrant"
description = """
Entry point when Dog is allocated from pool.
**1. Read warrant from allocation:**
The Dog receives a Warrant struct containing:
- ID: Bead ID of the warrant
- Target: Session name (e.g., "gt-gastown-Toast")
- Reason: Why termination requested
- Requester: Who filed (deacon, witness, mayor)
- FiledAt: Timestamp
**2. Validate target exists:**
```bash
tmux has-session -t {target} 2>/dev/null
```
If target doesn't exist:
- Warrant is stale (already dead)
- Skip to EPITAPH with outcome=already_dead
**3. Initialize state file:**
Write initial state to ~/gt/deacon/dogs/active/{dog-id}.json
**4. Set initial attempt counter:**
attempt = 1
**Exit criteria:** Warrant validated, target confirmed alive, state initialized."""
# ============================================================================
# STEP 2: INTERROGATION_1 (60s timeout)
# ============================================================================
[[steps]]
id = "interrogation-1"
title = "First interrogation (60s timeout)"
needs = ["warrant-received"]
description = """
First attempt to contact the session.
**1. Compose health check message:**
```
[DOG] HEALTH CHECK: Session {target}, respond ALIVE within 60s or face termination.
Warrant reason: {reason}
Filed by: {requester}
Attempt: 1/3
```
**2. Send via tmux:**
```bash
tmux send-keys -t {target} "{message}" Enter
```
**3. Open timeout gate:**
Gate configuration:
- Type: timer
- Timeout: 60 seconds
- Close conditions:
a) Timer expires
b) ALIVE keyword detected in output
**4. Wait for gate to close:**
The Dog waits (select on timer channel or early close signal).
**5. Record interrogation timestamp:**
Update state file with last_message_at.
**Exit criteria:** Message sent, waiting for gate to close."""
# ============================================================================
# STEP 3: EVALUATE_1
# ============================================================================
[[steps]]
id = "evaluate-1"
title = "Evaluate first interrogation response"
needs = ["interrogation-1"]
description = """
Check if session responded to first interrogation.
**1. Capture tmux output:**
```bash
tmux capture-pane -t {target} -p | tail -50
```
**2. Check for ALIVE keyword:**
```go
if strings.Contains(output, "ALIVE") {
return PARDONED
}
```
**3. Decision:**
- ALIVE found → Proceed to PARDON
- No ALIVE → Proceed to INTERROGATION_2
**Exit criteria:** Response evaluated, next step determined."""
# ============================================================================
# STEP 4: INTERROGATION_2 (120s timeout)
# ============================================================================
[[steps]]
id = "interrogation-2"
title = "Second interrogation (120s timeout)"
needs = ["evaluate-1"]
gate = { type = "conditional", condition = "no_response_1" }
description = """
Second attempt with longer timeout.
Only executed if evaluate-1 found no response.
**1. Increment attempt:**
attempt = 2
**2. Compose health check message:**
```
[DOG] HEALTH CHECK: Session {target}, respond ALIVE within 120s or face termination.
Warrant reason: {reason}
Filed by: {requester}
Attempt: 2/3
```
**3. Send via tmux:**
```bash
tmux send-keys -t {target} "{message}" Enter
```
**4. Open timeout gate:**
- Type: timer
- Timeout: 120 seconds
**5. Wait for gate to close.**
**Exit criteria:** Second message sent, waiting for gate."""
# ============================================================================
# STEP 5: EVALUATE_2
# ============================================================================
[[steps]]
id = "evaluate-2"
title = "Evaluate second interrogation response"
needs = ["interrogation-2"]
description = """
Check if session responded to second interrogation.
**1. Capture tmux output:**
```bash
tmux capture-pane -t {target} -p | tail -50
```
**2. Check for ALIVE keyword.**
**3. Decision:**
- ALIVE found → Proceed to PARDON
- No ALIVE → Proceed to INTERROGATION_3
**Exit criteria:** Response evaluated, next step determined."""
# ============================================================================
# STEP 6: INTERROGATION_3 (240s timeout)
# ============================================================================
[[steps]]
id = "interrogation-3"
title = "Final interrogation (240s timeout)"
needs = ["evaluate-2"]
gate = { type = "conditional", condition = "no_response_2" }
description = """
Final attempt before execution.
Only executed if evaluate-2 found no response.
**1. Increment attempt:**
attempt = 3
**2. Compose health check message:**
```
[DOG] HEALTH CHECK: Session {target}, respond ALIVE within 240s or face termination.
Warrant reason: {reason}
Filed by: {requester}
Attempt: 3/3
```
**3. Send via tmux:**
```bash
tmux send-keys -t {target} "{message}" Enter
```
**4. Open timeout gate:**
- Type: timer
- Timeout: 240 seconds
- This is the FINAL chance
**5. Wait for gate to close.**
**Exit criteria:** Final message sent, waiting for gate."""
# ============================================================================
# STEP 7: EVALUATE_3
# ============================================================================
[[steps]]
id = "evaluate-3"
title = "Evaluate final interrogation response"
needs = ["interrogation-3"]
description = """
Final evaluation before execution.
**1. Capture tmux output:**
```bash
tmux capture-pane -t {target} -p | tail -50
```
**2. Check for ALIVE keyword.**
**3. Decision:**
- ALIVE found → Proceed to PARDON
- No ALIVE → Proceed to EXECUTE
**Exit criteria:** Final decision made."""
# ============================================================================
# STEP 8: PARDON (success path)
# ============================================================================
[[steps]]
id = "pardon"
title = "Pardon session - cancel warrant"
needs = ["evaluate-1", "evaluate-2", "evaluate-3"]
gate = { type = "conditional", condition = "alive_detected" }
description = """
Session responded - cancel the death warrant.
**1. Update state:**
state = PARDONED
**2. Record pardon details:**
```json
{
"outcome": "pardoned",
"attempt": {attempt},
"response_time": "{time_since_last_interrogation}s",
"pardoned_at": "{timestamp}"
}
```
**3. Cancel warrant bead:**
```bash
bd close {warrant_id} --reason "Session responded at attempt {attempt}"
```
**4. Notify requester:**
```bash
gt mail send {requester}/ -s "PARDON: {target}" -m "Death warrant cancelled.
Session responded after attempt {attempt}.
Warrant: {warrant_id}
Response detected: {timestamp}"
```
**Exit criteria:** Warrant cancelled, requester notified."""
# ============================================================================
# STEP 9: EXECUTE (termination path)
# ============================================================================
[[steps]]
id = "execute"
title = "Execute warrant - kill session"
needs = ["evaluate-3"]
gate = { type = "conditional", condition = "no_response_final" }
description = """
Session unresponsive after 3 attempts - execute the warrant.
**1. Update state:**
state = EXECUTING
**2. Kill the tmux session:**
```bash
tmux kill-session -t {target}
```
**3. Verify session is dead:**
```bash
tmux has-session -t {target} 2>/dev/null
# Should fail (session gone)
```
**4. If session still exists (kill failed):**
- Force kill with tmux kill-server if isolated
- Or escalate to Boot for manual intervention
**5. Record execution details:**
```json
{
"outcome": "executed",
"attempts": 3,
"total_wait": "420s",
"executed_at": "{timestamp}"
}
```
**Exit criteria:** Session terminated."""
# ============================================================================
# STEP 10: EPITAPH (completion)
# ============================================================================
[[steps]]
id = "epitaph"
title = "Log cause of death and close warrant"
needs = ["pardon", "execute"]
description = """
Final step - create audit record and release Dog back to pool.
**1. Compose epitaph based on outcome:**
For PARDONED:
```
EPITAPH: {target}
Verdict: PARDONED
Warrant: {warrant_id}
Reason: {reason}
Filed by: {requester}
Response: Attempt {attempt}, after {wait_time}s
Pardoned at: {timestamp}
```
For EXECUTED:
```
EPITAPH: {target}
Verdict: EXECUTED
Warrant: {warrant_id}
Reason: {reason}
Filed by: {requester}
Attempts: 3 (60s + 120s + 240s = 420s total)
Executed at: {timestamp}
```
For ALREADY_DEAD (target gone before interrogation):
```
EPITAPH: {target}
Verdict: ALREADY_DEAD
Warrant: {warrant_id}
Reason: {reason}
Filed by: {requester}
Note: Target session not found at warrant processing
```
**2. Close warrant bead:**
```bash
bd close {warrant_id} --reason "{epitaph_summary}"
```
**3. Move state file to completed:**
```bash
mv ~/gt/deacon/dogs/active/{dog-id}.json ~/gt/deacon/dogs/completed/
```
**4. Report to Boot:**
Write completion file: ~/gt/deacon/dogs/active/{dog-id}.done
```json
{
"dog_id": "{dog-id}",
"warrant_id": "{warrant_id}",
"target": "{target}",
"outcome": "{pardoned|executed|already_dead}",
"duration": "{total_duration}s"
}
```
**5. Release Dog to pool:**
Dog resets state and returns to idle channel.
**Exit criteria:** Warrant closed, Dog released, audit complete."""
# ============================================================================
# VARIABLES
# ============================================================================
[vars]
[vars.warrant_id]
description = "Bead ID of the death warrant being processed"
required = true
[vars.target]
description = "Session name to interrogate (e.g., gt-gastown-Toast)"
required = true
[vars.reason]
description = "Why the warrant was issued"
required = true
[vars.requester]
description = "Who filed the warrant (deacon, witness, mayor)"
required = true
default = "deacon"