@claude-flow/cli 3.17.0 → 3.18.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 (53) hide show
  1. package/dist/src/commands/neural.d.ts.map +1 -1
  2. package/dist/src/commands/neural.js +16 -6
  3. package/dist/src/commands/neural.js.map +1 -1
  4. package/dist/src/mcp-tools/agentdb-tools.d.ts.map +1 -1
  5. package/dist/src/mcp-tools/agentdb-tools.js +52 -1
  6. package/dist/src/mcp-tools/agentdb-tools.js.map +1 -1
  7. package/dist/src/mcp-tools/browser-session-tools.d.ts +5 -1
  8. package/dist/src/mcp-tools/browser-session-tools.d.ts.map +1 -1
  9. package/dist/src/mcp-tools/browser-session-tools.js +92 -18
  10. package/dist/src/mcp-tools/browser-session-tools.js.map +1 -1
  11. package/dist/src/mcp-tools/hooks-tools.d.ts +1 -0
  12. package/dist/src/mcp-tools/hooks-tools.d.ts.map +1 -1
  13. package/dist/src/mcp-tools/hooks-tools.js +131 -0
  14. package/dist/src/mcp-tools/hooks-tools.js.map +1 -1
  15. package/dist/src/mcp-tools/neural-tools.d.ts.map +1 -1
  16. package/dist/src/mcp-tools/neural-tools.js +119 -84
  17. package/dist/src/mcp-tools/neural-tools.js.map +1 -1
  18. package/dist/src/memory/intelligence.d.ts.map +1 -1
  19. package/dist/src/memory/intelligence.js +20 -5
  20. package/dist/src/memory/intelligence.js.map +1 -1
  21. package/dist/src/memory/memory-bridge.d.ts +15 -2
  22. package/dist/src/memory/memory-bridge.d.ts.map +1 -1
  23. package/dist/src/memory/memory-bridge.js +64 -10
  24. package/dist/src/memory/memory-bridge.js.map +1 -1
  25. package/dist/src/ruvector/lora-adapter.d.ts +9 -0
  26. package/dist/src/ruvector/lora-adapter.d.ts.map +1 -1
  27. package/dist/src/ruvector/lora-adapter.js +21 -3
  28. package/dist/src/ruvector/lora-adapter.js.map +1 -1
  29. package/dist/src/ruvector/output-verifier.d.ts +83 -0
  30. package/dist/src/ruvector/output-verifier.d.ts.map +1 -0
  31. package/dist/src/ruvector/output-verifier.js +277 -0
  32. package/dist/src/ruvector/output-verifier.js.map +1 -0
  33. package/dist/src/ruvector/trajectory-tree.d.ts +113 -0
  34. package/dist/src/ruvector/trajectory-tree.d.ts.map +1 -0
  35. package/dist/src/ruvector/trajectory-tree.js +237 -0
  36. package/dist/src/ruvector/trajectory-tree.js.map +1 -0
  37. package/dist/tsconfig.tsbuildinfo +1 -1
  38. package/package.json +1 -1
  39. package/plugins/ruflo-metaharness/agents/metaharness-architect.md +1 -1
  40. package/plugins/ruflo-metaharness/commands/ruflo-metaharness.md +5 -3
  41. package/plugins/ruflo-metaharness/scripts/_darwin.mjs +47 -39
  42. package/plugins/ruflo-metaharness/scripts/_harness.mjs +173 -104
  43. package/plugins/ruflo-metaharness/scripts/_invoke.mjs +231 -0
  44. package/plugins/ruflo-metaharness/scripts/_redblue.mjs +36 -75
  45. package/plugins/ruflo-metaharness/scripts/evolve.mjs +163 -3
  46. package/plugins/ruflo-metaharness/scripts/gepa.mjs +7 -53
  47. package/plugins/ruflo-metaharness/scripts/smoke.sh +98 -47
  48. package/plugins/ruflo-metaharness/scripts/test-graceful-degradation.mjs +16 -0
  49. package/plugins/ruflo-metaharness/skills/harness-evolve/SKILL.md +41 -2
  50. package/plugins/ruflo-metaharness/skills/harness-learn/SKILL.md +4 -2
  51. package/plugins/ruflo-metaharness/skills/harness-mcp-scan/SKILL.md +3 -1
  52. package/plugins/ruflo-metaharness/skills/harness-score/SKILL.md +1 -1
  53. package/plugins/ruflo-metaharness/skills/harness-threat-model/SKILL.md +3 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.17.0",
