@claude-flow/cli 3.54.1 → 3.56.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 (116) hide show
  1. package/.claude/helpers/helpers.manifest.json +2 -2
  2. package/.claude/settings.json +9 -9
  3. package/bin/cli.js +1 -1
  4. package/bin/mcp-server.js +1 -1
  5. package/catalog-manifest.json +2 -2
  6. package/dist/src/auth/client.js +40 -24
  7. package/dist/src/commands/agent.js +12 -0
  8. package/dist/src/commands/analyze.js +18 -14
  9. package/dist/src/commands/autopilot.js +2 -2
  10. package/dist/src/commands/daemon.js +15 -3
  11. package/dist/src/commands/doctor.js +65 -58
  12. package/dist/src/commands/hive-mind.js +7 -0
  13. package/dist/src/commands/mcp.js +35 -1
  14. package/dist/src/commands/memory-backup.d.ts +1 -1
  15. package/dist/src/commands/memory-backup.js +23 -9
  16. package/dist/src/commands/memory.d.ts +0 -4
  17. package/dist/src/commands/memory.js +10 -2
  18. package/dist/src/commands/plugins.js +13 -1
  19. package/dist/src/commands/policy.js +10 -2
  20. package/dist/src/commands/task.js +12 -0
  21. package/dist/src/init/settings-generator.js +3 -3
  22. package/dist/src/mcp-server.d.ts +33 -0
  23. package/dist/src/mcp-server.js +96 -4
  24. package/dist/src/mcp-tools/agent-execute-core.js +244 -188
  25. package/dist/src/mcp-tools/agent-tools.js +15 -16
  26. package/dist/src/mcp-tools/agentdb-tools.js +22 -3
  27. package/dist/src/mcp-tools/claims-tools.js +3 -11
  28. package/dist/src/mcp-tools/hive-mind-tools.d.ts +2 -4
  29. package/dist/src/mcp-tools/hive-mind-tools.js +214 -40
  30. package/dist/src/mcp-tools/hooks-tools.js +47 -2
  31. package/dist/src/mcp-tools/memory-tools.js +29 -10
  32. package/dist/src/mcp-tools/record-store.d.ts +3 -0
  33. package/dist/src/mcp-tools/record-store.js +21 -0
  34. package/dist/src/mcp-tools/session-tools.js +47 -13
  35. package/dist/src/mcp-tools/task-tools.js +5 -11
  36. package/dist/src/mcp-tools/workflow-tools.js +78 -29
  37. package/dist/src/memory/append-conditions.d.ts +28 -0
  38. package/dist/src/memory/append-conditions.js +56 -0
  39. package/dist/src/memory/graph-edge-writer.d.ts +9 -0
  40. package/dist/src/memory/graph-edge-writer.js +90 -0
  41. package/dist/src/memory/memory-bridge.d.ts +4 -0
  42. package/dist/src/memory/memory-bridge.js +50 -9
  43. package/dist/src/memory/memory-initializer.d.ts +4 -0
  44. package/dist/src/memory/memory-initializer.js +95 -61
  45. package/dist/src/memory/memory-root.d.ts +2 -0
  46. package/dist/src/memory/memory-root.js +28 -0
  47. package/dist/src/memory/sona-optimizer.d.ts +2 -0
  48. package/dist/src/memory/sona-optimizer.js +4 -0
  49. package/dist/src/missions/schemas.d.ts +125 -125
  50. package/dist/src/mods/command-registry/schema.d.ts +199 -199
  51. package/dist/src/parser.d.ts +3 -0
  52. package/dist/src/parser.js +71 -13
  53. package/dist/src/plugins/store/discovery.d.ts +6 -0
  54. package/dist/src/plugins/store/discovery.js +7 -3
  55. package/dist/src/production/retry.js +1 -1
  56. package/dist/src/ruvector/neural-router.js +7 -2
  57. package/dist/src/services/bounded-worker-pool.js +3 -0
  58. package/dist/src/services/claim-service.js +39 -18
  59. package/dist/src/services/claims-store.d.ts +3 -0
  60. package/dist/src/services/claims-store.js +40 -0
  61. package/dist/src/services/config-file-manager.d.ts +3 -0
  62. package/dist/src/services/config-file-manager.js +24 -7
  63. package/dist/src/services/distill-tuning.js +17 -10
  64. package/dist/src/services/memory-backup.d.ts +7 -0
  65. package/dist/src/services/memory-backup.js +29 -2
  66. package/dist/src/services/memory-distillation.js +2 -2
  67. package/dist/src/services/policy-ledger-anchor.d.ts +42 -0
  68. package/dist/src/services/policy-ledger-anchor.js +163 -0
  69. package/dist/src/services/policy-runtime.d.ts +11 -5
  70. package/dist/src/services/policy-runtime.js +70 -15
  71. package/dist/src/services/worker-daemon.d.ts +5 -2
  72. package/dist/src/services/worker-daemon.js +26 -6
  73. package/dist/src/services/worker-queue.js +11 -2
  74. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  75. package/node_modules/@claude-flow/mcp/dist/connection-pool.d.ts.map +1 -1
  76. package/node_modules/@claude-flow/mcp/dist/connection-pool.js +5 -2
  77. package/node_modules/@claude-flow/mcp/dist/connection-pool.js.map +1 -1
  78. package/node_modules/@claude-flow/mcp/dist/transport/http.d.ts +7 -1
  79. package/node_modules/@claude-flow/mcp/dist/transport/http.d.ts.map +1 -1
  80. package/node_modules/@claude-flow/mcp/dist/transport/http.js +60 -47
  81. package/node_modules/@claude-flow/mcp/dist/transport/http.js.map +1 -1
  82. package/node_modules/@claude-flow/mcp/package.json +1 -1
  83. package/node_modules/@claude-flow/plugin-agent-federation/dist/application/federation-coordinator.js +1 -1
  84. package/node_modules/@claude-flow/plugin-agent-federation/dist/application/federation-coordinator.js.map +1 -1
  85. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  86. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.d.ts +3 -0
  87. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.d.ts.map +1 -1
  88. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.js +4 -1
  89. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.js.map +1 -1
  90. package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.d.ts.map +1 -1
  91. package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.js +1 -0
  92. package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.js.map +1 -1
  93. package/node_modules/@claude-flow/plugin-agent-federation/package.json +1 -1
  94. package/node_modules/@claude-flow/security/dist/oauth/callback-server.d.ts +2 -1
  95. package/node_modules/@claude-flow/security/dist/oauth/callback-server.d.ts.map +1 -1
  96. package/node_modules/@claude-flow/security/dist/oauth/callback-server.js +48 -16
  97. package/node_modules/@claude-flow/security/dist/oauth/callback-server.js.map +1 -1
  98. package/node_modules/@claude-flow/security/dist/path-validator.d.ts +2 -2
  99. package/node_modules/@claude-flow/security/dist/path-validator.d.ts.map +1 -1
  100. package/node_modules/@claude-flow/security/dist/path-validator.js +15 -4
  101. package/node_modules/@claude-flow/security/dist/path-validator.js.map +1 -1
  102. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +11 -1
  103. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
  104. package/node_modules/@claude-flow/security/dist/policy/engine.js +19 -3
  105. package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
  106. package/node_modules/@claude-flow/security/dist/policy/types.d.ts +4 -2
  107. package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
  108. package/node_modules/@claude-flow/security/package.json +1 -1
  109. package/package.json +7 -7
  110. package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -1
  111. package/plugins/ruflo-metaharness/CHANGELOG.md +48 -0
  112. package/plugins/ruflo-metaharness/scripts/_invoke.mjs +80 -7
  113. package/plugins/ruflo-metaharness/scripts/smoke.sh +4 -4
  114. package/plugins/ruflo-metaharness/scripts/test-cli-resolution-3558.mjs +141 -0
  115. package/plugins/ruflo-metaharness/scripts/test-no-tool-time-npx.mjs +5 -0
  116. package/.claude/agents/tmp.json +0 -0
