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
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,38 @@

## [Unreleased]

## [1.2.2] - 2026-07-29

**Which packages this version covers.** Only `@beyondnet/evolith-cli` and
`@beyondnet/evolith-mcp`, the two that declared the stale dependency range. No
other package changed, and republishing them would only add noise to the
registry.

### Bug Fixes

* **deps:** the CLI and the MCP server now resolve `@beyondnet/evolith-sdk@^2.0.0`
instead of `^1.1.0` (GT-634). The SDK publishes `1.0.0`, `1.1.0` and `2.0.0`
with nothing in between, so a caret range under 1.x pinned **exactly 1.1.0** —
published 2026-07-18, five days before the security wave that shipped in 2.0.0
on 2026-07-27. Both consumers were published on 2026-07-28, after 2.0.0 existed,
still pointing at the 1.x line, so `npm install @beyondnet/evolith-cli@latest`
installed a pre-wave SDK. **The exposure described in the 1.2.0 section below is
therefore not closed by 1.2.0 alone for this dependency; it is closed here.**

The lockfile made it concrete rather than cosmetic: it pinned the registry
tarball of `sdk@1.1.0`, with integrity hashes, at
`src/sdk/cli/node_modules/@beyondnet/evolith-sdk` and
`src/packages/mcp-server/node_modules/@beyondnet/evolith-sdk`. A clean `npm ci`
fetched the pre-wave SDK and nested it inside each consumer, where it shadows
the workspace link — the top-level symlink to the local 2.0.0 is what a
developer reads, and the nested 1.1.0 is what a fresh install resolves. Both
nested entries disappear once the range is `^2.0.0`.

Neither package re-exports the SDK, and both use only `EvolithRestClient`, so
2.0.0's breaking change (payload types re-exported from `core-domain`,
`.passed` → `.verdict`, `'info'` severity retired) reaches no public API of
either. That is why this is a patch and not a minor.

## [1.2.0] - 2026-07-27

Changes accumulated since the 1.0.0 release of 2026-06-28. Automated changelog
Expand Down
20 changes: 4 additions & 16 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions product/products/smart-cli/product-inventory.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ Inventario generado de la superficie instalable de Evolith CLI y MCP. No editar

| Campo | Valor |
|---|---|
| Package | `@beyondnet/evolith-cli@1.2.1` |
| Package | `@beyondnet/evolith-cli@1.2.2` |
| Binary | `evolith`, `evolith-cli` |
| CLI commands | 35 |
| MCP tools | 50 |
| MCP resources | 12 |
| MCP prompts | 8 |
| MCP transports | `stdio (JSON-RPC 2.0)`, `Streamable HTTP (official MCP SDK)` |
| Phase-gate schemas | 45 |
| Cobertura de statements | 88.09% statements · 88.38% lines |
| Cobertura de statements | pendiente de un run de cobertura |
4 changes: 2 additions & 2 deletions product/products/smart-cli/product-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ Generated inventory of the installable Evolith CLI and MCP surface. Do not edit

