Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions internal/beads/handoff.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"time"

"github.com/steveyegge/gastown/internal/lock"
Expand Down Expand Up @@ -44,6 +45,27 @@ func (b *Beads) FindHandoffBead(role string) (*Issue, error) {
return nil, nil
}

// FindAllHandoffBeads fetches all pinned beads once and returns a map from
// role name to handoff bead. This avoids the N+1 subprocess problem where
// FindHandoffBead is called once per agent, each spawning a bd subprocess.
func (b *Beads) FindAllHandoffBeads() (map[string]*Issue, error) {
issues, err := b.List(ListOptions{Status: StatusPinned, Priority: -1})
if err != nil {
return nil, fmt.Errorf("listing pinned issues: %w", err)
}

result := make(map[string]*Issue)
for _, issue := range issues {
// Handoff bead titles follow the pattern "<role> Handoff"
if strings.HasSuffix(issue.Title, " Handoff") {
role := strings.TrimSuffix(issue.Title, " Handoff")
result[role] = issue
}
}

return result, nil
}

// GetOrCreateHandoffBead returns the handoff bead for a role, creating it if needed.
func (b *Beads) GetOrCreateHandoffBead(role string) (*Issue, error) {
// Check if it exists
Expand Down
161 changes: 122 additions & 39 deletions internal/cmd/rig.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"path/filepath"
"sort"
"strings"
"sync"
"time"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -1905,48 +1906,140 @@ func runRigStatus(cmd *cobra.Command, args []string) error {
}
fmt.Println()

// --- Parallel data gathering phase ---
// All expensive operations (tmux health checks, beads queries, git status)
// run concurrently. Display phase follows with pre-fetched data.
var dataWg sync.WaitGroup

// Witness status
fmt.Printf("%s\n", style.Bold.Render("Witness"))
witMgr := witness.NewManager(r)
witnessRunning, _ := witMgr.IsRunning()
var witnessRunning bool
dataWg.Add(1)
go func() {
defer dataWg.Done()
witnessRunning, _ = witMgr.IsRunning()
}()

// Refinery status + queue
refMgr := refinery.NewManager(r)
var refineryRunning bool
var refineryQueue []refinery.QueueItem
dataWg.Add(1)
go func() {
defer dataWg.Done()
refineryRunning, _ = refMgr.IsRunning()
if refineryRunning {
refineryQueue, _ = refMgr.Queue()
}
}()

// Polecats list (involves per-polecat beads + git queries)
polecatGit := git.NewGit(r.Path)
polecatMgr := polecat.NewManager(r, polecatGit, t)
var polecats []*polecat.Polecat
var polecatsErr error
dataWg.Add(1)
go func() {
defer dataWg.Done()
polecats, polecatsErr = polecatMgr.List()
}()

// Crew list
crewMgr := crew.NewManager(r, git.NewGit(townRoot))
var crewWorkers []*crew.CrewWorker
var crewErr error
dataWg.Add(1)
go func() {
defer dataWg.Done()
crewWorkers, crewErr = crewMgr.List()
}()

dataWg.Wait()

// --- Polecat + Crew session checks (parallel, after List completes) ---
type polecatInfo struct {
name string
state polecat.State
issue string
hasSession bool
}
var pInfos []polecatInfo
type crewInfo struct {
name string
hasSession bool
branch string
dirty bool
}
var cInfos []crewInfo

var sessionWg sync.WaitGroup

if polecatsErr == nil && len(polecats) > 0 {
pInfos = make([]polecatInfo, len(polecats))
for i, p := range polecats {
pInfos[i] = polecatInfo{name: p.Name, state: p.State, issue: p.Issue}
sessionWg.Add(1)
go func(idx int, p *polecat.Polecat) {
defer sessionWg.Done()
sessionName := session.PolecatSessionName(session.PrefixFor(rigName), p.Name)
pInfos[idx].hasSession, _ = t.HasSession(sessionName)
}(i, p)
}
}

if crewErr == nil && len(crewWorkers) > 0 {
cInfos = make([]crewInfo, len(crewWorkers))
for i, w := range crewWorkers {
cInfos[i] = crewInfo{name: w.Name}
sessionWg.Add(1)
go func(idx int, w *crew.CrewWorker) {
defer sessionWg.Done()
sessionName := crewSessionName(rigName, w.Name)
cInfos[idx].hasSession, _ = t.HasSession(sessionName)
crewGit := git.NewGit(w.ClonePath)
cInfos[idx].branch, _ = crewGit.CurrentBranch()
gitStatus, _ := crewGit.Status()
if gitStatus != nil && !gitStatus.Clean {
cInfos[idx].dirty = true
}
}(i, w)
}
}

sessionWg.Wait()

// --- Display phase (all data pre-fetched) ---

// Witness
fmt.Printf("%s\n", style.Bold.Render("Witness"))
if witnessRunning {
fmt.Printf(" %s running\n", style.Success.Render("●"))
} else {
fmt.Printf(" %s stopped\n", style.Dim.Render("○"))
}
fmt.Println()

// Refinery status
// Refinery
fmt.Printf("%s\n", style.Bold.Render("Refinery"))
refMgr := refinery.NewManager(r)
refineryRunning, _ := refMgr.IsRunning()
if refineryRunning {
fmt.Printf(" %s running\n", style.Success.Render("●"))
// Show queue size
queue, err := refMgr.Queue()
if err == nil && len(queue) > 0 {
fmt.Printf(" Queue: %d items\n", len(queue))
if len(refineryQueue) > 0 {
fmt.Printf(" Queue: %d items\n", len(refineryQueue))
}
} else {
fmt.Printf(" %s stopped\n", style.Dim.Render("○"))
}
fmt.Println()

// Polecats
polecatGit := git.NewGit(r.Path)
polecatMgr := polecat.NewManager(r, polecatGit, t)
polecats, err := polecatMgr.List()
fmt.Printf("%s", style.Bold.Render("Polecats"))
if err != nil || len(polecats) == 0 {
if polecatsErr != nil || len(polecats) == 0 {
fmt.Printf(" (none)\n")
} else {
fmt.Printf(" (%d)\n", len(polecats))
for _, p := range polecats {
sessionName := session.PolecatSessionName(session.PrefixFor(rigName), p.Name)
hasSession, _ := t.HasSession(sessionName)

for _, pi := range pInfos {
sessionIcon := style.Dim.Render("○")
if hasSession {
if pi.hasSession {
sessionIcon = style.Success.Render("●")
}

Expand All @@ -1957,51 +2050,41 @@ func runRigStatus(cmd *cobra.Command, args []string) error {
// witness can detect unsubmitted work (gt-3071b). Previously this
// showed "done" which masked failures where polecats died before
// running gt done, leaving work stranded in worktrees.
displayState := p.State
if hasSession && displayState == polecat.StateDone {
displayState := pi.state
if pi.hasSession && displayState == polecat.StateDone {
displayState = polecat.StateWorking
} else if !hasSession && displayState == polecat.StateWorking {
} else if !pi.hasSession && displayState == polecat.StateWorking {
displayState = polecat.State("stalled")
}

stateStr := string(displayState)
if p.Issue != "" {
stateStr = fmt.Sprintf("%s → %s", displayState, p.Issue)
if pi.issue != "" {
stateStr = fmt.Sprintf("%s → %s", displayState, pi.issue)
}

fmt.Printf(" %s %s: %s\n", sessionIcon, p.Name, stateStr)
fmt.Printf(" %s %s: %s\n", sessionIcon, pi.name, stateStr)
}
}
fmt.Println()

// Crew
crewMgr := crew.NewManager(r, git.NewGit(townRoot))
crewWorkers, err := crewMgr.List()
fmt.Printf("%s", style.Bold.Render("Crew"))
if err != nil || len(crewWorkers) == 0 {
if crewErr != nil || len(crewWorkers) == 0 {
fmt.Printf(" (none)\n")
} else {
fmt.Printf(" (%d)\n", len(crewWorkers))
for _, w := range crewWorkers {
sessionName := crewSessionName(rigName, w.Name)
hasSession, _ := t.HasSession(sessionName)

for _, ci := range cInfos {
sessionIcon := style.Dim.Render("○")
if hasSession {
if ci.hasSession {
sessionIcon = style.Success.Render("●")
}

// Get git info
crewGit := git.NewGit(w.ClonePath)
branch, _ := crewGit.CurrentBranch()
gitStatus, _ := crewGit.Status()

gitInfo := ""
if gitStatus != nil && !gitStatus.Clean {
if ci.dirty {
gitInfo = style.Warning.Render(" (dirty)")
}

fmt.Printf(" %s %s: %s%s\n", sessionIcon, w.Name, branch, gitInfo)
fmt.Printf(" %s %s: %s%s\n", sessionIcon, ci.name, ci.branch, gitInfo)
}
}

Expand Down
Loading
Loading