Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .agents/pm/history/ops-mqdi.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{"hash_algorithm":"sha256","ts":"2026-09-22T20:52:55.599Z","author":"claude-hub","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"e0824cdc8d5a73748e09fe93","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"medium","source":"environment"},"role":{"value":"implementer","source":"argv"},"topic":null,"version":{"value":"2.1.280","source":"probe"}},"op":"create","patch":[{"op":"add","path":"/metadata/id","value":"ops-mqdi"},{"op":"add","path":"/metadata/title","value":"Guarded merge-driver launcher: omit-dev clone installs complete instead of failing on the devDependency import"},{"op":"add","path":"/metadata/description","value":"The canonical consumer launcher statically imports pm-ops/merge-driver, but pm-ops is a devDependency. npm install --omit=dev with scripts enabled (a source checkout) fails with ERR_MODULE_NOT_FOUND in prepare before any fallback runs. Raised by CodeRabbit on pm-web#156 and Greptile on pm-slack-standup#89 and pm-changelog#207; hub item pm-cli-website-xy19."},{"op":"add","path":"/metadata/type","value":"Issue"},{"op":"add","path":"/metadata/status","value":"in_progress"},{"op":"add","path":"/metadata/priority","value":1},{"op":"add","path":"/metadata/tags","value":["fleet","launcher","merge-driver","omit-dev"]},{"op":"add","path":"/metadata/created_at","value":"2026-09-22T20:52:55.599Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-09-22T20:52:55.599Z"},{"op":"add","path":"/metadata/author","value":"claude-hub"},{"op":"add","path":"/metadata/acceptance_criteria","value":"templates/prepare-merge-driver.ts ships in the package and imports only node builtins; pm-ops/merge-driver/prepare is an exported entry; fixture tests run the shipped template for: pm-ops absent (exit 0, exactly one notice), current (runs pm merge install), failing pm (status propagated), stale pm-ops without the export and pm-ops with a missing entry file (both fail loudly); release:check exits 0 with 100/100/100/100 coverage"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"2ff4c90651f24c68d5d25f6e7f8be6d6b56e8f07794223995d8ef80eca6e1cac","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":"eda2c585a739e2262aa5aa5618eae9d6fbae5ce462f5bce7dfda924e7f8bb546"}
{"hash_algorithm":"sha256","ts":"2026-09-22T20:53:02.266Z","author":"claude-hub","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"e0824cdc8d5a73748e09fe93","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"medium","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.280","source":"probe"}},"op":"files_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-09-22T20:53:02.266Z"},{"op":"add","path":"/metadata/files","value":[{"path":"merge-driver-prepare.ts","scope":"project"},{"path":"templates/prepare-merge-driver.ts","scope":"project"},{"path":"test/merge-driver-launcher.test.ts","scope":"project"}]}],"before_hash":"2ff4c90651f24c68d5d25f6e7f8be6d6b56e8f07794223995d8ef80eca6e1cac","after_hash":"b8f1e6c5026583483b5b79e02f5c37a692bc640d02e256c0ad1d470fa179a2fc","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":"2f6d0bbb7445f9dd9deb72bf5cd5826a71935753e78cb0c5aa3ceb293621e309"}
{"hash_algorithm":"sha256","ts":"2026-09-22T20:53:02.793Z","author":"claude-hub","author_source":"asserted","agent_harness":"claude-code","agent_model":"claude-opus-5-5","agent_model_source":"probe","agent_instance":"e0824cdc8d5a73748e09fe93","agent_provenance":{"model":{"value":"claude-opus-5-5","source":"probe"},"effort":{"value":"medium","source":"environment"},"role":null,"topic":null,"version":{"value":"2.1.280","source":"probe"}},"op":"tests_add","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-09-22T20:53:02.793Z"},{"op":"add","path":"/metadata/tests","value":[{"command":"node --test test/merge-driver-launcher.test.ts","scope":"project","provenance":{"author":"claude-hub","created_at":"2026-09-22T20:53:02.768Z","source_kind":"local_mutation","source_ref":"guarded-merge-driver-launcher-omit-dev"}}]}],"before_hash":"b8f1e6c5026583483b5b79e02f5c37a692bc640d02e256c0ad1d470fa179a2fc","after_hash":"0f5806329a2aaf916f0334a5469e19def79152de52c02c0c009661491eb2de5b","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":"049394a38822ffb6ef192ed5aa89f255a05469e63332a278efb5e9cfc991c735"}
18 changes: 18 additions & 0 deletions .agents/pm/issues/ops-mqdi.toon
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
id: ops-mqdi
title: "Guarded merge-driver launcher: omit-dev clone installs complete instead of failing on the devDependency import"
description: "The canonical consumer launcher statically imports pm-ops/merge-driver, but pm-ops is a devDependency. npm install --omit=dev with scripts enabled (a source checkout) fails with ERR_MODULE_NOT_FOUND in prepare before any fallback runs. Raised by CodeRabbit on pm-web#156 and Greptile on pm-slack-standup#89 and pm-changelog#207; hub item pm-cli-website-xy19."
type: Issue
status: in_progress
priority: 1
tags[4]: fleet,launcher,merge-driver,omit-dev
created_at: "2026-09-22T20:52:55.599Z"
updated_at: "2026-09-22T20:53:02.793Z"
author: claude-hub
acceptance_criteria: "templates/prepare-merge-driver.ts ships in the package and imports only node builtins; pm-ops/merge-driver/prepare is an exported entry; fixture tests run the shipped template for: pm-ops absent (exit 0, exactly one notice), current (runs pm merge install), failing pm (status propagated), stale pm-ops without the export and pm-ops with a missing entry file (both fail loudly); release:check exits 0 with 100/100/100/100 coverage"
files[3]{path,scope}:
merge-driver-prepare.ts,project
templates/prepare-merge-driver.ts,project
test/merge-driver-launcher.test.ts,project
tests[1]{command,scope,provenance{author,created_at,source_kind,source_ref}}:
node --test test/merge-driver-launcher.test.ts,project,claude-hub,"2026-09-22T20:53:02.768Z",local_mutation,guarded-merge-driver-launcher-omit-dev
body: ""
29 changes: 19 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,19 +294,28 @@ Fleet totals are intentionally **not** pre-aggregated — expose per-repo series

