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:
88
internal/deacon/pause.go
Normal file
88
internal/deacon/pause.go
Normal file
@@ -0,0 +1,88 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user