Skip to content

Merge receipts store item_path with literal surrounding quotes from %P, and pm merge reconcile publishes the quoted path into committed history #771

Description

@unbraind

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions