gabriel / musehub public
filed by gabriel human · 8 days ago

Merge gates: approval count enforcement and conflict-blocked merge

0 Anchors
Blast radius
Churn 30d
0 Proposals

Background

Two merge gate features are missing from the current proposal system:

  1. Approval count: The UI shows 0/2 approvals by default but the merge endpoint does not enforce it — an MP can be merged with zero approvals regardless of what the UI shows.
  2. Conflict blocking: Muse does not prevent merging an MP that has known conflicts. On GitHub, a PR with conflicts cannot be merged until the author resolves them. We need the same gate.

Both gates must be enforced at the API layer (not just the UI) so that CLI merges (muse hub proposal merge) are equally blocked.


Phase 1 — Audit the current approval model

Read musehub_proposals.merge_conditions (JSONB column) and trace how it flows through create_proposal, merge_proposal, and the merge conditions check service.

Key questions to answer before writing any code:

  • Where is 0/2 coming from? Is it a hardcoded UI default, a DB default, or something set at proposal creation?
  • Does the merge endpoint call a check_merge_conditions function? Does it respect required_approvals?
  • What is the schema of merge_conditions? Does it already have a required_approvals field?
  • What approval states exist? (approve, request_changes, comment?)

Produce a written audit before Phase 2 begins.


Phase 2 — Fix approval default and enforcement

Default behavior: a newly created MP should have required_approvals = 0 (no gate). The 0/2 shown in the UI must come from somewhere — find it and remove the hardcoded default.

Enforcement logic (when required_approvals > 0 has been explicitly set):

  • merge_proposal() service must check: approved_count >= required_approvals
  • If not satisfied: return 409 Conflict with body {"error": "approval_gate", "approved": N, "required": M}
  • The merge button in the UI must be disabled (not just visually — the form submit must be blocked) with tooltip: "M approvals required, N given"

Setting required_approvals via the API:

muse hub proposal update <id> --merge-conditions '{"required_approvals": 2}' --json

Tests (TDD):

  • RED: test_merge_blocked_when_approvals_insufficient
  • RED: test_merge_allowed_when_approvals_met
  • RED: test_merge_allowed_when_required_is_zero
  • GREEN all three before shipping

Phase 3 — Conflict detection gate

Before a merge is executed, the server must check whether the source branch can be merged cleanly into the target. If there are conflicts, the merge must be blocked.

Detection approach:

  • Run muse merge --dry-run <from_branch> (or equivalent in-process) against the target branch snapshot at proposal-read time
  • Store the result as conflict_state on the proposal: {"has_conflicts": bool, "conflict_paths": [...]}
  • Recompute on every push to either branch (webhook/event driven)

API enforcement:

  • merge_proposal(): if conflict_state.has_conflicts == True, return 409 with {"error": "merge_conflict", "conflict_paths": [...]}
  • muse hub proposal merge CLI must surface this cleanly: ❌ Cannot merge: conflicts in src/foo.py, src/bar.py. Resolve and push.

UI:

  • Proposal hero: if conflicts detected, replace the merge button with a red ⚠ Conflicts banner listing the conflicting paths
  • Banner links to the diff view so the author knows what to resolve
  • Once the author pushes a resolved branch, the conflict state recalculates and the merge button reappears

DB:

  • Add conflict_state JSONB column to musehub_proposals (new migration)
  • Nullable — NULL means "not yet checked", {has_conflicts: false} means clean, {has_conflicts: true, paths: [...]} means blocked

Tests (TDD):

  • RED: test_merge_blocked_when_conflicts_detected
  • RED: test_merge_allowed_when_no_conflicts
  • RED: test_conflict_state_recomputed_on_push
  • RED: test_conflict_paths_returned_in_409

Phase 4 — Gate composition

Both gates (approval count + conflict) must compose correctly: a merge is only allowed when ALL gates pass. The error response must list every failing gate, not just the first:

{
  "error": "merge_blocked",
  "gates": [
    {"gate": "approval_count", "approved": 1, "required": 2},
    {"gate": "conflict", "conflict_paths": ["src/foo.py"]}
  ]
}

The UI merge button tooltip must reflect all failing gates simultaneously.


Phase 5 — CLI muse hub proposal merge hardening

The CLI must handle gate failures gracefully:

  • Print each failing gate as a separate ❌ line
  • Exit 1 (not crash)
  • --force flag explicitly NOT supported — gates are not bypassable from the CLI

Manual test checklist:

[ ] MP with 0 required approvals merges immediately
[ ] MP with 2 required approvals blocked at 0 approvals → 409
[ ] MP with 2 required approvals blocked at 1 approval → 409
[ ] MP with 2 required approvals merges at 2 approvals → 200
[ ] MP with conflicts blocked → 409 with paths
[ ] MP with conflicts resolved + pushed → merge unblocked
[ ] MP with both gates failing → 409 with both gates listed
[ ] CLI surfaces all gate failures on one 409 response
Activity
gabriel opened this issue 8 days ago
No activity yet. Use the CLI to comment.