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-type refresh the fence automatically when it is installed (warning merge_fence_refreshed); repos inside git that never installed it get the actionable merge_fence_not_installed hint instead.
  • pm validate (the storage_integrity check) audits the committed fence against the active schema's type folders and reports validate_merge_fence_drift with the exact missing/stale attribute lines when they diverge — for example after editing type folders out-of-band. Rerun pm merge install and commit .gitattributes to 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 budgetids.token_length in settings.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_integrity validate check reports validate_storage_duplicate_item_ids whenever 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 copy then 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.


Merge_safety remote
Ein Problem melden