# Merge Orchestrator

> The composed executor that lands a subagent worktree branch onto an integration branch with preflight + recorded recovery point. For a shared integration branch route through `serialize_merge` (the single-writer lease that composes this action); invoke `merge_orchestrate` directly only for a non-integration merge or when you already hold the lease. Triggers: operator (or `next_actions`) surfaces the merge verb via Exarchos MCP. Local git operation — NOT remote PR merging (that is `merge_pr`).

- Skill: `lvlup-sw/merge-orchestrator` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add lvlup-sw/merge-orchestrator`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lvlup-sw/merge-orchestrator/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: lvlup-sw (https://skillmd.com/u/lvlup-sw)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lvlup-sw/merge-orchestrator

---


# Merge Orchestrator Skill

## Local Git, Not Remote VCS

This skill performs **local `git merge`** of a source (subagent) worktree branch into a target (integration) branch, recording a **recovery point** SHA so the INV-14 recovery ladder (`git merge --abort` → `git reset --keep`, never `--hard`) can rewind the merge on any failure. It does **not** call the VCS provider (GitHub / GitLab / Azure DevOps) and does not require a PR id. For remote PR merging, see the companion skill that wraps the `merge_pr` action.

The mental model and the rationale for why these are two separate concerns are documented in `references/local-git-semantics.md`.

## Single-Writer Entry Point: `serialize_merge`

For a **shared integration branch** — landing a subagent worktree branch onto the integration ref during the `delegate → merge-pending → delegate` loop, where a sibling worktree merge can race — **`serialize_merge` is THE integration-merge path.** It acquires an optimistic per-`integrationRef` lease (at most one in-flight merge per integration ref), then composes **this** action UNCHANGED under that lease. Route integration merges through `serialize_merge`; do **not** dispatch raw `merge_orchestrate` for them.

Invoke `merge_orchestrate` directly **only** where no concurrent merge can race the target:

- **Non-integration merge** — the target is a private / scratch / solo branch no other agent will touch.
- **Crash-resumed caller** — a new pid finishing a merge it already leased, re-presenting the ORIGINAL claim's `leaseOperationId`.
- **The composed executor call** that `serialize_merge` itself makes (it threads its lease `operationId` so its own call passes the guard).

**DR-2 lease guard.** Raw `merge_orchestrate` against an integration ref that carries a live *foreign* lease (holder `operationId` ≠ the presented `leaseOperationId`, holder liveness ∈ {alive, unknown}) fails **closed** with a structured `MERGE_LEASE_HELD` error and **no git side effect** — the message names `serialize_merge` as the path. A no-lease or provably-dead-holder target proceeds exactly as before (back-compat; a dead holder is not a blocker). So the routing above is enforced at the handler chokepoint, not merely by convention — reach for `serialize_merge`.

## Overview

This skill activates whenever an operator (or an automated `next_actions` dispatcher) invokes the `merge_orchestrate` action against Exarchos MCP. After the source branch has been prepared in a worktree, this skill:

1. Performs a worktree-availability preflight (target branch must not be checked out in a sibling worktree).
2. Composes additional preflight guards (ancestry, current-branch protection, main-worktree assertion, working-tree drift).
3. Records pre-merge `HEAD` of the target branch as a recovery point (a write-ahead-log marker, persisted before any ref mutation).
4. Performs a local `git merge` of the source branch into the target branch.
5. On any failure, runs the INV-14 recovery ladder (`git merge --abort` → `git reset --keep <recoveryPointSha>`, never `--hard`) and surfaces a categorized failure reason.
6. Emits dedicated event types so the merge timeline is reconstructable from the event log alone.

Resumable: terminal phases (`completed` / `rolled-back` / `aborted`) short-circuit on re-entry without re-emitting events. Idempotent: re-dispatch with the same `taskId` collapses via the `next_actions` idempotency key.

## Triggers

Activate this skill when:

- The operator runs `exarchos merge-orchestrate ...` (CLI).
- `exarchos:exarchos_orchestrate({ action: "merge_orchestrate", ... })` is invoked directly.
- An automated `next_actions` envelope surfaces the merge verb (idempotency key `<streamId>:merge_orchestrate:<taskId>`) for the `merge-pending` detour.

> **Route shared-integration merges through `serialize_merge`.** When the trigger is a shared integration branch (the `merge-pending` detour is the common case), land it via `serialize_merge` per the single-writer entry point above — `serialize_merge` composes this action under a lease. A direct raw `merge_orchestrate` dispatch is for a non-integration or already-lease-held merge only; against a leased integration ref it fails closed (`MERGE_LEASE_HELD`).

Do **not** activate this skill:
- To merge a remote PR — that is `merge_pr`. The two are disjoint actions; see Disambiguation below.
- When the orchestrator's recorded `mergeOrchestrator.phase` is already terminal — the resume short-circuit runs but no fresh dispatch is needed.

## Process

> **Schema:** discover the action's argument schema with `exarchos:exarchos_orchestrate({ action: "describe", actions: ["merge_orchestrate"] })`. Strategy is required (no schema-level default) — pick `squash` / `merge` / `rebase` deliberately.

### Step 1: Pick the merge strategy

| Strategy | Local git operation | When to choose |
|----------|---------------------|----------------|
| `merge`  | `git merge --no-ff --no-edit <source>` — explicit merge commit | Preserves the subagent's commit history with a visible merge boundary. |
| `squash` | `git merge --squash <source>` then `git commit` — single squash commit on target | Subagent commit history is noise; one logical change should land as one commit. |
| `rebase` | rebases an ephemeral copy of source onto target then ff-merges target — linear history | No merge commit; integration branch stays linear. The original source ref is preserved (the rebase runs on a temporary branch that is deleted afterward), so an executor recovery only needs to rewind target to the recovery point. |

Strategy is required at the schema layer (collision check + user-visible parity). There is no implicit default — operator intent is always explicit in the event log.

### Step 2: Invoke

> **Integration merge?** The direct invocation below is the raw executor form — use it for a **non-integration** or already **lease-held** merge. For a shared integration branch, invoke `serialize_merge` instead (it composes this exact call under a single-writer lease); see the single-writer entry point above.

Via MCP (illustrative — the canonical arg names come from `describe`):

```typescript
exarchos:exarchos_orchestrate({
  action: "merge_orchestrate",
  // workflow-correlation identifier — name per the action's schema
  sourceBranch: "<subagent-branch>",
  targetBranch: "<integration-branch>",
  taskId: "<task-id>",          // present when auto-dispatched from next_actions
  strategy: "squash",            // required
  dryRun: false,                 // optional — preflight only, no executor invocation
  resume: false,                 // optional — short-circuit on terminal phases
})
```

Via CLI:

```bash
exarchos merge-orchestrate \
  --source-branch <subagent-branch> \
  --target-branch <integration-branch> \
  --task-id <task-id> \
  --strategy squash
  # plus the workflow-correlation id flag — see `--help`
  # add --dry-run for preflight-only, --resume for terminal-phase short-circuit
