Skip to content

feat(#651): auto-recover BLOCKED-by-merge-conflict tasks via worktree rebase (Option A)#656

Merged
lwgray merged 1 commit into
developfrom
fix/651-recovery-from-merge-conflict
May 25, 2026
Merged

feat(#651): auto-recover BLOCKED-by-merge-conflict tasks via worktree rebase (Option A)#656
lwgray merged 1 commit into
developfrom
fix/651-recovery-from-merge-conflict

Conversation

@lwgray

@lwgray lwgray commented May 25, 2026

Copy link
Copy Markdown
Owner

Why

PR #653 marked tasks BLOCKED on merge failure but left them unrecoverable. verify-snake-8 (test71, 2026-05-25) hit one such conflict and the runner spawned 49 ephemeral agents before the 20-min stall watchdog gave up — roughly $25-50 of wasted spawn cost from one recoverable conflict. On a commercial customer's API key the wasted spend could be $200-500 per stall event.

This PR implements Option A from Larry's 2026-05-25 decision: auto-recovery via worktree rebase. The proper architectural prevention (#206 baton arbitration) is the next sprint of work; this is the safety net that ships fast.

Most conflicts in DAG-based parallel work are stale-base false conflicts. Rebase resolves these mechanically. Real content conflicts abort the rebase and leave the task BLOCKED.

What changed

_attempt_merge_recovery(task, state) in src/marcus_mcp/tools/task.py:

  1. Reads source_context.merge_conflict (set by PR fix(#651): mark task BLOCKED when worktree merge to main fails #653's helper)
  2. Resolves worktree path: <project_root>/../worktrees/<agent_id>
  3. git rebase main in the worktree
  4. On clean rebase: checkout main + defensive reset + git merge --ff-only <branch> + kanban → DONE + clear merge_conflict from source_context
  5. On rebase conflict: git rebase --abort to clean state; leave BLOCKED; return None

_sweep_blocked_merge_conflicts(state) iterates state.project_tasks and runs recovery on each BLOCKED+merge_conflict task.

Wired into request_next_task: when no optimal_task found, sweep fires before returning no_task_ready. If recovery unblocks downstream tasks, refresh state + re-select for the current agent. Eliminates the spawn-thrash cycle.

Bright-line check

Marcus rebasing the agent's branch onto main is environment shaping, not HOW dictation. Same primitive as git pull --rebase. Agent's code unchanged; Marcus just integrates cleanly against advanced main. Passes Invariant #2.

Test plan

Automated (green)

  • 5 new tests in tests/unit/marcus_mcp/test_merge_conflict_recovery.py using real tmp_path git repos
  • 408 unit tests pass across tests/unit/marcus_mcp + tests/unit/mcp
  • mypy src/marcus_mcp/tools/task.py — clean

End-to-end (reviewer to run)

  • Re-run a spec that triggers a conflict (verify-snake-8 setup). Expected: conflict happens, task briefly BLOCKED, next poll's sweep recovers via rebase, task → DONE, downstream unblocks, run completes. Spawn count <10, not 49.

Empirical motivation

From verify-snake-8 (test71) runner log:

[02:19:16] start
[03:14:52]   spawned agent_unicorn_1_41    ← spawn #41 chasing the blocked task
[03:15:27]   spawned agent_unicorn_2_42
... 8 more spawns ...
[03:19:15] STALL: no task-count change for 20 min — tearing down
[03:20:15] run finished — spawned 49 ephemeral agents

This PR eliminates that failure mode.

What this PR does NOT do

Related

🤖 Generated with Claude Code

PR #653 marked tasks BLOCKED on merge fail (closed kanban/filesystem
divergence) but left them unrecoverable.  verify-snake-8 (test71,
2026-05-25) hit one such conflict; the runner spawned 49 ephemeral
agents chasing the unclaimed downstream task before the 20-min
stall watchdog gave up — ~$25-50 wasted spawn cost.

Most "conflicts" in DAG-based parallel work are stale-base false
conflicts.  Rebase resolves these mechanically.  Real content
conflicts abort the rebase and leave the task BLOCKED.

New helpers in src/marcus_mcp/tools/task.py:

- _attempt_merge_recovery(task, state): runs git rebase main in
  the agent's worktree; on clean rebase fast-forwards main and
  transitions task to DONE + clears merge_conflict source_context.
  Returns None on real conflict (worktree cleaned via rebase --abort).

- _sweep_blocked_merge_conflicts(state): iterates project_tasks,
  attempts recovery on each BLOCKED+merge_conflict task.

Wired into request_next_task: when no optimal_task, sweep BEFORE
returning no_task_ready.  If recovery unblocks downstream tasks,
the current agent claims directly — no spawn-thrash cycle.

Bright-line: rebasing the agent's branch onto main is environment
shaping, not HOW dictation.  Same primitive as git pull --rebase.

5 new tests with real tmp_path git repos.  408 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented May 25, 2026

Copy link
Copy Markdown

Claude finished @lwgray's task —— View job


Code Review Complete

I've thoroughly reviewed this PR implementing auto-recovery for merge-conflict blocked tasks. Here's my comprehensive feedback:

Review Checklist:

  • Read and analyze the main implementation in task.py
  • Review the test coverage and quality
  • Check code quality and best practices
  • Identify potential bugs or issues
  • Evaluate performance considerations
  • Assess security concerns
  • Provide final recommendations

Strengths

1. Excellent Problem Definition & Solution

  • Clear motivation: ~$25-50 wasted spawn costs from 49 ephemeral agents chasing unrecoverable conflicts
  • Smart architectural choice: auto-recovery via rebase handles the 90% case (stale-base conflicts) while leaving real content conflicts for human intervention
  • Well-integrated into existing request_next_task flow

2. Robust Implementation

  • Defensive error handling: Every subprocess call is wrapped with proper timeout and exception handling
  • Clean state guarantee: git rebase --abort on conflict ensures worktree isn't left in broken state
  • Proper Git operations: Defensive git reset --hard HEAD before merge, fast-forward-only merge strategy

3. Comprehensive Test Coverage

  • 5 well-structured test classes covering happy path, failure cases, and edge cases
  • Real Git repositories in tests (not mocked) - excellent for integration confidence
  • Specific assertions on git state, kanban updates, and return values

⚠️ Issues to Address

1. Missing Error Framework Integration (Minor)

# Current code at task.py:2988-2990
except (_sp.SubprocessError, OSError) as exc:
    logger.warning("[recovery] %s: rebase subprocess error: %s", task.id, exc)
    return None

Recommendation: Use Marcus error framework for consistency:

from src.core.error_framework import KanbanIntegrationError, ErrorContext

try:
    rebase = _sp.run([...])
except (_sp.SubprocessError, OSError) as exc:
    raise KanbanIntegrationError(
        operation="merge_conflict_recovery",
        context=ErrorContext(
            operation="git_rebase",
            task_id=task.id,
            agent_id=agent_id
        )
    ) from exc

2. Subprocess Security (Minor)
The subprocess calls at lines 2981-2987 are secure (no shell=True, fixed command args), but consider adding explicit security documentation.

3. Performance Considerations (Minor)

  • Git operations have 30-60s timeouts - appropriate for the use case
  • Sequential processing in _sweep_blocked_merge_conflicts is fine for current scale
  • Consider batching if recovery count grows significantly

🔍 Code Quality Assessment

Excellent:

  • Clear function signatures with proper type hints
  • Comprehensive docstrings following numpy style
  • Logical error handling flow
  • Good separation of concerns

Good naming and structure:

  • _attempt_merge_recovery vs _sweep_blocked_merge_conflicts - clear distinction
  • Helper functions are well-factored and testable

🧪 Test Quality Review

Excellent test design:

def _setup_recoverable_repo(tmp_path: Path) -> Path:
    """Build a tmp git project where rebase resolves cleanly."""
  • Real Git operations instead of mocking - builds confidence
  • Clear test scenarios: recoverable vs unrecoverable conflicts
  • Proper cleanup verification: checks that git rebase --abort worked
  • Edge case coverage: missing worktree, malformed source_context

🛡️ Security Assessment

✅ Secure:

  • No shell injection risks (subprocess args are lists, not strings)
  • Path traversal protected by working within known worktree structure
  • No credential exposure in logs or error messages

📊 Architectural Alignment

✅ Passes Multi-Agency Invariants:

  • Marcus is shaping the environment (rebasing branches), not dictating implementation HOW
  • This is equivalent to git pull --rebase that agents could run themselves
  • Agent code remains unchanged; only integration mechanics are automated

🎯 Final Recommendations

  1. Ship as-is - this is production-ready code that solves a real cost problem
  2. Consider follow-up: Integrate Marcus error framework for consistency with codebase patterns (see CLAUDE.md ERROR_HANDLING_FRAMEWORK section)
  3. Monitor in production: The logging at lines 2974-2979 and 3058-3062 provides good observability

🏆 Summary

This is high-quality defensive code that solves a concrete business problem ($25-50 wasted spawn costs). The implementation is robust, well-tested, and properly integrated. The bright-line architectural check passes - this is environment coordination, not agent control.

Recommendation: APPROVE


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant