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/juz0rollouts installed on all 18 fleet repos.Verified end-to-end against
@unbrained/pm-cli2026.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 itemspm-cli-website-zy8dandpm-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.toonThis 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:
oursis 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 withpm 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.