docs/CONTRIBUTOR_NAMESPACE_ISOLATION.md
Issue: bd-umbf Status: Design Complete Author: beads/agents/onyx Created: 2025-12-30
When contributors work on beads-the-project using beads-the-tool, their personal
work-tracking issues can leak into PRs. The .beads/ directory contains the project's
canonical issue database, but contributors' local issues can pollute the diff.
This is a recursion problem unique to self-hosting projects.
beads-the-project/
├── .beads/
│ └── dolt/ ← Project bugs, features, tasks (SHOULD be in PRs)
└── src/
└── ...
contributor-working-on-beads/
├── .beads/
│ └── dolt/ ← Project issues PLUS personal tracking (POLLUTES PRs)
└── src/
└── ...
When a contributor:
bd create "My TODO: fix tests before lunch" to track their workThe PR diff includes their personal issues in the beads database.
Each contributor gets a private prefix (e.g., bd-steve-xxxx) that's gitignored.
Pros:
Cons:
.gitignore entries per contributorVerdict: Too fragile for a zero-friction solution.
Contributors use BEADS_DIR pointing elsewhere for personal tracking.
Pros:
Cons:
Verdict: Viable but requires explicit setup.
Mark issues as "local-only" vs "project" with a flag.
Pros:
Cons:
Verdict: Adds complexity without solving the core problem.
Automatically detect if user is maintainer or contributor and route new issues accordingly:
./.beads/ (project database)~/.beads-planning/ (personal database)Pros:
Cons:
Verdict: Best balance of automation and isolation.
Role Detection (internal/routing/routing.go):
func DetectUserRole(repoPath string) (UserRole, error)
git config beads.role for explicit overrideRouting Configuration (internal/config/config.go):
v.SetDefault("routing.mode", "") // Empty = disabled by default
v.SetDefault("routing.default", ".")
v.SetDefault("routing.contributor", "~/.beads-planning")
Target Repo Calculation (internal/routing/routing.go):
func DetermineTargetRepo(config *RoutingConfig, userRole UserRole, repoPath string)
Contributor Setup Wizard (cmd/bd/init_contributor.go):
bd init --contributor
Creates ~/.beads-planning/ and configures routing.
Documentation:
docs/ROUTING.md - Auto-routing mechanicsdocs/MULTI_REPO_MIGRATION.md - Contributor workflow guideActual Routing in bd create (bd-6x6g):
// cmd/bd/create.go:181
// TODO(bd-6x6g): Switch to target repo for multi-repo support
// For now, we just log the target repo in debug mode
if repoPath != "." {
debug.Logf("DEBUG: Target repo: %s\n", repoPath)
}
The routing is calculated but NOT used. Issues still go to ./.beads/.
Pollution Detection for Preflight (bd-lfak): No way to detect if personal issues are in the PR diff.
First-Time Contributor Warning:
No prompt when a contributor first runs bd create without setup.
Make bd create actually route to the target repo:
// In cmd/bd/create.go, after DetermineTargetRepo()
if repoPath != "." {
// Switch store to target repo
targetBeadsDir := expandPath(repoPath)
if err := ensureBeadsDir(targetBeadsDir); err != nil {
return fmt.Errorf("failed to initialize target repo: %w", err)
}
store, err = storage.OpenStore(filepath.Join(targetBeadsDir, "beads.db"))
if err != nil {
return fmt.Errorf("failed to open target store: %w", err)
}
// Continue with issue creation in target store
}
When a contributor runs bd create without routing configured:
→ Detected fork/contributor setup
→ Personal issues would pollute upstream PRs
Options:
1. Configure auto-routing (recommended)
Creates ~/.beads-planning for personal tracking
2. Continue to current repo
Issue will appear in the project database (affects PRs)
Choice [1]:
Add check in bd preflight --check:
func checkBeadsPollution(ctx context.Context) (CheckResult, error) {
// Get git diff of .beads/issues.jsonl
diff, err := gitDiff(".beads/issues.jsonl")
if err != nil {
return CheckResult{}, err
}
// Parse added issues from diff
addedIssues := parseAddedIssues(diff)
// Check if any added issues have source_repo != "."
// OR were created by current user (heuristic)
for _, issue := range addedIssues {
if issue.SourceRepo != "." {
// Definite pollution - issue was routed elsewhere but leaked
return CheckResult{
Status: Fail,
Message: fmt.Sprintf("Personal issue %s in diff", issue.ID),
}, nil
}
// Heuristic: check created_by against git author
if issue.CreatedBy != "" && !isProjectMaintainer(issue.CreatedBy) {
return CheckResult{
Status: Warn,
Message: fmt.Sprintf("Issue %s may be personal (created by %s)",
issue.ID, issue.CreatedBy),
}, nil
}
}
return CheckResult{Status: Pass}, nil
}
Allow promoting a personal issue to a project issue:
# Move from personal to project database
bd migrate plan-42 --to . --dry-run
bd migrate plan-42 --to .
This creates a new issue in the target repo with a reference to the original.
Contributor routing works independently of the project repo's sync configuration. The planning repo has its own sync behavior:
| Sync Mode | Project Repo | Planning Repo | Notes |
|---|---|---|---|
| Direct | Uses .beads/ directly | Uses ~/.beads-planning/.beads/ | Both use direct storage, no interaction |
| Sync-branch | Uses separate branch for beads | Uses direct storage | Planning repo does NOT inherit sync.branch config |
| No-db mode | Lightweight operations | Routes operations to planning repo | Planning repo still uses database |
| Server mode | Background Dolt server | Server bypassed for routed issues | Planning repo operations are synchronous |
| Local-only | No git remote | Works normally | Planning repo can have its own git remote independently |
| External (BEADS_DIR) | Uses separate repo via env var | BEADS_DIR takes precedence over routing | If BEADS_DIR is set, routing config is ignored |
.beads/ directorysync.branch, no-db, or server mode settingsBEADS_DIR environment variable is set, it overrides routing configuration# One-time setup
bd init --contributor
# This configures:
# - Creates ~/.beads-planning/ with its own database
# - Sets routing.mode=auto
# - Sets routing.contributor=~/.beads-planning
# Verify
bd config get routing.mode # → auto
bd config get routing.contributor # → ~/.beads-planning
# Force maintainer mode (for CI or shared machines)
git config beads.role maintainer
# Force contributor mode
git config beads.role contributor
# Per-command override
BEADS_DIR=~/.beads-planning bd create "My task" -p 1
# Or per-shell session
export BEADS_DIR=~/.beads-planning
bd create "My task" -p 1
Note: bd init and bd doctor also respect BEADS_DIR:
# Initialize directly at BEADS_DIR location (no need to cd)
mkdir -p ~/.beads-planning/.beads
export BEADS_DIR=~/.beads-planning/.beads
bd init --prefix planning # Creates database at $BEADS_DIR
# Doctor checks BEADS_DIR location (not CWD)
bd doctor # Diagnoses database at $BEADS_DIR
Symptom: Issues appear in the current repo's database instead of planning repo
Diagnosis:
# Check routing configuration
bd config get routing.mode
bd config get routing.contributor
# Check detected role
git config beads.role # If set, this overrides auto-detection
git remote get-url --push origin # Should show HTTPS for contributors
Solutions:
routing.mode is set to autorouting.contributor points to planning repo pathBEADS_DIR is NOT set (it overrides routing)git config beads.role contributorSymptom: Warning message about BEADS_DIR overriding routing config
Explanation: BEADS_DIR environment variable takes precedence over all routing configuration. This is intentional for backward compatibility.
Solutions:
unset BEADS_DIRbd create "task" -p 1 --repo /path/to/repoSymptom: Error when creating issue: "failed to initialize target repo"
Diagnosis:
ls -la ~/.beads-planning/.beads/ # Should exist
Solution:
# Reinitialize planning repo
bd init --contributor # Wizard will recreate if missing
Symptom: Planning repo issues have different prefix than expected
Explanation: Planning repo inherits the project repo's prefix during initialization. If you want a different prefix:
Solution:
# Configure planning repo prefix
cd ~/.beads-planning
bd config set db.prefix plan # Use "plan-" prefix for planning issues
cd - # Return to project repo
Symptom: Old docs or scripts reference contributor.auto_route or contributor.planning_repo
Explanation: Config keys were renamed in v0.48.0:
contributor.auto_route → routing.mode (value: auto or explicit)contributor.planning_repo → routing.contributorSolution: Use new keys. Legacy keys still work for backward compatibility but are deprecated.
# Old (deprecated but still works)
bd config set contributor.auto_route true
bd config set contributor.planning_repo ~/.beads-planning
# New (preferred)
bd config set routing.mode auto
bd config set routing.contributor ~/.beads-planning
For bd preflight, we can detect pollution by checking:
source_repo != "." but is in ./.beads/created_by doesn't match known maintainersSome issues ARE meant to be in PRs:
Use --type to distinguish:
--type=task or --type=feature from contributor → likely personal--type=bug discovered during work → may be legitimate project issueThis design enables:
bd createbd preflight can detect and warn about pollutionShould we warn on first bd create without setup?
Should personal database be auto-created?
How to handle CI environments?
beads.role=maintainer config or skip routing in CI1. Check git config beads.role
- If "maintainer" → Maintainer
- If "contributor" → Contributor
2. Get push URL: git remote get-url --push origin
- If starts with git@ or ssh:// → Maintainer (SSH access implies write)
- If contains @ (credentials) → Maintainer
- If HTTPS without credentials → Contributor
3. Default → Contributor (safe fallback)
This algorithm prioritizes safety: when in doubt, route to personal database to avoid accidental pollution.