@claude-flow/cli 3.45.0 → 3.46.0

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 (73) hide show
  1. package/.claude/helpers/helpers.manifest.json +2 -2
  2. package/README.md +2 -0
  3. package/bin/cli.js +13 -0
  4. package/bin/mcp-server.js +16 -0
  5. package/catalog-manifest.json +2 -2
  6. package/dist/src/commands/doctor.d.ts +3 -0
  7. package/dist/src/commands/doctor.js +131 -2
  8. package/dist/src/commands/embeddings.js +50 -7
  9. package/dist/src/commands/init.js +5 -2
  10. package/dist/src/commands/mcp.js +27 -4
  11. package/dist/src/mcp-server.d.ts +50 -6
  12. package/dist/src/mcp-server.js +267 -33
  13. package/dist/src/mcp-tools/agent-execute-core.js +7 -0
  14. package/dist/src/mcp-tools/agentdb-tools.js +20 -8
  15. package/dist/src/mcp-tools/browser-tools.js +30 -23
  16. package/dist/src/mcp-tools/memory-tools.js +9 -4
  17. package/dist/src/mcp-tools/metaharness-tools.js +1 -1
  18. package/dist/src/mcp-tools/policy-enforcer.d.ts +4 -4
  19. package/dist/src/mcp-tools/policy-enforcer.js +4 -4
  20. package/dist/src/mcp-tools/terminal-tools.js +11 -1
  21. package/dist/src/mcp-tools/workflow-tools.js +1 -1
  22. package/dist/src/memory/ewc-consolidation.d.ts +16 -1
  23. package/dist/src/memory/ewc-consolidation.js +63 -13
  24. package/dist/src/memory/intelligence.d.ts +6 -2
  25. package/dist/src/memory/intelligence.js +18 -11
  26. package/dist/src/memory/memory-bridge.js +84 -0
  27. package/dist/src/ruvector/model-router.js +19 -1
  28. package/dist/src/services/distill-oracle.js +2 -1
  29. package/dist/src/services/git-workspace-identity.js +5 -8
  30. package/dist/src/services/policy-runtime.js +114 -4
  31. package/dist/src/services/worker-daemon.js +8 -3
  32. package/node_modules/@claude-flow/codex/dist/worktrees/coordinator.d.ts.map +1 -1
  33. package/node_modules/@claude-flow/codex/dist/worktrees/coordinator.js +2 -6
  34. package/node_modules/@claude-flow/codex/dist/worktrees/coordinator.js.map +1 -1
  35. package/node_modules/@claude-flow/mcp/dist/transport/http.d.ts.map +1 -1
  36. package/node_modules/@claude-flow/mcp/dist/transport/http.js +14 -1
  37. package/node_modules/@claude-flow/mcp/dist/transport/http.js.map +1 -1
  38. package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/planner-descriptor.d.ts +40 -0
  39. package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/planner-descriptor.d.ts.map +1 -0
  40. package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/planner-descriptor.js +100 -0
  41. package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/planner-descriptor.js.map +1 -0
  42. package/node_modules/@claude-flow/plugin-agent-federation/dist/index.d.ts +1 -0
  43. package/node_modules/@claude-flow/plugin-agent-federation/dist/index.d.ts.map +1 -1
  44. package/node_modules/@claude-flow/plugin-agent-federation/dist/index.js +1 -0
  45. package/node_modules/@claude-flow/plugin-agent-federation/dist/index.js.map +1 -1
  46. package/node_modules/@claude-flow/plugin-agent-federation/package.json +1 -0
  47. package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
  48. package/node_modules/@claude-flow/security/dist/safe-git.d.ts +22 -0
  49. package/node_modules/@claude-flow/security/dist/safe-git.d.ts.map +1 -0
  50. package/node_modules/@claude-flow/security/dist/safe-git.js +63 -0
  51. package/node_modules/@claude-flow/security/dist/safe-git.js.map +1 -0
  52. package/package.json +5 -5
  53. package/plugins/ruflo-metaharness/agents/metaharness-architect.md +1 -1
  54. package/plugins/ruflo-metaharness/commands/ruflo-metaharness.md +1 -1
  55. package/plugins/ruflo-metaharness/scripts/_darwin.mjs +84 -12
  56. package/plugins/ruflo-metaharness/scripts/_harness.mjs +10 -4
  57. package/plugins/ruflo-metaharness/scripts/_invoke.mjs +120 -6
  58. package/plugins/ruflo-metaharness/scripts/_redblue.mjs +33 -6
  59. package/plugins/ruflo-metaharness/scripts/audit-list.mjs +11 -10
  60. package/plugins/ruflo-metaharness/scripts/audit-trend.mjs +8 -7
  61. package/plugins/ruflo-metaharness/scripts/drift-from-history.mjs +2 -3
  62. package/plugins/ruflo-metaharness/scripts/oia-audit.mjs +8 -7
  63. package/plugins/ruflo-metaharness/scripts/similarity.mjs +9 -8
  64. package/plugins/ruflo-metaharness/scripts/smoke.sh +31 -0
  65. package/plugins/ruflo-metaharness/scripts/test-no-tool-time-npx.mjs +319 -0
  66. package/plugins/ruflo-metaharness/scripts/test-pin-alignment.mjs +182 -0
  67. package/plugins/ruflo-metaharness/skills/harness-evolve/SKILL.md +4 -2
  68. package/plugins/ruflo-metaharness/skills/harness-gepa/SKILL.md +1 -1
  69. package/plugins/ruflo-metaharness/skills/harness-learn/SKILL.md +1 -1
  70. package/plugins/ruflo-metaharness/skills/harness-mcp-scan/SKILL.md +1 -1
  71. package/plugins/ruflo-metaharness/skills/harness-score/SKILL.md +1 -1
  72. package/plugins/ruflo-metaharness/skills/harness-security-bench/SKILL.md +3 -1
  73. package/plugins/ruflo-metaharness/skills/harness-threat-model/SKILL.md +1 -1
@@ -253,6 +253,24 @@ grep -q "bin.harness" "$F" 2>/dev/null || miss="$miss no-harness-bin"
253
253
  grep -q "cwd: opts" "$F" || miss="$miss no-cwd-passthrough"
254
254
  [[ -z "$miss" ]] && ok || bad "$miss"
255
255
 
