fix(routing): complete bd init --contributor routing (bd-6x6g) (#1088)

Implements the missing contributor routing logic so bd init --contributor actually works. Contributors' issues automatically route to ~/.beads-planning/ while maintainers' issues stay local.
This commit is contained in:
Peter Chanthamynavong
2026-01-14 20:50:56 -08:00
committed by GitHub
parent b9d2799d29
commit 31239495f1
6 changed files with 1211 additions and 21 deletions

View File

@@ -1,6 +1,7 @@
package main
import (
"context"
"encoding/json"
"fmt"
"os"
@@ -14,6 +15,7 @@ import (
"github.com/steveyegge/beads/internal/hooks"
"github.com/steveyegge/beads/internal/routing"
"github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/timeparsing"
"github.com/steveyegge/beads/internal/types"
@@ -314,22 +316,64 @@ var createCmd = &cobra.Command{
debug.Logf("Warning: failed to detect user role: %v\n", err)
}
// Build routing config with backward compatibility for legacy contributor.* keys
routingMode := config.GetString("routing.mode")
contributorRepo := config.GetString("routing.contributor")
// NFR-001: Backward compatibility - fall back to legacy contributor.* keys
if routingMode == "" {
if config.GetString("contributor.auto_route") == "true" {
routingMode = "auto"
}
}
if contributorRepo == "" {
contributorRepo = config.GetString("contributor.planning_repo")
}
routingConfig := &routing.RoutingConfig{
Mode: config.GetString("routing.mode"),
Mode: routingMode,
DefaultRepo: config.GetString("routing.default"),
MaintainerRepo: config.GetString("routing.maintainer"),
ContributorRepo: config.GetString("routing.contributor"),
ContributorRepo: contributorRepo,
ExplicitOverride: repoOverride,
}
repoPath = routing.DetermineTargetRepo(routingConfig, userRole, ".")
}
// TODO(bd-6x6g): Switch to target repo for multi-repo support
// For now, we just log the target repo in debug mode
// Switch to target repo for multi-repo support (bd-6x6g)
// When routing to a different repo, we bypass daemon mode and use direct storage
var targetStore storage.Storage
var routedToTarget bool
if repoPath != "." {
debug.Logf("DEBUG: Target repo: %s\n", repoPath)
targetBeadsDir := routing.ExpandPath(repoPath)
debug.Logf("DEBUG: Routing to target repo: %s\n", targetBeadsDir)
// Ensure target beads directory exists with prefix inheritance
if err := ensureBeadsDirForPath(rootCtx, targetBeadsDir, store); err != nil {
FatalError("failed to initialize target repo: %v", err)
}
// Open new store for target repo
targetDBPath := filepath.Join(targetBeadsDir, ".beads", "beads.db")
var err error
targetStore, err = sqlite.New(rootCtx, targetDBPath)
if err != nil {
FatalError("failed to open target store: %v", err)
}
defer func() {
if err := targetStore.Close(); err != nil {
fmt.Fprintf(os.Stderr, "warning: failed to close target store: %v\n", err)
}
}()
// Replace store for remainder of create operation
// This also bypasses daemon mode since daemon owns the current repo's store
store = targetStore
daemonClient = nil // Bypass daemon for routed issues (T013)
routedToTarget = true
}
_ = routedToTarget // Used for logging context
// Check for conflicting flags
if explicitID != "" && parentID != "" {
@@ -899,3 +943,56 @@ func formatTimeForRPC(t *time.Time) string {
}
return t.Format(time.RFC3339)
}
// ensureBeadsDirForPath ensures a beads directory exists at the target path.
// If the .beads directory doesn't exist, it creates it and initializes with
// the same prefix as the source store (T010, T012: prefix inheritance).
func ensureBeadsDirForPath(ctx context.Context, targetPath string, sourceStore storage.Storage) error {
beadsDir := filepath.Join(targetPath, ".beads")
dbPath := filepath.Join(beadsDir, "beads.db")
// Check if beads directory already exists
if _, err := os.Stat(beadsDir); err == nil {
// Directory exists, check if database exists
if _, err := os.Stat(dbPath); err == nil {
// Database exists, nothing to do
return nil
}
}
// Create .beads directory
if err := os.MkdirAll(beadsDir, 0750); err != nil {
return fmt.Errorf("cannot create .beads directory: %w", err)
}
// Create issues.jsonl if it doesn't exist
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
if _, err := os.Stat(jsonlPath); os.IsNotExist(err) {
// #nosec G306 -- planning repo JSONL must be shareable across collaborators
if err := os.WriteFile(jsonlPath, []byte{}, 0644); err != nil {
return fmt.Errorf("failed to create issues.jsonl: %w", err)
}
}
// Initialize database - it will be created when sqlite.New is called
// But we need to set the prefix if source store has one (T012: prefix inheritance)
if sourceStore != nil {
sourcePrefix, err := sourceStore.GetConfig(ctx, "issue_prefix")
if err == nil && sourcePrefix != "" {
// Open target store temporarily to set prefix
tempStore, err := sqlite.New(ctx, dbPath)
if err != nil {
return fmt.Errorf("failed to initialize target database: %w", err)
}
if err := tempStore.SetConfig(ctx, "issue_prefix", sourcePrefix); err != nil {
_ = tempStore.Close()
return fmt.Errorf("failed to set prefix in target store: %w", err)
}
if err := tempStore.Close(); err != nil {
return fmt.Errorf("failed to close target store: %w", err)
}
}
}
return nil
}