Files
gastown/internal/deacon/pause.go
Steve Yegge c4d956ebe7 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>
2026-01-07 21:56:46 -08:00

89 lines
2.1 KiB
Go

// Package deacon provides the Deacon agent infrastructure.
package deacon
import (
"encoding/json"
"os"
"path/filepath"
"time"
)
// PauseState represents the Deacon pause file contents.
// When paused, the Deacon must not perform any patrol actions.
type PauseState struct {
// Paused is true if the Deacon is currently paused.
Paused bool `json:"paused"`
// Reason explains why the Deacon was paused.
Reason string `json:"reason,omitempty"`
// PausedAt is when the Deacon was paused.
PausedAt time.Time `json:"paused_at"`
// PausedBy identifies who paused the Deacon (e.g., "human", "mayor").
PausedBy string `json:"paused_by,omitempty"`
}
// GetPauseFile returns the path to the Deacon pause file.
func GetPauseFile(townRoot string) string {
return filepath.Join(townRoot, ".runtime", "deacon", "paused.json")
}
// IsPaused checks if the Deacon is currently paused.
// Returns (isPaused, pauseState, error).
// If the pause file doesn't exist, returns (false, nil, nil).
func IsPaused(townRoot string) (bool, *PauseState, error) {
pauseFile := GetPauseFile(townRoot)
data, err := os.ReadFile(pauseFile) //nolint:gosec // G304: path is constructed from trusted townRoot
if err != nil {
if os.IsNotExist(err) {
return false, nil, nil
}
return false, nil, err
}
var state PauseState
if err := json.Unmarshal(data, &state); err != nil {
return false, nil, err
}
return state.Paused, &state, nil
}
// Pause pauses the Deacon by creating the pause file.
func Pause(townRoot, reason, pausedBy string) error {
pauseFile := GetPauseFile(townRoot)
// Ensure parent directory exists
if err := os.MkdirAll(filepath.Dir(pauseFile), 0755); err != nil {
return err
}
state := PauseState{
Paused: true,
Reason: reason,
PausedAt: time.Now().UTC(),
PausedBy: pausedBy,
}
data, err := json.MarshalIndent(state, "", " ")
if err != nil {
return err
}
return os.WriteFile(pauseFile, data, 0600)
}
// Resume resumes the Deacon by removing the pause file.
func Resume(townRoot string) error {
pauseFile := GetPauseFile(townRoot)
err := os.Remove(pauseFile)
if err != nil && !os.IsNotExist(err) {
return err
}
return nil
}