diff --git a/internal/cmd/done.go b/internal/cmd/done.go index a385ac07..4fd24a35 100644 --- a/internal/cmd/done.go +++ b/internal/cmd/done.go @@ -82,6 +82,14 @@ func init() { } func runDone(cmd *cobra.Command, args []string) error { + // Guard: Only polecats should call gt done + // Crew, deacons, witnesses etc. don't use gt done - they persist across tasks. + // Polecats are ephemeral workers that self-destruct after completing work. + actor := os.Getenv("BD_ACTOR") + if actor != "" && !isPolecatActor(actor) { + return fmt.Errorf("gt done is for polecats only (you are %s)\nPolecats are ephemeral workers that self-destruct after completing work.\nOther roles persist across tasks and don't use gt done.", actor) + } + // Handle --phase-complete flag (overrides --status) var exitType string if donePhaseComplete { @@ -708,6 +716,14 @@ func selfNukePolecat(roleInfo RoleInfo, _ string) error { return nil } +// isPolecatActor checks if a BD_ACTOR value represents a polecat. +// Polecat actors have format: rigname/polecats/polecatname +// Non-polecat actors have formats like: gastown/crew/name, rigname/witness, etc. +func isPolecatActor(actor string) bool { + parts := strings.Split(actor, "/") + return len(parts) >= 2 && parts[1] == "polecats" +} + // selfKillSession terminates the polecat's own tmux session after logging the event. // This completes the self-cleaning model: "done means gone" - both worktree and session. // diff --git a/internal/cmd/done_test.go b/internal/cmd/done_test.go index 26272387..1166377a 100644 --- a/internal/cmd/done_test.go +++ b/internal/cmd/done_test.go @@ -341,3 +341,39 @@ func TestGetIssueFromAgentHook(t *testing.T) { }) } } + +// TestIsPolecatActor verifies that isPolecatActor correctly identifies +// polecat actors vs other roles based on the BD_ACTOR format. +func TestIsPolecatActor(t *testing.T) { + tests := []struct { + actor string + want bool + }{ + // Polecats: rigname/polecats/polecatname + {"testrig/polecats/furiosa", true}, + {"testrig/polecats/nux", true}, + {"myrig/polecats/witness", true}, // even if named "witness", still a polecat + + // Non-polecats + {"gastown/crew/george", false}, + {"gastown/crew/max", false}, + {"testrig/witness", false}, + {"testrig/deacon", false}, + {"testrig/mayor", false}, + {"gastown/refinery", false}, + + // Edge cases + {"", false}, + {"single", false}, + {"polecats/name", false}, // needs rig prefix + } + + for _, tt := range tests { + t.Run(tt.actor, func(t *testing.T) { + got := isPolecatActor(tt.actor) + if got != tt.want { + t.Errorf("isPolecatActor(%q) = %v, want %v", tt.actor, got, tt.want) + } + }) + } +}