3
+ "version": "3.18.1",
4
4
  "type": "module",
5
5
  "description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",
@@ -38,7 +38,7 @@ needs its own ADR.
38
38
 
39
39
  ## Tools (subprocess-only, no library imports)
40
40
 
41
- - All skills shell out to `npx metaharness ...` or `npx -p metaharness@latest harness ...` via the `_harness.mjs` shared helper.
41
+ - All skills shell out to the pinned `metaharness` / `harness` binaries (`metaharness@~0.3.0`, local install or one-time versioned cache never `@latest`) via the `_harness.mjs` shared helper.
42
42
  - 60s hard timeout per subprocess.
43
43
  - Output captured + parsed; `--json` flag forced unless the script opts out.
44
44
  - No `@metaharness/*` import statement appears outside the optional-router path in `v3/@claude-flow/cli/src/ruvector/neural-router.ts`.
@@ -3,9 +3,11 @@ name: ruflo-metaharness
3
3
  description: MetaHarness integration — score, genome, mint, mcp-scan, threat-model — via subprocess invocations honoring ADR-150 architectural constraint
4
4
  ---
5
5
 
6
- MetaHarness integration commands. All shell out to `npx metaharness ...`
7
- or `npx -p metaharness@latest harness ...` via the `_harness.mjs`
8
- shared helper; no library imports on ruflo's boot path.
6
+ MetaHarness integration commands. All shell out to the PINNED
7
+ `metaharness`/`harness` binaries (`metaharness@~0.3.0` resolved from a local
8
+ install or a one-time `~/.ruflo/metaharness-cache-<pin>` install, never
9
+ `@latest`) via the `_harness.mjs` shared helper; no library imports on
10
+ ruflo's boot path.
9
11
 
10
12
  **`harness score [--path .] [--alert-on-fit-below N] [--format table|json]`** -- 5-dimension readiness scorecard (harnessFit / compileConfidence / taskCoverage / toolSafety / memoryUsefulness + estCostPerRunUsd + scaffoldReady).
11
13
  1. Run `node plugins/ruflo-metaharness/scripts/score.mjs --path <dir>`
@@ -4,6 +4,11 @@
4
4
  // but targets the separate darwin binary (`metaharness-darwin`) which is
5
5
  // published as its own npm package (`@metaharness/darwin@~0.8.0`).
6
6
  //
7
+ // Shared plumbing (degraded classification, --json injection, trailing-JSON
8
+ // parse, degraded emitter) lives in `_invoke.mjs`. What stays here is the
9
+ // genuinely-darwin part: the ASYNC STREAMING variant (`runDarwinAsync`) that
10
+ // long `evolve` runs need for per-generation progress visibility.
11
+ //
7
12
  // Three subcommands surfaced (matches darwin 0.8.x — same verbs as 0.3.x,
8
13
  // with GEPA-engine evolve flags added upstream: --selection modes,
9
14
  // --crossover, --epistasis, --curriculum, --mutator ruvllm, --sandbox):
@@ -28,15 +33,24 @@
28
33
  // smoke shapes only.
29
34
 
30
35
  import { spawnSync, spawn } from 'node:child_process';
36
+ import {
37
+ classifyDegraded,
38
+ importOptionalLibrary,
39
+ injectJson,
40
+ makeDegradedEmitter,
41
+ parseTrailingJson,
42
+ } from './_invoke.mjs';
31
43
 
32
44
  const DEFAULT_TIMEOUT_MS = 60_000;
33
45
 
34
46
  // Pinned semver range. Bump in lock-step with optionalDependencies in
35
47
  // @claude-flow/cli/package.json + ruflo/package.json. The `~` allows
36
48
  // patch upgrades without re-pinning.
37
- const DARWIN_PIN = '@metaharness/darwin@~0.8.0';
49
+ const DARWIN_PKG = '@metaharness/darwin';
50
+ const DARWIN_PIN_VERSION = '~0.8.0';
51
+ const DARWIN_PIN = `${DARWIN_PKG}@${DARWIN_PIN_VERSION}`;
38
52
 
39
- const DEGRADED_RX = /could not determine executable|404|not installed|MODULE_NOT_FOUND|ENOTFOUND|getaddrinfo|ECONNREFUSED|ETIMEDOUT/i;
53
+ const REASON_PREFIX = 'metaharness-darwin';
40
54
 
41
55
  function buildArgv(args, wantJson) {
42
56
  // `metaharness-darwin` historically does NOT take --json on every
@@ -45,30 +59,14 @@ function buildArgv(args, wantJson) {
45
59
  // explicitly request it, but never silently inject it on subcommands
46
60
  // that don't accept it. The shape that does support --json is the
47
61
  // single-verb `evolve` and `bench verify`.
48
- if (!wantJson || args.includes('--json')) return args;
49
- return [...args, '--json'];
50
- }
51
-
52
- function classifyDegraded(stderr, exitCode) {
53
- if (exitCode === null) return { degraded: true, reason: 'metaharness-darwin-timeout' };
54
- if (DEGRADED_RX.test(stderr)) return { degraded: true, reason: 'metaharness-darwin-not-available' };
55
- return { degraded: false };
62
+ return injectJson(args, wantJson);
56
63
  }
57
64
 
58
- function maybeParseJson(stdout) {
59
- // darwin's evolve emits a final JSON object at end of stdout when --json
60
- // is passed; everything before is human-readable progress. Grab the LAST
61
- // {...} block, not the first the first may be a per-generation log line.
62
- const matches = [...stdout.matchAll(/\{[\s\S]*?\}/g)];
63
- if (matches.length === 0) return null;
64
- // Try last match first; fall back to greedy single-block match.
65
- for (let i = matches.length - 1; i >= 0; i--) {
66
- try { return JSON.parse(matches[i][0]); } catch { /* try previous */ }
67
- }
68
- const greedy = /\{[\s\S]*\}/.exec(stdout);
69
- if (greedy) { try { return JSON.parse(greedy[0]); } catch { return null; } }
70
- return null;
71
- }
65
+ // darwin's evolve emits a final JSON object at end of stdout when --json
66
+ // is passed; everything before is human-readable progress. Grab the LAST
67
+ // {...} block, not the first the first may be a per-generation log line.
68
+ // (This last-block parse is now the family-wide shared implementation.)
69
+ const maybeParseJson = parseTrailingJson;
72
70
 
73
71
  /**
74
72
  * Sync invocation. Use for short subcommands (`bench verify`, smoke shapes).
@@ -90,7 +88,7 @@ export function runDarwin(args, opts = {}) {
90
88
  const durationMs = Date.now() - start;
91
89
  const stdout = r.stdout || '';
92
90
  const stderr = r.stderr || '';
93
- const classified = classifyDegraded(stderr, r.status);
91
+ const classified = classifyDegraded(stderr, r.status, REASON_PREFIX);
94
92
  if (classified.degraded) {
95
93
  return {
96
94
  stdout, stderr,
@@ -142,7 +140,9 @@ export function runDarwinAsync(args, opts = {}) {
142
140
  for (const line of lines) opts.onProgress(line);
143
141
  }
144
142
  });
143
+ let timedOut = false;
145
144
  const timer = setTimeout(() => {
145
+ timedOut = true;
146
146
  try { p.kill('SIGTERM'); } catch { /* ignore */ }
147
147
  }, timeoutMs);
148
148
  if (opts.signal) {
@@ -156,13 +156,13 @@ export function runDarwinAsync(args, opts = {}) {
156
156
  stdout,
157
157
  stderr: stderr + String(e?.message ?? e),
158
158
  exitCode: 127, json: null, durationMs: Date.now() - start,
159
- degraded: true, reason: 'metaharness-darwin-not-available',
159
+ degraded: true, reason: `${REASON_PREFIX}-not-available`,
160
160
  });
161
161
  });
