Omniscio documentation
Browse all documentation
  1. Getting Started13
  2. Sessions & Agents115
  3. Inbox & Notifications59
  4. Projects & Tasks95
  5. Automation & Scheduling75
  6. Knowledge & Memory26
  7. AI Features60
  8. Integrations100
  9. Plugins & Marketplace33
  10. Cloud & Teams56
  11. Settings & Customization58
  12. Account & Billing28
  13. Troubleshooting84
  14. CLI & API Reference22
  15. Legal & Policies4
  16. Uncategorised22

Markdown merge driver (why two agents can edit one document)

Developer infrastructure rather than an app feature: a custom git merge driver that resolves the most common conflict in this repository — two agents independently appending to the same memory document — and falls back to ordinary conflict markers for anything it cannot resolve safely. There is no screen and no setting; it runs inside git.

What it is

A custom Git merge driver that auto-resolves the most common merge conflict in this repo — two agents independently appending to the same memory document — and steps aside (falls back to normal conflict markers) for anything it can't resolve safely. It is developer / repo-maintenance infrastructure, not an in-app feature: there is no UI, no setting, and no button. It runs automatically inside git merge once installed.

The problem it solves

Omniscio is normally driven by many Claude Code agents working in parallel git worktrees at the same time. Those agents constantly edit the same shared "memory" documents — appending a bullet to MEMORY.md, adding a row to a postmortem index, recording a new gotcha, dropping a new feedback note. These files are append-heavy logs by nature.

Git's default text merge is line-range based. The moment two branches insert different lines at the same place in such a file, Git declares a conflict and writes <<<<<<< / ======= / >>>>>>> markers, even though both edits were really just additions that could coexist. Before this driver, the repo's safe-merge workflow hit these spurious conflicts constantly, and historically the bash helper would even abort outright on CLAUDE.md. Each one cost a human (or an agent) a manual resolve for a merge that was never actually in conflict.

Where to find it

Nowhere in the app. It is part of how this repository's git is configured, so the way you meet it is by merging a branch: the conflict you expected simply does not appear.

How it behaves

What it does, in plain language

When Git performs a three-way merge of a file, it knows three versions: the common ancestor (base), the version on your current branch (ours), and the version being merged in (theirs). A registered merge driver receives all three and decides the outcome for that one file.

For each registered memory document, this driver asks a single conservative question: did both sides only insert new lines, without deleting or rewriting any existing line?

  • Yes — both sides are purely additive → it produces a union: every original line stays in place, and each side's inserted lines are interleaved at the position where that side put them. If both sides happened to add the exact same line at the same spot (e.g. two agents appended the identical bullet), the duplicate is collapsed to one copy. The pairing is one copy per side, so a line that ONE side wrote twice (two new rules that each end in the same "Enforced" line) still appears twice — until 2026-09-24 it collapsed too, and the git-authority guard refused that land as dropped work. Git is told the file merged cleanly — no conflict markers, no human intervention.
    • Except when the two additions are rival VERSIONS of one block — both branches append the same new section, differing in a few lines in the middle. Collapsing the lines they share would leave the lines they worded differently stranded at the end of the file, cut off from the sentences around them. The driver detects that case (a shared line that comes after a line only one side has) and conflicts instead, because two versions of one paragraph are a disagreement to resolve, not two separate additions to concatenate. This is the 2026-09-13 contract corruption: three link citations a fix had already converted to plain text came back as orphaned sentence fragments and re-broke the doc gate, with git reporting a clean merge.
  • No — anything else (one side deleted or edited an existing line in place, the two sides gave the same frontmatter field different values, a section was reordered, etc.) → the driver declines and hands the file back to Git's standard merge, so you get the normal <<<<<<< / ======= / >>>>>>> markers and resolve it by hand, exactly as you would have without the driver installed.

The bias toward declining is deliberate. A missed auto-merge costs a few seconds of manual resolve; an incorrect auto-merge silently corrupts a memory document that every future agent session trusts. When in doubt, it conflicts.

How "additive" is decided

