@unbrained/pm-cli 2026.9.23 → 2026.9.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/AGENTS.md +6 -20
  3. package/CHANGELOG.md +37 -0
  4. package/dist/cli/error-guidance.d.ts +9 -0
  5. package/dist/cli/error-guidance.js +1 -1
  6. package/dist/cli/help-content.js +1 -1
  7. package/dist/cli-bundle/bundle-manifest.json +153 -153
  8. package/dist/cli-bundle/chunks/{chunk-KOUT5SAA.js → chunk-25KTCTBL.js} +2 -2
  9. package/dist/cli-bundle/chunks/{chunk-E6ZES2X7.js → chunk-3IB4THWX.js} +2 -2
  10. package/dist/cli-bundle/chunks/{chunk-PFWCAI4X.js → chunk-445ARBKY.js} +2 -2
  11. package/dist/cli-bundle/chunks/{chunk-VO7DHENB.js → chunk-5AP67ZXH.js} +3 -3
  12. package/dist/cli-bundle/chunks/{chunk-SXRMM72R.js → chunk-COFQCKF4.js} +2 -2
  13. package/dist/cli-bundle/chunks/{chunk-WBB5B7G5.js → chunk-CZ3VJJEA.js} +2 -2
  14. package/dist/cli-bundle/chunks/{chunk-OBTHQ6UX.js → chunk-GAU55CWI.js} +28 -28
  15. package/dist/cli-bundle/chunks/{chunk-ZSVINTXT.js → chunk-GOWZEBTH.js} +2 -2
  16. package/dist/cli-bundle/chunks/{chunk-7IABMUB6.js → chunk-JDHWZYZU.js} +73 -73
  17. package/dist/cli-bundle/chunks/{chunk-ZWSWR2GX.js → chunk-P7ZSJOWH.js} +2 -2
  18. package/dist/cli-bundle/chunks/{chunk-6WBR6GMW.js → chunk-YABJDOOX.js} +63 -62
  19. package/dist/cli-bundle/chunks/{chunk-NBGSRFZE.js → chunk-YDIGMRLZ.js} +2 -2
  20. package/dist/cli-bundle/chunks/{register-list-query-IFPQ4YRD.js → register-list-query-IZ7ZVTDE.js} +2 -2
  21. package/dist/cli-bundle/chunks/{register-mutation-C3O7OSXW.js → register-mutation-IPAZTFDZ.js} +2 -2
  22. package/dist/cli-bundle/chunks/{register-operations-LGO7DBZ7.js → register-operations-APN33LNI.js} +2 -2
  23. package/dist/cli-bundle/chunks/{register-setup-4EW2PPZD.js → register-setup-HPHIKG5U.js} +2 -2
  24. package/dist/cli-bundle/focused-chunks/{chunk-YE6F5HGF.js → chunk-ABA3KRPY.js} +2 -2
  25. package/dist/cli-bundle/focused-chunks/{chunk-S5JYIFFS.js → chunk-HQPTDJLF.js} +2 -2
  26. package/dist/cli-bundle/focused-chunks/{chunk-FIQYVDLL.js → chunk-IXNNMP2R.js} +2 -2
  27. package/dist/cli-bundle/focused-chunks/chunk-KRIHJH5U.js +75 -0
  28. package/dist/cli-bundle/focused-chunks/{chunk-LZOEUNHR.js → chunk-MK5AKKHK.js} +2 -2
  29. package/dist/cli-bundle/focused-chunks/{chunk-6CWR2QFM.js → chunk-NRHTXBTI.js} +2 -2
  30. package/dist/cli-bundle/focused-chunks/{chunk-L554BKFH.js → chunk-NVVB2HJC.js} +2 -2
  31. package/dist/cli-bundle/focused-chunks/{chunk-PIHQNL32.js → chunk-O7MIBEMK.js} +2 -2
  32. package/dist/cli-bundle/focused-chunks/{chunk-BKXYQQDT.js → chunk-OY3VHCTX.js} +2 -2
  33. package/dist/cli-bundle/focused-chunks/{chunk-N2JYYEHM.js → chunk-PMPFLA6X.js} +2 -2
  34. package/dist/cli-bundle/focused-chunks/{chunk-HJA6OHWT.js → chunk-Q355NPLH.js} +28 -28
  35. package/dist/cli-bundle/focused-chunks/{chunk-BQSKFRL2.js → chunk-TLF6N56J.js} +2 -2
  36. package/dist/cli-bundle/focused-chunks/{chunk-QREDJ77O.js → chunk-UIFVIE5F.js} +2 -2
  37. package/dist/cli-bundle/focused-chunks/{chunk-KMNH4G6Y.js → chunk-V2X37U6J.js} +2 -2
  38. package/dist/cli-bundle/focused-chunks/{chunk-E5AOEXF5.js → chunk-XKH2FC5D.js} +2 -2
  39. package/dist/cli-bundle/focused-chunks/{chunk-BULUJEOL.js → chunk-Y2M4QLAA.js} +2 -2
  40. package/dist/cli-bundle/main.js +22 -22
  41. package/dist/cli-bundle/sdk-authoring.js +1 -1
  42. package/dist/cli-bundle/sdk-contracts.js +1 -1
  43. package/dist/cli-bundle/sdk-core.js +30 -45
  44. package/dist/cli-bundle/sdk-governance.js +1 -1
  45. package/dist/cli-bundle/sdk-graph.js +1 -1
  46. package/dist/cli-bundle/sdk-merge.js +58 -42
  47. package/dist/cli-bundle/sdk-query.js +1 -1
  48. package/dist/cli-bundle/sdk-runtime.js +1 -1
  49. package/dist/cli-bundle/sdk-testing.js +1 -1
  50. package/dist/cli-bundle/sdk.js +9 -10
  51. package/dist/core/policy/workflow-policy-store.js +1 -1
  52. package/dist/core/shared/errors.d.ts +13 -0
  53. package/dist/core/shared/errors.js +1 -1
  54. package/dist/sdk/completion/bash.js +1 -1
  55. package/dist/sdk/governance/health.d.ts +1 -1
  56. package/dist/sdk/governance/health.js +1 -1
  57. package/dist/sdk/init-agent-guidance.d.ts +2 -1
  58. package/dist/sdk/init-agent-guidance.js +2 -2
  59. package/dist/sdk/merge/install.js +2 -2
  60. package/dist/sdk/merge/worktree-config.d.ts +9 -0
  61. package/dist/sdk/merge/worktree-config.js +2 -0
  62. package/dist/sdk/public-surface.json +1 -1
  63. package/docs/AGENT_RECOVERY_AND_GUIDANCE.md +49 -0
  64. package/docs/CHATGPT_CODEX_PLUGIN_IMPLEMENTATION.md +2 -0
  65. package/docs/CLAUDE_CODE_PLUGIN.md +18 -7
  66. package/docs/CODEX_PLUGIN.md +5 -12
  67. package/docs/DEFECT_RECURRENCE.md +8 -1
  68. package/docs/DEPENDENCY_KIND_CONTRACT.md +27 -0
  69. package/docs/MERGE_SAFETY.md +28 -5
  70. package/docs/README.md +1 -0
  71. package/docs/RELEASE_RELIABILITY.md +70 -0
  72. package/docs/RELEASING.md +4 -0
  73. package/docs/TESTING.md +17 -0
  74. package/docs/VALIDATION_PORTABILITY.md +20 -11
  75. package/marketplace.json +2 -2
  76. package/package.json +6 -5
  77. package/packages/pm-beads/package.json +1 -1
  78. package/packages/pm-calendar/package.json +1 -1
  79. package/packages/pm-command-kit/package.json +1 -1
  80. package/packages/pm-digital-twin/package.json +1 -1
  81. package/packages/pm-governance-audit/package.json +1 -1
  82. package/packages/pm-guide-shell/package.json +1 -1
  83. package/packages/pm-kanban/package.json +1 -1
  84. package/packages/pm-lifecycle-hooks/package.json +1 -1
  85. package/packages/pm-linked-test-adapters/package.json +1 -1
  86. package/packages/pm-search-advanced/package.json +1 -1
  87. package/packages/pm-templates/package.json +1 -1
  88. package/packages/pm-todos/package.json +1 -1
  89. package/packages/pm-vcs/package.json +1 -1
  90. package/plugins/pm-claude/.claude-plugin/plugin.json +1 -1
  91. package/plugins/pm-claude/README.md +6 -5
  92. package/plugins/pm-claude/hooks/hooks.json +1 -1
  93. package/plugins/pm-claude/hooks/session-start.mjs +9 -6
  94. package/plugins/pm-claude/package.json +9 -0
  95. package/plugins/pm-claude/scripts/plugin-runtime.mjs +84 -0
  96. package/plugins/pm-claude/scripts/pm-mcp-server.mjs +39 -96
  97. package/plugins/pm-codex/.codex-plugin/plugin.json +1 -1
  98. package/plugins/pm-codex/.mcp.json +0 -3
  99. package/plugins/pm-codex/README.md +1 -1
  100. package/plugins/pm-codex/package.json +9 -0
  101. package/plugins/pm-codex/scripts/plugin-runtime.mjs +84 -0
  102. package/plugins/pm-codex/scripts/pm-mcp-server.mjs +38 -99
  103. package/dist/cli-bundle/focused-chunks/chunk-3JAEZ7WI.js +0 -59
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unbrained/pm-vcs-sdk-exemplar",
3
- "version": "2026.9.23",
3
+ "version": "2026.9.25",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Beyond-PM exemplar: a VCS-style changeset workflow built entirely on public pm SDK and extension primitives.",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pm-claude",
3
3
  "description": "Native pm CLI integration for Claude Code — 28 MCP tools, 5 workflow skills, 14 slash commands, 4 subagents (coordinator, delivery-chain, triage, verification), hybrid TUI task tracking (pm as persistent store + Claude Code task panel as live view), session context injection, and full git-based project management without leaving Claude Code.",
