From c94a2301ebd1a25b9e90ba68e79d353d7539a3cc Mon Sep 17 00:00:00 2001 From: Artem Bambalov Date: Mon, 26 Jan 2026 04:05:44 +0200 Subject: [PATCH] fix: align assigneeID format with hooked issue format (#946) The polecat manager was using rig/name format while sling sets rig/polecats/name. This caused gt polecat status to show 'done' for working polecats because GetAssignedIssue couldn't find them. Fixes gt-oohy --- internal/beads/beads.go | 2 +- internal/polecat/manager.go | 22 ++++---- internal/polecat/manager_test.go | 88 ++++++++++++++++---------------- 3 files changed, 57 insertions(+), 55 deletions(-) diff --git a/internal/beads/beads.go b/internal/beads/beads.go index 4d8dd610..e015b7b3 100644 --- a/internal/beads/beads.go +++ b/internal/beads/beads.go @@ -327,7 +327,7 @@ func (b *Beads) List(opts ListOptions) ([]*Issue, error) { } // ListByAssignee returns all issues assigned to a specific assignee. -// The assignee is typically in the format "rig/polecatName" (e.g., "gastown/Toast"). +// The assignee is typically in the format "rig/polecats/polecatName" (e.g., "gastown/polecats/Toast"). func (b *Beads) ListByAssignee(assignee string) ([]*Issue, error) { return b.List(ListOptions{ Status: "all", // Include both open and closed for state derivation diff --git a/internal/polecat/manager.go b/internal/polecat/manager.go index 12c8111c..375de9ec 100644 --- a/internal/polecat/manager.go +++ b/internal/polecat/manager.go @@ -90,9 +90,9 @@ func NewManager(r *rig.Rig, g *git.Git, t *tmux.Tmux) *Manager { } // assigneeID returns the beads assignee identifier for a polecat. -// Format: "rig/polecatName" (e.g., "gastown/Toast") +// Format: "rig/polecats/polecatName" (e.g., "gastown/polecats/Toast") func (m *Manager) assigneeID(name string) string { - return fmt.Sprintf("%s/%s", m.rig.Name, name) + return fmt.Sprintf("%s/polecats/%s", m.rig.Name, name) } // agentBeadID returns the agent bead ID for a polecat. @@ -265,8 +265,8 @@ func (m *Manager) buildBranchName(name, issue string) string { // {year} and {month} now := time.Now() - vars["{year}"] = now.Format("06") // YY format - vars["{month}"] = now.Format("01") // MM format + vars["{year}"] = now.Format("06") // YY format + vars["{month}"] = now.Format("01") // MM format // {name} vars["{name}"] = name @@ -1108,13 +1108,13 @@ func (m *Manager) CleanupStaleBranches() (int, error) { // StalenessInfo contains details about a polecat's staleness. type StalenessInfo struct { - Name string - CommitsBehind int // How many commits behind origin/main - HasActiveSession bool // Whether tmux session is running - HasUncommittedWork bool // Whether there's uncommitted or unpushed work - AgentState string // From agent bead (empty if no bead) - IsStale bool // Overall assessment: safe to clean up - Reason string // Why it's considered stale (or not) + Name string + CommitsBehind int // How many commits behind origin/main + HasActiveSession bool // Whether tmux session is running + HasUncommittedWork bool // Whether there's uncommitted or unpushed work + AgentState string // From agent bead (empty if no bead) + IsStale bool // Overall assessment: safe to clean up + Reason string // Why it's considered stale (or not) } // DetectStalePolecats identifies polecats that are candidates for cleanup. diff --git a/internal/polecat/manager_test.go b/internal/polecat/manager_test.go index 225962c2..a918aca7 100644 --- a/internal/polecat/manager_test.go +++ b/internal/polecat/manager_test.go @@ -135,7 +135,7 @@ func TestAssigneeID(t *testing.T) { m := NewManager(r, git.NewGit(r.Path), nil) id := m.assigneeID("Toast") - expected := "test-rig/Toast" + expected := "test-rig/polecats/Toast" if id != expected { t.Errorf("assigneeID = %q, want %q", id, expected) } @@ -446,81 +446,83 @@ func TestAddWithOptions_AgentsMDFallback(t *testing.T) { t.Errorf("AGENTS.md content = %q, want %q", gotContent, wantContent) } } + // TestReconcilePoolWith tests all permutations of directory and session existence. // This is the core allocation policy logic. // // Truth table: -// HasDir | HasSession | Result -// -------|------------|------------------ -// false | false | available (not in-use) -// true | false | in-use (normal finished polecat) -// false | true | orphan → kill session, available -// true | true | in-use (normal working polecat) +// +// HasDir | HasSession | Result +// -------|------------|------------------ +// false | false | available (not in-use) +// true | false | in-use (normal finished polecat) +// false | true | orphan → kill session, available +// true | true | in-use (normal working polecat) func TestReconcilePoolWith(t *testing.T) { t.Parallel() tests := []struct { - name string - namesWithDirs []string + name string + namesWithDirs []string namesWithSessions []string - wantInUse []string // names that should be marked in-use - wantOrphans []string // sessions that should be killed + wantInUse []string // names that should be marked in-use + wantOrphans []string // sessions that should be killed }{ { - name: "no dirs, no sessions - all available", - namesWithDirs: []string{}, + name: "no dirs, no sessions - all available", + namesWithDirs: []string{}, namesWithSessions: []string{}, - wantInUse: []string{}, - wantOrphans: []string{}, + wantInUse: []string{}, + wantOrphans: []string{}, }, { - name: "has dir, no session - in use", - namesWithDirs: []string{"toast"}, + name: "has dir, no session - in use", + namesWithDirs: []string{"toast"}, namesWithSessions: []string{}, - wantInUse: []string{"toast"}, - wantOrphans: []string{}, + wantInUse: []string{"toast"}, + wantOrphans: []string{}, }, { - name: "no dir, has session - orphan killed", - namesWithDirs: []string{}, + name: "no dir, has session - orphan killed", + namesWithDirs: []string{}, namesWithSessions: []string{"nux"}, - wantInUse: []string{}, - wantOrphans: []string{"nux"}, + wantInUse: []string{}, + wantOrphans: []string{"nux"}, }, { - name: "has dir, has session - in use", - namesWithDirs: []string{"capable"}, + name: "has dir, has session - in use", + namesWithDirs: []string{"capable"}, namesWithSessions: []string{"capable"}, - wantInUse: []string{"capable"}, - wantOrphans: []string{}, + wantInUse: []string{"capable"}, + wantOrphans: []string{}, }, { - name: "mixed: one with dir, one orphan session", - namesWithDirs: []string{"toast"}, + name: "mixed: one with dir, one orphan session", + namesWithDirs: []string{"toast"}, namesWithSessions: []string{"toast", "nux"}, - wantInUse: []string{"toast"}, - wantOrphans: []string{"nux"}, + wantInUse: []string{"toast"}, + wantOrphans: []string{"nux"}, }, { - name: "multiple dirs, no sessions", - namesWithDirs: []string{"toast", "nux", "capable"}, + name: "multiple dirs, no sessions", + namesWithDirs: []string{"toast", "nux", "capable"}, namesWithSessions: []string{}, - wantInUse: []string{"capable", "nux", "toast"}, - wantOrphans: []string{}, + wantInUse: []string{"capable", "nux", "toast"}, + wantOrphans: []string{}, }, { - name: "multiple orphan sessions", - namesWithDirs: []string{}, + name: "multiple orphan sessions", + namesWithDirs: []string{}, namesWithSessions: []string{"slit", "rictus"}, - wantInUse: []string{}, - wantOrphans: []string{"rictus", "slit"}, + wantInUse: []string{}, + wantOrphans: []string{"rictus", "slit"}, }, { - name: "complex: dirs, valid sessions, orphan sessions", - namesWithDirs: []string{"toast", "capable"}, + name: "complex: dirs, valid sessions, orphan sessions", + namesWithDirs: []string{"toast", "capable"}, namesWithSessions: []string{"toast", "nux", "slit"}, - wantInUse: []string{"capable", "toast"}, - wantOrphans: []string{"nux", "slit"}, + wantInUse: []string{"capable", "toast"}, + wantOrphans: []string{"nux", "slit"}, }, }