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
15 changes: 15 additions & 0 deletions .agents/pm/history/pm-6n63.jsonl

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions .agents/pm/issues/pm-6n63.toon
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
id: pm-6n63
title: "The manifest declared a pm CLI floor of 2026.7.28 while peerDependencies required 2026.8.7, so the CLI enforced a weaker minimum than npm"
description: "package.json peerDependencies declares >=2026.8.7 and npm enforces that at install time, but npm never sees a globally installed host CLI. The pm CLI enforces a different declaration: a top-level pm_min_version in manifest.json, which here read 2026.7.28. The two systems therefore enforced different minimums, and the one that applies to a globally installed CLI was the weaker of the two. Verified against 2026.8.15 by installing an extension whose manifest declared 2099.1.1: install returned ok false, the command never registered, and pm health reported extension_pm_min_version_unmet."
type: Issue
status: closed
priority: 2
tags: []
created_at: "2026-08-16T00:35:13.704Z"
updated_at: "2026-08-16T03:00:53.959Z"
closed_at: "2026-08-16T01:32:18.676Z"
completed_at: "2026-08-16T01:32:18.676Z"
author: claude-code
acceptance_criteria: "manifest.json declares a top-level pm_min_version equal to the peerDependencies floor; the devDependency on the CLI is an exact pin at or above that floor; removing the manifest floor, loosening the pin to a caret range, or changing either floor independently each fail the test"
resolution: "manifest.json declares pm_min_version 2026.8.7, equal to the peerDependencies floor, and devDependencies exact-pins the CLI at 2026.8.15. compatibility-floor.test.ts asserts three properties and each now has its own failing mutation: rewriting the peer range to an exact pin, removing or changing the manifest floor, and loosening the dev pin to a caret range. A fourth mutation covers the parse guard added during review."
expected_result: "A host CLI older than the package's declared floor is refused before the extension loads, on every install path — including a globally installed CLI that npm never sees."
actual_result: "npm refused a locally resolved dependency below 2026.8.7, while the pm CLI accepted any host at or above 2026.7.28 — a span of releases in which the package loaded against a CLI its own peer declaration says it does not support."
comments[3]{created_at,author,text}:
"2026-08-16T01:32:17.835Z",claude-code,"Revert-proof run against this exact tree: baseline exit 0; manifest pm_min_version removed exit 1; dev pin loosened to a caret range exit 1; manifest floor set to a version other than the peer floor exit 1; restored exit 0. Enforcement was verified directly against pm-cli 2026.8.15 rather than assumed: an extension declaring pm_min_version 2099.1.1 was refused at install with ok false, its command never registered, and pm health reported extension_pm_min_version_unmet."
"2026-08-16T02:18:43.032Z",claude-code,"Complete revert-proof, one mutation per assertion, run against this exact tree. Baseline exit 0. Assertion one, the peer dependency must be a >= floor rather than an exact pin: rewriting the peer range to an exact pin exits 1. Assertion two, the manifest must declare a pm_min_version equal to the peer floor: removing it exits 1 and setting it to a different version exits 1. Assertion three, the development dependency must be an exact pin at or above the floor: loosening it to a caret range exits 1. The parse guard added in the review round: setting the manifest floor to a two-part version exits 1. Restored exit 0. Every assertion in the file now has a mutation that fails it, which was not true when this item was first closed: the peer-range assertion was asserted but never proved, and CodeRabbit caught the gap between the close-reason claim and the recorded evidence."
"2026-08-16T02:37:08.229Z",claude-code,"Lower-bound mutation, added to the evidence after review: pinning the development CLI to an exact 2026.8.6, one day below the declared floor 2026.8.7, exits 1. This is the case the repository's own values cannot reach, because the pin and the floor are the same version here, so the ordering half of the comparison is never executed by the assertion. It is now driven directly by a test over synthetic pairs, including the lexicographic trap in which 2026.8.7 must not satisfy a floor of 2026.8.15."
notes[4]{created_at,author,text}:
"2026-08-16T02:19:36.440Z",claude-code,"Correction, appended rather than rewritten: the title of this item was changed twice after it was closed, and the close reason was corrected once. The first title stated the defect in the present tense, which pm-changelog emits verbatim and which therefore read in a shipped changelog as though the release still carried the defect. The second named each field only by its enforcer, which a reviewer read backwards. The close reason originally claimed all three assertions were proved to fail on revert; only two were, because the peer-range assertion was asserted but never mutated. Each of those corrections is an appended event in this item's history stream, so the record of what was claimed and when remains readable."
"2026-08-16T02:31:52.665Z",claude-code,"Correction appended after review: the earlier title said this package declared no pm_min_version at all. That was true of eight packages in this wave but not of this one, which declared 2026.7.28 — below its own peer floor of 2026.8.7. CodeRabbit caught the discrepancy between the title and the diff."
"2026-08-16T02:37:09.089Z",claude-code,"Correction to the earlier correction note, appended rather than rewritten. That note said this item's title changed twice after closure and that the close reason was corrected. Reading this item's own history stream: it closed at 2026-08-16T01:32:18, 0 update events precede the close and 4 follow it. The post-close updates changed the title and, in the last of them, the resolution field. The close_reason recorded at closure has not been altered. CodeRabbit caught that the note misstated both the field and, on some items, the chronology."
"2026-08-16T03:00:53.959Z",claude-code,"Third correction, and this one is derived from the patch entries of this item's own history stream rather than written as prose. Both earlier notes were inaccurate about which fields changed and when. The record, read from the stream: closed at 2026-08-16T01:32:18. Update events before the close: 0 (none). Update events after the close: 4 (2026-08-16T01:45:33 changed title; 2026-08-16T01:54:28 changed actual_result, expected_result, resolution, title; 2026-08-16T02:19:35 changed resolution; 2026-08-16T02:31:52 changed actual_result, description, title). Every one of those is an appended event; no prior entry was rewritten. CodeRabbit caught both earlier notes, and the reason both were wrong is the same: a claim about a history stream was composed from recollection of the commands issued instead of being read back out of the stream."
files[4]{path,scope}:
manifest.json,project
package-lock.json,project
package.json,project
tests/compatibility-floor.test.ts,project
tests[1]:
- command: PM_PATH=/tmp/pm-cf-pm-presets PM_GLOBAL_PATH=/tmp/pm-cf-g-pm-presets node --test tests/compatibility-floor.test.ts
path: tests/compatibility-floor.test.ts
scope: project
timeout_seconds: 120
assert_stdout_regex[1]: "the peer dependency declares the CLI floor as a minimum, not an exact pin[\\s\\S]*the extension manifest declares the same floor the CLI actually enforces[\\s\\S]*the development dependency is an exact pin at or above the declared floor"
note: "Binds the three compatibility-floor assertions to their real node:test titles so a renamed or deleted test fails this linked check instead of passing silently."
close_reason: "manifest.json now declares pm_min_version 2026.8.7, identical to the peerDependencies floor, and the development CLI is exact-pinned at 2026.8.15. Three assertions bind the declarations together and each was proved to fail on revert."
body: ""
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Fixed

