@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
@@ -0,0 +1,363 @@
1
+ #!/usr/bin/env node
2
+ // drift-from-history.mjs — iter 53 — one-command drift detection.
3
+ //
4
+ // Composes the existing pipeline into the workflow users actually want:
5
+ // "did my repo drift since the last audit?" Implementation:
6
+ //
7
+ // 1. List audit records from `metaharness-audit` memory namespace
8
+ // 2. Pick the most recent one (or filter --since Nd to skip recent ones
9
+ // and use an older baseline)
10
+ // 3. Run a fresh oia-audit against the current repo state
11
+ // 4. Diff via audit-trend → structural distance + severity rollup
12
+ // 5. Alert if structural similarity falls below --threshold
13
+ //
14
+ // Until this iter, doing this required the user to:
15
+ // $ npx ruflo metaharness audit-list --format json
16
+ // $ # ... pick a key by hand from the listing
17
+ // $ npx ruflo metaharness oia-audit --format json > /tmp/curr.json
18
+ // $ npx ruflo metaharness audit-trend --baseline-key X --current-file /tmp/curr.json
19
+ // Now it's:
20
+ // $ npx ruflo metaharness drift-from-history --threshold 0.95
21
+ //
22
+ // USAGE
23
+ // node scripts/drift-from-history.mjs # default: last record vs now
24
+ // node scripts/drift-from-history.mjs --baseline-since 7d # use an audit ≥ 7d old
25
+ // node scripts/drift-from-history.mjs --threshold 0.95 # alert under 0.95 similarity
26
+ // node scripts/drift-from-history.mjs --format json
27
+ // node scripts/drift-from-history.mjs --dry-run # don't persist current
28
+ //
29
+ // EXIT CODES
30
+ // 0 ok (no drift below --threshold)
31
+ // 1 drift below --threshold (alert fired)
32
+ // 2 config / input error (no history available, etc.)
33
+ // 3 upstream metaharness absent — degraded payload returned
34
+
35
+ import { spawnSync, spawn } from 'node:child_process';
36
+ // iter 78 — share the iter-63 SEVERITY_RANK for the new finding-alert gate
37
+ import { rankSeverity } from './_harness.mjs';
38
+ import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
39
+ import { tmpdir } from 'node:os';
40
+ import { join, dirname } from 'node:path';
41
+ import { fileURLToPath } from 'node:url';
42
+
43
+ const SCRIPTS_DIR = dirname(fileURLToPath(import.meta.url));
44
+ const NS = process.env.METAHARNESS_AUDIT_NAMESPACE || 'metaharness-audit';
45
+ const CLI_PKG = process.env.CLI_CORE === '1'
46
+ ? '@claude-flow/cli-core@alpha'
47
+ : '@claude-flow/cli@latest';
48
+
49
+ const ARGS = (() => {
50
+ const a = {
51
+ path: '.', baselineSince: null, baselineKey: null, baselineFile: null,
52
+ threshold: 0.95, dryRun: false, format: 'table',
53
+ // iter 78 — opt-in finding-severity alert gate. Default null (off)
54
+ // preserves iter-53 behavior; set to a severity name to ALSO alert
55
+ // when any introduced finding meets or exceeds that severity.
56
+ alertOnNewSeverity: null,
57
+ };
58
+ for (let i = 2; i < process.argv.length; i++) {
59
+ const v = process.argv[i];
60
+ if (v === '--path') a.path = process.argv[++i];
61
+ else if (v === '--baseline-since') a.baselineSince = process.argv[++i];
62
+ // iter 66 — explicit baseline-key skips the ONNX-heavy audit-list
63
+ // call entirely. Cron jobs that know the key avoid the ~25s warmup.
64
+ else if (v === '--baseline-key') a.baselineKey = process.argv[++i];
65
+ // iter 67 — baseline-file goes further: no memory call at all.
66
+ // Useful for CI workflows that store last-run audit as an artifact
67
+ // and download it for diff against fresh audit.
68
+ else if (v === '--baseline-file') a.baselineFile = process.argv[++i];
69
+ else if (v === '--threshold') a.threshold = Number(process.argv[++i]);
70
+ else if (v === '--dry-run') a.dryRun = true;
71
+ else if (v === '--format') a.format = process.argv[++i];
72
+ // iter 78 — orthogonal alert: any new finding ≥ this severity triggers
73
+ else if (v === '--alert-on-new-severity') a.alertOnNewSeverity = String(process.argv[++i]).toLowerCase();
74
+ }
75
+ return a;
76
+ })();
77
+
78
+ function emitAndExit(payload, code) {
79
+ if (ARGS.format === 'json') {
80
+ console.log(JSON.stringify(payload, null, 2));
81
+ } else {
82
+ console.log(`# drift-from-history\n`);
83
+ if (payload.degraded) {
84
+ console.log(`⊘ ${payload.reason}`);
85
+ } else if (payload.error) {
86
+ console.log(`✗ ${payload.error}`);
87
+ } else {
88
+ console.log(`Baseline: ${payload.baseline?.key ?? payload.baseline?.startedAt}`);
89
+ console.log(`Current: ${payload.current?.startedAt ?? '(fresh)'}`);
90
+ // iter 96 — surface the timing.path label so users see which
91
+ // fast-path ran (or didn't) at a glance.
92
+ if (payload.timing?.path) {
93
+ const wallMs = payload.timing.parallelWallMs;
94
+ console.log(`Path: ${payload.timing.path} (wall ${wallMs}ms)`);
95
+ }
96
+ console.log('');
97
+ const sd = payload.drift?.structuralDistance;
98
+ if (sd && sd.verdict !== 'unavailable') {
99
+ console.log(`Structural similarity: ${sd.overall} (${sd.verdict})`);
100
+ console.log(`Distance: ${sd.distance}`);
101
+ }
102
+ console.log('');
103
+ if (payload.alert?.triggered) {
104
+ console.log(`⚠ ALERT: ${payload.alert.reason}`);
105
+ } else if (payload.alert) {
106
+ console.log(`✓ ${payload.alert.reason}`);
107
+ }
108
+ }
109
+ }
110
+ process.exit(code);
111
+ }
112
+
113
+ function runScriptJson(script, args) {
114
+ const r = spawnSync('node', [join(SCRIPTS_DIR, script), ...args, '--format', 'json'], {
115
+ encoding: 'utf-8',
116
+ });
117
+ const m = /\{[\s\S]*\}/.exec(r.stdout || '');
118
+ const json = m ? JSON.parse(m[0]) : null;
119
+ // audit-list emits {records:[...]} — check that shape too
120
+ const arrM = /\[[\s\S]*\]/.exec(r.stdout || '');
121
+ return { json, exitCode: r.status ?? -1, stdout: r.stdout || '', stderr: r.stderr || '', arrMatch: arrM };
122
+ }
123
+
124
+ /**
125
+ * iter 58 — async variant. Drift-from-history's audit-list (memory query)
126
+ * and oia-audit (fresh subprocess chain) are mutually independent and
127
+ * can race. This shaves ~2-5s off every drift check by overlapping the
128
+ * subprocess wait time.
129
+ */
130
+ function runScriptJsonAsync(script, args) {
131
+ return new Promise((resolve) => {
132
+ const p = spawn('node', [join(SCRIPTS_DIR, script), ...args, '--format', 'json'], {
133
+ stdio: ['ignore', 'pipe', 'pipe'],
134
+ });
135
+ let stdout = '', stderr = '';
136
+ p.stdout?.on('data', (d) => { stdout += d.toString(); });
137
+ p.stderr?.on('data', (d) => { stderr += d.toString(); });
138
+ p.on('close', (code) => {
139
+ const m = /\{[\s\S]*\}/.exec(stdout);
140
+ let json = null;
141
+ if (m) { try { json = JSON.parse(m[0]); } catch { /* leave null */ } }
142
+ resolve({ json, exitCode: code ?? -1, stdout, stderr });
143
+ });
144
+ p.on('error', () => resolve({ json: null, exitCode: 127, stdout, stderr: 'spawn-failed' }));
145
+ });
146
+ }
147
+
148
+ async function main() {
149
+ // iter 58 — parallelize audit-list + oia-audit. They're mutually
150
+ // independent (audit-list queries memory, oia-audit runs fresh
151
+ // metaharness subprocesses) so racing them shaves ~2-5s off every
152
+ // drift check. The audit-trend step still serializes after because
153
+ // it consumes both prior outputs.
154
+ const listArgs = ['--limit', '50'];
155
+ if (ARGS.baselineSince) listArgs.push('--since', ARGS.baselineSince);
156
+ const auditArgs = ['--path', ARGS.path];
157
+ if (ARGS.dryRun) auditArgs.push('--dry-run');
158
+
159
+ // iter 65 — measure parallel batch wall-clock so a future iter that
160
+ // accidentally serializes (await audit-list; await oia-audit) doesn't
161
+ // silently regress. timing.parallelSpeedup is surfaced in payload.
162
+ // iter 66 — when --baseline-key is provided, skip audit-list entirely
163
+ // (it's ~25s of ONNX warmup for what would be one record lookup).
164
+ // Run oia-audit alone in that case.
165
+ const parallelStart = Date.now();
166
+ const listStart = Date.now();
167
+ const auditStart = Date.now();
168
+ let listResult;
169
+ let auditResult;
170
+ let skippedAuditList = false;
171
+ let usedBaselineFile = false;
172
+ if (ARGS.baselineFile) {
173
+ // iter 67 fast-fast path — baseline already on disk. Skip
174
+ // audit-list AND skip memory roundtrip when running audit-trend.
175
+ skippedAuditList = true;
176
+ usedBaselineFile = true;
177
+ listResult = {
178
+ json: { records: [{ key: `file:${ARGS.baselineFile}`, startedAt: null }] },
179
+ exitCode: 0, stdout: '', stderr: '', durationMs: 0,
180
+ };
181
+ auditResult = await runScriptJsonAsync('oia-audit.mjs', auditArgs);
182
+ auditResult.durationMs = Date.now() - auditStart;
183
+ } else if (ARGS.baselineKey) {
184
+ skippedAuditList = true;
185
+ // Synthesize a list result containing the user-provided key.
186
+ // No memory call needed — drift-from-history's downstream code
187
+ // only reads the `key` field from the picked record.
188
+ listResult = {
189
+ json: { records: [{ key: ARGS.baselineKey, startedAt: null }] },
190
+ exitCode: 0,
191
+ stdout: '',
192
+ stderr: '',
193
+ durationMs: 0,
194
+ };
195
+ auditResult = await runScriptJsonAsync('oia-audit.mjs', auditArgs);
196
+ auditResult.durationMs = Date.now() - auditStart;
197
+ } else {
198
+ [listResult, auditResult] = await Promise.all([
199
+ runScriptJsonAsync('audit-list.mjs', listArgs)
200
+ .then((r) => ({ ...r, durationMs: Date.now() - listStart })),
201
+ runScriptJsonAsync('oia-audit.mjs', auditArgs)
202
+ .then((r) => ({ ...r, durationMs: Date.now() - auditStart })),
203
+ ]);
204
+ }
205
+ const parallelWallMs = Date.now() - parallelStart;
206
+ const parallelSumMs = (listResult.durationMs || 0) + (auditResult.durationMs || 0);
207
+
208
+ if (listResult.exitCode !== 0) {
209
+ emitAndExit({
210
+ error: `audit-list failed (exit ${listResult.exitCode})`,
211
+ stderrTail: listResult.stderr.slice(-200),
212
+ }, 2);
213
+ }
214
+ const records = listResult.json?.records ?? listResult.json?.entries ?? [];
215
+ if (records.length === 0) {
216
+ // iter 57 + iter 58 — disambiguate "no history yet" (exit 2) from
217
+ // "metaharness absent" (exit 3). Use the audit we already ran
218
+ // (iter 58 fused the probe into the parallel batch) — no extra
219
+ // subprocess needed.
220
+ if (auditResult.json?.degraded === true) {
221
+ emitAndExit({
222
+ degraded: true,
223
+ reason: auditResult.json.reason || 'metaharness-not-available',
224
+ hint: 'Install metaharness to enable drift detection.',
225
+ }, 3);
226
+ }
227
+ emitAndExit({
228
+ error: 'no audit records found in namespace ' + NS,
229
+ hint: 'Run `ruflo metaharness oia-audit` at least once to seed history.',
230
+ }, 2);
231
+ }
232
+ // Pick the most recent record (records are typically newest-first; if not,
233
+ // sort by startedAt). Each record's key is in the entry.
234
+ const sorted = [...records].sort((a, b) =>
235
+ String(b.startedAt ?? b.key ?? '').localeCompare(String(a.startedAt ?? a.key ?? '')));
236
+ const baseline = sorted[0];
237
+ if (!baseline?.key) {
238
+ emitAndExit({
239
+ error: 'audit record has no `key` field — cannot reference',
240
+ sample: baseline,
241
+ }, 2);
242
+ }
243
+
244
+ // Step 2: run fresh oia-audit (write to temp file so audit-trend can read it)
245
+ const tmp = mkdtempSync(join(tmpdir(), 'drift-from-history-'));
246
+ const currPath = join(tmp, 'current.json');
247
+ try {
248
+ // iter 58 — reuse auditResult from the parallel batch above instead
249
+ // of re-running oia-audit. Saves the ~600ms-3s the second run took.
250
+ if (!auditResult.json || auditResult.exitCode !== 0) {
251
+ emitAndExit({
252
+ error: `oia-audit failed (exit ${auditResult.exitCode})`,
253
+ stderrTail: auditResult.stderr.slice(-200),
254
+ }, 2);
255
+ }
256
+ if (auditResult.json.degraded === true) {
257
+ emitAndExit({
258
+ degraded: true,
259
+ reason: auditResult.json.reason || 'metaharness-not-available',
260
+ }, 3);
261
+ }
262
+ writeFileSync(currPath, JSON.stringify(auditResult.json));
263
+
264
+ // Step 3: audit-trend
265
+ // iter 67 — when --baseline-file was given, pass it through as
266
+ // audit-trend's --baseline file input instead of --baseline-key.
267
+ // Avoids the memory roundtrip on the trend leg too.
268
+ const trendArgs = usedBaselineFile
269
+ ? [
270
+ '--baseline', ARGS.baselineFile,
271
+ '--current', currPath,
272
+ '--alert-on-distance-below', String(ARGS.threshold),
273
+ ]
274
+ : [
275
+ '--baseline-key', baseline.key,
276
+ '--current', currPath,
277
+ '--alert-on-distance-below', String(ARGS.threshold),
278
+ ];
279
+ const trendResult = runScriptJson('audit-trend.mjs', trendArgs);
280
+ if (!trendResult.json) {
281
+ emitAndExit({
282
+ error: `audit-trend produced no JSON (exit ${trendResult.exitCode})`,
283
+ stderrTail: trendResult.stderr.slice(-200),
284
+ }, 2);
285
+ }
286
+ const trend = trendResult.json;
287
+ let alertTriggered = trendResult.exitCode === 1;
288
+ const alertReasons = trend.alert?.reasons ? [...trend.alert.reasons] : [];
289
+
290
+ // iter 78 — orthogonal finding-severity gate: any introduced
291
+ // finding ≥ --alert-on-new-severity triggers regardless of
292
+ // structural-distance threshold. Catches the "security regression
293
+ // that didn't move the genome much" case.
294
+ let elevatedFindings = [];
295
+ if (ARGS.alertOnNewSeverity) {
296
+ const threshold = rankSeverity(ARGS.alertOnNewSeverity);
297
+ const introduced = trend?.delta?.findings?.introduced ?? [];
298
+ elevatedFindings = introduced.filter((f) => rankSeverity(f.severity) >= threshold);
299
+ if (elevatedFindings.length > 0) {
300
+ alertTriggered = true;
301
+ alertReasons.push(
302
+ `${elevatedFindings.length} new finding(s) at or above ${ARGS.alertOnNewSeverity} severity`,
303
+ );
304
+ }
305
+ }
306
+
307
+ const payload = {
308
+ adr: 'ADR-150 + ADR-152 §3.1',
309
+ command: 'drift-from-history',
310
+ // iter 65 — parallel batch metrics. parallelSpeedup>1 means
311
+ // audit-list + oia-audit raced; ~1.0 means serial regression.
312
+ timing: {
313
+ parallelWallMs,
314
+ parallelSumMs,
315
+ parallelSpeedup: parallelSumMs > 0
316
+ ? Math.round((parallelSumMs / Math.max(parallelWallMs, 1)) * 100) / 100
317
+ : 0,
318
+ // iter 66 — when true, audit-list was skipped via --baseline-key.
319
+ // Fastpath drops wall-clock from ~26s to ~1s (avoids ONNX warmup).
320
+ skippedAuditList,
321
+ // iter 67 — when true, baseline came from a file instead of memory.
322
+ // Skips audit-list AND the audit-trend memory roundtrip.
323
+ usedBaselineFile,
324
+ // iter 95 — derived path label for cleaner consumer code:
325
+ // 'slow' — default (no fast-path arg); audit-list ran
326
+ // 'key' — --baseline-key skipped audit-list, baseline came from memory
327
+ // 'file' — --baseline-file skipped audit-list AND memory roundtrip
328
+ path: usedBaselineFile ? 'file' : (skippedAuditList ? 'key' : 'slow'),
329
+ },
330
+ baseline: {
331
+ key: baseline.key,
332
+ startedAt: baseline.startedAt ?? null,
333
+ },
334
+ current: {
335
+ startedAt: auditResult.json.startedAt,
336
+ composite: auditResult.json.composite,
337
+ },
338
+ drift: trend.delta,
339
+ alert: {
340
+ threshold: ARGS.threshold,
341
+ // iter 78 — orthogonal gates surfaced: similarity threshold AND
342
+ // new-finding-severity. Either can flip triggered=true.
343
+ newSeverityThreshold: ARGS.alertOnNewSeverity,
344
+ triggered: alertTriggered,
345
+ reasons: alertReasons,
346
+ // Keep .reason for backward-compat with consumers that read it as a string
347
+ reason: alertReasons.length > 0
348
+ ? alertReasons.join('; ')
349
+ : (alertTriggered ? `similarity < ${ARGS.threshold}` : `similarity ≥ ${ARGS.threshold} — OK`),
350
+ elevatedFindings,
351
+ },
352
+ generatedAt: new Date().toISOString(),
353
+ };
354
+ emitAndExit(payload, alertTriggered ? 1 : 0);
355
+ } finally {
356
+ try { rmSync(tmp, { recursive: true, force: true }); } catch { /* ignore */ }
357
+ }
358
+ }
359
+
360
+ main().catch((e) => {
361
+ console.error('drift-from-history crashed:', e.message || e);
362
+ process.exit(2);
363
+ });
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ // genome.mjs — wrapper around `metaharness genome <path>`.
3
+ //
4
+ // Returns the 7-section readiness report: repo_type / agent_topology /
5
+ // risk_score / mcp_surface / test_confidence / publish_readiness +
6
+ // verdict (ready | needs-work | blocked). Reads-only.
7
+ //
8
+ // USAGE
9
+ // node scripts/genome.mjs
10
+ // node scripts/genome.mjs --path <dir> --alert-on-risk-above 0.5 --format json
11
+ //
12
+ // EXIT CODES
13
+ // 0 OK
14
+ // 1 --alert-on-risk-above threshold breached
15
+ // 2 config error or genome failure
16
+
17
+ import { runMetaharness, emitDegradedJsonAndExit } from './_harness.mjs';
18
+
19
+ const ARGS = (() => {
20
+ const a = { path: '.', format: 'json', alertRiskAbove: null };
21
+ for (let i = 2; i < process.argv.length; i++) {
22
+ const v = process.argv[i];
23
+ if (v === '--path') a.path = process.argv[++i];
24
+ else if (v === '--alert-on-risk-above') a.alertRiskAbove = parseFloat(process.argv[++i]);
25
+ else if (v === '--format') a.format = process.argv[++i];
26
+ }
27
+ return a;
28
+ })();
29
+
30
+ function main() {
31
+ const r = runMetaharness(['genome', ARGS.path]);
32
+ if (r.degraded) { emitDegradedJsonAndExit(r.reason); return; }
33
+ if (r.exitCode !== 0 || !r.json) {
34
+ console.error(`genome: metaharness exited ${r.exitCode}`);
35
+ if (r.stderr) console.error(r.stderr.slice(0, 400));
36
+ process.exit(2);
37
+ }
38
+ // iter 112 — generatedAt for consistency with other --format json outputs
39
+ const payload = { ...r.json, path: ARGS.path, durationMs: r.durationMs,
40
+ generatedAt: new Date().toISOString() };
41
+
42
+ if (ARGS.alertRiskAbove !== null) {
43
+ if (!isFinite(ARGS.alertRiskAbove)) {
44
+ console.error(`genome: --alert-on-risk-above must be a finite number`);
45
+ process.exit(2);
46
+ }
47
+ payload.alert = {
48
+ threshold: ARGS.alertRiskAbove,
49
+ triggered: typeof payload.risk_score === 'number' && payload.risk_score > ARGS.alertRiskAbove,
50
+ reason: typeof payload.risk_score === 'number' && payload.risk_score > ARGS.alertRiskAbove
51
+ ? `risk_score ${payload.risk_score} > ${ARGS.alertRiskAbove}`
52
+ : `risk_score ${payload.risk_score ?? 'unknown'} ≤ ${ARGS.alertRiskAbove} — OK`,
53
+ };
54
+ }
55
+
56
+ if (ARGS.format === 'json') {
57
+ console.log(JSON.stringify(payload, null, 2));
58
+ } else {
59
+ console.log(`# harness-genome — ${ARGS.path}`);
60
+ console.log('');
61
+ console.log(`| Section | Value |`);
62
+ console.log(`|---|---|`);
63
+ console.log(`| repo_type | ${payload.repo_type ?? '—'} |`);
64
+ console.log(`| agent_topology | ${(payload.agent_topology || []).join(', ') || '—'} |`);
65
+ console.log(`| risk_score | ${payload.risk_score ?? '—'} |`);
66
+ console.log(`| mcp_surface | ${payload.mcp_surface ?? '—'} |`);
67
+ console.log(`| test_confidence | ${payload.test_confidence ?? '—'} |`);
68
+ console.log(`| publish_readiness | ${payload.publish_readiness ?? '—'} |`);
69
+ console.log(`| **duration** | ${payload.durationMs}ms |`);
70
+ console.log('');
71
+ if (payload.alert) {
72
+ console.log(payload.alert.triggered ? `⚠ **ALERT**: ${payload.alert.reason}` : `✓ ${payload.alert.reason}`);
73
+ console.log('');
74
+ }
75
+ }
76
+
77
+ if (payload.alert?.triggered) process.exit(1);
78
+ }
79
+
80
+ main();
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ // mcp-scan.mjs — wrapper around `harness mcp-scan <path>`.
3
+ //
4
+ // Static security scan of the harness's declared MCP surface. Reads
5
+ // .mcp/servers.json + .harness/claims.json. Pure-read, no dispatch.
6
+ //
7
+ // USAGE
8
+ // node scripts/mcp-scan.mjs # current dir
9
+ // node scripts/mcp-scan.mjs --path <dir>
10
+ // node scripts/mcp-scan.mjs --fail-on high # exit 1 if any HIGH finding (default)
11
+ // node scripts/mcp-scan.mjs --fail-on medium # also fail on MEDIUM
12
+ // node scripts/mcp-scan.mjs --format json
13
+ //
14
+ // EXIT CODES
15
+ // 0 no findings at or above --fail-on (or degraded)
16
+ // 1 at least one finding ≥ --fail-on severity
17
+ // 2 config error or scan failure
18
+
19
+ // iter 50 — parseMcpScanText extracted to _harness.mjs so oia-audit
20
+ // can use the same parser without duplicating logic.
21
+ // iter 63 — SEVERITY_RANK also moves to _harness.mjs (single source of
22
+ // truth; previously this file had a literal that diverged from oia-audit's).
23
+ import { runHarness, emitDegradedJsonAndExit, parseMcpScanText, SEVERITY_RANK, rankSeverity } from './_harness.mjs';
24
+
25
+ const ARGS = (() => {
26
+ const a = { path: '.', format: 'json', failOn: 'high' };
27
+ for (let i = 2; i < process.argv.length; i++) {
28
+ const v = process.argv[i];
29
+ if (v === '--path') a.path = process.argv[++i];
30
+ else if (v === '--fail-on') a.failOn = String(process.argv[++i] || 'high').toLowerCase();
31
+ else if (v === '--format') a.format = process.argv[++i];
32
+ }
33
+ return a;
34
+ })();
35
+
36
+ function main() {
37
+ if (!SEVERITY_RANK[ARGS.failOn]) {
38
+ console.error(`mcp-scan: --fail-on must be one of low|medium|high; got ${ARGS.failOn}`);
39
+ process.exit(2);
40
+ }
41
+ const r = runHarness(['mcp-scan', ARGS.path]);
42
+ if (r.degraded) { emitDegradedJsonAndExit(r.reason); return; }
43
+ if (r.exitCode !== 0 && r.exitCode !== 1) {
44
+ // exit 1 from harness can be "findings present"; only treat other
45
+ // non-zero as a real failure.
46
+ console.error(`mcp-scan: harness exited ${r.exitCode}`);
47
+ if (r.stderr) console.error(r.stderr.slice(0, 400));
48
+ process.exit(2);
49
+ }
50
+ // The JSON output shape from `harness mcp-scan` historically didn't
51
+ // include structured findings — it emits text even with --json. Parse
52
+ // the text into findings (iter 50) so audit-trend's introduced/cleared
53
+ // diff actually works. If a future upstream version DOES emit
54
+ // findings[], the parsed-text findings are overridden.
55
+ const parsed = parseMcpScanText(r.stdout);
56
+ // iter 124 — normalize finding shape so consumers see a stable contract.
57
+ // Upstream metaharness@0.1.x JSON path emits {id, severity, title, detail};
58
+ // our parseMcpScanText emits {severity, message}. Project both into a
59
+ // unified {severity, message, title?, detail?} so audit-trend's fingerprint
60
+ // diff and test-mcp-tools' assertions don't care which path produced them.
61
+ const rawFindings = Array.isArray(r.json?.findings) ? r.json.findings : parsed.findings;
62
+ const findings = rawFindings.map((f) => ({
63
+ ...f,
64
+ message: typeof f.message === 'string' && f.message
65
+ ? f.message
66
+ : (f.title || f.detail || ''),
67
+ }));
68
+ const payload = {
69
+ ...(r.json ?? {}),
70
+ findings,
71
+ summary: r.json?.summary ?? parsed.summary,
72
+ rawStdout: r.stdout.slice(0, 400),
73
+ };
74
+ const threshold = SEVERITY_RANK[ARGS.failOn];
75
+ // iter 63 — rankSeverity() safe lookup. Pre-iter-63 `undefined >= 3`
76
+ // was false → unknown-severity findings (e.g., warn / error) were
77
+ // silently excluded from the offending set, weakening --fail-on alerts.
78
+ const offending = findings.filter((f) => rankSeverity(f.severity) >= threshold);
79
+
80
+ const alert = {
81
+ threshold: ARGS.failOn,
82
+ triggered: offending.length > 0,
83
+ offendingCount: offending.length,
84
+ reason: offending.length > 0
85
+ ? `${offending.length} finding(s) at or above ${ARGS.failOn} severity`
86
+ : `no findings at or above ${ARGS.failOn} severity — OK`,
87
+ };
88
+
89
+ if (ARGS.format === 'json') {
90
+ // iter 112 — generatedAt for consistency with other --format json outputs
91
+ console.log(JSON.stringify({
92
+ ...payload, durationMs: r.durationMs, alert,
93
+ generatedAt: new Date().toISOString(),
94
+ }, null, 2));
95
+ } else {
96
+ console.log(`# harness mcp-scan — ${ARGS.path}`);
97
+ console.log('');
98
+ console.log(`Total findings: ${findings.length}`);
99
+ console.log(`| Severity | ID | Server | Tool | Message |`);
100
+ console.log(`|---|---|---|---|---|`);
101
+ for (const f of findings.slice(0, 50)) {
102
+ console.log(`| ${f.severity} | ${f.id ?? '—'} | ${f.server ?? '—'} | ${f.tool ?? '—'} | ${f.message ?? ''} |`);
103
+ }
104
+ console.log('');
105
+ console.log(alert.triggered ? `⚠ **ALERT**: ${alert.reason}` : `✓ ${alert.reason}`);
106
+ }
107
+
108
+ if (alert.triggered) process.exit(1);
109
+ }
110
+
111
+ main();
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ // mint.mjs — wrapper around `metaharness new <name> --template <id> --host <id>`.
3
+ //
4
+ // SAFETY (ADR-150 architectural constraint + "executing actions with care"):
5
+ // - Target directory MUST be explicitly specified via --target. Defaults to
6
+ // a temp dir under /tmp/ruflo-mint-<timestamp>/ if not given — never the
7
+ // project root (ruflo behavioral rule "never save to root folder").
8
+ // - --confirm flag required. Without it the script prints a dry-run plan
9
+ // and exits 0 without writing files. This honors the "destructive-action
10
+ // confirmation" pattern in ruflo's CLAUDE.md.
11
+ //
12
+ // USAGE
13
+ // node scripts/mint.mjs --name my-harness --template vertical:coding --host claude-code
14
+ // # → prints dry-run plan, exits 0
15
+ // node scripts/mint.mjs --name my-harness --template vertical:coding --host claude-code --target /tmp/foo --confirm
16
+ // # → actually scaffolds
17
+
18
+ import { runMetaharness, emitDegradedJsonAndExit } from './_harness.mjs';
19
+ import { existsSync, mkdirSync } from 'node:fs';
20
+ import { resolve, dirname, basename } from 'node:path';
21
+ import { tmpdir } from 'node:os';
22
+
23
+ const ARGS = (() => {
24
+ const a = { name: null, template: null, host: 'claude-code', target: null, confirm: false, format: 'json' };
25
+ for (let i = 2; i < process.argv.length; i++) {
26
+ const v = process.argv[i];
27
+ if (v === '--name') a.name = process.argv[++i];
28
+ else if (v === '--template') a.template = process.argv[++i];
29
+ else if (v === '--host') a.host = process.argv[++i];
30
+ else if (v === '--target') a.target = process.argv[++i];
31
+ else if (v === '--confirm') a.confirm = true;
32
+ else if (v === '--format') a.format = process.argv[++i];
33
+ }
34
+ return a;
35
+ })();
36
+
37
+ function safetyChecks() {
38
+ if (!ARGS.name) {
39
+ console.error('mint: --name is required');
40
+ process.exit(2);
41
+ }
42
+ if (!ARGS.template) {
43
+ console.error('mint: --template is required (e.g. vertical:coding, minimal)');
44
+ process.exit(2);
45
+ }
46
+ if (!ARGS.target) {
47
+ ARGS.target = resolve(tmpdir(), `ruflo-mint-${Date.now()}-${ARGS.name}`);
48
+ }
49
+ const targetAbs = resolve(ARGS.target);
50
+ const repoRoot = resolve(process.cwd());
51
+ if (targetAbs === repoRoot) {
52
+ console.error(`mint: refusing to write to project root (${repoRoot}). Specify a different --target.`);
53
+ process.exit(2);
54
+ }
55
+ if (targetAbs.startsWith(repoRoot + '/')) {
56
+ console.error(`mint: refusing to write inside the calling repo root. Use a --target outside ${repoRoot}.`);
57
+ process.exit(2);
58
+ }
59
+ }
60
+
61
+ function main() {
62
+ safetyChecks();
63
+ const plan = {
64
+ action: 'metaharness new',
65
+ name: ARGS.name,
66
+ template: ARGS.template,
67
+ host: ARGS.host,
68
+ target: ARGS.target,
69
+ confirm: ARGS.confirm,
70
+ willWrite: ARGS.confirm,
71
+ };
72
+ if (!ARGS.confirm) {
73
+ if (ARGS.format === 'json') console.log(JSON.stringify({ ...plan, dryRun: true }, null, 2));
74
+ else {
75
+ console.log(`# harness-mint (dry-run)`);
76
+ console.log('');
77
+ for (const [k, v] of Object.entries(plan)) console.log(`- ${k}: ${v}`);
78
+ console.log('');
79
+ console.log('Re-run with `--confirm` to actually scaffold.');
80
+ }
81
+ process.exit(0);
82
+ }
83
+ if (existsSync(ARGS.target)) {
84
+ console.error(`mint: target ${ARGS.target} already exists`);
85
+ process.exit(2);
86
+ }
87
+ // ITER 27 FIX (forward-compatible) — at the time of writing,
88
+ // metaharness@0.1.12 silently ignored the --target flag and wrote
89
+ // to $CWD/<name> instead. Upstream issue #9
90
+ // (ruvnet/agent-harness-generator) was filed + fixed in 0.1.13
91
+ // within ~20 min — but this workaround is intentionally
92
+ // forward-compatible: setting `cwd: dirname(target)` + passing
93
+ // `basename(target)` as <name> produces the same correct landing
94
+ // path on BOTH broken (0.1.12) and fixed (0.1.13+) upstream
95
+ // versions. Keeping it as belt-and-braces against any future
96
+ // regression. See `iter 29` for the verification log.
97
+ const parentDir = dirname(ARGS.target);
98
+ const cliName = basename(ARGS.target);
99
+ mkdirSync(parentDir, { recursive: true });
100
+ const r = runMetaharness(
101
+ ['new', cliName, '--template', ARGS.template, '--host', ARGS.host],
102
+ { json: false, cwd: parentDir, timeoutMs: 180_000 },
103
+ );
104
+ if (r.degraded) { emitDegradedJsonAndExit(r.reason); return; }
105
+ if (r.exitCode !== 0) {
106
+ console.error(`mint: metaharness exited ${r.exitCode}`);
107
+ if (r.stderr) console.error(r.stderr.slice(0, 400));
108
+ process.exit(2);
109
+ }
110
+ const result = { ...plan, exitCode: r.exitCode, durationMs: r.durationMs, ok: true };
111
+ if (ARGS.format === 'json') console.log(JSON.stringify(result, null, 2));
112
+ else {
113
+ console.log(`# harness-mint — done`);
114
+ console.log('');
115
+ for (const [k, v] of Object.entries(result)) console.log(`- ${k}: ${v}`);
116
+ }
117
+ }
118
+
119
+ main();