@claude-flow/cli 3.25.4 → 3.25.6

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 (34) hide show
  1. package/.claude/helpers/helpers.manifest.json +2 -2
  2. package/dist/src/commands/doctor.js +74 -29
  3. package/dist/src/commands/neural.js +18 -5
  4. package/dist/src/init/executor.js +16 -16
  5. package/dist/src/init/mcp-generator.js +11 -6
  6. package/dist/src/ruvector/index.js +5 -1
  7. package/dist/src/ruvector/neural-router.js +6 -2
  8. package/dist/src/services/ruvector-training.js +7 -2
  9. package/package.json +1 -1
  10. package/plugins/ruflo-metaharness/.claude-flow/daemon-state.json +178 -0
  11. package/plugins/ruflo-metaharness/.claude-flow/daemon.pid +1 -0
  12. package/plugins/ruflo-metaharness/.claude-flow/logs/daemon.log +43 -0
  13. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/audit_1783604774864_ozbujc_prompt.log +19 -0
  14. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/audit_1783604774864_ozbujc_result.log +108 -0
  15. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/audit_1783605513587_ulvmpb_prompt.log +19 -0
  16. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/audit_1783605513587_ulvmpb_result.log +209 -0
  17. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/audit_1783606368867_ahysui_prompt.log +19 -0
  18. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/audit_1783606368867_ahysui_result.log +192 -0
  19. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/optimize_1783604894861_v6n3ut_prompt.log +14 -0
  20. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/optimize_1783604894861_v6n3ut_result.log +66 -0
  21. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/optimize_1783605934532_9h8ikb_prompt.log +14 -0
  22. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/optimize_1783605934532_9h8ikb_result.log +68 -0
  23. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/testgaps_1783605134860_9jssz9_prompt.log +14 -0
  24. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/testgaps_1783605134860_9jssz9_result.log +69 -0
  25. package/plugins/ruflo-metaharness/.claude-flow/logs/headless/testgaps_1783606516743_zftbaa_prompt.log +14 -0
  26. package/plugins/ruflo-metaharness/.claude-flow/metrics/backup.json +7 -0
  27. package/plugins/ruflo-metaharness/.claude-flow/metrics/codebase-map.json +11 -0
  28. package/plugins/ruflo-metaharness/.claude-flow/metrics/consolidation.json +16 -0
  29. package/plugins/ruflo-metaharness/.claude-flow/metrics/harness-loop.json +83 -0
  30. package/plugins/ruflo-metaharness/.claude-flow/metrics/performance.json +67 -0
  31. package/plugins/ruflo-metaharness/.claude-flow/metrics/security-audit.json +191 -0
  32. package/plugins/ruflo-metaharness/.claude-flow/metrics/test-gaps.json +68 -0
  33. package/plugins/ruflo-metaharness/.claude-flow/neural/stats.json +3 -3
  34. package/plugins/ruflo-metaharness/scripts/smoke.sh +18 -5
