Files
ruflo/scripts/check-metaharness-pins.mjs
Mr. MakandClaude Opus 5 0a2df2bfd2 fix(metaharness): use the @metaharness/* and ruflo CLI already installed instead of npm-installing at tool-call time (#3366)
On a stock ruflo 3.42.4 install every metaharness_* MCP tool reached for the
npm registry at tool-call time, although the packages it needs are already on
disk. Two causes, in the same six helper files, which is why they ship
together: the helper PINS rejected what ruflo installs, and three code paths
never looked for an installed copy at all. Fixing only the pins leaves darwin
and the memory tools on npx; fixing only the lookups leaves them resolving a
pin that nothing on disk satisfies.

1. Pins (metaharness, @metaharness/darwin)

The metaharness_* tools resolve MetaHarness through the plugin helpers' own
tilde pins, not through package.json. Those pins were never bumped:
_harness.mjs stayed at ~0.3.0 after the CLI declared metaharness ^0.4.1
(b8ec03f34), and _darwin.mjs stayed at ~0.8.0 while the CLI moved
@metaharness/darwin to ~0.9.0 (#2958) and ~0.10.2 (#3262). On a stock install
(metaharness 0.4.2, darwin 0.10.2) findLocalPackageDir() therefore rejected
the bundled metaharness, and every score / genome / mcp_scan / threat_model /
oia_audit / learn call ran
`npm install --prefix ~/.ruflo/metaharness-cache-0.3.0 metaharness@~0.3.0`
at tool-call time — a download of the older 0.3.2 (180 s install budget inside
a 120 s MCP subprocess budget), degraded offline. The darwin tools fetched
darwin 0.8.x through npx while gepa imported the bundled 0.10.2.

- _harness.mjs: METAHARNESS_PIN_VERSION ~0.3.0 -> ~0.4.1 (= CLI ^0.4.1)
- _darwin.mjs:  DARWIN_PIN_VERSION      ~0.8.0 -> ~0.10.2 (= CLI ~0.10.2)
- scripts/check-metaharness-pins.mjs: the watcher compared only the CLI's
  declared ranges (and MH_DARWIN_PIN), so it reported "all current" while the
  pins that decide what runs were two minors behind. It now reads each
  helper's *_PIN_VERSION: the pin must be a `~X.Y.Z` range (the only form
  _invoke.satisfiesTildeRange accepts), and must cover the declared range, or
  admit npm latest when the CLI declares nothing (redblue). An unreadable
  constant is fatal. `satisfies` now shares a `bounds()` helper with the new
  `covers()`; the results for the existing rows are unchanged.
- metaharness-pin-drift.yml also triggers on the three helper files.
- The two current-pin comments in TypeScript (distill-oracle.ts MH_DARWIN_PIN,
  metaharness-tools.ts ADR-153 header) named the old ~0.8.0 plugin pin; they
  now name ~0.10.2. Comments only, no behaviour.

2. No npm/npx at tool-call time (darwin, redblue, memory)

- _darwin.mjs ran `npx -y -p @metaharness/darwin@<pin> metaharness-darwin` on
  every evolve / bench / security_bench call: npm resolved the range each
  time, it ignored the optionalDependency the CLI ships, and it could run a
  different darwin than the gepa import in the same plugin. It now resolves
  like _harness.mjs — installed copy satisfying the pin, then the one-time
  versioned darwin-cache-<pin> that gepa already shares, then
  `process.execPath <abs bin>` with shell:false.
- _redblue.mjs always npm-installed into redblue-cache-<pin> on first use,
  never looking at the installed copy (ruflo 3.42.4 carries 0.1.4). It now
  checks the installed copy first. The chosen CLI path is realpath'd, because
  redblue only dispatches when import.meta.url === file://argv[1]; a
  pnpm-symlinked package dir (or a cache base under /tmp on macOS) otherwise
  makes it exit 0 having done nothing. Its spawn moves to process.execPath
  with shell:false: the argv carries MCP-supplied --config/--out/--in, which
  under the previous `shell: win32` went through cmd.exe unquoted.
- audit-list / audit-trend / oia-audit / similarity ran
  `npx @claude-flow/cli@latest memory ...` — an npm-registry resolution per
  call (the check _harness.mjs already removed for its own npx path), and
  possibly a different CLI version than the server that spawned the tool
  (#3306; cold-cache npx cost #3145/#3154). Without registry access oia_audit
  reported persisted:false and audit_list silently showed 0 records. They now
  call _invoke.runRufloCli(): process.execPath + bin/cli.js of the
  @claude-flow/cli that ships the plugin (<cli>/plugins/ruflo-metaharness, or
  v3/@claude-flow/cli in a checkout). A candidate needs dist/src/index.js next
  to bin/cli.js (mcp-launch.cjs's unbuilt-checkout guard) plus a package.json
  named @claude-flow/cli (a new check, so an unrelated parent directory can
  never qualify). CLI_CORE=1 still opts into `npx @claude-flow/cli-core@alpha`,
  and a layout with no usable local CLI still falls back to
  `npx @claude-flow/cli@latest`.
- drift-from-history.mjs: drop its unused CLI_PKG constant.

The two new lookups end in a spawn of a package bin with MCP-supplied argv, so
they pass findLocalPackageDir(..., { fromCwd: false }) and search only the
ruflo install that owns the plugin — never the tool's cwd or its ancestors.
The pre-existing metaharness lookup keeps its cwd walk unchanged.

Tests (both hermetic, published layout, stub packages, npm/npx PATH trap):

- test-pin-alignment.mjs (+ smoke 17z74a): 3 passed / 9 failed on main,
  12 / 0 here.
- test-no-tool-time-npx.mjs (+ smoke 17r2): 7 passed / 19 failed on main,
  26 / 0 here. It covers both darwin success branches (installed copy and the
  versioned cache), the memory round trip through the shipping CLI, the kept
  contracts (CLI_CORE, npx fallback, a bin/cli.js that is not
  @claude-flow/cli, darwin absent -> degraded exit 0), and the hardening
  above: a darwin/redblue in the tool's own cwd node_modules is never
  executed, a cache dir with package.json but no bin gets one reinstall
  instead of degrading forever, and neither helper spawns the resolved bin
  through a shell.

ADR-150 is unchanged: no static @metaharness import is added, no new
dependency, and every unresolvable path still returns the same
{degraded:true, reason} with exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTedGmPKpEdArVBMMn3JaP
2026-09-20 11:55:51 -04:00

276 lines
15 KiB
JavaScript

#!/usr/bin/env node
// check-metaharness-pins — pin-drift watcher for ruflo's metaharness deps.
//
// WHY: a version pin that was correct at publish time silently rots when the
// upstream ships a new release the pin's range excludes. This is the failure
// mode reported upstream in agent-harness-generator#142 (`@metaharness/darwin`
// caret-locked to 0.2.x, three majors behind) and #149 (META_PROXY_VERSION
// pinned, three releases behind, with no watcher). Ruflo pins three metaharness
// packages; this script diffs each declared range against npm `latest` and
// flags any pin whose range no longer admits the current release.
//
// Checked pins (single source of truth = v3/@claude-flow/cli/package.json).
// Each pin is looked up across dependencies, optionalDependencies, AND
// peerDependencies — an earlier revision searched only the first two, so pins
// moved to (optional) peerDependencies silently vanished from the watcher
// ('undeclared' was not treated as drift). Both failure modes are now fatal:
// - undeclared: the pin is missing from every dependency block
// - peer-only for an installable pin: darwin / flywheel / radio are
// advertised integration surfaces and MUST live in optionalDependencies so
// a clean `npm install` actually materializes them; an optional PEER
// dependency is never auto-installed, which is how a fresh ruflo install
// shipped with zero MetaHarness packages on disk.
// - metaharness — the umbrella / MCP subprocess CLI (npx path; peer ok)
// - @metaharness/router — neural-router.ts dynamic import (peer ok — triple-gated)
// - @metaharness/darwin — Darwin evolve / GEPA subprocess (must be installable)
// - @metaharness/flywheel — receipt/gate interop (must be installable)
// - @metaharness/radio — coordination-policy optimizer (must be installable)
// - @metaharness/turn-credit — recursive turn-level credit assignment, ADR-248 (must be installable)
// Plus a lock-step check: the MH_DARWIN_PIN constant in distill-oracle.ts (used
// for the Tier-1 oracle's `npx @metaharness/darwin@<pin>` calls) must satisfy
// the declared @metaharness/darwin range, so the two can't drift apart.
//
// Plus the ruflo-metaharness PLUGIN helper pins (#3366). The metaharness_* MCP
// tools never read package.json: each helper in plugins/ruflo-metaharness/
// scripts/ carries its own tilde range, and that range alone decides which
// INSTALLED copy the helper accepts (_invoke.findLocalPackageDir) and what it
// npm-installs at tool-call time when no copy qualifies (ensureCachedInstall).
// This watcher used to compare only the CLI's declared ranges, so the helper
// pins drifted unseen (_harness.mjs ~0.3.0 vs declared ^0.4.1; _darwin.mjs
// ~0.8.0 vs declared ~0.10.2) and every tool call re-downloaded an older
// release while the one ruflo shipped sat unused on disk. Rules:
// - the helper pin must be `~X.Y.Z` — _invoke.satisfiesTildeRange(), the
// predicate the helpers actually apply, parses nothing else, so a `^` or
// exact pin rejects every installed copy even though covers() below would
// call it green
// - package declared by the CLI → the helper range must COVER the declared
// range (accept every version a clean install can put on disk); offline-safe
// - package not declared (redblue) → the helper range must admit npm latest
// - constant not found in the helper → fatal (the watcher cannot protect a
// pin it cannot read — same stance as UNDECLARED above)
//
// USAGE
// node scripts/check-metaharness-pins.mjs # exits 1 if any pin is stale
// node scripts/check-metaharness-pins.mjs --format json # CI/issue-body consumable
// node scripts/check-metaharness-pins.mjs --offline # skip npm, only lock-step check
//
// EXIT CODES
// 0 every pin's range admits npm latest (and the darwin constant is in range,
// and every plugin helper pin covers its declared range)
// 1 at least one pin is behind (range excludes latest) OR constant out of range
// OR a plugin helper pin no longer covers its declared range / is not a
// tilde range / is unreadable
// 2 unexpected error (network flake surfaces as a warning, NOT a false drift)
import { readFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { dirname, join } from 'node:path';
const HERE = dirname(fileURLToPath(import.meta.url));
const REPO = join(HERE, '..');
const CLI_PKG = join(REPO, 'v3', '@claude-flow', 'cli', 'package.json');
const DISTILL = join(REPO, 'v3', '@claude-flow', 'cli', 'src', 'services', 'distill-oracle.ts');
const PLUGIN_SCRIPTS = join(REPO, 'plugins', 'ruflo-metaharness', 'scripts');
// The plugin helper constants that pick what the metaharness_* tools run.
const PLUGIN_PINS = [
{ name: 'metaharness', file: '_harness.mjs', constant: 'METAHARNESS_PIN_VERSION' },
{ name: '@metaharness/darwin', file: '_darwin.mjs', constant: 'DARWIN_PIN_VERSION' },
{ name: '@metaharness/redblue', file: '_redblue.mjs', constant: 'REDBLUE_PIN_VERSION' },
];
const ARGS = { format: 'table', offline: false, requireInstalled: false };
for (let i = 2; i < process.argv.length; i++) {
if (process.argv[i] === '--format') ARGS.format = process.argv[++i];
else if (process.argv[i] === '--offline') ARGS.offline = true;
else if (process.argv[i] === '--require-installed') ARGS.requireInstalled = true;
}
/** Parse "1.2.3" → [1,2,3]; tolerant of pre-release/build suffixes. */
function parseVer(v) {
const m = String(v).trim().replace(/^v/, '').match(/^(\d+)\.(\d+)\.(\d+)/);
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
}
/** Half-open [lo, hi) bounds of the caret/tilde/exact forms ruflo uses; null if unparseable. */
function bounds(range) {
const op = range[0] === '^' || range[0] === '~' ? range[0] : '=';
const lo = parseVer(op === '=' ? range : range.slice(1));
if (!lo) return null;
// Upper bound: caret locks the left-most non-zero component; tilde locks minor.
let hi;
if (op === '=') hi = [lo[0], lo[1], lo[2] + 1];
else if (op === '^') hi = lo[0] > 0 ? [lo[0] + 1, 0, 0] : lo[1] > 0 ? [0, lo[1] + 1, 0] : [0, 0, lo[2] + 1];
else hi = [lo[0], lo[1] + 1, 0]; // ~
return { lo, hi };
}
/** Does `latest` satisfy `range`? Supports the caret/tilde/exact forms ruflo uses. */
function satisfies(range, latest) {
const lv = parseVer(latest);
const b = bounds(range);
if (!lv || !b) return null;
return cmp(lv, b.lo) >= 0 && cmp(lv, b.hi) < 0;
}
/** Does `outer` admit every version `inner` admits? (plugin helper pin vs declared range) */
function covers(outer, inner) {
const o = bounds(outer);
const i = bounds(inner);
if (!o || !i) return null;
return cmp(o.lo, i.lo) <= 0 && cmp(i.hi, o.hi) <= 0;
}
function cmp(a, b) { for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] - b[i]; return 0; }
function npmLatest(pkg) {
const out = execFileSync('npm', ['view', pkg, 'version'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
return out.trim();
}
/** Find a package's declared range across every dependency block, in priority order. */
function declaredRange(pkg, name) {
for (const where of ['dependencies', 'optionalDependencies', 'peerDependencies']) {
const range = pkg[where]?.[name];
if (range) return { range, where };
}
return { range: undefined, where: null };
}
async function main() {
const pkg = JSON.parse(readFileSync(CLI_PKG, 'utf-8'));
// installable: true ⇒ the package must be declared in `dependencies` or
// `optionalDependencies` (something npm actually installs); a peer-only
// declaration is a contract break, because optional peers never materialize
// on a clean install.
const WATCHED = [
{ name: 'metaharness', installable: false },
{ name: '@metaharness/router', installable: false },
{ name: '@metaharness/darwin', installable: true },
{ name: '@metaharness/flywheel', installable: true },
{ name: '@metaharness/radio', installable: true },
{ name: '@metaharness/turn-credit', installable: true },
];
const pins = WATCHED.map((w) => ({ ...w, ...declaredRange(pkg, w.name) }));
const rows = [];
for (const p of pins) {
if (!p.range) { rows.push({ ...p, status: 'UNDECLARED', latest: null, note: 'pin not found in any dependency block — the watcher cannot protect an undeclared pin' }); continue; }
if (p.installable && p.where === 'peerDependencies') {
rows.push({ ...p, status: 'PEER-ONLY', latest: null, note: 'declared only as a peer — never installed by a clean `npm install`; move to optionalDependencies' });
continue;
}
if (ARGS.offline) { rows.push({ ...p, status: 'skipped', latest: null, note: 'offline mode' }); continue; }
let latest;
try { latest = npmLatest(p.name); }
catch { rows.push({ ...p, status: 'unknown', latest: null, note: 'npm view failed (network?) — not treated as drift' }); continue; }
const ok = satisfies(p.range, latest);
rows.push({ ...p, latest, status: ok === false ? 'STALE' : ok === true ? 'current' : 'unparseable' });
}
// Lock-step: MH_DARWIN_PIN constant must satisfy the declared darwin range.
let constRow = null;
try {
const m = readFileSync(DISTILL, 'utf-8').match(/MH_DARWIN_PIN\s*=\s*['"]([^'"]+)['"]/);
const declared = declaredRange(pkg, '@metaharness/darwin').range;
if (m && declared) {
const inRange = satisfies(declared, m[1]);
constRow = { name: 'MH_DARWIN_PIN (distill-oracle.ts)', pin: m[1], declared, status: inRange === false ? 'OUT-OF-RANGE' : 'in-range' };
}
} catch { /* non-fatal */ }
// Plugin helper pins (#3366): what the metaharness_* tools actually resolve
// and install. Declared by the CLI → must cover the declared range (no
// network needed); otherwise → must admit npm latest, like the rows above.
const pluginRows = [];
for (const p of PLUGIN_PINS) {
const row = { name: `${p.constant} (plugins/ruflo-metaharness/scripts/${p.file})`, package: p.name, pin: null, declared: null };
try {
const m = readFileSync(join(PLUGIN_SCRIPTS, p.file), 'utf-8').match(new RegExp(`${p.constant}\\s*=\\s*['"]([^'"]+)['"]`));
if (m) row.pin = m[1];
} catch { /* unreadable — reported below */ }
if (!row.pin) { pluginRows.push({ ...row, status: 'UNREADABLE', note: `${p.constant} not found in ${p.file} — the watcher cannot protect a pin it cannot read` }); continue; }
// The helpers match installed copies with _invoke.satisfiesTildeRange(),
// which parses ONLY `~X.Y.Z` and returns false for everything else. A
// `^`/exact/`latest` pin would therefore reject every installed copy and
// npm-install on each call, while covers()/satisfies() below — which do
// understand those forms — reported the row green. Fail closed instead.
if (!/^~\d+\.\d+\.\d+$/.test(row.pin)) {
pluginRows.push({ ...row, status: 'NOT-TILDE', note: `${row.pin} is not \`~X.Y.Z\`; _invoke.satisfiesTildeRange() accepts no other form, so every installed copy would be rejected` });
continue;
}
row.declared = declaredRange(pkg, p.name).range ?? null;
if (row.declared) {
const ok = covers(row.pin, row.declared);
pluginRows.push({ ...row, status: ok === false ? 'NOT-COVERED' : ok === true ? 'covers' : 'unparseable' });
continue;
}
if (ARGS.offline) { pluginRows.push({ ...row, status: 'skipped', note: 'not declared by the CLI; offline mode' }); continue; }
let latest;
try { latest = npmLatest(p.name); }
catch { pluginRows.push({ ...row, status: 'unknown', note: 'npm view failed (network?) — not treated as drift' }); continue; }
const ok = satisfies(row.pin, latest);
pluginRows.push({ ...row, latest, status: ok === false ? 'STALE' : ok === true ? 'current' : 'unparseable' });
}
const pluginBad = pluginRows.filter((r) => ['NOT-COVERED', 'NOT-TILDE', 'UNREADABLE', 'STALE', 'unparseable'].includes(r.status));
const stale = rows.filter((r) => r.status === 'STALE' || r.status === 'UNDECLARED' || r.status === 'PEER-ONLY');
const constBad = constRow && constRow.status === 'OUT-OF-RANGE';
const apiErrors = [];
if (ARGS.requireInstalled) {
const cliDir = dirname(CLI_PKG);
// Advertised symbol contract per package — verified against the real
// published artifacts (darwin 0.9.0 / flywheel 0.1.10 / radio 0.1.0 / turn-credit 0.1.0).
const API_CONTRACT = {
'@metaharness/darwin': ['evolve', 'RefineMutator', 'summarizeFailedTraces'],
'@metaharness/flywheel': ['runFlywheelGenerations', 'meetsPromotionRule', 'makeSigner', 'verifyReplayBundle', 'sequentialEvidence', 'withSequentialEvidence'],
'@metaharness/radio': ['RadioBus', 'runProtocol', 'runSim'],
// PAPER_DEFAULTS/GOVERNED_DEFAULTS are exported consts, not functions —
// excluded here since this contract only asserts `typeof === 'function'`.
'@metaharness/turn-credit': ['processTrajectory', 'creditByLabel', 'evidenceFromLogProbs', 'buildCreditReceiptPayload'],
};
for (const [pkgName, symbols] of Object.entries(API_CONTRACT)) {
try {
const mod = await import(pathToFileURL(join(cliDir, 'node_modules', pkgName, 'dist/index.js')).href);
for (const symbol of symbols) {
if (typeof mod[symbol] !== 'function') apiErrors.push(`${pkgName} must export ${symbol}`);
}
} catch (error) {
apiErrors.push(`${pkgName} import failed: ${error.message}`);
}
}
}
const drift = stale.length > 0 || constBad || pluginBad.length > 0 || apiErrors.length > 0;
const payload = {
generatedAt: new Date().toISOString(),
drift,
pins: rows,
constCheck: constRow,
pluginPins: pluginRows,
installedApiChecked: ARGS.requireInstalled,
apiErrors,
};
if (ARGS.format === 'json') {
console.log(JSON.stringify(payload, null, 2));
} else {
console.log('# check-metaharness-pins\n');
console.log('| Package | Declared | Where | npm latest | Status |');
console.log('|---|---|---|---|---|');
for (const r of rows) console.log(`| ${r.name} | ${r.range ?? '—'} | ${r.where ?? '—'} | ${r.latest ?? '—'} | ${r.status} |`);
if (constRow) console.log(`| ${constRow.name} | =${constRow.pin} (vs ${constRow.declared}) | — | — | ${constRow.status} |`);
for (const r of pluginRows) console.log(`| ${r.name} | ${r.pin ?? '—'}${r.declared ? ` (vs ${r.declared})` : ''} | plugin | ${r.latest ?? '—'} | ${r.status} |`);
if (ARGS.requireInstalled) console.log(`\nInstalled API check: ${apiErrors.length ? `FAIL — ${apiErrors.join('; ')}` : 'PASS'}`);
console.log('');
if (drift) {
console.log('⚠ **Pin drift detected.** A metaharness pin is stale, undeclared, declared peer-only, or a plugin helper pin no longer covers it (or is not a `~X.Y.Z` range).');
console.log('Fix the declaration in v3/@claude-flow/cli/package.json (installable pins belong in optionalDependencies;');
console.log('bump MH_DARWIN_PIN if darwin moved, and the *_PIN_VERSION constants in plugins/ruflo-metaharness/scripts/), then re-run.');
} else {
console.log('✓ All metaharness pins are declared installably and admit the current npm `latest`; plugin helper pins cover them.');
}
}
process.exit(drift ? 1 : 0);
}
main().catch((e) => { console.error('check-metaharness-pins crashed:', e?.message || e); process.exit(2); });