Merge gates: approval count enforcement and conflict-blocked merge
Background
Two merge gate features are missing from the current proposal system:
- Approval count: The UI shows
0/2 approvalsby default but the merge endpoint does not enforce it — an MP can be merged with zero approvals regardless of what the UI shows. - 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/2coming from? Is it a hardcoded UI default, a DB default, or something set at proposal creation? - Does the merge endpoint call a
check_merge_conditionsfunction? Does it respectrequired_approvals? - What is the schema of
merge_conditions? Does it already have arequired_approvalsfield? - 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 Conflictwith 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_stateon the proposal:{"has_conflicts": bool, "conflict_paths": [...]} - Recompute on every push to either branch (webhook/event driven)
API enforcement:
merge_proposal(): ifconflict_state.has_conflicts == True, return409with{"error": "merge_conflict", "conflict_paths": [...]}muse hub proposal mergeCLI 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
⚠ Conflictsbanner 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 JSONBcolumn tomusehub_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)
--forceflag 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