4
- "version": "2026.9.23",
4
+ "version": "2026.9.25",
5
5
  "author": {
6
6
  "name": "unbrained",
7
7
  "url": "https://github.com/unbraind/pm-cli"
@@ -11,7 +11,7 @@ Native pm CLI integration for Claude Code. Use pm project management tools direc
11
11
  | **14 slash commands** | Full lifecycle coverage — status, start, close, triage, audit, search, new, list, calendar, developer, planner, release, workflow, init |
12
12
  | **4 subagents** | `pm-coordinator` (batch/multi-item), `pm-triage-agent` (duplicate-safe item creation), `pm-verification-agent` (evidence + close readiness), and `pm-delivery-chain` (orchestrated delivery) |
13
13
  | **Hybrid TUI tracking** | pm items sync to Claude Code's task panel — pm is the persistent store, the task panel is the live session view |
14
- | **Session hook** | Injects active pm item summary at session start when pm is initialized (uses native modules, no CLI required) |
14
+ | **Session hook** | Injects active pm item summary at session start using the same pinned pm-cli runtime as the MCP server |
15
15
 
16
16
  ## Installation
17
17
 
@@ -192,8 +192,9 @@ All skills and commands implement this pattern for every claimed item:
192
192
 
193
193
  ## Session Context Injection
194
194
 
195
- At session start, the hook runs the published `pm` CLI without requiring a global install:
196
- - Uses `npx --yes --package=@unbrained/pm-cli@latest pm context --limit 5 --json`
195
+ At session start, the hook runs the plugin's exact-version `pm` CLI without requiring a global install:
196
+ - Uses the version pinned in `plugins/pm-claude/package.json` and persisted in the plugin data directory
197
+ - Reuses the installed runtime offline on later sessions
197
198
  - Injects a compact summary of in-progress/open/blocked items
198
199
 
199
200
  Example output:
@@ -215,8 +216,8 @@ Use pm_context tool or /pm-status for full details.
215
216
 
216
217
  ## Requirements
217
218
 
218
- - Node.js ≥ 20
219
- - pm CLI resolved automatically via local dist (in repo) or `npx --yes @unbrained/pm-cli@latest` (no global install needed)
219
+ - Node.js ≥ 22.18.0
220
+ - pm CLI resolved automatically from the plugin's pinned package installation (no global install needed)
220
221
  - Project initialized with `pm init` (or use `/pm-init`)
221
222
 
222
223
  ## Links
@@ -7,7 +7,7 @@
7
7
  {
8
8
  "type": "command",
9
9
  "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.mjs\"",
10
- "timeout": 8,
10
+ "timeout": 130,
11
11
  "async": true
12
12
  }
13
13
  ]
