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`).
How do I install this agent skill?
npx skills add https://github.com/lvlup-sw/exarchos --skill merge-orchestratorIs this agent skill safe to install?
- Gen Agent Trust Hubpass
The merge-orchestrator skill facilitates local Git merges with automated recovery logic. It presents a low security risk due to its diagnostic feature, which encourages sharing sensitive repository metadata, including absolute file paths, to external platforms. It also exhibits an indirect prompt injection surface as it lacks instructions for sanitizing user-provided branch names before execution.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
What does this agent skill do?
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_mergeitself makes (it threads its leaseoperationIdso 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:
- Performs a worktree-availability preflight (target branch must not be checked out in a sibling worktree).
- Composes additional preflight guards (ancestry, current-branch protection, main-worktree assertion, working-tree drift).
- Records pre-merge
HEADof the target branch as a recovery point (a write-ahead-log marker, persisted before any ref mutation). - Performs a local
git mergeof the source branch into the target branch. - On any failure, runs the INV-14 recovery ladder (
git merge --abort→git reset --keep <recoveryPointSha>, never--hard) and surfaces a categorized failure reason. - 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_actionsenvelope surfaces the merge verb (idempotency key<streamId>:merge_orchestrate:<taskId>) for themerge-pendingdetour.
Route shared-integration merges through
serialize_merge. When the trigger is a shared integration branch (themerge-pendingdetour is the common case), land it viaserialize_mergeper the single-writer entry point above —serialize_mergecomposes this action under a lease. A direct rawmerge_orchestratedispatch 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.phaseis 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) — picksquash/merge/rebasedeliberately.
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_mergeinstead (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):
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:
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.recoveredis the successor tomerge.rollbackand, since DR-2, the sole recovery event the executor emits; the legacymerge.rollbackwrite path is retired (read-tolerant-not-emittable, kept only so pre-DR-2 logs replay identically).merge.recoveredcarries the resolvedrecoveryPointSha/recoveryErrorDetailnames alongside the unchanged INV-14recoveryErrordiscriminator; the retainedmerge.executedevent still carries itsrollbackShawire 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 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.rollbackis 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) |
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:
EXARCHOS_PREFLIGHT_DEBUG=1is present in the orchestrator process environment.- 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 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.executedevent present in the stream with the recordedmergeShaandrollbackSha(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.
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/lvlup-sw/exarchos/merge-orchestrator">View merge-orchestrator on skillZs</a>