refactor(witness,refinery): ZFC-compliant state management

Remove state files from witness and refinery managers, following
the "Discover, Don't Track" principle. Tmux session existence is
now the source of truth for running state (like deacon).

Changes:
- Add IsRunning() that checks tmux HasSession
- Change Status() to return *tmux.SessionInfo
- Remove loadState/saveState/stateManager
- Simplify Start()/Stop() to not use state files
- Update CLI commands (witness/refinery/rig) for new API
- Update tests to be ZFC-compliant

This fixes state file divergence issues where witness/refinery
could show "running" when the actual tmux session was dead.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown/crew/mel
2026-01-20 20:00:43 -08:00
committed by Steve Yegge
parent 126ec84bb3
commit 5218102f49
6 changed files with 237 additions and 513 deletions

View File

@@ -337,6 +337,14 @@ func runRefineryStop(cmd *cobra.Command, args []string) error {
return nil
}
// RefineryStatusOutput is the JSON output format for refinery status.
type RefineryStatusOutput struct {
Running bool `json:"running"`
RigName string `json:"rig_name"`
Session string `json:"session,omitempty"`
QueueLength int `json:"queue_length"`
}
func runRefineryStatus(cmd *cobra.Command, args []string) error {
rigName := ""
if len(args) > 0 {
@@ -348,58 +356,42 @@ func runRefineryStatus(cmd *cobra.Command, args []string) error {
return err
}
ref, err := mgr.Status()
if err != nil {
return fmt.Errorf("getting status: %w", err)
}
// ZFC: tmux is source of truth for running state
running, _ := mgr.IsRunning()
sessionInfo, _ := mgr.Status() // may be nil if not running
// Get queue from beads
queue, _ := mgr.Queue()
queueLen := len(queue)
// JSON output
if refineryStatusJSON {
output := RefineryStatusOutput{
Running: running,
RigName: rigName,
QueueLength: queueLen,
}
if sessionInfo != nil {
output.Session = sessionInfo.Name
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(ref)
return enc.Encode(output)
}
// Human-readable output
fmt.Printf("%s Refinery: %s\n\n", style.Bold.Render("⚙"), rigName)
stateStr := string(ref.State)
switch ref.State {
case refinery.StateRunning:
stateStr = style.Bold.Render("● running")
case refinery.StateStopped:
stateStr = style.Dim.Render("○ stopped")
case refinery.StatePaused:
stateStr = style.Dim.Render("⏸ paused")
}
fmt.Printf(" State: %s\n", stateStr)
if ref.StartedAt != nil {
fmt.Printf(" Started: %s\n", ref.StartedAt.Format("2006-01-02 15:04:05"))
}
if ref.CurrentMR != nil {
fmt.Printf("\n %s\n", style.Bold.Render("Currently Processing:"))
fmt.Printf(" Branch: %s\n", ref.CurrentMR.Branch)
fmt.Printf(" Worker: %s\n", ref.CurrentMR.Worker)
if ref.CurrentMR.IssueID != "" {
fmt.Printf(" Issue: %s\n", ref.CurrentMR.IssueID)
if running {
fmt.Printf(" State: %s\n", style.Bold.Render("● running"))
if sessionInfo != nil {
fmt.Printf(" Session: %s\n", sessionInfo.Name)
}
} else {
fmt.Printf(" State: %s\n", style.Dim.Render("○ stopped"))
}
// Get queue length
queue, _ := mgr.Queue()
pendingCount := 0
for _, item := range queue {
if item.Position > 0 { // Not currently processing
pendingCount++
}
}
fmt.Printf("\n Queue: %d pending\n", pendingCount)
if ref.LastMergeAt != nil {
fmt.Printf(" Last merge: %s\n", ref.LastMergeAt.Format("2006-01-02 15:04:05"))
}
fmt.Printf("\n Queue: %d pending\n", queueLen)
return nil
}