diff --git a/internal/cmd/install.go b/internal/cmd/install.go index 0724611c..f62bc547 100644 --- a/internal/cmd/install.go +++ b/internal/cmd/install.go @@ -344,44 +344,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 @@ -448,5 +410,55 @@ 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.", + }, + } + + existingAgents, err := bd.List(beads.ListOptions{ + Status: "all", + Type: "agent", + Priority: -1, + }) + if err != nil { + return fmt.Errorf("listing existing agent beads: %w", err) + } + existingAgentIDs := make(map[string]struct{}, len(existingAgents)) + for _, issue := range existingAgents { + existingAgentIDs[issue.ID] = struct{}{} + } + + for _, agent := range agentDefs { + if _, ok := existingAgentIDs[agent.id]; ok { + 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 } diff --git a/internal/cmd/install_integration_test.go b/internal/cmd/install_integration_test.go index 83495e5e..65d8b68f 100644 --- a/internal/cmd/install_integration_test.go +++ b/internal/cmd/install_integration_test.go @@ -112,6 +112,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) { @@ -295,3 +320,31 @@ 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.Output() + if err != nil { + debugCmd := exec.Command("bd", "--no-daemon", "--json", "slot", "show", issueID) + debugCmd.Dir = townRoot + combined, _ := debugCmd.CombinedOutput() + t.Fatalf("bd slot show %s failed: %v\nOutput: %s", issueID, err, combined) + } + + 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) + } +}