@@ -0,0 +1,191 @@
1
+ {
2
+ "timestamp": "2026-07-09T14:15:20.255Z",
3
+ "mode": "headless",
4
+ "workerType": "audit",
5
+ "model": "haiku",
6
+ "durationMs": 151388,
7
+ "executionId": "audit_1783606368867_ahysui",
8
+ "success": true,
9
+ "findings": {
10
+ "reportMetadata": {
11
+ "timestamp": "2026-07-09",
12
+ "target": "ruflo-metaharness plugin",
13
+ "scope": "scripts/, commands/, agents/, skills/ directories"
14
+ },
15
+ "summary": {
16
+ "overallRiskScore": 32,
17
+ "riskLevel": "Low-to-Moderate",
18
+ "totalVulnerabilities": 11,
19
+ "severity": {
20
+ "critical": 0,
21
+ "high": 1,
22
+ "medium": 5,
23
+ "low": 5
24
+ },
25
+ "productionReadiness": "Acceptable with recommended fixes",
26
+ "estimatedRemediationTime": "14-18 hours"
27
+ },
28
+ "vulnerabilities": [
29
+ {
30
+ "severity": "high",
31
+ "file": "scripts/audit-trend.mjs",
32
+ "line": 75,
33
+ "description": "Path traversal vulnerability: unvalidated file paths from --baseline and --current flags are directly used with readFileSync() without directory constraints. An attacker could read arbitrary files (e.g., --baseline /etc/passwd).",
34
+ "category": "Path Traversal",
35
+ "impact": "Information disclosure - exposure of sensitive files outside intended directories",
36
+ "fixComplexity": "low",
37
+ "fix": "Normalize and validate paths using path.resolve() to ensure they stay within designated directory"
38
+ },
39
+ {
40
+ "severity": "medium",
41
+ "file": "scripts/drift-from-history.mjs",
42
+ "line": 172,
43
+ "description": "Path traversal in --baseline-file parameter: arbitrary file paths accepted without validation.",
44
+ "category": "Path Traversal",
45
+ "impact": "Information disclosure - access to sensitive files outside intended scope",
46
+ "fixComplexity": "low",
47
+ "fix": "Implement path validation helper restricting to current directory"
48
+ },
49
+ {
50
+ "severity": "medium",
51
+ "file": "scripts/evolve.mjs",
52
+ "line": 83,
53
+ "description": "Missing numeric validation: parseInt() result not checked for NaN; invalid --generations could cause process hangs",
54
+ "category": "Input Validation",
55
+ "impact": "DoS via invalid numeric input causing process hangs or undefined behavior",
56
+ "fixComplexity": "low",
57
+ "fix": "Add isNaN() check and bounds validation (1-50)"
58
+ },
59
+ {
60
+ "severity": "medium",
61
+ "file": "scripts/test-with-openrouter.mjs",
62
+ "line": 64,
63
+ "description": "Command injection potential: execSync() uses template literal string interpolation without proper escaping. Although currently called with hardcoded 'OPENROUTER_API_KEY', the pattern is vulnerable if reused.",
64
+ "category": "Command Injection",
65
+ "impact": "Command injection risk if pattern is copied with untrusted input",
66
+ "fixComplexity": "low",
67
+ "fix": "Replace with spawnSync() array form to avoid shell interpretation"
68
+ },
69
+ {
70
+ "severity": "medium",
71
+ "file": "scripts/redblue.mjs",
72
+ "line": 84,
73
+ "description": "Incomplete flag validation: unknown flags are forwarded without validation (passthrough mechanism). Could allow malicious flag injection from untrusted contexts",
74
+ "category": "Input Validation",
75
+ "impact": "Potential flag injection leading to unexpected behavior or exposure of internal functionality",
76
+ "fixComplexity": "medium",
77
+ "fix": "Implement allow-list of safe flags before forwarding"
78
+ },
79
+ {
80
+ "severity": "medium",
81
+ "file": "scripts/redblue.mjs",
82
+ "line": 112,
83
+ "description": "Missing timeout bounds validation: parsed timeout values not validated for positive/reasonable bounds. Negative, zero, or extremely large values could cause DoS",
84
+ "category": "Input Validation",
85
+ "impact": "DoS via negative or zero timeouts causing process hangs",
86
+ "fixComplexity": "low",
87
+ "fix": "Add range checking: if (timeout < 100 || timeout > 3600000) throw Error()"
88
+ },
89
+ {
90
+ "severity": "low",
91
+ "file": "scripts/audit-trend.mjs",
92
+ "line": 78,
93
+ "description": "Information disclosure: error messages include full absolute paths in user-facing output",
94
+ "category": "Information Disclosure",
95
+ "impact": "Minor filesystem structure exposure in error messages",
96
+ "fixComplexity": "low",
97
+ "fix": "Use relative paths or just filename in error messages"
98
+ },
99
+ {
100
+ "severity": "low",
101
+ "file": "scripts/redblue.mjs",
102
+ "line": 202,
103
+ "description": "Silent error swallowing: file cleanup errors are silently ignored with empty catch blocks",
104
+ "category": "Error Handling",
105
+ "impact": "Silent errors making troubleshooting difficult; potential resource leaks",
106
+ "fixComplexity": "low",
107
+ "fix": "Add debug logging for ignored errors"
108
+ },
109
+ {
110
+ "severity": "low",
111
+ "file": "scripts/learn.mjs",
112
+ "line": 104,
113
+ "description": "Unvalidated JSON parsing from subprocess: greedy regex extraction ({[\\s\\S]*?}) with no size limits. Large payloads could cause memory exhaustion",
114
+ "category": "DoS Prevention",
115
+ "impact": "DoS via memory exhaustion from large JSON payloads",
116
+ "fixComplexity": "low",
117
+ "fix": "Add size limit before parsing: if (json.length > 10MB) throw Error()"
118
+ },
119
+ {
120
+ "severity": "low",
121
+ "file": "scripts/drift-from-history.mjs",
122
+ "line": 44,
123
+ "description": "Environment variables used without validation: METAHARNESS_AUDIT_NAMESPACE and CLI_PKG used directly without schema validation",
124
+ "category": "Input Validation",
125
+ "impact": "Namespace/package spoofing if environment variables are controlled by attacker",
126
+ "fixComplexity": "low",
127
+ "fix": "Validate environment variables against whitelist/regex patterns on startup"
128
+ },
129
+ {
130
+ "severity": "low",
131
+ "file": "scripts/mint.mjs",
132
+ "line": 27,
133
+ "description": "Missing format validation: --name and --template parameters not validated against format requirements",
134
+ "category": "Input Validation",
135
+ "impact": "Invalid harness generation with malformed names",
136
+ "fixComplexity": "low",
137
+ "fix": "Add format validation (alphanumeric + colon/hyphen): /^[a-zA-Z0-9:-]+$/"
138
+ }
139
+ ],
140
+ "positiveFindings": [
141
+ "✅ No hardcoded secrets - all sensitive data sourced from environment variables",
142
+ "✅ Pinned dependency versions using tilde ranges (~0.3.0), not @latest",
143
+ "✅ Graceful degradation for optional @metaharness/* dependencies (ADR-150)",
144
+ "✅ Safe subprocess handling with consistent shell:false and array-based argv",
145
+ "✅ Safe JSON parsing with proper try-catch blocks",
146
+ "✅ All subprocesses have hard timeouts (prevents hangs)",
147
+ "✅ Good separation of concerns with shared helpers",
148
+ "✅ No eval(), Function(), or dynamic code execution"
149
+ ],
150
+ "recommendations": [
151
+ {
152
+ "priority": "CRITICAL",
153
+ "action": "Fix path traversal vulnerabilities in audit-trend.mjs and drift-from-history.mjs",
154
+ "timeline": "Week 1",
155
+ "effort": "2-3 hours"
156
+ },
157
+ {
158
+ "priority": "HIGH",
159
+ "action": "Add comprehensive numeric parameter validation across all scripts",
160
+ "timeline": "Week 1-2",
161
+ "effort": "3-4 hours"
162
+ },
163
+ {
164
+ "priority": "HIGH",
165
+ "action": "Fix command injection risk pattern in test-with-openrouter.mjs (line 64-65)",
166
+ "timeline": "Week 1",
167
+ "effort": "1 hour"
168
+ },
169
+ {
170
+ "priority": "HIGH",
171
+ "action": "Implement flag validation in redblue.mjs with allow-list",
172
+ "timeline": "Week 2",
173
+ "effort": "2-3 hours"
174
+ },
175
+ {
176
+ "priority": "MEDIUM",
177
+ "action": "Add environment variable schema validation on startup",
178
+ "timeline": "Week 2",
179
+ "effort": "1-2 hours"
180
+ },
181
+ {
182
+ "priority": "MEDIUM",
183
+ "action": "Sanitize error messages to prevent information disclosure",
184
+ "timeline": "Week 2",
185
+ "effort": "1-2 hours"
186
+ }
187
+ ]
188
+ },
189
+ "rawOutputPreview": "Based on my comprehensive security analysis of the ruflo-metaharness codebase, here's the security audit report in JSON format:\n\n```json\n{\n \"reportMetadata\": {\n \"timestamp\": \"2026-07-09\",\n \"target\": \"ruflo-metaharness plugin\",\n \"scope\": \"scripts/, commands/, agents/, skills/ directories\"\n },\n \"summary\": {\n \"overallRiskScore\": 32,\n \"riskLevel\": \"Low-to-Moderate\",\n \"totalVulnerabilities\": 11,\n \"severity\": {\n \"critical\": 0,\n \"high\": 1,\n \"medium\": 5,\n \"low\": 5\n },\n \"productionReadiness\": \"Acceptable with recommended fixes\",\n \"estimatedRemediationTime\": \"14-18 hours\"\n },\n \"vulnerabilities\": [\n {\n \"severity\": \"high\",\n \"file\": \"scripts/audit-trend.mjs\",\n \"line\": 75,\n \"description\": \"Path traversal vulnerability: unvalidated file paths from --baseline and --current flags are directly used with readFileSync() without directory constraints. An attacker could read arbitrary files (e.g., --baseline /etc/passwd).\",\n \"category\": \"Path Traversal\",\n \"impact\": \"Information disclosure - exposure of sensitive files outside intended directories\",\n \"fixComplexity\": \"low\",\n \"fix\": \"Normalize and validate paths using path.resolve() to ensure they stay within designated directory\"\n },\n {\n \"severity\": \"medium\",\n \"file\": \"scripts/drift-from-history.mjs\",\n \"line\": 172,\n \"description\": \"Path traversal in --baseline-file parameter: arbitrary file paths accepted without validation.\",\n \"category\": \"Path Traversal\",\n \"impact\": \"Information disclosure - access to sensitive files outside intended scope\",\n \"fixComplexity\": \"low\",\n \"fix\": \"Implement path validation helper restricting to current directory\"\n },\n {\n \"severity\": \"medium\",\n \"file\": \"scripts/evolve.mjs\",\n \"line\": 83,\n \"description\": \"Missing numeric validation: parseInt() result not checked for NaN; invalid --generations could cause process hangs\",\n \"category\": \"Input ",
190
+ "rawOutputLength": 9183
191
+ }
@@ -0,0 +1,68 @@
1
+ {
2
+ "timestamp": "2026-07-09T13:55:16.742Z",
3
+ "mode": "headless",
4
+ "workerType": "testgaps",
5
+ "model": "sonnet",
6
+ "durationMs": 181882,
7
+ "executionId": "testgaps_1783605134860_9jssz9",
8
+ "success": true,
9
+ "findings": {
10
+ "sections": [
11
+ {
12
+ "title": "Test Coverage Analysis — `plugins/ruflo-metaharness`",
13
+ "content": "\nThis plugin uses a hand-rolled test harness (`test-*.mjs` scripts with a custom `assert()` helper, no Jest/Vitest), plus `scripts/smoke.sh` (bash, mostly structural/CLI-registration grep checks) run in CI via `no-metaharness-smoke.yml` / `metaharness-ci.yml`.\n\n**Existing coverage:**\n| Test file | Covers |\n|---|---|\n| `test-similarity.mjs` | `_similarity.mjs` (all 5 fns) + `_harness.mjs` severity primitives |\n| `test-graceful-degradation.mjs` | CLI-level exit-0/degraded contract for `score/genome/mcp-scan/threat-model/oia-audit/audit-list/audit-trend/mint` only |\n| `test-mcp-tools.mjs` | MCP tool handler contract (shape, no-throw) against compiled dist |\n| `test-parallel-pipeline.mjs` | `router-parallel-analyze.mjs` as a black-box subprocess against a synthesized JSONL fixture |\n| `test-pipeline-roundtrip.mjs` | `oia-audit` → `audit-trend` e2e chain |\n| `test-with-openrouter.mjs` | Real e2e against OpenRouter (costs money, needs GCP secret) |\n| `smoke.sh` | Structural grep + CLI subcommand/`--help` registration for everything |\n\n",
14
+ "level": 2
15
+ },
16
+ {
17
+ "title": "1. Untested functions/classes",
18
+ "content": "\n**`_invoke.mjs` — zero direct unit tests.** This is the highest-value gap: it's the extracted shared plumbing (per its own header, \"converged security/perf/arch review\") used by ~15 scripts, made of small pure/near-pure functions, yet no test imports it directly — only indirectly through subprocess-level e2e tests that need npm/network.\n- `classifyDegraded(stderr, exitCode, reasonPrefix)` — untested\n- `injectJson(args, wantJson)` — untested\n- `parseTrailingJson(stdout)` — untested (this one **replaced a live bug** per the comment — the exact kind of function that regresses silently without a unit test)\n- `satisfiesTildeRange(version, pinVersion)` — untested\n- `findLocalPackageDir(pkg, pinVersion)` — untested (env-seam `RUFLO_METAHARNESS_SKIP_LOCAL` exists but nothing exercises it)\n- `cacheBaseDir()` — untested (env-seam `RUFLO_METAHARNESS_CACHE_BASE` exists but nothing exercises it)\n- `makeDegradedEmitter(pkg, pinVersion)` — untested\n\n**`_darwin.mjs` / `_redblue.mjs`** — only exercised transitively (network-dependent, via smoke.sh registration checks), never unit-tested directly:\n- `_darwin.mjs`: `runDarwin`, `runDarwinAsync` (streaming/`onProgress`/abort-signal paths), `importGepa`\n- `_redblue.mjs`: `runRedblue`, `emitRedblueDegradedJsonAndExit`\n\n**Pure parsing/logic functions, module-private (not exported), with no direct test:**\n- `security-bench.mjs: parseSecurityBenchMarkdown()` — 3 independent regex extractions (overall verdict, gate lines, baseline table)\n- `redblue.mjs: buildUpstreamArgs()` — per-subcommand argv construction + validation (`attack` family check, `report` requires `--in`)\n- `evolve.mjs: looksLikeGepaTranscript()`, `extractGepaTranscripts()`, `summarizeTraces()`, `safetyChecks()`\n- `audit-list.mjs: parseDurationMs()` — duration-spec parser (`7d`, `24h`, `1w`, `1m`)\n- `gepa.mjs: readJsonFile()`, `loadGenomeOrExit()`\n- `router-parallel-analyze.mjs: median()`, `pctile()`, `mean()` — pure stats, only exercised indirectly through a full subprocess run in `test-parallel-pipeline.mjs`\n\n**Note:** all of the above are unexported module-level functions in CLI scripts that run `main()` on import — that's *why* nobody unit-tests them directly today. Testing them requires either (a) exporting them (cheap, mirrors what `_similarity.mjs` already does) or (b) testing through stdout/exit-code capture of the whole script. Skeletons below assume (a) since it's the established pattern in this codebase.\n\n",
19
+ "level": 2
20
+ },
21
+ {
22
+ "title": "2. Edge cases not covered",
23
+ "content": "\n- `parseTrailingJson`: multiple `{...}` blocks where an earlier block is unparseable JSON but a *later* one nests braces the lazy regex fragments (the exact bug class the comment says this fixed) — no fixture actually exercises nested-object recovery via the greedy fallback.\n- `satisfiesTildeRange`: version with a `-alpha.N` suffix, `pinVersion` without a leading `~`, garbage input.\n- `classifyDegraded`: `exitCode === 0` with degraded-looking stderr (should NOT degrade — success takes precedence over noisy stderr); `stderr` undefined vs empty string.\n- `findLocalPackageDir`: version present but pre-1.0 (`0.x.y`) tilde matching; symlinked node_modules; two ancestors both having a stale version (should pick a satisfying one over the first found).\n- `parseDurationMs`: unit `m` is ambiguous (30-day \"month\" vs minutes) — worth a regression test pinning the intended semantics; malformed specs (`7`, `d7`, `-7d`).\n- `parseSecurityBenchMarkdown`: `overallMatch` is `null` (upstream changed emoji/wording) — does the caller handle a `null` gate list gracefully?\n- `buildUpstreamArgs('attack')`: missing `--count` (`ARGS.count === null`) should omit the flag, not emit `--count null`.\n\n",
24
+ "level": 2
25
+ },
26
+ {
27
+ "title": "3. Missing error handling tests",
28
+ "content": "\n- `ensureCachedInstall`: `mkdirSync` throw path (`cache-dir-create-failed`) and `npm install` non-zero exit (`install-failed`) — both branches untested.\n- `importOptionalLibrary`: the `recoverable` regex gate — an *unrelated* `TypeError` thrown from within an installed module must NOT be swallowed (re-thrown), only `MODULE_NOT_FOUND`-shaped errors should fall through to cached-install. No test proves non-recoverable errors propagate.\n- `evolve.mjs safetyChecks()`: none of `--generations`/`--children`/`--concurrency` out-of-range, invalid `--sandbox`/`--selection`/`--mutator`, or nonexistent `--repo` path are unit-tested (only reachable via full CLI spawn today).\n- `security-bench.mjs safetyChecks()`: `--population`/`--cycles` bounds untested directly.\n- `redblue.mjs`: `report` without `--in`, `attack` with invalid family — both call `process.exit(2)`, untested.\n\n",
29
+ "level": 2
30
+ },
31
+ {
32
+ "title": "4. Integration test gaps",
33
+ "content": "\n- **`evolve` / `gepa` / `learn` / `redblue` / `bench` / `security-bench` are absent from `test-graceful-degradation.mjs`.** The file's own header explicitly scopes to 8 skills and says nothing about these six. Given ADR-150's architectural constraint (\"ruflo remains operational if every MetaHarness package is removed\") is supposed to apply project-wide, this is a real gate gap, not just a nice-to-have — a regression in `_darwin.mjs`'s or `_redblue.mjs`'s degraded path for these specific subcommands would ship silently.\n- `drift-from-history.mjs` composing 3 primitives (`audit-list` → `similarity`/`audit-trend`) has no dedicated round-trip test analogous to `test-pipeline-roundtrip.mjs` — only structural grep in `smoke.sh`.\n- No test exercises `RUFLO_METAHARNESS_CACHE_BASE` / `RUFLO_METAHARNESS_SKIP_LOCAL` env seams that `_invoke.mjs` explicitly documents as \"used by test-graceful-degradation.mjs\" — grep confirms they aren't actually referenced there.\n\n",
34
+ "level": 2
35
+ },
36
+ {
37
+ "title": "Test skeletons",
38
+ "content": "\n**New file: `scripts/test-invoke.mjs`** (mirrors `test-similarity.mjs`'s style; requires exporting nothing new — everything below is already exported)\n\n```js\n#!/usr/bin/env node\n// test-invoke.mjs — unit tests for the shared _invoke.mjs plumbing\n// (classifyDegraded, injectJson, parseTrailingJson, satisfiesTildeRange,\n// findLocalPackageDir, cacheBaseDir). No network, no subprocess spawns.\n\nimport { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\nimport {\n classifyDegraded, injectJson, parseTrailingJson,\n satisfiesTildeRange, findLocalPackageDir, cacheBaseDir,\n} from './_invoke.mjs';\n\nlet passed = 0, failed = 0;\nconst failures = [];\nfunction assert(cond, label) {\n if (cond) { console.log(` ✓ ${label}`); passed++; }\n else { console.log(` ✗ ${label}`); failures.push(label); failed++; }\n}\n\nconsole.log('# classifyDegraded');\nassert(classifyDegraded('', null, 'x').degraded === true, 'null exitCode -> timeout');\nassert(classifyDegraded('', null, 'x').reason === 'x-timeout', 'timeout reason prefixed');\nassert(classifyDegraded('npm ERR! 404', 1, 'x').reason === 'x-not-available', 'npm ERR matches DEGRADED_RX');\nassert(classifyDegraded('some noisy stderr', 0, 'x').degraded === false, 'exit 0 with noisy stderr is NOT degraded');\nassert(classifyDegraded(undefined, 1, 'x').degraded === false, 'undefined stderr does not throw / does not false-positive');\n\nconsole.log('\\n# injectJson');\nassert(injectJson(['run'], true).includes('--json'), 'appends --json when wanted');\nassert(!injectJson(['run'], false).includes('--json'), 'omits --json when not wanted');\nassert(injectJson(['run', '--json'], true).filter((a) => a === '--json').length === 1, 'no duplicate --json');\n\nconsole.log('\\n# parseTrailingJson');\nassert(parseTrailingJson('progress\\n{\"a\":1}\\n{\"b\":2}').b === 2, 'picks LAST block, not first');\nassert(parseTrailingJson('{\"a\": {\"nested\": {\"deep\": 1}}}').a.nested.deep === 1, 'greedy fallback recovers nested braces');\nassert(parseTrailingJson('no json here') === null, 'no braces -> null, never throws');\nassert(parseTrailingJson('{\"a\":1} garbage {not json}').a === 1, 'unparseable trailing block falls back to earlier valid one');\n\nconsole.log('\\n# satisfiesTildeRange');\nassert(satisfiesTildeRange('0.8.3', '~0.8.0') === true, 'patch >= pin satisfies');\nassert(satisfiesTildeRange('0.8.0', '~0.8.0') === true, 'exact patch satisfies');\nassert(satisfiesTildeRange('0.9.0', '~0.8.0') === false, 'minor bump does not satisfy tilde range');\nassert(satisfiesTildeRange('not-a-version', '~0.8.0') === false, 'garbage version -> false, not throw');\nassert(satisfiesTildeRange('0.8.3', 'garbage-pin') === false, 'garbage pin -> false, not throw');\n\nconsole.log('\\n# cacheBaseDir + RUFLO_METAHARNESS_CACHE_BASE seam');\n{\n const prev = process.env.RUFLO_METAHARNESS_CACHE_BASE;\n delete process.env.RUFLO_METAHARNESS_CACHE_BASE;\n assert(cacheBaseDir().endsWith('.ruflo'), 'defaults to ~/.ruflo');\n process.env.RUFLO_METAHARNESS_CACHE_BASE = '/tmp/custom-cache';\n assert(cacheBaseDir() === '/tmp/custom-cache', 'env override respected');\n if (prev === undefined) delete process.env.RUFLO_METAHARNESS_CACHE_BASE;\n else process.env.RUFLO_METAHARNESS_CACHE_BASE = prev;\n}\n\nconsole.log('\\n# findLocalPackageDir + RUFLO_METAHARNESS_SKIP_LOCAL seam');\n{\n const fixture = mkdtempSync(join(tmpdir(), 'invoke-fixture-'));\n const pkgDir = join(fixture, 'node_modules', '@metaharness', 'darwin');\n mkdirSync(pkgDir, { recursive: true });\n writeFileSync(join(pkgDir, 'package.json'), JSON.stringify({ version: '0.8.3' }));\n const prevCwd = process.cwd();\n process.chdir(fixture);\n try {\n assert(findLocalPackageDir('@metaharness/darwin', '~0.8.0') === pkgDir, 'finds satisfying local install by walking cwd');\n assert(findLocalPackageDir('@metaharness/darwin', '~0.9.0') === null, 'rejects local install that does not satisfy pin');\n process.env.RUFLO_METAHARNESS_SKIP_LOCAL = '1';\n assert(findLocalPackageDir('@metaharness/darwin', '~0.8.0') === null, 'SKIP_LOCAL seam disables resolution entirely');\n } finally {\n delete process.env.RUFLO_METAHARNESS_SKIP_LOCAL;\n process.chdir(prevCwd);\n rmSync(fixture, { recursive: true, force: true });\n }\n}\n\nconsole.log(`\\n${passed} passed, ${failed} failed`);\nif (failed > 0) { console.log('Failures:', failures.join(', ')); process.exit(1); }\n```\n\n**Extend `test-graceful-degradation.mjs`** — add the 6 missing skills to close the ADR-150 gate gap:\n\n```js\n// Add alongside the existing 8-skill list:\nconst MISSING_SKILLS = ['evolve', 'gepa', 'learn', 'redblue', 'bench', 'security-bench'];\nfor (const skill of MISSING_SKILLS) {\n // same drill as the existing loop: spawn with npm_config_registry pointed\n // at an unresolvable host, assert exitCode === 0 and stdout contains\n // \"degraded\": true — these six currently escape the drill entirely.\n}\n```\n\n**New file: `scripts/test-cli-parsers.mjs`** (requires adding `export` to the named functions in `security-bench.mjs`, `redblue.mjs`, `audit-list.mjs`, `evolve.mjs`, `gepa.mjs` — currently module-private, which is *why* they're untestable in isolation today):\n\n```js\n#!/usr/bin/env node\n// test-cli-parsers.mjs — unit tests for pure parsing/validation logic\n// embedded in CLI scripts. Requires exporting: parseSecurityBenchMarkdown\n// (security-bench.mjs), buildUpstreamArgs (redblue.mjs), parseDurationMs\n// (audit-list.mjs), looksLikeGepaTranscript/extractGepaTranscripts/\n// summarizeTraces (evolve.mjs).\n\nimport { parseSecurityBenchMarkdown } from './security-bench.mjs';\nimport { parseDurationMs } from './audit-list.mjs';\nimport { looksLikeGepaTranscript, extractGepaTranscripts, summarizeTraces } from './evolve.mjs';\n\nlet passed = 0, failed = 0;\nconst failures = [];\nfunction assert(cond, label) {\n if (cond) { console.log(` ✓ ${label}`); passed++; }\n else { console.log(` ✗ ${label}`); failures.push(label); failed++; }\n}\n\nconsole.log('# parseSecurityBenchMarkdown');\nconst md = `\n**Overall: ✅ PASS**\n- ✅ **TPR improvement ≥ 25% vs fixed harness** — +150%\n- ❌ **FPR ≤ 5%** — 8%\n| B1 baseline | 0.42 | 0.60 | 0.10 | 0.90 | 0.95 | 2 | $0.10 |\n`;\nconst parsed = parseSecurityBenchMarkdown(md);\nassert(parsed.overall.ok === true, 'overall PASS parsed');\nassert(parsed.gates.length === 2, 'both gate lines parsed');\nassert(parsed.gates[1].ok === false, 'FAIL gate icon parsed correctly');\nassert(parsed.baselines[0].fitness === 0.42, 'baseline table row parsed');\nassert(parseSecurityBenchMarkdown('no matches here').overall === null, 'missing Overall line -> null, not throw');\nassert(parseSecurityBenchMarkdown('no matches here').gates.length === 0, 'no gate lines -> empty array, not throw');\n\nconsole.log('\\n# parseDurationMs');\nassert(parseDurationMs('7d') === 7 * 86400_000, '7d parses');\nassert(parseDurationMs('24h') === 24 * 3600_000, '24h parses');\nassert(parseDurationMs('1w') === 7 * 86400_000, '1w parses');\nassert(parseDurationMs('garbage') === null, 'malformed spec -> null');\nassert(parseDurationMs('-7d') === null, 'negative spec -> null (regex requires digits only)');\nassert(parseDurationMs('7') === null, 'missing unit -> null');\n\nconsole.log('\\n# evolve.mjs gepa-transcript helpers');\nassert(looksLikeGepaTranscript([{ actionRaw: 'x' }]) === true, 'actionRaw-shaped entries recognized');\nassert(looksLikeGepaTranscript([{ obs: 'y' }]) === true, 'obs-shaped entries recognized');\nassert(looksLikeGepaTranscript([]) === false, 'empty array is not a transcript');\nassert(looksLikeGepaTranscript([{ foo: 1 }]) === false, 'unrelated shape rejected');\nassert(looksLikeGepaTranscript(null) === false, 'null input -> false, not throw');\n\nconst record = { transcript: [{ actionRaw: 'a' }], traces: [{ taskId: 't1', exitCode: 1, timedOut: true, blockedActions: ['x'] }] };\nassert(extractGepaTranscripts(record).length === 1, 'top-level transcript extracted');\nconst summary = summarizeTraces(record);\nassert(summary.tasks === 1 && summary.failed === 1 && summary.timedOut === 1 && summary.blockedActions === 1, 'summarizeTraces tallies correctly');\nassert(summarizeTraces({}).tasks === 0, 'missing traces array -> zeroed summary, not throw');\n\nconsole.log(`\\n${passed} passed, ${failed} failed`);\nif (failed > 0) { console.log('Failures:', failures.join(', ')); process.exit(1); }\n```\n\n**New file: `scripts/test-router-parallel-math.mjs`** (requires exporting `median`/`pctile`/`mean` from `router-parallel-analyze.mjs`):\n\n```js\n#!/usr/bin/env node\nimport { median, pctile, mean } from './router-parallel-analyze.mjs';\n\nlet passed = 0, failed = 0;\nfunction assert(cond, label) { if (cond) passed++; else { console.log(`✗ ${label}`); failed++; } }\n\nassert(median([1, 2, 3]) === 2, 'odd-length median');\nassert(median([1, 2, 3, 4]) === 2.5, 'even-length median averages middle two');\nassert(median([]) === undefined || Number.isNaN(median([])), 'empty array does not throw');\nassert(pctile([1, 2, 3, 4, 5], 0.5) === median([1, 2, 3, 4, 5]), 'p50 matches median');\nassert(mean([2, 4, 6]) === 4, 'mean computes correctly');\nassert(Number.isNaN(mean([])) || mean([]) === 0, 'empty array mean defined (pin the actual convention)');\n\nconsole.log(`${passed} passed, ${failed} failed`);\nif (failed > 0) process.exit(1);\n```\n\n",
39
+ "level": 2
40
+ },
41
+ {
42
+ "title": "Priority",
43
+ "content": "1. `scripts/test-invoke.mjs` — closes the biggest real gap (shared plumbing, zero direct tests, already caused one documented regression).\n2. Extend `test-graceful-degradation.mjs` to the 6 missing skills — closes an actual ADR-150 CI gate hole.\n3. `test-cli-parsers.mjs` + `test-router-parallel-math.mjs` — cheap, pure-function tests once the target functions are exported (small, low-risk diffs to each source file).",
44
+ "level": 2
45
+ }
46
+ ],
47
+ "codeBlocks": [
48
+ {
49
+ "language": "js",
50
+ "code": "#!/usr/bin/env node\n// test-invoke.mjs — unit tests for the shared _invoke.mjs plumbing\n// (classifyDegraded, injectJson, parseTrailingJson, satisfiesTildeRange,\n// findLocalPackageDir, cacheBaseDir). No network, no subprocess spawns.\n\nimport { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\nimport {\n classifyDegraded, injectJson, parseTrailingJson,\n satisfiesTildeRange, findLocalPackageDir, cacheBaseDir,\n} from './_invoke.mjs';\n\nlet passed = 0, failed = 0;\nconst failures = [];\nfunction assert(cond, label) {\n if (cond) { console.log(` ✓ ${label}`); passed++; }\n else { console.log(` ✗ ${label}`); failures.push(label); failed++; }\n}\n\nconsole.log('# classifyDegraded');\nassert(classifyDegraded('', null, 'x').degraded === true, 'null exitCode -> timeout');\nassert(classifyDegraded('', null, 'x').reason === 'x-timeout', 'timeout reason prefixed');\nassert(classifyDegraded('npm ERR! 404', 1, 'x').reason === 'x-not-available', 'npm ERR matches DEGRADED_RX');\nassert(classifyDegraded('some noisy stderr', 0, 'x').degraded === false, 'exit 0 with noisy stderr is NOT degraded');\nassert(classifyDegraded(undefined, 1, 'x').degraded === false, 'undefined stderr does not throw / does not false-positive');\n\nconsole.log('\\n# injectJson');\nassert(injectJson(['run'], true).includes('--json'), 'appends --json when wanted');\nassert(!injectJson(['run'], false).includes('--json'), 'omits --json when not wanted');\nassert(injectJson(['run', '--json'], true).filter((a) => a === '--json').length === 1, 'no duplicate --json');\n\nconsole.log('\\n# parseTrailingJson');\nassert(parseTrailingJson('progress\\n{\"a\":1}\\n{\"b\":2}').b === 2, 'picks LAST block, not first');\nassert(parseTrailingJson('{\"a\": {\"nested\": {\"deep\": 1}}}').a.nested.deep === 1, 'greedy fallback recovers nested braces');\nassert(parseTrailingJson('no json here') === null, 'no braces -> null, never throws');\nassert(parseTrailingJson('{\"a\":1} garbage {not json}').a === 1, 'unparseable trailing block falls back to earlier valid one');\n\nconsole.log('\\n# satisfiesTildeRange');\nassert(satisfiesTildeRange('0.8.3', '~0.8.0') === true, 'patch >= pin satisfies');\nassert(satisfiesTildeRange('0.8.0', '~0.8.0') === true, 'exact patch satisfies');\nassert(satisfiesTildeRange('0.9.0', '~0.8.0') === false, 'minor bump does not satisfy tilde range');\nassert(satisfiesTildeRange('not-a-version', '~0.8.0') === false, 'garbage version -> false, not throw');\nassert(satisfiesTildeRange('0.8.3', 'garbage-pin') === false, 'garbage pin -> false, not throw');\n\nconsole.log('\\n# cacheBaseDir + RUFLO_METAHARNESS_CACHE_BASE seam');\n{\n const prev = process.env.RUFLO_METAHARNESS_CACHE_BASE;\n delete process.env.RUFLO_METAHARNESS_CACHE_BASE;\n assert(cacheBaseDir().endsWith('.ruflo'), 'defaults to ~/.ruflo');\n process.env.RUFLO_METAHARNESS_CACHE_BASE = '/tmp/custom-cache';\n assert(cacheBaseDir() === '/tmp/custom-cache', 'env override respected');\n if (prev === undefined) delete process.env.RUFLO_METAHARNESS_CACHE_BASE;\n else process.env.RUFLO_METAHARNESS_CACHE_BASE = prev;\n}\n\nconsole.log('\\n# findLocalPackageDir + RUFLO_METAHARNESS_SKIP_LOCAL seam');\n{\n const fixture = mkdtempSync(join(tmpdir(), 'invoke-fixture-'));\n const pkgDir = join(fixture, 'node_modules', '@metaharness', 'darwin');\n mkdirSync(pkgDir, { recursive: true });\n writeFileSync(join(pkgDir, 'package.json'), JSON.stringify({ version: '0.8.3' }));\n const prevCwd = process.cwd();\n process.chdir(fixture);\n try {\n assert(findLocalPackageDir('@metaharness/darwin', '~0.8.0') === pkgDir, 'finds satisfying local install by walking cwd');\n assert(findLocalPackageDir('@metaharness/darwin', '~0.9.0') === null, 'rejects local install that does not satisfy pin');\n process.env.RUFLO_METAHARNESS_SKIP_LOCAL = '1';\n assert(findLocalPackageDir('@metaharness/darwin', '~0.8.0') === null, 'SKIP_LOCAL seam disables resolution entirely');\n } finally {\n delete process.env.RUFLO_METAHARNESS_SKIP_LOCAL;\n process.chdir(prevCwd);\n rmSync(fixture, { recursive: true, force: true });\n }\n}\n\nconsole.log(`\\n${passed} passed, ${failed} failed`);\nif (failed > 0) { console.log('Failures:', failures.join(', ')); process.exit(1); }"
51
+ },
52
+ {
53
+ "language": "js",
54
+ "code": "// Add alongside the existing 8-skill list:\nconst MISSING_SKILLS = ['evolve', 'gepa', 'learn', 'redblue', 'bench', 'security-bench'];\nfor (const skill of MISSING_SKILLS) {\n // same drill as the existing loop: spawn with npm_config_registry pointed\n // at an unresolvable host, assert exitCode === 0 and stdout contains\n // \"degraded\": true — these six currently escape the drill entirely.\n}"
55
+ },
56
+ {
57
+ "language": "js",
58
+ "code": "#!/usr/bin/env node\n// test-cli-parsers.mjs — unit tests for pure parsing/validation logic\n// embedded in CLI scripts. Requires exporting: parseSecurityBenchMarkdown\n// (security-bench.mjs), buildUpstreamArgs (redblue.mjs), parseDurationMs\n// (audit-list.mjs), looksLikeGepaTranscript/extractGepaTranscripts/\n// summarizeTraces (evolve.mjs).\n\nimport { parseSecurityBenchMarkdown } from './security-bench.mjs';\nimport { parseDurationMs } from './audit-list.mjs';\nimport { looksLikeGepaTranscript, extractGepaTranscripts, summarizeTraces } from './evolve.mjs';\n\nlet passed = 0, failed = 0;\nconst failures = [];\nfunction assert(cond, label) {\n if (cond) { console.log(` ✓ ${label}`); passed++; }\n else { console.log(` ✗ ${label}`); failures.push(label); failed++; }\n}\n\nconsole.log('# parseSecurityBenchMarkdown');\nconst md = `\n**Overall: ✅ PASS**\n- ✅ **TPR improvement ≥ 25% vs fixed harness** — +150%\n- ❌ **FPR ≤ 5%** — 8%\n| B1 baseline | 0.42 | 0.60 | 0.10 | 0.90 | 0.95 | 2 | $0.10 |\n`;\nconst parsed = parseSecurityBenchMarkdown(md);\nassert(parsed.overall.ok === true, 'overall PASS parsed');\nassert(parsed.gates.length === 2, 'both gate lines parsed');\nassert(parsed.gates[1].ok === false, 'FAIL gate icon parsed correctly');\nassert(parsed.baselines[0].fitness === 0.42, 'baseline table row parsed');\nassert(parseSecurityBenchMarkdown('no matches here').overall === null, 'missing Overall line -> null, not throw');\nassert(parseSecurityBenchMarkdown('no matches here').gates.length === 0, 'no gate lines -> empty array, not throw');\n\nconsole.log('\\n# parseDurationMs');\nassert(parseDurationMs('7d') === 7 * 86400_000, '7d parses');\nassert(parseDurationMs('24h') === 24 * 3600_000, '24h parses');\nassert(parseDurationMs('1w') === 7 * 86400_000, '1w parses');\nassert(parseDurationMs('garbage') === null, 'malformed spec -> null');\nassert(parseDurationMs('-7d') === null, 'negative spec -> null (regex requires digits only)');\nassert(parseDurationMs('7') === null, 'missing unit -> null');\n\nconsole.log('\\n# evolve.mjs gepa-transcript helpers');\nassert(looksLikeGepaTranscript([{ actionRaw: 'x' }]) === true, 'actionRaw-shaped entries recognized');\nassert(looksLikeGepaTranscript([{ obs: 'y' }]) === true, 'obs-shaped entries recognized');\nassert(looksLikeGepaTranscript([]) === false, 'empty array is not a transcript');\nassert(looksLikeGepaTranscript([{ foo: 1 }]) === false, 'unrelated shape rejected');\nassert(looksLikeGepaTranscript(null) === false, 'null input -> false, not throw');\n\nconst record = { transcript: [{ actionRaw: 'a' }], traces: [{ taskId: 't1', exitCode: 1, timedOut: true, blockedActions: ['x'] }] };\nassert(extractGepaTranscripts(record).length === 1, 'top-level transcript extracted');\nconst summary = summarizeTraces(record);\nassert(summary.tasks === 1 && summary.failed === 1 && summary.timedOut === 1 && summary.blockedActions === 1, 'summarizeTraces tallies correctly');\nassert(summarizeTraces({}).tasks === 0, 'missing traces array -> zeroed summary, not throw');\n\nconsole.log(`\\n${passed} passed, ${failed} failed`);\nif (failed > 0) { console.log('Failures:', failures.join(', ')); process.exit(1); }"
59
+ },
60
+ {
61
+ "language": "js",
62
+ "code": "#!/usr/bin/env node\nimport { median, pctile, mean } from './router-parallel-analyze.mjs';\n\nlet passed = 0, failed = 0;\nfunction assert(cond, label) { if (cond) passed++; else { console.log(`✗ ${label}`); failed++; } }\n\nassert(median([1, 2, 3]) === 2, 'odd-length median');\nassert(median([1, 2, 3, 4]) === 2.5, 'even-length median averages middle two');\nassert(median([]) === undefined || Number.isNaN(median([])), 'empty array does not throw');\nassert(pctile([1, 2, 3, 4, 5], 0.5) === median([1, 2, 3, 4, 5]), 'p50 matches median');\nassert(mean([2, 4, 6]) === 4, 'mean computes correctly');\nassert(Number.isNaN(mean([])) || mean([]) === 0, 'empty array mean defined (pin the actual convention)');\n\nconsole.log(`${passed} passed, ${failed} failed`);\nif (failed > 0) process.exit(1);"
63
+ }
64
+ ]
65
+ },
66
+ "rawOutputPreview": "## Test Coverage Analysis — `plugins/ruflo-metaharness`\n\nThis plugin uses a hand-rolled test harness (`test-*.mjs` scripts with a custom `assert()` helper, no Jest/Vitest), plus `scripts/smoke.sh` (bash, mostly structural/CLI-registration grep checks) run in CI via `no-metaharness-smoke.yml` / `metaharness-ci.yml`.\n\n**Existing coverage:**\n| Test file | Covers |\n|---|---|\n| `test-similarity.mjs` | `_similarity.mjs` (all 5 fns) + `_harness.mjs` severity primitives |\n| `test-graceful-degradation.mjs` | CLI-level exit-0/degraded contract for `score/genome/mcp-scan/threat-model/oia-audit/audit-list/audit-trend/mint` only |\n| `test-mcp-tools.mjs` | MCP tool handler contract (shape, no-throw) against compiled dist |\n| `test-parallel-pipeline.mjs` | `router-parallel-analyze.mjs` as a black-box subprocess against a synthesized JSONL fixture |\n| `test-pipeline-roundtrip.mjs` | `oia-audit` → `audit-trend` e2e chain |\n| `test-with-openrouter.mjs` | Real e2e against OpenRouter (costs money, needs GCP secret) |\n| `smoke.sh` | Structural grep + CLI subcommand/`--help` registration for everything |\n\n## 1. Untested functions/classes\n\n**`_invoke.mjs` — zero direct unit tests.** This is the highest-value gap: it's the extracted shared plumbing (per its own header, \"converged security/perf/arch review\") used by ~15 scripts, made of small pure/near-pure functions, yet no test imports it directly — only indirectly through subprocess-level e2e tests that need npm/network.\n- `classifyDegraded(stderr, exitCode, reasonPrefix)` — untested\n- `injectJson(args, wantJson)` — untested\n- `parseTrailingJson(stdout)` — untested (this one **replaced a live bug** per the comment — the exact kind of function that regresses silently without a unit test)\n- `satisfiesTildeRange(version, pinVersion)` — untested\n- `findLocalPackageDir(pkg, pinVersion)` — untested (env-seam `RUFLO_METAHARNESS_SKIP_LOCAL` exists but nothing exercises it)\n- `cacheBaseDir()` — untested (env-seam `RUFLO_METAHARNESS_CACHE_BASE` ex",
67
+ "rawOutputLength": 16417
68
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
- "trajectoriesRecorded": 51,
3
- "patternsLearned": 51,
2
+ "trajectoriesRecorded": 131,
3
+ "patternsLearned": 131,
4
4
  "signalsProcessed": 0,
5
- "lastAdaptation": 1782427967363
5
+ "lastAdaptation": 1783606647775
6
6
  }
