refactor: Move agent field parsing to shared beads package
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,105 @@ package beads
|
|||||||
|
|
||||||
import "strings"
|
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.
|
// AttachmentFields holds the attachment info for pinned beads.
|
||||||
// These fields track which molecule is attached to a handoff/pinned bead.
|
// These fields track which molecule is attached to a handoff/pinned bead.
|
||||||
type AttachmentFields struct {
|
type AttachmentFields struct {
|
||||||
|
|||||||
@@ -14,67 +14,7 @@ import (
|
|||||||
"github.com/steveyegge/gastown/internal/workspace"
|
"github.com/steveyegge/gastown/internal/workspace"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AgentBeadFields holds parsed fields from an agent bead's description.
|
// Note: Agent field parsing is now in internal/beads/fields.go (AgentFields, ParseAgentFieldsFromDescription)
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildAgentBeadID constructs the agent bead ID from an agent identity.
|
// buildAgentBeadID constructs the agent bead ID from an agent identity.
|
||||||
// Examples:
|
// Examples:
|
||||||
@@ -388,7 +328,7 @@ func runMoleculeStatus(cmd *cobra.Command, args []string) error {
|
|||||||
status.AgentBeadID = agentBeadID
|
status.AgentBeadID = agentBeadID
|
||||||
|
|
||||||
// Parse hook_bead from the agent bead's description
|
// Parse hook_bead from the agent bead's description
|
||||||
agentFields := ParseAgentBeadFields(agentBead.Description)
|
agentFields := beads.ParseAgentFieldsFromDescription(agentBead.Description)
|
||||||
if agentFields != nil && agentFields.HookBead != "" {
|
if agentFields != nil && agentFields.HookBead != "" {
|
||||||
// Fetch the bead on the hook
|
// Fetch the bead on the hook
|
||||||
hookBead, err = b.Show(agentFields.HookBead)
|
hookBead, err = b.Show(agentFields.HookBead)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/steveyegge/gastown/internal/beads"
|
||||||
"github.com/steveyegge/gastown/internal/tmux"
|
"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
|
// bd show --json returns an array with one element
|
||||||
var beads []struct {
|
var issues []struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Type string `json:"issue_type"`
|
Type string `json:"issue_type"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
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)
|
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)
|
return nil, fmt.Errorf("agent bead not found: %s", agentBeadID)
|
||||||
}
|
}
|
||||||
|
|
||||||
bead := beads[0]
|
issue := issues[0]
|
||||||
if bead.Type != "agent" {
|
if issue.Type != "agent" {
|
||||||
return nil, fmt.Errorf("bead %s is not an agent bead (type=%s)", agentBeadID, bead.Type)
|
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{
|
info := &AgentBeadInfo{
|
||||||
ID: bead.ID,
|
ID: issue.ID,
|
||||||
Type: bead.Type,
|
Type: issue.Type,
|
||||||
LastUpdate: bead.UpdatedAt,
|
LastUpdate: issue.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, line := range strings.Split(bead.Description, "\n") {
|
if fields != nil {
|
||||||
line = strings.TrimSpace(line)
|
info.State = fields.AgentState
|
||||||
if line == "" {
|
info.HookBead = fields.HookBead
|
||||||
continue
|
info.RoleBead = fields.RoleBead
|
||||||
}
|
info.RoleType = fields.RoleType
|
||||||
colonIdx := strings.Index(line, ":")
|
info.Rig = fields.Rig
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return info, nil
|
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).
|
// identityToBDActor converts a daemon identity (with dashes) to BD_ACTOR format (with slashes).
|
||||||
// Examples:
|
// Examples:
|
||||||
// - "mayor" → "mayor"
|
// - "mayor" → "mayor"
|
||||||
|
|||||||
Reference in New Issue
Block a user