256
+ step "17r2. no tool-time npx: darwin / redblue / memory use what is installed (#3366)"
257
+ miss=""
258
+ # 17r locked _harness.mjs off npx; _darwin.mjs still ran `npx -y -p
259
+ # @metaharness/darwin@<pin>` per call, _redblue.mjs npm-installed into its
260
+ # cache ignoring the installed copy, and 4 scripts ran
261
+ # `npx @claude-flow/cli@latest memory …`. Static guard + hermetic runtime
262
+ # gate (stub packages in the published layout, npm/npx trap).
263
+ if grep -qE "spawn(Sync)?\('npx'" "$ROOT/scripts/_darwin.mjs" 2>/dev/null; then
264
+ miss="$miss darwin-npx-regressed"
265
+ fi
266
+ for s in audit-list audit-trend oia-audit similarity; do
267
+ grep -q "runRufloCli" "$ROOT/scripts/${s}.mjs" 2>/dev/null || miss="$miss ${s}-not-using-runRufloCli"
268
+ done
269
+ F="$ROOT/scripts/test-no-tool-time-npx.mjs"
270
+ [[ -x "$F" ]] || miss="$miss not-executable"
271
+ node "$F" >/dev/null 2>&1 || miss="$miss test-no-tool-time-npx-fails"
272
+ [[ -z "$miss" ]] && ok || bad "$miss"
273
+
256
274
  step "17z80. HIGH security findings survive wrapper + composite boundaries (#2750)"
257
275
  miss=""
258
276
  SEC_FIXTURE=$(mktemp -d)
@@ -461,6 +479,19 @@ for pkg in "metaharness" "@metaharness/router" "@metaharness/kernel" "@metaharne
461
479
  done
462
480
  [[ -z "$miss" ]] && ok || bad "$miss"
463
481
 
482
+ step "17z74a. plugin helper pins accept the CLI-declared ranges — no tool-time download (#3366)"
483
+ miss=""
484
+ # 17z74 compares package.json files with each other; the helpers' own
485
+ # *_PIN_VERSION constants (what findLocalPackageDir accepts) were never
486
+ # compared with them and drifted (~0.3.0 / ~0.8.0 vs declared ^0.4.1 /
487
+ # ~0.10.2), so every metaharness_* call npm-installed an older release next
488
+ # to the one ruflo ships. Hermetic runtime gate: stub packages at the declared
489
+ # versions, published layout, npm/npx trap.
490
+ F="$ROOT/scripts/test-pin-alignment.mjs"
491
+ [[ -x "$F" ]] || miss="$miss not-executable"
492
+ node "$F" >/dev/null 2>&1 || miss="$miss test-pin-alignment-fails"
493
+ [[ -z "$miss" ]] && ok || bad "$miss"
494
+
464
495
  step "17z73. metaharness packages tilde-pinned (anti-caret regression, iter 110)"
465
496
  miss=""
