diff --git a/.agents/pm/chores/pm-github-d2az.toon b/.agents/pm/chores/pm-github-d2az.toon new file mode 100644 index 0000000..f575129 --- /dev/null +++ b/.agents/pm/chores/pm-github-d2az.toon @@ -0,0 +1,25 @@ +id: pm-github-d2az +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:14:55.781Z" +updated_at: "2026-09-25T08:58:09.880Z" +closed_at: "2026-09-25T08:22:45.260Z" +completed_at: "2026-09-25T08:22:45.260Z" +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" +comments[1]{created_at,author,text}: + "2026-09-25T08:58:09.880Z",fleet-wave-script,"Review round 1 (Sourcery and Greptile on the wave PRs): the launcher test now builds isolated git-init checkouts, so drivers registered by this repository's own npm ci cannot mask a launcher that registers none; it asserts the full driver set .gitattributes declares, and it covers the omit-dev skip, stale pm-ops, failing and killed installer branches. The launcher stays in the coverage sources where it was, covered through the child processes as in pm-ops." +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:15:08.770Z",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, @types/node ^26.1.1 -> ^26.6.2 (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-github-d2az.jsonl b/.agents/pm/history/pm-github-d2az.jsonl new file mode 100644 index 0000000..e72bbcd --- /dev/null +++ b/.agents/pm/history/pm-github-d2az.jsonl @@ -0,0 +1,12 @@ +{"hash_algorithm":"sha256","ts":"2026-09-25T08:14:55.781Z","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-github-d2az"},{"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:14:55.781Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-09-25T08:14:55.781Z"},{"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":"b24215f18c3503e566dddd74c0a0419512d57aa7e317bd965389be0e4c394a32","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":"2b5c0ac6fd5b3c8a1ee7b3b4e54c536680b31182a56fbb9df31bcb052ee2d858"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:14:56.976Z","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:14:56.976Z"},{"op":"add","path":"/metadata/assignee","value":"fleet-wave-script"},{"op":"add","path":"/metadata/claim_principal","value":"fleet-wave-script"}],"before_hash":"b24215f18c3503e566dddd74c0a0419512d57aa7e317bd965389be0e4c394a32","after_hash":"6d195ab10ba1cb3cac9bd9667bf9f7d3aefc562e53d43a0e1c56e65d09b56c00","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":"0d3a0ef51bbb7539b315aaefc1c2fbcd12e2341b0da34a8baaef94503f305364"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:14:58.320Z","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:14:58.320Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"6d195ab10ba1cb3cac9bd9667bf9f7d3aefc562e53d43a0e1c56e65d09b56c00","after_hash":"c9fcc30da3f8006f03b4269b6914e6566546e214d69a2d58035af112045ce599","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":"5cb7533d93109ae5bb34f7299c02080b72bc2b13b14436550dd9b7c84f1eff35"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:14:59.462Z","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:14:59.462Z"},{"op":"add","path":"/metadata/files","value":[{"path":"README.md","scope":"project"}]}],"before_hash":"c9fcc30da3f8006f03b4269b6914e6566546e214d69a2d58035af112045ce599","after_hash":"6e2be55e3a69a8f57ce215a62d1acc0f8f8103916491968575f41c6c27ce85a3","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":"6ee5805977f57c91115f0c94a4e72ef7c6b944ed29d213e404678b3b05a6f135"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:15:00.777Z","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:15:00.777Z"}],"before_hash":"6e2be55e3a69a8f57ce215a62d1acc0f8f8103916491968575f41c6c27ce85a3","after_hash":"92d0afdec534e4105b3a0ffb96207614c376b81fb34de977e3a5da6e3bf758a5","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":"48c3ac1560fe7b1e2557db09ceb8648b35972e42a849d55030ef7113ec2c25a8"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:15:02.727Z","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:15:02.727Z"}],"before_hash":"92d0afdec534e4105b3a0ffb96207614c376b81fb34de977e3a5da6e3bf758a5","after_hash":"d2f43daeafc29883d429ab1969497743b2af1a35812028dd639c61951a92f344","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":"6abdb1acfd4e428497aae7b864645b96f9b4cc3bfd5b75468d46a9a9f0bbc0b0"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:15:05.356Z","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:15:05.356Z"}],"before_hash":"d2f43daeafc29883d429ab1969497743b2af1a35812028dd639c61951a92f344","after_hash":"0039c37fb3de116d63d87798de39694f28d2a69f5a1a3aeeb48787893adca717","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":"aee743442e4dbf2a1a6f9163e4050666f34555945a95d1c289f6029d1aaedf74"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:15:07.594Z","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:15:07.594Z"}],"before_hash":"0039c37fb3de116d63d87798de39694f28d2a69f5a1a3aeeb48787893adca717","after_hash":"4bc37e7c9b3c028b8c3fb91e7c26609265032daaac646e5f27e31cab6768a33f","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":"adadd25a6f469abc581fa38d6e70c61c55318d0ea918397ee11d5da665b421ad"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:15:08.841Z","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:15:08.841Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"npm run release:check","scope":"project","provenance":{"author":"fleet-wave-script","created_at":"2026-09-25T08:15:08.770Z","source_kind":"local_mutation","source_ref":"certify-pm-cli-2026-9-23-and-roll-out-guarded-merge-driver-launcher"}}]}],"before_hash":"4bc37e7c9b3c028b8c3fb91e7c26609265032daaac646e5f27e31cab6768a33f","after_hash":"61979c079d9be9dc8948b8eba8bfc82332044e395a7f657cdde14833181d7e87","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":"7a3ec8243a8cdabe2226b314284d12ae7f20f744a896d0b852e420c00aa565ec"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:22:45.339Z","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:22:45.339Z"},{"op":"replace","path":"/metadata/status","value":"closed"},{"op":"add","path":"/metadata/closed_at","value":"2026-09-25T08:22:45.260Z"},{"op":"add","path":"/metadata/completed_at","value":"2026-09-25T08:22:45.260Z"},{"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, @types/node ^26.1.1 -> ^26.6.2 (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":"61979c079d9be9dc8948b8eba8bfc82332044e395a7f657cdde14833181d7e87","after_hash":"0d684d9dc2ffc0eac7b64dd4b4f1c5d08371b8ff05fa39a5e1ff833df511d2da","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":"4467c36b0083dfde74e512b1c6294167af61ac5edc6c8b22b492512117460a6e"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:22:49.165Z","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:22:49.165Z"}],"before_hash":"0d684d9dc2ffc0eac7b64dd4b4f1c5d08371b8ff05fa39a5e1ff833df511d2da","after_hash":"5c5a6e5df58e7a58e202f31d8190875d49296f66798686e88c2c783cd6813616","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":"8e24cd67cdccd8a609bb4d15a224918afb52d4004a71ecb337ee58a5b1b354be"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:58:09.880Z","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":"comment_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-09-25T08:58:09.880Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-09-25T08:58:09.880Z","author":"fleet-wave-script","text":"Review round 1 (Sourcery and Greptile on the wave PRs): the launcher test now builds isolated git-init checkouts, so drivers registered by this repository's own npm ci cannot mask a launcher that registers none; it asserts the full driver set .gitattributes declares, and it covers the omit-dev skip, stale pm-ops, failing and killed installer branches. The launcher stays in the coverage sources where it was, covered through the child processes as in pm-ops."}]}],"before_hash":"5c5a6e5df58e7a58e202f31d8190875d49296f66798686e88c2c783cd6813616","after_hash":"06f847b1fe5e44f1aeca688b40b6c7113f749d58838651aa6d13c793d8e74950","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":"8ae6072c5e18631b0f9b2b3d79e56e8e1d70b3cb253e99998e1b3122bacddf27"} diff --git a/CHANGELOG.md b/CHANGELOG.md index 61ec257..79075dc 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-github-d2az](https://github.com/unbraind/pm-github/blob/main/.agents/pm/chores/pm-github-d2az.toon)) + ## 2026.9.22 - 2026-09-22 ### Fixed diff --git a/README.md b/README.md index 705a3df..03d42b7 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,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. The driver definitions live in per-clone Git config; `npm install` / -`npm ci` wires them automatically via the `prepare` script (a portable Node guard, `scripts/prepare-merge-driver.ts`, a thin launcher over `pm-ops/merge-driver`: 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 +`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 diff --git a/package-lock.json b/package-lock.json index 41660d9..9213c2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,10 @@ "version": "2026.9.22", "license": "MIT", "devDependencies": { - "@types/node": "^26.1.1", - "@unbrained/pm-cli": "2026.9.21", - "pm-changelog": "2026.9.18", - "pm-ops": "2026.9.18", + "@types/node": "^26.6.2", + "@unbrained/pm-cli": "2026.9.23", + "pm-changelog": "2026.9.23", + "pm-ops": "2026.9.23", "typescript": "^7.0.2" }, "engines": { @@ -319,9 +319,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.5.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.1.tgz", - "integrity": "sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz", + "integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==", "dev": true, "license": "MIT", "dependencies": { @@ -669,9 +669,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": { @@ -982,9 +982,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": { @@ -998,9 +998,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": { @@ -1016,7 +1016,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 575dac2..8d1a995 100644 --- a/package.json +++ b/package.json @@ -44,10 +44,10 @@ "@unbrained/pm-cli": ">=2026.8.20" }, "devDependencies": { - "@types/node": "^26.1.1", - "@unbrained/pm-cli": "2026.9.21", - "pm-changelog": "2026.9.18", - "pm-ops": "2026.9.18", + "@types/node": "^26.6.2", + "@unbrained/pm-cli": "2026.9.23", + "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 d513e7f..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(); +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 new file mode 100644 index 0000000..c3bb230 --- /dev/null +++ b/test/prepare-merge-driver.test.ts @@ -0,0 +1,121 @@ +/** + * 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 { 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); +});