fix(done): use ResolveHookDir for dispatcher lookup (sc-g7bl3)

When a polecat runs gt done after work is complete, it should notify the
dispatcher (the agent that slung the work). This notification was failing
silently when the polecat's worktree was deleted before gt done finished.

The issue was that getDispatcherFromBead() used ResolveBeadsDir(cwd) which
relies on the polecat's .beads/redirect file. If the worktree is deleted
(e.g., by Witness cleanup), the redirect file is gone and bead lookup fails.

Fix: Use ResolveHookDir(townRoot, issueID, cwd) instead. ResolveHookDir uses
prefix-based routing via routes.jsonl which works regardless of worktree
state. This ensures dispatcher notifications are sent reliably even when
the worktree is cleaned up before gt done completes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
slit
2026-01-22 12:22:32 -08:00
committed by John Ogle
parent 6be7fdd76c
commit 9e3eb094c5

View File

@@ -456,7 +456,7 @@ notifyWitness:
// Notify dispatcher if work was dispatched by another agent // Notify dispatcher if work was dispatched by another agent
if issueID != "" { if issueID != "" {
if dispatcher := getDispatcherFromBead(cwd, issueID); dispatcher != "" && dispatcher != sender { if dispatcher := getDispatcherFromBead(townRoot, cwd, issueID); dispatcher != "" && dispatcher != sender {
dispatcherNotification := &mail.Message{ dispatcherNotification := &mail.Message{
To: dispatcher, To: dispatcher,
From: sender, From: sender,
@@ -645,7 +645,7 @@ func updateAgentStateOnDone(cwd, townRoot, exitType, _ string) { // issueID unus
if _, err := bd.Run("agent", "state", agentBeadID, "awaiting-gate"); err != nil { if _, err := bd.Run("agent", "state", agentBeadID, "awaiting-gate"); err != nil {
fmt.Fprintf(os.Stderr, "Warning: couldn't set agent %s to awaiting-gate: %v\n", agentBeadID, err) fmt.Fprintf(os.Stderr, "Warning: couldn't set agent %s to awaiting-gate: %v\n", agentBeadID, err)
} }
// ExitCompleted and ExitDeferred don't set state - observable from tmux // ExitCompleted and ExitDeferred don't set state - observable from tmux
} }
// ZFC #10: Self-report cleanup status // ZFC #10: Self-report cleanup status
@@ -678,12 +678,19 @@ func getIssueFromAgentHook(bd *beads.Beads, agentBeadID string) string {
// getDispatcherFromBead retrieves the dispatcher agent ID from the bead's attachment fields. // getDispatcherFromBead retrieves the dispatcher agent ID from the bead's attachment fields.
// Returns empty string if no dispatcher is recorded. // Returns empty string if no dispatcher is recorded.
func getDispatcherFromBead(cwd, issueID string) string { //
// BUG FIX (sc-g7bl3): Use townRoot and ResolveHookDir for bead lookup instead of
// ResolveBeadsDir(cwd). When the polecat's worktree is deleted before gt done finishes,
// ResolveBeadsDir(cwd) fails because the redirect file is gone. ResolveHookDir uses
// prefix-based routing via routes.jsonl which works regardless of worktree state.
func getDispatcherFromBead(townRoot, cwd, issueID string) string {
if issueID == "" { if issueID == "" {
return "" return ""
} }
bd := beads.New(beads.ResolveBeadsDir(cwd)) // Use ResolveHookDir for resilient bead lookup - works even if worktree is deleted
beadsDir := beads.ResolveHookDir(townRoot, issueID, cwd)
bd := beads.New(beadsDir)
issue, err := bd.Show(issueID) issue, err := bd.Show(issueID)
if err != nil { if err != nil {
return "" return ""