466
497
  # ADR-150 architectural-constraint review-round-1 mandated tilde pinning
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env node
2
+ // test-no-tool-time-npx.mjs — regression test: metaharness_* tool calls must
3
+ // not reach for npm / npx when what they need is already installed (#3366).
4
+ //
5
+ // THE BUG
6
+ // Three helpers spawned the npm registry at TOOL-CALL time even on a complete
7
+ // ruflo install:
8
+ // - _darwin.mjs `npx -y -p @metaharness/darwin@<pin> metaharness-darwin`
9
+ // on EVERY evolve / bench / security-bench call, although
10
+ // the CLI ships @metaharness/darwin as an optionalDependency
11
+ // - _redblue.mjs `npm install --prefix ~/.ruflo/redblue-cache-<pin>` on
12
+ // first use, never looking at the installed copy
13
+ // - audit-list / audit-trend / oia-audit / similarity
14
+ // `npx @claude-flow/cli@latest memory …` — a registry
15
+ // round trip per call, possibly a DIFFERENT CLI version
16
+ // than the server that spawned the tool (#3306), and a
17
+ // silent failure offline (oia_audit persisted:false,
18
+ // audit_list "0 records")
19
+ //
20
+ // WHAT THIS TEST PROVES (hermetic — stub packages, no network)
21
+ // A PUBLISHED layout is built in a temp dir:
22
+ // <p>/node_modules/@claude-flow/cli/{package.json,bin/cli.js,dist/src/index.js}
23
+ // <p>/node_modules/@claude-flow/cli/plugins/ruflo-metaharness/scripts/* (copied)
24
+ // <p>/node_modules/metaharness stub, satisfies the _harness pin
25
+ // <p>/node_modules/@metaharness/darwin stub, satisfies the _darwin pin
26
+ // <p>/node_modules/@metaharness/redblue → <p>/store/redblue (pnpm-style
27
+ // SYMLINK; the stub keeps upstream redblue's
28
+ // `import.meta.url === file://${argv[1]}` isMain guard)
29
+ // with a PATH trap whose npm / npx log every call and exit 1 (offline).
30
+ // Phase 1 darwin: bench verify (sync) + security bench (async) run the
31
+ // installed bin — not degraded, zero npm/npx; and, with no
32
+ // installed copy, the existing darwin-cache-<pin> runs instead
33
+ // Phase 2 redblue: the symlinked installed copy runs (isMain holds because
34
+ // the resolved bin is realpath'd) — zero npm/npx
35
+ // Phase 3 memory round trip through the CLI that ships the plugin:
36
+ // oia-audit persists → audit-list lists it → audit-trend and
37
+ // similarity read it back — zero npm/npx
38
+ // Phase 4 contracts kept: CLI_CORE=1 still opts into npx cli-core@alpha; a
39
+ // layout with no usable local CLI — missing, or present but not
40
+ // named @claude-flow/cli — still falls back to
41
+ // `npx @claude-flow/cli@latest`; darwin absent → degraded, exit 0
42
+ // (ADR-150 rule #3)
43
+ // Phase 5 the two new lookups stay inside ruflo's tree: a darwin / redblue
44
+ // in the tool's OWN cwd node_modules is never executed
45
+ // ({fromCwd:false}); a cache dir with package.json but no bin gets
46
+ // one reinstall instead of degrading forever; and neither helper
47
+ // spawns the resolved bin through a shell
48
+ //
49
+ // USAGE
50
+ // node plugins/ruflo-metaharness/scripts/test-no-tool-time-npx.mjs
51
+ //
52
+ // EXIT CODES
53
+ // 0 all assertions passed (or skipped on win32 — the trap is POSIX sh)
54
+ // 1 at least one assertion failed
55
+
56
+ import { spawnSync } from 'node:child_process';
57
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
58
+ import { tmpdir } from 'node:os';
59
+ import { dirname, join } from 'node:path';
60
+ import { fileURLToPath, pathToFileURL } from 'node:url';
61
+
62
+ const SCRIPTS_DIR = dirname(fileURLToPath(import.meta.url));
63
+
64
+ if (process.platform === 'win32') {
65
+ console.log('# test-no-tool-time-npx — SKIPPED on win32 (the npm/npx trap is a POSIX sh script)');
66
+ process.exit(0);
67
+ }
68
+
69
+ let passed = 0, failed = 0;
70
+ const failures = [];
71
+ function assert(cond, label) {
72
+ console.log(` ${cond ? '✓' : '✗'} ${label}`);
73
+ if (cond) passed++;
74
+ else { failures.push(label); failed++; }
75
+ }
76
+
77
+ /** Lowest version a tilde pin admits — the stubs sit exactly there. */
78
+ function floorOfPin(fullPin) {
79
+ const m = /@[~^]?(\d+\.\d+\.\d+)$/.exec(fullPin);
80
+ return m ? m[1] : '0.0.0';
81
+ }
82
+ const { METAHARNESS_VERSION_PIN } = await import(pathToFileURL(join(SCRIPTS_DIR, '_harness.mjs')).href);
83
+ const { DARWIN_VERSION_PIN } = await import(pathToFileURL(join(SCRIPTS_DIR, '_darwin.mjs')).href);
84
+ const { REDBLUE_VERSION_PIN } = await import(pathToFileURL(join(SCRIPTS_DIR, '_redblue.mjs')).href);
85
+
86
+ const write = (file, body, mode) => {
87
+ mkdirSync(dirname(file), { recursive: true });
88
+ writeFileSync(file, body);
89
+ if (mode) chmodSync(file, mode);
90
+ };
91
+ const echoStub = (name, version) =>
92
+ `console.log(JSON.stringify({ stub: '${name}', version: '${version}', argv: process.argv.slice(2) }));\n`;
93
+
94
+ // realpath: macOS tmpdir() is /var/… while resolved paths come back as /private/var/…
95
+ const TMP = realpathSync(mkdtempSync(join(tmpdir(), 'ruflo-no-tool-time-npx-')));
96
+ const NM = join(TMP, 'node_modules');
97
+ const CLI = join(NM, '@claude-flow', 'cli');
98
+ const PLUGIN_SCRIPTS = join(CLI, 'plugins', 'ruflo-metaharness', 'scripts');
99
+ const TRAP = join(TMP, 'trap-bin');
100
+ const TRAP_LOG = join(TMP, 'trap.log');
101
+ const CLI_LOG = join(TMP, 'cli-calls.log');
102
+ const PROJECT = join(TMP, 'project');
103
+
104
+ try {
105
+ // ── fixture ───────────────────────────────────────────────────────────
106
+ mkdirSync(PLUGIN_SCRIPTS, { recursive: true });
107
+ for (const f of readdirSync(SCRIPTS_DIR)) {
108
+ if (f.endsWith('.mjs')) copyFileSync(join(SCRIPTS_DIR, f), join(PLUGIN_SCRIPTS, f));
109
+ }
110
+ mkdirSync(PROJECT);
111
+
112
+ // The ruflo CLI that ships the plugin: a stub with a tiny file-backed
113
+ // `memory store|list|retrieve`, logging every invocation.
114
+ write(join(CLI, 'package.json'), JSON.stringify({ name: '@claude-flow/cli', version: '0.0.0-test', type: 'module', bin: { cli: 'bin/cli.js' } }));
115
+ write(join(CLI, 'dist', 'src', 'index.js'), 'export {};\n');
116
+ write(join(CLI, 'bin', 'cli.js'), `
117
+ import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
118
+ const argv = process.argv.slice(2);
119
+ appendFileSync(${JSON.stringify(CLI_LOG)}, argv.slice(0, 2).join(' ') + '\\n');
120
+ const opt = (k) => { const i = argv.indexOf(k); return i >= 0 ? argv[i + 1] : undefined; };
121
+ const DB = ${JSON.stringify(join(TMP, 'stub-memory.json'))};
122
+ const db = existsSync(DB) ? JSON.parse(readFileSync(DB, 'utf-8')) : {};
123
+ const ns = opt('--namespace') ?? 'default';
124
+ if (argv[0] !== 'memory') process.exit(2);
125
+ if (argv[1] === 'store') { db[ns] = { ...(db[ns] ?? {}), [opt('--key')]: opt('--value') }; writeFileSync(DB, JSON.stringify(db)); console.log('[OK] Data stored successfully'); }
126
+ else if (argv[1] === 'list') console.log(JSON.stringify(Object.keys(db[ns] ?? {}).map((key) => ({ key, namespace: ns }))));
127
+ else if (argv[1] === 'retrieve') { const v = db[ns]?.[opt('--key')]; if (v === undefined) { console.error('not found'); process.exit(1); } console.log(v); }
128
+ else process.exit(2);
129
+ `);
130
+
131
+ const mhVersion = floorOfPin(METAHARNESS_VERSION_PIN);
132
+ write(join(NM, 'metaharness', 'package.json'), JSON.stringify({ name: 'metaharness', version: mhVersion, type: 'module', bin: { metaharness: 'dist/bin.js', harness: 'dist/harness-bin.js' } }));
133
+ write(join(NM, 'metaharness', 'dist', 'bin.js'), echoStub('metaharness', mhVersion));
134
+ write(join(NM, 'metaharness', 'dist', 'harness-bin.js'), echoStub('harness', mhVersion));
135
+
136
+ const darwinVersion = floorOfPin(DARWIN_VERSION_PIN);
137
+ const pinDigits = DARWIN_VERSION_PIN.replace(/^.*@[~^]?/, ''); // cache-dir suffix
138
+ const DARWIN = join(NM, '@metaharness', 'darwin');
139
+ write(join(DARWIN, 'package.json'), JSON.stringify({ name: '@metaharness/darwin', version: darwinVersion, type: 'module', bin: { 'metaharness-darwin': './dist/cli.js' } }));
140
+ write(join(DARWIN, 'dist', 'cli.js'), echoStub('metaharness-darwin', darwinVersion));
141
+
142
+ // redblue lives in a pnpm-like store and is SYMLINKED into node_modules;
143
+ // its CLI keeps upstream's isMain guard, so it only prints when argv[1] is
144
+ // the real path.
145
+ const redblueVersion = floorOfPin(REDBLUE_VERSION_PIN);
146
+ const RB_STORE = join(TMP, 'store', 'redblue');
147
+ write(join(RB_STORE, 'package.json'), JSON.stringify({ name: '@metaharness/redblue', version: redblueVersion, type: 'module', bin: { redblue: './dist/cli/index.js', 'metaharness-redblue': './dist/cli/index.js' } }));
148
+ write(join(RB_STORE, 'dist', 'cli', 'index.js'),
149
+ 'const isMain = import.meta.url === `file://${process.argv[1]}`;\n' +
150
+ `if (isMain) console.log(JSON.stringify({ stub: 'redblue', version: '${redblueVersion}', argv: process.argv.slice(2) }));\n`);
151
+ symlinkSync(RB_STORE, join(NM, '@metaharness', 'redblue'));
152
+
153
+ for (const t of ['npm', 'npx']) {
154
+ write(join(TRAP, t), `#!/bin/sh\nprintf '%s %s\\n' "${t}" "$*" >> "${TRAP_LOG}"\necho "npm ERR! ${t} blocked by test-no-tool-time-npx trap" >&2\nexit 1\n`, 0o755);
155
+ }
156
+ const baseEnv = { ...process.env, PATH: `${TRAP}:${process.env.PATH}`, RUFLO_METAHARNESS_CACHE_BASE: join(TMP, 'empty-cache-base') };
157
+ delete baseEnv.RUFLO_METAHARNESS_SKIP_LOCAL;
158
+ delete baseEnv.CLI_CORE;
159
+ const trapped = () => (existsSync(TRAP_LOG) ? readFileSync(TRAP_LOG, 'utf-8').trim() : '');
160
+ const resetTrap = () => rmSync(TRAP_LOG, { force: true });
161
+ const run = (script, args, env = baseEnv, scriptsDir = PLUGIN_SCRIPTS, cwd = PROJECT) => {
162
+ const r = spawnSync(process.execPath, [join(scriptsDir, script), ...args], { cwd, env, encoding: 'utf-8', timeout: 60_000 });
163
+ let json = null;
164
+ try { json = JSON.parse(r.stdout); } catch { /* callers assert */ }
165
+ return { ...r, json };
166
+ };
167
+ const noNpm = (label) => {
168
+ const t = trapped();
169
+ assert(t === '', `${label}: no npm / npx spawned${t ? ` (got: ${t.split('\n')[0].slice(0, 160)})` : ''}`);
170
+ resetTrap();
171
+ };
172
+
173
+ console.log('# test-no-tool-time-npx\n');
174
+
175
+ // ── Phase 1 — darwin runs the installed bin ──────────────────────────
176
+ console.log(`Phase 1 — @metaharness/darwin ${darwinVersion} (installed) serves bench + security bench`);
177
+ const suite = join(PROJECT, 'suite.json');
178
+ writeFileSync(suite, '{}');
179
+ const bench = run('bench.mjs', ['--op', 'verify', '--suite', suite]);
180
+ assert(bench.status === 0 && bench.json?.success === true, `bench verify exits 0 with success (got ${bench.status}, reason ${bench.json?.reason ?? 'none'})`);
181
+ assert(bench.json?.data?.stub === 'metaharness-darwin' && bench.json?.data?.argv?.[0] === 'bench',
182
+ 'bench verify ran the installed metaharness-darwin (sync path)');
183
+ noNpm('bench verify');
184
+ const sec = run('security-bench.mjs', ['--population', '1', '--cycles', '1']);
185
+ assert(sec.json?.degraded !== true, `security bench is not degraded (reason ${sec.json?.reason ?? 'none'})`);
186
+ noNpm('security bench (async path)');
187
+ // …and the OTHER success branch: the one-time versioned cache that gepa
188
+ // already shares, which is what a machine without a qualifying installed
189
+ // copy falls back to. Without this, deleting that branch leaves the suite
190
+ // green.
191
+ const seededCache = join(TMP, 'seeded-cache');
192
+ const cachedDarwin = join(seededCache, `darwin-cache-${pinDigits}`, 'node_modules', '@metaharness', 'darwin');
193
+ write(join(cachedDarwin, 'package.json'), JSON.stringify({ name: '@metaharness/darwin', version: darwinVersion, type: 'module', bin: { 'metaharness-darwin': './dist/cli.js' } }));
194
+ write(join(cachedDarwin, 'dist', 'cli.js'), echoStub('darwin-from-cache', darwinVersion));
195
+ const cachedBench = run('bench.mjs', ['--op', 'verify', '--suite', suite],
196
+ { ...baseEnv, RUFLO_METAHARNESS_CACHE_BASE: seededCache, RUFLO_METAHARNESS_SKIP_LOCAL: '1' });
197
+ assert(cachedBench.json?.data?.stub === 'darwin-from-cache',
198
+ `no installed copy → the existing darwin-cache-${pinDigits} runs (got stub ${cachedBench.json?.data?.stub ?? 'none'})`);
199
+ noNpm('bench verify via darwin-cache');
200
+
201
+ // ── Phase 2 — redblue runs the installed (symlinked) copy ────────────
202
+ console.log(`\nPhase 2 — @metaharness/redblue ${redblueVersion} (installed, symlinked) serves attack`);
203
+ const rb = run('redblue.mjs', ['attack', 'prompt', '--count', '1']);
204
+ assert(rb.status === 0 && rb.json?.ok === true, `redblue attack exits 0 (got ${rb.status}, reason ${rb.json?.reason ?? 'none'})`);
205
+ assert(/"stub":"redblue"/.test(rb.json?.stdout ?? ''), 'redblue CLI actually dispatched (isMain held: resolved bin is realpath\'d)');
206
+ noNpm('redblue attack');
207
+
208
+ // ── Phase 3 — memory round trip through the CLI that ships the plugin ─
209
+ console.log('\nPhase 3 — oia-audit → audit-list → audit-trend / similarity via the shipping CLI');
210
+ rmSync(CLI_LOG, { force: true });
211
+ const oia = run('oia-audit.mjs', ['--path', PROJECT]);
212
+ assert(oia.json?.persisted?.ok === true, `oia-audit persisted the record (got ${JSON.stringify(oia.json?.persisted ?? null)})`);
213
+ const key = oia.json?.persisted?.key;
214
+ const list = run('audit-list.mjs', ['--format', 'json']);
215
+ assert(list.json?.returned === 1 && list.json?.records?.[0]?.key === key, `audit-list returns the persisted record (got ${list.json?.returned})`);
216
+ const trend = run('audit-trend.mjs', ['--baseline-key', key, '--current-key', key, '--format', 'json']);
217
+ assert(trend.status === 0 && trend.json !== null, `audit-trend reads both records back (exit ${trend.status})`);
218
+ const sim = run('similarity.mjs', ['--a-key', key, '--b-key', key, '--format', 'json']);
219
+ assert(sim.status === 0 && sim.json?.degraded !== true, `similarity reads both records back (exit ${sim.status}, reason ${sim.json?.reason ?? 'none'})`);
220
+ const cliCalls = existsSync(CLI_LOG) ? readFileSync(CLI_LOG, 'utf-8').trim().split('\n') : [];
221
+ assert(cliCalls.includes('memory store') && cliCalls.includes('memory list') && cliCalls.includes('memory retrieve'),
222
+ `the shipping CLI served store + list + retrieve (saw: ${[...new Set(cliCalls)].join(', ') || 'nothing'})`);
223
+ noNpm('memory round trip');
224
+
225
+ // ── Phase 4 — contracts kept ─────────────────────────────────────────
226
+ console.log('\nPhase 4 — opt-in and fallback contracts unchanged');
227
+ run('audit-list.mjs', ['--format', 'json'], { ...baseEnv, CLI_CORE: '1' });
228
+ assert(trapped().startsWith('npx @claude-flow/cli-core@alpha memory list'), 'CLI_CORE=1 still opts into `npx @claude-flow/cli-core@alpha`');
229
+ resetTrap();
230
+ // No usable local CLI (e.g. a marketplace clone with no dist/ build) → the
231
+ // pre-existing npx fallback, unchanged.
232
+ const bare = join(TMP, 'bare', 'plugins', 'ruflo-metaharness', 'scripts');
233
+ mkdirSync(bare, { recursive: true });
234
+ for (const f of readdirSync(PLUGIN_SCRIPTS)) copyFileSync(join(PLUGIN_SCRIPTS, f), join(bare, f));
235
+ const bareList = run('audit-list.mjs', ['--format', 'json'], baseEnv, bare);
236
+ assert(bareList.status === 0 && trapped().startsWith('npx @claude-flow/cli@latest memory list'),
237
+ 'no local CLI → falls back to `npx @claude-flow/cli@latest` (exit 0, as before)');
238
+ resetTrap();
239
+ // A directory two levels up that has bin/cli.js AND dist/src/index.js but
240
+ // is NOT @claude-flow/cli must not be run as the ruflo CLI.
241
+ const impostorScripts = join(TMP, 'impostor', 'plugins', 'ruflo-metaharness', 'scripts');
242
+ mkdirSync(impostorScripts, { recursive: true });
243
+ for (const f of readdirSync(PLUGIN_SCRIPTS)) copyFileSync(join(PLUGIN_SCRIPTS, f), join(impostorScripts, f));
244
+ write(join(TMP, 'impostor', 'package.json'), JSON.stringify({ name: 'some-other-package', version: '1.0.0' }));
245
+ write(join(TMP, 'impostor', 'bin', 'cli.js'), 'console.log("[]");\n');
246
+ write(join(TMP, 'impostor', 'dist', 'src', 'index.js'), 'export {};\n');
247
+ const impostor = run('audit-list.mjs', ['--format', 'json'], baseEnv, impostorScripts);
248
+ assert(impostor.status === 0 && trapped().startsWith('npx @claude-flow/cli@latest memory list'),
249
+ `a bin/cli.js whose package.json is not @claude-flow/cli is rejected (trap: ${trapped().split('\n')[0].slice(0, 60) || 'nothing'})`);
250
+ resetTrap();
251
+ const absent = run('bench.mjs', ['--op', 'verify', '--suite', suite], { ...baseEnv, RUFLO_METAHARNESS_SKIP_LOCAL: '1' });
252
+ assert(absent.status === 0 && absent.json?.degraded === true && absent.json?.reason === 'metaharness-darwin-not-available',
253
+ `darwin absent → {degraded:true, reason:'metaharness-darwin-not-available'}, exit 0 (got ${absent.status}, ${absent.json?.reason})`);
254
+ resetTrap();
255
+
256
+ // ── Phase 5 — the resolution stays inside ruflo's own tree ───────────
257
+ // The darwin / redblue lookups END IN A SPAWN with MCP-supplied argv, so
258
+ // they pass { fromCwd: false } and must ignore a node_modules in the tool's
259
+ // cwd (or any ancestor of it). ISOLATED layout: plugin scripts under their
260
+ // own temp root, so the walk-up from the scripts finds no darwin/redblue at
261
+ // all and only the cwd copy could match.
262
+ console.log('\nPhase 5 — installed-copy lookup is scoped to ruflo, and a half-written cache is repaired');
263
+ const ISO = realpathSync(mkdtempSync(join(tmpdir(), 'ruflo-no-tool-time-npx-iso-')));
264
+ try {
265
+ const isoScripts = join(ISO, 'scripts');
266
+ mkdirSync(isoScripts, { recursive: true });
267
+ for (const f of readdirSync(PLUGIN_SCRIPTS)) copyFileSync(join(PLUGIN_SCRIPTS, f), join(isoScripts, f));
268
+ const isoProject = join(ISO, 'project');
269
+ mkdirSync(isoProject, { recursive: true });
270
+ const cwdDarwin = join(isoProject, 'node_modules', '@metaharness', 'darwin');
271
+ write(join(cwdDarwin, 'package.json'), JSON.stringify({ name: '@metaharness/darwin', version: darwinVersion, type: 'module', bin: { 'metaharness-darwin': './dist/cli.js' } }));
272
+ write(join(cwdDarwin, 'dist', 'cli.js'), echoStub('cwd-darwin', darwinVersion));
273
+ const cwdRedblue = join(isoProject, 'node_modules', '@metaharness', 'redblue');
274
+ write(join(cwdRedblue, 'package.json'), JSON.stringify({ name: '@metaharness/redblue', version: redblueVersion, type: 'module', bin: { redblue: './dist/cli/index.js' } }));
275
+ write(join(cwdRedblue, 'dist', 'cli', 'index.js'), echoStub('cwd-redblue', redblueVersion));
276
+ const isoEnv = { ...baseEnv, RUFLO_METAHARNESS_CACHE_BASE: join(ISO, 'empty-cache-base') };
277
+
278
+ const cwdBench = run('bench.mjs', ['--op', 'verify', '--suite', suite], isoEnv, isoScripts, isoProject);
279
+ assert(cwdBench.json?.data?.stub !== 'cwd-darwin',
280
+ `darwin in the tool's cwd node_modules is NOT executed (got stub ${cwdBench.json?.data?.stub ?? 'none'})`);
281
+ resetTrap();
282
+ const cwdRb = run('redblue.mjs', ['attack', 'prompt', '--count', '1'], isoEnv, isoScripts, isoProject);
283
+ assert(!/"stub":"cwd-redblue"/.test(cwdRb.json?.stdout ?? ''),
284
+ 'redblue in the tool\'s cwd node_modules is NOT executed');
285
+ resetTrap();
286
+
287
+ // A cache dir holding package.json but no bin (npm killed by the 180s
288
+ // install timeout, or two first calls racing into the same prefix) must
289
+ // not count as installed forever: one reinstall is attempted.
290
+ const halfCache = join(ISO, 'half-cache');
291
+ write(join(halfCache, `darwin-cache-${pinDigits}`, 'node_modules', '@metaharness', 'darwin', 'package.json'),
292
+ JSON.stringify({ name: '@metaharness/darwin', version: darwinVersion, type: 'module', bin: { 'metaharness-darwin': './dist/cli.js' } }));
293
+ const halfRun = run('bench.mjs', ['--op', 'verify', '--suite', suite], { ...isoEnv, RUFLO_METAHARNESS_CACHE_BASE: halfCache }, isoScripts, isoProject);
294
+ assert(/npm install/.test(trapped()), `a cache with package.json but no bin triggers one reinstall (trap: ${trapped().split('\n')[0].slice(0, 120) || 'nothing'})`);
295
+ assert(halfRun.status === 0 && halfRun.json?.degraded === true,
296
+ `…and still degrades with exit 0 when that reinstall fails (got ${halfRun.status}, degraded ${halfRun.json?.degraded})`);
297
+ resetTrap();
298
+ } finally {
299
+ rmSync(ISO, { recursive: true, force: true });
300
+ }
301
+
302
+ // Static: the resolved bins are spawned directly, never through a shell —
303
+ // their argv carries MCP-supplied values (redblue --config/--out/--in).
304
+ for (const f of ['_darwin.mjs', '_redblue.mjs']) {
305
+ const src = readFileSync(join(SCRIPTS_DIR, f), 'utf-8');
306
+ const code = src.split('\n').filter((l) => !l.trimStart().startsWith('//')).join('\n');
307
+ assert(!/shell:\s*(true|process\.platform)/.test(code), `${f} spawns the resolved bin with shell:false`);
308
+ }
309
+ } finally {
310
+ rmSync(TMP, { recursive: true, force: true });
311
+ }
312
+
313
+ console.log(`\n${passed} passed, ${failed} failed`);
314
+ if (failed > 0) {
315
+ console.log('\nFailures:');
316
+ for (const f of failures) console.log(` - ${f}`);
317
+ process.exit(1);
318
+ }
319
+ console.log('\n✓ No npm / npx at tool-call time when darwin, redblue and the ruflo CLI are installed.');
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env node
2
+ // test-pin-alignment.mjs — regression test for plugin helper pins that
3
+ // drifted away from the ranges @claude-flow/cli declares (#3366).
4
+ //
5
+ // THE BUG
6
+ // The plugin helpers carry their own tilde pins (_harness.mjs
7
+ // METAHARNESS_PIN_VERSION, _darwin.mjs DARWIN_PIN_VERSION). Those pins decide
8
+ // which INSTALLED copy `findLocalPackageDir()` accepts. They sat at ~0.3.0 /
9
+ // ~0.8.0 while v3/@claude-flow/cli/package.json declared metaharness ^0.4.1 and
10
+ // @metaharness/darwin ~0.10.2, so on a normal ruflo install every
11
+ // metaharness_* tool call rejected the copy ruflo ships and ran
12
+ // `npm install --prefix ~/.ruflo/metaharness-cache-0.3.0 metaharness@~0.3.0`
13
+ // at TOOL-CALL time (a network download of an older release; `degraded` when
14
+ // offline). scripts/check-metaharness-pins.mjs compared only the CLI's ranges,
15
+ // so nothing noticed.
16
+ //
17
+ // WHAT THIS TEST PROVES (hermetic — no network, no real MetaHarness package)
18
+ // Phase 1 each helper pin accepts the declared range's floor AND a later
19
+ // patch in it, using the same `satisfiesTildeRange()` predicate
20
+ // findLocalPackageDir() applies at runtime.
21
+ // Phase 2 end-to-end in the PUBLISHED layout
22
+ // (<prefix>/node_modules/@claude-flow/cli/plugins/ruflo-metaharness/
23
+ // scripts, next to <prefix>/node_modules/metaharness at the declared
24
+ // floor version): metaharnessResolution() reports source 'local',
25
+ // score.mjs returns the installed copy's output, and NOT ONE
26
+ // npm / npx process is spawned (a PATH trap logs any attempt).
27
+ // The scripts are copied into that layout because resolution walks up from
28
+ // the helper's own directory; in the repo checkout (plugins/ at the repo
29
+ // root) the walk never reaches an installed CLI's node_modules.
30
+ //
31
+ // USAGE
32
+ // node plugins/ruflo-metaharness/scripts/test-pin-alignment.mjs
33
+ //
34
+ // EXIT CODES
35
+ // 0 all assertions passed (or skipped on win32 — the trap is POSIX sh)
36
+ // 1 at least one assertion failed
37
+ // 2 setup error
38
+
39
+ import { spawnSync } from 'node:child_process';
40
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
41
+ import { tmpdir } from 'node:os';
42
+ import { dirname, join } from 'node:path';
43
+ import { fileURLToPath, pathToFileURL } from 'node:url';
44
+
45
+ const SCRIPTS_DIR = dirname(fileURLToPath(import.meta.url));
46
+ const CLI_PKG_JSON = join(SCRIPTS_DIR, '..', '..', '..', 'v3', '@claude-flow', 'cli', 'package.json');
47
+
48
+ if (process.platform === 'win32') {
49
+ console.log('# test-pin-alignment — SKIPPED on win32 (the npm/npx trap is a POSIX sh script)');
50
+ process.exit(0);
51
+ }
52
+ if (!existsSync(CLI_PKG_JSON)) {
53
+ console.error(`test-pin-alignment: ${CLI_PKG_JSON} not found — run from a ruflo repo checkout`);
54
+ process.exit(2);
55
+ }
56
+
57
+ let passed = 0, failed = 0;
58
+ const failures = [];
59
+ function assert(cond, label) {
60
+ console.log(` ${cond ? '✓' : '✗'} ${label}`);
61
+ if (cond) passed++;
62
+ else { failures.push(label); failed++; }
63
+ }
64
+
65
+ /** Same lookup order as scripts/check-metaharness-pins.mjs. */
66
+ function declaredRange(pkgJson, name) {
67
+ for (const where of ['dependencies', 'optionalDependencies', 'peerDependencies']) {
68
+ if (pkgJson[where]?.[name]) return pkgJson[where][name];
69
+ }
70
+ return null;
71
+ }
72
+ function floorOf(range) {
73
+ const m = /^[~^]?(\d+)\.(\d+)\.(\d+)/.exec(String(range));
74
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
75
+ }
76
+
77
+ const cliPkg = JSON.parse(readFileSync(CLI_PKG_JSON, 'utf-8'));
78
+ const { satisfiesTildeRange } = await import(pathToFileURL(join(SCRIPTS_DIR, '_invoke.mjs')).href);
79
+ const { METAHARNESS_VERSION_PIN } = await import(pathToFileURL(join(SCRIPTS_DIR, '_harness.mjs')).href);
80
+ const { DARWIN_VERSION_PIN } = await import(pathToFileURL(join(SCRIPTS_DIR, '_darwin.mjs')).href);
81
+ const pinOf = (full) => full.slice(full.lastIndexOf('@') + 1);
82
+
83
+ console.log('# test-pin-alignment\n');
84
+
85
+ // ──────────────────────────────────────────────────────────────────────
86
+ // Phase 1 — every helper pin accepts what the CLI declares
87
+ // ──────────────────────────────────────────────────────────────────────
88
+ console.log('Phase 1 — helper pins accept the versions @claude-flow/cli declares');
89
+ const PINS = [
90
+ { pkg: 'metaharness', helper: '_harness.mjs', pin: pinOf(METAHARNESS_VERSION_PIN) },
91
+ { pkg: '@metaharness/darwin', helper: '_darwin.mjs', pin: pinOf(DARWIN_VERSION_PIN) },
92
+ ];
93
+ const floors = {};
94
+ for (const p of PINS) {
95
+ const declared = declaredRange(cliPkg, p.pkg);
96
+ const floor = floorOf(declared);
97
+ assert(!!floor, `${p.pkg}: declared in v3/@claude-flow/cli/package.json (got ${declared})`);
98
+ if (!floor) continue;
99
+ floors[p.pkg] = floor.join('.');
100
+ const laterPatch = [floor[0], floor[1], floor[2] + 100].join('.');
101
+ assert(satisfiesTildeRange(floors[p.pkg], p.pin),
102
+ `${p.helper} pin ${p.pin} accepts ${floors[p.pkg]} (floor of declared ${declared})`);
103
+ assert(satisfiesTildeRange(laterPatch, p.pin),
104
+ `${p.helper} pin ${p.pin} accepts ${laterPatch} (a later patch inside declared ${declared})`);
105
+ }
106
+
107
+ // ──────────────────────────────────────────────────────────────────────
108
+ // Phase 2 — published layout: the installed copy is used, nothing downloads
109
+ // ──────────────────────────────────────────────────────────────────────
110
+ console.log('\nPhase 2 — published layout resolves the installed metaharness without npm/npx');
111
+ // realpath: macOS tmpdir() is /var/… while resolved paths come back as /private/var/…
112
+ const TMP = realpathSync(mkdtempSync(join(tmpdir(), 'ruflo-pin-alignment-')));
113
+ try {
114
+ const NM = join(TMP, 'node_modules');
115
+ const PLUGIN_SCRIPTS = join(NM, '@claude-flow', 'cli', 'plugins', 'ruflo-metaharness', 'scripts');
116
+ mkdirSync(PLUGIN_SCRIPTS, { recursive: true });
117
+ for (const f of readdirSync(SCRIPTS_DIR)) {
118
+ if (f.endsWith('.mjs')) copyFileSync(join(SCRIPTS_DIR, f), join(PLUGIN_SCRIPTS, f));
119
+ }
120
+
121
+ // Stub `metaharness` at the declared floor: both bins echo a marker JSON.
122
+ const mhVersion = floors.metaharness ?? '0.0.0';
123
+ const MH = join(NM, 'metaharness');
124
+ mkdirSync(join(MH, 'dist'), { recursive: true });
125
+ writeFileSync(join(MH, 'package.json'), JSON.stringify({
126
+ name: 'metaharness', version: mhVersion, type: 'module',
127
+ bin: { metaharness: 'dist/bin.js', harness: 'dist/harness-bin.js' },
128
+ }, null, 2));
129
+ for (const [file, bin] of [['bin.js', 'metaharness'], ['harness-bin.js', 'harness']]) {
130
+ writeFileSync(join(MH, 'dist', file),
131
+ `console.log(JSON.stringify({ stub: '${bin}', version: '${mhVersion}', argv: process.argv.slice(2) }));\n`);
132
+ }
133
+
134
+ // npm / npx trap: log the attempt, fail like an offline machine would.
135
+ const TRAP = join(TMP, 'trap-bin');
136
+ const TRAP_LOG = join(TMP, 'trap.log');
137
+ mkdirSync(TRAP);
138
+ for (const t of ['npm', 'npx']) {
139
+ writeFileSync(join(TRAP, t), `#!/bin/sh\nprintf '%s %s\\n' "${t}" "$*" >> "${TRAP_LOG}"\necho "npm ERR! ${t} blocked by test-pin-alignment trap" >&2\nexit 1\n`);
140
+ chmodSync(join(TRAP, t), 0o755);
141
+ }
142
+ const PROJECT = join(TMP, 'project');
143
+ mkdirSync(PROJECT);
144
+ const env = {
145
+ ...process.env,
146
+ PATH: `${TRAP}:${process.env.PATH}`,
147
+ RUFLO_METAHARNESS_CACHE_BASE: join(TMP, 'empty-cache-base'),
148
+ };
149
+ delete env.RUFLO_METAHARNESS_SKIP_LOCAL;
150
+ const run = (args) => spawnSync(process.execPath, args, { cwd: PROJECT, env, encoding: 'utf-8', timeout: 60_000 });
151
+
152
+ const probe = run(['--input-type=module', '-e',
153
+ `const m = await import(${JSON.stringify(pathToFileURL(join(PLUGIN_SCRIPTS, '_harness.mjs')).href)});` +
154
+ 'console.log(JSON.stringify(m.metaharnessResolution()));']);
155
+ let resolution = null;
156
+ try { resolution = JSON.parse(probe.stdout.trim().split('\n').pop()); } catch { /* asserted below */ }
157
+ assert(resolution?.ok === true && resolution?.source === 'local',
158
+ `metaharnessResolution() → source 'local' (got ${JSON.stringify(resolution)})`);
159
+ assert(typeof resolution?.bins?.metaharness === 'string' && resolution.bins.metaharness.startsWith(MH),
160
+ 'resolved metaharness bin is the installed copy next to @claude-flow/cli');
161
+
162
+ const score = run([join(PLUGIN_SCRIPTS, 'score.mjs'), '--path', PROJECT, '--format', 'json']);
163
+ let payload = null;
164
+ try { payload = JSON.parse(score.stdout); } catch { /* asserted below */ }
165
+ assert(score.status === 0, `score.mjs exits 0 (got ${score.status})`);
166
+ assert(payload?.degraded !== true, `score.mjs is not degraded (reason: ${payload?.reason ?? 'none'})`);
167
+ assert(payload?.stub === 'metaharness' && payload?.version === mhVersion,
168
+ `score.mjs ran the installed metaharness ${mhVersion}`);
169
+
170
+ const trapped = existsSync(TRAP_LOG) ? readFileSync(TRAP_LOG, 'utf-8').trim() : '';
171
+ assert(trapped === '', `no npm / npx process spawned${trapped ? ` (got: ${trapped.split('\n')[0]})` : ''}`);
172
+ } finally {
173
+ rmSync(TMP, { recursive: true, force: true });
174
+ }
175
+
176
+ console.log(`\n${passed} passed, ${failed} failed`);
177
+ if (failed > 0) {
178
+ console.log('\nFailures:');
179
+ for (const f of failures) console.log(` - ${f}`);
180
+ process.exit(1);
181
+ }
182
+ console.log('\n✓ Plugin helper pins accept the versions @claude-flow/cli declares; no tool-time download.');
@@ -35,8 +35,10 @@ Implementation: [`scripts/evolve.mjs`](../../scripts/evolve.mjs).
35
35
  ≤ 20, `--concurrency` ≤ 8, sandbox/selection/mutator are known values).
