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>
89 lines
2.1 KiB
Go
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
|
|
}
|