- The manifest declared a pm CLI floor of 2026.7.28 while peerDependencies required 2026.8.7, so the CLI enforced a weaker minimum than npm ([pm-6n63](https://github.com/unbraind/pm-presets/blob/main/.agents/pm/issues/pm-6n63.toon))

## 2026.8.14 - 2026-08-14

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"description": "All 7 official pm-cli workspace presets in one package: bug-triage, indie-dev, open-source, software-sprint, startup-roadmap, kanban, agent-workflow",
"author": "@unbraind",
"entry": "./dist/index.js",
"pm_min_version": "2026.8.7",
"priority": 50,
"manifest_version": 2,
"pm_min_version": "2026.7.28",
"trusted": true,
"sandbox_profile": "none",
"permissions": {
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
},
"devDependencies": {
"@types/node": "^26.2.0",
"@unbrained/pm-cli": "2026.8.13",
"@unbrained/pm-cli": "2026.8.15",
"pm-changelog": "^2026.8.7",
"pm-ops": "2026.8.10",
"typescript": "^7.0.2"
Expand Down
135 changes: 135 additions & 0 deletions tests/compatibility-floor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import test from "node:test";

const repoRoot = resolve(import.meta.dirname, "..");

interface PackageManifest {
readonly devDependencies?: Record<string, string>;
readonly peerDependencies?: Record<string, string>;
}

interface ExtensionManifest {
/**
* Declared as `unknown` deliberately: `manifest.json` is untrusted JSON, and
* typing this `string | undefined` would assert the very shape these tests
* exist to verify. Each test narrows it explicitly before use, so a manifest
* carrying a number, an object, or nothing at all fails with a message that
* names the actual type rather than being coerced into a plausible string.
*/
readonly pm_min_version?: unknown;
}

const packageJson = JSON.parse(
readFileSync(resolve(repoRoot, "package.json"), "utf8"),
) as PackageManifest;
const extensionManifest = JSON.parse(
readFileSync(resolve(repoRoot, "manifest.json"), "utf8"),
) as ExtensionManifest;

const CLI = "@unbrained/pm-cli";
const EXACT_VERSION = /^\d+\.\d+\.\d+$/;

/**
* Two independent systems enforce the pm CLI compatibility floor, and each reads
* a different declaration.
*
* `npm` enforces `peerDependencies["@unbrained/pm-cli"]` at install time, and it
* never sees a globally installed host CLI. The pm CLI itself enforces the
* top-level `pm_min_version` in `manifest.json` — it refuses to install or
* activate an extension whose floor exceeds the running CLI, and reports
* `extension_pm_min_version_unmet` from `pm health`. Neither reads the other's
* field, and a floor written into a field nothing reads is silently inert.
*
* These tests bind the two declarations to the same version so that whichever
* enforcement path a consumer takes, it applies the same floor.
*/
/**
* Whether `pinned` is the same version as `floor` or a later one.
*
* Fleet versions are `YYYY.M.D` with unpadded month and day, so a
* lexicographic comparison is wrong in a way that reads as correct:
* `"2026.8.15" < "2026.8.7"` is `true` as strings. Each component is therefore
* compared as a number, at the first position where the two differ.
*
* Both arguments must already match {@link EXACT_VERSION}; the callers assert
* that first, so no part can be `NaN` here.
*
* @param pinned - The exact version pinned in `devDependencies`.
* @param floor - The exact version declared as the compatibility floor.
* @returns `true` when `pinned` is at or above `floor`.
*/
function atOrAbove(pinned: string, floor: string): boolean {
const floorParts = floor.split(".").map(Number);
const pinnedParts = pinned.split(".").map(Number);
const differing = floorParts.findIndex((part, index) => pinnedParts[index] !== part);
return differing === -1 || pinnedParts[differing]! > floorParts[differing]!;
}

test("the peer dependency declares the CLI floor as a minimum, not an exact pin", () => {
const peer = packageJson.peerDependencies?.[CLI];
assert.ok(peer, `package.json peerDependencies must declare ${CLI}`);
assert.match(
peer,
/^>=\d+\.\d+\.\d+$/,
`peerDependencies["${CLI}"] must be a >= floor so any newer host CLI satisfies it, got ${peer}`,
);
});

test("the extension manifest declares the same floor the CLI actually enforces", () => {
const peer = packageJson.peerDependencies?.[CLI];
assert.ok(peer);
const declared = extensionManifest.pm_min_version;
assert.strictEqual(
typeof declared,
"string",
"manifest.json must declare a top-level pm_min_version — it is the only floor the pm CLI reads, so without it the CLI enforces no floor. npm still enforces the peerDependencies floor, but only for a locally resolved dependency, never for a globally installed host CLI",
);
assert.strictEqual(
declared,
peer.slice(">=".length),
"manifest.json pm_min_version must equal the peerDependencies floor, or npm and the pm CLI enforce different minimums",
);
});

test("the development dependency is an exact pin at or above the declared floor", () => {
const dev = packageJson.devDependencies?.[CLI];
assert.ok(dev, `package.json devDependencies must declare ${CLI}`);
assert.match(
dev,
EXACT_VERSION,
`devDependencies["${CLI}"] must be an exact pin so CI and a working copy resolve the same CLI, got ${dev}`,
);
const declared = extensionManifest.pm_min_version;
assert.strictEqual(
typeof declared,
"string",
`manifest.json pm_min_version must be a string to be comparable, got ${typeof declared}`,
);
assert.match(
declared as string,
EXACT_VERSION,
`manifest.json pm_min_version must be an exact three-part version to be comparable, got ${String(declared)}`,
);
assert.ok(
atOrAbove(dev, declared as string),
`the pinned development CLI ${dev} is below the declared floor ${String(declared)}`,
);
});

test("the version comparison orders YYYY.M.D numerically, not lexicographically", () => {
// In the repository as it stands the pin equals the floor, so the
// greater-than branch of atOrAbove is never reached by the assertion above.
// A comparison whose ordering branch is never executed is not verified by
// the suite passing — V8 does not even report a branch it never reaches —
// so the ordering is exercised here directly.
assert.ok(atOrAbove("2026.8.15", "2026.8.15"), "an equal pin satisfies the floor");
assert.ok(atOrAbove("2026.8.15", "2026.8.7"), "a later day satisfies an earlier floor");
assert.ok(!atOrAbove("2026.8.14", "2026.8.15"), "an earlier day must not satisfy a later floor");
assert.ok(!atOrAbove("2026.8.7", "2026.8.15"), "the lexicographic trap: 2026.8.7 is BELOW 2026.8.15");
assert.ok(atOrAbove("2026.9.1", "2026.8.31"), "a later month outranks any day of an earlier one");
assert.ok(!atOrAbove("2026.7.31", "2026.8.1"), "an earlier month never satisfies a later one");
assert.ok(atOrAbove("2027.1.1", "2026.12.31"), "a later year outranks any date of an earlier one");
assert.ok(!atOrAbove("2025.12.31", "2026.1.1"), "an earlier year never satisfies a later one");
});