lvlup-sw/exarchos

merge-orchestrator

The composed executor that lands a subagent worktree branch onto an integration branch with preflight + recorded recovery point.

查看源码
仓库原始内容

按源仓库内容呈现,保留标题、案例、代码、表格、链接以及原文引用的演示图片。

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 --abortgit 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 --abortgit 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

StrategyLocal git operationWhen to choose
mergegit merge --no-ff --no-edit <source> — explicit merge commitPreserves the subagent's commit history with a visible merge boundary.
squashgit merge --squash <source> then git commit — single squash commit on targetSubagent commit history is noise; one logical change should land as one commit.
rebaserebases an ephemeral copy of source onto target then ff-merges target — linear historyNo 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:

phaseMeaningOperator action
completedLocal merge landed; mergeSha is the new HEAD of target.None — workflow continues per the orchestrator's playbook.
abortedPreflight 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-backMerge was attempted, failed (reason: 'merge-failed' / 'verification-failed' / 'timeout'), and the INV-14 recovery ladder (git merge --abortgit 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.reasonPayload fieldsCauseOperator remediation
target-checked-out-elsewheresiblingWorktreePath: 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 typeWhenCarries
merge.preflightAlways (after preflight runs, before any merge attempt) — except for the early-abort target-checked-out-elsewhere path, which emits nothingFull structured guard sub-results + failureReasons if passed: false
merge.requestedAfter preflight passes, before the executor runs (Phase A intent record from the two-event split) — suppressed on the early-abort target-checked-out-elsewhere pathsourceBranch, targetBranch, strategy, taskId
merge.executedOn successful local mergemergeSha, rollbackSha, taskId, source/target branches
merge.recoveredOn post-merge failure followed by recovery — the canonical recovery event and, since DR-2, the only one emittedrecoveryPointSha, reason, recoveryError?, recoveryErrorDetail?, taskId, source/target branches
merge.rollbackRETIRED (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.recoveredrollbackSha, 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` 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:

Aspectmerge_orchestrate (this skill)merge_pr (companion skill)
LayerLocal SDLC handoffRemote PR primitive
What it mergesA subagent worktree branch into a target branchA user-facing PR via the VCS provider API
Identifier requiredsourceBranch + targetBranchprId
Underlying operationgit merge (local)provider.mergePr() (remote API)
RecoveryINV-14 recovery ladder: git merge --abortgit reset --keep <recoveryPointSha> (real, rewinds the merge; never --hard)None — the VCS provider owns merge state
Eventsmerge.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'tDo Instead
Dispatch raw merge_orchestrate to land onto a shared integration branchRoute 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 PRUse the merge_pr skill
Manually emit merge.preflight / merge.requested / merge.executed / merge.recovered in normal flowLet the handler auto-emit; manual emission causes duplicates (one exception: documented manual-recovery flow in `recovery-runbook.md`)
Wrap merge events under gate.executedDirect stream append with the dedicated event type — these are state transitions, not gate executions
Re-dispatch after a rolled-back outcome without inspecting the reasonRead 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 worktreeRemove or re-checkout the sibling worktree referenced by data.siblingWorktreePath, then re-dispatch
Omit --strategy / strategy: field expecting a defaultStrategy is required; supply squash / merge / rebase explicitly
Invoke from a subagent worktreePreflight 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:

FieldSourcePurpose
gitVersiongit --versionDifferentiate behaviors across git releases.
repoRootgit rev-parse --show-toplevelDistinguish symlinked or normalized vs raw repo paths.
worktreeListgit worktree list --porcelainSurface sibling worktree topology that could affect ref resolution.
refsHeadsSourcegit for-each-ref refs/heads/<source>SHA + packed-state of the source branch ref.
refsHeadsTargetgit for-each-ref refs/heads/<target>SHA + packed-state of the target branch ref.
mergeBaseCommandconstructedExact argv re-run by the helper, including 'git' prefix — copy-pasteable for the operator.
mergeBaseExitCodererun of git merge-base --is-ancestor <target> <source>The exit code that drove the ancestry failure.
mergeBaseStdoutsame invocationCaptured stdout.
mergeBaseStderrsame invocationCaptured 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.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.

来自同一仓库

更多 Skills

全部 Skills