36
36
  2. Without `--confirm`: print plan + exit 0 (mirrors `harness-mint` safety
37
37
  convention; defense in depth over the upstream `safety.ts` checks).
38
- 3. With `--confirm`: shell to `npx -y @metaharness/darwin@~0.8.0 metaharness-darwin evolve <repo> ...`
39
- via the shared `_darwin.mjs` async helper. Per-generation progress is
38
+ 3. With `--confirm`: run `metaharness-darwin evolve <repo> ...` from the installed
39
+ `@metaharness/darwin` (pin in `_darwin.mjs`; one-time `~/.ruflo/darwin-cache-<pin>`
40
+ install only if no installed copy qualifies — never npx) via the shared
41
+ `_darwin.mjs` async helper. Per-generation progress is
40
42
  forwarded to stderr; final champion JSON is captured from stdout.
41
43
  4. Compute timeout from `generations × children × per-variant` (per-variant
42
44
  ≈ 60s real, ≈ 2s mock). Caller may override with `--timeout-ms`.
@@ -39,7 +39,7 @@ cross a subprocess boundary. Two supported paths instead:
39
39
  Implementation: [`scripts/gepa.mjs`](../../scripts/gepa.mjs).
40
40
 
41
41
  1. `import('@metaharness/darwin/gepa')`; on MODULE_NOT_FOUND fall back to a
