Implement auto-routing for bd create (bd-ubu2)

- Add internal/routing package with DetectUserRole and DetermineTargetRepo
- Add routing config schema (mode, default, maintainer, contributor)
- Add --repo flag to bd create for explicit override
- Integrate routing logic into create command
- Test with contributor/maintainer roles and explicit override

Part of bd-8hf (Auto-routing and maintainer detection)
This commit is contained in:
Steve Yegge
2025-11-04 17:00:13 -08:00
parent 8bcb8a40f1
commit 58e915f22b
5 changed files with 233 additions and 0 deletions

100
internal/routing/routing.go Normal file
View File

@@ -0,0 +1,100 @@
package routing
import (
"os/exec"
"strings"
)
// UserRole represents whether the user is a maintainer or contributor
type UserRole string
const (
Maintainer UserRole = "maintainer"
Contributor UserRole = "contributor"
)
// DetectUserRole determines if the user is a maintainer or contributor
// based on git configuration and repository permissions.
//
// Detection strategy:
// 1. Check if user has push access to origin (git remote -v shows write URL)
// 2. Check git config for beads.role setting (explicit override)
// 3. Fall back to contributor if uncertain
func DetectUserRole(repoPath string) (UserRole, error) {
// First check for explicit role in git config
cmd := exec.Command("git", "config", "--get", "beads.role")
if repoPath != "" {
cmd.Dir = repoPath
}
output, err := cmd.Output()
if err == nil {
role := strings.TrimSpace(string(output))
if role == string(Maintainer) {
return Maintainer, nil
}
if role == string(Contributor) {
return Contributor, nil
}
}
// Check push access by examining remote URL
cmd = exec.Command("git", "remote", "get-url", "--push", "origin")
if repoPath != "" {
cmd.Dir = repoPath
}
output, err = cmd.Output()
if err != nil {
// No remote or error - default to contributor
return Contributor, nil
}
pushURL := strings.TrimSpace(string(output))
// Check if URL indicates write access
// SSH URLs (git@github.com:user/repo.git) typically indicate write access
// HTTPS with token/password also indicates write access
if strings.HasPrefix(pushURL, "git@") ||
strings.HasPrefix(pushURL, "ssh://") ||
strings.Contains(pushURL, "@") {
return Maintainer, nil
}
// HTTPS without credentials likely means read-only contributor
return Contributor, nil
}
// RoutingConfig defines routing rules for issues
type RoutingConfig struct {
Mode string // "auto" or "explicit"
DefaultRepo string // Default repo for new issues
MaintainerRepo string // Repo for maintainers (in auto mode)
ContributorRepo string // Repo for contributors (in auto mode)
ExplicitOverride string // Explicit --repo flag override
}
// DetermineTargetRepo determines which repo should receive a new issue
// based on routing configuration and user role
func DetermineTargetRepo(config *RoutingConfig, userRole UserRole, repoPath string) string {
// Explicit override takes precedence
if config.ExplicitOverride != "" {
return config.ExplicitOverride
}
// Auto mode: route based on user role
if config.Mode == "auto" {
if userRole == Maintainer && config.MaintainerRepo != "" {
return config.MaintainerRepo
}
if userRole == Contributor && config.ContributorRepo != "" {
return config.ContributorRepo
}
}
// Fall back to default repo
if config.DefaultRepo != "" {
return config.DefaultRepo
}
// No routing configured - use current repo
return "."
}