Git never clones `.git/config`, so every fleet package has to run `pm merge install` from its
npm `prepare` script or concurrent agents conflict on tracker files. This package is the one
canonical implementation. Consumers ship a one-line launcher instead of vendoring JavaScript:
canonical implementation.

```ts
// scripts/prepare-merge-driver.ts
import { runPrepareMergeDriver } from "pm-ops/merge-driver";
process.exitCode = runPrepareMergeDriver();
```
Consumers copy [`templates/prepare-merge-driver.ts`](templates/prepare-merge-driver.ts) unchanged to
`scripts/prepare-merge-driver.ts` and set `"prepare": "node scripts/prepare-merge-driver.ts"`. The
template imports **nothing** from pm-ops. pm-ops is a devDependency, so a checkout installed with
`npm install --omit=dev` (scripts enabled) does not have it, and a static
`import … from "pm-ops/merge-driver"` would fail at module load, before any fallback could run.
Dynamic `import()` is forbidden by the fleet lint gate. Instead the template resolves
`pm-ops/merge-driver/prepare` from the package root and runs it in a child process:

- **pm-ops not installed** (`--omit=dev`): exits `0` after exactly one notice line
- **pm-ops too old to export the entry, or its entry file missing**: fails the install loudly, and never skips
- **pm-ops installed**: runs `runPrepareMergeDriver` (below) and propagates its exit status

Fixture tests in `test/merge-driver-launcher.test.ts` execute the shipped template against each of
those consumer layouts with a stub `pm` on PATH.

`runPrepareMergeDriver` resolves the real `pm` launcher on the supplied PATH/platform, then:
`runPrepareMergeDriver` (also exported from `pm-ops/merge-driver`) resolves the real `pm` launcher on the supplied PATH/platform, then:

- **missing `pm`** — exits `0` and prints one notice line, so production / `--omit=dev` installs are not broken
- **present `pm` whose `pm merge install` fails** — non-zero exit with that command's output
- **Windows** — honours quoted PATH entries and PATHEXT shims, and sets `shell: true` only on `win32` so `.cmd` launchers run; the POSIX path is never faked
- **missing `pm`**: exits `0` and prints one notice line
- **present `pm` whose `pm merge install` fails**: non-zero exit with that command's output
- **Windows**: honours quoted PATH entries and PATHEXT shims, and sets `shell: true` only on `win32` so `.cmd` launchers run; the POSIX path is never faked

