From a1715fa91f55c6b0125205780d0ac2b0472ba2ce Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 28 Dec 2025 01:43:27 -0800 Subject: [PATCH] refactor: Move agent field parsing to shared beads package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add AgentFields and ParseAgentFieldsFromDescription to internal/beads/fields.go - Update daemon/lifecycle.go to use shared parsing - Update cmd/molecule_status.go to use shared parsing - Remove duplicate parsing code and unused isAgentRunningByBead function This consolidates agent bead field parsing in one place, following the pattern established for AttachmentFields and MRFields. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/beads/fields.go | 99 +++++++++++++++++++++++++++++++++ internal/cmd/molecule_status.go | 64 +-------------------- internal/daemon/lifecycle.go | 75 +++++++------------------ 3 files changed, 120 insertions(+), 118 deletions(-) diff --git a/internal/beads/fields.go b/internal/beads/fields.go index 45258d5a..487a95ab 100644 --- a/internal/beads/fields.go +++ b/internal/beads/fields.go @@ -3,6 +3,105 @@ package beads import "strings" +// AgentFields holds parsed fields from an agent bead's description. +// Agent beads store their state as key: value lines in the description. +type AgentFields struct { + RoleType string // role_type: mayor, deacon, witness, refinery, polecat + Rig string // rig: gastown (or null) + AgentState string // agent_state: idle, running, working, stopped + HookBead string // hook_bead: the bead ID on the hook (or null) + RoleBead string // role_bead: the role definition bead +} + +// ParseAgentFields extracts agent fields from an issue's description. +// Fields are expected as "key: value" lines. Returns nil if no agent fields found. +func ParseAgentFields(issue *Issue) *AgentFields { + if issue == nil || issue.Description == "" { + return nil + } + return ParseAgentFieldsFromDescription(issue.Description) +} + +// ParseAgentFieldsFromDescription extracts agent fields from a description string. +// Returns nil if no agent fields found. +func ParseAgentFieldsFromDescription(description string) *AgentFields { + if description == "" { + return nil + } + + fields := &AgentFields{} + hasFields := false + + for _, line := range strings.Split(description, "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + colonIdx := strings.Index(line, ":") + if colonIdx == -1 { + continue + } + + key := strings.TrimSpace(line[:colonIdx]) + value := strings.TrimSpace(line[colonIdx+1:]) + if value == "" || value == "null" { + continue + } + + switch strings.ToLower(key) { + case "role_type", "role-type", "roletype": + fields.RoleType = value + hasFields = true + case "rig": + fields.Rig = value + hasFields = true + case "agent_state", "agent-state", "agentstate": + fields.AgentState = value + hasFields = true + case "hook_bead", "hook-bead", "hookbead": + fields.HookBead = value + hasFields = true + case "role_bead", "role-bead", "rolebead": + fields.RoleBead = value + hasFields = true + } + } + + if !hasFields { + return nil + } + return fields +} + +// FormatAgentFields formats AgentFields as a string suitable for an issue description. +// Only non-empty fields are included. +func FormatAgentFields(fields *AgentFields) string { + if fields == nil { + return "" + } + + var lines []string + + if fields.RoleType != "" { + lines = append(lines, "role_type: "+fields.RoleType) + } + if fields.Rig != "" { + lines = append(lines, "rig: "+fields.Rig) + } + if fields.AgentState != "" { + lines = append(lines, "agent_state: "+fields.AgentState) + } + if fields.HookBead != "" { + lines = append(lines, "hook_bead: "+fields.HookBead) + } + if fields.RoleBead != "" { + lines = append(lines, "role_bead: "+fields.RoleBead) + } + + return strings.Join(lines, "\n") +} + // AttachmentFields holds the attachment info for pinned beads. // These fields track which molecule is attached to a handoff/pinned bead. type AttachmentFields struct { diff --git a/internal/cmd/molecule_status.go b/internal/cmd/molecule_status.go index 1152797f..813752fa 100644 --- a/internal/cmd/molecule_status.go +++ b/internal/cmd/molecule_status.go @@ -14,67 +14,7 @@ import ( "github.com/steveyegge/gastown/internal/workspace" ) -// AgentBeadFields holds parsed fields from an agent bead's description. -// Agent beads store their state as key: value lines in the description. -type AgentBeadFields struct { - RoleType string // role_type: mayor, deacon, witness, refinery, polecat - Rig string // rig: gastown (or null) - AgentState string // agent_state: idle, working, done - HookBead string // hook_bead: the bead ID on the hook (or null) - RoleBead string // role_bead: the role definition bead -} - -// ParseAgentBeadFields extracts agent bead fields from a bead's description. -// Returns nil if no agent fields found. -func ParseAgentBeadFields(description string) *AgentBeadFields { - if description == "" { - return nil - } - - fields := &AgentBeadFields{} - hasFields := false - - for _, line := range strings.Split(description, "\n") { - line = strings.TrimSpace(line) - if line == "" { - continue - } - - colonIdx := strings.Index(line, ":") - if colonIdx == -1 { - continue - } - - key := strings.TrimSpace(line[:colonIdx]) - value := strings.TrimSpace(line[colonIdx+1:]) - if value == "" || value == "null" { - continue - } - - switch strings.ToLower(key) { - case "role_type": - fields.RoleType = value - hasFields = true - case "rig": - fields.Rig = value - hasFields = true - case "agent_state": - fields.AgentState = value - hasFields = true - case "hook_bead": - fields.HookBead = value - hasFields = true - case "role_bead": - fields.RoleBead = value - hasFields = true - } - } - - if !hasFields { - return nil - } - return fields -} +// Note: Agent field parsing is now in internal/beads/fields.go (AgentFields, ParseAgentFieldsFromDescription) // buildAgentBeadID constructs the agent bead ID from an agent identity. // Examples: @@ -388,7 +328,7 @@ func runMoleculeStatus(cmd *cobra.Command, args []string) error { status.AgentBeadID = agentBeadID // Parse hook_bead from the agent bead's description - agentFields := ParseAgentBeadFields(agentBead.Description) + agentFields := beads.ParseAgentFieldsFromDescription(agentBead.Description) if agentFields != nil && agentFields.HookBead != "" { // Fetch the bead on the hook hookBead, err = b.Show(agentFields.HookBead) diff --git a/internal/daemon/lifecycle.go b/internal/daemon/lifecycle.go index 2803fb49..df86ead7 100644 --- a/internal/daemon/lifecycle.go +++ b/internal/daemon/lifecycle.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/tmux" ) @@ -501,60 +502,41 @@ func (d *Daemon) getAgentBeadInfo(agentBeadID string) (*AgentBeadInfo, error) { } // bd show --json returns an array with one element - var beads []struct { + var issues []struct { ID string `json:"id"` Type string `json:"issue_type"` Description string `json:"description"` UpdatedAt string `json:"updated_at"` } - if err := json.Unmarshal(output, &beads); err != nil { + if err := json.Unmarshal(output, &issues); err != nil { return nil, fmt.Errorf("parsing bd show output: %w", err) } - if len(beads) == 0 { + if len(issues) == 0 { return nil, fmt.Errorf("agent bead not found: %s", agentBeadID) } - bead := beads[0] - if bead.Type != "agent" { - return nil, fmt.Errorf("bead %s is not an agent bead (type=%s)", agentBeadID, bead.Type) + issue := issues[0] + if issue.Type != "agent" { + return nil, fmt.Errorf("bead %s is not an agent bead (type=%s)", agentBeadID, issue.Type) } - // Parse agent fields from description (YAML-like format) + // Use shared parsing from beads package + fields := beads.ParseAgentFieldsFromDescription(issue.Description) + info := &AgentBeadInfo{ - ID: bead.ID, - Type: bead.Type, - LastUpdate: bead.UpdatedAt, + ID: issue.ID, + Type: issue.Type, + LastUpdate: issue.UpdatedAt, } - for _, line := range strings.Split(bead.Description, "\n") { - line = strings.TrimSpace(line) - if line == "" { - continue - } - colonIdx := strings.Index(line, ":") - if colonIdx == -1 { - continue - } - key := strings.TrimSpace(line[:colonIdx]) - value := strings.TrimSpace(line[colonIdx+1:]) - if value == "" || value == "null" { - continue - } - - switch strings.ToLower(key) { - case "agent_state": - info.State = value - case "hook_bead": - info.HookBead = value - case "role_bead": - info.RoleBead = value - case "role_type": - info.RoleType = value - case "rig": - info.Rig = value - } + if fields != nil { + info.State = fields.AgentState + info.HookBead = fields.HookBead + info.RoleBead = fields.RoleBead + info.RoleType = fields.RoleType + info.Rig = fields.Rig } return info, nil @@ -595,25 +577,6 @@ func (d *Daemon) identityToAgentBeadID(identity string) string { } } -// isAgentRunningByBead checks if an agent reports itself as running via its agent bead. -// Returns (running, found) where found indicates if the agent bead exists. -func (d *Daemon) isAgentRunningByBead(identity string) (bool, bool) { - agentBeadID := d.identityToAgentBeadID(identity) - if agentBeadID == "" { - return false, false - } - - state, err := d.getAgentBeadState(agentBeadID) - if err != nil { - // Agent bead not found or not readable - return false, false - } - - // Consider "running" or "working" as running states - running := state == "running" || state == "working" - return running, true -} - // identityToBDActor converts a daemon identity (with dashes) to BD_ACTOR format (with slashes). // Examples: // - "mayor" → "mayor"