Merge reconcilers
Some files in a repo are derived: a pure function of a directory, checked in
alongside their inputs. A migrations lockfile (atlas.sum) is a checksum over
the migration directory; a secrets baseline (.secrets.baseline) is a set of
reviewed findings keyed by source file. When two beads each add an input and
each regenerate the derived file, the inputs merge cleanly (different filenames)
but the derived file collides — git's line-level merge cannot tell that the file
is a checksum-over-a-listing, so it calls the divergent block a conflict and the
rebase aborts.
The correct merged artifact is just the regeneration over the union of both
sides' inputs — which the rebase has already produced in the working tree.
Merge reconcilers close this gap: a per-project allowlist of path → command
that koryph consults only at a rebase conflict, regenerating the derived
file and continuing instead of surfacing a spurious conflict.
This is the belt, not the suspenders. The first-line fix is a footprint label — see Parallelism: footprints. A bead that adds a file to a directory with a checked-in derived artifact should share a write footprint with every other such bead, so the scheduler serializes them at dispatch and the collision never happens. Reconcilers heal the residual case a label cannot cover (an out-of-band push, a non-dispatch-shaped merge landing between a worktree's creation and its merge). A rising heal rate in the logs is the signal that a label is missing upstream.
Configuring
Add merge_reconcilers to koryph.project.json — a list of {path, command}
entries:
"merge_reconcilers": [
{
"path": "migrations/atlas.sum",
"command": "atlas migrate hash --dir file://migrations"
},
{
"path": ".secrets.baseline",
"command": "koryph-secrets-union \"$KORYPH_MERGE_OURS\" \"$KORYPH_MERGE_THEIRS\" \"$KORYPH_MERGE_PATH\""
}
]
path— matched against each conflicted path withpath.Matchglob semantics. An exact path (migrations/atlas.sum) is the common case; globs likemigrations/*.sumwork; there is no**.command— runs viash -cin the worktree (underdirenv execwhen available), with the same allowlisted environment as the green gate — the orchestrator's ambient secrets are not exposed. It must leave$KORYPH_MERGE_PATHa valid, conflict-marker-free file.
Empty or absent merge_reconcilers is exactly today's behavior: any rebase
conflict aborts and requeues the bead for in-worktree resolution.
The command contract
Each reconciler command receives these environment variables:
| Variable | Meaning |
|---|---|
KORYPH_MERGE_PATH |
Absolute path to the conflicted file to write. |
KORYPH_MERGE_OURS |
Temp file with the base branch's version (git stage 2). Empty if absent. |
KORYPH_MERGE_THEIRS |
Temp file with the bead's version (git stage 3). Empty if absent. |
KORYPH_MERGE_BASE |
Temp file with the merge-base version (git stage 1). Empty if absent. |
Two idioms cover the real cases:
- Regenerate from the tree (a checksum). After the conflict, the input files
have already merged cleanly into the working tree — it is the post-merge
union. So
atlas migrate hash --dir file://migrationsrewrites the lockfile correctly from the tree and ignores the stage variables entirely. - Structured union (a secrets baseline). A blind rescan would drop the
audited review state both sides hold, so the command must merge the two sides.
Read
$KORYPH_MERGE_OURSand$KORYPH_MERGE_THEIRS, union the file-keyed findings, and write$KORYPH_MERGE_PATH.
Stage inversion. During a rebase, git's "ours" is the branch being rebased onto (your default branch) and "theirs" is the bead's commit — the reverse of a normal merge.
KORYPH_MERGE_OURS/_THEIRSare labeled from git's stage numbers as-is. A union command is symmetric and does not care; only write an asymmetric command if you have accounted for this.
Guarantees
Reconcilers are conservative by construction:
- All-or-nothing. A reconciler runs only when every conflicted path in a rebase step matches a configured entry. If any conflicted path is not covered — including a genuine conflict on an input file itself — the whole rebase aborts exactly as before. Auto-heal never partially resolves a step and never touches a hand-authored file.
- Fail safe. A command that fails, times out, or leaves conflict markers aborts the rebase to the normal requeue-for-agent path. Auto-heal only ever turns a fatal conflict into a clean merge, or leaves it fatal.
- The gate is the backstop. A healed tree still runs your green gate before
the merge. Put the artifact's validator in your gate —
atlas migrate validate,detect-secrets audit --report. That is what catches a bad regeneration; a reconciler without a validating gate is the one setup that can land a wrong derivative. - Bounded. The cascade (a renumber that re-collides with the next migration) heals round by round, capped so a misbehaving command cannot loop forever.
Observing
A healed merge is logged distinctly and audited:
bead koryph-abc: merged (a1b2c3d)
bead koryph-abc: rebase conflict auto-healed (1 generated file(s), 1 round(s)): migrations/atlas.sum
and a merge-reconcile audit event records the bead, branch, healed paths, and
round count. Watch that rate: frequent heals mean a footprint label is missing
on the beads that touch that directory — fix the label, and most collisions stop
happening in the first place.
Migration numbers: merge_prepare
A reconciler heals a conflict on a derived file. But two beads that each add a
migration can pick the same sequence number (0002_a.sql and 0002_b.sql)
— distinct filenames, so it is not even a git conflict, yet a migration tool
rejects the duplicate. The renumber cascade (renumber 13→14, but 14 landed too,
so 14→15…) is this problem compounding.
The root fix is to allocate the number at merge time, against the branch the
work is actually landing on. merge_prepare is that seam — an ordered command
list run in the worktree after the (possibly reconciler-healed) rebase and
before the gate. Point it at a small project script that renames the newly
added migration to the next free sequence and regenerates the checksum with a
community command (atlas migrate hash):
"merge_prepare": [
"scripts/renumber-migration-to-tip.sh"
]
- The command sees
KORYPH_DEFAULT_BRANCHin its environment, so a renumber-to-tip command can diff the rebased tree against its target. - If the command leaves the tree changed, koryph commits it as a single
conventional
chore(merge): …commit, signed with the same configuration the rebase just used, so the normalization rides the fast-forward merge and is gated. A clean tree is a no-op — most merges need nothing. - A command that exits non-zero is a gate-shaped failure: the merge is not made and the bead requeues, exactly like a gate regression.
Because a duplicate number is not a git conflict, merge_prepare runs on every
merge, not only on a conflict. As with reconcilers, the first-line fix is still a
footprint label — with the migration-touching beads serialized, they pick
sequential numbers to begin with and merge_prepare is only the backstop.
Tooling: keep it OSS/community
koryph invokes whatever you put in these fields as an opaque shell command and depends on no migration tool itself — nothing here is a koryph dependency. Keep the commands you configure on OSS/community tooling.
If you use Atlas, install the Apache-2.0 Community Edition — the default
atlas install is a source-available (Atlas EULA) binary with proprietary
extras, not the OSS build:
curl -sSf https://atlasgo.sh | sh -s -- --community
atlas migrate hash (regenerate the lockfile) and atlas migrate validate (a
gate check) are in the Community build. atlas migrate rebase, migrate lint,
and migrate checkpoint are not — they require the standard binary — so
renumber-to-tip with a small script rather than atlas migrate rebase.
Applies to
Both the reconciler self-heal and the merge_prepare normalization run on every
merge path: the wave/rolling auto-merge loop, the PR-open path
(merge_policy=pr), and the koryph merge / koryph land CLI.