From ec53dfbb4016039a13eac9e8449935f8d18ecd01 Mon Sep 17 00:00:00 2001 From: furiosa Date: Tue, 6 Jan 2026 18:54:55 -0800 Subject: [PATCH] feat(rig): add rig identity bead schema and creation (gt-zmznh) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add RigFields struct, CreateRigBead, RigBeadID helpers to beads package - Modify gt rig add to create rig identity bead after rig creation - Schema: id=-rig-, type=rig, with repo/prefix/state fields 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/beads/beads.go | 110 ++++++++++++++++++++++++++++++++++++++++ internal/cmd/rig.go | 21 ++++++++ 2 files changed, 131 insertions(+) diff --git a/internal/beads/beads.go b/internal/beads/beads.go index 17abf5a2..574b77a7 100644 --- a/internal/beads/beads.go +++ b/internal/beads/beads.go @@ -1780,3 +1780,113 @@ func (b *Beads) MergeSlotEnsureExists() (string, error) { return status.ID, nil } + +// ===== Rig Identity Beads ===== + +// RigFields contains the fields specific to rig identity beads. +type RigFields struct { + Repo string // Git URL for the rig's repository + Prefix string // Beads prefix for this rig (e.g., "gt", "bd") + State string // Operational state: active, archived, maintenance +} + +// FormatRigDescription formats the description field for a rig identity bead. +func FormatRigDescription(name string, fields *RigFields) string { + if fields == nil { + return "" + } + + var lines []string + lines = append(lines, fmt.Sprintf("Rig identity bead for %s.", name)) + lines = append(lines, "") + + if fields.Repo != "" { + lines = append(lines, fmt.Sprintf("repo: %s", fields.Repo)) + } + if fields.Prefix != "" { + lines = append(lines, fmt.Sprintf("prefix: %s", fields.Prefix)) + } + if fields.State != "" { + lines = append(lines, fmt.Sprintf("state: %s", fields.State)) + } + + return strings.Join(lines, "\n") +} + +// ParseRigFields extracts rig fields from an issue's description. +func ParseRigFields(description string) *RigFields { + fields := &RigFields{} + + for _, line := range strings.Split(description, "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + colonIdx := strings.Index(line, ":") + if colonIdx == -1 { + continue + } + + key := strings.TrimSpace(line[:colonIdx]) + value := strings.TrimSpace(line[colonIdx+1:]) + if value == "null" || value == "" { + value = "" + } + + switch strings.ToLower(key) { + case "repo": + fields.Repo = value + case "prefix": + fields.Prefix = value + case "state": + fields.State = value + } + } + + return fields +} + +// CreateRigBead creates a rig identity bead for tracking rig metadata. +// The ID format is: -rig- (e.g., gt-rig-gastown) +// Use RigBeadID() helper to generate correct IDs. +// The created_by field is populated from BD_ACTOR env var for provenance tracking. +func (b *Beads) CreateRigBead(id, title string, fields *RigFields) (*Issue, error) { + description := FormatRigDescription(title, fields) + + args := []string{"create", "--json", + "--id=" + id, + "--type=rig", + "--title=" + title, + "--description=" + description, + } + + // Default actor from BD_ACTOR env var for provenance tracking + if actor := os.Getenv("BD_ACTOR"); actor != "" { + args = append(args, "--actor="+actor) + } + + out, err := b.run(args...) + if err != nil { + return nil, err + } + + var issue Issue + if err := json.Unmarshal(out, &issue); err != nil { + return nil, fmt.Errorf("parsing bd create output: %w", err) + } + + return &issue, nil +} + +// RigBeadIDWithPrefix generates a rig identity bead ID using the specified prefix. +// Format: -rig- (e.g., gt-rig-gastown) +func RigBeadIDWithPrefix(prefix, name string) string { + return fmt.Sprintf("%s-rig-%s", prefix, name) +} + +// RigBeadID generates a rig identity bead ID using "gt" prefix. +// For non-gastown rigs, use RigBeadIDWithPrefix with the rig's configured prefix. +func RigBeadID(name string) string { + return RigBeadIDWithPrefix("gt", name) +} diff --git a/internal/cmd/rig.go b/internal/cmd/rig.go index fa1ee1a7..7d80d8f4 100644 --- a/internal/cmd/rig.go +++ b/internal/cmd/rig.go @@ -366,12 +366,16 @@ func runRigAdd(cmd *cobra.Command, args []string) error { // - Otherwise route to rig root (where initBeads creates the database) // The conditional routing is necessary because initBeads creates the database at // "/.beads", while repos with tracked beads have their database at mayor/rig/.beads. + var beadsWorkDir string if newRig.Config.Prefix != "" { routePath := name mayorRigBeads := filepath.Join(townRoot, name, "mayor", "rig", ".beads") if _, err := os.Stat(mayorRigBeads); err == nil { // Source repo has .beads/ tracked - route to mayor/rig routePath = name + "/mayor/rig" + beadsWorkDir = filepath.Join(townRoot, name, "mayor", "rig") + } else { + beadsWorkDir = filepath.Join(townRoot, name) } route := beads.Route{ Prefix: newRig.Config.Prefix + "-", @@ -383,6 +387,23 @@ func runRigAdd(cmd *cobra.Command, args []string) error { } } + // Create rig identity bead + if newRig.Config.Prefix != "" && beadsWorkDir != "" { + bd := beads.New(beadsWorkDir) + rigBeadID := beads.RigBeadIDWithPrefix(newRig.Config.Prefix, name) + fields := &beads.RigFields{ + Repo: gitURL, + Prefix: newRig.Config.Prefix, + State: "active", + } + if _, err := bd.CreateRigBead(rigBeadID, name, fields); err != nil { + // Non-fatal: rig is functional without the identity bead + fmt.Printf(" %s Could not create rig identity bead: %v\n", style.Warning.Render("!"), err) + } else { + fmt.Printf(" Created rig identity bead: %s\n", rigBeadID) + } + } + elapsed := time.Since(startTime) // Read default branch from rig config