42
- one-time `npm install --prefix ~/.ruflo/darwin-cache-0.8.0` and import
42
+ one-time `npm install --prefix ~/.ruflo/darwin-cache-<pin>` and import
43
43
  the cached `dist/gepa/index.js` (versioned dir → pin bumps invalidate).
44
44
  2. Dispatch `--op`:
45
45
  - `genome` → `loadGenome(fs, path)` or `loadCand6Genome()` + `validateGenome`
@@ -41,7 +41,7 @@ ADR-235 follow-up and not available yet.
41
41
  Implementation: [`scripts/learn.mjs`](../../scripts/learn.mjs).
42
42
 
43
43
  1. Validate `--repo` exists when given; export it as `$METAHARNESS_REPO`.
44
- 2. Invoke the pinned `metaharness` binary (`metaharness@~0.3.0`, local install
44
+ 2. Invoke the pinned `metaharness` binary (`metaharness@~0.4.1`, local install
45
45
  or one-time versioned cache — never `@latest`): `metaharness learn --host <h>
46
46
  --model <m> --slice <s> [--run]` via `_harness.mjs` (graceful degradation,
47
47
  hard timeout).
@@ -13,7 +13,7 @@ tool; pure static analysis.
13
13
 
14
14
  Implementation: [`scripts/mcp-scan.mjs`](../../scripts/mcp-scan.mjs).
15
15
 
16
- 1. Invoke the pinned `harness` binary (`metaharness@~0.3.0`, resolved from a
16
+ 1. Invoke the pinned `harness` binary (`metaharness@~0.4.1`, resolved from a
17
17
  local install or the one-time `~/.ruflo/metaharness-cache-<pin>` cache —
18
18
  never `@latest`): `harness mcp-scan <path> --json`.
19
19
  2. Parse `findings[]` with `{ severity, id, server, tool, message }`.
@@ -58,7 +58,7 @@ no network, registry unreachable), the script emits:
58
58
  {
59
59
  "degraded": true,
60
60
  "reason": "metaharness-not-available",
61
- "hint": "Install with `npm i -D metaharness@~0.3.0` (pinned range — this plugin never fetches @latest) or verify network access for the one-time cache install."
61
+ "hint": "Install with `npm i -D metaharness@~0.4.1` (pinned range — this plugin never fetches @latest) or verify network access for the one-time cache install."
62
62
  }
63
63
  ```
64
64
 
@@ -29,7 +29,9 @@ ground-truth set. Running this nightly gives us:
29
29
 
30
30
  Implementation: [`scripts/security-bench.mjs`](../../scripts/security-bench.mjs).
31
31
 
32
- 1. Shell to `npx -y @metaharness/darwin@~0.8.0 metaharness-darwin security bench --population N --cycles N [--seed S]`.
32
+ 1. Run `metaharness-darwin security bench --population N --cycles N [--seed S]` from the
33
+ installed `@metaharness/darwin` (pin in `_darwin.mjs`; one-time cache install only if
34
+ no installed copy qualifies — never npx).
33
35
  2. Default timeout = `3s × 19 evaluations × population × cycles + 30s overhead`.
34
36
  At default `--population 2 --cycles 1` ≈ 144s; at `--population 4 --cycles 3` ≈ 12 min.
35
37
  3. Parse the markdown report — overall PASS/FAIL plus per-gate
@@ -13,7 +13,7 @@ categorized report suitable for sharing with an InfoSec team.
13
13
 
14
14
  Implementation: [`scripts/threat-model.mjs`](../../scripts/threat-model.mjs).
15
15
 
16
- 1. Invoke the pinned `harness` binary (`metaharness@~0.3.0`, resolved from a
16
+ 1. Invoke the pinned `harness` binary (`metaharness@~0.4.1`, resolved from a
17
17
  local install or the one-time `~/.ruflo/metaharness-cache-<pin>` cache —
18
18
  never `@latest`): `harness threat-model <path> --json`.
19
19
  2. Parse `{ worst, findings[] }`.