fix: auto-create agent bead when bd agent state called on non-existent agent

Previously, `bd agent state <agent> <state>` would fail if the agent bead
didn't exist in the database. This caused issues when `gt sling` tried to
update agent state for newly spawned polecats.

Now when the agent doesn't exist:
1. Parse role_type and rig from the agent ID (e.g., gt-gastown-polecat-nux)
2. Auto-create the agent bead with type=agent
3. Add role_type and rig labels for filtering (bd list --label=role_type:polecat)
4. Continue with the state update

This enables:
- Work history accumulation per polecat name
- Skill/success tracking over time
- `bd list --type=agent` to see all agents

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
obsidian
2026-01-02 12:26:56 -08:00
committed by Steve Yegge
parent 76359764fd
commit f37fe949e8
2 changed files with 229 additions and 21 deletions

View File

@@ -146,45 +146,107 @@ func runAgentState(cmd *cobra.Command, args []string) error {
ctx := rootCtx
// Resolve agent ID
// Resolve agent ID - if not found, we'll auto-create the agent bead
var agentID string
var notFound bool
if daemonClient != nil {
resp, err := daemonClient.ResolveID(&rpc.ResolveIDArgs{ID: agentArg})
if err != nil {
return fmt.Errorf("failed to resolve agent %s: %w", agentArg, err)
}
if err := json.Unmarshal(resp.Data, &agentID); err != nil {
return fmt.Errorf("parsing response: %w", err)
// Check if it's a "not found" error
if strings.Contains(err.Error(), "no issue found matching") {
notFound = true
agentID = agentArg // Use the input as the ID for creation
} else {
return fmt.Errorf("failed to resolve agent %s: %w", agentArg, err)
}
} else {
if err := json.Unmarshal(resp.Data, &agentID); err != nil {
return fmt.Errorf("parsing response: %w", err)
}
}
} else {
var err error
agentID, err = utils.ResolvePartialID(ctx, store, agentArg)
if err != nil {
return fmt.Errorf("failed to resolve agent %s: %w", agentArg, err)
// Check if it's a "not found" error
if strings.Contains(err.Error(), "no issue found matching") {
notFound = true
agentID = agentArg // Use the input as the ID for creation
} else {
return fmt.Errorf("failed to resolve agent %s: %w", agentArg, err)
}
}
}
// Get agent bead to verify it's an agent
var agent *types.Issue
if daemonClient != nil {
resp, err := daemonClient.Show(&rpc.ShowArgs{ID: agentID})
if err != nil {
return fmt.Errorf("agent bead not found: %s", agentID)
// If agent not found, auto-create it
if notFound {
roleType, rig := parseAgentIDFields(agentID)
agent = &types.Issue{
ID: agentID,
Title: fmt.Sprintf("Agent: %s", agentID),
IssueType: types.TypeAgent,
Status: types.StatusOpen,
RoleType: roleType,
Rig: rig,
CreatedBy: actor,
}
if err := json.Unmarshal(resp.Data, &agent); err != nil {
return fmt.Errorf("parsing response: %w", err)
if daemonClient != nil {
createArgs := &rpc.CreateArgs{
ID: agentID,
Title: agent.Title,
IssueType: string(types.TypeAgent),
RoleType: roleType,
Rig: rig,
CreatedBy: actor,
}
resp, err := daemonClient.Create(createArgs)
if err != nil {
return fmt.Errorf("failed to auto-create agent bead %s: %w", agentID, err)
}
if err := json.Unmarshal(resp.Data, &agent); err != nil {
return fmt.Errorf("parsing create response: %w", err)
}
} else {
if err := store.CreateIssue(ctx, agent, actor); err != nil {
return fmt.Errorf("failed to auto-create agent bead %s: %w", agentID, err)
}
// Add role_type and rig labels for filtering
if roleType != "" {
if err := store.AddLabel(ctx, agent.ID, "role_type:"+roleType, actor); err != nil {
fmt.Fprintf(os.Stderr, "warning: failed to add role_type label: %v\n", err)
}
}
if rig != "" {
if err := store.AddLabel(ctx, agent.ID, "rig:"+rig, actor); err != nil {
fmt.Fprintf(os.Stderr, "warning: failed to add rig label: %v\n", err)
}
}
}
} else {
var err error
agent, err = store.GetIssue(ctx, agentID)
if err != nil || agent == nil {
return fmt.Errorf("agent bead not found: %s", agentID)
// Get existing agent bead to verify it's an agent
if daemonClient != nil {
resp, err := daemonClient.Show(&rpc.ShowArgs{ID: agentID})
if err != nil {
return fmt.Errorf("agent bead not found: %s", agentID)
}
if err := json.Unmarshal(resp.Data, &agent); err != nil {
return fmt.Errorf("parsing response: %w", err)
}
} else {
var err error
agent, err = store.GetIssue(ctx, agentID)
if err != nil || agent == nil {
return fmt.Errorf("agent bead not found: %s", agentID)
}
}
}
// Verify agent bead is actually an agent
if agent.IssueType != "agent" {
return fmt.Errorf("%s is not an agent bead (type=%s)", agentID, agent.IssueType)
// Verify agent bead is actually an agent
if agent.IssueType != "agent" {
return fmt.Errorf("%s is not an agent bead (type=%s)", agentID, agent.IssueType)
}
}
// Update state and last_activity
@@ -627,3 +689,57 @@ func containsLabel(labels []string, label string) bool {
}
return false
}
// parseAgentIDFields extracts role_type and rig from an agent bead ID.
// Agent ID patterns:
// - Town-level: <prefix>-<role> (e.g., gt-mayor) → role="mayor", rig=""
// - Per-rig singleton: <prefix>-<rig>-<role> (e.g., gt-gastown-witness) → role="witness", rig="gastown"
// - Per-rig named: <prefix>-<rig>-<role>-<name> (e.g., gt-gastown-polecat-nux) → role="polecat", rig="gastown"
func parseAgentIDFields(agentID string) (roleType, rig string) {
// Must contain a hyphen to have a prefix
hyphenIdx := strings.Index(agentID, "-")
if hyphenIdx <= 0 {
return "", ""
}
// Split into parts after the prefix
rest := agentID[hyphenIdx+1:] // Skip "<prefix>-"
parts := strings.Split(rest, "-")
if len(parts) < 1 {
return "", ""
}
// Known roles for classification
townLevelRoles := map[string]bool{"mayor": true, "deacon": true}
rigLevelRoles := map[string]bool{"witness": true, "refinery": true}
namedRoles := map[string]bool{"crew": true, "polecat": true}
// Case 1: Town-level roles (gt-mayor, gt-deacon)
if len(parts) == 1 {
role := parts[0]
if townLevelRoles[role] {
return role, ""
}
return "", "" // Unknown format
}
// Case 2: Rig-level roles (<prefix>-<rig>-witness, <prefix>-<rig>-refinery)
if len(parts) == 2 {
potentialRig, potentialRole := parts[0], parts[1]
if rigLevelRoles[potentialRole] {
return potentialRole, potentialRig
}
return "", "" // Unknown format
}
// Case 3: Named roles (<prefix>-<rig>-crew-<name>, <prefix>-<rig>-polecat-<name>)
if len(parts) >= 3 {
potentialRig, potentialRole := parts[0], parts[1]
if namedRoles[potentialRole] {
return potentialRole, potentialRig
}
}
return "", "" // Unknown format
}

View File

@@ -48,3 +48,95 @@ func TestFormatTimeOrNil(t *testing.T) {
t.Errorf("expected nil for nil time, got %v", result)
}
}
func TestParseAgentIDFields(t *testing.T) {
tests := []struct {
name string
agentID string
wantRoleType string
wantRig string
}{
// Town-level roles
{
name: "town-level mayor",
agentID: "gt-mayor",
wantRoleType: "mayor",
wantRig: "",
},
{
name: "town-level deacon",
agentID: "bd-deacon",
wantRoleType: "deacon",
wantRig: "",
},
// Per-rig singleton roles
{
name: "rig-level witness",
agentID: "gt-gastown-witness",
wantRoleType: "witness",
wantRig: "gastown",
},
{
name: "rig-level refinery",
agentID: "bd-beads-refinery",
wantRoleType: "refinery",
wantRig: "beads",
},
// Per-rig named roles
{
name: "named polecat",
agentID: "gt-gastown-polecat-nux",
wantRoleType: "polecat",
wantRig: "gastown",
},
{
name: "named crew",
agentID: "bd-beads-crew-dave",
wantRoleType: "crew",
wantRig: "beads",
},
{
name: "polecat with hyphenated name",
agentID: "gt-gastown-polecat-nux-123",
wantRoleType: "polecat",
wantRig: "gastown",
},
// Edge cases
{
name: "no hyphen",
agentID: "invalid",
wantRoleType: "",
wantRig: "",
},
{
name: "empty string",
agentID: "",
wantRoleType: "",
wantRig: "",
},
{
name: "unknown role",
agentID: "gt-gastown-unknown",
wantRoleType: "",
wantRig: "",
},
{
name: "prefix only",
agentID: "gt-",
wantRoleType: "",
wantRig: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotRoleType, gotRig := parseAgentIDFields(tt.agentID)
if gotRoleType != tt.wantRoleType {
t.Errorf("parseAgentIDFields(%q) roleType = %q, want %q", tt.agentID, gotRoleType, tt.wantRoleType)
}
if gotRig != tt.wantRig {
t.Errorf("parseAgentIDFields(%q) rig = %q, want %q", tt.agentID, gotRig, tt.wantRig)
}
})
}
}