From 278abf15d692eb52061736e534391926cfc7f656 Mon Sep 17 00:00:00 2001 From: gastown/polecats/furiosa Date: Thu, 1 Jan 2026 17:38:39 -0800 Subject: [PATCH] fix(hook): Read hook_bead from database column, not description (gt-7m33w) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hook discovery code was reading hook_bead from the agent bead's description field (parsed via ParseAgentFieldsFromDescription), but the slot update code writes to the hook_bead database column via 'bd slot set'. This mismatch caused polecats to see stale hook values from the description instead of the current value from the database. Fixed in: - molecule_status.go: Use agentBead.HookBead instead of parsing description - status.go: Use issue.HookBead directly - lifecycle.go: Update all GUPP and orphan detection to read from database columns instead of parsing description 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/cmd/molecule_status.go | 10 +++-- internal/cmd/status.go | 6 +-- internal/daemon/lifecycle.go | 69 +++++++++++++++++---------------- 3 files changed, 44 insertions(+), 41 deletions(-) diff --git a/internal/cmd/molecule_status.go b/internal/cmd/molecule_status.go index 0b439cfb..ba08dd39 100644 --- a/internal/cmd/molecule_status.go +++ b/internal/cmd/molecule_status.go @@ -322,11 +322,13 @@ func runMoleculeStatus(cmd *cobra.Command, args []string) error { if err == nil && agentBead != nil && agentBead.Type == "agent" { status.AgentBeadID = agentBeadID - // Parse hook_bead from the agent bead's description - agentFields := beads.ParseAgentFieldsFromDescription(agentBead.Description) - if agentFields != nil && agentFields.HookBead != "" { + // Read hook_bead from the agent bead's database field (not description!) + // The hook_bead column is updated by `bd slot set` in UpdateAgentState. + // IMPORTANT: Don't use ParseAgentFieldsFromDescription - the description + // field may contain stale data, causing the wrong issue to be hooked. + if agentBead.HookBead != "" { // Fetch the bead on the hook - hookBead, err = b.Show(agentFields.HookBead) + hookBead, err = b.Show(agentBead.HookBead) if err != nil { // Hook bead referenced but not found - report error but continue hookBead = nil diff --git a/internal/cmd/status.go b/internal/cmd/status.go index 9b6aa905..3d12376a 100644 --- a/internal/cmd/status.go +++ b/internal/cmd/status.go @@ -174,11 +174,11 @@ func runStatus(cmd *cobra.Command, args []string) error { } // Pre-fetch all hook beads (referenced in agent beads) in a single query + // Use the HookBead field from the database column, not parsed from description. var allHookIDs []string for _, issue := range allAgentBeads { - fields := beads.ParseAgentFields(issue.Description) - if fields != nil && fields.HookBead != "" { - allHookIDs = append(allHookIDs, fields.HookBead) + if issue.HookBead != "" { + allHookIDs = append(allHookIDs, issue.HookBead) } } allHookBeads, _ := agentBeads.ShowMultiple(allHookIDs) diff --git a/internal/daemon/lifecycle.go b/internal/daemon/lifecycle.go index 1897a1c4..2f794dd4 100644 --- a/internal/daemon/lifecycle.go +++ b/internal/daemon/lifecycle.go @@ -654,6 +654,8 @@ func (d *Daemon) getAgentBeadInfo(agentBeadID string) (*AgentBeadInfo, error) { Type string `json:"issue_type"` Description string `json:"description"` UpdatedAt string `json:"updated_at"` + HookBead string `json:"hook_bead"` // Read from database column + AgentState string `json:"agent_state"` // Read from database column } if err := json.Unmarshal(output, &issues); err != nil { @@ -669,7 +671,7 @@ func (d *Daemon) getAgentBeadInfo(agentBeadID string) (*AgentBeadInfo, error) { return nil, fmt.Errorf("bead %s is not an agent bead (type=%s)", agentBeadID, issue.Type) } - // Use shared parsing from beads package + // Parse agent fields from description for role/state info fields := beads.ParseAgentFieldsFromDescription(issue.Description) info := &AgentBeadInfo{ @@ -680,12 +682,15 @@ func (d *Daemon) getAgentBeadInfo(agentBeadID string) (*AgentBeadInfo, error) { if fields != nil { info.State = fields.AgentState - info.HookBead = fields.HookBead info.RoleBead = fields.RoleBead info.RoleType = fields.RoleType info.Rig = fields.Rig } + // Use HookBead from database column directly (not from description) + // The description may contain stale data - the slot is the source of truth. + info.HookBead = issue.HookBead + return info, nil } @@ -872,6 +877,8 @@ func (d *Daemon) checkRigGUPPViolations(rigName string) { Type string `json:"issue_type"` Description string `json:"description"` UpdatedAt string `json:"updated_at"` + HookBead string `json:"hook_bead"` // Read from database column, not description + AgentState string `json:"agent_state"` } if err := json.Unmarshal(output, &agents); err != nil { @@ -885,19 +892,14 @@ func (d *Daemon) checkRigGUPPViolations(rigName string) { continue } - // Parse agent fields - fields := beads.ParseAgentFieldsFromDescription(agent.Description) - if fields == nil { - continue - } - // Check if agent has work on hook - if fields.HookBead == "" { + // Use HookBead from database column directly (not parsed from description) + if agent.HookBead == "" { continue // No hooked work - no GUPP violation possible } // Check if agent is actively working - if fields.AgentState == "working" || fields.AgentState == "running" { + if agent.AgentState == "working" || agent.AgentState == "running" { // Check when the agent bead was last updated updatedAt, err := time.Parse(time.RFC3339, agent.UpdatedAt) if err != nil { @@ -907,10 +909,10 @@ func (d *Daemon) checkRigGUPPViolations(rigName string) { age := time.Since(updatedAt) if age > GUPPViolationTimeout { d.logger.Printf("GUPP violation: agent %s has hook_bead=%s but hasn't updated in %v (timeout: %v)", - agent.ID, fields.HookBead, age.Round(time.Minute), GUPPViolationTimeout) + agent.ID, agent.HookBead, age.Round(time.Minute), GUPPViolationTimeout) // Notify the witness for this rig - d.notifyWitnessOfGUPP(rigName, agent.ID, fields.HookBead, age) + d.notifyWitnessOfGUPP(rigName, agent.ID, agent.HookBead, age) } } } @@ -948,28 +950,31 @@ func (d *Daemon) checkOrphanedWork() { } // For each dead agent, check if they have hooked work + // Use HookBead from database column directly (not parsed from description) for _, agent := range deadAgents { - fields := beads.ParseAgentFieldsFromDescription(agent.Description) - if fields == nil || fields.HookBead == "" { + if agent.HookBead == "" { continue // No hooked work to orphan } d.logger.Printf("Orphaned work detected: agent %s is dead but has hook_bead=%s", - agent.ID, fields.HookBead) + agent.ID, agent.HookBead) // Determine the rig from the agent ID (gt-polecat--) rigName := d.extractRigFromAgentID(agent.ID) if rigName != "" { - d.notifyWitnessOfOrphanedWork(rigName, agent.ID, fields.HookBead) + d.notifyWitnessOfOrphanedWork(rigName, agent.ID, agent.HookBead) } } } +// deadAgentInfo holds info about a dead agent for orphaned work detection. +type deadAgentInfo struct { + ID string + HookBead string // Read from database column, not description +} + // getDeadAgents returns all agent beads with state=dead. -func (d *Daemon) getDeadAgents() []struct { - ID string - Description string -} { +func (d *Daemon) getDeadAgents() []deadAgentInfo { cmd := exec.Command("bd", "list", "--type=agent", "--json") cmd.Dir = d.config.TownRoot @@ -979,27 +984,23 @@ func (d *Daemon) getDeadAgents() []struct { } var agents []struct { - ID string `json:"id"` - Type string `json:"issue_type"` - Description string `json:"description"` + ID string `json:"id"` + Type string `json:"issue_type"` + HookBead string `json:"hook_bead"` // Read from database column + AgentState string `json:"agent_state"` // Read from database column } if err := json.Unmarshal(output, &agents); err != nil { return nil } - var dead []struct { - ID string - Description string - } - + var dead []deadAgentInfo for _, agent := range agents { - fields := beads.ParseAgentFieldsFromDescription(agent.Description) - if fields != nil && fields.AgentState == "dead" { - dead = append(dead, struct { - ID string - Description string - }{agent.ID, agent.Description}) + if agent.AgentState == "dead" { + dead = append(dead, deadAgentInfo{ + ID: agent.ID, + HookBead: agent.HookBead, + }) } }