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 <noreply@anthropic.com>
This commit is contained in:
aleiby
2026-01-19 10:11:00 -08:00
committed by GitHub
parent 460d2bf113
commit f4ee7ee73b
2 changed files with 30 additions and 7 deletions

View File

@@ -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
}
}

View File

@@ -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.