refactor(prime): split 1833-line file into logical modules
Extract prime.go into focused files: - prime_session.go: session ID handling, hooks, persistence - prime_output.go: all output/rendering functions - prime_molecule.go: molecule workflow context - prime_state.go: handoff markers, session state detection Main prime.go now ~730 lines with core flow visible as "table of contents". No behavior changes - pure file organization following Go idioms. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
116
internal/cmd/prime_state.go
Normal file
116
internal/cmd/prime_state.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"github.com/steveyegge/gastown/internal/checkpoint"
|
||||
"github.com/steveyegge/gastown/internal/constants"
|
||||
)
|
||||
|
||||
// SessionState represents the detected session state for observability.
|
||||
type SessionState struct {
|
||||
State string `json:"state"` // normal, post-handoff, crash-recovery, autonomous
|
||||
Role Role `json:"role"` // detected role
|
||||
PrevSession string `json:"prev_session,omitempty"` // for post-handoff
|
||||
CheckpointAge string `json:"checkpoint_age,omitempty"` // for crash-recovery
|
||||
HookedBead string `json:"hooked_bead,omitempty"` // for autonomous
|
||||
}
|
||||
|
||||
// detectSessionState returns the current session state without side effects.
|
||||
func detectSessionState(ctx RoleContext) SessionState {
|
||||
state := SessionState{
|
||||
State: "normal",
|
||||
Role: ctx.Role,
|
||||
}
|
||||
|
||||
// Check for handoff marker (post-handoff state)
|
||||
markerPath := filepath.Join(ctx.WorkDir, constants.DirRuntime, constants.FileHandoffMarker)
|
||||
if data, err := os.ReadFile(markerPath); err == nil {
|
||||
state.State = "post-handoff"
|
||||
state.PrevSession = strings.TrimSpace(string(data))
|
||||
return state
|
||||
}
|
||||
|
||||
// Check for checkpoint (crash-recovery state) - only for polecat/crew
|
||||
if ctx.Role == RolePolecat || ctx.Role == RoleCrew {
|
||||
if cp, err := checkpoint.Read(ctx.WorkDir); err == nil && cp != nil && !cp.IsStale(24*time.Hour) {
|
||||
state.State = "crash-recovery"
|
||||
state.CheckpointAge = cp.Age().Round(time.Minute).String()
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
// Check for hooked work (autonomous state)
|
||||
agentID := getAgentIdentity(ctx)
|
||||
if agentID != "" {
|
||||
b := beads.New(ctx.WorkDir)
|
||||
hookedBeads, err := b.List(beads.ListOptions{
|
||||
Status: beads.StatusHooked,
|
||||
Assignee: agentID,
|
||||
Priority: -1,
|
||||
})
|
||||
if err == nil && len(hookedBeads) > 0 {
|
||||
state.State = "autonomous"
|
||||
state.HookedBead = hookedBeads[0].ID
|
||||
return state
|
||||
}
|
||||
// Also check in_progress beads
|
||||
inProgressBeads, err := b.List(beads.ListOptions{
|
||||
Status: "in_progress",
|
||||
Assignee: agentID,
|
||||
Priority: -1,
|
||||
})
|
||||
if err == nil && len(inProgressBeads) > 0 {
|
||||
state.State = "autonomous"
|
||||
state.HookedBead = inProgressBeads[0].ID
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
// checkHandoffMarker checks for a handoff marker file and outputs a warning if found.
|
||||
// This prevents the "handoff loop" bug where a new session sees /handoff in context
|
||||
// and incorrectly runs it again. The marker tells the new session: "handoff is DONE,
|
||||
// the /handoff you see in context was from YOUR PREDECESSOR, not a request for you."
|
||||
func checkHandoffMarker(workDir string) {
|
||||
markerPath := filepath.Join(workDir, constants.DirRuntime, constants.FileHandoffMarker)
|
||||
data, err := os.ReadFile(markerPath)
|
||||
if err != nil {
|
||||
// No marker = not post-handoff, normal startup
|
||||
return
|
||||
}
|
||||
|
||||
// Marker found - this is a post-handoff session
|
||||
prevSession := strings.TrimSpace(string(data))
|
||||
|
||||
// Remove the marker FIRST so we don't warn twice
|
||||
_ = os.Remove(markerPath)
|
||||
|
||||
// Output prominent warning
|
||||
outputHandoffWarning(prevSession)
|
||||
}
|
||||
|
||||
// checkHandoffMarkerDryRun checks for handoff marker without removing it (for --dry-run).
|
||||
func checkHandoffMarkerDryRun(workDir string) {
|
||||
markerPath := filepath.Join(workDir, constants.DirRuntime, constants.FileHandoffMarker)
|
||||
data, err := os.ReadFile(markerPath)
|
||||
if err != nil {
|
||||
// No marker = not post-handoff, normal startup
|
||||
explain(true, "Post-handoff: no handoff marker found")
|
||||
return
|
||||
}
|
||||
|
||||
// Marker found - this is a post-handoff session
|
||||
prevSession := strings.TrimSpace(string(data))
|
||||
explain(true, fmt.Sprintf("Post-handoff: marker found (predecessor: %s), marker NOT removed in dry-run", prevSession))
|
||||
|
||||
// Output the warning but don't remove marker
|
||||
outputHandoffWarning(prevSession)
|
||||
}
|
||||
Reference in New Issue
Block a user