@@ -3,12 +3,13 @@
3
3
  * pm-cli Claude Code session-start hook.
4
4
  *
5
5
  * Injects a brief pm context summary into the session when pm is initialized
6
- * in the current workspace. Uses the published pm CLI through npx without
6
+ * in the current workspace. Uses the plugin's exact-version runtime without
7
7
  * requiring a global install. Exits silently if pm is not set up.
8
8
  */
9
9
  import { existsSync } from "node:fs";
10
10
  import { join } from "node:path";
11
- import { execSync } from "node:child_process";
11
+ import { execFileSync } from "node:child_process";
12
+ import { resolvePluginRuntime } from "../scripts/plugin-runtime.mjs";
12
13
 
13
14
  const workspace = process.cwd();
14
15
  const pmSettingsPath = join(workspace, ".agents", "pm", "settings.json");
@@ -40,10 +41,12 @@ function formatSummary(ctx) {
40
41
  );
41
42
  }
42
43
 
43
- function tryNpxContext() {
44
+ async function readContext() {
44
45
  try {
45
- const raw = execSync(
46
- "npx -y --package=@unbrained/pm-cli@latest pm context --limit 5 --json",
46
+ const runtime = await resolvePluginRuntime();
47
+ const raw = execFileSync(
48
+ process.execPath,
49
+ [runtime.cli, "context", "--limit", "5", "--json"],
47
50
  {
48
51
  cwd: workspace,
49
52
  encoding: "utf-8",
@@ -58,7 +61,7 @@ function tryNpxContext() {
58
61
  }
59
62
 
60
63
  try {
61
- const ctx = tryNpxContext();
64
+ const ctx = await readContext();
62
65
  if (!ctx) process.exit(0);
63
66
 
64
67
  const message = formatSummary(ctx);
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "pm-claude-runtime",
3
+ "private": true,
4
+ "type": "module",
5
+ "version": "2026.9.25",
6
+ "dependencies": {
7
+ "@unbrained/pm-cli": "2026.9.25"
8
+ }
9
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Resolve the exact pm-cli runtime declared by a copied agent plugin.
3
+ * This file is copied into each plugin by gen-plugin-mcp-wrappers.mjs so a
4
+ * marketplace cache never needs to reach back into the repository checkout.
5
+ */
6
+ import { spawnSync } from "node:child_process";
7
+ import { access, mkdir, mkdtemp, readFile, rename, rm } from "node:fs/promises";
8
+ import os from "node:os";
9
+ import path from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+
12
+ const pluginRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
13
+ const packageName = "@unbrained/pm-cli";
14
+ const versionPattern = /^[1-9]\d{3}\.[1-9]\d*\.[1-9]\d*(?:-[1-9]\d*)?$/;
15
+
16
+ /** Require both the declared version and the executable shipped by that package. */
17
+ async function installedRuntime(root, version) {
18
+ const packageRoot = path.join(root, "node_modules", "@unbrained", "pm-cli");
19
+ try {
20
+ const manifest = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"));
21
+ if (manifest.version !== version) return null;
22
+ const server = path.join(packageRoot, "dist", "mcp", "server.js");
23
+ const cli = path.join(packageRoot, "dist", "cli.js");
24
+ await access(server);
25
+ await access(cli);
26
+ return { server, cli, version };
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ /** Install into a private staging directory and publish only a complete tree. */
33
+ async function installRuntime(dataRoot, version, installer) {
34
+ await mkdir(dataRoot, { recursive: true });
35
+ const finalRoot = path.join(dataRoot, `v${version}`);
36
+ const existing = await installedRuntime(finalRoot, version);
37
+ if (existing) return existing;
38
+ const stagingRoot = await mkdtemp(path.join(dataRoot, `.install-${version}-`));
39
+ try {
40
+ const args = ["install", "--prefix", stagingRoot, "--no-save", "--ignore-scripts", "--no-audit", "--no-fund", "--package-lock=false", `${packageName}@${version}`];
41
+ const windows = process.platform === "win32";
42
+ // The command text contains only fixed tokens and a validated version.
43
+ // Put the caller-selected path in the child environment so cmd.exe never
44
+ // parses path metacharacters as command syntax.
45
+ const command = windows ? process.env.ComSpec || "cmd.exe" : "npm";
46
+ const commandArgs = windows
47
+ ? ["/d", "/v:off", "/s", "/c", `"npm.cmd ${args.map((arg) => arg === stagingRoot ? '"%PM_PLUGIN_STAGING_ROOT%"' : arg).join(" ")}"`]
48
+ : args;
49
+ const result = installer(command, commandArgs, {
50
+ encoding: "utf8", timeout: 120000, stdio: ["ignore", "pipe", "pipe"],
51
+ ...(windows ? { windowsVerbatimArguments: true, env: { ...process.env, PM_PLUGIN_STAGING_ROOT: stagingRoot } } : {}),
52
+ });
53
+ if (result.error || result.status !== 0 || !(await installedRuntime(stagingRoot, version))) {
54
+ throw new Error(`Cannot install ${packageName}@${version}: ${result.error?.message || result.stderr?.trim() || "incomplete package"}`);
55
+ }
56
+ try {
57
+ await rename(stagingRoot, finalRoot);
58
+ } catch (error) {
59
+ if (!(await installedRuntime(finalRoot, version))) {
60
+ throw new Error(`Plugin runtime at ${finalRoot} is incomplete; remove that directory and retry.`, { cause: error });
61
+ }
62
+ }
63
+ const runtime = await installedRuntime(finalRoot, version);
64
+ if (!runtime) throw new Error(`Plugin runtime at ${finalRoot} is incomplete; remove that directory and retry.`);
65
+ return runtime;
66
+ } finally {
67
+ await rm(stagingRoot, { recursive: true, force: true });
68
+ }
69
+ }
70
+
71
+ /** Resolve a copied plugin's runtime without ever consulting npm's latest tag. */
72
+ export async function resolvePluginRuntime(options = {}) {
73
+ const root = options.pluginRoot ?? pluginRoot;
74
+ const plugin = JSON.parse(await readFile(path.join(root, "package.json"), "utf8"));
75
+ const version = plugin.dependencies?.[packageName];
76
+ if (typeof version !== "string" || !versionPattern.test(version) || plugin.version !== version) {
77
+ throw new Error(`Plugin ${plugin.name ?? root} needs an exact pm-cli dependency matching its version.`);
78
+ }
79
+ const bundled = await installedRuntime(root, version);
80
+ if (bundled) return bundled;
81
+ const dataRoot = process.env.PLUGIN_DATA || process.env.CLAUDE_PLUGIN_DATA ||
82
+ path.join(os.homedir(), ".cache", "pm-cli", "plugins", path.basename(root));
83
+ return installRuntime(dataRoot, version, options.installer ?? spawnSync);
84
+ }
@@ -1,108 +1,51 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * This file is auto-generated by scripts/gen-plugin-mcp-wrappers.mjs.
4
- * Do not edit directly.
3
+ * Cached-plugin MCP launcher, copied verbatim into Claude and Codex plugins.
4
+ * The server runs from an exact-version package, never from an npm tag.
5
5
  */
6
6
  import { spawn } from "node:child_process";
7
- import { access } from "node:fs/promises";
7
+ import { access, readFile } from "node:fs/promises";
8
8
  import path from "node:path";
9
- import { fileURLToPath, pathToFileURL } from "node:url";
10
- const here = path.dirname(fileURLToPath(import.meta.url));
9
+ import { fileURLToPath } from "node:url";
10
+ import { resolvePluginRuntime } from "./plugin-runtime.mjs";
11
11
 
12
- async function exists(target) {
13
- try { await access(target); return true; } catch { return false; }
14
- }
12
+ const here = path.dirname(fileURLToPath(import.meta.url));
15
13
 
16
- function isImportUrl(target) {
17
- if (/^[A-Za-z]:[\\/]/.test(target)) {
18
- return false;
19
- }
14
+ /** Return an explicit local server path or this plugin's matching checkout build. */
15
+ async function localServer() {
16
+ const override = process.env.PM_CLI_MCP_SERVER;
17
+ if (override) {
18
+ const target = override.startsWith("file:") ? fileURLToPath(override) : path.resolve(override);
19
+ await access(target);
20
+ return target;
21
+ }
22
+ const repoRoot = path.resolve(here, "..", "..", "..");
23
+ const pluginRoot = path.resolve(here, "..");
24
+ const candidate = path.join(repoRoot, "dist", "mcp", "server.js");
20
25
  try {
21
- return new URL(target).protocol.length > 0;
26
+ const [repo, plugin] = await Promise.all([
27
+ readFile(path.join(repoRoot, "package.json"), "utf8").then(JSON.parse),
28
+ readFile(path.join(pluginRoot, "package.json"), "utf8").then(JSON.parse),
29
+ ]);
30
+ if (repo.name !== "@unbrained/pm-cli" || repo.version !== plugin.version ||
31
+ plugin.dependencies?.[repo.name] !== repo.version) return null;
32
+ await access(candidate);
33
+ return candidate;
22
34
  } catch {
23
- return false;
24
- }
25
- }
26
-
27
- function isMissingServerModule(error) {
28
- return typeof error === "object" && error !== null && error.code === "ERR_MODULE_NOT_FOUND";
29
- }
30
-
31
- async function importServerModule(target) {
32
- try {
33
- return await import(target);
34
- } catch (error) {
35
- if (isMissingServerModule(error)) {
36
- return null;
37
- }
38
- throw error;
35
+ return null;
39
36
  }
40
37
  }
41
38
 
42
- async function findRepoServer() {
43
- let cursor = here;
44
- for (let depth = 0; depth < 10; depth += 1) {
45
- const candidate = path.join(cursor, "dist", "mcp", "server.js");
46
- if (await exists(candidate)) {
47
- return candidate;
48
- }
49
- const parent = path.dirname(cursor);
50
- if (parent === cursor) {
51
- break;
52
- }
53
- cursor = parent;
54
- }
55
- return null;
56
- }
57
-
58
- async function startServer(target) {
59
- if (!target) {
60
- return false;
61
- }
62
- if (isImportUrl(target)) {
63
- let url;
64
- try {
65
- url = new URL(target);
66
- } catch {
67
- return false;
68
- }
69
- if (url.protocol === "file:") {
70
- let filePath;
71
- try {
72
- filePath = fileURLToPath(url);
73
- } catch {
74
- return false;
75
- }
76
- if (!(await exists(filePath))) {
77
- return false;
78
- }
79
- }
80
- const server = await importServerModule(target);
81
- if (!server) {
82
- return false;
83
- }
84
- server.startMcpServer();
85
- return true;
86
- }
87
- if (!(await exists(target))) {
88
- return false;
89
- }
90
- const server = await importServerModule(pathToFileURL(path.resolve(target)).href);
91
- if (!server) {
92
- return false;
93
- }
94
- server.startMcpServer();
95
- return true;
96
- }
97
-
98
- if (!(await startServer(process.env.PM_CLI_MCP_SERVER)) && !(await startServer(await findRepoServer()))) {
99
- const npxCommand = process.platform === "win32" ? "npx.cmd" : "npx";
100
- const child = spawn(npxCommand, ["-y", "--package=@unbrained/pm-cli@latest", "pm-mcp"], { stdio: "inherit", env: process.env });
101
- child.on("exit", (code, signal) => {
102
- if (signal) {
103
- process.kill(process.pid, signal);
104
- return;
105
- }
106
- process.exit(code ?? 1);
107
- });
108
- }
39
+ const server = (await localServer()) ?? (await resolvePluginRuntime()).server;
40
+ const child = spawn(process.execPath, [server], { stdio: "inherit", env: process.env });
41
+ child.on("error", (error) => {
42
+ console.error(`pm-mcp plugin runtime failed: ${error.message}`);
43
+ process.exitCode = 1;
44
+ });
45
+ child.on("exit", (code, signal) => {
46
+ if (signal) {
47
+ process.kill(process.pid, signal);
48
+ } else {
49
+ process.exitCode = code ?? 1;
50
+ }
51
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pm-codex",
3
- "version": "2026.9.23",
3
+ "version": "2026.9.25",
4
4
  "description": "Native Codex integration for pm-cli with bundled MCP tools, skills, commands, and agent workflows.",
5
5
  "author": {
6
6
  "name": "unbrained",
@@ -5,9 +5,6 @@
5
5
  "args": [
6
6
  "./scripts/pm-mcp-server.mjs"
7
7
  ],
8
- "env": {
9
- "PM_AUTHOR": "codex-agent"
10
- },
11
8
  "note": "Native pm-cli MCP server. It imports pm command modules directly and does not spawn the pm CLI for tool calls."
12
9
  }
13
10
  }
@@ -17,7 +17,7 @@ codex plugin marketplace add .
17
17
 
18
18
  Restart Codex, then install **pm CLI** from the repo marketplace. The bundled MCP server starts through `plugins/pm-codex/scripts/pm-mcp-server.mjs`.
19
19
 
20
- For a published package install, keep `@unbrained/pm-cli` available through npm. The launcher uses the local repo build when present and falls back to `npx -y --package=@unbrained/pm-cli@latest pm-mcp` when the plugin is cached outside the repository.
20
+ The launcher uses the local repo build when present. In a copied plugin cache it installs the exact `@unbrained/pm-cli` version declared in this plugin's `package.json` into the plugin data directory, then starts that server directly with Node. The runtime is reused offline after the first installation. If the pinned version is unavailable, startup fails with an install error instead of silently running a different release.
21
21
 
22
22
  ## Native Tools
23
23
 
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "pm-codex-runtime",
3
+ "private": true,
4
+ "type": "module",
5
+ "version": "2026.9.25",
6
+ "dependencies": {
7
+ "@unbrained/pm-cli": "2026.9.25"
8
+ }
9
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Resolve the exact pm-cli runtime declared by a copied agent plugin.
3
+ * This file is copied into each plugin by gen-plugin-mcp-wrappers.mjs so a
4
+ * marketplace cache never needs to reach back into the repository checkout.
5
+ */
6
+ import { spawnSync } from "node:child_process";
7
+ import { access, mkdir, mkdtemp, readFile, rename, rm } from "node:fs/promises";
8
+ import os from "node:os";
9
+ import path from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+
12
+ const pluginRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
13
+ const packageName = "@unbrained/pm-cli";
14
+ const versionPattern = /^[1-9]\d{3}\.[1-9]\d*\.[1-9]\d*(?:-[1-9]\d*)?$/;
15
+
16
+ /** Require both the declared version and the executable shipped by that package. */
17
+ async function installedRuntime(root, version) {
18
+ const packageRoot = path.join(root, "node_modules", "@unbrained", "pm-cli");
19
+ try {
20
+ const manifest = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"));
21
+ if (manifest.version !== version) return null;
22
+ const server = path.join(packageRoot, "dist", "mcp", "server.js");
23
+ const cli = path.join(packageRoot, "dist", "cli.js");
24
+ await access(server);
25
+ await access(cli);
26
+ return { server, cli, version };
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ /** Install into a private staging directory and publish only a complete tree. */
33
+ async function installRuntime(dataRoot, version, installer) {
34
+ await mkdir(dataRoot, { recursive: true });
35
+ const finalRoot = path.join(dataRoot, `v${version}`);
36
+ const existing = await installedRuntime(finalRoot, version);
37
+ if (existing) return existing;
38
+ const stagingRoot = await mkdtemp(path.join(dataRoot, `.install-${version}-`));
39
+ try {
40
+ const args = ["install", "--prefix", stagingRoot, "--no-save", "--ignore-scripts", "--no-audit", "--no-fund", "--package-lock=false", `${packageName}@${version}`];
41
+ const windows = process.platform === "win32";
42
+ // The command text contains only fixed tokens and a validated version.
43
+ // Put the caller-selected path in the child environment so cmd.exe never
44
+ // parses path metacharacters as command syntax.
45
+ const command = windows ? process.env.ComSpec || "cmd.exe" : "npm";
46
+ const commandArgs = windows
47
+ ? ["/d", "/v:off", "/s", "/c", `"npm.cmd ${args.map((arg) => arg === stagingRoot ? '"%PM_PLUGIN_STAGING_ROOT%"' : arg).join(" ")}"`]
48
+ : args;
49
+ const result = installer(command, commandArgs, {
50
+ encoding: "utf8", timeout: 120000, stdio: ["ignore", "pipe", "pipe"],
51
+ ...(windows ? { windowsVerbatimArguments: true, env: { ...process.env, PM_PLUGIN_STAGING_ROOT: stagingRoot } } : {}),
52
+ });
53
+ if (result.error || result.status !== 0 || !(await installedRuntime(stagingRoot, version))) {
54
+ throw new Error(`Cannot install ${packageName}@${version}: ${result.error?.message || result.stderr?.trim() || "incomplete package"}`);
55
+ }
56
+ try {
57
+ await rename(stagingRoot, finalRoot);
58
+ } catch (error) {
59
+ if (!(await installedRuntime(finalRoot, version))) {
60
+ throw new Error(`Plugin runtime at ${finalRoot} is incomplete; remove that directory and retry.`, { cause: error });
61
+ }
62
+ }
63
+ const runtime = await installedRuntime(finalRoot, version);
64
+ if (!runtime) throw new Error(`Plugin runtime at ${finalRoot} is incomplete; remove that directory and retry.`);
65
+ return runtime;
66
+ } finally {
67
+ await rm(stagingRoot, { recursive: true, force: true });
68
+ }
69
+ }
70
+
71
+ /** Resolve a copied plugin's runtime without ever consulting npm's latest tag. */
72
+ export async function resolvePluginRuntime(options = {}) {
73
+ const root = options.pluginRoot ?? pluginRoot;
74
+ const plugin = JSON.parse(await readFile(path.join(root, "package.json"), "utf8"));
75
+ const version = plugin.dependencies?.[packageName];
76
+ if (typeof version !== "string" || !versionPattern.test(version) || plugin.version !== version) {
77
+ throw new Error(`Plugin ${plugin.name ?? root} needs an exact pm-cli dependency matching its version.`);
78
+ }
79
+ const bundled = await installedRuntime(root, version);
80
+ if (bundled) return bundled;
81
+ const dataRoot = process.env.PLUGIN_DATA || process.env.CLAUDE_PLUGIN_DATA ||
82
+ path.join(os.homedir(), ".cache", "pm-cli", "plugins", path.basename(root));
83
+ return installRuntime(dataRoot, version, options.installer ?? spawnSync);
84
+ }
@@ -1,112 +1,51 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * This file is auto-generated by scripts/gen-plugin-mcp-wrappers.mjs.
4
- * Do not edit directly.
3
+ * Cached-plugin MCP launcher, copied verbatim into Claude and Codex plugins.
4
+ * The server runs from an exact-version package, never from an npm tag.
5
5
  */
6
6
  import { spawn } from "node:child_process";
7
- import { access } from "node:fs/promises";
7
+ import { access, readFile } from "node:fs/promises";
8
8
  import path from "node:path";
9
- import { fileURLToPath, pathToFileURL } from "node:url";
9
+ import { fileURLToPath } from "node:url";
10
+ import { resolvePluginRuntime } from "./plugin-runtime.mjs";
10
11
 
11
- async function canRead(target) {
12
- try {
13
- await access(target);
14
- return true;
15
- } catch {
16
- return false;
17
- }
18
- }
12
+ const here = path.dirname(fileURLToPath(import.meta.url));
19
13
 
20
- function hasImportProtocol(target) {
21
- if (/^[A-Za-z]:[\\/]/.test(target)) {
22
- return false;
14
+ /** Return an explicit local server path or this plugin's matching checkout build. */
15
+ async function localServer() {
16
+ const override = process.env.PM_CLI_MCP_SERVER;
17
+ if (override) {
18
+ const target = override.startsWith("file:") ? fileURLToPath(override) : path.resolve(override);
19
+ await access(target);
20
+ return target;
23
21
  }
22
+ const repoRoot = path.resolve(here, "..", "..", "..");
23
+ const pluginRoot = path.resolve(here, "..");
24
+ const candidate = path.join(repoRoot, "dist", "mcp", "server.js");
24
25
  try {
25
- return new URL(target).protocol.length > 0;
26
+ const [repo, plugin] = await Promise.all([
27
+ readFile(path.join(repoRoot, "package.json"), "utf8").then(JSON.parse),
28
+ readFile(path.join(pluginRoot, "package.json"), "utf8").then(JSON.parse),
29
+ ]);
30
+ if (repo.name !== "@unbrained/pm-cli" || repo.version !== plugin.version ||
31
+ plugin.dependencies?.[repo.name] !== repo.version) return null;
32
+ await access(candidate);
33
+ return candidate;
26
34
  } catch {
27
- return false;
28
- }
29
- }
30
-
31
- function isMissingLaunchModule(error) {
32
- return typeof error === "object" && error !== null && error.code === "ERR_MODULE_NOT_FOUND";
33
- }
34
-
35
- async function importLaunchModule(target) {
36
- try {
37
- return await import(target);
38
- } catch (error) {
39
- if (isMissingLaunchModule(error)) {
40
- return null;
41
- }
42
- throw error;
43
- }
44
- }
45
-
46
- async function locateRepoServer() {
47
- let cursor = path.dirname(fileURLToPath(import.meta.url));
48
- for (let depth = 0; depth < 10; depth += 1) {
49
- const candidate = path.join(cursor, "dist", "mcp", "server.js");
50
- if (await canRead(candidate)) {
51
- return candidate;
52
- }
53
- const parent = path.dirname(cursor);
54
- if (parent === cursor) {
55
- return null;
56
- }
57
- cursor = parent;
35
+ return null;
58
36
  }
59
- return null;
60
37
  }
61
38
 
62
- async function launchServer(target) {
63
- if (!target) {
64
- return false;
65
- }
66
- if (hasImportProtocol(target)) {
67
- let url;
68
- try {
69
- url = new URL(target);
70
- } catch {
71
- return false;
72
- }
73
- if (url.protocol === "file:") {
74
- let filePath;
75
- try {
76
- filePath = fileURLToPath(url);
77
- } catch {
78
- return false;
79
- }
80
- if (!(await canRead(filePath))) {
81
- return false;
82
- }
83
- }
84
- const server = await importLaunchModule(target);
85
- if (!server) {
86
- return false;
87
- }
88
- server.startMcpServer();
89
- return true;
90
- }
91
- if (!(await canRead(target))) {
92
- return false;
93
- }
94
- const server = await importLaunchModule(pathToFileURL(path.resolve(target)).href);
95
- if (!server) {
96
- return false;
97
- }
98
- server.startMcpServer();
99
- return true;
100
- }
101
-
102
- if (!(await launchServer(process.env.PM_CLI_MCP_SERVER)) && !(await launchServer(await locateRepoServer()))) {
103
- const npxCommand = process.platform === "win32" ? "npx.cmd" : "npx";
104
- const child = spawn(npxCommand, ["-y", "--package=@unbrained/pm-cli@latest", "pm-mcp"], { stdio: "inherit", env: process.env });
105
- child.on("exit", (code, signal) => {
106
- if (signal) {
107
- process.kill(process.pid, signal);
108
- return;
109
- }
110
- process.exit(code ?? 1);
111
- });
112
- }
39
+ const server = (await localServer()) ?? (await resolvePluginRuntime()).server;
40
+ const child = spawn(process.execPath, [server], { stdio: "inherit", env: process.env });
41
+ child.on("error", (error) => {
42
+ console.error(`pm-mcp plugin runtime failed: ${error.message}`);
43
+ process.exitCode = 1;
44
+ });
45
+ child.on("exit", (code, signal) => {
46
+ if (signal) {
47
+ process.kill(process.pid, signal);
48
+ } else {
49
+ process.exitCode = code ?? 1;
50
+ }
51
+ });