From 80ec8094739c8f106882a8eea855d4b6d91f5ba3 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 29 Nov 2025 23:22:52 -0800 Subject: [PATCH] feat: configure sync.remote for contributor fork workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When bd init --contributor detects a fork setup (upstream remote exists), it now configures sync.remote = upstream. This ensures bd sync pulls beads from the source repo (upstream/main) rather than the fork's potentially outdated origin/main. Changes: - Add sync.remote config in contributor wizard when fork detected - Modify doSyncFromMain() to use configured sync.remote - Add getDefaultBranchForRemote() to support any remote name - Verify configured remote exists before fetching Fixes bd-bx9 šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .beads/issues.jsonl | 2 +- cmd/bd/init_contributor.go | 10 +++++++ cmd/bd/sync.go | 59 ++++++++++++++++++++++++++------------ 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 6667f0cb..737da846 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -30,7 +30,7 @@ {"id":"bd-bhd","title":"Git history fallback assumes .beads is direct child of repo root","description":"## Problem\n\n`checkGitHistoryForDeletions` assumes the repo structure:\n\n```go\nrepoRoot := filepath.Dir(beadsDir) // Assumes .beads is in repo root\njsonlPath := filepath.Join(\".beads\", \"beads.jsonl\")\n```\n\nBut `.beads` could be in a subdirectory (monorepo, nested project), and the actual JSONL filename could be different (configured via `metadata.json`).\n\n## Location\n`internal/importer/importer.go:865-869`\n\n## Impact\n- Git search will fail silently for repos with non-standard structure\n- Monorepo users won't get deletion propagation\n\n## Fix\n1. Use `git rev-parse --show-toplevel` to find actual repo root\n2. Compute relative path from repo root to JSONL\n3. Or use `git -C \u003cdir\u003e` to run from beadsDir directly","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-11-25T12:51:03.46856-08:00","updated_at":"2025-11-25T15:05:40.754716-08:00","closed_at":"2025-11-25T15:05:40.754716-08:00"} {"id":"bd-bok","title":"bd doctor --fix needs non-interactive mode (-y/--yes flag)","description":"When running `bd doctor --fix` in non-interactive mode (scripts, CI, Claude Code), it prompts 'Continue? (Y/n):' and fails with EOF.\n\n**Expected**: A `-y` or `--yes` flag to auto-confirm fixes.\n\n**Workaround**: Currently have to run `bd init` instead, but that's not discoverable from the doctor output.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-11-27T20:21:10.290649-08:00","updated_at":"2025-11-28T22:17:12.607642-08:00","closed_at":"2025-11-28T21:56:14.708313-08:00"} {"id":"bd-bt6y","title":"Improve compact/daemon/merge documentation and UX","description":"Multiple documentation and UX issues encountered:\n1. \"bd compact --analyze\" fails with misleading \"requires SQLite storage\" error when daemon is running. Needs --no-daemon or better error.\n2. \"bd merge\" help text is outdated (refers to 3-way merge instead of issue merging).\n3. Daemon mode purpose isn't clear to local-only users.\n4. Compact/cleanup commands are hard to discover.\n\nProposed fixes:\n- Fix compact+daemon interaction or error message.\n- Update \"bd merge\" help text.\n- Add \"when to use daemon\" section to docs.\n- Add maintenance section to quickstart.\n","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-20T18:55:43.637047-05:00","updated_at":"2025-11-29T22:06:06.330457-08:00","closed_at":"2025-11-28T23:10:43.884784-08:00"} -{"id":"bd-bx9","title":"bd init --contributor should configure sync.remote=upstream for fork workflows","description":"When running `bd init --contributor` in a fork workflow (where `upstream` remote points to the original repo), the wizard should configure beads to sync from `upstream/main` rather than `origin/main`.\n\n**Current behavior:**\n- Contributor mode detects the fork setup (upstream remote exists)\n- Sets up planning repo and auto-routing\n- Does NOT configure sync remote\n- `bd sync` on feature branches shows \"No upstream configured, using --from-main mode\" and syncs from `origin/main`\n\n**Expected behavior:**\n- Contributor mode should also set `sync.remote = upstream` (or similar config)\n- `bd sync` should pull beads from `upstream/main` (source of truth)\n\n**Why this matters:**\n- The fork's `origin/main` may be behind `upstream/main`\n- Contributors want the latest issues from the source repo\n- Code PRs go: local -\u003e origin -\u003e upstream, but beads should come FROM upstream\n\n**Suggested fix:**\nAdd to `runContributorWizard()` after detecting fork:\n```go\nif isFork {\n store.SetConfig(ctx, \"sync.remote\", \"upstream\")\n}\n```","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-29T00:39:05.137488727-05:00","updated_at":"2025-11-29T00:39:05.137488727-05:00","labels":["contributor","sync"]} +{"id":"bd-bx9","title":"bd init --contributor should configure sync.remote=upstream for fork workflows","description":"When running `bd init --contributor` in a fork workflow (where `upstream` remote points to the original repo), the wizard should configure beads to sync from `upstream/main` rather than `origin/main`.\n\n**Current behavior:**\n- Contributor mode detects the fork setup (upstream remote exists)\n- Sets up planning repo and auto-routing\n- Does NOT configure sync remote\n- `bd sync` on feature branches shows \"No upstream configured, using --from-main mode\" and syncs from `origin/main`\n\n**Expected behavior:**\n- Contributor mode should also set `sync.remote = upstream` (or similar config)\n- `bd sync` should pull beads from `upstream/main` (source of truth)\n\n**Why this matters:**\n- The fork's `origin/main` may be behind `upstream/main`\n- Contributors want the latest issues from the source repo\n- Code PRs go: local -\u003e origin -\u003e upstream, but beads should come FROM upstream\n\n**Suggested fix:**\nAdd to `runContributorWizard()` after detecting fork:\n```go\nif isFork {\n store.SetConfig(ctx, \"sync.remote\", \"upstream\")\n}\n```","status":"in_progress","priority":2,"issue_type":"feature","created_at":"2025-11-29T00:39:05.137488727-05:00","updated_at":"2025-11-29T23:22:53.036271-08:00","labels":["contributor","sync"]} {"id":"bd-c362","title":"Extract database search logic into helper function","description":"The logic for finding a database in a beads directory is duplicated:\n- FindDatabasePath() BEADS_DIR section (beads.go:141-169)\n- findDatabaseInTree() (beads.go:248-280)\n\nBoth implement the same search order:\n1. Check config.json first (single source of truth)\n2. Fall back to canonical beads.db\n3. Search for *.db files, filtering backups and vc.db\n\nRefactoring suggestion:\nExtract to a helper function like:\n func findDatabaseInBeadsDir(beadsDir string) string\n\nBenefits:\n- Single source of truth for database search logic\n- Easier to maintain and update search order\n- Reduces code duplication\n\nRelated to [deleted:bd-e16b] implementation.","status":"closed","priority":3,"issue_type":"chore","created_at":"2025-11-02T18:34:02.831543-08:00","updated_at":"2025-11-25T22:27:33.794656-08:00","closed_at":"2025-11-25T22:27:33.794656-08:00"} {"id":"bd-c4rq","title":"Refactor: Move staleness check inside daemon branch","description":"## Problem\n\nCurrently ensureDatabaseFresh() is called before the daemon mode check, but it checks daemonClient != nil internally and returns early. This is redundant.\n\n**Location:** All read commands (list.go:196, show.go:27, ready.go:102, status.go:80, etc.)\n\n## Current Pattern\n\nCall happens before daemon check, function checks daemonClient internally.\n\n## Better Pattern\n\nMove staleness check to direct mode branch only, after daemon check.\n\n## Impact\nLow - minor performance improvement (avoids one function call per command in daemon mode)\n\n## Effort\nMedium - requires refactoring 8 command files\n\n## Priority\nLow - can defer to future cleanup PR","status":"closed","priority":3,"issue_type":"chore","created_at":"2025-11-20T20:17:45.119583-05:00","updated_at":"2025-11-29T22:06:06.330716-08:00","closed_at":"2025-11-28T23:37:52.276192-08:00"} {"id":"bd-c8x","title":"Don't search parent directories for .beads databases","description":"bd currently walks up the directory tree looking for .beads directories, which can find unrelated databases (e.g., ~/.beads). This causes confusing warnings and potential data pollution.\n\nShould either:\n1. Stop at git root (don't search above it)\n2. Only use explicit BEADS_DB env var or local .beads\n3. At minimum, don't search in home directory","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-11-27T22:10:41.992686-08:00","updated_at":"2025-11-28T22:17:12.607956-08:00","closed_at":"2025-11-28T22:15:55.878353-08:00"} diff --git a/cmd/bd/init_contributor.go b/cmd/bd/init_contributor.go index badd0639..7ad67589 100644 --- a/cmd/bd/init_contributor.go +++ b/cmd/bd/init_contributor.go @@ -179,6 +179,16 @@ Created by: bd init --contributor fmt.Printf("%s Auto-routing enabled\n", green("āœ“")) + // If this is a fork, configure sync to pull beads from upstream (bd-bx9) + // This ensures `bd sync` gets the latest issues from the source repo, + // not from the fork's potentially outdated origin/main + if isFork { + if err := store.SetConfig(ctx, "sync.remote", "upstream"); err != nil { + return fmt.Errorf("failed to set sync remote: %w", err) + } + fmt.Printf("%s Sync configured to pull from upstream (source repo)\n", green("āœ“")) + } + // Step 5: Summary fmt.Printf("\n%s %s\n\n", green("āœ“"), bold("Contributor setup complete!")) diff --git a/cmd/bd/sync.go b/cmd/bd/sync.go index 4350fbef..607be90d 100644 --- a/cmd/bd/sync.go +++ b/cmd/bd/sync.go @@ -853,27 +853,34 @@ func gitPush(ctx context.Context) error { return nil } -// getDefaultBranch returns the default branch name (main or master) +// getDefaultBranch returns the default branch name (main or master) for origin remote // Checks remote HEAD first, then falls back to checking if main/master exist func getDefaultBranch(ctx context.Context) string { + return getDefaultBranchForRemote(ctx, "origin") +} + +// getDefaultBranchForRemote returns the default branch name for a specific remote +// Checks remote HEAD first, then falls back to checking if main/master exist +func getDefaultBranchForRemote(ctx context.Context, remote string) string { // Try to get default branch from remote - cmd := exec.CommandContext(ctx, "git", "symbolic-ref", "refs/remotes/origin/HEAD") + cmd := exec.CommandContext(ctx, "git", "symbolic-ref", fmt.Sprintf("refs/remotes/%s/HEAD", remote)) output, err := cmd.Output() if err == nil { ref := strings.TrimSpace(string(output)) - // Extract branch name from refs/remotes/origin/main - if strings.HasPrefix(ref, "refs/remotes/origin/") { - return strings.TrimPrefix(ref, "refs/remotes/origin/") + // Extract branch name from refs/remotes//main + prefix := fmt.Sprintf("refs/remotes/%s/", remote) + if strings.HasPrefix(ref, prefix) { + return strings.TrimPrefix(ref, prefix) } } - // Fallback: check if origin/main exists - if exec.CommandContext(ctx, "git", "rev-parse", "--verify", "origin/main").Run() == nil { + // Fallback: check if /main exists + if exec.CommandContext(ctx, "git", "rev-parse", "--verify", fmt.Sprintf("%s/main", remote)).Run() == nil { return "main" } - // Fallback: check if origin/master exists - if exec.CommandContext(ctx, "git", "rev-parse", "--verify", "origin/master").Run() == nil { + // Fallback: check if /master exists + if exec.CommandContext(ctx, "git", "rev-parse", "--verify", fmt.Sprintf("%s/master", remote)).Run() == nil { return "master" } @@ -884,11 +891,21 @@ func getDefaultBranch(ctx context.Context) string { // doSyncFromMain performs a one-way sync from the default branch (main/master) // Used for ephemeral branches without upstream tracking (gt-ick9) // This fetches beads from main and imports them, discarding local beads changes. +// If sync.remote is configured (e.g., "upstream" for fork workflows), uses that remote +// instead of "origin" (bd-bx9). func doSyncFromMain(ctx context.Context, jsonlPath string, renameOnImport bool, dryRun bool, noGitHistory bool) error { + // Determine which remote to use (default: origin, but can be configured via sync.remote) + remote := "origin" + if err := ensureStoreActive(); err == nil && store != nil { + if configuredRemote, err := store.GetConfig(ctx, "sync.remote"); err == nil && configuredRemote != "" { + remote = configuredRemote + } + } + if dryRun { fmt.Println("→ [DRY RUN] Would sync beads from main branch") - fmt.Println(" 1. Fetch origin main") - fmt.Println(" 2. Checkout .beads/ from origin/main") + fmt.Printf(" 1. Fetch %s main\n", remote) + fmt.Printf(" 2. Checkout .beads/ from %s/main\n", remote) fmt.Println(" 3. Import JSONL into database") fmt.Println("\nāœ“ Dry run complete (no changes made)") return nil @@ -904,20 +921,26 @@ func doSyncFromMain(ctx context.Context, jsonlPath string, renameOnImport bool, return fmt.Errorf("no git remote configured") } - defaultBranch := getDefaultBranch(ctx) + // Verify the configured remote exists + checkRemoteCmd := exec.CommandContext(ctx, "git", "remote", "get-url", remote) + if err := checkRemoteCmd.Run(); err != nil { + return fmt.Errorf("configured sync.remote '%s' does not exist (run 'git remote add %s ')", remote, remote) + } + + defaultBranch := getDefaultBranchForRemote(ctx, remote) // Step 1: Fetch from main - fmt.Printf("→ Fetching from origin/%s...\n", defaultBranch) - fetchCmd := exec.CommandContext(ctx, "git", "fetch", "origin", defaultBranch) + fmt.Printf("→ Fetching from %s/%s...\n", remote, defaultBranch) + fetchCmd := exec.CommandContext(ctx, "git", "fetch", remote, defaultBranch) if output, err := fetchCmd.CombinedOutput(); err != nil { - return fmt.Errorf("git fetch origin %s failed: %w\n%s", defaultBranch, err, output) + return fmt.Errorf("git fetch %s %s failed: %w\n%s", remote, defaultBranch, err, output) } // Step 2: Checkout .beads/ directory from main - fmt.Printf("→ Checking out beads from origin/%s...\n", defaultBranch) - checkoutCmd := exec.CommandContext(ctx, "git", "checkout", fmt.Sprintf("origin/%s", defaultBranch), "--", ".beads/") + fmt.Printf("→ Checking out beads from %s/%s...\n", remote, defaultBranch) + checkoutCmd := exec.CommandContext(ctx, "git", "checkout", fmt.Sprintf("%s/%s", remote, defaultBranch), "--", ".beads/") if output, err := checkoutCmd.CombinedOutput(); err != nil { - return fmt.Errorf("git checkout .beads/ from origin/%s failed: %w\n%s", defaultBranch, err, output) + return fmt.Errorf("git checkout .beads/ from %s/%s failed: %w\n%s", remote, defaultBranch, err, output) } // Step 3: Import JSONL