@claude-flow/cli 3.12.0 → 3.12.2
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/.claude/helpers/auto-memory-hook.mjs +34 -4
- package/.claude/helpers/hook-handler.cjs +17 -13
- package/.claude/helpers/intelligence.cjs +16 -2
- package/README.md +7 -4
- package/dist/src/commands/daemon.d.ts.map +1 -1
- package/dist/src/commands/daemon.js +76 -7
- package/dist/src/commands/daemon.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +24 -3
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/security.d.ts.map +1 -1
- package/dist/src/commands/security.js +70 -12
- package/dist/src/commands/security.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -2
- package/plugins/ruflo-metaharness/.claude-flow/data/pending-insights.jsonl +1 -0
- package/plugins/ruflo-metaharness/.claude-flow/neural/stats.json +6 -0
- package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +32 -0
- package/plugins/ruflo-metaharness/README.md +64 -0
- package/plugins/ruflo-metaharness/agents/metaharness-architect.md +58 -0
- package/plugins/ruflo-metaharness/commands/ruflo-metaharness.md +46 -0
- package/plugins/ruflo-metaharness/scripts/_harness.mjs +261 -0
- package/plugins/ruflo-metaharness/scripts/_similarity.mjs +161 -0
- package/plugins/ruflo-metaharness/scripts/_spike-similarity.mjs +223 -0
- package/plugins/ruflo-metaharness/scripts/audit-list.mjs +158 -0
- package/plugins/ruflo-metaharness/scripts/audit-trend.mjs +272 -0
- package/plugins/ruflo-metaharness/scripts/bench-parse-mcp-scan.mjs +146 -0
- package/plugins/ruflo-metaharness/scripts/bench-recordpair-overhead.mjs +186 -0
- package/plugins/ruflo-metaharness/scripts/bench-similarity.mjs +177 -0
- package/plugins/ruflo-metaharness/scripts/drift-from-history.mjs +363 -0
- package/plugins/ruflo-metaharness/scripts/genome.mjs +80 -0
- package/plugins/ruflo-metaharness/scripts/mcp-scan.mjs +111 -0
- package/plugins/ruflo-metaharness/scripts/mint.mjs +119 -0
- package/plugins/ruflo-metaharness/scripts/oia-audit.mjs +228 -0
- package/plugins/ruflo-metaharness/scripts/router-parallel-analyze.mjs +250 -0
- package/plugins/ruflo-metaharness/scripts/score.mjs +92 -0
- package/plugins/ruflo-metaharness/scripts/similarity.mjs +158 -0
- package/plugins/ruflo-metaharness/scripts/smoke.sh +2268 -0
- package/plugins/ruflo-metaharness/scripts/test-graceful-degradation.mjs +141 -0
- package/plugins/ruflo-metaharness/scripts/test-mcp-tools.mjs +437 -0
- package/plugins/ruflo-metaharness/scripts/test-parallel-pipeline.mjs +204 -0
- package/plugins/ruflo-metaharness/scripts/test-pipeline-roundtrip.mjs +586 -0
- package/plugins/ruflo-metaharness/scripts/test-similarity.mjs +334 -0
- package/plugins/ruflo-metaharness/scripts/test-with-openrouter.mjs +229 -0
- package/plugins/ruflo-metaharness/scripts/threat-model.mjs +59 -0
- package/plugins/ruflo-metaharness/skills/harness-drift-from-history/SKILL.md +65 -0
- package/plugins/ruflo-metaharness/skills/harness-genome/SKILL.md +54 -0
- package/plugins/ruflo-metaharness/skills/harness-mcp-scan/SKILL.md +47 -0
- package/plugins/ruflo-metaharness/skills/harness-mint/SKILL.md +72 -0
- package/plugins/ruflo-metaharness/skills/harness-oia-audit/SKILL.md +79 -0
- package/plugins/ruflo-metaharness/skills/harness-score/SKILL.md +66 -0
- package/plugins/ruflo-metaharness/skills/harness-similarity/SKILL.md +67 -0
- 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();
|