fix(beads): align agent bead prefixes and force multi-hyphen IDs (#482)

* fix(beads): align agent bead prefixes and force multi-hyphen IDs

* fix(checkpoint): treat threshold as stale at boundary
This commit is contained in:
JJ
2026-01-16 15:33:51 -05:00
committed by GitHub
parent 03213a7307
commit b1a5241430
17 changed files with 141 additions and 26 deletions

View File

@@ -424,6 +424,9 @@ func (b *Beads) Create(opts CreateOptions) (*Issue, error) {
// deterministic IDs rather than auto-generated ones.
func (b *Beads) CreateWithID(id string, opts CreateOptions) (*Issue, error) {
args := []string{"create", "--json", "--id=" + id}
if NeedsForceForID(id) {
args = append(args, "--force")
}
if opts.Title != "" {
args = append(args, "--title="+opts.Title)

View File

@@ -139,6 +139,9 @@ func (b *Beads) CreateAgentBead(id, title string, fields *AgentFields) (*Issue,
"--type=agent",
"--labels=gt:agent",
}
if NeedsForceForID(id) {
args = append(args, "--force")
}
// Default actor from BD_ACTOR env var for provenance tracking
if actor := os.Getenv("BD_ACTOR"); actor != "" {

View File

@@ -85,6 +85,9 @@ func (b *Beads) CreateRigBead(id, title string, fields *RigFields) (*Issue, erro
"--description=" + description,
"--labels=gt:rig",
}
if NeedsForceForID(id) {
args = append(args, "--force")
}
// Default actor from BD_ACTOR env var for provenance tracking
if actor := os.Getenv("BD_ACTOR"); actor != "" {

11
internal/beads/force.go Normal file
View File

@@ -0,0 +1,11 @@
package beads
import "strings"
// NeedsForceForID returns true when a bead ID uses multiple hyphens.
// Recent bd versions infer the prefix from the last hyphen, which can cause
// prefix-mismatch errors for valid system IDs like "st-stockdrop-polecat-nux"
// and "hq-cv-abc". We pass --force to honor the explicit ID in those cases.
func NeedsForceForID(id string) bool {
return strings.Count(id, "-") > 1
}

View File

@@ -0,0 +1,23 @@
package beads
import "testing"
func TestNeedsForceForID(t *testing.T) {
tests := []struct {
id string
want bool
}{
{id: "", want: false},
{id: "hq-mayor", want: false},
{id: "gt-abc123", want: false},
{id: "hq-mayor-role", want: true},
{id: "st-stockdrop-polecat-nux", want: true},
{id: "hq-cv-abc", want: true},
}
for _, tc := range tests {
if got := NeedsForceForID(tc.id); got != tc.want {
t.Fatalf("NeedsForceForID(%q) = %v, want %v", tc.id, got, tc.want)
}
}
}

View File

@@ -8,6 +8,8 @@ import (
"os"
"path/filepath"
"strings"
"github.com/steveyegge/gastown/internal/config"
)
// Route represents a prefix-to-path routing rule.
@@ -150,7 +152,7 @@ func GetPrefixForRig(townRoot, rigName string) string {
beadsDir := filepath.Join(townRoot, ".beads")
routes, err := LoadRoutes(beadsDir)
if err != nil || routes == nil {
return "gt" // Default prefix
return config.GetRigPrefix(townRoot, rigName)
}
// Look for a route where the path starts with the rig name
@@ -163,7 +165,7 @@ func GetPrefixForRig(townRoot, rigName string) string {
}
}
return "gt" // Default prefix
return config.GetRigPrefix(townRoot, rigName)
}
// FindConflictingPrefixes checks for duplicate prefixes in routes.

View File

@@ -4,6 +4,8 @@ import (
"os"
"path/filepath"
"testing"
"github.com/steveyegge/gastown/internal/config"
)
func TestGetPrefixForRig(t *testing.T) {
@@ -52,6 +54,33 @@ func TestGetPrefixForRig_NoRoutesFile(t *testing.T) {
}
}
func TestGetPrefixForRig_RigsConfigFallback(t *testing.T) {
tmpDir := t.TempDir()
// Write rigs.json with a non-gt prefix
rigsPath := filepath.Join(tmpDir, "mayor", "rigs.json")
if err := os.MkdirAll(filepath.Dir(rigsPath), 0755); err != nil {
t.Fatal(err)
}
cfg := &config.RigsConfig{
Version: config.CurrentRigsVersion,
Rigs: map[string]config.RigEntry{
"project_ideas": {
BeadsConfig: &config.BeadsConfig{Prefix: "pi"},
},
},
}
if err := config.SaveRigsConfig(rigsPath, cfg); err != nil {
t.Fatalf("SaveRigsConfig: %v", err)
}
result := GetPrefixForRig(tmpDir, "project_ideas")
if result != "pi" {
t.Errorf("Expected prefix from rigs config, got %q", result)
}
}
func TestExtractPrefix(t *testing.T) {
tests := []struct {
beadID string
@@ -100,7 +129,7 @@ func TestGetRigPathForPrefix(t *testing.T) {
}{
{"ap-", filepath.Join(tmpDir, "ai_platform/mayor/rig")},
{"gt-", filepath.Join(tmpDir, "gastown/mayor/rig")},
{"hq-", tmpDir}, // Town-level beads return townRoot
{"hq-", tmpDir}, // Town-level beads return townRoot
{"unknown-", ""}, // Unknown prefix returns empty
{"", ""}, // Empty prefix returns empty
}