This repository's own `prepare` script is that launcher (with an `isMainInvocation` guard so the suite can import it). To (re)run manually: `npm run merge:install`.
## Canonical code-quality exports
Expand Down
15 changes: 15 additions & 0 deletions dist/merge-driver-prepare.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Executable entry that registers pm's merge drivers for a consumer's npm
* `prepare` hook, published as `pm-ops/merge-driver/prepare`.
*
* Consumer launchers must not import pm-ops: it is a devDependency, so an
* `npm install --omit=dev` checkout does not have it and a static import fails
* before any fallback can run. The canonical launcher
* (`templates/prepare-merge-driver.ts`) instead resolves this subpath and runs
* it in a child process. Running it performs {@link runPrepareMergeDriver} and
* reports its status as the process exit code.
*
* @packageDocumentation
*/
export {};
//# sourceMappingURL=merge-driver-prepare.d.ts.map
1 change: 1 addition & 0 deletions dist/merge-driver-prepare.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions dist/merge-driver-prepare.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions dist/merge-driver-prepare.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions merge-driver-prepare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Executable entry that registers pm's merge drivers for a consumer's npm
* `prepare` hook, published as `pm-ops/merge-driver/prepare`.
*
* Consumer launchers must not import pm-ops: it is a devDependency, so an
* `npm install --omit=dev` checkout does not have it and a static import fails
* before any fallback can run. The canonical launcher
* (`templates/prepare-merge-driver.ts`) instead resolves this subpath and runs
* it in a child process. Running it performs {@link runPrepareMergeDriver} and
* reports its status as the process exit code.
*
* @packageDocumentation
*/

import { runPrepareMergeDriver } from "./merge-driver.ts";

