@claude-flow/cli 3.16.3 → 3.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/metaharness.d.ts.map +1 -1
- package/dist/src/commands/metaharness.js +9 -3
- package/dist/src/commands/metaharness.js.map +1 -1
- package/dist/src/mcp-tools/agentdb-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/agentdb-tools.js +52 -1
- package/dist/src/mcp-tools/agentdb-tools.js.map +1 -1
- package/dist/src/mcp-tools/browser-session-tools.d.ts +5 -1
- package/dist/src/mcp-tools/browser-session-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/browser-session-tools.js +92 -18
- package/dist/src/mcp-tools/browser-session-tools.js.map +1 -1
- package/dist/src/mcp-tools/hooks-tools.d.ts +1 -0
- package/dist/src/mcp-tools/hooks-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/hooks-tools.js +131 -0
- package/dist/src/mcp-tools/hooks-tools.js.map +1 -1
- package/dist/src/mcp-tools/metaharness-tools.d.ts +7 -0
- package/dist/src/mcp-tools/metaharness-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/metaharness-tools.js +89 -0
- package/dist/src/mcp-tools/metaharness-tools.js.map +1 -1
- package/dist/src/mcp-tools/neural-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/neural-tools.js +119 -84
- package/dist/src/mcp-tools/neural-tools.js.map +1 -1
- package/dist/src/memory/memory-bridge.d.ts +15 -2
- package/dist/src/memory/memory-bridge.d.ts.map +1 -1
- package/dist/src/memory/memory-bridge.js +64 -10
- package/dist/src/memory/memory-bridge.js.map +1 -1
- package/dist/src/ruvector/output-verifier.d.ts +83 -0
- package/dist/src/ruvector/output-verifier.d.ts.map +1 -0
- package/dist/src/ruvector/output-verifier.js +277 -0
- package/dist/src/ruvector/output-verifier.js.map +1 -0
- package/dist/src/ruvector/trajectory-tree.d.ts +113 -0
- package/dist/src/ruvector/trajectory-tree.d.ts.map +1 -0
- package/dist/src/ruvector/trajectory-tree.js +237 -0
- package/dist/src/ruvector/trajectory-tree.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/plugins/ruflo-metaharness/README.md +2 -0
- package/plugins/ruflo-metaharness/agents/metaharness-architect.md +1 -1
- package/plugins/ruflo-metaharness/commands/ruflo-metaharness.md +5 -3
- package/plugins/ruflo-metaharness/scripts/_darwin.mjs +51 -41
- package/plugins/ruflo-metaharness/scripts/_harness.mjs +173 -104
- package/plugins/ruflo-metaharness/scripts/_invoke.mjs +231 -0
- package/plugins/ruflo-metaharness/scripts/_redblue.mjs +37 -70
- package/plugins/ruflo-metaharness/scripts/evolve.mjs +163 -3
- package/plugins/ruflo-metaharness/scripts/gepa.mjs +153 -0
- package/plugins/ruflo-metaharness/scripts/learn.mjs +127 -0
- package/plugins/ruflo-metaharness/scripts/smoke.sh +107 -51
- package/plugins/ruflo-metaharness/scripts/test-graceful-degradation.mjs +16 -0
- package/plugins/ruflo-metaharness/scripts/test-mcp-tools.mjs +24 -5
- package/plugins/ruflo-metaharness/skills/harness-evolve/SKILL.md +42 -3
- package/plugins/ruflo-metaharness/skills/harness-gepa/SKILL.md +65 -0
- package/plugins/ruflo-metaharness/skills/harness-learn/SKILL.md +65 -0
- package/plugins/ruflo-metaharness/skills/harness-mcp-scan/SKILL.md +3 -1
- package/plugins/ruflo-metaharness/skills/harness-score/SKILL.md +1 -1
- package/plugins/ruflo-metaharness/skills/harness-security-bench/SKILL.md +1 -1
- package/plugins/ruflo-metaharness/skills/harness-threat-model/SKILL.md +3 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// _invoke.mjs — shared subprocess/plumbing layer for the metaharness plugin family.
|
|
2
|
+
//
|
|
3
|
+
// Extracted from the copy-pasted plumbing that had accreted across
|
|
4
|
+
// _harness.mjs / _darwin.mjs / _redblue.mjs / gepa.mjs (converged
|
|
5
|
+
// security/perf/arch review, 2026-07). The four invocation helpers are now
|
|
6
|
+
// thin adapters on top of this module; their exported function signatures
|
|
7
|
+
// are unchanged (~15 scripts import them).
|
|
8
|
+
//
|
|
9
|
+
// WHAT LIVES HERE (the genuinely-shared parts):
|
|
10
|
+
// - DEGRADED_RX one superset regex (incl. `npm ERR` — previously
|
|
11
|
+
// only _redblue had it) for "upstream unavailable"
|
|
12
|
+
// - classifyDegraded() timeout vs not-available distinction, per-package
|
|
13
|
+
// reason prefix
|
|
14
|
+
// - injectJson() append --json unless caller opted out / present
|
|
15
|
+
// - parseTrailingJson() LAST-{...}-block extraction (the _darwin variant;
|
|
16
|
+
// the old _harness FIRST-block variant was a live
|
|
17
|
+
// bug — a progress line containing `{...}` would
|
|
18
|
+
// shadow the final structured result)
|
|
19
|
+
// - ensureCachedInstall() one-time `npm install --prefix ~/.ruflo/<name>-cache-<pin>`
|
|
20
|
+
// of a PINNED range — generalizes _redblue.mjs /
|
|
21
|
+
// gepa.mjs; the versioned dir means pin bumps
|
|
22
|
+
// invalidate stale caches automatically
|
|
23
|
+
// - findLocalPackageDir() walk-up node_modules resolution so an already
|
|
24
|
+
// installed optionalDependency is used for free
|
|
25
|
+
// - importOptionalLibrary() bare-import → cached-install fallback for
|
|
26
|
+
// library entries (gepa) — never throws on absence
|
|
27
|
+
// - makeDegradedEmitter() the ADR-150 rule-#3 exit-0 degraded payload
|
|
28
|
+
//
|
|
29
|
+
// WHAT DOES NOT LIVE HERE (the per-consumer parts):
|
|
30
|
+
// - _redblue's node-direct isMain workaround rationale
|
|
31
|
+
// - _darwin's async streaming (`onProgress`) for long evolve runs
|
|
32
|
+
// - _harness's dual-binary (metaharness + harness) resolution
|
|
33
|
+
//
|
|
34
|
+
// TEST SEAMS (used by test-graceful-degradation.mjs so the ADR-150 drill
|
|
35
|
+
// stays meaningful on machines with a warm cache / local install):
|
|
36
|
+
// - RUFLO_METAHARNESS_CACHE_BASE overrides ~/.ruflo as the cache root
|
|
37
|
+
// - RUFLO_METAHARNESS_SKIP_LOCAL=1 disables local node_modules resolution
|
|
38
|
+
|
|
39
|
+
import { spawnSync } from 'node:child_process';
|
|
40
|
+
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
41
|
+
import { homedir } from 'node:os';
|
|
42
|
+
import { dirname, join } from 'node:path';
|
|
43
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
44
|
+
|
|
45
|
+
const INSTALL_TIMEOUT_MS = 180_000; // npm install can be slow on cold cache
|
|
46
|
+
|
|
47
|
+
// Superset of the three per-file regexes. `npm ERR` (from _redblue) is
|
|
48
|
+
// included for everyone: an npm-level failure during a cached install or an
|
|
49
|
+
// npx shim means "upstream unavailable", never a ruflo bug.
|
|
50
|
+
export const DEGRADED_RX = /could not determine executable|404|not installed|MODULE_NOT_FOUND|ENOTFOUND|getaddrinfo|ECONNREFUSED|ETIMEDOUT|npm ERR/i;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Classify a finished subprocess as degraded or healthy.
|
|
54
|
+
* `exitCode === null` means the harness killed it (timeout) — that is a
|
|
55
|
+
* DIFFERENT operational signal from "package not installable", so the two
|
|
56
|
+
* reasons stay distinct: `<prefix>-timeout` vs `<prefix>-not-available`.
|
|
57
|
+
*/
|
|
58
|
+
export function classifyDegraded(stderr, exitCode, reasonPrefix) {
|
|
59
|
+
if (exitCode === null) return { degraded: true, reason: `${reasonPrefix}-timeout` };
|
|
60
|
+
if (DEGRADED_RX.test(stderr || '')) return { degraded: true, reason: `${reasonPrefix}-not-available` };
|
|
61
|
+
return { degraded: false };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Append --json unless the caller opted out or already passed it. */
|
|
65
|
+
export function injectJson(args, wantJson) {
|
|
66
|
+
if (!wantJson || args.includes('--json')) return [...args];
|
|
67
|
+
return [...args, '--json'];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parse the trailing JSON object from mixed stdout. CLIs in this family emit
|
|
72
|
+
* human-readable progress first and a structured JSON object LAST — so grab
|
|
73
|
+
* the last parseable {...} block, falling back to a greedy whole-span match
|
|
74
|
+
* for nested objects the lazy regex fragments.
|
|
75
|
+
*/
|
|
76
|
+
export function parseTrailingJson(stdout) {
|
|
77
|
+
const s = stdout || '';
|
|
78
|
+
const matches = [...s.matchAll(/\{[\s\S]*?\}/g)];
|
|
79
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
80
|
+
try { return JSON.parse(matches[i][0]); } catch { /* try previous */ }
|
|
81
|
+
}
|
|
82
|
+
const greedy = /\{[\s\S]*\}/.exec(s);
|
|
83
|
+
if (greedy) { try { return JSON.parse(greedy[0]); } catch { return null; } }
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Cache root — ~/.ruflo unless the test seam overrides it. */
|
|
88
|
+
export function cacheBaseDir() {
|
|
89
|
+
return process.env.RUFLO_METAHARNESS_CACHE_BASE || join(homedir(), '.ruflo');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Minimal `~X.Y.Z` satisfaction check (no semver dep): same major.minor, patch >= Z. */
|
|
93
|
+
export function satisfiesTildeRange(version, pinVersion) {
|
|
94
|
+
const pin = /^~(\d+)\.(\d+)\.(\d+)/.exec(String(pinVersion));
|
|
95
|
+
const ver = /^(\d+)\.(\d+)\.(\d+)/.exec(String(version));
|
|
96
|
+
if (!pin || !ver) return false;
|
|
97
|
+
return ver[1] === pin[1] && ver[2] === pin[2] && parseInt(ver[3], 10) >= parseInt(pin[3], 10);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Walk up node_modules from this plugin's directory AND from $CWD looking
|
|
102
|
+
* for an already-installed copy of `pkg` (e.g. the optionalDependency the
|
|
103
|
+
* user's ruflo install shipped with). Returns the package dir or null.
|
|
104
|
+
* When `pinVersion` is given, only a copy satisfying the pin is accepted —
|
|
105
|
+
* a stale major/minor in an ancestor node_modules is skipped, not used.
|
|
106
|
+
*/
|
|
107
|
+
export function findLocalPackageDir(pkg, pinVersion) {
|
|
108
|
+
if (process.env.RUFLO_METAHARNESS_SKIP_LOCAL === '1') return null;
|
|
109
|
+
const segments = pkg.split('/');
|
|
110
|
+
const starts = [dirname(fileURLToPath(import.meta.url)), process.cwd()];
|
|
111
|
+
const seen = new Set();
|
|
112
|
+
for (const start of starts) {
|
|
113
|
+
let dir = start;
|
|
114
|
+
for (;;) {
|
|
115
|
+
if (!seen.has(dir)) {
|
|
116
|
+
seen.add(dir);
|
|
117
|
+
const candidate = join(dir, 'node_modules', ...segments);
|
|
118
|
+
const pj = join(candidate, 'package.json');
|
|
119
|
+
if (existsSync(pj)) {
|
|
120
|
+
if (!pinVersion) return candidate;
|
|
121
|
+
try {
|
|
122
|
+
const version = JSON.parse(readFileSync(pj, 'utf-8')).version;
|
|
123
|
+
if (satisfiesTildeRange(version, pinVersion)) return candidate;
|
|
124
|
+
} catch { /* unreadable — keep walking */ }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const parent = dirname(dir);
|
|
128
|
+
if (parent === dir) break;
|
|
129
|
+
dir = parent;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* One-time versioned cache install of a PINNED range. Generalizes
|
|
137
|
+
* gepa.mjs:98-121 / _redblue.mjs:72-102. The cache dir is
|
|
138
|
+
* `<base>/<shortname>-cache-<pin-digits>` (e.g. redblue-cache-0.1.4,
|
|
139
|
+
* darwin-cache-0.8.0, metaharness-cache-0.3.0) so existing caches created
|
|
140
|
+
* by the pre-consolidation helpers remain valid, and bumping the pin
|
|
141
|
+
* invalidates stale installs automatically.
|
|
142
|
+
*
|
|
143
|
+
* @param {object} spec
|
|
144
|
+
* @param {string} spec.pkg npm package name (may be scoped)
|
|
145
|
+
* @param {string} spec.pinVersion tilde range, e.g. '~0.3.0'
|
|
146
|
+
* @param {string} [spec.cliRelPath] path inside the package that must exist
|
|
147
|
+
* post-install (also returned as cliPath)
|
|
148
|
+
* @param {number} [spec.timeoutMs] install timeout (default 180s)
|
|
149
|
+
* @returns {{ok:true, cacheDir:string, pkgDir:string, cliPath:string|null} |
|
|
150
|
+
* {ok:false, reason:string, stderr?:string, stdout?:string, error?:string}}
|
|
151
|
+
*/
|
|
152
|
+
export function ensureCachedInstall({ pkg, pinVersion, cliRelPath, timeoutMs = INSTALL_TIMEOUT_MS }) {
|
|
153
|
+
const short = pkg.split('/').pop();
|
|
154
|
+
const cacheDir = join(cacheBaseDir(), `${short}-cache-${pinVersion.replace(/[~^]/g, '')}`);
|
|
155
|
+
const pkgDir = join(cacheDir, 'node_modules', ...pkg.split('/'));
|
|
156
|
+
const cliPath = cliRelPath ? join(pkgDir, cliRelPath) : null;
|
|
157
|
+
const probe = cliPath ?? join(pkgDir, 'package.json');
|
|
158
|
+
if (existsSync(probe)) return { ok: true, cacheDir, pkgDir, cliPath };
|
|
159
|
+
try {
|
|
160
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
161
|
+
} catch (e) {
|
|
162
|
+
return { ok: false, reason: 'cache-dir-create-failed', error: String(e) };
|
|
163
|
+
}
|
|
164
|
+
// `npm install --prefix` puts the package in a known location; this both
|
|
165
|
+
// avoids npx's symlinked bin shim (the _redblue isMain upstream bug) and
|
|
166
|
+
// removes the per-call registry check that `npx -y pkg@latest` forced.
|
|
167
|
+
// shell:false; argv only.
|
|
168
|
+
const r = spawnSync('npm', [
|
|
169
|
+
'install',
|
|
170
|
+
'--no-audit', '--no-fund', '--no-package-lock',
|
|
171
|
+
'--prefix', cacheDir,
|
|
172
|
+
`${pkg}@${pinVersion}`,
|
|
173
|
+
], {
|
|
174
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
175
|
+
encoding: 'utf-8',
|
|
176
|
+
timeout: timeoutMs,
|
|
177
|
+
shell: process.platform === 'win32',
|
|
178
|
+
});
|
|
179
|
+
if (r.status !== 0 || !existsSync(probe)) {
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
reason: 'install-failed',
|
|
183
|
+
stderr: (r.stderr || '').slice(0, 600),
|
|
184
|
+
stdout: (r.stdout || '').slice(0, 600),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return { ok: true, cacheDir, pkgDir, cliPath };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Import an optional LIBRARY entry (as opposed to spawning a CLI):
|
|
192
|
+
* 1. bare `import(specifier)` — free when the optional dep is installed in
|
|
193
|
+
* an ancestor node_modules;
|
|
194
|
+
* 2. versioned cache install + file-URL import of `entryRelPath`.
|
|
195
|
+
* Returns the module namespace or null. Never throws on absence / stale
|
|
196
|
+
* installs (ERR_PACKAGE_PATH_NOT_EXPORTED covers pre-subpath versions).
|
|
197
|
+
*/
|
|
198
|
+
export async function importOptionalLibrary({ specifier, pkg, pinVersion, entryRelPath }) {
|
|
199
|
+
try {
|
|
200
|
+
return await import(specifier);
|
|
201
|
+
} catch (e) {
|
|
202
|
+
const msg = String(e?.message ?? e);
|
|
203
|
+
const recoverable = /Cannot find (module|package)|ERR_MODULE_NOT_FOUND|MODULE_NOT_FOUND|ERR_PACKAGE_PATH_NOT_EXPORTED|is not defined by "exports"/i;
|
|
204
|
+
if (!recoverable.test(msg)) throw e;
|
|
205
|
+
}
|
|
206
|
+
const r = ensureCachedInstall({ pkg, pinVersion, cliRelPath: entryRelPath });
|
|
207
|
+
if (!r.ok || !r.cliPath) return null;
|
|
208
|
+
try {
|
|
209
|
+
return await import(pathToFileURL(r.cliPath).href);
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Build the per-package `emit*DegradedJsonAndExit(reason)` helper. Emits the
|
|
217
|
+
* structured degraded payload and exits 0 — ADR-150 architectural constraint
|
|
218
|
+
* rule #3: ruflo continues to function when MetaHarness is absent.
|
|
219
|
+
*/
|
|
220
|
+
export function makeDegradedEmitter(pkg, pinVersion) {
|
|
221
|
+
return function emitDegradedAndExit(reason) {
|
|
222
|
+
const payload = {
|
|
223
|
+
degraded: true,
|
|
224
|
+
reason,
|
|
225
|
+
hint: `Install with \`npm i -D ${pkg}@${pinVersion}\` (pinned range — this plugin never fetches @latest) or verify network access for the one-time cache install.`,
|
|
226
|
+
generatedAt: new Date().toISOString(),
|
|
227
|
+
};
|
|
228
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
229
|
+
process.exit(0);
|
|
230
|
+
};
|
|
231
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// _redblue.mjs — invocation helper for `@metaharness/redblue`.
|
|
2
2
|
//
|
|
3
3
|
// Sibling of `_darwin.mjs` / `_harness.mjs`. Targets the `redblue` binary
|
|
4
|
-
// from the standalone `@metaharness/redblue@~0.1.
|
|
4
|
+
// from the standalone `@metaharness/redblue@~0.1.4` package. Shared plumbing
|
|
5
|
+
// (degraded classification, versioned cache install, degraded emitter) lives
|
|
6
|
+
// in `_invoke.mjs`; what stays here is the genuinely-redblue part: the
|
|
7
|
+
// node-direct invocation rationale below.
|
|
5
8
|
//
|
|
6
9
|
// Subcommands surfaced (matches redblue 0.1.x):
|
|
7
10
|
// - `redblue init [--out redblue.yaml]`
|
|
@@ -21,12 +24,13 @@
|
|
|
21
24
|
// no error. Darwin's CLI doesn't suffer this (calls main() unconditionally).
|
|
22
25
|
//
|
|
23
26
|
// Workaround: install the package once into a ruflo-owned cache dir
|
|
24
|
-
// (~/.ruflo/redblue-cache) and invoke
|
|
25
|
-
// directly. argv[1] then equals the real
|
|
27
|
+
// (~/.ruflo/redblue-cache-<pin>, via _invoke.ensureCachedInstall) and invoke
|
|
28
|
+
// `node <abs_path>/dist/cli/index.js` directly. argv[1] then equals the real
|
|
29
|
+
// path and isMain becomes true.
|
|
26
30
|
//
|
|
27
31
|
// Track upstream fix at: github.com/ruvnet/agent-harness-generator/issues
|
|
28
|
-
// (file separately — when fixed, we
|
|
29
|
-
//
|
|
32
|
+
// (file separately — when fixed, we could go back to a bin-shim pattern,
|
|
33
|
+
// though the node-direct cached path is now the family-wide standard anyway).
|
|
30
34
|
//
|
|
31
35
|
// CONTRACT (matches runMetaharness/runDarwin):
|
|
32
36
|
// - returns `{ stdout, stderr, exitCode, durationMs, degraded, reason? }`
|
|
@@ -43,63 +47,29 @@
|
|
|
43
47
|
// $OPENROUTER_API_KEY which we never inject.
|
|
44
48
|
|
|
45
49
|
import { spawnSync } from 'node:child_process';
|
|
46
|
-
import { existsSync, mkdirSync } from 'node:fs';
|
|
47
50
|
import { homedir } from 'node:os';
|
|
48
51
|
import { join } from 'node:path';
|
|
52
|
+
import {
|
|
53
|
+
classifyDegraded,
|
|
54
|
+
ensureCachedInstall,
|
|
55
|
+
makeDegradedEmitter,
|
|
56
|
+
} from './_invoke.mjs';
|
|
49
57
|
|
|
50
58
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
51
|
-
const INSTALL_TIMEOUT_MS = 180_000; // npm install can be slow on cold cache
|
|
52
59
|
|
|
53
60
|
// Pinned semver range. Bump in lock-step with optionalDependencies in
|
|
54
61
|
// @claude-flow/cli/package.json + ruflo/package.json.
|
|
55
|
-
const REDBLUE_PIN = '@metaharness/redblue@~0.1.1';
|
|
56
62
|
const REDBLUE_PKG = '@metaharness/redblue';
|
|
57
|
-
const REDBLUE_PIN_VERSION = '~0.1.
|
|
63
|
+
const REDBLUE_PIN_VERSION = '~0.1.4';
|
|
64
|
+
const REDBLUE_PIN = `${REDBLUE_PKG}@${REDBLUE_PIN_VERSION}`;
|
|
58
65
|
|
|
59
|
-
const
|
|
60
|
-
const RESOLVED_CLI = join(
|
|
61
|
-
CACHE_DIR, 'node_modules', '@metaharness', 'redblue', 'dist', 'cli', 'index.js',
|
|
62
|
-
);
|
|
66
|
+
const REASON_PREFIX = 'metaharness-redblue';
|
|
63
67
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
mkdirSync(CACHE_DIR, { recursive: true });
|
|
70
|
-
} catch (e) {
|
|
71
|
-
return { ok: false, reason: 'cache-dir-create-failed', error: String(e) };
|
|
72
|
-
}
|
|
73
|
-
// Use `npm install --prefix` so the package lands in a known location;
|
|
74
|
-
// this avoids npx's symlinked shim entirely (the upstream bin-bootstrap bug).
|
|
75
|
-
// shell:false; argv only.
|
|
76
|
-
const r = spawnSync('npm', [
|
|
77
|
-
'install',
|
|
78
|
-
'--no-audit', '--no-fund', '--no-package-lock',
|
|
79
|
-
'--prefix', CACHE_DIR,
|
|
80
|
-
`${REDBLUE_PKG}@${REDBLUE_PIN_VERSION}`,
|
|
81
|
-
], {
|
|
82
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
83
|
-
encoding: 'utf-8',
|
|
84
|
-
timeout: INSTALL_TIMEOUT_MS,
|
|
85
|
-
shell: process.platform === 'win32',
|
|
86
|
-
});
|
|
87
|
-
if (r.status !== 0 || !existsSync(RESOLVED_CLI)) {
|
|
88
|
-
return {
|
|
89
|
-
ok: false,
|
|
90
|
-
reason: 'install-failed',
|
|
91
|
-
stderr: (r.stderr || '').slice(0, 600),
|
|
92
|
-
stdout: (r.stdout || '').slice(0, 600),
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
return { ok: true };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function classifyDegraded(stderr, exitCode) {
|
|
99
|
-
if (exitCode === null) return { degraded: true, reason: 'metaharness-redblue-timeout' };
|
|
100
|
-
if (DEGRADED_RX.test(stderr)) return { degraded: true, reason: 'metaharness-redblue-not-available' };
|
|
101
|
-
return { degraded: false };
|
|
102
|
-
}
|
|
68
|
+
// Cache dir is versioned by the pin so bumping REDBLUE_PIN_VERSION
|
|
69
|
+
// invalidates stale installs (handled inside ensureCachedInstall). The
|
|
70
|
+
// resolved layout matches the pre-consolidation helper so existing caches
|
|
71
|
+
// keep serving.
|
|
72
|
+
const CLI_REL_PATH = join('dist', 'cli', 'index.js');
|
|
103
73
|
|
|
104
74
|
/**
|
|
105
75
|
* Sync invocation. `redblue` runs are bounded by `max_cost_usd` and
|
|
@@ -111,7 +81,11 @@ export function runRedblue(args, opts = {}) {
|
|
|
111
81
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
112
82
|
|
|
113
83
|
// 1. Ensure redblue is installed in our cache dir (one-time install).
|
|
114
|
-
const install =
|
|
84
|
+
const install = ensureCachedInstall({
|
|
85
|
+
pkg: REDBLUE_PKG,
|
|
86
|
+
pinVersion: REDBLUE_PIN_VERSION,
|
|
87
|
+
cliRelPath: CLI_REL_PATH,
|
|
88
|
+
});
|
|
115
89
|
if (!install.ok) {
|
|
116
90
|
return {
|
|
117
91
|
stdout: install.stdout ?? '',
|
|
@@ -120,14 +94,14 @@ export function runRedblue(args, opts = {}) {
|
|
|
120
94
|
durationMs: Date.now() - start,
|
|
121
95
|
degraded: true,
|
|
122
96
|
reason: install.reason === 'install-failed'
|
|
123
|
-
?
|
|
124
|
-
:
|
|
97
|
+
? `${REASON_PREFIX}-not-available`
|
|
98
|
+
: `${REASON_PREFIX}-${install.reason}`,
|
|
125
99
|
};
|
|
126
100
|
}
|
|
127
101
|
|
|
128
102
|
// 2. Invoke `node <real-path-to-cli> <args>` so upstream's isMain check
|
|
129
103
|
// succeeds (argv[1] matches import.meta.url).
|
|
130
|
-
const r = spawnSync('node', [
|
|
104
|
+
const r = spawnSync('node', [install.cliPath, ...args], {
|
|
131
105
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
132
106
|
encoding: 'utf-8',
|
|
133
107
|
timeout: timeoutMs,
|
|
@@ -138,7 +112,7 @@ export function runRedblue(args, opts = {}) {
|
|
|
138
112
|
const durationMs = Date.now() - start;
|
|
139
113
|
const stdout = r.stdout || '';
|
|
140
114
|
const stderr = r.stderr || '';
|
|
141
|
-
const classified = classifyDegraded(stderr, r.status);
|
|
115
|
+
const classified = classifyDegraded(stderr, r.status, REASON_PREFIX);
|
|
142
116
|
if (classified.degraded) {
|
|
143
117
|
return {
|
|
144
118
|
stdout, stderr,
|
|
@@ -158,19 +132,12 @@ export function runRedblue(args, opts = {}) {
|
|
|
158
132
|
|
|
159
133
|
/**
|
|
160
134
|
* Match the existing `emit*DegradedJsonAndExit` shape used by sibling
|
|
161
|
-
* scripts so MCP tool consumers see one contract.
|
|
135
|
+
* scripts so MCP tool consumers see one contract. Exit 0 — ADR-150
|
|
136
|
+
* architectural constraint #3.
|
|
162
137
|
*/
|
|
163
|
-
export
|
|
164
|
-
const payload = {
|
|
165
|
-
degraded: true,
|
|
166
|
-
reason,
|
|
167
|
-
hint: 'Install with `npm i -D ' + REDBLUE_PIN + '` or run `npx -y ' + REDBLUE_PIN + ' --help` to verify network access.',
|
|
168
|
-
generatedAt: new Date().toISOString(),
|
|
169
|
-
};
|
|
170
|
-
console.log(JSON.stringify(payload, null, 2));
|
|
171
|
-
// Exit 0 — ADR-150 architectural constraint #3.
|
|
172
|
-
process.exit(0);
|
|
173
|
-
}
|
|
138
|
+
export const emitRedblueDegradedJsonAndExit = makeDegradedEmitter(REDBLUE_PKG, REDBLUE_PIN_VERSION);
|
|
174
139
|
|
|
175
140
|
export const REDBLUE_VERSION_PIN = REDBLUE_PIN;
|
|
176
|
-
export const REDBLUE_CACHE_DIR =
|
|
141
|
+
export const REDBLUE_CACHE_DIR = join(
|
|
142
|
+
homedir(), '.ruflo', `redblue-cache-${REDBLUE_PIN_VERSION.replace(/[~^]/g, '')}`,
|
|
143
|
+
);
|
|
@@ -24,6 +24,23 @@
|
|
|
24
24
|
// node scripts/evolve.mjs --repo . --confirm --generations 5 --children 3
|
|
25
25
|
// node scripts/evolve.mjs --repo . --confirm --sandbox mock # no real tests
|
|
26
26
|
// node scripts/evolve.mjs --repo . --confirm --selection pareto
|
|
27
|
+
// node scripts/evolve.mjs --repo . --confirm --diagnose # + GEPA failure diagnosis
|
|
28
|
+
//
|
|
29
|
+
// --diagnose (GEPA failure diagnosis — the natural-language-diagnosis trick
|
|
30
|
+
// from GEPA, scoped modestly): after the evolve run completes, the losing /
|
|
31
|
+
// failed variants' transcripts are run through darwin's gepa
|
|
32
|
+
// `analyzeTranscript` + `classifyFailure` ops and a `diagnosis` section
|
|
33
|
+
// (failure classes + counts + dominant class per variant) is appended to the
|
|
34
|
+
// emitted JSON report. UPSTREAM SHAPE NOTE (verified against darwin 0.8.0):
|
|
35
|
+
// `metaharness-darwin evolve --json` emits a TEXT leaderboard on stdout — no
|
|
36
|
+
// JSON, no transcripts. The per-variant run records live at
|
|
37
|
+
// `<repo>/.metaharness/runs/<id>.json` as sandbox exec traces
|
|
38
|
+
// ({taskId, exitCode, stdout, stderr}), which are NOT gepa {actionRaw, obs}
|
|
39
|
+
// transcripts. So the diagnosis layer (a) uses gepa-shaped transcripts when
|
|
40
|
+
// a run record embeds them (agent-sandbox / future upstream), (b) falls back
|
|
41
|
+
// to the champion's transcript, and (c) otherwise emits
|
|
42
|
+
// `diagnosis: {available: false, reason, traceSummary}` with a mechanical
|
|
43
|
+
// per-variant trace summary — it NEVER fails the run.
|
|
27
44
|
//
|
|
28
45
|
// EXIT CODES
|
|
29
46
|
// 0 evolved OK (or dry-run, or degraded — MetaHarness Darwin not available)
|
|
@@ -31,9 +48,9 @@
|
|
|
31
48
|
// 2 config error or evolution failure
|
|
32
49
|
// 99 reserved — upstream "safety-disqualified" (propagated)
|
|
33
50
|
|
|
34
|
-
import { runDarwinAsync, emitDarwinDegradedJsonAndExit } from './_darwin.mjs';
|
|
35
|
-
import { existsSync } from 'node:fs';
|
|
36
|
-
import { resolve } from 'node:path';
|
|
51
|
+
import { runDarwinAsync, emitDarwinDegradedJsonAndExit, importGepa } from './_darwin.mjs';
|
|
52
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
53
|
+
import { join, resolve } from 'node:path';
|
|
37
54
|
|
|
38
55
|
const ARGS = (() => {
|
|
39
56
|
const a = {
|
|
@@ -56,6 +73,7 @@ const ARGS = (() => {
|
|
|
56
73
|
ruvllmModel: null,
|
|
57
74
|
confirm: false,
|
|
58
75
|
alertOnNoImprovement: false,
|
|
76
|
+
diagnose: false,
|
|
59
77
|
format: 'json',
|
|
60
78
|
timeoutMs: null, // computed below if unset
|
|
61
79
|
};
|
|
@@ -80,6 +98,7 @@ const ARGS = (() => {
|
|
|
80
98
|
else if (v === '--ruvllm-model') a.ruvllmModel = process.argv[++i];
|
|
81
99
|
else if (v === '--confirm') a.confirm = true;
|
|
82
100
|
else if (v === '--alert-on-no-improvement') a.alertOnNoImprovement = true;
|
|
101
|
+
else if (v === '--diagnose') a.diagnose = true;
|
|
83
102
|
else if (v === '--format') a.format = process.argv[++i];
|
|
84
103
|
else if (v === '--timeout-ms') a.timeoutMs = parseInt(process.argv[++i], 10);
|
|
85
104
|
}
|
|
@@ -119,6 +138,141 @@ function safetyChecks() {
|
|
|
119
138
|
return repoPath;
|
|
120
139
|
}
|
|
121
140
|
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// --diagnose support (GEPA failure diagnosis)
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
/** True when `arr` looks like a gepa transcript: [{actionRaw?, obs?}, ...]. */
|
|
146
|
+
function looksLikeGepaTranscript(arr) {
|
|
147
|
+
return Array.isArray(arr) && arr.length > 0 &&
|
|
148
|
+
arr.every((e) => e && typeof e === 'object' && ('actionRaw' in e || 'obs' in e));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Pull every gepa-shaped transcript out of one run record. Upstream darwin
|
|
153
|
+
* 0.8.0 run records carry sandbox exec traces (NOT gepa transcripts), but a
|
|
154
|
+
* record MAY embed `transcript` arrays (agent sandbox / future upstream) —
|
|
155
|
+
* accept `record.transcript`, `record.traces[i].transcript`, or `traces`
|
|
156
|
+
* itself when its entries are {actionRaw, obs}-shaped.
|
|
157
|
+
*/
|
|
158
|
+
function extractGepaTranscripts(record) {
|
|
159
|
+
const out = [];
|
|
160
|
+
if (looksLikeGepaTranscript(record?.transcript)) {
|
|
161
|
+
out.push({ taskId: null, exitCode: null, entries: record.transcript });
|
|
162
|
+
}
|
|
163
|
+
const traces = Array.isArray(record?.traces) ? record.traces : [];
|
|
164
|
+
if (looksLikeGepaTranscript(traces)) {
|
|
165
|
+
out.push({ taskId: null, exitCode: null, entries: traces });
|
|
166
|
+
} else {
|
|
167
|
+
for (const t of traces) {
|
|
168
|
+
if (looksLikeGepaTranscript(t?.transcript)) {
|
|
169
|
+
out.push({ taskId: t.taskId ?? null, exitCode: t.exitCode ?? null, entries: t.transcript });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Mechanical per-variant summary of sandbox exec traces (the always-available fallback signal). */
|
|
177
|
+
function summarizeTraces(record) {
|
|
178
|
+
const traces = Array.isArray(record?.traces) ? record.traces : [];
|
|
179
|
+
return {
|
|
180
|
+
tasks: traces.length,
|
|
181
|
+
failed: traces.filter((t) => t?.exitCode !== 0).length,
|
|
182
|
+
timedOut: traces.filter((t) => t?.timedOut === true).length,
|
|
183
|
+
blockedActions: traces.reduce((n, t) => n + (Array.isArray(t?.blockedActions) ? t.blockedActions.length : 0), 0),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Build the `diagnosis` section for the emitted report. Never throws and
|
|
189
|
+
* never affects the run's exit code — any internal failure degrades to
|
|
190
|
+
* `{available: false, reason}`.
|
|
191
|
+
*/
|
|
192
|
+
async function buildDiagnosis(repoPath) {
|
|
193
|
+
try {
|
|
194
|
+
const metaDir = join(repoPath, '.metaharness');
|
|
195
|
+
const runsDir = join(metaDir, 'runs');
|
|
196
|
+
if (!existsSync(runsDir)) {
|
|
197
|
+
return { available: false, reason: 'no-run-records: <repo>/.metaharness/runs does not exist' };
|
|
198
|
+
}
|
|
199
|
+
let winnerId = null;
|
|
200
|
+
try {
|
|
201
|
+
winnerId = JSON.parse(readFileSync(join(metaDir, 'reports', 'winner.json'), 'utf-8'))?.variant?.id ?? null;
|
|
202
|
+
} catch { /* winner unknown — treat all variants as candidates */ }
|
|
203
|
+
|
|
204
|
+
const records = [];
|
|
205
|
+
for (const f of readdirSync(runsDir).filter((f) => f.endsWith('.json')).slice(0, 100)) {
|
|
206
|
+
try {
|
|
207
|
+
records.push({ id: f.replace(/\.json$/, ''), rec: JSON.parse(readFileSync(join(runsDir, f), 'utf-8')) });
|
|
208
|
+
} catch { /* skip unreadable record */ }
|
|
209
|
+
}
|
|
210
|
+
if (records.length === 0) {
|
|
211
|
+
return { available: false, reason: 'no-run-records: <repo>/.metaharness/runs contains no parseable records' };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Losing/failed variants are the primary diagnosis targets; the champion
|
|
215
|
+
// is the fallback when no loser exposes a transcript.
|
|
216
|
+
const losers = records.filter((r) =>
|
|
217
|
+
r.id !== winnerId &&
|
|
218
|
+
(r.rec?.score?.promoted === false ||
|
|
219
|
+
(Array.isArray(r.rec?.traces) && r.rec.traces.some((t) => t?.exitCode !== 0 || t?.timedOut))));
|
|
220
|
+
const champion = records.filter((r) => r.id === winnerId);
|
|
221
|
+
|
|
222
|
+
let scope = 'losing-variants';
|
|
223
|
+
let pool = losers.map((r) => ({ ...r, transcripts: extractGepaTranscripts(r.rec) }))
|
|
224
|
+
.filter((r) => r.transcripts.length > 0);
|
|
225
|
+
if (pool.length === 0) {
|
|
226
|
+
scope = 'champion-fallback';
|
|
227
|
+
pool = champion.map((r) => ({ ...r, transcripts: extractGepaTranscripts(r.rec) }))
|
|
228
|
+
.filter((r) => r.transcripts.length > 0);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (pool.length === 0) {
|
|
232
|
+
// Verified upstream shape (darwin 0.8.0): run records are sandbox exec
|
|
233
|
+
// traces, not gepa transcripts. Emit the mechanical trace summary so
|
|
234
|
+
// --diagnose still yields signal.
|
|
235
|
+
const target = losers.length ? losers : records;
|
|
236
|
+
return {
|
|
237
|
+
available: false,
|
|
238
|
+
reason: 'no-gepa-transcripts: run records contain sandbox exec traces ({taskId, exitCode, stdout, stderr}), not gepa {actionRaw, obs} transcripts',
|
|
239
|
+
traceSummary: Object.fromEntries(target.map((r) => [r.id, summarizeTraces(r.rec)])),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const gepa = await importGepa();
|
|
244
|
+
if (!gepa || typeof gepa.analyzeTranscript !== 'function') {
|
|
245
|
+
return { available: false, reason: 'metaharness-darwin-gepa-not-available' };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const classLabel = (n) => {
|
|
249
|
+
const raw = gepa.FAILURE_CLASSES?.[n];
|
|
250
|
+
return raw ? String(raw).split(' (')[0] : `class-${n}`;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const totals = {};
|
|
254
|
+
const variants = [];
|
|
255
|
+
for (const r of pool) {
|
|
256
|
+
const classes = {};
|
|
257
|
+
for (const t of r.transcripts) {
|
|
258
|
+
const analysis = gepa.analyzeTranscript(t.entries);
|
|
259
|
+
const cls = typeof gepa.classifyFailure === 'function'
|
|
260
|
+
? gepa.classifyFailure({ goldResolved: t.exitCode === 0, analysis })
|
|
261
|
+
: -1;
|
|
262
|
+
const label = classLabel(cls);
|
|
263
|
+
classes[label] = (classes[label] || 0) + 1;
|
|
264
|
+
totals[label] = (totals[label] || 0) + 1;
|
|
265
|
+
}
|
|
266
|
+
const dominantClass = Object.entries(classes).sort((a, b) => b[1] - a[1])[0]?.[0] ?? null;
|
|
267
|
+
variants.push({ id: r.id, transcripts: r.transcripts.length, failureClasses: classes, dominantClass });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { available: true, scope, variants, totals };
|
|
271
|
+
} catch (e) {
|
|
272
|
+
return { available: false, reason: `diagnosis-failed: ${e?.message ?? e}` };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
122
276
|
// Compute a sensible timeout from the search shape if caller didn't specify.
|
|
123
277
|
// Rough budget: each variant ~= 30s (sandbox test command + safety inspect),
|
|
124
278
|
// total ~= generations × children × per-variant / concurrency.
|
|
@@ -146,6 +300,7 @@ async function main() {
|
|
|
146
300
|
epistasis: ARGS.epistasis,
|
|
147
301
|
curriculum: ARGS.curriculum,
|
|
148
302
|
mutator: ARGS.mutator,
|
|
303
|
+
diagnose: ARGS.diagnose,
|
|
149
304
|
estVariants: ARGS.generations * ARGS.children,
|
|
150
305
|
timeoutMs: ARGS.timeoutMs ?? defaultTimeoutMs(),
|
|
151
306
|
output: `${repoPath}/.metaharness/{archive.json, lineage.json, variants/, runs/, reports/winner.json}`,
|
|
@@ -233,6 +388,11 @@ async function main() {
|
|
|
233
388
|
generatedAt: new Date().toISOString(),
|
|
234
389
|
};
|
|
235
390
|
|
|
391
|
+
// GEPA failure diagnosis — opt-in, additive, never fails the run.
|
|
392
|
+
if (ARGS.diagnose) {
|
|
393
|
+
payload.data.diagnosis = await buildDiagnosis(repoPath);
|
|
394
|
+
}
|
|
395
|
+
|
|
236
396
|
console.log(JSON.stringify(payload, null, 2));
|
|
237
397
|
if (ARGS.alertOnNoImprovement && noImprovement) process.exit(1);
|
|
238
398
|
process.exit(0);
|