From 87bdd6c63e3bff891c05a5fd9cec085ab6a3e984 Mon Sep 17 00:00:00 2001 From: Dan Shapiro Date: Sun, 4 Jan 2026 07:46:49 -0800 Subject: [PATCH 1/2] fix: create town role beads before agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- internal/cmd/install.go | 78 ++++++++++++------------ internal/cmd/install_integration_test.go | 50 +++++++++++++++ 2 files changed, 90 insertions(+), 38 deletions(-) diff --git a/internal/cmd/install.go b/internal/cmd/install.go index d4c63985..74d37d90 100644 --- a/internal/cmd/install.go +++ b/internal/cmd/install.go @@ -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 } diff --git a/internal/cmd/install_integration_test.go b/internal/cmd/install_integration_test.go index 778dadfd..96cca1c6 100644 --- a/internal/cmd/install_integration_test.go +++ b/internal/cmd/install_integration_test.go @@ -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) + } +} From e98ad32d7fe752fd5f1d03870dfe5126fd9581f5 Mon Sep 17 00:00:00 2001 From: Dan Shapiro Date: Sun, 4 Jan 2026 08:48:20 -0800 Subject: [PATCH 2/2] fix: list existing town agent beads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Install now detects existing town-level agents via bd list to avoid relying on bd show prefix matching, and the role slot test reads JSON from stdout only to ignore stderr warnings. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/cmd/install.go | 20 +++++++++++++++----- internal/cmd/install_integration_test.go | 7 +++++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/internal/cmd/install.go b/internal/cmd/install.go index 74d37d90..7b645aff 100644 --- a/internal/cmd/install.go +++ b/internal/cmd/install.go @@ -424,12 +424,22 @@ func initTownAgentBeads(townPath string) error { }, } + 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 { - // 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 - } + if _, ok := existingAgentIDs[agent.id]; ok { + continue } fields := &beads.AgentFields{ diff --git a/internal/cmd/install_integration_test.go b/internal/cmd/install_integration_test.go index 96cca1c6..c4f4e4a1 100644 --- a/internal/cmd/install_integration_test.go +++ b/internal/cmd/install_integration_test.go @@ -342,9 +342,12 @@ 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() + output, err := cmd.Output() if err != nil { - t.Fatalf("bd slot show %s failed: %v\nOutput: %s", issueID, err, output) + 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 {