fix(done): restrict gt done to polecats only

Add BD_ACTOR check at start of runDone() to prevent non-polecat roles
(crew, deacon, witness, etc.) from calling gt done. Only polecats are
ephemeral workers that self-destruct after completing work.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown/crew/george
2026-01-20 19:51:27 -08:00
committed by Steve Yegge
parent f82477d6a6
commit 9a91a1b94f
2 changed files with 52 additions and 0 deletions

View File

@@ -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.
//

View File

@@ -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)
}
})
}
}