@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
@@ -2,16 +2,17 @@
2
2
  //
3
3
  // Mirrors `_harness.mjs` for the umbrella `metaharness` / `harness` binaries,
4
4
  // but targets the separate darwin binary (`metaharness-darwin`) which is
5
- // published as its own npm package (`@metaharness/darwin@~0.8.0`).
5
+ // published as its own npm package (`@metaharness/darwin@~0.10.2`).
6
6
  //
7
7
  // Shared plumbing (degraded classification, --json injection, trailing-JSON
8
8
  // parse, degraded emitter) lives in `_invoke.mjs`. What stays here is the
9
9
  // genuinely-darwin part: the ASYNC STREAMING variant (`runDarwinAsync`) that
10
10
  // long `evolve` runs need for per-generation progress visibility.
11
11
  //
12
- // Three subcommands surfaced (matches darwin 0.8.x — same verbs as 0.3.x,
13
- // with GEPA-engine evolve flags added upstream: --selection modes,
14
- // --crossover, --epistasis, --curriculum, --mutator ruvllm, --sandbox):
12
+ // Three subcommands surfaced (matches darwin 0.8.x-0.10.x — same verbs as
13
+ // 0.3.x, with GEPA-engine evolve flags added upstream: --selection modes,
14
+ // --crossover, --epistasis, --curriculum, --mutator ruvllm, --sandbox;
15
+ // 0.10.x only ADDS `evolve-numeric`, which this helper does not surface):
15
16
  // - `metaharness-darwin evolve <repo> [...]` — harness self-improvement
16
17
  // - `metaharness-darwin bench <create|verify> ...` — bench-suite lifecycle
17
18
  // - `metaharness-darwin security bench [...]` — Darwin Shield (upstream ADR-155)
@@ -31,23 +32,43 @@
31
32
  // matched to their --generations × --children × sandbox cost; the 60s
32
33
  // default is for `bench verify` and `security bench --population 2 --cycles 1`
33
34
  // smoke shapes only.
35
+ //
36
+ // RESOLUTION (#3366 — was `npx -y -p @metaharness/darwin@<pin>` on every call)
37
+ // Same order as _harness.mjs: (a) an installed @metaharness/darwin that
38
+ // satisfies the pin (the @claude-flow/cli optionalDependency — free), then
39
+ // (b) the one-time versioned `~/.ruflo/darwin-cache-<pin>` install that gepa
40
+ // already shares, then `process.execPath <abs bin>` with shell:false. The
41
+ // npx path had npm resolve the range on every evolve / bench /
42
+ // security-bench call (registry or npx cache state), ignored the copy ruflo
43
+ // ships, and could run a different darwin than the gepa import in the same
44
+ // plugin. (a) searches ruflo's own tree only — see findLocalPackageDir's
45
+ // `fromCwd` in _invoke.mjs.
34
46
 
35
47
  import { spawnSync, spawn } from 'node:child_process';
36
48
  import {
37
49
  classifyDegraded,
50
+ ensureCachedInstall,
51
+ findLocalPackageDir,
38
52
  importOptionalLibrary,
39
53
  injectJson,
40
54
  makeDegradedEmitter,
55
+ packageBinRelPath,
41
56
  parseTrailingJson,
57
+ resolvePackageBin,
42
58
  } from './_invoke.mjs';
43
59
 
44
60
  const DEFAULT_TIMEOUT_MS = 60_000;
45
61
 
46
- // Pinned semver range. Bump in lock-step with optionalDependencies in
47
- // @claude-flow/cli/package.json + ruflo/package.json. The `~` allows
48
- // patch upgrades without re-pinning.
62
+ // Pinned semver range. The `~` allows patch upgrades without re-pinning.
63
+ // Must cover the `@metaharness/darwin` optionalDependency range in
64
+ // v3/@claude-flow/cli/package.json (`~0.10.2`). It sat at ~0.8.0 while that
65
+ // range moved to ~0.9.0 (#2958) and ~0.10.2 (#3262), because the pin-drift
66
+ // guard only compared MH_DARWIN_PIN (distill-oracle.ts). This range picks
67
+ // the darwin version the evolve / bench / security-bench tools run and the
68
+ // gepa cache fallback installs; scripts/check-metaharness-pins.mjs now fails
69
+ // when it no longer covers the declared range.
49
70
  const DARWIN_PKG = '@metaharness/darwin';
50
- const DARWIN_PIN_VERSION = '~0.8.0';
71
+ const DARWIN_PIN_VERSION = '~0.10.2';
51
72
  const DARWIN_PIN = `${DARWIN_PKG}@${DARWIN_PIN_VERSION}`;
52
73
 