@@ -28,7 +28,8 @@
28
28
  // library entries (gepa) — never throws on absence
29
29
  // - makeDegradedEmitter() the ADR-150 rule-#3 exit-0 degraded payload
30
30
  // - runRufloCli() `memory store|list|retrieve` through the ruflo CLI
31
- // that ships this plugin (#3366) — replaces
31
+ // that ships this plugin (#3366), else an installed
32
+ // `ruflo` / `claude-flow` on PATH (#3558) — replaces
32
33
  // `npx @claude-flow/cli@latest` in 4 scripts
33
34
  //
34
35
  // WHAT DOES NOT LIVE HERE (the per-consumer parts):
@@ -40,11 +41,16 @@
40
41
  // stays meaningful on machines with a warm cache / local install):
41
42
  // - RUFLO_METAHARNESS_CACHE_BASE overrides ~/.ruflo as the cache root
42
43
  // - RUFLO_METAHARNESS_SKIP_LOCAL=1 disables local node_modules resolution
44
+ // - RUFLO_PLUGIN_SKIP_LOCAL_CLI=1 resolveRufloCli() skips the shipping CLI /
45
+ // repo checkout (#3558; same name in
46
+ // ruflo-cost-tracker and ruflo-adr)
47
+ // - RUFLO_PLUGIN_SKIP_PATH_CLI=1 resolveRufloCli() skips `ruflo` /
48
+ // `claude-flow` on PATH (#3558)
43
49
 