process.exitCode = runPrepareMergeDriver();
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
"types": "./dist/merge-driver.d.ts",
"default": "./dist/merge-driver.js"
},
"./merge-driver/prepare": {
"types": "./dist/merge-driver-prepare.d.ts",
"default": "./dist/merge-driver-prepare.js"
},
"./lifecycle-policy": {
"types": "./dist/lifecycle-policy.d.ts",
"default": "./dist/lifecycle-policy.js"
Expand All @@ -53,6 +57,7 @@
},
"files": [
"dist/",
"templates/",
"manifest.json",
"README.md",
"CHANGELOG.md"
Expand Down
42 changes: 42 additions & 0 deletions templates/prepare-merge-driver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* npm `prepare` hook that registers pm's field-aware Git merge drivers.
*
* 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 { 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;
}
6 changes: 4 additions & 2 deletions test/coverage-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ test("coverage gate defaults to its package root and native process boundaries",
"assurance.ts",
"invocation-audit.ts",
"merge-driver.ts",
"merge-driver-prepare.ts",
"templates/prepare-merge-driver.ts",
"eslint.ts",
"duplication.ts",
"scripts/coverage-gate.ts",
Expand Down Expand Up @@ -198,13 +200,13 @@ test("coverage gate direct entrypoint executes against an explicit package root"
if (process.platform === "win32") {
writeFileSync(
join(bin, "npx.cmd"),
`@echo off\r\nmkdir coverage 2>nul\r\n(echo SF:index.ts& echo end_of_record& echo SF:attestation.ts& echo end_of_record& echo SF:shell-scan.ts& echo end_of_record& echo SF:docstrings.ts& echo end_of_record& echo SF:assurance.ts& echo end_of_record& echo SF:invocation-audit.ts& echo end_of_record& echo SF:merge-driver.ts& echo end_of_record& echo SF:eslint.ts& echo end_of_record& echo SF:duplication.ts& echo end_of_record& echo SF:scripts/coverage-gate.ts& echo end_of_record& echo SF:scripts/lint.ts& echo end_of_record& echo SF:scripts/duplication-gate.ts& echo end_of_record& echo SF:scripts/docstring-gate.ts& echo end_of_record& echo SF:scripts/main-invocation.ts& echo end_of_record& echo SF:scripts/prepare-merge-driver.ts& echo end_of_record& echo SF:scripts/shell-command-scan.ts& echo end_of_record& echo SF:scripts/verify-release-changelog-date.ts& echo end_of_record& echo SF:scripts/verify-release-completeness.ts& echo end_of_record& echo SF:scripts/verify-release-publish-attestation.ts& echo end_of_record& echo SF:lifecycle-policy.ts& echo end_of_record& echo SF:scripts/verify-lifecycle-policy.ts& echo end_of_record)>coverage\\lcov.info\r\n`,
`@echo off\r\nmkdir coverage 2>nul\r\n(echo SF:index.ts& echo end_of_record& echo SF:attestation.ts& echo end_of_record& echo SF:shell-scan.ts& echo end_of_record& echo SF:docstrings.ts& echo end_of_record& echo SF:assurance.ts& echo end_of_record& echo SF:invocation-audit.ts& echo end_of_record& echo SF:merge-driver.ts& echo end_of_record& echo SF:merge-driver-prepare.ts& echo end_of_record& echo SF:templates/prepare-merge-driver.ts& echo end_of_record& echo SF:eslint.ts& echo end_of_record& echo SF:duplication.ts& echo end_of_record& echo SF:scripts/coverage-gate.ts& echo end_of_record& echo SF:scripts/lint.ts& echo end_of_record& echo SF:scripts/duplication-gate.ts& echo end_of_record& echo SF:scripts/docstring-gate.ts& echo end_of_record& echo SF:scripts/main-invocation.ts& echo end_of_record& echo SF:scripts/prepare-merge-driver.ts& echo end_of_record& echo SF:scripts/shell-command-scan.ts& echo end_of_record& echo SF:scripts/verify-release-changelog-date.ts& echo end_of_record& echo SF:scripts/verify-release-completeness.ts& echo end_of_record& echo SF:scripts/verify-release-publish-attestation.ts& echo end_of_record& echo SF:lifecycle-policy.ts& echo end_of_record& echo SF:scripts/verify-lifecycle-policy.ts& echo end_of_record)>coverage\\lcov.info\r\n`,

);
} else {
writeFileSync(
join(bin, "npx"),
"#!/usr/bin/env sh\nmkdir -p coverage\nprintf 'SF:index.ts\\nend_of_record\\nSF:attestation.ts\\nend_of_record\\nSF:shell-scan.ts\\nend_of_record\\nSF:docstrings.ts\\nend_of_record\\nSF:assurance.ts\\nend_of_record\\nSF:invocation-audit.ts\\nend_of_record\\nSF:merge-driver.ts\\nend_of_record\\nSF:eslint.ts\\nend_of_record\\nSF:duplication.ts\\nend_of_record\\nSF:scripts/coverage-gate.ts\\nend_of_record\\nSF:scripts/lint.ts\\nend_of_record\\nSF:scripts/duplication-gate.ts\\nend_of_record\\nSF:scripts/docstring-gate.ts\\nend_of_record\\nSF:scripts/main-invocation.ts\\nend_of_record\\nSF:scripts/prepare-merge-driver.ts\\nend_of_record\\nSF:scripts/shell-command-scan.ts\\nend_of_record\\nSF:scripts/verify-release-changelog-date.ts\\nend_of_record\\nSF:scripts/verify-release-completeness.ts\\nend_of_record\\nSF:scripts/verify-release-publish-attestation.ts\\nend_of_record\\nSF:lifecycle-policy.ts\\nend_of_record\\nSF:scripts/verify-lifecycle-policy.ts\\nend_of_record\\n' > coverage/lcov.info\n",
"#!/usr/bin/env sh\nmkdir -p coverage\nprintf 'SF:index.ts\\nend_of_record\\nSF:attestation.ts\\nend_of_record\\nSF:shell-scan.ts\\nend_of_record\\nSF:docstrings.ts\\nend_of_record\\nSF:assurance.ts\\nend_of_record\\nSF:invocation-audit.ts\\nend_of_record\\nSF:merge-driver.ts\\nend_of_record\\nSF:merge-driver-prepare.ts\\nend_of_record\\nSF:templates/prepare-merge-driver.ts\\nend_of_record\\nSF:eslint.ts\\nend_of_record\\nSF:duplication.ts\\nend_of_record\\nSF:scripts/coverage-gate.ts\\nend_of_record\\nSF:scripts/lint.ts\\nend_of_record\\nSF:scripts/duplication-gate.ts\\nend_of_record\\nSF:scripts/docstring-gate.ts\\nend_of_record\\nSF:scripts/main-invocation.ts\\nend_of_record\\nSF:scripts/prepare-merge-driver.ts\\nend_of_record\\nSF:scripts/shell-command-scan.ts\\nend_of_record\\nSF:scripts/verify-release-changelog-date.ts\\nend_of_record\\nSF:scripts/verify-release-completeness.ts\\nend_of_record\\nSF:scripts/verify-release-publish-attestation.ts\\nend_of_record\\nSF:lifecycle-policy.ts\\nend_of_record\\nSF:scripts/verify-lifecycle-policy.ts\\nend_of_record\\n' > coverage/lcov.info\n",

);
}
Expand Down
Loading
Loading