From 3ca11dc44059108f9a06879bef2436bda95d225f Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 23 Nov 2025 20:41:25 -0800 Subject: [PATCH 1/2] bd sync: 2025-11-23 20:41:25 --- .beads/beads.jsonl | 1 + 1 file changed, 1 insertion(+) diff --git a/.beads/beads.jsonl b/.beads/beads.jsonl index 61de89c6..bda8bfb1 100644 --- a/.beads/beads.jsonl +++ b/.beads/beads.jsonl @@ -395,6 +395,7 @@ {"id":"bd-cjxp","content_hash":"2a2c0aa49be01be64c5e0a6bd24ebd7b762846d31a06fd8e9360672fb476b879","title":"Bug P0","description":"","status":"closed","priority":0,"issue_type":"bug","assignee":"alice","created_at":"2025-11-07T19:00:22.536449-08:00","updated_at":"2025-11-07T22:07:17.345535-08:00","closed_at":"2025-11-07T21:55:09.429643-08:00","source_repo":"."} {"id":"bd-ckvw","content_hash":"a4b27c0e21e3ae0a1e8fb9f64913c286164ff6746c657d70bb7cbbdbf2e365c9","title":"Add schema compatibility probe to prevent silent migration failures","description":"Issue #262 revealed a serious bug: migrations may fail silently, causing UNIQUE constraint errors later.\n\nRoot cause:\n- sqlite.New() runs migrations once on open\n- checkVersionMismatch() prints 'database will be upgraded automatically' but only updates metadata\n- If migrations fail or daemon runs older version, queries expecting new columns fail with 'no such column'\n- Import logic misinterprets this as 'not found' and tries INSERT on existing ID\n- Result: UNIQUE constraint failed: issues.id\n\nFix strategy (minimal):\n1. Add schema probe in sqlite.New() after RunMigrations\n - SELECT all expected columns from all tables with LIMIT 0\n - If fails, retry RunMigrations and probe again\n - If still fails, return fatal error with clear message\n2. Fix checkVersionMismatch to not claim 'will upgrade' unless probe passes\n3. Only update bd_version after successful migration probe\n4. Add schema verification before import operations\n5. Map 'no such column' errors to clear actionable message\n\nRelated: #262","design":"Minimal path (now includes daemon gating):\n\n1. Schema probe in sqlite.New()\n - After RunMigrations, verify all expected columns exist\n - SELECT id, title, description, created_at, updated_at, closed_at, content_hash, external_ref, source_repo, compacted_at, compacted_at_commit FROM issues LIMIT 0\n - Also probe: dependencies, labels, events, dirty_issues, export_hashes, snapshots, child_counters\n - If probe fails: retry RunMigrations once, probe again\n - If still fails: return fatal error with missing columns/tables\n\n2. Fix checkVersionMismatch()\n - Don't claim 'will be upgraded automatically' unless probe verified\n - Only update bd_version after successful probe\n\n3. Better error surfacing\n - Wrap storage errors: if 'no such column/table', return ErrSchemaIncompatible\n - Actionable message: 'Database schema is incompatible. Run bd doctor to diagnose.'\n\n4. Add 'bd doctor' command\n - Runs migrations + probe\n - Reports missing columns/tables\n - Suggests fixes (upgrade daemon, run migrations manually, etc.)\n - Exit 1 if incompatible\n\n5. Daemon version gating (REQUIRED - prevents future schema bugs)\n - On RPC connect, client/daemon exchange semver\n - If client.minor \u003e daemon.minor: refuse RPC, print 'Client vX.Y requires daemon upgrade. Run: bd daemons killall'\n - Forces users to restart daemon when bd binary is upgraded\n - Prevents stale daemon serving requests with old schema assumptions\n - Already documented best practice, now enforced\n\nEstimated effort: M-L (3-5h with daemon gating + bd doctor)","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-11-08T13:23:26.934246-08:00","updated_at":"2025-11-08T13:53:29.219542-08:00","closed_at":"2025-11-08T13:53:29.219542-08:00","source_repo":"."} {"id":"bd-csvy","content_hash":"88e2ed15c2fe9d9622b16daa530907af7069ef69e621c74dc2a2fafa1da4ac8c","title":"Add tests for merge driver auto-config in bd init","description":"Add comprehensive tests for the merge driver auto-configuration functionality in `bd init`.\n\n**Test cases needed:**\n- Auto-install in quiet mode\n- Skip with --skip-merge-driver flag\n- Detect already-installed merge driver\n- Append to existing .gitattributes\n- Interactive prompt behavior (if feasible)\n\n**File:** `cmd/bd/init_test.go`","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-05T19:27:04.133078-08:00","updated_at":"2025-11-06T18:19:16.233673-08:00","closed_at":"2025-11-06T15:56:36.014814-08:00","source_repo":".","dependencies":[{"issue_id":"bd-csvy","depends_on_id":"bd-32nm","type":"discovered-from","created_at":"2025-11-05T19:27:04.134299-08:00","created_by":"daemon"}]} +{"id":"bd-cwmt","content_hash":"b7d4aace45558b96380aa3096bba54b26303eb5dfdb01b8cc7b4fadd12143561","title":"bd sync should auto-resolve JSONL merge conflicts by exporting from DB","description":"## Problem\n\nWhen bd sync encounters a merge conflict in beads.jsonl during git pull/rebase, it fails and tells the user to manually resolve it. The typical resolution is:\n\n```bash\nbd export -o .beads/beads.jsonl\ngit add .beads/beads.jsonl \ngit rebase --continue\nbd sync # try again\n```\n\nThis is tedious and error-prone. The user shouldn't need to know these git internals.\n\n## Root Cause\n\nbd sync doesn't handle the case where:\n1. Export succeeds → commit created\n2. Pull/rebase starts\n3. JSONL has merge conflict\n4. Process aborts, leaving user in broken rebase state\n\n## Proposed Solution\n\nWhen bd sync detects a merge conflict in beads.jsonl during pull/rebase:\n\n1. **Auto-detect conflict**: Check if git status shows 'both modified' on beads.jsonl\n2. **Auto-resolve**: Run `bd export -o .beads/beads.jsonl` to overwrite with DB state\n3. **Mark resolved**: Run `git add .beads/beads.jsonl`\n4. **Continue**: Run `git rebase --continue` automatically\n5. **Complete sync**: Continue with normal push flow\n\n## Implementation\n\nLocation: `cmd/bd/sync.go`\n\nAfter git pull fails:\n```go\nif err := runGitPull(); err != nil {\n // Check if it's a rebase conflict\n if isInRebase() \u0026\u0026 hasJSONLConflict() {\n log.Info(\"→ Auto-resolving JSONL merge conflict...\")\n \n // Export clean JSONL from DB\n if err := exportJSONL(); err != nil {\n return fmt.Errorf(\"failed to export for conflict resolution: %w\", err)\n }\n \n // Mark as resolved\n if err := runGitAdd(\".beads/beads.jsonl\"); err != nil {\n return fmt.Errorf(\"failed to mark conflict resolved: %w\", err)\n }\n \n // Continue rebase\n if err := runGitRebaseContinue(); err != nil {\n return fmt.Errorf(\"failed to continue rebase: %w\", err)\n }\n \n log.Info(\"✓ Auto-resolved JSONL conflict\")\n } else {\n return err // Other error, fail normally\n }\n}\n```\n\nHelper functions needed:\n- `isInRebase() bool` - check if .git/rebase-merge or .git/rebase-apply exists\n- `hasJSONLConflict() bool` - check git status for 'both modified' on beads.jsonl\n- `runGitRebaseContinue() error` - run git rebase --continue\n\n## Benefits\n\n- Seamless multi-user workflow\n- No manual git intervention needed \n- Database is always source of truth for local changes\n- Matches user mental model: 'bd sync should just work'\n\n## Edge Cases\n\n1. **Other files in conflict**: Only auto-resolve if ONLY beads.jsonl is conflicted\n2. **Export fails**: Fall back to manual resolution instructions\n3. **Multiple rebases**: Handle case where there are multiple commits being rebased\n4. **Nested conflicts**: If rebase --continue hits another conflict, handle it\n\n## Testing\n\n1. Two clones, both create issues\n2. Clone A syncs successfully \n3. Clone B syncs → should auto-resolve and complete\n4. Verify both clones have all issues\n5. Verify no merge conflict markers in JSONL\n\n## User Experience\n\nBefore:\n```\n$ bd sync\nError: merge conflict in beads.jsonl\n[manual steps required]\n```\n\nAfter:\n```\n$ bd sync\n→ Pulling from remote...\n→ Auto-resolving JSONL merge conflict...\n✓ Auto-resolved JSONL conflict \n→ Pushing to remote...\n✓ Sync complete\n```\n\n## Related Issues\n\nThis is the most common friction point in multi-user beads workflows.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-11-23T20:33:30.128334-08:00","updated_at":"2025-11-23T20:41:20.678447-08:00","closed_at":"2025-11-23T20:41:20.678447-08:00","source_repo":"."} {"id":"bd-d19a","content_hash":"5ff9ba5e70c3e3eeaff40887421797e30dfb75e56e97fcaaf3f3d32332f22aa2","title":"Fix import failure on missing parent issues","description":"Import process fails atomically when JSONL references deleted parent issues. Implement hybrid solution: topological sorting + parent resurrection to handle deleted parents gracefully while maintaining referential integrity. See docs/import-bug-analysis-bd-3xq.md for full analysis.","status":"closed","priority":0,"issue_type":"epic","created_at":"2025-11-04T12:31:30.994759-08:00","updated_at":"2025-11-05T00:08:42.814239-08:00","closed_at":"2025-11-05T00:08:42.814243-08:00","source_repo":"."} {"id":"bd-d33c","content_hash":"d0820d5dd6ea4ab198e013861d3d7d01da701daa8ab8ec59ad5ef855e6f83b2b","title":"Separate process/lock/PID concerns into process.go","description":"Create internal/daemonrunner/process.go with: acquireDaemonLock, PID file read/write, stopDaemon, isDaemonRunning, getPIDFilePath, socket path helpers, version check.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-01T11:41:14.871122-07:00","updated_at":"2025-11-01T23:43:55.66159-07:00","closed_at":"2025-11-01T23:43:55.66159-07:00","source_repo":"."} {"id":"bd-d355a07d","content_hash":"e5e88defa034e6758f63ac603963209245ab74f531510366b25ebbf7b4be36b3","title":"Import validation falsely reports data loss on collision resolution","description":"## Problem\n\nPost-import validation reports 'data loss detected!' when import count reduces due to legitimate collision resolution.\n\n## Example\n\n```\nImport complete: 1 created, 8 updated, 142 unchanged, 19 skipped, 1 issues remapped\nPost-import validation failed: import reduced issue count: 165 → 164 (data loss detected!)\n```\n\nThis was actually successful collision resolution (bd-70419816 duplicated → remapped to-70419816), not data loss.\n\n## Impact\n\n- False alarms waste investigation time\n- Undermines confidence in import validation\n- Confuses users/agents about sync health\n\n## Solution\n\nImprove validation to distinguish:\n- Collision-resolution merges (expected count reduction)\n- Actual data loss (unexpected disappearance)\n\nTrack remapped issue count and adjust expected post-import count accordingly.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-10-29T23:15:00.815227-07:00","updated_at":"2025-11-08T01:58:15.283088-08:00","closed_at":"2025-11-08T00:33:04.659308-08:00","source_repo":"."} From 3cf5e26d1ec29b10b3155df2a2999f8e8e67318b Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 23 Nov 2025 20:41:35 -0800 Subject: [PATCH 2/2] Implement auto-resolution of JSONL merge conflicts during bd sync - Add isInRebase() to detect rebase state - Add hasJSONLConflict() to check for JSONL-only conflicts - Add runGitRebaseContinue() to continue rebase after resolution - Auto-export from DB and resolve conflict when detected - Add comprehensive tests for auto-resolution logic Implements bd-cwmt --- cmd/bd/init.go | 1 - cmd/bd/sync.go | 110 ++++++++++++++++++++++++++--- cmd/bd/sync_test.go | 166 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 265 insertions(+), 12 deletions(-) diff --git a/cmd/bd/init.go b/cmd/bd/init.go index 17fc65cd..ad2861fa 100644 --- a/cmd/bd/init.go +++ b/cmd/bd/init.go @@ -353,7 +353,6 @@ With --no-db: creates .beads/ directory and issues.jsonl file instead of SQLite green := color.New(color.FgGreen).SprintFunc() cyan := color.New(color.FgCyan).SprintFunc() - yellow := color.New(color.FgYellow).SprintFunc() fmt.Printf("\n%s bd initialized successfully!\n\n", green("✓")) fmt.Printf(" Database: %s\n", cyan(initDBPath)) diff --git a/cmd/bd/sync.go b/cmd/bd/sync.go index 0d5f8869..34eeeb46 100644 --- a/cmd/bd/sync.go +++ b/cmd/bd/sync.go @@ -204,19 +204,49 @@ Use --merge to merge the sync branch back to main branch.`, fmt.Println("→ Pulling from remote...") if err := gitPull(ctx); err != nil { - fmt.Fprintf(os.Stderr, "Error pulling: %v\n", err) + // Check if it's a rebase conflict on beads.jsonl that we can auto-resolve + if isInRebase() && hasJSONLConflict() { + fmt.Println("→ Auto-resolving JSONL merge conflict...") - // Check if this looks like a merge driver failure - errStr := err.Error() - if strings.Contains(errStr, "merge driver") || - strings.Contains(errStr, "no such file or directory") || - strings.Contains(errStr, "MERGE DRIVER INVOKED") { - fmt.Fprintf(os.Stderr, "\nThis may be caused by an incorrect merge driver configuration.\n") - fmt.Fprintf(os.Stderr, "Fix: bd doctor --fix\n\n") + // Export clean JSONL from DB (database is source of truth) + if exportErr := exportToJSONL(ctx, jsonlPath); exportErr != nil { + fmt.Fprintf(os.Stderr, "Error: failed to export for conflict resolution: %v\n", exportErr) + fmt.Fprintf(os.Stderr, "Hint: resolve conflicts manually and run 'bd import' then 'bd sync' again\n") + os.Exit(1) + } + + // Mark conflict as resolved + addCmd := exec.CommandContext(ctx, "git", "add", jsonlPath) + if addErr := addCmd.Run(); addErr != nil { + fmt.Fprintf(os.Stderr, "Error: failed to mark conflict resolved: %v\n", addErr) + fmt.Fprintf(os.Stderr, "Hint: resolve conflicts manually and run 'bd import' then 'bd sync' again\n") + os.Exit(1) + } + + // Continue rebase + if continueErr := runGitRebaseContinue(ctx); continueErr != nil { + fmt.Fprintf(os.Stderr, "Error: failed to continue rebase: %v\n", continueErr) + fmt.Fprintf(os.Stderr, "Hint: resolve conflicts manually and run 'bd import' then 'bd sync' again\n") + os.Exit(1) + } + + fmt.Println("✓ Auto-resolved JSONL conflict") + } else { + // Not an auto-resolvable conflict, fail with original error + fmt.Fprintf(os.Stderr, "Error pulling: %v\n", err) + + // Check if this looks like a merge driver failure + errStr := err.Error() + if strings.Contains(errStr, "merge driver") || + strings.Contains(errStr, "no such file or directory") || + strings.Contains(errStr, "MERGE DRIVER INVOKED") { + fmt.Fprintf(os.Stderr, "\nThis may be caused by an incorrect merge driver configuration.\n") + fmt.Fprintf(os.Stderr, "Fix: bd doctor --fix\n\n") + } + + fmt.Fprintf(os.Stderr, "Hint: resolve conflicts manually and run 'bd import' then 'bd sync' again\n") + os.Exit(1) } - - fmt.Fprintf(os.Stderr, "Hint: resolve conflicts manually and run 'bd import' then 'bd sync' again\n") - os.Exit(1) } // Count issues before import for validation @@ -439,6 +469,64 @@ func hasGitRemote(ctx context.Context) bool { return len(strings.TrimSpace(string(output))) > 0 } +// isInRebase checks if we're currently in a git rebase state +func isInRebase() bool { + // Check for rebase-merge directory (interactive rebase) + if _, err := os.Stat(".git/rebase-merge"); err == nil { + return true + } + // Check for rebase-apply directory (non-interactive rebase) + if _, err := os.Stat(".git/rebase-apply"); err == nil { + return true + } + return false +} + +// hasJSONLConflict checks if beads.jsonl has a merge conflict +// Returns true only if beads.jsonl is the only file in conflict +func hasJSONLConflict() bool { + cmd := exec.Command("git", "status", "--porcelain") + out, err := cmd.Output() + if err != nil { + return false + } + + var hasJSONLConflict bool + var hasOtherConflict bool + + for _, line := range strings.Split(string(out), "\n") { + if len(line) < 3 { + continue + } + + // Check for unmerged status codes (UU = both modified, AA = both added, etc.) + status := line[:2] + if status == "UU" || status == "AA" || status == "DD" || + status == "AU" || status == "UA" || status == "DU" || status == "UD" { + filepath := strings.TrimSpace(line[3:]) + + if strings.HasSuffix(filepath, "beads.jsonl") { + hasJSONLConflict = true + } else { + hasOtherConflict = true + } + } + } + + // Only return true if ONLY beads.jsonl has a conflict + return hasJSONLConflict && !hasOtherConflict +} + +// runGitRebaseContinue continues a rebase after resolving conflicts +func runGitRebaseContinue(ctx context.Context) error { + cmd := exec.CommandContext(ctx, "git", "rebase", "--continue") + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("git rebase --continue failed: %w\n%s", err, output) + } + return nil +} + // gitPull pulls from the current branch's upstream // Returns nil if no remote configured (local-only mode) func checkMergeDriverConfig() { diff --git a/cmd/bd/sync_test.go b/cmd/bd/sync_test.go index 36bc1844..bf4ff212 100644 --- a/cmd/bd/sync_test.go +++ b/cmd/bd/sync_test.go @@ -441,3 +441,169 @@ func TestGetSyncBranch_EnvOverridesDB(t *testing.T) { t.Errorf("getSyncBranch() = %q, want %q (env override)", branch, "env-branch") } } + +func TestIsInRebase_NotInRebase(t *testing.T) { + tmpDir := t.TempDir() + originalWd, _ := os.Getwd() + defer os.Chdir(originalWd) + + // Create a git repo + os.Chdir(tmpDir) + exec.Command("git", "init").Run() + exec.Command("git", "config", "user.email", "test@test.com").Run() + exec.Command("git", "config", "user.name", "Test User").Run() + + // Create initial commit + os.WriteFile("test.txt", []byte("test"), 0644) + exec.Command("git", "add", "test.txt").Run() + exec.Command("git", "commit", "-m", "initial").Run() + + // Should not be in rebase + if isInRebase() { + t.Error("expected false when not in rebase") + } +} + +func TestIsInRebase_InRebase(t *testing.T) { + tmpDir := t.TempDir() + originalWd, _ := os.Getwd() + defer os.Chdir(originalWd) + + // Create a git repo + os.Chdir(tmpDir) + exec.Command("git", "init").Run() + exec.Command("git", "config", "user.email", "test@test.com").Run() + exec.Command("git", "config", "user.name", "Test User").Run() + + // Create initial commit + os.WriteFile("test.txt", []byte("test"), 0644) + exec.Command("git", "add", "test.txt").Run() + exec.Command("git", "commit", "-m", "initial").Run() + + // Simulate rebase by creating rebase-merge directory + os.MkdirAll(filepath.Join(tmpDir, ".git", "rebase-merge"), 0755) + + // Should detect rebase + if !isInRebase() { + t.Error("expected true when .git/rebase-merge exists") + } +} + +func TestIsInRebase_InRebaseApply(t *testing.T) { + tmpDir := t.TempDir() + originalWd, _ := os.Getwd() + defer os.Chdir(originalWd) + + // Create a git repo + os.Chdir(tmpDir) + exec.Command("git", "init").Run() + + // Simulate non-interactive rebase by creating rebase-apply directory + os.MkdirAll(filepath.Join(tmpDir, ".git", "rebase-apply"), 0755) + + // Should detect rebase + if !isInRebase() { + t.Error("expected true when .git/rebase-apply exists") + } +} + +func TestHasJSONLConflict_NoConflict(t *testing.T) { + tmpDir := t.TempDir() + originalWd, _ := os.Getwd() + defer os.Chdir(originalWd) + + // Create a git repo + os.Chdir(tmpDir) + exec.Command("git", "init").Run() + exec.Command("git", "config", "user.email", "test@test.com").Run() + exec.Command("git", "config", "user.name", "Test User").Run() + + // Create initial commit + os.WriteFile("test.txt", []byte("test"), 0644) + exec.Command("git", "add", "test.txt").Run() + exec.Command("git", "commit", "-m", "initial").Run() + + // Should not have JSONL conflict + if hasJSONLConflict() { + t.Error("expected false when no conflicts") + } +} + +func TestHasJSONLConflict_OnlyJSONLConflict(t *testing.T) { + tmpDir := t.TempDir() + originalWd, _ := os.Getwd() + defer os.Chdir(originalWd) + + // Create a git repo + os.Chdir(tmpDir) + exec.Command("git", "init", "-b", "main").Run() + exec.Command("git", "config", "user.email", "test@test.com").Run() + exec.Command("git", "config", "user.name", "Test User").Run() + + // Create initial commit + beadsDir := filepath.Join(tmpDir, ".beads") + os.MkdirAll(beadsDir, 0755) + os.WriteFile(filepath.Join(beadsDir, "beads.jsonl"), []byte(`{"id":"bd-1","title":"original"}`), 0644) + exec.Command("git", "add", ".").Run() + exec.Command("git", "commit", "-m", "initial").Run() + + // Create a second commit on main (modify same issue) + os.WriteFile(filepath.Join(beadsDir, "beads.jsonl"), []byte(`{"id":"bd-1","title":"main-version"}`), 0644) + exec.Command("git", "add", ".").Run() + exec.Command("git", "commit", "-m", "main change").Run() + + // Create a branch from the first commit + exec.Command("git", "checkout", "-b", "feature", "HEAD~1").Run() + os.WriteFile(filepath.Join(beadsDir, "beads.jsonl"), []byte(`{"id":"bd-1","title":"feature-version"}`), 0644) + exec.Command("git", "add", ".").Run() + exec.Command("git", "commit", "-m", "feature change").Run() + + // Attempt rebase onto main (will conflict) + exec.Command("git", "rebase", "main").Run() + + // Should detect JSONL conflict during rebase + if !hasJSONLConflict() { + t.Error("expected true when only beads.jsonl has conflict during rebase") + } +} + +func TestHasJSONLConflict_MultipleConflicts(t *testing.T) { + tmpDir := t.TempDir() + originalWd, _ := os.Getwd() + defer os.Chdir(originalWd) + + // Create a git repo + os.Chdir(tmpDir) + exec.Command("git", "init", "-b", "main").Run() + exec.Command("git", "config", "user.email", "test@test.com").Run() + exec.Command("git", "config", "user.name", "Test User").Run() + + // Create initial commit with beads.jsonl and another file + beadsDir := filepath.Join(tmpDir, ".beads") + os.MkdirAll(beadsDir, 0755) + os.WriteFile(filepath.Join(beadsDir, "beads.jsonl"), []byte(`{"id":"bd-1","title":"original"}`), 0644) + os.WriteFile("other.txt", []byte("line1\nline2\nline3"), 0644) + exec.Command("git", "add", ".").Run() + exec.Command("git", "commit", "-m", "initial").Run() + + // Create a second commit on main (modify both files) + os.WriteFile(filepath.Join(beadsDir, "beads.jsonl"), []byte(`{"id":"bd-1","title":"main-version"}`), 0644) + os.WriteFile("other.txt", []byte("line1\nmain-version\nline3"), 0644) + exec.Command("git", "add", ".").Run() + exec.Command("git", "commit", "-m", "main change").Run() + + // Create a branch from the first commit + exec.Command("git", "checkout", "-b", "feature", "HEAD~1").Run() + os.WriteFile(filepath.Join(beadsDir, "beads.jsonl"), []byte(`{"id":"bd-1","title":"feature-version"}`), 0644) + os.WriteFile("other.txt", []byte("line1\nfeature-version\nline3"), 0644) + exec.Command("git", "add", ".").Run() + exec.Command("git", "commit", "-m", "feature change").Run() + + // Attempt rebase (will conflict on both files) + exec.Command("git", "rebase", "main").Run() + + // Should NOT auto-resolve when multiple files conflict + if hasJSONLConflict() { + t.Error("expected false when multiple files have conflicts (should not auto-resolve)") + } +}