Summary
Merge decision receipts store item_path with literal single-quote characters embedded in the string, because the value is taken verbatim from git's %P and the .gitattributes patterns that pm merge install writes are themselves quoted. The bad value is not confined to the clone-local receipt — pm merge reconcile copies it into the committed history/<id>.jsonl audit entry, so it is published to every consumer of the tracker.
Reproduction (pm 2026.7.27)
git init -q lab && cd lab
git config user.email a@b.c && git config user.name t
export PM_PATH="$PWD/.pm"
pm init lab --json >/dev/null && pm merge install --json >/dev/null
ID=$(pm create --title "Shared item" --type Task --json | node -pe 'JSON.parse(require("fs").readFileSync(0)).id')
git add -A && git commit -qm base && BASE=$(git rev-parse HEAD)
git checkout -qb agent-a && pm update "$ID" --description "A" --json >/dev/null && git add -A && git commit -qm a
git checkout -q "$BASE" && git checkout -qb agent-b && pm update "$ID" --description "B" --json >/dev/null && git add -A && git commit -qm b
git merge agent-a -m merge
pm merge report --json | grep item_path
Observed:
"item_path": "'.pm/tasks/lab-fx0u.toon'"
Expected:
"item_path": ".pm/tasks/lab-fx0u.toon"
The quotes are part of the string value (confirmed in the raw receipt file
.git/pm-merge-receipts/<uuid>.json, not a display artifact of the JSON encoder).
After pm merge reconcile, the same quoted value is persisted into the committed history entry:
{"op":"merge_reconcile", ..., "context":{"merge":{"receipts":[
{"item_id":"lab-fx0u","item_path":"'.pm/tasks/lab-fx0u.toon'", ...}]}}}
Cause
pm merge install writes quoted patterns into .gitattributes (correct — the fence needs them for paths with spaces):
".pm/tasks/*.toon" merge=pm-item-toon
git then passes the matched path through to the driver's %P placeholder, and the driver stores the received --item-path string without stripping the surrounding quotes.
Impact
Any consumer that wants to act on a receipt — open the conflicted item, link it in a report, diff it, group receipts by directory — has to know to strip quotes first:
fs.readFileSync(receipt.item_path) // ENOENT
path.resolve(root, receipt.item_path) // .../'.pm/tasks/lab-fx0u.toon'
receipt.item_path.replace(/^'|'$/g, "") // the workaround every consumer needs
item_id is clean, so receipts remain usable via id lookup — this is a correctness/ergonomics bug rather than a blocker, but it is baked into published history and will need a migration if left.
Suggested fix
Strip a single matched layer of surrounding '/" from the --item-path value where the driver ingests it (before it reaches the receipt writer), so both the clone-local receipt and the committed merge_reconcile audit entry carry a plain repository-relative path. Existing receipts/history entries can be normalised on read for backward compatibility.
Environment
- pm-cli 2026.7.27 (
@unbrained/pm-cli), Node 26.5.0, Linux
.gitattributes fence written by pm merge install (unmodified)
Summary
Merge decision receipts store
item_pathwith literal single-quote characters embedded in the string, because the value is taken verbatim from git's%Pand the.gitattributespatterns thatpm merge installwrites are themselves quoted. The bad value is not confined to the clone-local receipt —pm merge reconcilecopies it into the committedhistory/<id>.jsonlaudit entry, so it is published to every consumer of the tracker.Reproduction (pm 2026.7.27)
Observed:
Expected:
The quotes are part of the string value (confirmed in the raw receipt file
.git/pm-merge-receipts/<uuid>.json, not a display artifact of the JSON encoder).After
pm merge reconcile, the same quoted value is persisted into the committed history entry:{"op":"merge_reconcile", ..., "context":{"merge":{"receipts":[ {"item_id":"lab-fx0u","item_path":"'.pm/tasks/lab-fx0u.toon'", ...}]}}}Cause
pm merge installwrites quoted patterns into.gitattributes(correct — the fence needs them for paths with spaces):git then passes the matched path through to the driver's
%Pplaceholder, and the driver stores the received--item-pathstring without stripping the surrounding quotes.Impact
Any consumer that wants to act on a receipt — open the conflicted item, link it in a report, diff it, group receipts by directory — has to know to strip quotes first:
item_idis clean, so receipts remain usable via id lookup — this is a correctness/ergonomics bug rather than a blocker, but it is baked into published history and will need a migration if left.Suggested fix
Strip a single matched layer of surrounding
'/"from the--item-pathvalue where the driver ingests it (before it reaches the receipt writer), so both the clone-local receipt and the committedmerge_reconcileaudit entry carry a plain repository-relative path. Existing receipts/history entries can be normalised on read for backward compatibility.Environment
@unbrained/pm-cli), Node 26.5.0, Linux.gitattributesfence written bypm merge install(unmodified)