Multi-Agent Merge Safety — Canonical Workflow

Hub doc. This is the pm-ecosystem reference for how multiple agents work the same tracker on different git branches and merge without conflicts or history corruption. It is the "manual" for the field-aware merge driver that the 5cx1 / xydx / juz0 rollouts installed on all 18 fleet repos.

Verified end-to-end against @unbrained/pm-cli 2026.8.3 on 2026-08-04 (previously 2026.7.22 on 2026-07-22) — both the disjoint-edit path and the contended-field path in §3b. Tracked by companion items pm-cli-website-zy8d and pm-cli-website-m1mm (related: 7pnx, juz0).

Philosophy: project management = context management. A tracker that corrupts when two agents touch it in parallel is worse than no tracker — it destroys the shared context it exists to protect. The workflow below guarantees that two agents editing the same item on two branches lose zero work on merge.


The moving parts (already installed fleet-wide)

Every public fleet repo ships two things, committed to the repo so every branch and clone inherits them:

Artifact Role
.gitattributes Maps .agents/pm/** history/item/relationship/json files to the pm merge drivers. Inert on its own — needs the local git config below.
scripts/prepare-merge-driver.mjs npm prepare lifecycle hook. On install/clone, if pm is resolvable on PATH, runs pm merge install to wire the drivers into this clone's local git config. Node (not shell) so it behaves identically on POSIX and Windows cmd.exe. Absent pm ⇒ silent skip; present ⇒ fail-loud.

git config merge.* is per-clone, so each fresh clone / worktree runs the prepare hook once (automatic on npm install). Custom item types registered later refresh the .gitattributes fence automatically.


The four-step workflow

1. Install the driver (automatic, or once per clone)

pm merge install        # done for you by scripts/prepare-merge-driver.mjs on npm install
git add -A && git commit -m "chore: wire field-aware merge driver"   # commit .gitattributes

2. Agents work concurrently on branches

Each agent uses its own --author so history attribution stays clean:

# agent-a on branch agent-a
pm --author agent-a notes SHARED-ID --add "note from agent A"
pm --author agent-a update SHARED-ID --priority 1

# agent-b on branch agent-b (branched from the same base)
pm --author agent-b notes SHARED-ID --add "note from agent B"
pm --author agent-b update SHARED-ID --assignee agent-b

3. Merge — the driver unions, no conflicts

git merge agent-a -m "merge a into b"

With the driver active this completes cleanly (exit 0, zero UU conflicts). The driver output shows the field-level resolution:

ok: true
artifact: "item"
item:
  fields_from_theirs:
    - "priority"      # scalar fields: last-writer / prefer side
  union_fields:
    - "notes"         # append-only fields: unioned, never dropped
ok: true
artifact: "history"   # history streams merged too

Contrast — without the driver the same merge fails:

CONFLICT (content): Merge conflict in .agents/pm/history/SHARED-ID.jsonl
CONFLICT (content): Merge conflict in .agents/pm/issues/SHARED-ID.toon

This is exactly the corruption the driver prevents. Never resolve pm conflict markers by hand — reset and let the driver run.

3b. The case that does conflict, and is supposed to

"No conflicts" above means disjoint field edits. If two agents set the same scalar field to different values, the driver deliberately does not invent a winner — that would be silent data loss dressed up as a clean merge. Verified on 2026-08-04 against 2026.8.3: both agents changed priority and title on one item, and the merge stopped:

Auto-merging .agents/pm/features/<id>.toon
CONFLICT (content): Merge conflict in .agents/pm/features/<id>.toon
guidance:
  - "Both branches changed priority, title; the ours value was kept.
     Review the merged file, re-apply the losing change if needed, then git add it."

Three things stay true even through the conflict, and they are the guarantee:

  • ours is retained — the file is left in a valid, parseable state, not smeared with conflict markers.
  • Append-only fields still union. Both agents' notes survived; nothing in the history stream was dropped.
  • The losing values are recoverable, because the driver wrote a receipt.

The receipt is the part agents miss, because the guidance above does not name it (filed upstream as pm-cli#889). It is pm merge report, and it is the command to run before resolving:

$ pm merge report
    preferred: "ours"
    union_fields: [ "notes" ]
    decisions:
      - field: "priority"
        base: 3     ours: 1     theirs: 0
        retained: 1              discarded: 0
      - field: "title"
        retained: "…as agent B renamed it"
        discarded: "…as agent A renamed it"
    state: "pending"

So the workflow for a contended field is: run pm merge report, decide which value should win on the merits rather than on merge order, pm update the item if the discarded side was right, then git add and commit. Use pm merge driver --prefer theirs only when you already know the incoming branch should win every tie — it is a policy, not a resolution.

4. Reconcile residual history drift — the step you must not skip

The driver correctly unions the item and history streams, but the merged item's current-state hash can still diverge from the latest history-chain hash. Immediately after a merge, pm history --verify / pm validate may be red:

pm history --verify SHARED-ID
  errors:
    - "verify_failed:current_item_hash_mismatch"
pm validate
  - "validate_history_drift_hash_mismatches:1"

Fix it in one audited command (pm merge reconcile, GH-623, new in 2026.7.22):

pm merge reconcile --dry-run                 # preview affected streams first
pm merge reconcile --message "reconcile after agent-a/agent-b merge"

After reconcile, both are green and no data is lost:

pm history --verify SHARED-ID
  ok: true
  current_matches_latest: true
pm validate                                  # only benign metadata warnings remain
pm show SHARED-ID --depth full
  notes: [ "note from agent A", "note from agent B" ]   # BOTH survive
  priority: 1        (from agent-a)
  assignee: agent-b  (from agent-b)                      # every field preserved

Then commit — always include the sibling history/<id>.jsonl:

git add -A && git commit -m "merge: reconcile SHARED-ID history"

⚠️ Reading the reconcile output correctly

pm merge reconcile reports things like:

reconciliation:
  reverted_fields: [ "notes" ]
  discarded_events:
    - author: "agent-b"   op: "note_add"
  discarded_authors: [ "agent-b" ]
warnings:
  - "history_repair_discarded_authors:agent-b"

This does not mean agent-b's work was lost. discarded_events / discarded_authors refer to redundant replayed history-log events, not item content. Because the driver already unioned the field in step 3, the on-disk item still holds agent-b's note (verified above: count = 2). Reconcile keeps the on-disk item as authoritative and rewrites the history chain to match it.

The alarming phrasing is a known agent-UX rough edge — filed upstream as an improvement so reconcile distinguishes net-lossless replayed-event discards (content still present in the item) from net-lossy ones. Until then: trust the item, not the discarded_* labels — confirm with pm show <id> --depth full.


Copy-paste checklist for an agent about to merge

git merge <other-branch>                 # 1. driver unions, expect exit 0
pm merge reconcile --dry-run             # 2. preview drift
pm merge reconcile --message "..."       # 3. apply
pm history --verify <id>                 # 4. expect ok:true / current_matches_latest:true
pm validate                              #    expect no *_hash_mismatch warnings
pm show <id> --depth full                # 5. confirm your + their content present
git add -A && git commit                 # 6. commit item + history/<id>.jsonl together

If git merge ever reports CONFLICT in .agents/pm/**, the driver is not wired in this clone → run pm merge install, git merge --abort, retry.


Multi Agent Merge local
Ein Problem melden