53
74
  const REASON_PREFIX = 'metaharness-darwin';
@@ -68,22 +89,71 @@ function buildArgv(args, wantJson) {
68
89
  // (This last-block parse is now the family-wide shared implementation.)
69
90
  const maybeParseJson = parseTrailingJson;
70
91
 
92
+ const DARWIN_BIN = 'metaharness-darwin';
93
+
94
+ /**
95
+ * Absolute path of the `metaharness-darwin` bin (see RESOLUTION above).
96
+ * Memoized per process, like _harness.mjs resolveMetaharnessBins().
97
+ */
98
+ let RESOLVED = null;
99
+ function resolveDarwinBin() {
100
+ if (RESOLVED) return RESOLVED;
101
+ // fromCwd:false — this lookup ends in a spawn of the bin with argv that
102
+ // comes from MCP input, so it only searches the ruflo install that owns
103
+ // this plugin, never the tool's cwd or its ancestors (see _invoke.mjs).
104
+ const localDir = findLocalPackageDir(DARWIN_PKG, DARWIN_PIN_VERSION, { fromCwd: false });
105
+ const localBin = localDir ? resolvePackageBin(localDir, DARWIN_BIN) : null;
106
+ if (localBin) return (RESOLVED = { ok: true, bin: localBin, source: 'local' });
107
+ let cached = ensureCachedInstall({ pkg: DARWIN_PKG, pinVersion: DARWIN_PIN_VERSION });
108
+ let cachedBin = cached.ok ? resolvePackageBin(cached.pkgDir, DARWIN_BIN) : null;
109
+ // The default cache probe is `<pkgDir>/package.json`, so a half-written
110
+ // cache — npm killed by the 180s install timeout, or two first calls
111
+ // installing into the same prefix — would count as installed forever and
112
+ // degrade every later call. npx repaired that by re-resolving; re-run the
113
+ // install once, probing the bin file itself, then give up (#3366).
114
+ if (cached.ok && !cachedBin) {
115
+ cached = ensureCachedInstall({
116
+ pkg: DARWIN_PKG,
117
+ pinVersion: DARWIN_PIN_VERSION,
118
+ cliRelPath: packageBinRelPath(cached.pkgDir, DARWIN_BIN) ?? 'dist/cli.js',
119
+ });
120
+ cachedBin = cached.ok ? resolvePackageBin(cached.pkgDir, DARWIN_BIN) : null;
121
+ }
122
+ if (cachedBin) return (RESOLVED = { ok: true, bin: cachedBin, source: 'cache' });
123
+ return (RESOLVED = {
124
+ ok: false,
125
+ stdout: cached.stdout ?? '',
126
+ stderr: cached.stderr ?? cached.error ?? `${DARWIN_PIN} has no ${DARWIN_BIN} bin`,
127
+ });
128
+ }
129
+
130
+ /** Same degraded shape the npx path produced when the package was unreachable. */
131
+ function unavailable(resolved, start) {
132
+ return {
133
+ stdout: resolved.stdout, stderr: resolved.stderr,
134
+ exitCode: 127, json: null, durationMs: Date.now() - start,
135
+ degraded: true, reason: `${REASON_PREFIX}-not-available`,
136
+ };
137
+ }
138
+
71
139
  /**
72
140
  * Sync invocation. Use for short subcommands (`bench verify`, smoke shapes).
73
141
  * Async variant below for long-running `evolve` calls.
74
142
  */
75
143
  export function runDarwin(args, opts = {}) {
76
144
  const start = Date.now();
145
+ const resolved = resolveDarwinBin();
146
+ if (!resolved.ok) return unavailable(resolved, start);
77
147
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
78
148
  const wantJson = opts.json !== false;
79
149
  const argv = buildArgv(args, wantJson);
80
- const r = spawnSync('npx', ['-y', '-p', DARWIN_PIN, 'metaharness-darwin', ...argv], {
150
+ const r = spawnSync(process.execPath, [resolved.bin, ...argv], {
81
151
  stdio: ['ignore', 'pipe', 'pipe'],
82
152
  encoding: 'utf-8',
83
153
  timeout: timeoutMs,
84
154
  cwd: opts.cwd,
85
155
  env: { ...process.env, ...(opts.env || {}) },
86
- shell: process.platform === 'win32',
156
+ shell: false,
87
157
  });
88
158
  const durationMs = Date.now() - start;
89
159
  const stdout = r.stdout || '';
@@ -119,14 +189,16 @@ export function runDarwin(args, opts = {}) {
119
189
  export function runDarwinAsync(args, opts = {}) {
120
190
  return new Promise((resolve) => {
121
191
  const start = Date.now();
192
+ const resolved = resolveDarwinBin();
193
+ if (!resolved.ok) { resolve(unavailable(resolved, start)); return; }
122
194
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
123
195
  const wantJson = opts.json !== false;
124
196
  const argv = buildArgv(args, wantJson);
125
- const p = spawn('npx', ['-y', '-p', DARWIN_PIN, 'metaharness-darwin', ...argv], {
197
+ const p = spawn(process.execPath, [resolved.bin, ...argv], {
126
198
  stdio: ['ignore', 'pipe', 'pipe'],
127
199
  cwd: opts.cwd,
128
200
  env: { ...process.env, ...(opts.env || {}) },
129
- shell: process.platform === 'win32',
201
+ shell: false,
130
202
  });
131
203
  let stdout = '', stderr = '', stderrBuf = '';
132
204
  p.stdout?.on('data', (d) => { stdout += d.toString(); });
@@ -26,7 +26,8 @@
26
26
  // arbitrary code on user machines on the very next skill invocation.
27
27
  // The version is now PINNED to METAHARNESS_PIN_VERSION (tilde range —
28
28
  // patch updates only) and only bumped deliberately, in lock-step with
29
- // optionalDependencies in @claude-flow/cli + ruflo package.json.
29
+ // the range declared in @claude-flow/cli/package.json (enforced by
30
+ // scripts/check-metaharness-pins.mjs).
30
31
  // 2. PERF: @latest forced an npm-registry metadata check on EVERY call.
31
32
  // Resolution is now (a) an already-installed local metaharness
32
33
  // satisfying the pin (walk-up node_modules — free), then (b) a ONE-TIME
@@ -53,10 +54,15 @@ import {
53
54
 
54
55
  const DEFAULT_TIMEOUT_MS = 60_000;
55
56
 
56
- // Pinned semver range. Bump in lock-step with optionalDependencies in
57
- // @claude-flow/cli/package.json + ruflo/package.json. NEVER @latest.
57
+ // Pinned semver range. NEVER @latest. Must cover the `metaharness` range
58
+ // declared in v3/@claude-flow/cli/package.json (optional peer `^0.4.1`):
59
+ // this constant decides which INSTALLED copy findLocalPackageDir() accepts,
60
+ // so a range that misses the declared one rejects the copy ruflo ships and
61
+ // npm-installs another at tool-call time. It sat at ~0.3.0 after the CLI
62
+ // moved to ^0.4.1 (b8ec03f34) because nothing compared the two;
63
+ // scripts/check-metaharness-pins.mjs now fails when they diverge.
58
64
  const METAHARNESS_PKG = 'metaharness';
59
- const METAHARNESS_PIN_VERSION = '~0.3.0';
65
+ const METAHARNESS_PIN_VERSION = '~0.4.1';
60
66
 
61
67
  const REASON_NOT_AVAILABLE = 'metaharness-not-available';
62
68
 
@@ -21,9 +21,15 @@
21
21
  // invalidate stale caches automatically
22
22
  // - findLocalPackageDir() walk-up node_modules resolution so an already
23
23
  // installed optionalDependency is used for free
24
+ // - resolvePackageBin() realpath'd bin-map entry of a resolved package
25
+ // (#3366 — lets _darwin/_redblue run the installed
26
+ // copy instead of npx / a cache install)
24
27
  // - importOptionalLibrary() bare-import → cached-install fallback for
25
28
  // library entries (gepa) — never throws on absence
26
29
  // - makeDegradedEmitter() the ADR-150 rule-#3 exit-0 degraded payload
30
+ // - runRufloCli() `memory store|list|retrieve` through the ruflo CLI
31
+ // that ships this plugin (#3366) — replaces
32
+ // `npx @claude-flow/cli@latest` in 4 scripts
27
33
  //
28
34
  // WHAT DOES NOT LIVE HERE (the per-consumer parts):
29
35
  // - _redblue's node-direct isMain workaround rationale
@@ -36,7 +42,7 @@
36
42
  // - RUFLO_METAHARNESS_SKIP_LOCAL=1 disables local node_modules resolution
37
43
 
38
44
  import { spawnSync } from 'node:child_process';
39
- import { existsSync, mkdirSync, readFileSync } from 'node:fs';
45
+ import { existsSync, mkdirSync, readFileSync, realpathSync } from 'node:fs';
40
46
  import { homedir } from 'node:os';
41
47
  import { dirname, join } from 'node:path';
42
48
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -102,11 +108,22 @@ export function satisfiesTildeRange(version, pinVersion) {
102
108
  * user's ruflo install shipped with). Returns the package dir or null.
103
109
  * When `pinVersion` is given, only a copy satisfying the pin is accepted —
104
110
  * a stale major/minor in an ancestor node_modules is skipped, not used.
111
+ *
112
+ * `{ fromCwd: false }` (#3366) searches ONLY the ruflo install that owns this
113
+ * plugin. The $CWD walk reaches every ancestor of the tool's working
114
+ * directory, i.e. directories the user's project — or anything above it —
115
+ * controls. That is tolerable for the pre-existing `metaharness` lookup,
116
+ * which this PR does not change, but the two lookups added here (_darwin,
117
+ * _redblue) END IN A SPAWN of the package's bin with MCP-supplied argv, so
118
+ * they stay inside ruflo's own tree and fall back to the pinned cache
119
+ * install instead.
105
120
  */
106
- export function findLocalPackageDir(pkg, pinVersion) {
121
+ export function findLocalPackageDir(pkg, pinVersion, { fromCwd = true } = {}) {
107
122
  if (process.env.RUFLO_METAHARNESS_SKIP_LOCAL === '1') return null;
108
123
  const segments = pkg.split('/');
109
- const starts = [dirname(fileURLToPath(import.meta.url)), process.cwd()];
124
+ const starts = fromCwd
125
+ ? [dirname(fileURLToPath(import.meta.url)), process.cwd()]
126
+ : [dirname(fileURLToPath(import.meta.url))];
110
127
  const seen = new Set();
111
128
  for (const start of starts) {
112
129
  let dir = start;
@@ -131,17 +148,51 @@ export function findLocalPackageDir(pkg, pinVersion) {
131
148
  return null;
132
149
  }
133
150
 
151
+ /**
152
+ * Absolute path of `binName` from the package.json `bin` map of an already
153
+ * resolved package dir (findLocalPackageDir / ensureCachedInstall), or null.
154
+ * Read from the bin map rather than hardcoded, same as _harness.mjs.
155
+ *
156
+ * REALPATH'd on purpose (#3366): pnpm and other symlinked layouts hand us a
157
+ * symlinked package dir, and @metaharness/redblue's CLI only dispatches when
158
+ * `import.meta.url === file://${process.argv[1]}` (see _redblue.mjs) — Node
159
+ * reports import.meta.url as the REAL path, so argv[1] has to be one too or
160
+ * the CLI exits 0 having done nothing.
161
+ */
162
+ export function resolvePackageBin(pkgDir, binName) {
163
+ const rel = packageBinRelPath(pkgDir, binName);
164
+ if (!rel) return null;
165
+ try {
166
+ const abs = join(pkgDir, rel);
167
+ return existsSync(abs) ? realpathSync(abs) : null;
168
+ } catch {
169
+ return null;
170
+ }
171
+ }
172
+
173
+ /** The package.json `bin` entry for `binName`, as declared (relative), or null. */
174
+ export function packageBinRelPath(pkgDir, binName) {
175
+ try {
176
+ const pj = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf-8'));
177
+ const rel = typeof pj.bin === 'string' ? pj.bin : pj.bin?.[binName];
178
+ return rel || null;
179
+ } catch {
180
+ return null;
181
+ }
182
+ }
183
+
134
184
  /**
135
185
  * One-time versioned cache install of a PINNED range. Generalizes
136
186
  * gepa.mjs:98-121 / _redblue.mjs:72-102. The cache dir is
137
187
  * `<base>/<shortname>-cache-<pin-digits>` (e.g. redblue-cache-0.1.4,
138
- * darwin-cache-0.8.0, metaharness-cache-0.3.0) so existing caches created
188
+ * darwin-cache-0.10.2, metaharness-cache-0.4.1) so existing caches created
139
189
  * by the pre-consolidation helpers remain valid, and bumping the pin
140
- * invalidates stale installs automatically.
190
+ * invalidates stale installs automatically (the pre-bump darwin-cache-0.8.0
191
+ * and metaharness-cache-0.3.0 dirs are simply no longer selected).
141
192
  *
142
193
  * @param {object} spec
143
194
  * @param {string} spec.pkg npm package name (may be scoped)
144
- * @param {string} spec.pinVersion tilde range, e.g. '~0.3.0'
195
+ * @param {string} spec.pinVersion tilde range, e.g. '~0.4.1'
145
196
  * @param {string} [spec.cliRelPath] path inside the package that must exist
146
197
  * post-install (also returned as cliPath)
147
198
  * @param {number} [spec.timeoutMs] install timeout (default 180s)
@@ -228,3 +279,66 @@ export function makeDegradedEmitter(pkg, pinVersion) {
228
279
  process.exit(0);
229
280
  };
230
281
  }
282
+
283
+ // ---------------------------------------------------------------------------
284
+ // The ruflo CLI that ships this plugin (#3366)
285
+ // ---------------------------------------------------------------------------
286
+ // audit-list / audit-trend / oia-audit / similarity read and write the
287
+ // `metaharness-audit` memory namespace through the ruflo CLI. They used to
288
+ // spawn `npx @claude-flow/cli@latest memory …`, which on every call:
289
+ // - resolved `latest` through the npm registry (the "metadata check on
290
+ // EVERY call" _harness.mjs removed for its own npx path); without registry
291
+ // access it fails, so oia_audit reports `persisted: false` and audit_list
292
+ // silently reports 0 records;
293
+ // - could run a DIFFERENT @claude-flow/cli than the one that spawned the
294
+ // tool (npx cache vs registry skew — #3306), i.e. another memory backend /
295
+ // schema than the MCP server's own memory_* tools see;
296
+ // - cold-fetched the whole CLI when the npx cache was empty (#3145, #3154).
297
+ // The plugin is published INSIDE @claude-flow/cli
298
+ // (<cli>/plugins/ruflo-metaharness/scripts — see prepare-publish.mjs), and the
299
+ // MCP server / `ruflo metaharness` dispatcher locate it by walking up from
300
+ // their own dist/, so the owning CLI is two directories above this plugin.
301
+ // It is run with process.execPath + bin/cli.js (the mcp-launch.cjs /
302
+ // daemon-autostart.ts pattern). A candidate only counts when dist/src/index.js
303
+ // exists next to bin/cli.js — mcp-launch.cjs resolveLocalCliBin()'s guard
304
+ // against an unbuilt checkout — AND its package.json is named
305
+ // @claude-flow/cli, a check added here so an unrelated directory two levels up
306
+ // can never qualify. Unchanged:
307
+ // - CLI_CORE=1 still opts into `npx @claude-flow/cli-core@alpha` (ADR-100);
308
+ // - no usable local CLI (e.g. a marketplace clone with no build) still falls
309
+ // back to `npx @claude-flow/cli@latest`.
310
+ const PLUGIN_SCRIPTS_DIR = dirname(fileURLToPath(import.meta.url));
311
+
312
+ let RUFLO_CLI = null;
313
+ /** `{ command, args, shell, source }` for invoking the ruflo CLI. Memoized. */
314
+ export function resolveRufloCli() {
315
+ if (RUFLO_CLI) return RUFLO_CLI;
316
+ if (process.env.CLI_CORE === '1') {
317
+ return (RUFLO_CLI = { command: 'npx', args: ['@claude-flow/cli-core@alpha'], shell: process.platform === 'win32', source: 'npx-cli-core' });
318
+ }
319
+ const candidates = [
320
+ join(PLUGIN_SCRIPTS_DIR, '..', '..', '..'), // published: <cli>/plugins/ruflo-metaharness/scripts
321
+ join(PLUGIN_SCRIPTS_DIR, '..', '..', '..', 'v3', '@claude-flow', 'cli'), // repo checkout / marketplace clone
322
+ ];
323
+ for (const dir of candidates) {
324
+ try {
325
+ const bin = join(dir, 'bin', 'cli.js');
326
+ const pj = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'));
327
+ if (pj.name === '@claude-flow/cli' && existsSync(bin) && existsSync(join(dir, 'dist', 'src', 'index.js'))) {
328
+ return (RUFLO_CLI = { command: process.execPath, args: [bin], shell: false, source: 'local' });
329
+ }
330
+ } catch { /* not this layout — try the next */ }
331
+ }
332
+ return (RUFLO_CLI = { command: 'npx', args: ['@claude-flow/cli@latest'], shell: process.platform === 'win32', source: 'npx' });
333
+ }
334
+
335
+ /** spawnSync the ruflo CLI with `args` (e.g. ['memory', 'list', …]); same result shape as before. */
336
+ export function runRufloCli(args, opts = {}) {
337
+ const cli = resolveRufloCli();
338
+ return spawnSync(cli.command, [...cli.args, ...args], {
339
+ stdio: ['ignore', 'pipe', 'pipe'],
340
+ encoding: 'utf-8',
341
+ shell: cli.shell,
342
+ ...opts,
343
+ });
344
+ }
@@ -32,6 +32,16 @@
32
32
  // (file separately — when fixed, we could go back to a bin-shim pattern,
33
33
  // though the node-direct cached path is now the family-wide standard anyway).
34
34
  //
35
+ // INSTALLED COPY FIRST (#3366): an @metaharness/redblue already on disk that
36
+ // satisfies the pin (a ruflo 3.42.4 install carries 0.1.4, via metaharness /
37
+ // ruvector) is used before the cache install, same order as _harness.mjs /
38
+ // _darwin.mjs. Previously the first call always ran `npm install` into the
39
+ // cache even with that copy present.
40
+ // Whichever copy is chosen, its CLI path is realpath'd
41
+ // (_invoke.resolvePackageBin / realpathSync): a pnpm-symlinked package dir,
42
+ // or a cache base under a symlink such as macOS /tmp -> /private/tmp, would
43
+ // otherwise hit the same isMain mismatch described above.
44
+ //
35
45
  // CONTRACT (matches runMetaharness/runDarwin):
36
46
  // - returns `{ stdout, stderr, exitCode, durationMs, degraded, reason? }`
37
47
  // - subprocess hard timeout (default 120s; --mock-judge runs are seconds)
@@ -47,12 +57,15 @@
47
57
  // $OPENROUTER_API_KEY which we never inject.
48
58
 
49
59
  import { spawnSync } from 'node:child_process';
60
+ import { realpathSync } from 'node:fs';
50
61
  import { homedir } from 'node:os';
51
62
  import { join } from 'node:path';
52
63
  import {
53
64
  classifyDegraded,
54
65
  ensureCachedInstall,
66
+ findLocalPackageDir,
55
67
  makeDegradedEmitter,
68
+ resolvePackageBin,
56
69
  } from './_invoke.mjs';
57
70
 
58
71
  const DEFAULT_TIMEOUT_MS = 120_000;
@@ -80,8 +93,15 @@ export function runRedblue(args, opts = {}) {
80
93
  const start = Date.now();
81
94
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
82
95
 
83
- // 1. Ensure redblue is installed in our cache dir (one-time install).
84
- const install = ensureCachedInstall({
96
+ // 0. An installed copy satisfying the pin — free, no npm (#3366).
97
+ // fromCwd:false: the resolved bin is SPAWNED with argv that comes from
98
+ // MCP input, so only ruflo's own tree is searched, never the tool's cwd
99
+ // or its ancestors (see findLocalPackageDir in _invoke.mjs).
100
+ const localDir = findLocalPackageDir(REDBLUE_PKG, REDBLUE_PIN_VERSION, { fromCwd: false });
101
+ const localCli = localDir ? resolvePackageBin(localDir, 'redblue') : null;
102
+
103
+ // 1. Otherwise ensure redblue is installed in our cache dir (one-time install).
104
+ const install = localCli ? { ok: true, cliPath: localCli } : ensureCachedInstall({
85
105
  pkg: REDBLUE_PKG,
86
106
  pinVersion: REDBLUE_PIN_VERSION,
87
107
  cliRelPath: CLI_REL_PATH,
@@ -99,15 +119,22 @@ export function runRedblue(args, opts = {}) {
99
119
  };
100
120
  }
101
121
 
102
- // 2. Invoke `node <real-path-to-cli> <args>` so upstream's isMain check
103
- // succeeds (argv[1] matches import.meta.url).
104
- const r = spawnSync('node', [install.cliPath, ...args], {
122
+ // 2. Invoke `<this node> <real-path-to-cli> <args>` so upstream's isMain
123
+ // check succeeds (argv[1] matches import.meta.url) — realpath'd, see
124
+ // header. process.execPath + shell:false, like _darwin/_harness: the
125
+ // argv carries MCP-supplied values (--config / --out / --in), and with
126
+ // shell:true on win32 Node hands cmd.exe one unquoted string, so a value
127
+ // such as `x.yaml & whoami` would run as a command (and a path with a
128
+ // space — `C:\Program Files\…` — would break). node needs no shell.
129
+ let cliPath = install.cliPath;
130
+ try { cliPath = realpathSync(cliPath); } catch { /* missing — node's MODULE_NOT_FOUND → degraded below */ }
131
+ const r = spawnSync(process.execPath, [cliPath, ...args], {
105
132
  stdio: ['ignore', 'pipe', 'pipe'],
106
133
  encoding: 'utf-8',
107
134
  timeout: timeoutMs,
108
135
  cwd: opts.cwd,
109
136
  env: { ...process.env, ...(opts.env || {}) },
110
- shell: process.platform === 'win32',
137
+ shell: false,
111
138
  });
112
139
  const durationMs = Date.now() - start;
113
140
  const stdout = r.stdout || '';
@@ -16,12 +16,13 @@
16
16
  // 0 ok
17
17
  // 2 config error
18
18
 
19
- import { spawnSync } from 'node:child_process';
19
+ import { runRufloCli } from './_invoke.mjs';
20
20
 
21
21
  const NS = process.env.AUDIT_LIST_NAMESPACE || 'metaharness-audit';
22
- const CLI_PKG = process.env.CLI_CORE === '1'
23
- ? '@claude-flow/cli-core@alpha'
24
- : '@claude-flow/cli@latest';
22
+ // #3366 — memory calls go through runRufloCli() (_invoke.mjs): the ruflo CLI
23
+ // that ships this plugin, not `npx @claude-flow/cli@latest` (npm-registry
24
+ // resolution per call, possible version skew, fails without registry access).
25
+ // CLI_CORE=1 still opts into `npx @claude-flow/cli-core@alpha`.
25
26
 
26
27
  const ARGS = (() => {
27
28
  const a = { limit: 20, since: null, format: 'table' };
@@ -43,10 +44,10 @@ function parseDurationMs(spec) {
43
44
  }
44
45
 
45
46
  function memList() {
46
- const r = spawnSync('npx', [
47
- CLI_PKG, 'memory', 'list',
47
+ const r = runRufloCli([
48
+ 'memory', 'list',
48
49
  '--namespace', NS, '--format', 'json',
49
- ], { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf-8', shell: process.platform === 'win32' });
50
+ ]);
50
51
  if (r.status !== 0) return [];
51
52
  const m = /\[[\s\S]*\]/.exec(r.stdout || '');
52
53
  if (!m) return [];
@@ -54,10 +55,10 @@ function memList() {
54
55
  }
55
56
 
56
57
  function memRetrieve(key) {
57
- const r = spawnSync('npx', [
58
- CLI_PKG, 'memory', 'retrieve',
58
+ const r = runRufloCli([
59
+ 'memory', 'retrieve',
59
60
  '--namespace', NS, '--key', key,
60
- ], { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf-8', shell: process.platform === 'win32' });
61
+ ]);
61
62
  if (r.status !== 0) return null;
62
63
  const m = /\{[\s\S]*\}/.exec(r.stdout || '');
63
64
  if (!m) return null;
@@ -25,7 +25,7 @@
25
25
  // 2 config error or input not found
26
26
 
27
27
  import { readFileSync, existsSync } from 'node:fs';
28
- import { spawnSync } from 'node:child_process';
28
+ import { runRufloCli } from './_invoke.mjs';
29
29
  // iter 38 — structural-distance drift via ADR-152 §3.1 production module.
30
30
  // Falls back to null if either record predates iter-38 oia-audit (no
31
31
  // fingerprint field) — graceful degradation, never throws.
@@ -36,9 +36,10 @@ import { SEVERITY_RANK, rankSeverity } from './_harness.mjs';
36
36
 
37
37
  // iter 63 — SEVERITY_RANK moved to _harness.mjs (imported above)
38
38
  const NS = process.env.AUDIT_TREND_NAMESPACE || 'metaharness-audit';
39
- const CLI_PKG = process.env.CLI_CORE === '1'
40
- ? '@claude-flow/cli-core@alpha'
41
- : '@claude-flow/cli@latest';
39
+ // #3366 — memory calls go through runRufloCli() (_invoke.mjs): the ruflo CLI
40
+ // that ships this plugin, not `npx @claude-flow/cli@latest` (npm-registry
41
+ // resolution per call, possible version skew, fails without registry access).
42
+ // CLI_CORE=1 still opts into `npx @claude-flow/cli-core@alpha`.
42
43
 
43
44
  const ARGS = (() => {
44
45
  const a = {
@@ -62,10 +63,10 @@ const ARGS = (() => {
62
63
  })();
63
64
 
64
65
  function memRetrieve(key) {
65
- const r = spawnSync('npx', [
66
- CLI_PKG, 'memory', 'retrieve',
66
+ const r = runRufloCli([
67
+ 'memory', 'retrieve',
67
68
  '--namespace', NS, '--key', key,
68
- ], { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf-8', shell: process.platform === 'win32' });
69
+ ]);
69
70
  if (r.status !== 0) return null;
70
71
  const m = /\{[\s\S]*\}/.exec(r.stdout || '');
71
72
  if (!m) return null;
@@ -42,9 +42,8 @@ import { fileURLToPath } from 'node:url';
42
42
 
43
43
  const SCRIPTS_DIR = dirname(fileURLToPath(import.meta.url));
44
44
  const NS = process.env.METAHARNESS_AUDIT_NAMESPACE || 'metaharness-audit';
45
- const CLI_PKG = process.env.CLI_CORE === '1'
46
- ? '@claude-flow/cli-core@alpha'
47
- : '@claude-flow/cli@latest';
45
+ // Memory access is delegated to audit-list / oia-audit / audit-trend (they
46
+ // call the shipping ruflo CLI through _invoke.runRufloCli — #3366).
48
47
 
49
48
  const ARGS = (() => {
50
49
  const a = {
@@ -24,7 +24,7 @@
24
24
  // 1 --alert-on-worst threshold exceeded
25
25
  // 2 config error or audit failure
26
26
 
27
- import { spawnSync } from 'node:child_process';
27
+ import { runRufloCli } from './_invoke.mjs';
28
28
  // iter 63 — SEVERITY_RANK + rankSeverity consolidated to _harness.mjs
29
29
  // (was local in iter 62; now shared with audit-trend + mcp-scan).
30
30
  import { runHarness, runMetaharness, runHarnessAsync, runMetaharnessAsync, emitDegradedJsonAndExit, parseMcpScanText, SEVERITY_RANK, rankSeverity } from './_harness.mjs';
@@ -33,9 +33,10 @@ import { runHarness, runMetaharness, runHarnessAsync, runMetaharnessAsync, emitD
33
33
  // source of truth). The local copy from iter 62 is gone; the imports
34
34
  // at the top of this file provide the same names.
35
35
  const NS = process.env.OIA_AUDIT_NAMESPACE || 'metaharness-audit';
36
- const CLI_PKG = process.env.CLI_CORE === '1'
37
- ? '@claude-flow/cli-core@alpha'
38
- : '@claude-flow/cli@latest';
36
+ // #3366 — memory calls go through runRufloCli() (_invoke.mjs): the ruflo CLI
37
+ // that ships this plugin, not `npx @claude-flow/cli@latest` (npm-registry
38
+ // resolution per call, possible version skew, fails without registry access).
39
+ // CLI_CORE=1 still opts into `npx @claude-flow/cli-core@alpha`.
39
40
 
40
41
  const ARGS = (() => {
41
42
  const a = { path: '.', format: 'json', dryRun: false, alertWorst: null };
@@ -97,12 +98,12 @@ async function runAllParallel(path) {
97
98
 
98
99
  function persist(payload) {
99
100
  const key = `audit-${new Date().toISOString().replace(/[:.]/g, '-')}`;
100
- const r = spawnSync('npx', [
101
- CLI_PKG, 'memory', 'store',
101
+ const r = runRufloCli([
102
+ 'memory', 'store',
102
103
  '--namespace', NS,
103
104
  '--key', key,
104
105
  '--value', JSON.stringify(payload),
105
- ], { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf-8', shell: process.platform === 'win32' });
106
+ ]);
106
107
  return {
107
108
  ok: r.status === 0,
108
109
  namespace: NS,
@@ -18,19 +18,20 @@
18
18
  //
19
19
  // ADR-150 ARCHITECTURAL CONSTRAINTS PRESERVED
20
20
  // - Pure-TS function (`_similarity.mjs`), no `@metaharness/*` import
21
- // - No new dep; uses node:fs + node:child_process only for memory lookup
21
+ // - No new dep; uses node:fs + _invoke.runRufloCli only for memory lookup
22
22
  // - Graceful degradation: missing keys / malformed JSON → exit 2 with
23
23
  // a structured `{ degraded: true, reason: ... }` payload on stdout
24
24
  // - CI-gate ready: smoke step 17y locks this contract
25
25
 
26
26
  import { readFileSync, existsSync } from 'node:fs';
27
- import { spawnSync } from 'node:child_process';
27
+ import { runRufloCli } from './_invoke.mjs';
28
28
  import { similarity } from './_similarity.mjs';
29
29
 
30
30
  const NS = process.env.HARNESS_SIMILARITY_NAMESPACE || 'metaharness-audit';
31
- const CLI_PKG = process.env.CLI_CORE === '1'
32
- ? '@claude-flow/cli-core@alpha'
33
- : '@claude-flow/cli@latest';
31
+ // #3366 — memory calls go through runRufloCli() (_invoke.mjs): the ruflo CLI
32
+ // that ships this plugin, not `npx @claude-flow/cli@latest` (npm-registry
33
+ // resolution per call, possible version skew, fails without registry access).
34
+ // CLI_CORE=1 still opts into `npx @claude-flow/cli-core@alpha`.
34
35
 
35
36
  const ARGS = (() => {
36
37
  const a = {
@@ -66,10 +67,10 @@ function emitDegradedAndExit(reason, code = 2) {
66
67
  }
67
68
 
68
69
  function memRetrieve(key) {
69
- const r = spawnSync('npx', [
70
- CLI_PKG, 'memory', 'retrieve',
70
+ const r = runRufloCli([
71
+ 'memory', 'retrieve',
71
72
  '--namespace', NS, '--key', key,
72
- ], { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf-8', shell: process.platform === 'win32' });
73
+ ]);
73
74
  if (r.status !== 0) return null;
74
75
  const m = /\{[\s\S]*\}/.exec(r.stdout || '');
75
76
  if (!m) return null;