```

CLI exit codes: 0 = success, 1 = invalid input, 2 = merge failed (preflight blocked or recovery executed), 3 = uncaught exception.

### Step 3: Interpret the result

The handler returns a `ToolResult` whose `data.phase` discriminates the outcome:

| `phase` | Meaning | Operator action |
|---------|---------|-----------------|
| `completed` | Local merge landed; `mergeSha` is the new HEAD of target. | None — workflow continues per the orchestrator's playbook. |
| `aborted` | Preflight failed; no merge attempted. `data.preflight` carries the structured guard sub-results (when produced by the body preflight) OR `data.reason` discriminates the early-abort cause. | See the abort-reason table below. Resolve the underlying condition and re-dispatch. |
| `rolled-back` | Merge was attempted, failed (`reason: 'merge-failed' / 'verification-failed' / 'timeout'`), and the INV-14 recovery ladder (`git merge --abort` → `git reset --keep <recoveryPointSha>`) ran. The target branch is rewound to the recovery point. (The phase value stays `rolled-back` — a load-bearing state token unchanged by the recovery reframe.) | Inspect `data.reason`. If `data.recoveryError` / `data.recoveryErrorDetail` is also present, the reset itself failed — the working tree is stranded and requires operator intervention. |

#### Abort-reason payload shapes

When `phase === 'aborted'`, the data payload discriminates the cause:

| `data.reason` | Payload fields | Cause | Operator remediation |
|---------------|----------------|-------|----------------------|
| `target-checked-out-elsewhere` | `siblingWorktreePath: string` (absolute path) | Target branch is already checked out in a sibling worktree of the same repository. Detected by the worktree-availability preflight *before* any event emission, executor invocation, or state persistence. | Resolve the sibling worktree: either remove it (`git worktree remove <path>`), switch its checkout to another branch, or re-run the merge against a different target. Then re-dispatch (through `serialize_merge` for a shared integration ref). |
| *(body preflight failures)* | `data.preflight: { ancestry, worktree, currentBranchProtection, drift }` | Body-preflight guard failed (ancestry mismatch, worktree drift, protected current branch, etc.). | Inspect `preflight.*` sub-results to identify which guard failed. Resolve the underlying condition (e.g., commit/stash drift, switch off a protected branch) and re-dispatch. |

The `target-checked-out-elsewhere` abort path is special: it suppresses both the `merge.requested` and `merge.preflight` events and skips state persistence entirely. This guarantees the event log is never contaminated with an attempt that could not have captured a correct recovery point (the executor would have read HEAD from the wrong worktree).

For the full recovery flow per outcome, see `references/recovery-runbook.md`.

### Step 4: Confirm event emissions

Events are emitted directly to the orchestrator's event stream (stream id is the value passed as `streamId`) — **not** wrapped in `gate.executed`:

| Event type | When | Carries |
|------------|------|---------|
| `merge.preflight` | Always (after preflight runs, before any merge attempt) — except for the early-abort `target-checked-out-elsewhere` path, which emits nothing | Full structured guard sub-results + `failureReasons` if `passed: false` |
| `merge.requested` | After preflight passes, before the executor runs (Phase A intent record from the two-event split) — suppressed on the early-abort `target-checked-out-elsewhere` path | `sourceBranch`, `targetBranch`, `strategy`, `taskId` |
| `merge.executed`  | On successful local merge | `mergeSha`, `rollbackSha`, `taskId`, source/target branches |
| `merge.recovered` | On post-merge failure followed by recovery — the canonical recovery event and, since DR-2, the **only** one emitted | `recoveryPointSha`, `reason`, `recoveryError?`, `recoveryErrorDetail?`, `taskId`, source/target branches |
| `merge.rollback`  | **RETIRED** (DR-2) — legacy wire shape, read-tolerant-not-emittable: nothing writes it any more; it survives only in pre-DR-2 event logs, where it folds identically to `merge.recovered` | `rollbackSha`, `reason`, `taskId`, source/target branches |

> **Recovery vocabulary.** The canonical frame is database-transaction, not saga: a **recovery point** (the recorded HEAD SHA, persisted before the merge as a write-ahead-log marker) and a **recovery event** (`merge.recovered`). `merge.recovered` is the successor to `merge.rollback` and, since DR-2, the sole recovery event the executor emits; the legacy `merge.rollback` write path is retired (read-tolerant-not-emittable, kept only so pre-DR-2 logs replay identically). `merge.recovered` carries the resolved `recoveryPointSha` / `recoveryErrorDetail` names alongside the unchanged INV-14 `recoveryError` discriminator; the retained `merge.executed` event still carries its `rollbackSha` wire field. The phase value `'rolled-back'` is intentionally retained.

These events are auto-emitted by the handler — do **not** manually append them via `exarchos:exarchos_event` during normal operation. Manual emission is only sanctioned during the documented manual-recovery flow in [`recovery-runbook.md`](references/recovery-runbook.md) when a merge has been completed out-of-band (e.g., conflict resolution) and the event log must be brought back in sync — follow that runbook's event-first sequencing.

> Discover the event payload schemas via `exarchos:exarchos_event({ action: "describe", eventTypes: ["merge.preflight", "merge.requested", "merge.executed", "merge.rollback", "merge.recovered"] })`. (`merge.rollback` is **legacy, read-tolerant** — retained so pre-DR-2 logs replay, no longer emitted.)

## Disambiguation: `merge_orchestrate` vs `merge_pr`

Two related actions, two distinct concerns:

| Aspect | `merge_orchestrate` (this skill) | `merge_pr` (companion skill) |
|--------|----------------------------------|------------------------------|
| **Layer** | Local SDLC handoff | Remote PR primitive |
| **What it merges** | A subagent worktree branch into a target branch | A user-facing PR via the VCS provider API |
| **Identifier required** | `sourceBranch` + `targetBranch` | `prId` |
| **Underlying operation** | `git merge` (local) | `provider.mergePr()` (remote API) |
| **Recovery** | INV-14 recovery ladder: `git merge --abort` → `git reset --keep <recoveryPointSha>` (real, rewinds the merge; never `--hard`) | None — the VCS provider owns merge state |
| **Events** | `merge.preflight` / `merge.requested` / `merge.executed` / `merge.recovered` (legacy `merge.rollback` retired — read-tolerant, not emitted) | `pr.merged` |

If you reach for `merge_orchestrate` thinking "I want to merge a PR," you want `merge_pr` instead.

## Resume Semantics

When invoked with `resume: true`, the handler reads existing `mergeOrchestrator` state. Terminal phases (`completed` / `rolled-back` / `aborted`, members of `EXCLUDED_MERGE_PHASES`) short-circuit and return the recorded result with no new events and no executor call. Non-terminal phases (`pending` / `executing`) fall through to a fresh preflight + executor run, which is safe because the underlying git operations are idempotent on already-merged branches.

When invoked without `resume`, prior state is deliberately ignored — fresh-dispatch semantics.

Note: the `target-checked-out-elsewhere` early-abort path runs *before* the resume state read, so it short-circuits regardless of `resume` and never persists state.

## Dry-Run

`dryRun: true` runs the body preflight, emits `merge.preflight`, and short-circuits before the executor runs and before any state persistence. Returns `{ dryRun: true, preflight, phase: 'pending' | 'aborted' }`. Useful for CI integrations that check merge readiness before the merge window opens. Dry-run still observes the worktree-availability early abort — a sibling-worktree conflict aborts with `reason: 'target-checked-out-elsewhere'` even under `dryRun`.

## Anti-Patterns

| Don't | Do Instead |
|-------|------------|
| Dispatch raw `merge_orchestrate` to land onto a **shared integration branch** | Route through `serialize_merge` (single-writer lease); raw `merge_orchestrate` is for a non-integration / lease-held merge — a live foreign lease makes it fail closed (`MERGE_LEASE_HELD`) |
| Use this skill to merge a remote PR | Use the `merge_pr` skill |
| Manually emit `merge.preflight` / `merge.requested` / `merge.executed` / `merge.recovered` in normal flow | Let the handler auto-emit; manual emission causes duplicates (one exception: documented manual-recovery flow in [`recovery-runbook.md`](references/recovery-runbook.md)) |
| Wrap merge events under `gate.executed` | Direct stream append with the dedicated event type — these are state transitions, not gate executions |
| Re-dispatch after a `rolled-back` outcome without inspecting the reason | Read `data.reason` and `data.recoveryErrorDetail` (with the `data.recoveryError` discriminator); address the root cause first |
| Re-dispatch after `reason: 'target-checked-out-elsewhere'` without first freeing the sibling worktree | Remove or re-checkout the sibling worktree referenced by `data.siblingWorktreePath`, then re-dispatch |
| Omit `--strategy` / `strategy:` field expecting a default | Strategy is required; supply `squash` / `merge` / `rebase` explicitly |
| Invoke from a subagent worktree | Preflight refuses (main-worktree assertion); invoke from the main worktree |

## Diagnostics

Set `EXARCHOS_PREFLIGHT_DEBUG=1` in the environment before invoking `merge_orchestrate` to attach a structured debug payload to `merge.preflight` events. The payload is gated on **two** conditions, both of which must hold:

1. `EXARCHOS_PREFLIGHT_DEBUG=1` is present in the orchestrator process environment.
2. The preflight's ancestry guard failed (i.e., `ancestry.passed === false`).

Passing preflights do **not** carry the debug payload even when the env var is set — the failure-only gating is deliberate (event-store growth concern). A future `EXARCHOS_PREFLIGHT_DEBUG=2` channel may add verbose / passing-preflight diagnostics; that is out of scope for phase 1.

The debug block carries nine fields:

| Field | Source | Purpose |
|-------|--------|---------|
| `gitVersion` | `git --version` | Differentiate behaviors across git releases. |
| `repoRoot` | `git rev-parse --show-toplevel` | Distinguish symlinked or normalized vs raw repo paths. |
| `worktreeList` | `git worktree list --porcelain` | Surface sibling worktree topology that could affect ref resolution. |
| `refsHeadsSource` | `git for-each-ref refs/heads/<source>` | SHA + packed-state of the source branch ref. |
| `refsHeadsTarget` | `git for-each-ref refs/heads/<target>` | SHA + packed-state of the target branch ref. |
| `mergeBaseCommand` | constructed | Exact argv re-run by the helper, including `'git'` prefix — copy-pasteable for the operator. |
| `mergeBaseExitCode` | rerun of `git merge-base --is-ancestor <target> <source>` | The exit code that drove the ancestry failure. |
| `mergeBaseStdout` | same invocation | Captured stdout. |
| `mergeBaseStderr` | same invocation | Captured stderr (collapsed into stdout under the default git adapter). |

Fail-closed: any individual git invocation that fails inside the debug helper degrades to an empty string / default value for that field rather than throwing. The debug attachment must never mask the underlying preflight failure the operator is trying to investigate.

**Reporting workflow.** When you capture a debug-bearing event, attach the full `data.debug` block to a new GitHub issue tagged with relevant scope labels (e.g., `windows`, `merge-orchestrator`, `preflight`). Phase-2 root-cause analysis depends on at least one real-host event with this payload.

## Schema Discovery

For the argument schema, call `exarchos:exarchos_orchestrate({ action: "describe", actions: ["merge_orchestrate"] })`. Event payload shapes come from `exarchos:exarchos_event({ action: "describe", eventTypes: ["merge.preflight", "merge.requested", "merge.executed", "merge.rollback", "merge.recovered"] })` (`merge.rollback` is **legacy, read-tolerant** — retained so pre-DR-2 logs replay, no longer emitted).

`mergeOrchestrator.*` fields on workflow state are written by this skill and `mergeOrchestrator.phase` is read by gates; the underlying `phase` workflow field is immutable and must be changed via `transition`, not `update`. See the [Reserved fields](../checkpoint/SKILL.md#reserved-fields) section in the `checkpoint` skill for the full immutable-key list and the typed `RESERVED_FIELD` error envelope.

## Completion Criteria

- [ ] Preflight result `passed: true` (or operator has decided to proceed despite a documented preflight gap)
- [ ] `mergeOrchestrator.phase === 'completed'` in orchestrator state
- [ ] `merge.executed` event present in the stream with the recorded `mergeSha` and `rollbackSha` (the legacy wire field for the recovery point, retained during the v2.11.x deprecation window)
- [ ] Target branch's HEAD matches the recorded `mergeSha`

If any criterion fails, consult `references/recovery-runbook.md` before re-dispatching.

