Add agent bead lifecycle to gt dog add/remove

When adding a dog, creates an agent bead with role_type:dog label.
When removing a dog, deletes the corresponding agent bead.

This enables @dogs group resolution in the mail router by allowing
queries like `bd list --type=agent --label=role_type:dog`.

Changes:
- Add DogBeadID(), DogRoleBeadID() helper functions
- Add CreateDogAgentBead() for creating dog agent beads with labels
- Add FindDogAgentBead() and DeleteDogAgentBead() for cleanup
- Add Labels field to Issue struct for label parsing
- Update ParseAgentBeadID() to handle dog bead IDs (gt-dog-<name>)
- Update IsAgentSessionBead() to include "dog" as valid role

(gt-qha0g)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown/polecats/furiosa
2025-12-30 22:38:04 -08:00
committed by Steve Yegge
parent 3db961c4bb
commit 4f99617b49
2 changed files with 140 additions and 2 deletions

View File

@@ -100,6 +100,7 @@ type Issue struct {
DependsOn []string `json:"depends_on,omitempty"`
Blocks []string `json:"blocks,omitempty"`
BlockedBy []string `json:"blocked_by,omitempty"`
Labels []string `json:"labels,omitempty"`
// Agent bead slots (type=agent only)
HookBead string `json:"hook_bead,omitempty"` // Current work attached to agent's hook
@@ -996,6 +997,101 @@ func DeaconBeadID() string {
return "gt-deacon"
}
// DogBeadID returns a Dog agent bead ID.
// Dogs are town-level agents, so they follow the pattern: gt-dog-<name>
func DogBeadID(name string) string {
return "gt-dog-" + name
}
// DogRoleBeadID returns the Dog role bead ID.
func DogRoleBeadID() string {
return RoleBeadID("dog")
}
// CreateDogAgentBead creates an agent bead for a dog.
// Dogs use a different schema than other agents - they use labels for metadata.
// Returns the created issue or an error.
func (b *Beads) CreateDogAgentBead(name, location string) (*Issue, error) {
title := fmt.Sprintf("Dog: %s", name)
labels := []string{
"role_type:dog",
"rig:town",
"location:" + location,
}
args := []string{
"create", "--json",
"--type=agent",
"--role-type=dog",
"--title=" + title,
"--labels=" + strings.Join(labels, ","),
}
// Default actor from BD_ACTOR env var for provenance tracking
if actor := os.Getenv("BD_ACTOR"); actor != "" {
args = append(args, "--actor="+actor)
}
out, err := b.run(args...)
if err != nil {
return nil, err
}
var issue Issue
if err := json.Unmarshal(out, &issue); err != nil {
return nil, fmt.Errorf("parsing bd create output: %w", err)
}
return &issue, nil
}
// FindDogAgentBead finds the agent bead for a dog by name.
// Searches for agent beads with role_type:dog and matching title.
// Returns nil if not found.
func (b *Beads) FindDogAgentBead(name string) (*Issue, error) {
// List all agent beads and filter by role_type:dog label
issues, err := b.List(ListOptions{
Type: "agent",
Status: "all",
Priority: -1, // No priority filter
})
if err != nil {
return nil, fmt.Errorf("listing agents: %w", err)
}
expectedTitle := fmt.Sprintf("Dog: %s", name)
for _, issue := range issues {
// Check title match and role_type:dog label
if issue.Title == expectedTitle {
for _, label := range issue.Labels {
if label == "role_type:dog" {
return issue, nil
}
}
}
}
return nil, nil
}
// DeleteDogAgentBead finds and deletes the agent bead for a dog.
// Returns nil if the bead doesn't exist (idempotent).
func (b *Beads) DeleteDogAgentBead(name string) error {
issue, err := b.FindDogAgentBead(name)
if err != nil {
return fmt.Errorf("finding dog bead: %w", err)
}
if issue == nil {
return nil // Already doesn't exist - idempotent
}
err = b.DeleteAgentBead(issue.ID)
if err != nil {
return fmt.Errorf("deleting bead %s: %w", issue.ID, err)
}
return nil
}
// WitnessBeadIDWithPrefix returns the Witness agent bead ID for a rig using the specified prefix.
func WitnessBeadIDWithPrefix(prefix, rig string) string {
return AgentBeadIDWithPrefix(prefix, rig, "witness", "")
@@ -1057,14 +1153,25 @@ func ParseAgentBeadID(id string) (rig, role, name string, ok bool) {
// Town-level: gt-mayor, bd-deacon
return "", parts[0], "", true
case 2:
// Rig-level singleton: gt-gastown-witness, bd-beads-witness
// Could be rig-level singleton (gt-gastown-witness) or
// town-level named (gt-dog-alpha for dogs)
if parts[0] == "dog" {
// Dogs are town-level named agents: gt-dog-<name>
return "", "dog", parts[1], true
}
// Rig-level singleton: gt-gastown-witness
return parts[0], parts[1], "", true
case 3:
// Rig-level named: gt-gastown-crew-max, bd-beads-polecat-pearl
return parts[0], parts[1], parts[2], true
default:
// Handle names with hyphens: gt-gastown-polecat-my-agent-name
// or gt-dog-my-agent-name
if len(parts) >= 3 {
if parts[0] == "dog" {
// Dog with hyphenated name: gt-dog-my-dog-name
return "", "dog", strings.Join(parts[1:], "-"), true
}
return parts[0], parts[1], strings.Join(parts[2:], "-"), true
}
return "", "", "", false
@@ -1082,7 +1189,7 @@ func IsAgentSessionBead(beadID string) bool {
}
// Known agent roles
switch role {
case "mayor", "deacon", "witness", "refinery", "crew", "polecat":
case "mayor", "deacon", "witness", "refinery", "crew", "polecat", "dog":
return true
default:
return false