fix(create): use agent-aware prefix extraction for agent beads

The generic ValidateIDFormat() used isLikelyHash() which treated
3-character suffixes like "nux" as valid hashes, causing agent IDs
like "nx-nexus-polecat-nux" to extract prefix as "nx-nexus-polecat"
instead of the correct "nx".

Fix: For --type=agent, validate agent ID format first and use
ExtractAgentPrefix() which correctly extracts prefix from the
first hyphen for agent IDs.

Fixes #591

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown/crew/max
2026-01-17 00:42:06 -08:00
committed by gastown/crew/dennis
parent 2c00d6d203
commit 87f84c5fa6
3 changed files with 73 additions and 10 deletions

View File

@@ -400,9 +400,23 @@ var createCmd = &cobra.Command{
// Validate explicit ID format if provided // Validate explicit ID format if provided
if explicitID != "" { if explicitID != "" {
requestedPrefix, err := validation.ValidateIDFormat(explicitID) var requestedPrefix string
if err != nil { var err error
FatalError("%v", err)
// For agent types, use agent-aware prefix extraction.
// This fixes the bug where 3-char polecat names like "nux" in
// "nx-nexus-polecat-nux" were incorrectly treated as hash suffixes,
// causing prefix to be extracted as "nx-nexus-polecat" instead of "nx".
if issueType == "agent" {
if err := validation.ValidateAgentID(explicitID); err != nil {
FatalError("invalid agent ID: %v", err)
}
requestedPrefix = validation.ExtractAgentPrefix(explicitID)
} else {
requestedPrefix, err = validation.ValidateIDFormat(explicitID)
if err != nil {
FatalError("%v", err)
}
} }
// Validate prefix matches database prefix // Validate prefix matches database prefix
@@ -431,13 +445,6 @@ var createCmd = &cobra.Command{
if err := validation.ValidatePrefixWithAllowed(requestedPrefix, dbPrefix, allowedPrefixes, forceCreate); err != nil { if err := validation.ValidatePrefixWithAllowed(requestedPrefix, dbPrefix, allowedPrefixes, forceCreate); err != nil {
FatalError("%v", err) FatalError("%v", err)
} }
// Validate agent ID pattern if type is agent
if issueType == "agent" {
if err := validation.ValidateAgentID(explicitID); err != nil {
FatalError("invalid agent ID: %v", err)
}
}
} }
var externalRefPtr *string var externalRefPtr *string

View File

@@ -170,6 +170,22 @@ func isNamedRole(s string) bool {
return false return false
} }
// ExtractAgentPrefix extracts the prefix from an agent ID.
// Agent IDs have the format: prefix-rig-role-name or prefix-role
// The prefix is always the part before the first hyphen.
// Examples:
// - "gt-gastown-polecat-nux" -> "gt"
// - "nx-nexus-polecat-nux" -> "nx"
// - "gt-mayor" -> "gt"
// - "bd-beads-witness" -> "bd"
func ExtractAgentPrefix(id string) string {
hyphenIdx := strings.Index(id, "-")
if hyphenIdx <= 0 {
return ""
}
return id[:hyphenIdx]
}
// ValidateAgentID validates that an agent ID follows the expected pattern. // ValidateAgentID validates that an agent ID follows the expected pattern.
// Canonical format: prefix-rig-role-name // Canonical format: prefix-rig-role-name
// Patterns: // Patterns:

View File

@@ -404,3 +404,43 @@ func TestValidateAgentID(t *testing.T) {
}) })
} }
} }
func TestExtractAgentPrefix(t *testing.T) {
tests := []struct {
name string
id string
wantPrefix string
}{
// Town-level agents
{"mayor", "gt-mayor", "gt"},
{"deacon", "gt-deacon", "gt"},
{"bd mayor", "bd-mayor", "bd"},
// Per-rig agents
{"witness", "gt-gastown-witness", "gt"},
{"refinery", "bd-beads-refinery", "bd"},
// Named agents - the bug case
{"polecat 3-char name", "nx-nexus-polecat-nux", "nx"},
{"polecat regular", "gt-gastown-polecat-phoenix", "gt"},
{"crew", "gt-beads-crew-dave", "gt"},
// Hyphenated rig names
{"hyphenated rig", "gt-my-project-witness", "gt"},
{"multi-hyphen rig polecat", "bd-my-cool-app-polecat-bob", "bd"},
// Edge cases
{"no hyphen", "nohyphen", ""},
{"empty", "", ""},
{"just prefix", "gt-", "gt"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ExtractAgentPrefix(tt.id)
if got != tt.wantPrefix {
t.Errorf("ExtractAgentPrefix(%q) = %q, want %q", tt.id, got, tt.wantPrefix)
}
})
}
}