@claude-flow/cli 3.12.0 → 3.12.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 (39) hide show
  1. package/package.json +3 -2
  2. package/plugins/ruflo-metaharness/.claude-flow/data/pending-insights.jsonl +1 -0
  3. package/plugins/ruflo-metaharness/.claude-flow/neural/stats.json +6 -0
  4. package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +32 -0
  5. package/plugins/ruflo-metaharness/README.md +64 -0
  6. package/plugins/ruflo-metaharness/agents/metaharness-architect.md +58 -0
  7. package/plugins/ruflo-metaharness/commands/ruflo-metaharness.md +46 -0
  8. package/plugins/ruflo-metaharness/scripts/_harness.mjs +261 -0
  9. package/plugins/ruflo-metaharness/scripts/_similarity.mjs +161 -0
  10. package/plugins/ruflo-metaharness/scripts/_spike-similarity.mjs +223 -0
  11. package/plugins/ruflo-metaharness/scripts/audit-list.mjs +158 -0
  12. package/plugins/ruflo-metaharness/scripts/audit-trend.mjs +272 -0
  13. package/plugins/ruflo-metaharness/scripts/bench-parse-mcp-scan.mjs +146 -0
  14. package/plugins/ruflo-metaharness/scripts/bench-recordpair-overhead.mjs +186 -0
  15. package/plugins/ruflo-metaharness/scripts/bench-similarity.mjs +177 -0
  16. package/plugins/ruflo-metaharness/scripts/drift-from-history.mjs +363 -0
  17. package/plugins/ruflo-metaharness/scripts/genome.mjs +80 -0
  18. package/plugins/ruflo-metaharness/scripts/mcp-scan.mjs +111 -0
  19. package/plugins/ruflo-metaharness/scripts/mint.mjs +119 -0
  20. package/plugins/ruflo-metaharness/scripts/oia-audit.mjs +228 -0
  21. package/plugins/ruflo-metaharness/scripts/router-parallel-analyze.mjs +250 -0
  22. package/plugins/ruflo-metaharness/scripts/score.mjs +92 -0
  23. package/plugins/ruflo-metaharness/scripts/similarity.mjs +158 -0
  24. package/plugins/ruflo-metaharness/scripts/smoke.sh +2268 -0
  25. package/plugins/ruflo-metaharness/scripts/test-graceful-degradation.mjs +141 -0
  26. package/plugins/ruflo-metaharness/scripts/test-mcp-tools.mjs +437 -0
  27. package/plugins/ruflo-metaharness/scripts/test-parallel-pipeline.mjs +204 -0
  28. package/plugins/ruflo-metaharness/scripts/test-pipeline-roundtrip.mjs +586 -0
  29. package/plugins/ruflo-metaharness/scripts/test-similarity.mjs +334 -0
  30. package/plugins/ruflo-metaharness/scripts/test-with-openrouter.mjs +229 -0
  31. package/plugins/ruflo-metaharness/scripts/threat-model.mjs +59 -0
  32. package/plugins/ruflo-metaharness/skills/harness-drift-from-history/SKILL.md +65 -0
  33. package/plugins/ruflo-metaharness/skills/harness-genome/SKILL.md +54 -0
  34. package/plugins/ruflo-metaharness/skills/harness-mcp-scan/SKILL.md +47 -0
  35. package/plugins/ruflo-metaharness/skills/harness-mint/SKILL.md +72 -0
  36. package/plugins/ruflo-metaharness/skills/harness-oia-audit/SKILL.md +79 -0
  37. package/plugins/ruflo-metaharness/skills/harness-score/SKILL.md +66 -0
  38. package/plugins/ruflo-metaharness/skills/harness-similarity/SKILL.md +67 -0
  39. package/plugins/ruflo-metaharness/skills/harness-threat-model/SKILL.md +39 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.12.0",
3
+ "version": "3.12.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",
@@ -78,6 +78,7 @@
78
78
  "bin",
79
79
  "scripts",
80
80
  ".claude",
81
+ "plugins",
81
82
  "README.md"
82
83
  ],