The driver treats each file as two regions and runs the same logic on each:

  1. YAML frontmatter (the --- ... --- block at the top, if present). Metadata is too semantically loaded to blend silently, so it merges field by field: one side's changed fields combine with fields the other side only added, but if both sides gave the same field different values, each side rewrote a different existing field, or a side removed or reordered fields, that region conflicts even when the body could have merged.
  2. The markdown body. The body auto-merges only when the base version's lines appear, in order, as a subsequence of both ours and theirs — i.e. every original line is still present and in its original relative order on each side. That is exactly the signature of "only insertions happened." If any base line went missing or moved on either side (a deletion, an in-place edit, a reorder), the subsequence check fails and the body conflicts.

Everything is compared as opaque lines — a table row, a bullet, and a heading are all "just a line." There is no markdown parsing, no "merge these two bullets by their leading text," no heading-dedup-by-slug. Every smarter heuristic that was considered introduced a case where two different additions silently collapsed into one, so the driver deliberately stays dumb and literal. Blank lines always pass through (they are meaningful paragraph separators in markdown).

Which files it applies to

The driver is wired up in .gitattributes (the merge=mddoc attribute). As of 2026-05-31 a single catch-all covers EVERY memory document:

  • .claude/memory/**/*.md — every hand-edited memory doc, at any depth
  • docs/llm-library/*.md — this library's index and all its doc bodies, plus docs/developing/DEVELOPER_GUIDE.md (INDEX 2026-05-30; bodies + guide 2026-08-01 — additive prose, so two branches' disjoint edits union and a same-section change still falls back to conflict markers)

This replaced an earlier hand-maintained prefix list (gotchas-*, postmortems/*, feedback_*, project_*, reference_*, contracts/*-contract.md) that silently missed any file whose name matched none of those patterns — about half the memory corpus, including hubs like agent-test-concurrency.md, data-model.md, process-management.md, ipc-contracts.md, and the frontend-* / testing-* / patterns-* families. Those uncovered files fell back to Git's default text merge and hard-conflicted on the simplest two-branch append, which aborted a whole "Full Local Git" batch land and forced a manual rebase+resolve of the offending branch. The catch-all closes that gap; a lint test (tests/unit/lint/memory-merge-driver-coverage.test.ts) now fails if any memory file is ever left uncovered.

The generated indexes — MEMORY.md, and the contracts-index* / postmortems-index* / postmortems-archive* catalogs — sit below the catch-all in .gitattributes and override it (last match wins) to dedicated drivers (memory-index, contracts-index, postmortems-index) that resolve a merge by combining the rows already present in both branches' copies of the index and re-rendering them in canonical order — not by rebuilding from the source files. (At merge time the incoming branch's newly-added source file isn't on disk yet, so a rebuild would silently drop its row; reading both sides' already-rendered indexes recovers it.) The contracts and postmortems catalogs are each a hub + per-domain files (e.g. contracts-index-<domain>.md), so their driver routes by pathname: a per-domain file unions its rows, a hub recomputes each domain's count (guarded against a degenerate / wiped ancestor so the arithmetic can never inflate — see the auto-lander contract's S25). In the normal case they resolve rather than conflict — but "never" is too strong: safe-merge keeps an explicit --ours fallback for each of these paths, used only when git has actually left the file unmerged, so do not treat that handling as dead code. mddoc is only for files a human or agent edits directly.

Because a hub's count is derived (merged by count arithmetic, not by re-reading the row sets), a from-disk re-settle is the authoritative backstop once the merge completes and the source files are materialized: .husky/post-merge regenerates the catalogs after a git merge only in the master sync's regen-only mode (an ordinary merge or pull regenerates nothing), and — since the auto-lander lands in a HOOK-LESS recovery worktree (mostly by cherry-pick; a clean-merge branch whose replay would diverge lands by git merge --no-ff instead — but that recovery worktree has no .husky/, so post-merge never fires on EITHER path) — the lander runs the equivalent settle after every land (land-index-settle.ts, auto-lander contract S25). Without that lander settle the contracts-index hub count silently doubled against a wiped ancestor on every contract-touching land (811 → 1606 → …) and contracts-index-fresh stayed red; with it, a committed hub always matches a fresh regen.

CLAUDE.md is deliberately not registered. It lives at the repository root, not under .claude/memory/, so the catch-all above never reaches it. It has structural rules — table-row "contracts," an ordered Frontend / Backend / Security section layout — where a naive additive union could interleave two edits into something subtly wrong. It also carries a hard CI size budget — 10,000 tokens for the whole always-loaded rule set, with an 8,000-token target. The budget counts tokens, not lines: what is measured is the text a session actually loads (CLAUDE.md plus every file under .claude/rules/), so a big-print file and a dense one are not treated alike. Because an additive union keeps both branches' new rules, two branches each adding bullets could quietly push it over the cap and turn a safe conflict into a failed build. For now CLAUDE.md keeps Git's default text merge and the safe-merge workflow's hand-resolve flow.

Files larger than 1 MiB also skip the driver and fall back to Git's default merge, to avoid pathological memory use if an oversized file is ever registered by accident.

Frontmatter: when two branches both add a field

The rule above — "if both sides changed the frontmatter, conflict" — was too blunt in the one case that happens most. Contracts carry small metadata fields (system:, subsystem:, roadmap:), and a repo-wide re-classification means dozens of branches each adding a field to the same file.

So the frontmatter now merges when both sides only ADDED keys — even the same key with the same value, added in different places, collapses to a single line. It still refuses when the two sides give the same key different values: only a human knows which value is right.

Field by field (2026-09-23). One side editing a field while the other adds a different one used to conflict too. Measured on three postmortem files: master bumped status:, recurrences: and last_updated: while a branch added symptom: — plain git merges that cleanly, but the driver conflicted the whole frontmatter and the auto-lander bounced the branch. The field merge now handles it: a field only one side changed is taken from that side, and a field only one side added is kept where that side put it. It still conflicts when both sides change the same field differently, when each side rewrote a different field that was already there (metadata fields are coupled — system and subsystem, say — and only an author knows whether two edits agree), or when a side deletes or reorders fields.

There is a second, sharper reason this matters. When the driver used to decline a frontmatter merge, it handed the file to Git's plain text merge — which has no idea that frontmatter is a list of fields rather than a list of lines. Given two additions at different spots it kept both and reported success. On 2026-09-05 that produced a contract with two system: lines from a merge that looked perfectly clean; nothing noticed until an unrelated tool crashed trying to read it, hours later and in a different place. A refused frontmatter merge now writes real conflict markers itself, so "clean" always means clean. A repo-wide check also fails the build if any memory doc, contract, or postmortem ever ends up with a repeated field.

Frontmatter: the four stamps a machine writes

Most header fields are written by a person — a title, a status, a roadmap: anchor — and when two branches set one of those differently, only a person knows which is right, so the driver still stops. Four fields are different, because nobody types them:

  • last_updated — the date a commit hook stamps on every commit that touches the doc.
  • tokens — a size estimate the same hook recomputes from the text.
  • last_incident and recurrences — a postmortem's most recent incident date and how many times it has happened.

Two branches that touched one doc on different days disagree on these nearly every time, and until 2026-09-23 the driver treated that like a real disagreement: the whole doc conflicted even when the prose merged cleanly. In the week before the change, 68 of the 90 doc conflicts the driver refused were fights over these lines and nothing else.

So the driver now works them out instead of asking:

  • the later of the two dates;
  • the incident count as the starting count plus each side's increase — two branches that each record one new incident end two above where they started, while the same increase arriving from both sides (a replayed commit) counts once;
  • the size re-counted from the merged text — never either side's stamp, since each side counted only its own text — and only once that text itself merged cleanly.

It still refuses — ordinary conflict markers, a person decides — whenever the answer is not mechanical: a count one side lowered, a stamp one side deleted while the other changed it, or a value that does not read as a date or a whole number. Every hand-written field keeps the old rule too, including one branch editing an existing field while the other adds a new one: the owner kept that case stopping for a person on 2026-09-23, to revisit after the weekly merge-health report. Setting AMC_DISABLE_MDDOC_COMPUTED_FIELDS=1 puts the four stamps back on the old rule — for bisecting one suspect merge, never as a way of working.

Generated files get regenerated, not merged

Two documents describe how the app throttles its own background work: a full generated table (agent-load-governance-registry.md) and a hand-written map with one generated table inside it (performance-gating-map.md). Both are produced from a single source — the registry, which is one file per entry under scripts/lib/agent-load-governance-registry/, assembled by the generated collector beside it.

They used to merge like ordinary prose, which is wrong for generated content in a specific way: when two branches each regenerate the same table, the right answer is neither branch's version — it is a fresh render from the combined source. A text merge just sees two rewrites of the same rows and conflicts on every one. On 2026-09-05 that was a large share of a 71-conflict rebase where the correct resolution, every single time, was "re-run the generator."

They now have their own driver that does exactly that: it re-renders the fully generated document from source, and for the mixed one it merges the human prose normally while re-rendering just the generated block inside it. A genuine disagreement in the prose still conflicts for a human, because nothing can regenerate someone's sentence. If the driver cannot run for any reason it steps aside rather than failing — a merge driver that crashes makes Git report a conflict that does not exist, which would bounce a perfectly clean branch out of the automatic merge queue.

The registry's collector, index.generated.mjs, gets its own driver, load-governance-registry, suited to a list. It holds one import line and one array entry per governor file, in id order, so two branches that each add a governor whose names sort side by side insert at the same spot, and a text merge conflicts even though the answer is simply "keep both". The driver reads the list of entries out of each side, keeps every entry either side added, drops every entry either side removed, and writes a fresh render of the result. It never rebuilds the list from the entry files on disk: while Git merges, the other branch's new files are not in the folder yet, so a rebuild would silently drop them. A side that is not an exact generated copy — a hand edit, or a file cut short — goes to Git's ordinary text merge instead, the merge this file got before it had a driver, so a damaged side can never be read as "every governor was deleted".

Generated files and registries: the structural merge rules (2026-09-27)

Four more rules sit beside this driver, for the conflict shapes that stopped the most sessions in a scan of 664 stopped sessions over eight days:

  • A file the land rebuilds keeps one side. The alert catalog pair, the guard registry and the help-site analytics script are re-rendered by the auto-lander after every land, so a merge keeps master's copy and moves on (the settle-owned rule). Only a file whose name or header says it is generated qualifies; a rebuilt file with no such header keeps a listed exemption until its generator writes one, because the rebase check would otherwise report the dropped side as lost work.
  • A front door's generated table merges row by row. When two branches each re-render the generated block of a front door, its rows merge by their link, so both branches' new rows survive; the hand-written part merges exactly as described above.
  • Hand-written registries merge entry by entry. Lists every branch appends to — the lander switches, the alert type table, the alert key sets, a guard's allow-list — carry a // @merge-keyed-list line above their declaration. Two branches that each add a different entry both keep theirs, the same entry changed two ways still conflicts, and anything the rule cannot prove gets git's ordinary merge. Off switch: AMC_DISABLE_KEYED_LIST_MERGE=1.
  • Every conflict shows the original. The installer pins merge.conflictStyle to zdiff3, so each conflict carries the base text between the two sides, and the auto-lander restores the setting if a rebuilt config ever loses it.

A new generated file has to arrive with one of these rules or a reasoned exemption — a build check refuses it otherwise. The rules themselves are in the structural merge rules contract.

When the fallback itself cannot run

"Hands the file back to Git" means the driver shells out to git merge-file, which is what actually writes the <<<<<<< markers. On a fork-starved box that child can fail to start — the same process-exhaustion storm the driver family already retries around. Until 2026-08-26 the driver then reported a conflict while leaving the file byte-identical to your side, with no markers in it at all: Git left the path unmerged, but anyone opening the file saw something that looked cleanly merged, staged it, and silently dropped the incoming branch’s changes. All four dedicated drivers now share one fallback that writes the markers itself when git merge-file never ran, so a "conflict" always means there is genuinely something to resolve in the file.

The two lists that must agree

.gitattributes says WHICH driver a path uses; a separate installer (scripts/setup-md-merge-driver.mjs) REGISTERS what each driver name actually runs. Git does not check that those two lists match — an attribute naming a driver nobody registered silently falls back to the plain text merge, with no warning anywhere. A lint now compares the two sets in both directions, so a renamed or forgotten driver fails the build instead of quietly turning the protection off.

How it gets installed

The driver is a small TypeScript program. Two pieces have to be in place for Git to use it:

  1. The .gitattributes rule — committed to the repo, so it travels with every clone. This says "for these paths, use the merge driver named mddoc."
  2. A local Git config entry that maps the name mddoc to the actual command. This is per-clone and lives in .git/config (it is not, and cannot be, committed — Git deliberately ignores committed driver definitions for security).

Step 2 is handled automatically by an idempotent installer, scripts/setup-md-merge-driver.mjs, which runs:

  • as part of npm install (a postinstall hook in package.json), and
  • as a fallback step inside the repo's safe-merge skill, so the driver self-heals if a clone never ran a full install.

It registers each driver as a bounded sh retry loop wrapping node --import "file://<main-checkout>/scripts/lib/node-ts-register.mjs" scripts/md-merge-driver.ts %O %A %B %P — the repo's shared TypeScript preload, which runs the driver on Node's own type stripping (see below). The four %-tokens are how Git passes the base / ours / theirs file paths and the file's pathname to the program. Three robustness details matter:

  • node --import + the absolute shared preload, not a bare tsx and not a shim. Git runs merge drivers with the ambient PATH of whatever triggered the merge — merge-all-ready's recon probe, a cherry-pick, a plain git merge — so nothing but node can be assumed on PATH (a bare tsx died there with command not found). An absolute file:// URL to the main checkout's preload resolves from any working directory, including a recovery worktree or an older commit replayed during a land that has no preload of its own. The launcher has moved three times, each for a measured failure: the node_modules/.bin/tsx shim was banned after an incident class wiped the whole .bin/ directory while the packages survived (2026-07-04, every driver a permanent not found); the tsx CLI gave way to tsx's in-process --import loader because it forked a child node per driver per file, which EAGAIN-failed or was OOM-killed under load (2026-07-31); and since 2026-09-27 every driver starts on the shared preload (scripts/lib/node-ts-register.mjs), which runs TypeScript on Node's own type stripping — one process, no loader thread, no esbuild, and no listing of tsx's shared cache folder, which had grown past a million files and stalled merges (2026-09-24). The absolute URL's space (in "Agent Orchestrator") is %20-encoded, which Git preserves through its %O/%A/%B/%P placeholder expansion.
  • Spawn-retry resilience. When the machine is under heavy load (many parallel worktrees / processes), the OS can momentarily refuse to start a new process — even loader mode's single node can fail with "Resource temporarily unavailable" before the driver runs (loader mode makes this far rarer than the old CLI's double-fork, but not impossible). Git cannot tell "the driver could not start" apart from "the files genuinely conflict", so without help a momentary hiccup becomes a phantom conflict that bounces an otherwise-clean branch out of the auto-lander. The retry loop catches exactly that: a spawn-level failure (the shell could not run the driver at all) is retried a few times — it succeeds the instant a slot frees — while a genuine non-zero result (the driver ran and reported a real conflict) is passed straight through, never retried. The loop is bounded, so sustained exhaustion still gives up cleanly.
  • The auto-lander verifies driver health before landing. Its per-repo health gate existence-checks every registered driver's launcher file (the preload, or tsx's loader on an older registration), re-runs this installer automatically when one is missing, and — only if that self-heal doesn't take — pauses landing for the repo with an inbox alert instead of bouncing clean branches as fake conflicts. The same gate is how the preload reached every machine without anyone running npm install: a registration still on tsx's loader triggers one re-run of the installer (another only while each run makes progress). Since tsx left the dependencies (2026-09-28) that loader file is missing, so such a registration is a broken launcher like any other.

Because all worktrees of one clone share a single .git/config, installing once registers the driver for every worktree of that clone — you don't re-run it per worktree.

Pre-built bundles: why node --import is the fallback, not the fast path

Every registration is a two-armed guard:

if [ -f scripts/generated/merge-drivers/<name>.mjs ]; then
  AMC_MERGE_DRIVER_TSX="<preload url>" node scripts/generated/merge-drivers/<name>.mjs %O %A %B %P
else
  node --import "<preload url>" scripts/<driver>.ts %O %A %B %P
fi

Git runs a merge driver once per conflicted file, and a TypeScript launch is paid per invocation. Under tsx's loader it registered a module hook and transpiled the driver plus its whole relative import graph, with no cache — measured on the owner's box 2026-09-17, same fixture, real --merge mode: 18.14 s per per-domain index shard under tsx against 0.36 s for a pre-built bundle, with node -e 0 at 0.15 s and tsx startup alone at ~16 s. The shared preload that replaced tsx (2026-09-27) is far cheaper, and a plain bundle is cheaper still. A rebase of a memory-doc branch conflicts on a dozen or more regenerated index shards at once, which is how a 5- and a 10-minute tool budget both blew inside the first few files.

So scripts/build-merge-drivers.mjs (npm run merge-drivers:build) pre-bundles each qualifying driver with esbuild into a plain committed .mjs under scripts/generated/merge-drivers/. Three properties matter:

  • The bundle is a plain node program, so the common arm costs process startup only. It is committed rather than built on demand because the auto-lander merges inside a recovery worktree with no node_modules — and because the bundle a worktree runs then comes from the same commit as the sources in that worktree.
  • Staleness is FAIL-OPEN. Each bundle records the sha256 of every source that went into it and re-checks them at startup; on any mismatch it runs the TypeScript source instead. It never exits non-zero for being stale — git records a failing driver as a CONFLICT, which is the class that wedged the auto-lander three times (2026-06-25 / 07-04 / 07-31).
  • The bundle graph must stay inside scripts/ (BUNDLE_SOURCE_ROOT). A bundle hashes every file it inlined, so each becomes a file somebody must rebuild after touching — fair for merge-driver infrastructure, unfair for a high-churn shared utility like src/shared/utils.ts. A driver that grows such an import silently drops back to the TypeScript arm.

bundleEntry — bundling from a file that is not the registered script

The two memory-doc index drivers (postmortems-index, contracts-index) could not be bundled as written: the generators reach src/shared/utils.ts and src/shared/token-estimate.ts, which the size-and-scope rule refuses. Only their corpus scan needs those, so each driver's merge surface was split into scripts/lib/ and given its own bundle entry.

That splits the registration from the thing it runs, and the registration must NOT be re-pointed at the new entry: .git/config is shared by every worktree of the clone, so a new path would make an older commit replayed during a land resolve to a file that does not exist in its tree — a driver that cannot start, which git records as a phantom conflict. Instead an entry is declared as bundleEntry in scripts/setup-md-merge-driver.mjs: the launcher string and the bundle's FILENAME stay exactly as they were, and only the file the bundle is built from changes. An old tree with no bundle still takes the else arm to a script that exists there.

  • contracts-index runs entirely in-process: neither its shard path (a row union re-rendered through the shared renderer) nor its hub (count arithmetic over the two sides) scans the corpus.
  • postmortems-index resolves a per-domain shard in-process and delegates the hub — one or two files of the family — to the generator through the same spawn the bundle already uses for staleness, because a hub's count is DERIVED from the whole corpus. Nothing approximates it.

Both bundles are rebuilt by npm run merge-drivers:build after any change to the modules they inline, and tests/unit/lint/merge-driver-build-freshness.test.ts fails if a bundle no longer matches its sources. Never hand-edit a file under scripts/generated/merge-drivers/.

Verifying it's active

git config --local --get merge.mddoc.driver

prints a bounded sh retry loop wrapping node --import + an absolute file:// URL to the main checkout's shared preload, e.g.:

n=0; until if [ -f scripts/generated/merge-drivers/md-merge-driver.mjs ]; then AMC_MERGE_DRIVER_TSX="file://<main>/scripts/lib/node-ts-register.mjs" node scripts/generated/merge-drivers/md-merge-driver.mjs %O %A %B %P; else node --import "file://<main>/scripts/lib/node-ts-register.mjs" scripts/md-merge-driver.ts %O %A %B %P; fi; do … done

If it prints nothing — or prints tsx's loader .../node_modules/tsx/dist/loader.mjs (pre-2026-09-27), a bare tsx scripts/..., any node_modules/.bin/tsx form (pre-2026-07-04), the tsx CLI .../tsx/dist/cli.mjs form (pre-2026-07-31), or an unwrapped value with no retry loop (pre-2026-06-25) — run node scripts/setup-md-merge-driver.mjs from the repo root (or just npm install) to re-register the current value. tsx is no longer installed (since 2026-09-28), so a tsx-shaped value fails as soon as a merge needs the TypeScript source rather than the pre-built bundle, and git reads that file as a conflict. The auto-lander also re-registers automatically when its health gate finds a broken launcher.

Since 2026-09-18 the installer also checks the OTHER direction for you, on every run. After it has written the drivers it reads the names .gitattributes attributes and warns, naming them, for any this clone has no merge.<name>.driver for — a warning only, never a failure, so npm install still exits 0. That line is the sole signal for this failure mode, because git does not error on an attribute pointing at an unconfigured driver: no log line, no exit code, no marker, just its default text merge. Silence from it means every attributed driver is registered. (It says so explicitly when git config --local is unusable for the checkout, rather than implying coverage it could not check.)

Debugging a single merge by hand

You can invoke the driver directly against three files:

node --import ./scripts/lib/node-ts-register.mjs scripts/md-merge-driver.ts <base> <ours> <theirs> <pathname>

It overwrites <ours> with the merged result and exits 0 on a clean merge or non-zero on a conflict — the same exit code Git itself honors.

Relationship to the merge workflows

This driver is a low-level assist that sits underneath the repo's higher-level merge tooling (the safe-merge skill, the merge-all-ready batch orchestrator, and the PR merge queue). Those workflows still run; the driver just removes a class of spurious conflicts they would otherwise have surfaced. In particular, merge-all-ready's recon step trial-merges each ready branch with git merge-tree, which honors this driver — so a branch whose only conflicts are in the registered additive files is reported "clean" and auto-lands, while a branch with a real code/prose clash is held back for a human. The driver changes nothing about which branches get merged, by whom, or under what approval — only the outcome of the file-level three-way merge for the registered documents.

For agents

What lives where (for repo-access agents)

  • Merge logic — scripts/md-merge-driver.ts. The pure function mergeMarkdown(base, ours, theirs) returns { merged: string | null } (null means "conflict"); runCli(argv) is the only part that touches the filesystem (read the three files, merge, write the result or fall back to git merge-file).
  • Installer — scripts/setup-md-merge-driver.mjs. Plain .mjs (not TypeScript) so it can run before npm install has.
  • Registration — .gitattributes.
  • Tests — tests/unit/scripts/md-merge-driver.test.ts, backed by fixture folders under tests/unit/scripts/md-merge-driver/fixtures/ (each holds base.md + ours.md + theirs.md and either an expected.md for an auto-merge case or an expected-conflict sentinel for a must-conflict case). The 1 MiB size-cap boundary has its own file, tests/unit/scripts/md-merge-driver-size-cap.test.ts — run both when you change the driver.
  • The contract — md-merge-driver-contract.md is the present-tense engineering source of truth: the exact region cascade, the test-locked invariants (its whole "Invariants — each locked by a test" section — read to the end of it, not to a remembered number), and the checklist for changing the driver without regressing it. Read it before touching the merge logic.

Related

  • land-on-local-master.md — how finished branches reach master, and where these merges actually run.
  • git-guardrails.md — the rules that keep shared branches safe around those merges.
  • agent-lanes.md — why so many agents are editing the same documents at once.

Last verified 2026-09-28