fix(sync): handle redirect + sync-branch incompatibility (bd-wayc3)

When a crew worker's .beads/ is redirected to another repo, bd sync
now detects this and skips all git operations (sync-branch worktree
manipulation). Instead, it just exports to JSONL and lets the target
repo's owner handle the git sync.

Changes:
- sync.go: Detect redirect early, skip git operations when active
- beads.go: Update GetRedirectInfo() to check git repo even when
  BEADS_DIR is pre-set (findLocalBdsDirInRepo helper)
- validation.go: Add doctor check for redirect + sync-branch conflict
- doctor.go: Register new check, remove undefined CheckMisclassifiedWisps

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Executed-By: beads/crew/dave
Rig: beads
Role: crew
This commit is contained in:
beads/crew/dave
2026-01-14 20:36:30 -08:00
committed by Steve Yegge
parent ff67b88ea9
commit 3298c45e4b
5 changed files with 1416 additions and 1281 deletions

View File

@@ -465,7 +465,12 @@ func runDiagnostics(path string) doctorResult {
result.Checks = append(result.Checks, vestigialWorktreesCheck)
// Don't fail overall check for vestigial worktrees, just warn
// Check 14f: last-touched file tracking (runtime state shouldn't be committed)
// Check 14f: redirect + sync-branch conflict (bd-wayc3)
redirectSyncBranchCheck := convertDoctorCheck(doctor.CheckRedirectSyncBranchConflict(path))
result.Checks = append(result.Checks, redirectSyncBranchCheck)
// Don't fail overall check for redirect+sync-branch conflict, just warn
// Check 14g: last-touched file tracking (runtime state shouldn't be committed)
lastTouchedTrackingCheck := convertWithCategory(doctor.CheckLastTouchedNotTracked(), doctor.CategoryGit)
result.Checks = append(result.Checks, lastTouchedTrackingCheck)
// Don't fail overall check for last-touched tracking, just warn
@@ -577,10 +582,9 @@ func runDiagnostics(path string) doctorResult {
result.Checks = append(result.Checks, staleMQFilesCheck)
// Don't fail overall check for legacy MQ files, just warn
// Check 26d: Misclassified wisps (wisp-patterned IDs without ephemeral flag)
misclassifiedWispsCheck := convertDoctorCheck(doctor.CheckMisclassifiedWisps(path))
result.Checks = append(result.Checks, misclassifiedWispsCheck)
// Don't fail overall check for misclassified wisps, just warn
// Note: Check 26d (misclassified wisps) was referenced but never implemented.
// The commit f703237c added importer-based auto-detection instead.
// Removing the undefined reference to fix build.
// Check 27: Expired tombstones (maintenance)
tombstonesExpiredCheck := convertDoctorCheck(doctor.CheckExpiredTombstones(path))

View File

@@ -391,6 +391,79 @@ func CheckChildParentDependencies(path string) DoctorCheck {
}
}
// CheckRedirectSyncBranchConflict detects when both redirect and sync-branch are configured.
// This is a configuration error: redirect means "my database is elsewhere (I'm a client)",
// while sync-branch means "I own my database and sync it myself". These are mutually exclusive.
// bd-wayc3: Added to detect incompatible configuration before sync fails.
func CheckRedirectSyncBranchConflict(path string) DoctorCheck {
beadsDir := filepath.Join(path, ".beads")
// Check if redirect file exists
redirectFile := filepath.Join(beadsDir, beads.RedirectFileName)
if _, err := os.Stat(redirectFile); os.IsNotExist(err) {
return DoctorCheck{
Name: "Redirect + Sync-Branch",
Status: StatusOK,
Message: "No redirect configured",
Category: CategoryData,
}
}
// Redirect exists - check if sync-branch is also configured
// Read config.yaml directly since we need to check the local config, not the resolved one
configPath := filepath.Join(beadsDir, "config.yaml")
data, err := os.ReadFile(configPath) // #nosec G304 - path constructed safely
if err != nil {
// No config file - no conflict possible
return DoctorCheck{
Name: "Redirect + Sync-Branch",
Status: StatusOK,
Message: "Redirect active (no local config)",
Category: CategoryData,
}
}
// Parse sync-branch from config.yaml (simple line-based parsing)
// Handles: sync-branch: value, sync-branch: "value", sync-branch: 'value'
// Also handles trailing comments: sync-branch: value # comment
configStr := string(data)
for _, line := range strings.Split(configStr, "\n") {
line = strings.TrimSpace(line)
// Skip comments
if strings.HasPrefix(line, "#") {
continue
}
if strings.HasPrefix(line, "sync-branch:") {
value := strings.TrimPrefix(line, "sync-branch:")
// Remove trailing comment if present
if idx := strings.Index(value, "#"); idx != -1 {
value = value[:idx]
}
value = strings.TrimSpace(value)
// Remove quotes if present
value = strings.Trim(value, `"'`)
if value != "" {
// Found both redirect and sync-branch - conflict!
return DoctorCheck{
Name: "Redirect + Sync-Branch",
Status: StatusWarning,
Message: fmt.Sprintf("Redirect active but sync-branch=%q configured", value),
Detail: "Redirect and sync-branch are mutually exclusive. Redirected clones should not have sync-branch.",
Fix: "Remove sync-branch from config.yaml (set to empty string or delete the line)",
Category: CategoryData,
}
}
}
}
return DoctorCheck{
Name: "Redirect + Sync-Branch",
Status: StatusOK,
Message: "Redirect active (no sync-branch conflict)",
Category: CategoryData,
}
}
// CheckGitConflicts detects git conflict markers in JSONL file.
func CheckGitConflicts(path string) DoctorCheck {
// Follow redirect to resolve actual beads directory (bd-tvus fix)

View File

@@ -208,6 +208,36 @@ Use --merge to merge the sync branch back to main branch.`,
}
hasSyncBranchConfig := syncBranchName != ""
// bd-wayc3: Check for redirect + sync-branch incompatibility
// Redirect and sync-branch are mutually exclusive:
// - Redirect says: "My database is in another repo (I am a client)"
// - Sync-branch says: "I own my database and sync it myself via worktree"
// When redirect is active, the sync-branch worktree operations fail because
// the beads files are in a different git repo than the current working directory.
redirectInfo := beads.GetRedirectInfo()
if redirectInfo.IsRedirected {
if hasSyncBranchConfig {
fmt.Printf("⚠️ Redirect active (-> %s), skipping sync-branch operations\n", redirectInfo.TargetDir)
fmt.Println(" Hint: Redirected clones should not have sync-branch configured")
fmt.Println(" The owner of the target .beads directory handles sync-branch")
} else {
fmt.Printf("→ Redirect active (-> %s)\n", redirectInfo.TargetDir)
}
// For redirected clones, just do import/export - skip all git operations
// The target repo's owner (e.g., mayor) handles git commit/push via sync-branch
if dryRun {
fmt.Println("→ [DRY RUN] Would export to JSONL (redirected clone, git operations skipped)")
fmt.Println("✓ Dry run complete (no changes made)")
} else {
fmt.Println("→ Exporting to JSONL (redirected clone, skipping git operations)...")
if err := exportToJSONL(ctx, jsonlPath); err != nil {
FatalError("exporting: %v", err)
}
fmt.Println("✓ Export complete (target repo owner handles git sync)")
}
return
}
// Preflight: check for upstream tracking
// If no upstream, automatically switch to --from-main mode (gt-ick9: ephemeral branch support)
// GH#638: Skip this fallback if sync.branch is explicitly configured