@claude-flow/cli 3.45.0 → 3.46.1

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 (77) 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 +59 -48
  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/runtime/browser-command.d.ts +29 -0
  28. package/dist/src/runtime/browser-command.js +101 -0
  29. package/dist/src/ruvector/model-router.js +19 -1
  30. package/dist/src/services/distill-oracle.js +2 -1
  31. package/dist/src/services/git-workspace-identity.js +5 -8
  32. package/dist/src/services/policy-runtime.js +114 -4
  33. package/dist/src/services/worker-daemon.js +8 -3
  34. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  35. package/node_modules/@claude-flow/codex/dist/worktrees/coordinator.d.ts.map +1 -1
  36. package/node_modules/@claude-flow/codex/dist/worktrees/coordinator.js +2 -6
  37. package/node_modules/@claude-flow/codex/dist/worktrees/coordinator.js.map +1 -1
  38. package/node_modules/@claude-flow/mcp/dist/transport/http.d.ts.map +1 -1
  39. package/node_modules/@claude-flow/mcp/dist/transport/http.js +14 -1
  40. package/node_modules/@claude-flow/mcp/dist/transport/http.js.map +1 -1
  41. package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/planner-descriptor.d.ts +40 -0
  42. package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/planner-descriptor.d.ts.map +1 -0
  43. package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/planner-descriptor.js +100 -0
  44. package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/planner-descriptor.js.map +1 -0
  45. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  46. package/node_modules/@claude-flow/plugin-agent-federation/dist/index.d.ts +1 -0
  47. package/node_modules/@claude-flow/plugin-agent-federation/dist/index.d.ts.map +1 -1
  48. package/node_modules/@claude-flow/plugin-agent-federation/dist/index.js +1 -0
  49. package/node_modules/@claude-flow/plugin-agent-federation/dist/index.js.map +1 -1
  50. package/node_modules/@claude-flow/plugin-agent-federation/package.json +1 -0
  51. package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
  52. package/node_modules/@claude-flow/security/dist/safe-git.d.ts +22 -0
  53. package/node_modules/@claude-flow/security/dist/safe-git.d.ts.map +1 -0
  54. package/node_modules/@claude-flow/security/dist/safe-git.js +63 -0
  55. package/node_modules/@claude-flow/security/dist/safe-git.js.map +1 -0
  56. package/package.json +5 -5
  57. package/plugins/ruflo-metaharness/agents/metaharness-architect.md +1 -1
  58. package/plugins/ruflo-metaharness/commands/ruflo-metaharness.md +1 -1
  59. package/plugins/ruflo-metaharness/scripts/_darwin.mjs +84 -12
  60. package/plugins/ruflo-metaharness/scripts/_harness.mjs +10 -4
  61. package/plugins/ruflo-metaharness/scripts/_invoke.mjs +120 -6
  62. package/plugins/ruflo-metaharness/scripts/_redblue.mjs +33 -6
  63. package/plugins/ruflo-metaharness/scripts/audit-list.mjs +11 -10
  64. package/plugins/ruflo-metaharness/scripts/audit-trend.mjs +8 -7
  65. package/plugins/ruflo-metaharness/scripts/drift-from-history.mjs +2 -3
  66. package/plugins/ruflo-metaharness/scripts/oia-audit.mjs +8 -7
  67. package/plugins/ruflo-metaharness/scripts/similarity.mjs +9 -8
  68. package/plugins/ruflo-metaharness/scripts/smoke.sh +31 -0
  69. package/plugins/ruflo-metaharness/scripts/test-no-tool-time-npx.mjs +319 -0
  70. package/plugins/ruflo-metaharness/scripts/test-pin-alignment.mjs +182 -0
  71. package/plugins/ruflo-metaharness/skills/harness-evolve/SKILL.md +4 -2
  72. package/plugins/ruflo-metaharness/skills/harness-gepa/SKILL.md +1 -1
  73. package/plugins/ruflo-metaharness/skills/harness-learn/SKILL.md +1 -1
  74. package/plugins/ruflo-metaharness/skills/harness-mcp-scan/SKILL.md +1 -1
  75. package/plugins/ruflo-metaharness/skills/harness-score/SKILL.md +1 -1
  76. package/plugins/ruflo-metaharness/skills/harness-security-bench/SKILL.md +3 -1
  77. package/plugins/ruflo-metaharness/skills/harness-threat-model/SKILL.md +1 -1
@@ -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;
@@ -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.');