@@ -204,14 +204,27 @@ LOADER="$ROOT/../../v3/@claude-flow/cli/src/commands/index.ts"
204
204
  grep -q "metaharness: () => import" "$LOADER" 2>/dev/null || miss="$miss not-registered-in-loader"
205
205
  [[ -z "$miss" ]] && ok || bad "$miss"
206
206
 
207
- step "16. ruflo wrapper has metaharness in optionalDependencies (architectural constraint #2)"
207
+ step "16. ruflo wrapper keeps metaharness out of hard dependencies (ADR-150 constraint #2)"
208
208
  F="$ROOT/../../ruflo/package.json"
209
+ # ADR-150 §"Architectural constraint" rule #2 states that when @metaharness/*
210
+ # or `metaharness` appears in a package's dep graph, it MUST be in
211
+ # `optionalDependencies` — never in `dependencies`. It does NOT require
212
+ # ruflo to depend on metaharness at all. Historically this smoke asserted
213
+ # the presence of metaharness in ruflo optionalDeps, which conflicted with
214
+ # the #2561 "guard" budget (RUFLO_MAX=0 for optionalDependencies) that was
215
+ # added to protect the cold `npx -y ruflo@alpha --version` startup path
216
+ # from a heavy optional-dep install. #2561 is stronger evidence: metaharness
217
+ # is on its FORBIDDEN list because installing it during ruflo's postinstall
218
+ # caused a measured `npx --version` timeout. Correct reading of ADR-150 #2:
219
+ # if metaharness IS present anywhere in ruflo's deps, it MUST be optional;
220
+ # absence is fine (ruflo delegates to @claude-flow/cli, which itself keeps
221
+ # metaharness as a runtime dynamic import via ADR-150 rule #1).
209
222
  node -e "
210
223
  const j = JSON.parse(require('fs').readFileSync('$F','utf-8'));
211
- const od = j.optionalDependencies || {};
212
- if (!od.metaharness) { console.error('missing metaharness in optionalDependencies'); process.exit(1); }
213
- if (j.dependencies && j.dependencies.metaharness) { console.error('metaharness leaked into dependencies'); process.exit(1); }
214
- " 2>/dev/null && ok || bad "ruflo wrapper missing metaharness optionalDep"
224
+ const deps = j.dependencies || {};
225
+ const forbidden = Object.keys(deps).filter(k => k === 'metaharness' || k.startsWith('@metaharness/'));
226
+ if (forbidden.length) { console.error('ADR-150 rule #2 violated: ' + forbidden.join(', ') + ' must be optionalDependencies, not dependencies'); process.exit(1); }
227
+ " 2>/dev/null && ok || bad "metaharness leaked into ruflo wrapper hard dependencies"
215
228
 
216
229
  step "17r. _harness.mjs pinned-version + no-@latest regression guard (supersedes iter 27)"
217
230
  F="$ROOT/scripts/_harness.mjs"