162
162
  p.on('close', (code) => {
163
163
  clearTimeout(timer);
164
164
  const durationMs = Date.now() - start;
165
- const classified = classifyDegraded(stderr, code);
165
+ const classified = classifyDegraded(stderr, timedOut ? null : code, REASON_PREFIX);
166
166
  if (classified.degraded) {
167
167
  resolve({
168
168
  stdout, stderr,
@@ -185,18 +185,26 @@ export function runDarwinAsync(args, opts = {}) {
185
185
  /**
186
186
  * Match the existing `emitDegradedJsonAndExit` shape used by the
187
187
  * metaharness-umbrella scripts so MCP tool consumers see one contract.
188
+ * Exit 0 — ADR-150 architectural constraint: ruflo continues to function
189
+ * when MetaHarness is absent. Same posture as the umbrella scripts.
188
190
  */
189
- export function emitDarwinDegradedJsonAndExit(reason) {
190
- const payload = {
191
- degraded: true,
192
- reason,
193
- hint: 'Install with `npm i -D ' + DARWIN_PIN + '` or run `npx -y ' + DARWIN_PIN + ' --help` to verify network access.',
194
- generatedAt: new Date().toISOString(),
195
- };
196
- console.log(JSON.stringify(payload, null, 2));
197
- // Exit 0 — ADR-150 architectural constraint: ruflo continues to function
198
- // when MetaHarness is absent. Same posture as the umbrella scripts.
199
- process.exit(0);
200
- }
191
+ export const emitDarwinDegradedJsonAndExit = makeDegradedEmitter(DARWIN_PKG, DARWIN_PIN_VERSION);
201
192
 
202
193
  export const DARWIN_VERSION_PIN = DARWIN_PIN;
194
+
195
+ /**
196
+ * Resolver for the darwin GEPA LIBRARY entry (`@metaharness/darwin/gepa`).
197
+ * Lives here (not in gepa.mjs) because gepa.mjs is a CLI script that runs
198
+ * main() on import — this module is the import-safe home for the darwin
199
+ * pin. Used by gepa.mjs (genome/validate/render/analyze ops) and by
200
+ * evolve.mjs --diagnose (failure-class analysis of run transcripts).
201
+ * Returns the module namespace or null. Never throws on absence.
202
+ */
203
+ export function importGepa() {
204
+ return importOptionalLibrary({
205
+ specifier: '@metaharness/darwin/gepa',
206
+ pkg: DARWIN_PKG,
207
+ pinVersion: DARWIN_PIN_VERSION,
208
+ entryRelPath: 'dist/gepa/index.js',
209
+ });
210
+ }
@@ -7,41 +7,147 @@
7
7
  // vetted bridge that every skill imports from.
8
8
  //
9
9
  // CONTRACT
10
- // - `runMetaharness(args, opts)` — invoke `npx metaharness <args>`
11
- // - `runHarness(args, opts)` — invoke `npx -p metaharness harness <args>`
12
- // - both return `{ stdout, stderr, exitCode, json|null, durationMs }`
10
+ // - `runMetaharness(args, opts)` — invoke the `metaharness` binary
11
+ // - `runHarness(args, opts)` — invoke the sibling `harness` binary
12
+ // - both return `{ stdout, stderr, exitCode, json|null, durationMs, degraded, reason? }`
13
13
  // - `--json` flag is appended automatically when `opts.json !== false`
14
14
  // - subprocess hard timeout (default 60s) — captured in opts.timeoutMs
15
- // - on MODULE_NOT_FOUND or "not installed", returns degraded result with
16
- // `degraded: true, reason: 'metaharness-not-available'` — never throws
17
- // (ADR-150 graceful-degradation rule #3)
15
+ // - when the package cannot be resolved/installed, returns degraded result
16
+ // with `degraded: true, reason: 'metaharness-not-available'` — never
17
+ // throws (ADR-150 graceful-degradation rule #3). A subprocess killed by
18
+ // the timeout reports `reason: 'metaharness-timeout'` instead (matches
19
+ // the sibling _darwin/_redblue helpers).
20
+ //
21
+ // SECURITY + PERF (supersedes the iter-27 npx-with-@latest-dist-tag path)
22
+ // =========================================================================
23
+ // The pre-consolidation implementation shelled to `npx -y` with the @latest
24
+ // dist-tag. Two problems, both fixed here:
25
+ // 1. SECURITY (HIGH): @latest means a compromised upstream publish executes
26
+ // arbitrary code on user machines on the very next skill invocation.
27
+ // The version is now PINNED to METAHARNESS_PIN_VERSION (tilde range —
28
+ // patch updates only) and only bumped deliberately, in lock-step with
29
+ // optionalDependencies in @claude-flow/cli + ruflo package.json.
30
+ // 2. PERF: @latest forced an npm-registry metadata check on EVERY call.
31
+ // Resolution is now (a) an already-installed local metaharness
32
+ // satisfying the pin (walk-up node_modules — free), then (b) a ONE-TIME
33
+ // `npm install --prefix ~/.ruflo/metaharness-cache-<pin>` versioned
34
+ // cache (same proven pattern as _redblue.mjs / gepa.mjs), after which
35
+ // every call is a plain `node <abs-path-to-cli>` spawn — zero network.
36
+ //
37
+ // BOTH BINARIES: the `metaharness` package ships two bins (`metaharness`
38
+ // and `harness`); their entry paths are read from the resolved package.json
39
+ // bin map rather than hardcoded, so upstream layout changes inside the
40
+ // pinned range cannot silently break us.
18
41
 
19
42
  import { spawnSync, spawn } from 'node:child_process';
43
+ import { existsSync, readFileSync } from 'node:fs';
44
+ import { join } from 'node:path';
45
+ import {
46
+ classifyDegraded,
47
+ ensureCachedInstall,
48
+ findLocalPackageDir,
49
+ injectJson,
50
+ makeDegradedEmitter,
51
+ parseTrailingJson,
52
+ } from './_invoke.mjs';
20
53
 
21
54
  const DEFAULT_TIMEOUT_MS = 60_000;
22
55
 
56
+ // Pinned semver range. Bump in lock-step with optionalDependencies in
57
+ // @claude-flow/cli/package.json + ruflo/package.json. NEVER @latest.
58
+ const METAHARNESS_PKG = 'metaharness';
59
+ const METAHARNESS_PIN_VERSION = '~0.3.0';
60
+
61
+ const REASON_NOT_AVAILABLE = 'metaharness-not-available';
62
+
63
+ /**
64
+ * Resolve absolute paths for the two bins shipped by the metaharness
65
+ * package. Memoized per process — the whole point is that after the first
66
+ * resolution (or one-time cache install) every call is local-only.
67
+ */
68
+ let RESOLVED = null;
69
+ function resolveMetaharnessBins() {
70
+ if (RESOLVED) return RESOLVED;
71
+ // (a) already-installed local copy satisfying the pin — free.
72
+ const localDir = findLocalPackageDir(METAHARNESS_PKG, METAHARNESS_PIN_VERSION);
73
+ if (localDir) {
74
+ const bins = readBinMap(localDir);
75
+ if (bins) return (RESOLVED = { ok: true, bins, source: 'local' });
76
+ }
77
+ // (b) one-time versioned cache install of the pinned range.
78
+ const cached = ensureCachedInstall({ pkg: METAHARNESS_PKG, pinVersion: METAHARNESS_PIN_VERSION });
79
+ if (!cached.ok) {
80
+ return (RESOLVED = {
81
+ ok: false,
82
+ reason: REASON_NOT_AVAILABLE,
83
+ stderr: cached.stderr ?? cached.error ?? '',
84
+ stdout: cached.stdout ?? '',
85
+ });
86
+ }
87
+ const bins = readBinMap(cached.pkgDir);
88
+ if (!bins) {
89
+ return (RESOLVED = {
90
+ ok: false,
91
+ reason: REASON_NOT_AVAILABLE,
92
+ stderr: `metaharness package at ${cached.pkgDir} is missing the expected bin map (metaharness + harness)`,
93
+ stdout: '',
94
+ });
95
+ }
96
+ return (RESOLVED = { ok: true, bins, source: 'cache' });
97
+ }
98
+
99
+ /** Read both bin entry points from the package's bin map. */
100
+ function readBinMap(pkgDir) {
101
+ try {
102
+ const pj = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf-8'));
103
+ const bin = pj.bin || {};
104
+ if (!bin.metaharness || !bin.harness) return null;
105
+ const metaharness = join(pkgDir, bin.metaharness);
106
+ const harness = join(pkgDir, bin.harness);
107
+ if (!existsSync(metaharness) || !existsSync(harness)) return null;
108
+ return { metaharness, harness };
109
+ } catch {
110
+ return null;
111
+ }
112
+ }
113
+
114
+ function degradedResult(resolved, start) {
115
+ return {
116
+ stdout: resolved.stdout ?? '',
117
+ stderr: resolved.stderr ?? '',
118
+ exitCode: 127,
119
+ json: null,
120
+ durationMs: Date.now() - start,
121
+ degraded: true,
122
+ reason: resolved.reason ?? REASON_NOT_AVAILABLE,
123
+ };
124
+ }
125
+
23
126
  /**
24
- * iter 56 — async variant of execCli. Used by oia-audit.mjs to parallelize
25
- * its 5 subprocess calls, dropping worst-case wall-clock from 5×TIMEOUT
26
- * (sequential) to 1×TIMEOUT (parallel). Identical return shape to the
27
- * sync execCli so callers can swap without ceremony.
127
+ * Async variant (iter 56)used by oia-audit.mjs to parallelize its 5
128
+ * subprocess calls, dropping worst-case wall-clock from 5×TIMEOUT to
129
+ * 1×TIMEOUT. Identical return shape to the sync path.
28
130
  */
29
- function execCliAsync(npxArgs, opts = {}) {
131
+ function execBinAsync(binName, args, opts = {}) {
132
+ const start = Date.now();
133
+ const resolved = resolveMetaharnessBins();
134
+ if (!resolved.ok) return Promise.resolve(degradedResult(resolved, start));
30
135
  return new Promise((resolve) => {
31
- const start = Date.now();
32
136
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
33
137
  const wantJson = opts.json !== false;
34
- const argv = wantJson && !npxArgs.includes('--json') ? [...npxArgs, '--json'] : [...npxArgs];
35
- const p = spawn('npx', argv, {
138
+ const argv = injectJson(args, wantJson);
139
+ const p = spawn('node', [resolved.bins[binName], ...argv], {
36
140
  stdio: ['ignore', 'pipe', 'pipe'],
37
141
  cwd: opts.cwd,
38
142
  env: { ...process.env, ...(opts.env || {}) },
39
- shell: process.platform === 'win32',
143
+ shell: false,
40
144
  });
41
145
  let stdout = '', stderr = '';
42
146
  p.stdout?.on('data', (d) => { stdout += d.toString(); });
43
147
  p.stderr?.on('data', (d) => { stderr += d.toString(); });
148
+ let timedOut = false;
44
149
  const timer = setTimeout(() => {
150
+ timedOut = true;
45
151
  try { p.kill('SIGTERM'); } catch { /* ignore */ }
46
152
  }, timeoutMs);
47
153
  p.on('error', (e) => {
@@ -49,123 +155,87 @@ function execCliAsync(npxArgs, opts = {}) {
49
155
  resolve({
50
156
  stdout, stderr: stderr + String(e?.message ?? e),
51
157
  exitCode: 127, json: null, durationMs: Date.now() - start,
52
- degraded: true, reason: 'metaharness-not-available',
158
+ degraded: true, reason: REASON_NOT_AVAILABLE,
53
159
  });
54
160
  });
55
161
  p.on('close', (code) => {
56
162
  clearTimeout(timer);
57
163
  const durationMs = Date.now() - start;
58
- if (code === null || /could not determine executable|404|not installed|MODULE_NOT_FOUND|ENOTFOUND|getaddrinfo|ECONNREFUSED|ETIMEDOUT/i.test(stderr)) {
164
+ const classified = classifyDegraded(stderr, timedOut ? null : code, 'metaharness');
165
+ if (classified.degraded) {
59
166
  resolve({
60
167
  stdout, stderr,
61
168
  exitCode: code ?? 127, json: null, durationMs,
62
- degraded: true, reason: 'metaharness-not-available',
169
+ degraded: true, reason: classified.reason,
63
170
  });
64
171
  return;
65
172
  }
66
- let json = null;
67
- if (wantJson) {
68
- const m = /\{[\s\S]*\}/.exec(stdout);
69
- if (m) { try { json = JSON.parse(m[0]); } catch { /* leave null */ } }
70
- }
71
- resolve({ stdout, stderr, exitCode: code ?? 0, json, durationMs, degraded: false });
173
+ resolve({
174
+ stdout, stderr,
175
+ exitCode: code ?? 0,
176
+ json: wantJson ? parseTrailingJson(stdout) : null,
177
+ durationMs,
178
+ degraded: false,
179
+ });
72
180
  });
73
181
  });
74
182
  }
75
183
 
76
- export function runMetaharnessAsync(args, opts) {
77
- return execCliAsync(['-y', 'metaharness@latest', ...args], opts);
78
- }
79
-
80
- export function runHarnessAsync(args, opts) {
81
- return execCliAsync(['-y', '-p', 'metaharness@latest', 'harness', ...args], opts);
82
- }
83
-
84
- // ITER 27 — npx invocation hardening.
85
- // The pre-iter-27 implementation passed `'-y metaharness@latest'` as a
86
- // SINGLE argv element to npx (`spawnSync('npx', [bin, ...argv])` where
87
- // bin contained two whitespace-separated tokens). spawnSync with
88
- // shell:false does no word-splitting, so npx received a literal string
89
- // with an embedded space and either failed silently or treated the
90
- // whole thing as a package name. The graceful-degradation path then
91
- // reported `degraded: true` for every skill — masking the bug. Every
92
- // argv token must now be its own array element.
93
- function execCli(npxArgs, opts = {}) {
184
+ /** Sync invocation of one of the two resolved bins via `node <abs-path>`. */
185
+ function execBin(binName, args, opts = {}) {
94
186
  const start = Date.now();
187
+ const resolved = resolveMetaharnessBins();
188
+ if (!resolved.ok) return degradedResult(resolved, start);
95
189
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
96
190
  const wantJson = opts.json !== false;
97
- const argv = wantJson && !npxArgs.includes('--json') ? [...npxArgs, '--json'] : [...npxArgs];
98
- const r = spawnSync('npx', argv, {
191
+ const argv = injectJson(args, wantJson);
192
+ const r = spawnSync('node', [resolved.bins[binName], ...argv], {
99
193
  stdio: ['ignore', 'pipe', 'pipe'],
100
194
  encoding: 'utf-8',
101
195
  timeout: timeoutMs,
102
196
  cwd: opts.cwd, // iter 27 — let callers redirect $CWD (mint.mjs needs this)
103
197
  env: { ...process.env, ...(opts.env || {}) },
104
- shell: process.platform === 'win32',
198
+ shell: false,
105
199
  });
106
200
  const durationMs = Date.now() - start;
107
201
  const stdout = r.stdout || '';
108
202
  const stderr = r.stderr || '';
109
- // Graceful degradation npx couldn't find the binary.
110
- if (r.status === null || /could not determine executable|404|not installed|MODULE_NOT_FOUND|ENOTFOUND|getaddrinfo|ECONNREFUSED|ETIMEDOUT/i.test(stderr)) {
203
+ const classified = classifyDegraded(stderr, r.status, 'metaharness');
204
+ if (classified.degraded) {
111
205
  return {
112
206
  stdout, stderr,
113
207
  exitCode: r.status ?? 127,
114
208
  json: null,
115
209
  durationMs,
116
210
  degraded: true,
117
- reason: 'metaharness-not-available',
211
+ reason: classified.reason,
118
212
  };
119
213
  }
120
- let json = null;
121
- if (wantJson) {
122
- const m = /\{[\s\S]*\}/.exec(stdout);
123
- if (m) { try { json = JSON.parse(m[0]); } catch { /* leave null */ } }
124
- }
125
- return { stdout, stderr, exitCode: r.status ?? 0, json, durationMs, degraded: false };
214
+ return {
215
+ stdout, stderr,
216
+ exitCode: r.status ?? 0,
217
+ json: wantJson ? parseTrailingJson(stdout) : null,
218
+ durationMs,
219
+ degraded: false,
220
+ };
126
221
  }
127
222
 
128
223
  export function runMetaharness(args, opts) {
129
- // iter 27 — explicit argv tokens (was: '-y metaharness@latest' as one
130
- // string, which silently degraded every skill).
131
- return execCli(['-y', 'metaharness@latest', ...args], opts);
224
+ return execBin('metaharness', args, opts);
132
225
  }
133
226
 
134
227
  export function runHarness(args, opts) {
135
- // The `harness` binary ships inside the `metaharness` package, so we
136
- // need `npx -p metaharness@latest harness <args>`. spawnSync receives
137
- // a single argv array, so encode the `-p` flag as its own argument.
138
- const start = Date.now();
139
- const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
140
- const wantJson = opts?.json !== false;
141
- const argv = wantJson && !args.includes('--json') ? [...args, '--json'] : [...args];
142
- const r = spawnSync('npx', ['-y', '-p', 'metaharness@latest', 'harness', ...argv], {
143
- stdio: ['ignore', 'pipe', 'pipe'],
144
- encoding: 'utf-8',
145
- timeout: timeoutMs,
146
- cwd: opts?.cwd, // iter 27 — same cwd-redirect support as runMetaharness
147
- env: { ...process.env, ...(opts?.env || {}) },
148
- shell: process.platform === 'win32',
149
- });
150
- const durationMs = Date.now() - start;
151
- const stdout = r.stdout || '';
152
- const stderr = r.stderr || '';
153
- if (r.status === null || /could not determine executable|404|not installed|MODULE_NOT_FOUND|ENOTFOUND|getaddrinfo|ECONNREFUSED|ETIMEDOUT/i.test(stderr)) {
154
- return {
155
- stdout, stderr,
156
- exitCode: r.status ?? 127,
157
- json: null,
158
- durationMs,
159
- degraded: true,
160
- reason: 'metaharness-not-available',
161
- };
162
- }
163
- let json = null;
164
- if (wantJson) {
165
- const m = /\{[\s\S]*\}/.exec(stdout);
166
- if (m) { try { json = JSON.parse(m[0]); } catch { /* leave null */ } }
167
- }
168
- return { stdout, stderr, exitCode: r.status ?? 0, json, durationMs, degraded: false };
228
+ // The `harness` binary ships inside the same `metaharness` package
229
+ // resolved from the package.json bin map above.
230
+ return execBin('harness', args, opts);
231
+ }
232
+
233
+ export function runMetaharnessAsync(args, opts) {
234
+ return execBinAsync('metaharness', args, opts);
235
+ }
236
+
237
+ export function runHarnessAsync(args, opts) {
238
+ return execBinAsync('harness', args, opts);
169
239
  }
170
240
 
171
241
  /**
@@ -244,18 +314,17 @@ export function parseMcpScanText(stdout) {
244
314
  return { findings, summary };
245
315
  }
246
316
 
247
- // Convenience emitters for skill scripts — keep the boilerplate out of
317
+ // Convenience emitter for skill scripts — keep the boilerplate out of
248
318
  // each skill so they focus on argument parsing + exit-code semantics.
249
- export function emitDegradedJsonAndExit(reason) {
250
- const payload = {
251
- degraded: true,
252
- reason,
253
- hint: 'Install metaharness manually with `npm i -D metaharness` or run `npx metaharness@latest --version` to verify network access.',
254
- generatedAt: new Date().toISOString(),
255
- };
256
- console.log(JSON.stringify(payload, null, 2));
257
- // Exit 0 — ADR-150 architectural constraint says ruflo continues to
258
- // function when MetaHarness is absent. Skills emit a structured
259
- // degraded payload rather than failing.
260
- process.exit(0);
319
+ // Exit 0 — ADR-150 architectural constraint says ruflo continues to
320
+ // function when MetaHarness is absent. Skills emit a structured
321
+ // degraded payload rather than failing.
322
+ export const emitDegradedJsonAndExit = makeDegradedEmitter(METAHARNESS_PKG, METAHARNESS_PIN_VERSION);
323
+
324
+ export const METAHARNESS_VERSION_PIN = `${METAHARNESS_PKG}@${METAHARNESS_PIN_VERSION}`;
325
+
326
+ /** Expose resolution for diagnostics/tests (which path served the bins). */
327
+ export function metaharnessResolution() {
328
+ const r = resolveMetaharnessBins();
329
+ return r.ok ? { ok: true, source: r.source, bins: r.bins } : { ok: false, reason: r.reason };
261
330
  }