fix(plugin): pin the plugin server launch to the release version

Both manifests started `rea-agents@latest`, so a server start could run a
different package version from the installed plugin and bundled skill, and
would execute whatever npm's latest tag pointed at. Pin the launch to the
manifest version and have Release Please rewrite it with each release.

Release Please reserializes JSON extra-files with JSON.stringify, which the
formatter would reject in every release PR. Store both manifests in that
form, exempt them from the formatter, and assert the form in conformance.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
GamerDubzandClaude Opus 5.5 committed 2026-10-12 09:50:02 +02:00
1 parent 2f134aaa7e
commit 102661ca6f
6 files changed
+110 -58

No files matched your search

+8 -2
View File
@@ -15,11 +15,17 @@
"decompiler",
"mcp-server"
],
"skills": ["./.agents/skills/reverse-engineer-anything"],
"skills": [
"./.agents/skills/reverse-engineer-anything"
],
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@latest", "mcp"]
"args": [
"-y",
"[email protected]",
"mcp"
]
}
}
}
+13 -3
View File
@@ -15,11 +15,17 @@
"decompiler",
"mcp-server"
],
"skills": ["./.agents/skills/reverse-engineer-anything"],
"skills": [
"./.agents/skills/reverse-engineer-anything"
],
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@latest", "mcp"],
"args": [
"-y",
"[email protected]",
"mcp"
],
"startup_timeout_sec": 30
}
},
@@ -29,7 +35,11 @@
"longDescription": "Use REA to explain shipped behavior from native binaries, managed assemblies, Electron and JavaScript bundles, packages, firmware, and browser targets, with evidence that separates observations from inferences.",
"developerName": "morluto",
"category": "Developer Tools",
"capabilities": ["Interactive", "Read", "Write"],
"capabilities": [
"Interactive",
"Read",
"Write"
],
"websiteURL": "https://rea.tools/",
"defaultPrompt": [
"Use REA to explain how this binary implements a feature.",
+2
View File
@@ -10,6 +10,8 @@
"node_modules/**",
"CHANGELOG.md",
"package-lock.json",
".claude-plugin/plugin.json",
".codex-plugin/plugin.json",
"_reference/**",
"third_party/**"
]
+5 -4
View File
@@ -978,8 +978,8 @@ The repository root is both a Claude Code plugin
(`.claude-plugin/plugin.json`) and a Codex plugin
(`.codex-plugin/plugin.json`). Each manifest bundles the
`reverse-engineer-anything` skill from `.agents/skills/` and registers the
stdio server as `npx -y rea-agents@latest mcp`, without a second distribution
artifact.
stdio server through `npx`, pinned to the exact package version in that
manifest, without a second distribution artifact.
The repository is also its own single-plugin marketplace, through
`.claude-plugin/marketplace.json` and `.agents/plugins/marketplace.json`.
@@ -997,8 +997,9 @@ In Codex, add the marketplace and then install REA from the plugin list:
codex plugin marketplace add morluto/rea
```
Each release updates both manifest versions, which is when installed copies
refresh the bundled skill. A plugin does not run `rea setup`, so provider
Each release updates both manifest versions and their pinned server version
together, which is when installed copies refresh the bundled skill and move to
the new server. A plugin does not run `rea setup`, so provider
configuration still follows the sections above. Use the plugin or a
`rea setup` registration for a client, not both, to avoid two `rea` servers.
+10
View File
@@ -37,10 +37,20 @@
"path": ".claude-plugin/plugin.json",
"jsonpath": "$.version"
},
{
"type": "json",
"path": ".claude-plugin/plugin.json",
"jsonpath": "$.mcpServers.rea.args[1]"
},
{
"type": "json",
"path": ".codex-plugin/plugin.json",
"jsonpath": "$.version"
},
{
"type": "json",
"path": ".codex-plugin/plugin.json",
"jsonpath": "$.mcpServers.rea.args[1]"
}
]
}
+72 -49
View File
@@ -47,11 +47,13 @@ describe("release configuration", () => {
path: "server.json",
jsonpath: "$.packages[0].version",
},
...pluginManifests.map((path) => ({
type: "json",
path,
jsonpath: "$.version",
})),
...pluginManifests.flatMap((path) =>
["$.version", "$.mcpServers.rea.args[1]"].map((jsonpath) => ({
type: "json",
path,
jsonpath,
})),
),
],
},
},
@@ -89,50 +91,6 @@ describe("release configuration", () => {
expect(/^## \[([^\]]+)\]/mu.exec(changelog)?.[1]).toBe(packageJson.version);
});
it.each(pluginManifests)(
"keeps the plugin manifest %s aligned with the package version",
async (path) => {
const packageJson = (await readJson("package.json")) as {
version: string;
};
expect(await readJson(path)).toMatchObject({
version: packageJson.version,
});
},
);
it("declares the interface fields Codex plugin validation requires", async () => {
const manifest = (await readJson(".codex-plugin/plugin.json")) as {
interface: Record<string, unknown>;
};
for (const field of [
"displayName",
"shortDescription",
"longDescription",
"developerName",
"category",
])
expect(manifest.interface[field]).toEqual(expect.stringMatching(/\S/u));
expect(manifest.interface.capabilities).toEqual(
expect.arrayContaining([expect.stringMatching(/\S/u)]),
);
expect(manifest.interface.defaultPrompt).toEqual(
expect.arrayContaining([expect.stringMatching(/\S/u)]),
);
});
it("carries the owned Codex startup timeout in the Codex plugin server entry", async () => {
expect(await readJson(".codex-plugin/plugin.json")).toMatchObject({
mcpServers: {
rea: {
startup_timeout_sec: MCP_STARTUP_POLICY.codexStartupTimeoutSeconds,
},
},
});
});
it("keeps the npm package and MCP Registry metadata aligned", async () => {
const packageJson = (await readJson("package.json")) as {
name: string;
@@ -174,3 +132,68 @@ describe("release configuration", () => {
});
});
});
describe("plugin manifests", () => {
it.each(pluginManifests)(
"pins the plugin manifest %s and its server launch to the package version",
async (path) => {
const packageJson = (await readJson("package.json")) as {
name: string;
version: string;
};
expect(await readJson(path)).toMatchObject({
version: packageJson.version,
mcpServers: {
rea: {
command: "npx",
args: ["-y", `${packageJson.name}@${packageJson.version}`, "mcp"],
},
},
});
},
);
it.each(pluginManifests)(
"stores %s in the form Release Please rewrites it to",
async (path) => {
// Release Please reserializes JSON extra-files, so any other layout
// would change formatting in every release PR.
const content = await readFile(path, "utf8");
expect(content).toBe(`${JSON.stringify(JSON.parse(content), null, 2)}
`);
},
);
it("declares the interface fields Codex plugin validation requires", async () => {
const manifest = (await readJson(".codex-plugin/plugin.json")) as {
interface: Record<string, unknown>;
};
for (const field of [
"displayName",
"shortDescription",
"longDescription",
"developerName",
"category",
])
expect(manifest.interface[field]).toEqual(expect.stringMatching(/\S/u));
expect(manifest.interface.capabilities).toEqual(
expect.arrayContaining([expect.stringMatching(/\S/u)]),
);
expect(manifest.interface.defaultPrompt).toEqual(
expect.arrayContaining([expect.stringMatching(/\S/u)]),
);
});
it("carries the owned Codex startup timeout in the Codex plugin server entry", async () => {
expect(await readJson(".codex-plugin/plugin.json")).toMatchObject({
mcpServers: {
rea: {
startup_timeout_sec: MCP_STARTUP_POLICY.codexStartupTimeoutSeconds,
},
},
});
});
});