83
84
  "scripts": {
@@ -86,7 +87,7 @@
86
87
  "test:plugin-store": "npx tsx src/plugins/tests/standalone-test.ts",
87
88
  "test:pattern-store": "npx tsx src/transfer/store/tests/standalone-test.ts",
88
89
  "postinstall": "node ./scripts/postinstall.cjs",
89
- "prepublishOnly": "cp ../../../README.md ./README.md",
90
+ "prepublishOnly": "cp ../../../README.md ./README.md && rm -rf plugins && mkdir -p plugins && cp -r ../../../plugins/ruflo-metaharness plugins/",
90
91
  "release": "npm version prerelease --preid=alpha && npm run publish:all",
91
92
  "publish:all": "./scripts/publish.sh"
92
93
  },
@@ -0,0 +1 @@
1
+ {"type":"edit","file":"/Users/cohen/Projects/ruflo/plugins/ruflo-metaharness/scripts/test-mcp-tools.mjs","timestamp":1781649922735}
@@ -0,0 +1,6 @@
1
+ {
2
+ "trajectoriesRecorded": 22,
3
+ "patternsLearned": 22,
4
+ "signalsProcessed": 0,
5
+ "lastAdaptation": 1781650872839
6
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "ruflo-metaharness",
3
+ "description": "MetaHarness integration for ruflo — surfaces score/genome/mint/mcp-scan/threat-model via skills; pairs with @metaharness/router (ADR-148/149) for cost-optimal model routing; honors ADR-150's architectural constraint that MetaHarness remains an optional augmentation, never a required runtime dependency",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "ruvnet",
7
+ "url": "https://github.com/ruvnet"
8
+ },
9
+ "homepage": "https://github.com/ruvnet/ruflo",
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "ruflo",
13
+ "metaharness",
14
+ "harness",
15
+ "scaffolding",
16
+ "scorecard",
17
+ "genome",
18
+ "mcp-scan",
19
+ "threat-model",
20
+ "router",
21
+ "adr-150",
22
+ "adr-148",
23
+ "adr-149",
24
+ "optional-dependency",
25
+ "graceful-degradation",
26
+ "subprocess",
27
+ "namespace-routing",
28
+ "verified",
29
+ "ci",
30
+ "phase-1-mvp"
31
+ ]
32
+ }
@@ -0,0 +1,64 @@
1
+ # ruflo-metaharness
2
+
3
+ MetaHarness integration plugin for ruflo. Surfaces the upstream `metaharness` / `harness` CLIs through five ruflo skills, honoring [ADR-150](../../v3/docs/adr/ADR-150-metaharness-integration-surfaces.md)'s architectural constraint that MetaHarness must remain a removable augmentation — never a required runtime dependency.
4
+
5
+ ## ADR-150 architectural constraint (load-bearing)
6
+
7
+ **Ruflo remains operational if every MetaHarness package is removed.** Every code path in this plugin satisfies four rules:
8
+
9
+ 1. **Removable** — no static `import '@metaharness/*'` outside the optional-router path in `v3/@claude-flow/cli/src/ruvector/neural-router.ts`.
10
+ 2. **Optional in package.json** — `metaharness` is in `optionalDependencies`, never `dependencies`.
11
+ 3. **Graceful degradation** — every script catches `MODULE_NOT_FOUND`/network failure and emits `{ degraded: true, reason: 'metaharness-not-available' }` JSON, exits 0. The graceful path is the default behavior, not a special case.
12
+ 4. **CI gate** — `no-metaharness-smoke.yml` runs the plugin smoke with `npm install --no-optional` and asserts the contract still passes.
13
+
14
+ ## Skills
15
+
16
+ | Skill | Usage | Description |
17
+ |-------|-------|-------------|
18
+ | `harness-score` | `/harness-score [--path .] [--alert-on-fit-below 70]` | 5-dim readiness scorecard (harnessFit/compile/coverage/safety/memory + cost) |
19
+ | `harness-genome` | `/harness-genome [--path .] [--alert-on-risk-above 0.5]` | 7-section categorical report (repo_type/topology/risk/mcp/test/publish) |
20
+ | `harness-mcp-scan` | `/harness-mcp-scan [--path .] [--fail-on high]` | Static MCP security findings — pure-read, no dispatch |
21
+ | `harness-threat-model` | `/harness-threat-model [--path .] [--fail-on high]` | Enterprise-grade threat model (clean/low/medium/high + findings) |
22
+ | `harness-mint` | `/harness-mint --name <id> --template <id> [--confirm]` | Scaffold a custom harness; DRY-RUN by default; refuses project-root writes |
23
+
24
+ ## Phase-0 baseline (ruflo itself, 2026-06-16)
25
+
26
+ ```json
27
+ {
28
+ "harnessFit": 82,
29
+ "compileConfidence": 100,
30
+ "taskCoverage": 79,
31
+ "toolSafety": 100,
32
+ "memoryUsefulness": 40,
33
+ "estCostPerRunUsd": 0.048,
34
+ "recommendedMode": "CLI + MCP",
35
+ "archetype": "typescript-sdk-harness",
36
+ "template": "vertical:coding",
37
+ "scaffoldReady": true,
38
+ "risk_score": 0.27,
39
+ "publish_readiness": 0.9
40
+ }
41
+ ```
42
+
43
+ ## Architecture
44
+
45
+ All skills use subprocess invocation through the `_harness.mjs` shared helper:
46
+
47
+ ```
48
+ skills/X/SKILL.md → scripts/X.mjs → scripts/_harness.mjs → spawnSync('npx', ['metaharness', …])
49
+ ↘ on MODULE_NOT_FOUND → emit degraded JSON, exit 0
50
+ ```
51
+
52
+ This means:
53
+ - No library import overhead on ruflo's boot path
54
+ - 60s hard timeout per subprocess (bounded blast radius)
55
+ - `--json` flag forced for structured parsing
56
+ - Graceful degradation is a single helper used by every skill
57
+
58
+ ## Cross-links
59
+
60
+ - [ADR-150](../../v3/docs/adr/ADR-150-metaharness-integration-surfaces.md) — decision + architectural constraint
61
+ - [Issue #2399](https://github.com/ruvnet/ruflo/issues/2399) — phase rollout tracker
62
+ - [Research dossier](https://gist.github.com/ruvnet/19d166ff9acf368c9da4172d91ac9113) — full graded-evidence sourcing
63
+ - [Upstream](https://github.com/ruvnet/agent-harness-generator) — `metaharness` source
64
+ - ADR-148/149 — `@metaharness/router` cost-optimal routing (sibling integration)
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: metaharness-architect
3
+ description: MetaHarness integration architect for ruflo. Surfaces score/genome/mint/mcp-scan/threat-model upstream capabilities via skills; enforces ADR-150 architectural constraint (MetaHarness as removable augmentation, never required runtime dep); coordinates Phase 1 MVP rollout
4
+ model: haiku
5
+ ---
6
+
7
+ You are the metaharness-architect for ruflo. Your job is to expose the
8
+ `@metaharness/*` ecosystem's capabilities through ruflo's UX while
9
+ keeping ruflo independently operational at all times.
10
+
11
+ ## ADR-150 invariants (load-bearing)
12
+
13
+ 1. **Removable** — `npm ls --without-deps @metaharness/*` must still
14
+ produce a working CLI.
15
+ 2. **Optional in package.json** — every `@metaharness/*` package goes in
16
+ `optionalDependencies` or `peerDependencies` (optional), NEVER
17
+ `dependencies`.
18
+ 3. **Graceful degradation** — every code path that imports a
19
+ `@metaharness/*` symbol catches `MODULE_NOT_FOUND` and falls back.
20
+ The `emitDegradedJsonAndExit()` helper in `scripts/_harness.mjs` is
21
+ the reference implementation.
22
+ 4. **CI gate** — at least one CI job runs ruflo with no MetaHarness
23
+ packages installed and asserts smoke still passes.
24
+
25
+ If a PR breaks any of these four rules, it is a breaking change and
26
+ needs its own ADR.
27
+
28
+ ## Skills (6 — what each does, when to invoke)
29
+
30
+ | Skill | Role | Invoke when |
31
+ |---|---|---|
32
+ | `harness-score` | 5-dim numeric scorecard | Pre-mint readiness check; CI regression gate |
33
+ | `harness-genome` | 7-section categorical report | Pre-mint architecture review; drift detection over time |
34
+ | `harness-mcp-scan` | Static MCP security findings | Every PR; enterprise security review |
35
+ | `harness-threat-model` | Categorized threat report | Pre-launch review; periodic OIA-audit cadence |
36
+ | `harness-oia-audit` | Composite weekly audit worker (iter 7) | Cron-scheduled; bundles oia+threat+mcp into one timestamped record in `metaharness-audit` namespace |
37
+ | `harness-mint` | Scaffold a custom harness | User wants to fork; ALWAYS dry-run first, never write to project root |
38
+
39
+ ## Tools (subprocess-only, no library imports)
40
+
41
+ - All skills shell out to `npx metaharness ...` or `npx -p metaharness@latest harness ...` via the `_harness.mjs` shared helper.
42
+ - 60s hard timeout per subprocess.
43
+ - Output captured + parsed; `--json` flag forced unless the script opts out.
44
+ - No `@metaharness/*` import statement appears outside the optional-router path in `v3/@claude-flow/cli/src/ruvector/neural-router.ts`.
45
+
46
+ ## Phase tracker
47
+
48
+ - ✅ Phase 0 — measurement spike (ruflo's own scorecard captured 2026-06-16: harnessFit 82, risk_score 0.27, publish_readiness 0.9)
49
+ - 🔄 Phase 1 — MVP plugin (this commit + CI gates + KRR retraining)
50
+ - ⏳ Phase 2 — Expansion (eject command, SelfEvolvingRouter parallel-log, harness registry, oia-audit worker)
51
+ - ⏳ Phase 3 — Harness Intelligence Layer (each item gets its own ADR)
52
+
53
+ ## Cross-links
54
+
55
+ - [ADR-150](../../../v3/docs/adr/ADR-150-metaharness-integration-surfaces.md) — decision
56
+ - [Issue #2399](https://github.com/ruvnet/ruflo/issues/2399) — phase tracker
57
+ - [Research dossier](https://gist.github.com/ruvnet/19d166ff9acf368c9da4172d91ac9113) — graded evidence
58
+ - [Upstream](https://github.com/ruvnet/agent-harness-generator) — `metaharness` source
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: ruflo-metaharness
3
+ description: MetaHarness integration — score, genome, mint, mcp-scan, threat-model — via subprocess invocations honoring ADR-150 architectural constraint
4
+ ---
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.
9
+
10
+ **`harness score [--path .] [--alert-on-fit-below N] [--format table|json]`** -- 5-dimension readiness scorecard (harnessFit / compileConfidence / taskCoverage / toolSafety / memoryUsefulness + estCostPerRunUsd + scaffoldReady).
11
+ 1. Run `node plugins/ruflo-metaharness/scripts/score.mjs --path <dir>`
12
+ 2. Output JSON (default) with all dimensions + recommended template + archetype
13
+ 3. `--alert-on-fit-below N` exits 1 when harnessFit < N — CI regression gate
14
+ 4. Subprocess + 60s hard timeout; graceful degradation when metaharness unavailable
15
+
16
+ **`harness genome [--path .] [--alert-on-risk-above 0.5] [--format table|json]`** -- 7-section repo readiness report (repo_type / agent_topology / risk_score / mcp_surface / test_confidence / publish_readiness).
17
+ 1. Run `node plugins/ruflo-metaharness/scripts/genome.mjs --path <dir>`
18
+ 2. Pairs with harness-score for full readiness view — score is numeric, genome is categorical
19
+ 3. `--alert-on-risk-above N` exits 1 when risk_score > N
20
+ 4. Useful for drift detection: snapshot genome over time, diff to spot agent_topology drift
21
+
22
+ **`harness mcp-scan [--path .] [--fail-on low|medium|high] [--format table|json]`** -- Static security scan of `.mcp/servers.json` + `.harness/claims.json`. Reads only; no dispatch.
23
+ 1. Run `node plugins/ruflo-metaharness/scripts/mcp-scan.mjs --path <dir>`
24
+ 2. Severity-ranked findings (low/medium/high); default fail-on `high`
25
+ 3. CI integration: `mcp-scan --fail-on high` fails the build on any HIGH finding
26
+ 4. Pairs with harness-threat-model for enterprise-review-grade categorization
27
+
28
+ **`harness threat-model [--path .] [--fail-on clean|low|medium|high] [--format table|json]`** -- Enterprise-review threat model. Returns `worst` severity + categorized `findings[]`.
29
+ 1. Run `node plugins/ruflo-metaharness/scripts/threat-model.mjs --path <dir>`
30
+ 2. Default fail-on `high`; tightenable to `medium` for stricter gates
31
+ 3. Output suitable for sharing with security/infosec team
32
+ 4. Will be auto-fired by the Phase-2 oia-audit background worker on a schedule
33
+
34
+ **`harness oia-audit [--path .] [--dry-run] [--alert-on-worst clean|low|medium|high] [--format table|json]`** -- Phase-2 composite worker (ADR-150). Bundles oia-manifest + threat-model + mcp-scan into one timestamped audit record, stores in `metaharness-audit` memory namespace.
35
+ 1. Run `node plugins/ruflo-metaharness/scripts/oia-audit.mjs`
36
+ 2. Composite worst-severity = max(threatModel.worst, mcpScan.findings.severity)
37
+ 3. `--alert-on-worst high` exits 1 when composite ≥ high — CI weekly drift gate
38
+ 4. `--dry-run` skips memory persistence — useful for local checks
39
+ 5. Designed for cron schedule: weekly snapshot enables audit drift tracking via memory diff
40
+
41
+ **`harness mint --name <id> --template <vertical:coding|minimal|…> [--host claude-code|codex|…] [--target /abs/path] [--confirm] [--format table|json]`** -- Scaffold a custom AI agent harness. DRY-RUN by default; --confirm required to write.
42
+ 1. Run `node plugins/ruflo-metaharness/scripts/mint.mjs --name <id> --template <id> --host <id>`
43
+ 2. **Safety**: refuses to write to the calling repo root or any path inside it; defaults to `/tmp/ruflo-mint-<ts>-<name>/`
44
+ 3. Without `--confirm`: prints dry-run plan, exits 0 without touching disk
45
+ 4. With `--confirm`: shells `npx metaharness new ... --yes`
46
+ 5. Templates: minimal + 19 verticals (coding, devops, support, legal, …). Hosts: claude-code, codex, pi-dev, opencode, github-actions, +4 more.
@@ -0,0 +1,261 @@
1
+ // _harness.mjs — shared invocation helper for the metaharness/harness CLIs.
2
+ //
3
+ // All ruflo-metaharness skills shell out to the upstream CLI rather than
4
+ // linking the library — this honors ADR-150's architectural constraint
5
+ // (MetaHarness must remain a removable augmentation, never a required
6
+ // runtime dep) while still giving us "deep integration" through a single
7
+ // vetted bridge that every skill imports from.
8
+ //
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 }`
13
+ // - `--json` flag is appended automatically when `opts.json !== false`
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)
18
+
19
+ import { spawnSync, spawn } from 'node:child_process';
20
+
21
+ const DEFAULT_TIMEOUT_MS = 60_000;
22
+
23
+ /**
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.
28
+ */
29
+ function execCliAsync(npxArgs, opts = {}) {
30
+ return new Promise((resolve) => {
31
+ const start = Date.now();
32
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
33
+ const wantJson = opts.json !== false;
34
+ const argv = wantJson && !npxArgs.includes('--json') ? [...npxArgs, '--json'] : [...npxArgs];
35
+ const p = spawn('npx', argv, {
36
+ stdio: ['ignore', 'pipe', 'pipe'],
37
+ cwd: opts.cwd,
38
+ env: { ...process.env, ...(opts.env || {}) },
39
+ shell: process.platform === 'win32',
40
+ });
41
+ let stdout = '', stderr = '';
42
+ p.stdout?.on('data', (d) => { stdout += d.toString(); });
43
+ p.stderr?.on('data', (d) => { stderr += d.toString(); });
44
+ const timer = setTimeout(() => {
45
+ try { p.kill('SIGTERM'); } catch { /* ignore */ }
46
+ }, timeoutMs);
47
+ p.on('error', (e) => {
48
+ clearTimeout(timer);
49
+ resolve({
50
+ stdout, stderr: stderr + String(e?.message ?? e),
51
+ exitCode: 127, json: null, durationMs: Date.now() - start,
52
+ degraded: true, reason: 'metaharness-not-available',
53
+ });
54
+ });
55
+ p.on('close', (code) => {
56
+ clearTimeout(timer);
57
+ 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)) {
59
+ resolve({
60
+ stdout, stderr,
61
+ exitCode: code ?? 127, json: null, durationMs,
62
+ degraded: true, reason: 'metaharness-not-available',
63
+ });
64
+ return;
65
+ }
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 });
72
+ });
73
+ });
74
+ }
75
+
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 = {}) {
94
+ const start = Date.now();
95
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
96
+ const wantJson = opts.json !== false;
97
+ const argv = wantJson && !npxArgs.includes('--json') ? [...npxArgs, '--json'] : [...npxArgs];
98
+ const r = spawnSync('npx', argv, {
99
+ stdio: ['ignore', 'pipe', 'pipe'],
100
+ encoding: 'utf-8',
101
+ timeout: timeoutMs,
102
+ cwd: opts.cwd, // iter 27 — let callers redirect $CWD (mint.mjs needs this)
103
+ env: { ...process.env, ...(opts.env || {}) },
104
+ shell: process.platform === 'win32',
105
+ });
106
+ const durationMs = Date.now() - start;
107
+ const stdout = r.stdout || '';
108
+ 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)) {
111
+ return {
112
+ stdout, stderr,
113
+ exitCode: r.status ?? 127,
114
+ json: null,
115
+ durationMs,
116
+ degraded: true,
117
+ reason: 'metaharness-not-available',
118
+ };
119
+ }
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 };
126
+ }
127
+
128
+ 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);
132
+ }
133
+
134
+ 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 };
169
+ }
170
+
171
+ /**
172
+ * iter 63 — single source of truth for severity ranks across the
173
+ * metaharness plugin family. Pre-iter-63, three scripts (oia-audit,
174
+ * audit-trend, mcp-scan) maintained their own SEVERITY_RANK literal,
175
+ * each missing different keys, each producing different NaN-compare
176
+ * behavior on unknown severities. Iter 62 fixed oia-audit; iter 63
177
+ * propagates the fix and consolidates.
178
+ *
179
+ * Mapping rationale:
180
+ * clean / info → 0 (no harm)
181
+ * low → 1
182
+ * medium / warn → 2
183
+ * high / error → 3
184
+ * critical → 4 (explicit elevation above high)
185
+ *
186
+ * `rankSeverity(s)` is the safe accessor — returns 0 for any unknown
187
+ * string instead of `undefined`, eliminating the NaN-compare hazard
188
+ * (`undefined > 3` evaluates to false → unknown severities silently
189
+ * ignored in reduce expressions).
190
+ */
191
+ export const SEVERITY_RANK = Object.freeze({
192
+ clean: 0, info: 0,
193
+ low: 1,
194
+ medium: 2, warn: 2,
195
+ high: 3, error: 3,
196
+ critical: 4,
197
+ });
198
+
199
+ export function rankSeverity(s) {
200
+ if (s == null) return 0;
201
+ return SEVERITY_RANK[String(s).toLowerCase()] ?? 0;
202
+ }
203
+
204
+ /**
205
+ * iter 50 — parse `harness mcp-scan` text output into structured findings.
206
+ *
207
+ * Upstream `harness mcp-scan` emits plain text even with --json:
208
+ *
209
+ * harness mcp-scan — <path>
210
+ *
211
+ * [INFO] No MCP security issues found
212
+ * Policy is default-deny with safe capability grants and an audit log.
213
+ *
214
+ * Result: INFO (1 finding, 0 high)
215
+ *
216
+ * Closes the iter-49-flagged gap where audit-trend.mjs reads
217
+ * `json.findings` expecting an array, but mcp-scan's r.json was null.
218
+ * Used by BOTH mcp-scan.mjs (the wrapper) and oia-audit.mjs (composite
219
+ * audit) so the structured-findings invariant holds across the pipeline.
220
+ */
221
+ export function parseMcpScanText(stdout) {
222
+ const findings = [];
223
+ const lines = (stdout || '').split('\n');
224
+ let current = null;
225
+ for (const line of lines) {
226
+ const m = /^\s*\[([A-Z]+)\]\s+(.+?)\s*$/.exec(line);
227
+ if (m) {
228
+ if (current) findings.push(current);
229
+ current = { severity: m[1].toLowerCase(), message: m[2] };
230
+ } else if (current && /^\s{6,}\S/.test(line)) {
231
+ const cont = line.trim();
232
+ if (cont) current.message += ' ' + cont;
233
+ } else if (current && line.trim() === '') {
234
+ findings.push(current);
235
+ current = null;
236
+ }
237
+ }
238
+ if (current) findings.push(current);
239
+ const resultMatch = /Result:\s+([A-Z]+)\s+\((\d+)\s+finding/i.exec(stdout);
240
+ const summary = resultMatch ? {
241
+ overallSeverity: resultMatch[1].toLowerCase(),
242
+ totalCount: parseInt(resultMatch[2], 10),
243
+ } : null;
244
+ return { findings, summary };
245
+ }
246
+
247
+ // Convenience emitters for skill scripts — keep the boilerplate out of
248
+ // 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);
261
+ }
@@ -0,0 +1,161 @@
1
+ // _similarity.mjs — ADR-152 production similarity module.
2
+ //
3
+ // Graduated from `_spike-similarity.mjs` (iter 35) after both invariants
4
+ // passed on the LEGAL/SUPPORT/DEVOPS fixtures. The spike file STAYS as
5
+ // the regression-suite anchor; this file is what production callers
6
+ // (`similarity.mjs` skill, `metaharness_similarity` MCP tool) import.
7
+ //
8
+ // ADR-150 ARCHITECTURAL CONSTRAINTS PRESERVED
9
+ // Removable ✓ pure-TS — no `@metaharness/*` import path
10
+ // Optional ✓ no new dep on `@metaharness/*`
11
+ // Graceful ✓ malformed input → low-confidence output, never throws
12
+ // CI-gate ✓ standalone unit-importable; no `npx` needed
13
+ //
14
+ // CONTRACT (PUBLIC)
15
+ // projectToVec(input) → 9-dim numerical feature vector
16
+ // cosine(a, b) → [0,1]
17
+ // categoricalAgreement(a, b) → [0,1] over 4 enum fields
18
+ // jaccard(a, b) → [0,1] over agent_topology[]
19
+ // similarity(a, b, opts?) → { overall, components, perDimension? }
20
+ //
21
+ // WEIGHT DEFAULTS (from ADR-152 §Decision)
22
+ // overall = 0.60·cosine + 0.25·categorical + 0.15·jaccard
23
+ //
24
+ // ADR-152 reserves a future per-org weight override; for §3.1 the defaults
25
+ // are global. The opts.weights hook is here so consumers can experiment
26
+ // without forking the module — it is NOT a stable public API.
27
+
28
+ const DEFAULT_WEIGHTS = Object.freeze({ cosine: 0.6, categorical: 0.25, jaccard: 0.15 });
29
+
30
+ const CATEGORICAL_FIELDS = Object.freeze(['repo_type', 'archetype', 'template', 'recommendedMode']);
31
+
32
+ // ─────────────────────────────────────────────────────────────────────
33
+ // 9-dim feature vector. The mapping mirrors ADR-152 §Decision Table 1.
34
+ // Missing fields default to 0 — that's the graceful-degradation path.
35
+ // ─────────────────────────────────────────────────────────────────────
36
+
37
+ export function projectToVec(input) {
38
+ const s = input?.score ?? {};
39
+ const g = input?.genome ?? {};
40
+ return [
41
+ (s.harnessFit ?? 0) / 100,
42
+ (s.compileConfidence ?? 0) / 100,
43
+ (s.taskCoverage ?? 0) / 100,
44
+ (s.toolSafety ?? 0) / 100,
45
+ (s.memoryUsefulness ?? 0) / 100,
46
+ g.risk_score ?? 0,
47
+ g.test_confidence ?? 0,
48
+ g.publish_readiness ?? 0,
49
+ Math.max(0, Math.min(1, Math.log10((s.estCostPerRunUsd ?? 0) + 0.001) / Math.log10(10))),
50
+ ];
51
+ }
52
+
53
+ export function cosine(a, b) {
54
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return 0;
55
+ let dot = 0, na = 0, nb = 0;
56
+ for (let i = 0; i < a.length; i++) {
57
+ dot += a[i] * b[i];
58
+ na += a[i] * a[i];
59
+ nb += b[i] * b[i];
60
+ }
61
+ const denom = Math.sqrt(na) * Math.sqrt(nb);
62
+ if (denom === 0) return 0;
63
+ return Math.max(0, Math.min(1, dot / denom));
64
+ }
65
+
66
+ export function categoricalAgreement(a, b) {
67
+ let matches = 0;
68
+ for (const f of CATEGORICAL_FIELDS) {
69
+ const av = a?.genome?.[f] ?? a?.score?.[f];
70
+ const bv = b?.genome?.[f] ?? b?.score?.[f];
71
+ if (av && bv && av === bv) matches++;
72
+ }
73
+ return matches / CATEGORICAL_FIELDS.length;
74
+ }
75
+
76
+ export function jaccard(a, b) {
77
+ const A = new Set(a?.genome?.agent_topology ?? []);
78
+ const B = new Set(b?.genome?.agent_topology ?? []);
79
+ if (A.size === 0 && B.size === 0) return 1;
80
+ const intersection = [...A].filter((x) => B.has(x)).length;
81
+ const union = new Set([...A, ...B]).size;
82
+ return union === 0 ? 0 : intersection / union;
83
+ }
84
+
85
+ // ─────────────────────────────────────────────────────────────────────
86
+ // Composite similarity with optional per-dimension breakdown.
87
+ // Returns the ADR-152 §"return shape": overall + components + per-dim.
88
+ // ─────────────────────────────────────────────────────────────────────
89
+
90
+ export function similarity(a, b, opts = {}) {
91
+ const weights = { ...DEFAULT_WEIGHTS, ...(opts.weights ?? {}) };
92
+ const va = projectToVec(a);
93
+ const vb = projectToVec(b);
94
+ const cos = cosine(va, vb);
95
+ const cat = categoricalAgreement(a, b);
96
+ const jac = jaccard(a, b);
97
+
98
+ const overall = weights.cosine * cos + weights.categorical * cat + weights.jaccard * jac;
99
+
100
+ const result = {
101
+ overall: round4(overall),
102
+ components: {
103
+ cosine: round4(cos),
104
+ categorical: round4(cat),
105
+ jaccard: round4(jac),
106
+ },
107
+ weights,
108
+ };
109
+
110
+ if (opts.perDimension) {
111
+ result.perDimension = perDimensionBreakdown(a, b, va, vb, weights);
112
+ }
113
+ return result;
114
+ }
115
+
116
+ function round4(x) {
117
+ return Math.round(x * 10000) / 10000;
118
+ }
119
+
120
+ function perDimensionBreakdown(a, b, va, vb, weights) {
121
+ // Per-dimension contribution = squared-error-normalized cosine slice + categorical/jaccard direct.
122
+ // We surface raw a/b values + a contribution sign so callers can explain
123
+ // why two harnesses scored as they did (used by Recommendation Engine
124
+ // §3.2 confidence calc + Drift Detection §3.3 alert reason).
125
+ const out = {};
126
+
127
+ const numericKeys = [
128
+ ['harnessFit', 'score', 100], ['compileConfidence', 'score', 100],
129
+ ['taskCoverage', 'score', 100], ['toolSafety', 'score', 100],
130
+ ['memoryUsefulness', 'score', 100],
131
+ ['risk_score', 'genome', 1], ['test_confidence', 'genome', 1],
132
+ ['publish_readiness', 'genome', 1],
133
+ ['estCostPerRunUsd', 'score', 1],
134
+ ];
135
+ for (let i = 0; i < numericKeys.length; i++) {
136
+ const [k, src] = numericKeys[i];
137
+ const av = a?.[src]?.[k];
138
+ const bv = b?.[src]?.[k];
139
+ out[`numeric.${k}`] = {
140
+ a: av ?? null, b: bv ?? null,
141
+ contribution: round4(va[i] * vb[i] * weights.cosine / 9),
142
+ };
143
+ }
144
+ for (const f of CATEGORICAL_FIELDS) {
145
+ const av = a?.genome?.[f] ?? a?.score?.[f];
146
+ const bv = b?.genome?.[f] ?? b?.score?.[f];
147
+ out[`categorical.${f}`] = {
148
+ a: av ?? null, b: bv ?? null,
149
+ contribution: av && bv && av === bv ? round4(weights.categorical / 4) : 0,
150
+ };
151
+ }
152
+ const A = new Set(a?.genome?.agent_topology ?? []);
153
+ const B = new Set(b?.genome?.agent_topology ?? []);
154
+ const overlap = [...A].filter((x) => B.has(x));
155
+ const union = new Set([...A, ...B]);
156
+ out['set.agent_topology'] = {
157
+ a: [...A], b: [...B],
158
+ contribution: union.size === 0 ? 0 : round4((overlap.length / union.size) * weights.jaccard),
159
+ };
160
+ return out;
161
+ }