From 749f23d1e79ea79212500aae312be7ddc079e0c7 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:43:19 +0200 Subject: [PATCH] Certify pm CLI 2026.9.23 and adopt the guarded pm-ops merge-driver launcher - Pins @unbrained/pm-cli 2026.9.21 -> 2026.9.23, pm-ops 2026.9.18 -> 2026.9.23, pm-changelog 2026.9.18 -> 2026.9.23 exactly (package.json and package-lock.json). - scripts/prepare-merge-driver.ts is now the launcher template pm-ops ships, copied unchanged: it imports nothing from pm-ops, so `npm ci --omit=dev` in a clone skips with one notice instead of failing, while a stale or broken pm-ops still fails. A test keeps it byte-identical to the pinned template and proves it registers the drivers. pm items: pm-ts-starter-r5s2. Companion epic pm-cli-website-5s6z. release:check exits 0. --- .agents/pm/chores/pm-ts-starter-r5s2.toon | 23 ++++ .agents/pm/history/pm-ts-starter-r5s2.jsonl | 11 ++ CHANGELOG.md | 6 + README.md | 5 +- package-lock.json | 26 ++-- package.json | 6 +- scripts/prepare-merge-driver.ts | 44 ++++++- test/prepare-merge-driver.test.ts | 136 +++++++++++++++++--- 8 files changed, 215 insertions(+), 42 deletions(-) create mode 100644 .agents/pm/chores/pm-ts-starter-r5s2.toon create mode 100644 .agents/pm/history/pm-ts-starter-r5s2.jsonl diff --git a/.agents/pm/chores/pm-ts-starter-r5s2.toon b/.agents/pm/chores/pm-ts-starter-r5s2.toon new file mode 100644 index 0000000..d6a28d5 --- /dev/null +++ b/.agents/pm/chores/pm-ts-starter-r5s2.toon @@ -0,0 +1,23 @@ +id: pm-ts-starter-r5s2 +title: Certify pm CLI 2026.9.23 and adopt the guarded pm-ops merge-driver launcher +description: "Fleet wave 2026-09-25 (companion epic pm-cli-website-5s6z, launcher rollout pm-cli-website-xy19). Pins the toolchain to 2026.9.23 and replaces the prepare hook with the launcher template pm-ops ships: it imports nothing from pm-ops, so a production install of a clone (npm ci --omit=dev) skips with one notice instead of failing, while a stale or broken pm-ops still fails the install. A test keeps the copy byte-identical to the pinned template, whose branches pm-ops covers with real fixtures." +type: Chore +status: closed +priority: 1 +tags[4]: certify,merge-driver,multi-agent,pm-cli-2026.9.23 +created_at: "2026-09-25T08:37:52.477Z" +updated_at: "2026-09-25T08:43:00.472Z" +closed_at: "2026-09-25T08:42:55.808Z" +completed_at: "2026-09-25T08:42:55.808Z" +author: fleet-wave-script +acceptance_criteria: "package.json and package-lock.json pin pm-cli 2026.9.23 and pm-ops 2026.9.23 (pm-changelog 2026.9.23 where used); scripts/prepare-merge-driver.ts is byte-identical to the pinned pm-ops template, enforced by a test; CI runs pm health --strict-exit --require-merge-drivers and release:check exits 0" +files[5]{path,scope}: + package-lock.json,project + package.json,project + README.md,project + scripts/prepare-merge-driver.ts,project + test/prepare-merge-driver.test.ts,project +tests[1]{command,scope,provenance{author,created_at,source_kind,source_ref}}: + "npm run release:check",project,fleet-wave-script,"2026-09-25T08:38:06.225Z",local_mutation,certify-pm-cli-2026-9-23-and-roll-out-guarded-merge-driver-launcher +close_reason: "Pins: @unbrained/pm-cli 2026.9.21 -> 2026.9.23, pm-ops 2026.9.18 -> 2026.9.23, pm-changelog 2026.9.18 -> 2026.9.23 (package.json and package-lock.json). Launcher: pm-ops template copied unchanged, replacing the static-import launcher; test/prepare-merge-driver.test.ts proves byte identity and a real driver install. git config lists the pm merge drivers; the CI health block runs green with --require-merge-drivers; release:check exits 0." +body: "" diff --git a/.agents/pm/history/pm-ts-starter-r5s2.jsonl b/.agents/pm/history/pm-ts-starter-r5s2.jsonl new file mode 100644 index 0000000..53eae86 --- /dev/null +++ b/.agents/pm/history/pm-ts-starter-r5s2.jsonl @@ -0,0 +1,11 @@ +{"hash_algorithm":"sha256","ts":"2026-09-25T08:37:52.477Z","author":"fleet-wave-script","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"e0d15f3cda6eb998be619306","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":{"value":"implementer","source":"argv"},"topic":null,"version":{"value":"2.1.282","source":"probe"}},"op":"create","patch":[{"op":"add","path":"/metadata/id","value":"pm-ts-starter-r5s2"},{"op":"add","path":"/metadata/title","value":"Certify pm CLI 2026.9.23 and adopt the guarded pm-ops merge-driver launcher"},{"op":"add","path":"/metadata/description","value":"Fleet wave 2026-09-25 (companion epic pm-cli-website-5s6z, launcher rollout pm-cli-website-xy19). Pins the toolchain to 2026.9.23 and replaces the prepare hook with the launcher template pm-ops ships: it imports nothing from pm-ops, so a production install of a clone (npm ci --omit=dev) skips with one notice instead of failing, while a stale or broken pm-ops still fails the install. A test keeps the copy byte-identical to the pinned template, whose branches pm-ops covers with real fixtures."},{"op":"add","path":"/metadata/type","value":"Chore"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":1},{"op":"add","path":"/metadata/tags","value":["certify","merge-driver","multi-agent","pm-cli-2026.9.23"]},{"op":"add","path":"/metadata/created_at","value":"2026-09-25T08:37:52.477Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-09-25T08:37:52.477Z"},{"op":"add","path":"/metadata/author","value":"fleet-wave-script"},{"op":"add","path":"/metadata/acceptance_criteria","value":"package.json and package-lock.json pin pm-cli 2026.9.23 and pm-ops 2026.9.23 (pm-changelog 2026.9.23 where used); scripts/prepare-merge-driver.ts is byte-identical to the pinned pm-ops template, enforced by a test; CI runs pm health --strict-exit --require-merge-drivers and release:check exits 0"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"ba3e1bb8ea4f72642b1333e7cea05243d1ec1520068d30e777ac9da1d36f35e5","item_hash_version":3,"message":"","context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"},"version":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"c2ef6cb27b92dc48433bcfc0a48b9757de91cd071e00fccf586368b97acdbd74"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:37:54.508Z","author":"fleet-wave-script","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"e0d15f3cda6eb998be619306","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":{"value":"implementer","source":"argv"},"topic":null,"version":{"value":"2.1.282","source":"probe"}},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-09-25T08:37:54.508Z"},{"op":"add","path":"/metadata/assignee","value":"fleet-wave-script"},{"op":"add","path":"/metadata/claim_principal","value":"fleet-wave-script"}],"before_hash":"ba3e1bb8ea4f72642b1333e7cea05243d1ec1520068d30e777ac9da1d36f35e5","after_hash":"64341fa98cb32bfdbff9bfac50f52e638bc68ba15325dc868b6c1cea7e2379c4","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"},"version":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"f27d49725595fd0a8bd5cc712c066be3803b143c245fc8b0917fdf60bf1bd701"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:37:55.978Z","author":"fleet-wave-script","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"e0d15f3cda6eb998be619306","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":{"value":"implementer","source":"argv"},"topic":null,"version":{"value":"2.1.282","source":"probe"}},"op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-09-25T08:37:55.978Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"64341fa98cb32bfdbff9bfac50f52e638bc68ba15325dc868b6c1cea7e2379c4","after_hash":"aa58b39ac157bd6c8e2ad2b054907b733f45ecd7cfea1b2d202a1940d5a0a6c5","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"},"version":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"3e60a8d92d9c7249fb136f49e243bd36897bb71faed452df0bfb90d47772dd68"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:37:57.636Z","author":"fleet-wave-script","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"e0d15f3cda6eb998be619306","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.282","source":"probe"}},"op":"files_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-09-25T08:37:57.636Z"},{"op":"add","path":"/metadata/files","value":[{"path":"README.md","scope":"project"}]}],"before_hash":"aa58b39ac157bd6c8e2ad2b054907b733f45ecd7cfea1b2d202a1940d5a0a6c5","after_hash":"8a093a9d5abc0930093de35ef31438ca310828ca51f843f5d4d2cf16a4f22b3b","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"},"version":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"6584b18887393b53b56014ae7ec0418d5d991b82c5e8927cdd9baa57d1a92df3"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:37:59.148Z","author":"fleet-wave-script","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"e0d15f3cda6eb998be619306","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.282","source":"probe"}},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/0/path","value":"package-lock.json"},{"op":"add","path":"/metadata/files/1","value":{"path":"README.md","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-09-25T08:37:59.148Z"}],"before_hash":"8a093a9d5abc0930093de35ef31438ca310828ca51f843f5d4d2cf16a4f22b3b","after_hash":"7b8cc843c97b4ce73502b1e77206fa3921d4ec2b1f50b62f0240c054ab3e6889","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"},"version":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"1a5552ff81ad0ac7a1c635d71c20dffd7b2c65cef11b67bc537412cf72449b9c"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:38:00.914Z","author":"fleet-wave-script","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"e0d15f3cda6eb998be619306","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.282","source":"probe"}},"op":"files_add","patch":[{"op":"replace","path":"/metadata/files/1/path","value":"package.json"},{"op":"add","path":"/metadata/files/2","value":{"path":"README.md","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-09-25T08:38:00.914Z"}],"before_hash":"7b8cc843c97b4ce73502b1e77206fa3921d4ec2b1f50b62f0240c054ab3e6889","after_hash":"59ba27e0d0f9ec71369cc15fe5157a90c4c25b87214ac2626f295642e601c8ce","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"},"version":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"fcb5c65a60f132354fd1b61b2d06a344ba1e0db74d8e8f32c7951be30ed13fba"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:38:03.072Z","author":"fleet-wave-script","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"e0d15f3cda6eb998be619306","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.282","source":"probe"}},"op":"files_add","patch":[{"op":"add","path":"/metadata/files/3","value":{"path":"scripts/prepare-merge-driver.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-09-25T08:38:03.072Z"}],"before_hash":"59ba27e0d0f9ec71369cc15fe5157a90c4c25b87214ac2626f295642e601c8ce","after_hash":"7e35403dc088d0cf5ab93ce617784303629ce1996e0e4bb00f9e891d8be99e90","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"},"version":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"30915c03dc29809b057c116c5f24bda24683824669cbff51f61bca853d19ceb6"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:38:04.388Z","author":"fleet-wave-script","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"e0d15f3cda6eb998be619306","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.282","source":"probe"}},"op":"files_add","patch":[{"op":"add","path":"/metadata/files/4","value":{"path":"test/prepare-merge-driver.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-09-25T08:38:04.388Z"}],"before_hash":"7e35403dc088d0cf5ab93ce617784303629ce1996e0e4bb00f9e891d8be99e90","after_hash":"df2424d103b6ed136f10b7d7e756038dfcacb9b5eb9ba597b6ee95be4693525f","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"},"version":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"6d803b3c1d923d90e9ebdb17283aa0c507f8fd88b8acb65780dd9a256cf32e18"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:38:06.343Z","author":"fleet-wave-script","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"e0d15f3cda6eb998be619306","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.282","source":"probe"}},"op":"tests_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-09-25T08:38:06.343Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"npm run release:check","scope":"project","provenance":{"author":"fleet-wave-script","created_at":"2026-09-25T08:38:06.225Z","source_kind":"local_mutation","source_ref":"certify-pm-cli-2026-9-23-and-roll-out-guarded-merge-driver-launcher"}}]}],"before_hash":"df2424d103b6ed136f10b7d7e756038dfcacb9b5eb9ba597b6ee95be4693525f","after_hash":"fa87ee3258b5e3ac38098146d04ad8749dda5da70ecd022334dc5fa01ec24d82","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"},"version":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"9a093fda8961ab865953796edb48328b51a8835c1ff0367ec5cff43a76f07505"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:42:56.076Z","author":"fleet-wave-script","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"e0d15f3cda6eb998be619306","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":{"value":"implementer","source":"argv"},"topic":null,"version":{"value":"2.1.282","source":"probe"}},"op":"close","patch":[{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-09-25T08:42:56.076Z"},{"op":"replace","path":"/metadata/status","value":"closed"},{"op":"add","path":"/metadata/closed_at","value":"2026-09-25T08:42:55.808Z"},{"op":"add","path":"/metadata/completed_at","value":"2026-09-25T08:42:55.808Z"},{"op":"add","path":"/metadata/close_reason","value":"Pins: @unbrained/pm-cli 2026.9.21 -> 2026.9.23, pm-ops 2026.9.18 -> 2026.9.23, pm-changelog 2026.9.18 -> 2026.9.23 (package.json and package-lock.json). Launcher: pm-ops template copied unchanged, replacing the static-import launcher; test/prepare-merge-driver.test.ts proves byte identity and a real driver install. git config lists the pm merge drivers; the CI health block runs green with --require-merge-drivers; release:check exits 0."}],"before_hash":"fa87ee3258b5e3ac38098146d04ad8749dda5da70ecd022334dc5fa01ec24d82","after_hash":"c008fe998b0d551b5541247e803c39c615e69d322b659cc35d16c0784c321a2f","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"},"version":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"}}},"event_class":"substantive","record_hash_version":1,"record_hash":"7912ce16266c8e3d56fbb0f96aa079efa583e56a9d2ea080f9b498ca330f2606"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:43:00.472Z","author":"fleet-wave-script","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"e0d15f3cda6eb998be619306","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"xhigh","source":"environment"},"role":{"value":"implementer","source":"argv"},"topic":null,"version":{"value":"2.1.282","source":"probe"}},"op":"release","patch":[{"op":"remove","path":"/metadata/claim_principal"},{"op":"replace","path":"/metadata/updated_at","value":"2026-09-25T08:43:00.472Z"}],"before_hash":"c008fe998b0d551b5541247e803c39c615e69d322b659cc35d16c0784c321a2f","after_hash":"32a84b963de382ede8cb1ab8396bb16e5084d6f490a0258c7cc02bdbda904394","item_hash_version":3,"context":{"agent_provenance_outcomes":{"model":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"},"version":{"status":"resolved","resolver":"claude_session_file","rule_version":"v1"}}},"event_class":"maintenance","record_hash_version":1,"record_hash":"a21fc5b4f18c18b979dafb8b32c317fb0b4dc93dec4073c5067b18d4c1f09871"} diff --git a/CHANGELOG.md b/CHANGELOG.md index dacfc8b..e4aab05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Other + +- Certify pm CLI 2026.9.23 and adopt the guarded pm-ops merge-driver launcher ([pm-ts-starter-r5s2](https://github.com/unbraind/pm-ts-starter/blob/main/.agents/pm/chores/pm-ts-starter-r5s2.toon)) + ## 2026.9.22 - 2026-09-22 ### Fixed diff --git a/README.md b/README.md index 0ad17b5..e9f3504 100644 --- a/README.md +++ b/README.md @@ -137,10 +137,7 @@ This package is release-ready for GitHub, npm, and Bun-compatible installs. CI r This repo tracks its project management in `.agents/pm/` and ships a committed `.gitattributes` that maps those tracker artifacts to pm-cli's field-aware Git merge drivers, so concurrent-branch tracker edits merge cleanly instead of hard-conflicting. The driver **definitions** live in -per-clone Git config; `npm install` / `npm ci` wires them automatically via the `prepare` script (a thin -launcher, `scripts/prepare-merge-driver.ts`, over the canonical `pm-ops/merge-driver` export: it runs -`pm merge install` only when the `pm` CLI is on `PATH`, and no-ops cleanly when `pm` is absent. Registry installs of this package never run `prepare`; a production install of a clone (`npm ci --omit=dev`) omits `pm-ops` too, so it must pass `--ignore-scripts`; being Node-based it behaves identically -on POSIX shells and Windows `cmd.exe`). To (re)run manually: `npm run merge:install`. +per-clone Git config; `npm install` / `npm ci` wires them automatically via the `prepare` script, `scripts/prepare-merge-driver.ts`: the launcher template pm-ops ships, copied unchanged, which a test compares byte for byte with the pinned template. It runs pm-ops's installer, which calls `pm merge install` when the `pm` CLI is on `PATH` and skips with a notice when it is not. A production install of a clone (`npm ci --omit=dev`) has no `pm-ops`, so the launcher skips with one notice, while a stale or broken `pm-ops` fails the install. Registry installs of this package never run `prepare`. Being Node-based, it behaves identically on POSIX shells and Windows `cmd.exe`. To (re)run manually: `npm run merge:install`. After merging a branch that touched `.agents/pm/`, reconcile any residual history-hash drift with **`pm merge reconcile`** (pm-cli ≥ 2026.7.22): preview with `pm merge reconcile --dry-run`, apply with diff --git a/package-lock.json b/package-lock.json index c2777fb..034b332 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,12 @@ "@babel/eslint-parser": "^8.0.5", "@babel/plugin-syntax-typescript": "^8.0.3", "@types/node": "^26.2.0", - "@unbrained/pm-cli": "2026.9.21", + "@unbrained/pm-cli": "2026.9.23", "eslint": "^10.10.0", "fast-glob": "^3.3.3", "jscpd": "^4.3.0", - "pm-changelog": "2026.9.18", - "pm-ops": "2026.9.18", + "pm-changelog": "2026.9.23", + "pm-ops": "2026.9.23", "typescript": "^7.0.2" }, "engines": { @@ -1328,9 +1328,9 @@ } }, "node_modules/@unbrained/pm-cli": { - "version": "2026.9.21", - "resolved": "https://registry.npmjs.org/@unbrained/pm-cli/-/pm-cli-2026.9.21.tgz", - "integrity": "sha512-HPgGm/pU81Avtty9lVYqYh0jxKzNyQjHnvwQrRjSYNHaVQeJ3xNM/VV9i0CCPL0zLxRjdriYj/PnNDIMC2Ur2Q==", + "version": "2026.9.23", + "resolved": "https://registry.npmjs.org/@unbrained/pm-cli/-/pm-cli-2026.9.23.tgz", + "integrity": "sha512-tL1u+NDa6UkJyJaJR3j7noqi6byFlQdgWSZsFDG/k6k31LTBCQJNfX42EhgBE8kR100O61DleJSaMCtPq+LT9g==", "dev": true, "license": "MIT", "dependencies": { @@ -3144,9 +3144,9 @@ } }, "node_modules/pm-changelog": { - "version": "2026.9.18", - "resolved": "https://registry.npmjs.org/pm-changelog/-/pm-changelog-2026.9.18.tgz", - "integrity": "sha512-UJekN8TZUk/Q9Ri7pMl+EEtPqaGG5ePs/3/hS5JCx7w78dZAamfH7lUA+wUjJtoyKzLMX3XxTkCWpe61CquXdg==", + "version": "2026.9.23", + "resolved": "https://registry.npmjs.org/pm-changelog/-/pm-changelog-2026.9.23.tgz", + "integrity": "sha512-hrHFbRgz/LyiCBN9ic0I1C//SRL/zjU5fxcyCeCAnM2qTrz14A7f9ayMXeiu/ChDkVFMxfz8Ps2j3TuUG6K1QA==", "dev": true, "license": "MIT", "bin": { @@ -3160,9 +3160,9 @@ } }, "node_modules/pm-ops": { - "version": "2026.9.18", - "resolved": "https://registry.npmjs.org/pm-ops/-/pm-ops-2026.9.18.tgz", - "integrity": "sha512-x4KWL0Y9y6Sz2Hw9LDJmZPoZmbruKJ1x9Z9QCq2/W/yAG5Lujl5HiyrE9Er6Ne7gKFWJp3kxywYqUWDnLHEyMQ==", + "version": "2026.9.23", + "resolved": "https://registry.npmjs.org/pm-ops/-/pm-ops-2026.9.23.tgz", + "integrity": "sha512-FglZHOXjsuG8WWf9Ca90/IX1u4ZCxVTvHpwMaiA0TAWBx/lLalM2Ic12gtWZvm3OAJsMMuXvfRBHWxwYLzESsg==", "dev": true, "license": "MIT", "dependencies": { @@ -3178,7 +3178,7 @@ "@unbrained/pm-cli": ">=2026.8.20", "eslint": "^10.10.0", "fast-glob": "^3.3.3", - "jscpd": "^4.3.0" + "jscpd": ">=4.3.0 <6.0.0" }, "peerDependenciesMeta": { "@babel/eslint-parser": { diff --git a/package.json b/package.json index 7cae342..2143fa7 100644 --- a/package.json +++ b/package.json @@ -50,12 +50,12 @@ "@babel/eslint-parser": "^8.0.5", "@babel/plugin-syntax-typescript": "^8.0.3", "@types/node": "^26.2.0", - "@unbrained/pm-cli": "2026.9.21", + "@unbrained/pm-cli": "2026.9.23", "eslint": "^10.10.0", "fast-glob": "^3.3.3", "jscpd": "^4.3.0", - "pm-changelog": "2026.9.18", - "pm-ops": "2026.9.18", + "pm-changelog": "2026.9.23", + "pm-ops": "2026.9.23", "typescript": "^7.0.2" }, "keywords": [ diff --git a/scripts/prepare-merge-driver.ts b/scripts/prepare-merge-driver.ts index 5ac0834..66e512e 100644 --- a/scripts/prepare-merge-driver.ts +++ b/scripts/prepare-merge-driver.ts @@ -1,10 +1,42 @@ /** - * npm `prepare` hook that installs pm's field-aware Git merge drivers. + * npm `prepare` hook that registers pm's field-aware Git merge drivers. * - * Git never clones `.git/config`, so every clone must register the drivers that - * `.gitattributes` declares. The canonical implementation lives in - * `pm-ops/merge-driver`; this file stays a thin launcher over it. + * Git never clones `.git/config`, so every clone must register the drivers + * `.gitattributes` declares. The installer lives in the devDependency pm-ops, + * which an `npm install --omit=dev` checkout does not have. This launcher + * therefore imports nothing from pm-ops: it resolves the installer entry from + * the package root and runs it in a child process. Only a missing pm-ops package skips, with one + * notice; any other resolution failure (for example a pm-ops too old to export + * the entry) and any installer failure fail the install. + * + * Canonical copy: `pm-ops/templates/prepare-merge-driver.ts`. Copy it + * unchanged to `scripts/prepare-merge-driver.ts`. */ -import { runPrepareMergeDriver } from "pm-ops/merge-driver"; -process.exitCode = runPrepareMergeDriver(); \ No newline at end of file +import { spawnSync } from "node:child_process"; +import { createRequire } from "node:module"; +import { join } from "node:path"; + +// npm runs `prepare` from the package root, so pm-ops is resolved from there. +const resolver = createRequire(join(process.cwd(), "package.json")); +let installer: string | undefined; +try { + installer = resolver.resolve("pm-ops/merge-driver/prepare"); +} catch (error) { + // Only an absent pm-ops package may skip. Probing its package.json tells that + // apart from an installed pm-ops that cannot serve the entry (exports without + // it, no exports map, a missing file): those resolve or fail differently, and + // the original error is rethrown. + let packagePresent = true; + try { + resolver.resolve("pm-ops/package.json"); + } catch (probe) { + packagePresent = !(probe instanceof Error && "code" in probe && probe.code === "MODULE_NOT_FOUND"); + } + if (packagePresent) throw error; +} +if (installer === undefined) { + console.error("pm-ops is not installed (omit-dev install); skipping merge-driver install"); +} else { + process.exitCode = spawnSync(process.execPath, [installer], { stdio: "inherit" }).status ?? 1; +} diff --git a/test/prepare-merge-driver.test.ts b/test/prepare-merge-driver.test.ts index 4c31f3e..c3bb230 100644 --- a/test/prepare-merge-driver.test.ts +++ b/test/prepare-merge-driver.test.ts @@ -1,17 +1,121 @@ -/** Tests the thin `prepare` launcher over the canonical `pm-ops/merge-driver`. */ - +/** + * Tests for the npm `prepare` hook `scripts/prepare-merge-driver.ts`. + * + * The hook must stay the canonical pm-ops launcher byte for byte, and it is + * exercised the way npm runs it: as the entry point of a child process whose + * working directory is a consumer checkout. Every checkout is a fresh + * `git init` with its own local config, so the drivers this repository's own + * `npm ci` registered cannot mask a launcher that registers none. + */ import assert from "node:assert/strict"; -import test from "node:test"; - -// Importing the launcher executes it: the `prepare` hook is a two-line -// delegation to `runPrepareMergeDriver`, so loading this module IS running the -// hook. Absence of `pm` on `PATH` is a supported production-install state that -// the canonical installer turns into exit code 0, and a present `pm` that -// installs the drivers cleanly also returns 0 - so in every supported -// environment the import leaves `process.exitCode` at 0, and only a genuinely -// broken CLI (a failed `pm merge install`) fails the test loudly. -import "../scripts/prepare-merge-driver.ts"; - -test("the prepare launcher delegates to the canonical pm-ops merge-driver export", () => { - assert.strictEqual(process.exitCode, 0); -}); \ No newline at end of file +import { spawnSync, type SpawnSyncReturns } from "node:child_process"; +import { chmodSync, copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { delimiter, join } from "node:path"; +import test, { after } from "node:test"; + +// npm runs tests from the package root, which is also where it runs `prepare`. +const root = process.cwd(); +const launcher = join(root, "scripts", "prepare-merge-driver.ts"); +const hostPath = `${join(root, "node_modules", ".bin")}${delimiter}${process.env.PATH ?? ""}`; +const scratch = mkdtempSync(join(tmpdir(), "prepare-merge-driver-")); +after(() => rmSync(scratch, { recursive: true, force: true })); + +// The fixtures use POSIX stub executables and directory symlinks, like pm-ops's own launcher suite. +const posixOnly = { skip: process.platform === "win32" }; + +/** Every merge driver `.gitattributes` asks Git to use, e.g. `pm-history` from `merge=pm-history`. */ +const declaredDrivers = [ + ...new Set([...readFileSync(join(root, ".gitattributes"), "utf8").matchAll(/\bmerge=([\w-]+)/g)].map((match) => match[1])), +].sort(); + +/** + * Create a consumer checkout: a fresh Git repository carrying this + * repository's `.gitattributes` and tracker settings. `pmOps` selects what + * `node_modules/pm-ops` is: absent (an omit-dev install), the pinned package, + * or a stale pm-ops whose exports predate the launcher entry. + */ +function checkout(name: string, pmOps: "absent" | "pinned" | "stale"): string { + const directory = join(scratch, name); + mkdirSync(join(directory, ".agents", "pm"), { recursive: true }); + assert.equal(spawnSync("git", ["init", "-q"], { cwd: directory }).status, 0); + writeFileSync(join(directory, "package.json"), JSON.stringify({ name, type: "module" })); + copyFileSync(join(root, ".gitattributes"), join(directory, ".gitattributes")); + copyFileSync(join(root, ".agents", "pm", "settings.json"), join(directory, ".agents", "pm", "settings.json")); + if (pmOps === "pinned") { + mkdirSync(join(directory, "node_modules")); + symlinkSync(join(root, "node_modules", "pm-ops"), join(directory, "node_modules", "pm-ops"), "dir"); + } + if (pmOps === "stale") { + mkdirSync(join(directory, "node_modules", "pm-ops"), { recursive: true }); + writeFileSync( + join(directory, "node_modules", "pm-ops", "package.json"), + JSON.stringify({ name: "pm-ops", type: "module", exports: { "./merge-driver": "./merge-driver.js" } }), + ); + } + return directory; +} + +/** Put a stub `pm` that runs `body` then exits with `status` alone on a fresh PATH directory. */ +function stubPm(name: string, status: number, body = ""): string { + const bin = join(scratch, `${name}-bin`); + mkdirSync(bin); + writeFileSync(join(bin, "pm"), `#!/bin/sh\n${body}\nexit ${status}\n`); + chmodSync(join(bin, "pm"), 0o755); + return bin; +} + +/** Run the launcher as npm's `prepare` hook would: as the entry point, from `cwd`, with `path` as PATH. */ +function prepare(cwd: string, path: string): SpawnSyncReturns { + return spawnSync(process.execPath, [launcher], { cwd, encoding: "utf8", env: { ...process.env, PATH: path } }); +} + +/** The merge drivers registered in a checkout's LOCAL Git config, by name. */ +function registeredDrivers(cwd: string): string[] { + const config = spawnSync("git", ["config", "--local", "--name-only", "--get-regexp", "^merge\\..*\\.driver$"], { + cwd, + encoding: "utf8", + }); + // git exits 1 when no key matches, which is a genuine "none registered". + assert.ok(config.status === 0 || config.status === 1, config.stderr); + return config.stdout.split("\n").filter(Boolean).map((key) => key.split(".")[1]).sort(); +} + +test("the prepare launcher is the unmodified pm-ops template", () => { + const canonical = readFileSync(join(root, "node_modules", "pm-ops", "templates", "prepare-merge-driver.ts"), "utf8"); + assert.equal(readFileSync(launcher, "utf8"), canonical); +}); + +test("a full install registers every merge driver .gitattributes declares", posixOnly, () => { + const directory = checkout("full", "pinned"); + assert.deepEqual(registeredDrivers(directory), []); + const result = prepare(directory, hostPath); + assert.equal(result.status, 0, result.stderr); + assert.ok(declaredDrivers.length > 0, ".gitattributes declares no pm merge drivers"); + assert.deepEqual(registeredDrivers(directory), declaredDrivers); +}); + +test("an omit-dev checkout without pm-ops skips with one notice and registers nothing", posixOnly, () => { + const directory = checkout("omit-dev", "absent"); + const result = prepare(directory, hostPath); + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stderr, "pm-ops is not installed (omit-dev install); skipping merge-driver install\n"); + assert.deepEqual(registeredDrivers(directory), []); +}); + +test("a pm-ops too old to export the launcher entry fails the install", posixOnly, () => { + const result = prepare(checkout("stale", "stale"), hostPath); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /ERR_PACKAGE_PATH_NOT_EXPORTED/); +}); + +test("a failing pm merge install fails the install with the same status", posixOnly, () => { + const result = prepare(checkout("failing-pm", "pinned"), stubPm("failing-pm", 7)); + assert.equal(result.status, 7, result.stderr); +}); + +test("an installer killed by a signal fails the install instead of reporting success", posixOnly, () => { + // The stub's parent is the pm-ops installer process the launcher spawned. + const result = prepare(checkout("killed", "pinned"), stubPm("killed", 0, "kill -9 $PPID")); + assert.equal(result.status, 1, result.stderr); +});