44
50
  import { spawnSync } from 'node:child_process';
45
- import { existsSync, mkdirSync, readFileSync, realpathSync } from 'node:fs';
51
+ import { accessSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
46
52
  import { homedir } from 'node:os';
47
- import { dirname, join } from 'node:path';
53
+ import { basename, delimiter, dirname, isAbsolute, join, relative, sep } from 'node:path';
48
54
  import { fileURLToPath, pathToFileURL } from 'node:url';
49
55
 
50
56
  const INSTALL_TIMEOUT_MS = 180_000; // npm install can be slow on cold cache
@@ -303,12 +309,77 @@ export function makeDegradedEmitter(pkg, pinVersion) {
303
309
  // exists next to bin/cli.js — mcp-launch.cjs resolveLocalCliBin()'s guard
304
310
  // against an unbuilt checkout — AND its package.json is named
305
311
  // @claude-flow/cli, a check added here so an unrelated directory two levels up
306
- // can never qualify. Unchanged:
312
+ // can never qualify.
313
+ // #3558: a Claude Code marketplace install copies the plugin to
314
+ // ~/.claude/plugins/cache/<marketplace>/ruflo-metaharness/<version>/, where
315
+ // neither candidate exists, so every memory call still went to npx @latest on
316
+ // a machine with ruflo installed. A `ruflo`, then `claude-flow`, found on
317
+ // PATH (ruflo-core ruflo-hook.cjs's order) is now tried before npx.
318
+ // Unchanged:
307
319
  // - 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`.
320
+ // - no usable local CLI and no ruflo on PATH still falls back to
321
+ // `npx @claude-flow/cli@latest`.
310
322
  const PLUGIN_SCRIPTS_DIR = dirname(fileURLToPath(import.meta.url));
311
323
 
324
+ /** Case-insensitive env lookup — Windows env keys are not case-stable (`Path`). */
325
+ function envValue(env, name) {
326
+ const key = Object.keys(env).find((k) => k.toLowerCase() === name.toLowerCase());
327
+ return key ? env[key] : undefined;
328
+ }
329
+
330
+ /**
331
+ * npm's Windows shim (`<prefix>/ruflo.cmd`, or `node_modules/.bin/ruflo.cmd`)
332
+ * cannot be spawned without a shell (CVE-2024-27980), so map it to the entry
333
+ * it wraps: the package's own `bin` field, which must resolve inside the
334
+ * package — ruflo-core's resolveNpmShim(). null when it is not such a shim.
335
+ */
336
+ function npmShimEntry(shim, name) {
337
+ try {
338
+ const shimDir = dirname(shim);
339
+ const pkgDir = basename(shimDir).toLowerCase() === '.bin'
340
+ ? join(shimDir, '..', name)
341
+ : join(shimDir, 'node_modules', name);
342
+ const pj = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf-8'));
343
+ const declared = typeof pj.bin === 'string' ? pj.bin : pj.bin?.[name];
344
+ if (typeof declared !== 'string') return null;
345
+ const entry = realpathSync(join(pkgDir, declared));
346
+ const rel = relative(realpathSync(pkgDir), entry);
347
+ if (rel === '..' || rel.startsWith('..' + sep) || isAbsolute(rel) || !statSync(entry).isFile()) return null;
348
+ return entry;
349
+ } catch { return null; }
350
+ }
351
+
352
+ /**
353
+ * `ruflo`, then `claude-flow`, on PATH → `{ command, args, shell, source }`,
354
+ * else null. fs-only like ruflo-core's resolveCommandPath(): no `which` /
355
+ * `where` shell per call. Relative PATH entries (including the empty one) are
356
+ * skipped so a `ruflo` file inside the project being worked on is never run.
357
+ * On win32 only an npm shim that maps to its JS entry counts; anything else
358
+ * is skipped rather than run through cmd.exe. `env` / `platform` are
359
+ * parameters so the win32 branch is testable on POSIX.
360
+ */
361
+ export function findRufloOnPath(env = process.env, platform = process.platform) {
362
+ const dirs = (envValue(env, 'PATH') || '')
363
+ .split(platform === 'win32' ? ';' : delimiter)
364
+ .filter((d) => isAbsolute(d));
365
+ for (const name of ['ruflo', 'claude-flow']) {
366
+ for (const dir of dirs) {
367
+ if (platform === 'win32') {
368
+ const shim = join(dir, `${name}.cmd`);
369
+ const entry = existsSync(shim) ? npmShimEntry(shim, name) : null;
370
+ if (entry) return { command: process.execPath, args: [entry], shell: false, source: 'path' };
371
+ continue;
372
+ }
373
+ const file = join(dir, name);
374
+ try {
375
+ accessSync(file, constants.X_OK);
376
+ if (statSync(file).isFile()) return { command: file, args: [], shell: false, source: 'path' };
377
+ } catch { /* keep searching */ }
378
+ }
379
+ }
380
+ return null;
381
+ }
382
+
312
383
  let RUFLO_CLI = null;
313
384
  /** `{ command, args, shell, source }` for invoking the ruflo CLI. Memoized. */
314
385
  export function resolveRufloCli() {
@@ -316,7 +387,7 @@ export function resolveRufloCli() {
316
387
  if (process.env.CLI_CORE === '1') {
317
388
  return (RUFLO_CLI = { command: 'npx', args: ['@claude-flow/cli-core@alpha'], shell: process.platform === 'win32', source: 'npx-cli-core' });
318
389
  }
319
- const candidates = [
390
+ const candidates = process.env.RUFLO_PLUGIN_SKIP_LOCAL_CLI === '1' ? [] : [
320
391
  join(PLUGIN_SCRIPTS_DIR, '..', '..', '..'), // published: <cli>/plugins/ruflo-metaharness/scripts
321
392
  join(PLUGIN_SCRIPTS_DIR, '..', '..', '..', 'v3', '@claude-flow', 'cli'), // repo checkout / marketplace clone
322
393
  ];
@@ -329,6 +400,8 @@ export function resolveRufloCli() {
329
400
  }
330
401
  } catch { /* not this layout — try the next */ }
331
402
  }
403
+ const onPath = process.env.RUFLO_PLUGIN_SKIP_PATH_CLI === '1' ? null : findRufloOnPath();
404
+ if (onPath) return (RUFLO_CLI = onPath);
332
405
  return (RUFLO_CLI = { command: 'npx', args: ['@claude-flow/cli@latest'], shell: process.platform === 'win32', source: 'npx' });
333
406
  }
334
407
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # Structural smoke test for ruflo-metaharness v0.2.3 (ADR-150 Phase 1).
2
+ # Structural smoke test for ruflo-metaharness v0.2.4 (ADR-150 Phase 1).
3
3
  set -u
4
4
  ROOT="$(cd "$(dirname "$0")/.." && pwd)"
5
5
  PASS=0
@@ -32,10 +32,10 @@ else
32
32
  bad "extraction-regex-rot: EXPECTED_TOOLS=$EXPECTED_TOOLS EXPECTED_SUBS=$EXPECTED_SUBS"
33
33
  fi
34
34
 
35
- step "1. plugin.json declares 0.2.3 with adr-150 keywords"
35
+ step "1. plugin.json declares 0.2.4 with adr-150 keywords"
36
36
  v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
37
- if [[ "$v" != "0.2.3" ]]; then
38
- bad "expected 0.2.3, got '$v'"
37
+ if [[ "$v" != "0.2.4" ]]; then
38
+ bad "expected 0.2.4, got '$v'"
39
39
  else
40
40
  miss=""
41
41
  for k in ruflo metaharness harness scorecard genome mcp-scan threat-model router adr-150 adr-148 adr-149 optional-dependency graceful-degradation subprocess phase-1-mvp; do
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ // test-cli-resolution-3558.mjs — regression test for #3558: in a Claude Code
3
+ // marketplace install, metaharness memory calls must use the ruflo CLI that
4
+ // is already installed on PATH, not `npx @claude-flow/cli@latest`.
5
+ //
6
+ // #3366 taught resolveRufloCli() (_invoke.mjs) two local layouts: the
7
+ // @claude-flow/cli that ships the plugin, and a repo checkout. A marketplace
8
+ // install is neither — the plugin is copied to
9
+ // ~/.claude/plugins/cache/<marketplace>/ruflo-metaharness/<version>/ — so it
10
+ // still fell through to npx @latest on a machine with ruflo installed.
11
+ //
12
+ // Hermetic: the plugin is copied into a temp plugin-cache layout; PATH holds
13
+ // only temp dirs with fake `ruflo` / `claude-flow` / `npx` scripts that log
14
+ // their argv. No network, no real CLI. The end-to-end cases use POSIX sh
15
+ // fakes and are skipped on win32; the win32 shim-mapping case runs everywhere.
16
+ //
17
+ // USAGE
18
+ // node --test plugins/ruflo-metaharness/scripts/test-cli-resolution-3558.mjs
19
+ import { test, before, after } from 'node:test';
20
+ import assert from 'node:assert/strict';
21
+ import { spawnSync } from 'node:child_process';
22
+ import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
23
+ import { tmpdir } from 'node:os';
24
+ import { delimiter, dirname, join } from 'node:path';
25
+ import { fileURLToPath, pathToFileURL } from 'node:url';
26
+
27
+ const PLUGIN_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
28
+ const posixOnly = { skip: process.platform === 'win32' ? 'fake CLIs are POSIX sh scripts' : false };
29
+
30
+ const write = (file, body, mode) => {
31
+ mkdirSync(dirname(file), { recursive: true });
32
+ writeFileSync(file, body);
33
+ if (mode) chmodSync(file, mode);
34
+ };
35
+
36
+ let TMP, CACHE, LOG, WITH_RUFLO, WITH_CLAUDE_FLOW, NPX_ONLY, PROJECT, baseEnv;
37
+ before(() => {
38
+ // realpath: macOS tmpdir() is /var/… while resolved paths come back as /private/var/…
39
+ TMP = realpathSync(mkdtempSync(join(tmpdir(), 'metaharness-cli-resolution-')));
40
+ const home = join(TMP, 'home');
41
+ CACHE = join(home, '.claude', 'plugins', 'cache', 'ruflo', 'ruflo-metaharness', '0.0.0-test');
42
+ cpSync(PLUGIN_ROOT, CACHE, { recursive: true });
43
+ LOG = join(TMP, 'calls.log');
44
+ const fake = (dir, name) => write(join(dir, name),
45
+ `#!/bin/sh\nprintf '%s %s\\n' "${name}" "$*" >> "${LOG}"\ncase "$*" in *"memory list"*) echo '[]' ;; *) echo '[OK] done' ;; esac\n`, 0o755);
46
+ WITH_RUFLO = join(TMP, 'bin-ruflo');
47
+ fake(WITH_RUFLO, 'ruflo');
48
+ fake(WITH_RUFLO, 'claude-flow');
49
+ WITH_CLAUDE_FLOW = join(TMP, 'bin-claude-flow');
50
+ fake(WITH_CLAUDE_FLOW, 'claude-flow');
51
+ NPX_ONLY = join(TMP, 'bin-npx');
52
+ fake(NPX_ONLY, 'npx');
53
+ PROJECT = join(TMP, 'project');
54
+ mkdirSync(PROJECT);
55
+ baseEnv = { ...process.env, HOME: home, USERPROFILE: home, RUFLO_METAHARNESS_CACHE_BASE: join(TMP, 'empty-cache-base') };
56
+ for (const k of ['CLI_CORE', 'RUFLO_PLUGIN_SKIP_LOCAL_CLI', 'RUFLO_PLUGIN_SKIP_PATH_CLI', 'NODE_OPTIONS']) delete baseEnv[k];
57
+ });
58
+ after(() => rmSync(TMP, { recursive: true, force: true }));
59
+
60
+ const calls = () => (existsSync(LOG) ? readFileSync(LOG, 'utf-8').trim().split('\n').filter(Boolean) : []);
61
+ /** resolveRufloCli() as seen from `scriptsDir`, in a fresh process. */
62
+ function resolved(env, scriptsDir = join(CACHE, 'scripts')) {
63
+ const src = `import { resolveRufloCli } from ${JSON.stringify(pathToFileURL(join(scriptsDir, '_invoke.mjs')).href)};\nconsole.log(JSON.stringify(resolveRufloCli()));`;
64
+ const r = spawnSync(process.execPath, ['--input-type=module', '-e', src], { cwd: PROJECT, encoding: 'utf-8', env: { ...baseEnv, ...env } });
65
+ assert.equal(r.status, 0, r.stderr);
66
+ return JSON.parse(r.stdout);
67
+ }
68
+ function auditList(env, scriptsDir = join(CACHE, 'scripts')) {
69
+ rmSync(LOG, { force: true });
70
+ const r = spawnSync(process.execPath, [join(scriptsDir, 'audit-list.mjs'), '--format', 'json'], { cwd: PROJECT, encoding: 'utf-8', timeout: 30_000, env: { ...baseEnv, ...env } });
71
+ assert.equal(r.status, 0, r.stderr);
72
+ return calls();
73
+ }
74
+ const pathOf = (...dirs) => dirs.join(delimiter);
75
+
76
+ test('win32: npm\'s ruflo.cmd shim maps to `node <package bin>`, never cmd.exe', async () => {
77
+ const { findRufloOnPath } = await import(pathToFileURL(join(PLUGIN_ROOT, 'scripts', '_invoke.mjs')).href);
78
+ const prefix = join(TMP, 'win-prefix');
79
+ write(join(prefix, 'ruflo.cmd'), '@ECHO off\r\n');
80
+ write(join(prefix, 'node_modules', 'ruflo', 'package.json'), JSON.stringify({ name: 'ruflo', bin: { ruflo: 'bin/ruflo.js' } }));
81
+ write(join(prefix, 'node_modules', 'ruflo', 'bin', 'ruflo.js'), '');
82
+ assert.deepEqual(findRufloOnPath({ Path: prefix }, 'win32'),
83
+ { command: process.execPath, args: [realpathSync(join(prefix, 'node_modules', 'ruflo', 'bin', 'ruflo.js'))], shell: false, source: 'path' });
84
+ const bare = join(TMP, 'win-bare');
85
+ write(join(bare, 'ruflo.cmd'), '@ECHO off\r\n');
86
+ assert.equal(findRufloOnPath({ PATH: bare }, 'win32'), null, 'a ruflo.cmd that is not an npm package shim is skipped, not run through cmd.exe');
87
+ });
88
+
89
+ test('plugin-cache install: the installed `ruflo` on PATH serves memory calls, not npx', posixOnly, () => {
90
+ const env = { PATH: pathOf(WITH_RUFLO, NPX_ONLY) };
91
+ assert.deepEqual(resolved(env), { command: join(WITH_RUFLO, 'ruflo'), args: [], shell: false, source: 'path' });
92
+ const c = auditList(env);
93
+ assert.ok(c.some((l) => l.startsWith('ruflo memory list --namespace metaharness-audit')), c.join(' | ') || 'no CLI called');
94
+ assert.deepEqual(c.filter((l) => l.startsWith('npx ')), []);
95
+ });
96
+
97
+ test('no `ruflo` but `claude-flow` on PATH → claude-flow', posixOnly, () => {
98
+ const c = auditList({ PATH: pathOf(WITH_CLAUDE_FLOW, NPX_ONLY) });
99
+ assert.ok(c.some((l) => l.startsWith('claude-flow memory list')), c.join(' | '));
100
+ assert.deepEqual(c.filter((l) => l.startsWith('npx ')), []);
101
+ });
102
+
103
+ test('no installed CLI → still `npx @claude-flow/cli@latest` (unchanged fallback)', posixOnly, () => {
104
+ const env = { PATH: pathOf(NPX_ONLY) };
105
+ assert.equal(resolved(env).source, 'npx');
106
+ const c = auditList(env);
107
+ assert.ok(c.some((l) => l.startsWith('npx @claude-flow/cli@latest memory list')), c.join(' | '));
108
+ });
109
+
110
+ test('CLI_CORE=1 still opts into `npx @claude-flow/cli-core@alpha` (ADR-100)', posixOnly, () => {
111
+ const c = auditList({ PATH: pathOf(WITH_RUFLO, NPX_ONLY), CLI_CORE: '1' });
112
+ assert.ok(c.some((l) => l.startsWith('npx @claude-flow/cli-core@alpha memory list')), c.join(' | '));
113
+ assert.deepEqual(c.filter((l) => l.startsWith('ruflo ')), []);
114
+ });
115
+
116
+ test('RUFLO_PLUGIN_SKIP_PATH_CLI=1 skips the PATH lookup (test seam)', posixOnly, () => {
117
+ assert.equal(resolved({ PATH: pathOf(WITH_RUFLO, NPX_ONLY), RUFLO_PLUGIN_SKIP_PATH_CLI: '1' }).source, 'npx');
118
+ });
119
+
120
+ test('a `ruflo` in the working directory is never run via a relative PATH entry', posixOnly, () => {
121
+ write(join(PROJECT, 'ruflo'), '#!/bin/sh\nexit 0\n', 0o755);
122
+ try {
123
+ assert.equal(resolved({ PATH: ['', '.', NPX_ONLY].join(delimiter) }).source, 'npx');
124
+ } finally {
125
+ rmSync(join(PROJECT, 'ruflo'));
126
+ }
127
+ });
128
+
129
+ test('published layout: the @claude-flow/cli that ships the plugin still wins over PATH (#3366)', posixOnly, () => {
130
+ const cli = join(TMP, 'published', 'node_modules', '@claude-flow', 'cli');
131
+ const scripts = join(cli, 'plugins', 'ruflo-metaharness', 'scripts');
132
+ cpSync(join(PLUGIN_ROOT, 'scripts'), scripts, { recursive: true });
133
+ write(join(cli, 'package.json'), JSON.stringify({ name: '@claude-flow/cli', version: '0.0.0-test', type: 'module' }));
134
+ write(join(cli, 'dist', 'src', 'index.js'), 'export {};\n');
135
+ write(join(cli, 'bin', 'cli.js'), `import { appendFileSync } from 'node:fs';\nappendFileSync(${JSON.stringify(LOG)}, 'shipping-cli ' + process.argv.slice(2).join(' ') + '\\n');\nconsole.log('[]');\n`);
136
+ const env = { PATH: pathOf(WITH_RUFLO, NPX_ONLY) };
137
+ assert.equal(resolved(env, scripts).source, 'local');
138
+ const c = auditList(env, scripts);
139
+ assert.ok(c.some((l) => l.startsWith('shipping-cli memory list')), c.join(' | '));
140
+ assert.deepEqual(c.filter((l) => !l.startsWith('shipping-cli ')), []);
141
+ });
@@ -156,6 +156,11 @@ else process.exit(2);
156
156
  const baseEnv = { ...process.env, PATH: `${TRAP}:${process.env.PATH}`, RUFLO_METAHARNESS_CACHE_BASE: join(TMP, 'empty-cache-base') };
157
157
  delete baseEnv.RUFLO_METAHARNESS_SKIP_LOCAL;
158
158
  delete baseEnv.CLI_CORE;
159
+ delete baseEnv.RUFLO_PLUGIN_SKIP_LOCAL_CLI;
160
+ // #3558: Phase 4 asserts the npx fallback; an installed `ruflo` on the
161
+ // developer's PATH would now be preferred over it. PATH resolution has its
162
+ // own test (test-cli-resolution-3558.mjs).
163
+ baseEnv.RUFLO_PLUGIN_SKIP_PATH_CLI = '1';
159
164
  const trapped = () => (existsSync(TRAP_LOG) ? readFileSync(TRAP_LOG, 'utf-8').trim() : '');
160
165
  const resetTrap = () => rmSync(TRAP_LOG, { force: true });
161
166
  const run = (script, args, env = baseEnv, scriptsDir = PLUGIN_SCRIPTS, cwd = PROJECT) => {
File without changes