| Field | Value |
|---|---|
| Package | `@beyondnet/evolith-cli@1.2.1` |
| Package | `@beyondnet/evolith-cli@1.2.2` |
| Binary | `evolith`, `evolith-cli` |
| CLI commands | 35 |
| MCP tools | 50 |
| MCP resources | 12 |
| MCP prompts | 8 |
| MCP transports | `stdio (JSON-RPC 2.0)`, `Streamable HTTP (official MCP SDK)` |
| Phase-gate schemas | 45 |
| Statement coverage | 88.09% statements · 88.38% lines |
| Statement coverage | pending a coverage run |
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
},
"evidence": {
"closureRecords": 574,
"cliPackage": "@beyondnet/evolith-cli@1.2.1",
"cliPackage": "@beyondnet/evolith-cli@1.2.2",
"adrCount": 139,
"rulesetCount": 175,
"schemaCount": 45
Expand Down
4 changes: 2 additions & 2 deletions src/packages/mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@beyondnet/evolith-mcp",
"version": "1.2.1",
"version": "1.2.2",
"description": "Evolith MCP Gateway — NestJS-based Model Context Protocol server for AI agents. Supports stdio and SSE transports.",
"main": "dist/main.js",
"types": "dist/main.d.ts",
Expand Down Expand Up @@ -41,7 +41,7 @@
"@beyondnet/evolith-core": "^1.2.0",
"@beyondnet/evolith-core-domain": "^1.2.0",
"@beyondnet/evolith-infra-providers": "^1.2.0",
"@beyondnet/evolith-sdk": "^1.1.0",
"@beyondnet/evolith-sdk": "^2.0.0",
"@modelcontextprotocol/sdk": "1.29.0",
"@nestjs/cache-manager": "3.1.3",
"@nestjs/common": "11.1.27",
Expand Down
2 changes: 1 addition & 1 deletion src/rulesets/contracts/evolith-machine-contracts.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"compatibilityPolicy": "semver-major",
"producer": {
"package": "@beyondnet/evolith-cli",
"version": "1.2.1"
"version": "1.2.2"
},
"schemas": [
{
Expand Down
6 changes: 6 additions & 0 deletions src/sdk/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [1.2.2]

### Bug Fixes

* **deps:** resolve `@beyondnet/evolith-sdk@^2.0.0` instead of `^1.1.0` (GT-634). The SDK publishes `1.0.0`, `1.1.0` and `2.0.0` with nothing in between, so the caret range pinned **exactly 1.1.0** — the build of 2026-07-18, five days before the security wave that shipped in 2.0.0. `1.2.1` was published on 2026-07-28, after 2.0.0 existed, so `npm i -g @beyondnet/evolith-cli` installed a pre-wave SDK. The lockfile also pinned that tarball at `src/sdk/cli/node_modules/@beyondnet/evolith-sdk`, where a nested install shadows the workspace link — which is why no local run showed it. Only `EvolithRestClient` is used and the SDK is not re-exported, so 2.0.0's breaking change reaches no part of this package's surface.

## [1.0.2]

### Bug Fixes
Expand Down
4 changes: 2 additions & 2 deletions src/sdk/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@beyondnet/evolith-cli",
"version": "1.2.1",
"version": "1.2.2",
"description": "Evolith CLI - Governance, standards validation, and AI agent integration for satellite repositories",
"main": "dist/main.js",
"bin": {
Expand Down Expand Up @@ -73,7 +73,7 @@
"@beyondnet/evolith-agent-runtime": "^1.1.0",
"@beyondnet/evolith-core-domain": "^1.2.0",
"@beyondnet/evolith-infra-providers": "^1.2.0",
"@beyondnet/evolith-sdk": "^1.1.0",
"@beyondnet/evolith-sdk": "^2.0.0",
"@hono/node-server": "1.19.14",
"@modelcontextprotocol/sdk": "1.29.0",
"@nestjs/common": "11.1.27",
Expand Down
97 changes: 91 additions & 6 deletions src/sdk/cli/scripts/check-install-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
* 1 - an unresolvable specifier, a boot failure, a pack/install failure, or a vacuous scan
*/

import { readdirSync, readFileSync, existsSync, mkdtempSync, mkdirSync, rmSync } from 'node:fs';
import { readdirSync, readFileSync, existsSync, mkdtempSync, mkdirSync, rmSync, renameSync } from 'node:fs';
import { join, resolve, dirname } from 'node:path';
import { tmpdir } from 'node:os';
import { createRequire } from 'node:module';
Expand All @@ -54,6 +54,22 @@ import { fileURLToPath } from 'node:url';
const GUARD = 'check-install-smoke';
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');

/**
* The workspace root — a third place `npm pack` may write the tarball. Found by
* walking up to the lockfile rather than counting directory levels, because
* `--pkg` points this script at packages at different depths.
*/
const REPO_ROOT = (() => {
let dir = PKG_ROOT;
for (let i = 0; i < 8; i += 1) {
if (existsSync(join(dir, 'package-lock.json'))) return dir;
const up = dirname(dir);
if (up === dir) break;
dir = up;
}
return resolve(PKG_ROOT, '..', '..', '..');
})();

/** Both module systems, because a future build may emit ESM. */
const SPECIFIER_RE = /(?:require\(|from\s*)["'](@beyondnet\/[^"']+)["']/g;

Expand Down Expand Up @@ -114,8 +130,21 @@ function fail(lines) {
process.exit(1);
}

/**
* Every child here must be immune to an ambient dry-run.
*
* This guard runs inside `prepublishOnly`, and `npm-release.yml`'s rehearsal mode
* invokes `npm publish --dry-run`, which exports `npm_config_dry_run=true` to that
* script. Inherited, it makes `npm pack` write no tarball and `npm install` install
* nothing — while both still exit 0 — so the guard reported a good artifact as NOT
* installable. Packing and installing into a throwaway directory is this guard's
* measurement, not part of the publish being rehearsed, so the variable is stripped
* for every child rather than at one call site.
*/
function run(cmd, args, opts = {}) {
return spawnSync(cmd, args, { encoding: 'utf8', ...opts });
const env = { ...process.env, ...(opts.env || {}) };
delete env.npm_config_dry_run;
return spawnSync(cmd, args, { encoding: 'utf8', ...opts, env });
}

/**
Expand Down Expand Up @@ -220,13 +249,69 @@ function main(argv) {
if (!spec) {
// Pack THIS package. --ignore-scripts so a prepublishOnly that calls this
// guard cannot recurse into itself.
const packed = run('npm', ['pack', '--ignore-scripts', '--silent', '--pack-destination', temp], { cwd: pkgRoot });
// THIS GUARD COULD NOT RUN UNDER THE RELEASE REHEARSAL, AND SAID SO WRONGLY.
//
// `npm-release.yml`'s dry_run mode runs `npm publish --dry-run`, which puts
// `npm_config_dry_run=true` in the environment of `prepublishOnly` — and
// this guard runs there. The nested `npm pack` below INHERITS it, so npm
// printed the tarball name and wrote no file, and the guard reported "this
// artifact is NOT installable" about a perfectly good artifact. Measured, not
// guessed: `npm_config_dry_run=true npm pack --pack-destination <dir>` prints
// the filename and leaves <dir> empty, on both npm 10 and npm 11.
//
// So the pack is made immune to an ambient dry-run: the variable is stripped
// from the child environment AND `--dry-run=false` is passed. Packing into a
// temp directory is not the operation the rehearsal is rehearsing, and it
// must happen for the rehearsal to mean anything.
//
// Two earlier explanations were wrong and are recorded so they are not
// re-attempted: it is not that `--pack-destination` is ignored on the runner,
// and not that npm majors disagree about the destination. npm 10 honours it.
//
// The location is still DISCOVERED rather than predicted — snapshot the
// candidate directories, pack, take whatever appeared — and a failure prints
// what each directory actually holds, because a slow CI loop must not be
// spent discovering one more location.
const candidates = [temp, pkgRoot, REPO_ROOT];
const tgzIn = (dir) => {
try {
return new Set(readdirSync(dir).filter((f) => f.endsWith('.tgz')));
} catch {
return new Set();
}
};
const before = candidates.map(tgzIn);

const packed = run(
'npm',
['pack', '--ignore-scripts', '--silent', '--dry-run=false', '--pack-destination', temp],
{ cwd: pkgRoot },
);
if (packed.status !== 0) {
fail([`npm pack failed (${packed.status}):`, String(packed.stderr).trim()]);
}
const tarball = String(packed.stdout).trim().split('\n').pop();
spec = join(temp, tarball);
if (!existsSync(spec)) fail([`npm pack reported "${tarball}" but no such tarball exists in ${temp}.`]);
const reported = String(packed.stdout).trim().split('\n').pop();

let found = null;
candidates.forEach((dir, i) => {
if (found) return;
for (const f of tgzIn(dir)) {
if (!before[i].has(f)) { found = { dir, file: f }; return; }
}
});

if (!found) {
fail([
`npm pack reported "${reported}" and no new tarball appeared in any candidate directory:`,
...candidates.map((d, i) => ` ${d}\n holds: ${[...tgzIn(d)].join(', ') || '(no .tgz)'} · before: ${[...before[i]].join(', ') || '(no .tgz)'}`),
'npm prints the filename regardless of where it writes, so the name alone proves nothing.',
]);
}

spec = join(temp, found.file);
// Move it out of the repository if that is where npm put it, rather than
// leaving it for the root-cleanliness guard to trip over.
if (found.dir !== temp) renameSync(join(found.dir, found.file), spec);
}

const installDir = join(temp, 'consumer');
Expand Down
Loading