feat(sync): add per-field merge strategies for conflict resolution

Implements configurable per-field merge strategies (hq-ew1mbr.11):

- Add FieldStrategy type with strategies: newest, max, union, manual
- Add conflict.fields config section for per-field overrides
- compaction_level defaults to "max" (highest value wins)
- estimated_minutes defaults to "manual" (flags for user resolution)
- labels defaults to "union" (set merge)

Manual conflicts are displayed during sync with resolution options:
  bd sync --ours / --theirs, or bd resolve <id> <field> <value>

Config example:
  conflict:
    strategy: newest
    fields:
      compaction_level: max
      estimated_minutes: manual
      labels: union

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beads/crew/lydia
2026-01-21 19:39:20 -08:00
committed by Steve Yegge
parent e0dc3a37c3
commit 9a9704b451
9 changed files with 698 additions and 91 deletions

View File

@@ -572,6 +572,11 @@ func doPullFirstSync(ctx context.Context, jsonlPath string, renameOnImport, noGi
fmt.Printf(" Local wins: %d, Remote wins: %d, Same: %d, Conflicts (LWW): %d\n",
localCount, remoteCount, sameCount, mergeResult.Conflicts)
// Display manual conflicts that need user resolution
if len(mergeResult.ManualConflicts) > 0 {
displayManualConflicts(mergeResult.ManualConflicts)
}
// Step 6: Import merged state to DB
// First, write merged result to JSONL so import can read it
fmt.Println("→ Writing merged state to JSONL...")
@@ -1071,6 +1076,11 @@ func resolveSyncConflicts(ctx context.Context, jsonlPath string, strategy config
// Re-run merge with the resolved conflicts
mergeResult := MergeIssues(baseIssues, localIssues, remoteIssues)
// Display any remaining manual conflicts
if len(mergeResult.ManualConflicts) > 0 {
displayManualConflicts(mergeResult.ManualConflicts)
}
// Write merged state
if err := writeMergedStateToJSONL(jsonlPath, mergeResult.Merged); err != nil {
return fmt.Errorf("writing merged state: %w", err)
@@ -1177,7 +1187,7 @@ func resolveSyncConflictsManually(ctx context.Context, jsonlPath, beadsDir strin
local := localMap[id]
remote := remoteMap[id]
base := baseMap[id]
merged, _ := MergeIssue(base, local, remote)
merged, _, _ := MergeIssue(base, local, remote)
if merged != nil {
mergedIssues = append(mergedIssues, merged)
}