fix: create town role beads before agents
Install now creates town role beads before agents and only skips agent creation when the exact agent bead exists, so role slots get set reliably. Add an integration test that asserts role slots are populated after install. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -345,44 +345,6 @@ func ensureRepoFingerprint(beadsPath string) error {
|
||||
func initTownAgentBeads(townPath string) error {
|
||||
bd := beads.New(townPath)
|
||||
|
||||
// Town-level agent beads
|
||||
agentDefs := []struct {
|
||||
id string
|
||||
roleType string
|
||||
title string
|
||||
}{
|
||||
{
|
||||
id: beads.MayorBeadIDTown(),
|
||||
roleType: "mayor",
|
||||
title: "Mayor - global coordinator, handles cross-rig communication and escalations.",
|
||||
},
|
||||
{
|
||||
id: beads.DeaconBeadIDTown(),
|
||||
roleType: "deacon",
|
||||
title: "Deacon (daemon beacon) - receives mechanical heartbeats, runs town plugins and monitoring.",
|
||||
},
|
||||
}
|
||||
|
||||
for _, agent := range agentDefs {
|
||||
// Check if already exists
|
||||
if _, err := bd.Show(agent.id); err == nil {
|
||||
continue // Already exists
|
||||
}
|
||||
|
||||
fields := &beads.AgentFields{
|
||||
RoleType: agent.roleType,
|
||||
Rig: "", // Town-level agents have no rig
|
||||
AgentState: "idle",
|
||||
HookBead: "",
|
||||
RoleBead: beads.RoleBeadIDTown(agent.roleType),
|
||||
}
|
||||
|
||||
if _, err := bd.CreateAgentBead(agent.id, agent.title, fields); err != nil {
|
||||
return fmt.Errorf("creating %s: %w", agent.id, err)
|
||||
}
|
||||
fmt.Printf(" ✓ Created agent bead: %s\n", agent.id)
|
||||
}
|
||||
|
||||
// Role beads (global templates)
|
||||
roleDefs := []struct {
|
||||
id string
|
||||
@@ -444,5 +406,45 @@ func initTownAgentBeads(townPath string) error {
|
||||
fmt.Printf(" ✓ Created role bead: %s\n", role.id)
|
||||
}
|
||||
|
||||
// Town-level agent beads
|
||||
agentDefs := []struct {
|
||||
id string
|
||||
roleType string
|
||||
title string
|
||||
}{
|
||||
{
|
||||
id: beads.MayorBeadIDTown(),
|
||||
roleType: "mayor",
|
||||
title: "Mayor - global coordinator, handles cross-rig communication and escalations.",
|
||||
},
|
||||
{
|
||||
id: beads.DeaconBeadIDTown(),
|
||||
roleType: "deacon",
|
||||
title: "Deacon (daemon beacon) - receives mechanical heartbeats, runs town plugins and monitoring.",
|
||||
},
|
||||
}
|
||||
|
||||
for _, agent := range agentDefs {
|
||||
// Check if already exists (exact ID + agent type).
|
||||
if issue, err := bd.Show(agent.id); err == nil {
|
||||
if issue.ID == agent.id && issue.Type == "agent" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
fields := &beads.AgentFields{
|
||||
RoleType: agent.roleType,
|
||||
Rig: "", // Town-level agents have no rig
|
||||
AgentState: "idle",
|
||||
HookBead: "",
|
||||
RoleBead: beads.RoleBeadIDTown(agent.roleType),
|
||||
}
|
||||
|
||||
if _, err := bd.CreateAgentBead(agent.id, agent.title, fields); err != nil {
|
||||
return fmt.Errorf("creating %s: %w", agent.id, err)
|
||||
}
|
||||
fmt.Printf(" ✓ Created agent bead: %s\n", agent.id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -129,6 +129,31 @@ func TestInstallBeadsHasCorrectPrefix(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestInstallTownRoleSlots validates that town-level agent beads
|
||||
// have their role slot set after install.
|
||||
func TestInstallTownRoleSlots(t *testing.T) {
|
||||
// Skip if bd is not available
|
||||
if _, err := exec.LookPath("bd"); err != nil {
|
||||
t.Skip("bd not installed, skipping role slot test")
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
hqPath := filepath.Join(tmpDir, "test-hq")
|
||||
|
||||
gtBinary := buildGT(t)
|
||||
|
||||
// Run gt install (includes beads init by default)
|
||||
cmd := exec.Command(gtBinary, "install", hqPath)
|
||||
cmd.Env = append(os.Environ(), "HOME="+tmpDir)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("gt install failed: %v\nOutput: %s", err, output)
|
||||
}
|
||||
|
||||
assertSlotValue(t, hqPath, "hq-mayor", "role", "hq-mayor-role")
|
||||
assertSlotValue(t, hqPath, "hq-deacon", "role", "hq-deacon-role")
|
||||
}
|
||||
|
||||
// TestInstallIdempotent validates that running gt install twice
|
||||
// on the same directory fails without --force flag.
|
||||
func TestInstallIdempotent(t *testing.T) {
|
||||
@@ -312,3 +337,28 @@ func assertFileExists(t *testing.T, path, name string) {
|
||||
t.Errorf("%s is a directory, expected file", name)
|
||||
}
|
||||
}
|
||||
|
||||
func assertSlotValue(t *testing.T, townRoot, issueID, slot, want string) {
|
||||
t.Helper()
|
||||
cmd := exec.Command("bd", "--no-daemon", "--json", "slot", "show", issueID)
|
||||
cmd.Dir = townRoot
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("bd slot show %s failed: %v\nOutput: %s", issueID, err, output)
|
||||
}
|
||||
|
||||
var parsed struct {
|
||||
Slots map[string]*string `json:"slots"`
|
||||
}
|
||||
if err := json.Unmarshal(output, &parsed); err != nil {
|
||||
t.Fatalf("parsing slot show output failed: %v\nOutput: %s", err, output)
|
||||
}
|
||||
|
||||
var got string
|
||||
if value, ok := parsed.Slots[slot]; ok && value != nil {
|
||||
got = *value
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("slot %s for %s = %q, want %q", slot, issueID, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user