From f4ee7ee73b0224871478c2d598e677dc916d76b0 Mon Sep 17 00:00:00 2001 From: aleiby Date: Mon, 19 Jan 2026 10:11:00 -0800 Subject: [PATCH] fix(routing): default to Maintainer when no git remote exists (#1185) When no git remote is configured, DetectUserRole() now defaults to Maintainer instead of Contributor. This fixes issue routing for: 1. New personal projects (no remote configured yet) 2. Intentionally local-only repositories Previously, issues would silently route to ~/.beads-planning instead of the local .beads/ directory. Co-authored-by: Claude Opus 4.5 --- internal/routing/routing.go | 6 +++--- internal/routing/routing_test.go | 31 +++++++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/internal/routing/routing.go b/internal/routing/routing.go index 144b362c..990be7e7 100644 --- a/internal/routing/routing.go +++ b/internal/routing/routing.go @@ -29,7 +29,7 @@ const ( // 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 +// 3. Fall back to maintainer for local projects (no remote configured) func DetectUserRole(repoPath string) (UserRole, error) { // First check for explicit role in git config output, err := gitCommandRunner(repoPath, "config", "--get", "beads.role") @@ -49,8 +49,8 @@ func DetectUserRole(repoPath string) (UserRole, error) { // Fallback to standard fetch URL if push URL fails (some git versions/configs) output, err = gitCommandRunner(repoPath, "remote", "get-url", "origin") if err != nil { - // No remote or error - default to contributor - return Contributor, nil + // No remote means local project - default to maintainer + return Maintainer, nil } } diff --git a/internal/routing/routing_test.go b/internal/routing/routing_test.go index fdcbf02b..d2afcfae 100644 --- a/internal/routing/routing_test.go +++ b/internal/routing/routing_test.go @@ -83,13 +83,13 @@ func TestDetermineTargetRepo(t *testing.T) { } func TestDetectUserRole_Fallback(t *testing.T) { - // Test fallback behavior when git is not available + // Test fallback behavior when git is not available - local projects default to maintainer role, err := DetectUserRole("/nonexistent/path/that/does/not/exist") if err != nil { t.Fatalf("DetectUserRole() error = %v, want nil", err) } - if role != Contributor { - t.Errorf("DetectUserRole() = %v, want %v (fallback)", role, Contributor) + if role != Maintainer { + t.Errorf("DetectUserRole() = %v, want %v (local project fallback)", role, Maintainer) } } @@ -267,7 +267,7 @@ func TestDetectUserRole_HTTPSCredentialsMaintainer(t *testing.T) { } } -func TestDetectUserRole_DefaultContributor(t *testing.T) { +func TestDetectUserRole_HTTPSNoCredentialsContributor(t *testing.T) { orig := gitCommandRunner stub := &gitStub{t: t, responses: []gitResponse{ {expect: gitCall{"", []string{"config", "--get", "beads.role"}}, err: errors.New("missing")}, @@ -289,6 +289,29 @@ func TestDetectUserRole_DefaultContributor(t *testing.T) { } } +func TestDetectUserRole_NoRemoteMaintainer(t *testing.T) { + // When no git remote is configured, default to maintainer (local project) + orig := gitCommandRunner + stub := &gitStub{t: t, responses: []gitResponse{ + {expect: gitCall{"/local", []string{"config", "--get", "beads.role"}}, err: errors.New("missing")}, + {expect: gitCall{"/local", []string{"remote", "get-url", "--push", "origin"}}, err: errors.New("no remote")}, + {expect: gitCall{"/local", []string{"remote", "get-url", "origin"}}, err: errors.New("no remote")}, + }} + gitCommandRunner = stub.run + t.Cleanup(func() { + gitCommandRunner = orig + stub.verify() + }) + + role, err := DetectUserRole("/local") + if err != nil { + t.Fatalf("DetectUserRole error = %v", err) + } + if role != Maintainer { + t.Fatalf("expected %s for local project with no remote, got %s", Maintainer, role) + } +} + // TestFindTownRoutes_SymlinkedBeadsDir verifies that findTownRoutes correctly // handles symlinked .beads directories by using findTownRootFromCWD() instead of // walking up from the beadsDir path.