From ae90b08f467b7accdc5be8e8d8604ae1f5fcf1f3 Mon Sep 17 00:00:00 2001 From: kustrun Date: Fri, 2 Jan 2026 20:12:57 +0100 Subject: [PATCH] fix(beads): Detect and use existing prefix from source repo When adding a rig from a source repo that has .beads/ tracked in git, detect and use the project's existing prefix instead of generating a new one. This prevents prefix mismatch errors when accessing existing issues via bd commands. Adds detectBeadsPrefixFromConfig() which reads the prefix from either config.yaml or by parsing the first issue ID from issues.jsonl. --- internal/rig/manager.go | 85 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/internal/rig/manager.go b/internal/rig/manager.go index 0d9076d4..229f4dcb 100644 --- a/internal/rig/manager.go +++ b/internal/rig/manager.go @@ -249,6 +249,24 @@ func (m *Manager) AddRig(opts AddRigOptions) (*Rig, error) { if err := m.git.Clone(opts.GitURL, mayorRigPath); err != nil { return nil, fmt.Errorf("cloning for mayor: %w", err) } + + // Check if source repo has .beads/ with its own prefix - if so, use that prefix. + // This ensures we use the project's existing beads database instead of creating a new one. + // Without this, routing would fail when trying to access existing issues because the + // rig config would have a different prefix than what the issues actually use. + sourceBeadsConfig := filepath.Join(mayorRigPath, ".beads", "config.yaml") + if _, err := os.Stat(sourceBeadsConfig); err == nil { + if sourcePrefix := detectBeadsPrefixFromConfig(sourceBeadsConfig); sourcePrefix != "" { + fmt.Printf(" Detected existing beads prefix '%s' from source repo\n", sourcePrefix) + opts.BeadsPrefix = sourcePrefix + rigConfig.Beads.Prefix = sourcePrefix + // Re-save rig config with detected prefix + if err := m.saveRigConfig(rigPath, rigConfig); err != nil { + return nil, fmt.Errorf("updating rig config with detected prefix: %w", err) + } + } + } + // Create mayor CLAUDE.md (overrides any from cloned repo) if err := m.createRoleCLAUDEmd(mayorRigPath, "mayor", opts.Name, ""); err != nil { return nil, fmt.Errorf("creating mayor CLAUDE.md: %w", err) @@ -601,6 +619,73 @@ func deriveBeadsPrefix(name string) string { return strings.ToLower(name[:2]) } +// detectBeadsPrefixFromConfig reads the issue prefix from a beads config.yaml file. +// Returns empty string if the file doesn't exist or doesn't contain a prefix. +// Falls back to detecting prefix from existing issues in issues.jsonl. +// +// When adding a rig from a source repo that has .beads/ tracked in git (like a project +// that already uses beads for issue tracking), we need to use that project's existing +// prefix instead of generating a new one. Otherwise, the rig would have a mismatched +// prefix and routing would fail to find the existing issues. +func detectBeadsPrefixFromConfig(configPath string) string { + data, err := os.ReadFile(configPath) + if err != nil { + return "" + } + + // Parse YAML-style config (simple line-by-line parsing) + // Looking for "issue-prefix: " or "prefix: " + lines := strings.Split(string(data), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + // Skip comments and empty lines + if line == "" || strings.HasPrefix(line, "#") { + continue + } + // Check for issue-prefix or prefix key + for _, key := range []string{"issue-prefix:", "prefix:"} { + if strings.HasPrefix(line, key) { + value := strings.TrimSpace(strings.TrimPrefix(line, key)) + // Remove quotes if present + value = strings.Trim(value, `"'`) + if value != "" { + return value + } + } + } + } + + // Fallback: try to detect prefix from existing issues in issues.jsonl + // Look for the first issue ID pattern like "gt-abc123" + beadsDir := filepath.Dir(configPath) + issuesPath := filepath.Join(beadsDir, "issues.jsonl") + if issuesData, err := os.ReadFile(issuesPath); err == nil { + issuesLines := strings.Split(string(issuesData), "\n") + for _, line := range issuesLines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + // Look for "id":"-" pattern + if idx := strings.Index(line, `"id":"`); idx != -1 { + start := idx + 6 // len(`"id":"`) + if end := strings.Index(line[start:], `"`); end != -1 { + issueID := line[start : start+end] + // Extract prefix (everything before the last hyphen-hash part) + if dashIdx := strings.LastIndex(issueID, "-"); dashIdx > 0 { + prefix := issueID[:dashIdx] + // Handle prefixes like "gt" (from "gt-abc") - return without trailing hyphen + return prefix + } + } + } + break // Only check first issue + } + } + + return "" +} + // RemoveRig unregisters a rig (does not delete files). func (m *Manager) RemoveRig(name string) error { if !m.RigExists(name) {