Multi-Branch Tracker Merge Safety
Tracked by pm-wc1r, with the integrity and concurrency fixes pm-9q2t, pm-cxyv, pm-gpo7, pm-m3nl, pm-wwfd, and pm-xdn6. Fresh-init fence ownership is tracked by pm-1w3ljt; runtime-cache index governance by pm-hous; local allocation safety by pm-khdq; fence-coverage completeness and drift detection by pm-i4fx; package-defined item coverage by pm-5rexki; non-item JSON coverage by pm-gjicmx; portable driver identity by pm-w91mvg; pending receipt validation by pm-ysqb6n; receipt classification by pm-jtwsct; direction-independent item conflict selection by pm-dlx7v7; corrected reconciliation guidance by pm-lwmstb; cross-branch id collision safety by pm-pibw; auditable merge history by pm-9j2r3b; durable conflict decisions by pm-rh98vo; continuous conformance by pm-76dnfg; workspace-wide CI enforcement by pm-pdr8t1; post-merge reconciliation by pm-mfkv92; this repository's own adoption by pm-iwsj.
pm stores project context as reviewable repository files. Concurrent agents can therefore use ordinary branches and worktrees, but tracker artifacts need semantic merge behavior: raw line merging cannot preserve TOON collection counts, JSON object structure, or append-only history hash chains.
Install the repository merge contract
Fresh pm init runs this automatically when the tracker is inside a Git
worktree. It writes the shared fence and configures the current clone before
returning success. Use pm init --no-merge-fence only when another system
deliberately owns Git merge configuration.
Existing trackers and fresh clones can install or repair the contract directly:
pm merge install
git add .gitattributes
git commit -m "chore(pm): install tracker merge drivers"
pm merge install writes an idempotent, fenced .gitattributes block and repository-local git config entries. The attributes are committed; the driver commands are clone-local, so every collaborator and fresh CI clone that performs merges must run the install command.
The clone-local driver values record the absolute Node executable and bundled
dist/cli.js path resolved by the installing SDK. Git therefore does not depend
on a bare pm command or the caller's later PATH when it merges tracker data.
The item-path placeholder is stored as bare %P: Git performs the required
shell quoting when it expands the placeholder. Receipt ingestion also removes
one legacy matching quote pair so receipts written by older clone-local drivers
remain reconcilable.
pm validate --check-storage-integrity and pm health also compare every
clone-local driver definition with the installed SDK. Exact commands remain the
fast path. A command installed by another valid @unbrained/pm-cli package is
also accepted when its Node and dist/cli.js paths exist, its manifest owns the
pm bin, and the driver arguments are semantically identical. This keeps
copied worktrees and upgraded global installs healthy without accepting an
arbitrary executable. A missing, malformed, or semantically stale definition is
reported even when the committed attribute fence is correct.
The installer publishes the shared .gitattributes fence only after the clone-local driver commands are configured. If the repository Git config is read-only or another Git process holds its lock, the command returns the stable merge_git_config_unwritable error with recovery guidance and leaves an absent fence absent. Use pm merge install --dry-run --json to inspect the contract in intentionally read-only workspaces.
Fence coverage contract
The fenced block is generated by one shared coverage contract (buildMergeAttributePatterns) covering every mergeable artifact class: tracker-wide **/*.toon and **/*.md item patterns, explicit per-type diagnostic patterns, all tracker **/*.jsonl paths under the relationship-event driver with the more-specific history/*.jsonl rule overriding history streams, root settings.json, and every nested **/*.json document. The tracker-wide item rules protect extension-defined type folders even when a receiving branch has not installed the package that registered them; explicit type rows still make active-registry drift diagnosable. The broad JSON rules automatically cover schema, managed-extension, evaluation-corpus, and future package-owned authoritative JSON without another hand-maintained allowlist. The broad JSONL rule is required because RelationshipEventStore supports package-owned custom relative paths anywhere below the tracker root. Two mechanisms keep the committed fence from drifting out of that contract:
pm schema add-type/pm schema remove-typerefresh the fence automatically when it is installed (warningmerge_fence_refreshed); repos inside git that never installed it get the actionablemerge_fence_not_installedhint instead.pm validate(thestorage_integritycheck) audits the committed fence against the active schema's type folders and reportsvalidate_merge_fence_driftwith the exact missing/stale attribute lines when they diverge — for example after editing type folders out-of-band. Rerunpm merge installand commit.gitattributesto clear it.
Preview without mutation:
pm merge install --dry-run --json
Artifact semantics
| Artifact | Driver | Merge behavior |
|---|---|---|
Item .toon / .md |
pm-item-toon / pm-item-markdown |
Three-way field merge; append-like collections use set union, updated_at uses latest timestamp, scalar conflicts select a stable direction-independent value, and canonical serialization recomputes TOON counts. |
history/*.jsonl |
pm-history |
Preserves the common prefix and both divergent suffixes, orders deterministically, then re-anchors the resulting hash chain. |
tracker **/*.jsonl except the later history/*.jsonl override |
pm-relationship |
Covers default and package-owned custom relationship event paths, unions divergent suffixes by eventId (timestamp-ordered, ours-first on ties), and renumbers sequence consecutively so the strict-sequence store loader accepts the merged stream. |
root settings.json and nested **/*.json |
pm-json |
Recursively merges objects per key. Arrays compose when both branches preserve the base and add distinct entries, so independent extension installs and evaluation additions merge without weakening edit/removal conflict detection. |
When both sides change the same item scalar differently, the driver writes the same stable value regardless of which branch Git labels ours or theirs, but exits nonzero. JSON leaf conflicts retain the explicit preferred-side policy. Git keeps either path conflicted so a human or coordinating agent must review the discarded value and explicitly git add the resolution.
The driver result's guidance always points unresolved conflicts to pm merge report. When a clone-local receipt exists, guidance includes its privacy-safe receipt and item ids for exact correlation; discarded values remain confined to the local receipt and never appear in generic logs or tracker history. Tracked by pm-fbrz7p.
For item conflicts, the driver writes a clone-local receipt below the Git
directory and a durable privacy-safe sidecar below merge-receipts/ in the
tracker. The local receipt contains retained and discarded values so recovery
does not depend on a reflog. The tracked sidecar contains only field names and
value hashes, so fresh clones and CI can still fail closed on an unreviewed
decision without publishing either value. When both copies exist the SDK
deduplicates them and prefers the locally recoverable copy:
pm merge report
pm merge report --include-reconciled
The underlying public SDK exports are mergeItemDocuments, mergeHistoryStreams, mergeRelationshipEventStreams, mergeJsonDocuments, runMergeDriver, runMergeInstall, installMergeFence, findGitWorkspaceRoot, runMergeReconcile, runMergeReceiptReport, listMergeReceipts, auditMergeDriverConfiguration, refreshMergeAttributeFenceIfInstalled, buildMergeAttributePatterns, and auditMergeAttributeFence from @unbrained/pm-cli/sdk. installMergeFence accepts explicit tracker and workspace roots, so custom init hosts do not depend on process cwd or CLI globals.
Cross-branch id collision safety
Item ids are <prefix> plus random base36 characters, and uniqueness is only probed against the local working tree — two agents branching from the same commit can mint the same id for different items (GH-600 / pm-pibw). Two controls bound that risk:
- Entropy budget —
ids.token_lengthinsettings.json(default 4, accepted range 4–12) sets the random token length for newly minted ids. Approximate 1%-birthday-collision workloads per length: 4 chars ≈ 1.68M ids (184 concurrent unsynced creations), 6 chars ≈ 2.18B (6.6k), 8 chars ≈ 2.8T (~238k). Multi-agent repositories that fan out many branches between merges should raise it, e.g.pm config project set ids_token_length 6. - Post-merge detection — the
storage_integrityvalidate check reportsvalidate_storage_duplicate_item_idswhenever one id is claimed by multiple item documents (across type folders or format variants), which is how a same-id/different-item merge materializes. Remediation: keep one document, recreate the other item under a fresh id (pm copythen delete the colliding file), and re-point any dependencies.
Within one working tree, create and copy serialize on the candidate id and
recheck every built-in and extension-defined type folder before the
authoritative write. A raced collision fails with item_id_collision instead
of replacing the existing document.
Required post-merge gate
After every branch merge that touches .agents/pm, run:
pm merge reconcile --dry-run --json
pm merge reconcile --message "Reconcile merged tracker histories" --json
# Required only after every discarded field has been reviewed or re-applied:
pm merge reconcile --force --message "Accept reviewed merge decisions" --json
The preview reports every drifted stream and pending receipt without mutation.
Lossless receipts are reported as provenance but do not become blocking merge
decisions. Receipts with discarded scalar values remain blocking and the apply
pass refuses them unless the coordinator explicitly supplies --force after
review. This prevents a routine history repair from silently accepting data
loss while preserving a zero-noise path for merges that retained both changes.
It exits nonzero while either merge-critical validation check is non-green, so
CI and explicit post-merge hooks cannot approve unresolved receipts or drift.
The apply pass uses the audited history rewrite boundary to append a
merge_reconcile event whose patch reproduces the merged item exactly. The
event includes privacy-safe receipt provenance; a clean receipt-bearing stream
gets a no-op merge event so the merge remains addressable even when replay
already matches. The command immediately validates history drift plus storage
integrity and exits nonzero when an invariant remains red. Default Git hooks
remain unchanged: repositories that want automatic enforcement must opt in by
invoking this command from their own post-merge hook.
The default validation surface includes storage_integrity. It fails on unreadable item documents, history conflict markers, malformed history tails, live items whose latest history operation is delete (a delete/modify resurrection candidate), and unparseable settings/schema files. This prevents the ordinary tolerant read path from turning corruption into a green gate.
A history union can be structurally valid while replayed history and the chosen item document describe different effective state. In that case inspect both authors' events before reconciliation, then run the audited repair:
pm history <item-id> --verify --strict-exit
pm history-repair <item-id> --dry-run
pm history-repair <item-id>
pm merge reconcile --dry-run --json
history-repair records the reconciliation patch and classifies its changed fields against the final item. Append-only collection unions and deterministic reordering are reported as preserved context without a data-loss warning. Fields whose replayed values are actually removed or replaced remain loud with discarded event authors/operations and recovery guidance. Re-apply any intended losing mutation as a normal pm update so it remains explicit and auditable.
Delete versus modify policy
A delete on one branch and an edit on another is not safely resolvable by a generic file driver. The merged workspace must not silently resurrect the item. storage_integrity reports the live document plus delete-terminated history as a hard finding. The coordinator chooses one policy explicitly:
- Honor deletion: remove the live item document, keep its append-only history, and commit the resolution.
- Honor modification: restore/recreate through pm so the history contains an explicit post-delete operation and rationale.
Never remove conflict markers manually while leaving the authoritative item and history semantically inconsistent.
Runtime receipts and retention
transactions/ and checkpoints/ are per-branch crash-recovery state, not shared project context. pm init places both below the resolved tracker root in the managed .gitignore block, including custom --pm-path roots.
Ignore rules do not remove files that were already committed. pm health
(integrity) and pm validate (storage_integrity) therefore scan Git's
index for tracked files below runtime/, search/, locks/,
transactions/, and checkpoints/. A finding includes repository-relative
paths plus an exact git rm --cached -r -- ... command. That command removes
only the indexed copies; local caches and recovery receipts remain on disk
under the managed ignore fence. The intentional
search/eval-queries.json relevance-evaluation corpus remains tracked and is
excluded from this cache diagnostic.
Terminal SDK transaction journals use the same checkpoints.retention_days policy as rollback checkpoints. The default GC sweep includes both receipt classes:
pm gc --dry-run
pm gc --scope transactions --dry-run
pm gc --scope transactions
Only aged committed or compensated journals are removed. applying, compensating, unreadable, and unparseable journals are always retained so cleanup cannot destroy live recovery state.
Temporary-clone acceptance test
Before releasing merge-contract changes, validate the packed package in a temporary Git repository: initialize pm, install the merge contract, create one base item, branch twice, append disjoint metadata/history on both branches, merge, and run the strict validation commands above. Include a same-key conflict case and prove Git leaves it unresolved while the output remains parseable.