diff --git a/.agents/pm/chores/pm-starter-50az.toon b/.agents/pm/chores/pm-starter-50az.toon new file mode 100644 index 0000000..a9826c8 --- /dev/null +++ b/.agents/pm/chores/pm-starter-50az.toon @@ -0,0 +1,23 @@ +id: pm-starter-50az +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:35:59.117Z" +updated_at: "2026-09-25T08:38:11.638Z" +closed_at: "2026-09-25T08:38:09.572Z" +completed_at: "2026-09-25T08:38:09.572Z" +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:36:21.052Z",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-starter-50az.jsonl b/.agents/pm/history/pm-starter-50az.jsonl new file mode 100644 index 0000000..488544a --- /dev/null +++ b/.agents/pm/history/pm-starter-50az.jsonl @@ -0,0 +1,11 @@ +{"hash_algorithm":"sha256","ts":"2026-09-25T08:35:59.117Z","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-starter-50az"},{"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:35:59.117Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-09-25T08:35:59.117Z"},{"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":"5046b4e5f7422d2190fe8d2361b187391c6c7786aed68314924d005949a8b077","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":"597488bfb69227dbc8d64ff8a98dbe89ea97f8c0c8e16e98150a71cde77ace0f"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:36:01.328Z","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:36:01.328Z"},{"op":"add","path":"/metadata/assignee","value":"fleet-wave-script"},{"op":"add","path":"/metadata/claim_principal","value":"fleet-wave-script"}],"before_hash":"5046b4e5f7422d2190fe8d2361b187391c6c7786aed68314924d005949a8b077","after_hash":"d3468f617cff68e66e8c750deb54608c3ba5f7cd44916c349f2e1f1e89b1e619","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":"51d5103a2a4cc28055039e7f7916f6eb1fc90532c8930445c28d513038b67361"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:36:04.335Z","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:36:04.335Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"d3468f617cff68e66e8c750deb54608c3ba5f7cd44916c349f2e1f1e89b1e619","after_hash":"7da0aee4bdab095c1f9402e212a267d51ad9508b7116b617f156fc17b862319d","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":"e62e2cec190576b311a6cbb2127a07615340581c8bb1975be047280702e445c5"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:36:08.316Z","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:36:08.316Z"},{"op":"add","path":"/metadata/files","value":[{"path":"README.md","scope":"project"}]}],"before_hash":"7da0aee4bdab095c1f9402e212a267d51ad9508b7116b617f156fc17b862319d","after_hash":"dada9af28958ecbc006b025ee2783e0b6e6309552be77d14f7171befbdcdffa6","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":"a8a9d3edc49e71491d8f69ce38faf387af21f5edfe97a496e04762fc403868d2"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:36:11.825Z","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:36:11.825Z"}],"before_hash":"dada9af28958ecbc006b025ee2783e0b6e6309552be77d14f7171befbdcdffa6","after_hash":"480567cded97898c7342b2ed4b61bf5691571e4dbd1145332fd4f6abfeeeba1a","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":"c9884141b587d3b8f075e715b5d3215f65bcc54a1399f8121a22edd46375d14e"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:36:13.985Z","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:36:13.985Z"}],"before_hash":"480567cded97898c7342b2ed4b61bf5691571e4dbd1145332fd4f6abfeeeba1a","after_hash":"a5d7b45875e5a0b1211af5d3fcba3e2c6aee29cdc7d24780f1d756dadf5da401","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":"d8e28c51bac26f819009df0b35cc8e03d8d6bf3111baeda19df7041375218397"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:36:16.539Z","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:36:16.539Z"}],"before_hash":"a5d7b45875e5a0b1211af5d3fcba3e2c6aee29cdc7d24780f1d756dadf5da401","after_hash":"b1fcbd8590842a8b17be49d1f3cfe2183be393e2d26642ee3af816d119941408","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":"5856f181ce2a0c03691a92ca3f81a30faa167ff1ffb9c6e1df5f75ea934445ed"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:36:18.932Z","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:36:18.932Z"}],"before_hash":"b1fcbd8590842a8b17be49d1f3cfe2183be393e2d26642ee3af816d119941408","after_hash":"d780a48391fbd2e8c04cd098e91b4184a29daadeadf609bfc22ee4a830325383","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":"b9f8374fd08945d91a8c7326626a5dffc5b06fd671d700734586b4d156a46207"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:36:21.146Z","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:36:21.146Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"npm run release:check","scope":"project","provenance":{"author":"fleet-wave-script","created_at":"2026-09-25T08:36:21.052Z","source_kind":"local_mutation","source_ref":"certify-pm-cli-2026-9-23-and-roll-out-guarded-merge-driver-launcher"}}]}],"before_hash":"d780a48391fbd2e8c04cd098e91b4184a29daadeadf609bfc22ee4a830325383","after_hash":"1c630ae465445bf9c11b12bd32e02b07c3af669c9b72346fe90d830f45061cde","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":"1f03d6eda2390048e812898d1c1ca47b1aa4676ffca879b6db17d7301140afd9"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:38:09.634Z","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:38:09.634Z"},{"op":"replace","path":"/metadata/status","value":"closed"},{"op":"add","path":"/metadata/closed_at","value":"2026-09-25T08:38:09.572Z"},{"op":"add","path":"/metadata/completed_at","value":"2026-09-25T08:38:09.572Z"},{"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":"1c630ae465445bf9c11b12bd32e02b07c3af669c9b72346fe90d830f45061cde","after_hash":"e8bfe356a51a797eb8434249fad80f78eda843a257927ebdb339ddb204bc2ba4","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":"04ff382ab4e9e505936bf1336c15b0427cd013a570a01bd93e6a8edccff97f26"} +{"hash_algorithm":"sha256","ts":"2026-09-25T08:38:11.638Z","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:38:11.638Z"}],"before_hash":"e8bfe356a51a797eb8434249fad80f78eda843a257927ebdb339ddb204bc2ba4","after_hash":"7a8940df430f9e2784cfc23e6627e8d5ef940bbc96f07315ea623cac15cab375","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":"da240ee6bd5113cb9460227cee36ed3b4ab698526dd0543c99d423bba202f637"} diff --git a/CHANGELOG.md b/CHANGELOG.md index 2831668..d8e9fb8 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-starter-50az](https://github.com/unbraind/pm-starter/blob/main/.agents/pm/chores/pm-starter-50az.toon)) + ## 2026.9.22 - 2026-09-22 ### Fixed diff --git a/README.md b/README.md index 08a092b..397132e 100644 --- a/README.md +++ b/README.md @@ -173,9 +173,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 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 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 06e153e..d51a302 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,9 @@ "license": "MIT", "devDependencies": { "@types/node": "^26.1.1", - "@unbrained/pm-cli": "2026.9.21", - "pm-changelog": "2026.9.18", - "pm-ops": "2026.9.18", + "@unbrained/pm-cli": "2026.9.23", + "pm-changelog": "2026.9.23", + "pm-ops": "2026.9.23", "typescript": "^7.0.2" }, "engines": { @@ -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 de14bc3..390a2ef 100644 --- a/package.json +++ b/package.json @@ -45,9 +45,9 @@ }, "devDependencies": { "@types/node": "^26.1.1", - "@unbrained/pm-cli": "2026.9.21", - "pm-changelog": "2026.9.18", - "pm-ops": "2026.9.18", + "@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); +});