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>
520 lines
15 KiB
TOML
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"
|