feat(deacon): implement bulletproof pause mechanism (gt-bpo2c) (#265)

Add multi-layer pause mechanism to prevent Deacon from causing damage:

Layer 1: File-based pause state
- Location: ~/.runtime/deacon/paused.json
- Stores: paused flag, reason, timestamp, paused_by

Layer 2: Commands
- `gt deacon pause [--reason="..."]` - pause with optional reason
- `gt deacon resume` - remove pause file
- `gt deacon status` - shows pause state prominently

Layer 3: Guards
- `gt prime` for deacon: shows PAUSED message, skips patrol context
- `gt deacon heartbeat`: fails when paused

Helper package:
- internal/deacon/pause.go with IsPaused/Pause/Resume functions

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2026-01-07 21:56:46 -08:00
committed by GitHub
parent 7f6fe53c6f
commit c4d956ebe7
3 changed files with 260 additions and 0 deletions

View File

@@ -17,6 +17,7 @@ import (
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/checkpoint"
"github.com/steveyegge/gastown/internal/constants"
"github.com/steveyegge/gastown/internal/deacon"
"github.com/steveyegge/gastown/internal/events"
"github.com/steveyegge/gastown/internal/lock"
"github.com/steveyegge/gastown/internal/rig"
@@ -601,6 +602,11 @@ func outputStartupDirective(ctx RoleContext) {
fmt.Println(" - If attachment found → **RUN IT** (no human input needed)")
fmt.Println(" - If no attachment → await user instruction")
case RoleDeacon:
// Skip startup protocol if paused - the pause message was already shown
paused, _, _ := deacon.IsPaused(ctx.TownRoot)
if paused {
return
}
fmt.Println()
fmt.Println("---")
fmt.Println()
@@ -911,6 +917,13 @@ func showMoleculeProgress(b *beads.Beads, rootID string) {
// Deacon uses wisps (Wisp:true issues in main .beads/) for patrol cycles.
// Deacon is a town-level role, so it uses town root beads (not rig beads).
func outputDeaconPatrolContext(ctx RoleContext) {
// Check if Deacon is paused - if so, output PAUSED message and skip patrol context
paused, state, err := deacon.IsPaused(ctx.TownRoot)
if err == nil && paused {
outputDeaconPausedMessage(state)
return
}
cfg := PatrolConfig{
RoleName: "deacon",
PatrolMolName: "mol-deacon-patrol",
@@ -930,6 +943,32 @@ func outputDeaconPatrolContext(ctx RoleContext) {
outputPatrolContext(cfg)
}
// outputDeaconPausedMessage outputs a prominent PAUSED message for the Deacon.
// When paused, the Deacon must not perform any patrol actions.
func outputDeaconPausedMessage(state *deacon.PauseState) {
fmt.Println()
fmt.Printf("%s\n\n", style.Bold.Render("## ⏸️ DEACON PAUSED"))
fmt.Println("You are paused and must NOT perform any patrol actions.")
fmt.Println()
if state.Reason != "" {
fmt.Printf("Reason: %s\n", state.Reason)
}
fmt.Printf("Paused at: %s\n", state.PausedAt.Format(time.RFC3339))
if state.PausedBy != "" {
fmt.Printf("Paused by: %s\n", state.PausedBy)
}
fmt.Println()
fmt.Println("Wait for human to run `gt deacon resume` before working.")
fmt.Println()
fmt.Println("**DO NOT:**")
fmt.Println("- Create patrol molecules")
fmt.Println("- Run heartbeats")
fmt.Println("- Check agent health")
fmt.Println("- Take any autonomous actions")
fmt.Println()
fmt.Println("You may respond to direct human questions.")
}
// outputWitnessPatrolContext shows patrol molecule status for the Witness.
// Witness AUTO-BONDS its patrol molecule on startup if one isn't already running.
func outputWitnessPatrolContext(ctx RoleContext) {