Files
gastown/internal/witness/manager.go
Steve Yegge 2d6b93f26b fix: Remove PID/tmux state inference (gt-psuw7)
ZFC compliance: daemon becomes pure transport layer, trusting agent beads.

Changes:
- refinery Status(): Simply returns loaded state, no PID/tmux reconciliation
- witness Status(): Simply returns loaded state, no PID inference
- daemon ensureDeaconRunning(): Trusts agent bead state, no tmux fallback
- daemon pokeDeacon(): Trusts agent bead state, no HasSession check

Removed:
- 78 lines of state inference code (PID checks, tmux session parsing)
- "Reconciliation" logic that overwrote agent-reported state

Note: Timeout fallback for dead agents is gt-2hzl4 (separate issue).

Reference: ~/gt/docs/zfc-violations-audit.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 01:56:42 -08:00

145 lines
3.0 KiB
Go

package witness
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"time"
"github.com/steveyegge/gastown/internal/rig"
)
// Common errors
var (
ErrNotRunning = errors.New("witness not running")
ErrAlreadyRunning = errors.New("witness already running")
)
// Manager handles witness lifecycle and monitoring operations.
type Manager struct {
rig *rig.Rig
workDir string
}
// NewManager creates a new witness manager for a rig.
func NewManager(r *rig.Rig) *Manager {
return &Manager{
rig: r,
workDir: r.Path,
}
}
// stateFile returns the path to the witness state file.
func (m *Manager) stateFile() string {
return filepath.Join(m.rig.Path, ".runtime", "witness.json")
}
// loadState loads witness state from disk.
func (m *Manager) loadState() (*Witness, error) {
data, err := os.ReadFile(m.stateFile())
if err != nil {
if os.IsNotExist(err) {
return &Witness{
RigName: m.rig.Name,
State: StateStopped,
}, nil
}
return nil, err
}
var w Witness
if err := json.Unmarshal(data, &w); err != nil {
return nil, err
}
return &w, nil
}
// saveState persists witness state to disk.
func (m *Manager) saveState(w *Witness) error {
dir := filepath.Dir(m.stateFile())
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
data, err := json.MarshalIndent(w, "", " ")
if err != nil {
return err
}
return os.WriteFile(m.stateFile(), data, 0644)
}
// Status returns the current witness status.
// ZFC-compliant: trusts agent-reported state, no PID inference.
// The daemon reads agent bead state for liveness checks.
func (m *Manager) Status() (*Witness, error) {
w, err := m.loadState()
if err != nil {
return nil, err
}
// Update monitored polecats list (still useful for display)
w.MonitoredPolecats = m.rig.Polecats
return w, nil
}
// Start starts the witness (marks it as running).
// Patrol logic is now handled by mol-witness-patrol molecule executed by Claude.
func (m *Manager) Start() error {
w, err := m.loadState()
if err != nil {
return err
}
if w.State == StateRunning && w.PID > 0 && processExists(w.PID) {
return ErrAlreadyRunning
}
now := time.Now()
w.State = StateRunning
w.StartedAt = &now
w.PID = os.Getpid()
w.MonitoredPolecats = m.rig.Polecats
return m.saveState(w)
}
// Stop stops the witness.
func (m *Manager) Stop() error {
w, err := m.loadState()
if err != nil {
return err
}
if w.State != StateRunning {
return ErrNotRunning
}
// If we have a PID, try to stop it gracefully
if w.PID > 0 && w.PID != os.Getpid() {
// Send SIGTERM (best-effort graceful stop)
if proc, err := os.FindProcess(w.PID); err == nil {
_ = proc.Signal(os.Interrupt)
}
}
w.State = StateStopped
w.PID = 0
return m.saveState(w)
}
// processExists checks if a process with the given PID exists.
func processExists(pid int) bool {
proc, err := os.FindProcess(pid)
if err != nil {
return false
}
// On Unix, FindProcess always succeeds; signal 0 tests existence
err = proc.Signal(nil)
return err == nil
}