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:
committed by
Steve Yegge
parent
3db961c4bb
commit
4f99617b49
+109
-2
@@ -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
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/dog"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
@@ -201,6 +202,21 @@ func runDogAdd(cmd *cobra.Command, args []string) error {
|
||||
fmt.Printf(" %s: %s\n", rigName, path)
|
||||
}
|
||||
|
||||
// Create agent bead for the dog
|
||||
townRoot, _ := workspace.FindFromCwd()
|
||||
if townRoot != "" {
|
||||
b := beads.New(townRoot)
|
||||
location := filepath.Join("deacon", "dogs", name)
|
||||
|
||||
issue, err := b.CreateDogAgentBead(name, location)
|
||||
if err != nil {
|
||||
// Non-fatal: warn but don't fail dog creation
|
||||
fmt.Printf(" Warning: could not create agent bead: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf(" Agent bead: %s\n", issue.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -227,6 +243,13 @@ func runDogRemove(cmd *cobra.Command, args []string) error {
|
||||
names = args
|
||||
}
|
||||
|
||||
// Get beads client for cleanup
|
||||
townRoot, _ := workspace.FindFromCwd()
|
||||
var b *beads.Beads
|
||||
if townRoot != "" {
|
||||
b = beads.New(townRoot)
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
d, err := mgr.Get(name)
|
||||
if err != nil {
|
||||
@@ -244,6 +267,14 @@ func runDogRemove(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Removed dog %s\n", name)
|
||||
|
||||
// Delete agent bead for the dog
|
||||
if b != nil {
|
||||
if err := b.DeleteDogAgentBead(name); err != nil {
|
||||
// Non-fatal: warn but don't fail dog removal
|
||||
fmt.Printf(" Warning: could not delete agent bead: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user