@claude-flow/cli 3.10.15 → 3.10.17
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/mcp-tools/hooks-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/hooks-tools.js +69 -0
- package/dist/src/mcp-tools/hooks-tools.js.map +1 -1
- package/dist/src/memory/structured-distill.d.ts +48 -0
- package/dist/src/memory/structured-distill.d.ts.map +1 -0
- package/dist/src/memory/structured-distill.js +125 -0
- package/dist/src/memory/structured-distill.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/scripts/benchmark-pretrained-retrieval.mjs +124 -0
- package/scripts/benchmark-trajectory-mrr.mjs +191 -0
- package/scripts/pretrain-from-github.mjs +254 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.10.
|
|
3
|
+
"version": "3.10.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// benchmark-pretrained-retrieval.mjs — proof that pretrained patterns are
|
|
3
|
+
// retrievable, not just stored.
|
|
4
|
+
//
|
|
5
|
+
// Runs sample queries against the neural store (post-pretrain) and reports
|
|
6
|
+
// the top-k matches. Demonstrates that after `pretrain-from-github.mjs`
|
|
7
|
+
// runs, an agent can recall relevant past work by intent.
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// 1. node scripts/pretrain-from-github.mjs # populate the store
|
|
11
|
+
// 2. node scripts/benchmark-pretrained-retrieval.mjs # query + report
|
|
12
|
+
|
|
13
|
+
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { dirname, join, resolve } from 'node:path';
|
|
16
|
+
import { performance } from 'node:perf_hooks';
|
|
17
|
+
|
|
18
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const CLI_ROOT = resolve(SCRIPT_DIR, '..');
|
|
20
|
+
const REPO_ROOT = resolve(SCRIPT_DIR, '../../../..');
|
|
21
|
+
const RUNS_DIR = join(REPO_ROOT, 'docs', 'benchmarks', 'runs');
|
|
22
|
+
|
|
23
|
+
// Real ruflo-history-shaped queries. Each one targets a concept that should
|
|
24
|
+
// have been seen during pretrain (Opus 4.8 fix, self-learning wiring,
|
|
25
|
+
// codemod engine, etc.).
|
|
26
|
+
const QUERIES = [
|
|
27
|
+
'how was the Opus model alias fixed',
|
|
28
|
+
'self-learning wiring task-completed pretrain',
|
|
29
|
+
'deterministic codemod engine var-to-const',
|
|
30
|
+
'MCP server orphan leak parent-death',
|
|
31
|
+
'unified learning stats aggregator',
|
|
32
|
+
'structured distillation 4-field schema',
|
|
33
|
+
'SQL injection migrate.ts table identifier',
|
|
34
|
+
'recall@k HNSW benchmark harness',
|
|
35
|
+
'Q-learning encoder keyword block',
|
|
36
|
+
'security hardening crypto random IDs',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const TOP_K = 5;
|
|
40
|
+
|
|
41
|
+
async function main() {
|
|
42
|
+
const intel = await import(join(CLI_ROOT, 'dist/src/memory/intelligence.js'));
|
|
43
|
+
const neural = await import(join(CLI_ROOT, 'dist/src/mcp-tools/neural-tools.js'));
|
|
44
|
+
|
|
45
|
+
// §1 — snapshot the neural store + globalStats so we know what's there.
|
|
46
|
+
const unified = await intel.getUnifiedLearningStats();
|
|
47
|
+
const total = unified.neuralPatterns.patternCount;
|
|
48
|
+
|
|
49
|
+
if (total === 0) {
|
|
50
|
+
console.error('No patterns in neural store. Run scripts/pretrain-from-github.mjs first.');
|
|
51
|
+
process.exit(2);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// §2 — run each query through neural_patterns search. The tool's search
|
|
55
|
+
// action runs cosine similarity against the stored embeddings.
|
|
56
|
+
const listTool = neural.neuralTools.find((t) => t.name === 'neural_patterns');
|
|
57
|
+
const tQuery0 = performance.now();
|
|
58
|
+
const results = [];
|
|
59
|
+
for (const q of QUERIES) {
|
|
60
|
+
const r = await listTool.handler({ action: 'search', query: q });
|
|
61
|
+
const matches = (r.patterns || r.results || r.matches || []).slice(0, TOP_K);
|
|
62
|
+
results.push({
|
|
63
|
+
query: q,
|
|
64
|
+
matched: matches.length > 0,
|
|
65
|
+
topK: matches.map((m) => ({
|
|
66
|
+
id: m.id,
|
|
67
|
+
name: m.name?.slice(0, 100),
|
|
68
|
+
type: m.type,
|
|
69
|
+
score: m.score ?? m.similarity,
|
|
70
|
+
})),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const queryMs = performance.now() - tQuery0;
|
|
74
|
+
|
|
75
|
+
const matchedQueries = results.filter((r) => r.matched).length;
|
|
76
|
+
const summary = {
|
|
77
|
+
runAt: new Date().toISOString(),
|
|
78
|
+
benchmark: 'pretrained-retrieval',
|
|
79
|
+
storeSize: total,
|
|
80
|
+
queries: QUERIES.length,
|
|
81
|
+
matchedQueries,
|
|
82
|
+
matchRate: Number((matchedQueries / QUERIES.length).toFixed(4)),
|
|
83
|
+
avgQueryLatencyMs: Number((queryMs / QUERIES.length).toFixed(2)),
|
|
84
|
+
totalQueryMs: Number(queryMs.toFixed(2)),
|
|
85
|
+
results,
|
|
86
|
+
passed: matchedQueries === QUERIES.length,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (process.env.BENCH_JSON) {
|
|
90
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
91
|
+
} else {
|
|
92
|
+
console.log(`# Pretrained-retrieval benchmark — proof of learning`);
|
|
93
|
+
console.log(`Store size: ${total} patterns`);
|
|
94
|
+
console.log(`Queries: ${QUERIES.length}`);
|
|
95
|
+
console.log(`Match rate: ${(summary.matchRate * 100).toFixed(0)}% (${matchedQueries}/${QUERIES.length})`);
|
|
96
|
+
console.log(`Avg query latency: ${summary.avgQueryLatencyMs} ms`);
|
|
97
|
+
console.log('');
|
|
98
|
+
for (const r of results) {
|
|
99
|
+
console.log(`Q: "${r.query}"`);
|
|
100
|
+
if (r.topK.length === 0) {
|
|
101
|
+
console.log(` → no matches`);
|
|
102
|
+
} else {
|
|
103
|
+
for (const m of r.topK.slice(0, 3)) {
|
|
104
|
+
console.log(` → ${m.score?.toFixed?.(3) ?? '—'} ${m.name}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
console.log('');
|
|
109
|
+
console.log(`Overall: ${summary.passed ? '✅ PASSED' : '⚠️ partial'}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!process.env.BENCH_NO_WRITE) {
|
|
113
|
+
mkdirSync(RUNS_DIR, { recursive: true });
|
|
114
|
+
const stamp = summary.runAt.replace(/[:.]/g, '-');
|
|
115
|
+
writeFileSync(join(RUNS_DIR, `pretrained-retrieval-${stamp}.json`), JSON.stringify(summary, null, 2));
|
|
116
|
+
writeFileSync(join(RUNS_DIR, 'pretrained-retrieval-latest.json'), JSON.stringify(summary, null, 2));
|
|
117
|
+
if (!process.env.BENCH_JSON) console.log(`\nWrote ${join(RUNS_DIR, `pretrained-retrieval-${stamp}.json`)}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ONNX runtime keeps a worker thread alive — force exit.
|
|
121
|
+
process.exit(summary.passed ? 0 : 1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
main().catch((err) => { console.error(err); process.exit(1); });
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// benchmark-trajectory-mrr.mjs — proof harness for Structured Distillation
|
|
3
|
+
// (#2241 §SOTA, arXiv:2603.13017).
|
|
4
|
+
//
|
|
5
|
+
// Measures retrieval MRR for raw vs structured-distilled trajectory content
|
|
6
|
+
// against a paired corpus. The arXiv paper reports raw → distilled MRR
|
|
7
|
+
// going from 0.745 to 0.759 on a 214 K-pair consensus-graded set; our corpus
|
|
8
|
+
// is much smaller and hand-curated, so the absolute numbers won't match,
|
|
9
|
+
// but the *direction* of the delta is what we're proving.
|
|
10
|
+
//
|
|
11
|
+
// Two embedders are supported:
|
|
12
|
+
// - Real ONNX (Xenova/all-MiniLM-L6-v2 384-dim) via @claude-flow/embeddings
|
|
13
|
+
// when available. Best signal.
|
|
14
|
+
// - Hash-based deterministic fallback when ONNX isn't installed. Lower
|
|
15
|
+
// signal; results still relative-comparable.
|
|
16
|
+
//
|
|
17
|
+
// Usage:
|
|
18
|
+
// node scripts/benchmark-trajectory-mrr.mjs # default
|
|
19
|
+
// BENCH_JSON=1 node scripts/benchmark-trajectory-mrr.mjs
|
|
20
|
+
// BENCH_NO_WRITE=1 node scripts/benchmark-trajectory-mrr.mjs
|
|
21
|
+
|
|
22
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import { dirname, join, resolve } from 'node:path';
|
|
25
|
+
import { performance } from 'node:perf_hooks';
|
|
26
|
+
|
|
27
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const CLI_ROOT = resolve(SCRIPT_DIR, '..');
|
|
29
|
+
const REPO_ROOT = resolve(SCRIPT_DIR, '../../../..');
|
|
30
|
+
const RUNS_DIR = join(REPO_ROOT, 'docs', 'benchmarks', 'runs');
|
|
31
|
+
|
|
32
|
+
const { distillAndSerialise, compressionRatio } = await import(
|
|
33
|
+
join(CLI_ROOT, 'dist/src/memory/structured-distill.js')
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Embedder — prefers real ONNX, falls back to hash-based deterministic.
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
async function loadEmbedder() {
|
|
40
|
+
// Tier 1: AgentDB's bridge-loaded ONNX embedder (the path the live MCP
|
|
41
|
+
// server + vitest tests use, so we measure what users actually get).
|
|
42
|
+
try {
|
|
43
|
+
const mb = await import(join(CLI_ROOT, 'dist/src/memory/memory-bridge.js'));
|
|
44
|
+
const probe = await mb.bridgeGenerateEmbedding('warm-up').catch(() => null);
|
|
45
|
+
if (probe && probe.embedding && probe.embedding.length > 0 && probe.backend === 'onnx') {
|
|
46
|
+
return {
|
|
47
|
+
name: `bridge ONNX (${probe.model}, ${probe.dimensions}-dim)`,
|
|
48
|
+
embed: async (text) => {
|
|
49
|
+
const r = await mb.bridgeGenerateEmbedding(text);
|
|
50
|
+
if (!r) throw new Error('bridge embed returned null');
|
|
51
|
+
return r.embedding;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
} catch { /* fall through */ }
|
|
56
|
+
// Tier 2: hash-based deterministic. Clearly degraded; produces only
|
|
57
|
+
// direction-of-effect signal, not absolute MRR numbers comparable to paper.
|
|
58
|
+
console.error('⚠️ Real ONNX embedder unavailable — using hash-based deterministic fallback. Absolute MRR numbers are NOT comparable to the arXiv paper in this mode; relative comparison is also weak.');
|
|
59
|
+
return {
|
|
60
|
+
name: 'hash-fallback (no semantic signal — degraded mode)',
|
|
61
|
+
embed: async (text) => hashEmbed(text, 384),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function hashEmbed(text, dims) {
|
|
66
|
+
const v = new Float32Array(dims);
|
|
67
|
+
let seed = 0;
|
|
68
|
+
for (let i = 0; i < text.length; i++) seed = (seed * 31 + text.charCodeAt(i)) | 0;
|
|
69
|
+
let s = Math.abs(seed) | 1;
|
|
70
|
+
for (let i = 0; i < dims; i++) {
|
|
71
|
+
s = (s * 1103515245 + 12345) & 0x7fffffff;
|
|
72
|
+
v[i] = (s / 0x7fffffff) * 2 - 1;
|
|
73
|
+
}
|
|
74
|
+
// L2 normalise
|
|
75
|
+
let n = 0;
|
|
76
|
+
for (let i = 0; i < dims; i++) n += v[i] * v[i];
|
|
77
|
+
n = Math.sqrt(n) || 1;
|
|
78
|
+
for (let i = 0; i < dims; i++) v[i] /= n;
|
|
79
|
+
return Array.from(v);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function cosine(a, b) {
|
|
83
|
+
let s = 0;
|
|
84
|
+
for (let i = 0; i < a.length; i++) s += a[i] * b[i];
|
|
85
|
+
return s;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Main
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
async function main() {
|
|
92
|
+
const corpusPath = join(CLI_ROOT, 'bench', 'trajectory-mrr-corpus.json');
|
|
93
|
+
const corpus = JSON.parse(readFileSync(corpusPath, 'utf-8'));
|
|
94
|
+
const trajectories = corpus.trajectories;
|
|
95
|
+
const embedder = await loadEmbedder();
|
|
96
|
+
|
|
97
|
+
// Per-trajectory: embed both raw and distilled forms.
|
|
98
|
+
const tEmbed0 = performance.now();
|
|
99
|
+
const rawEmbs = [];
|
|
100
|
+
const distEmbs = [];
|
|
101
|
+
let totalRawBytes = 0;
|
|
102
|
+
let totalDistBytes = 0;
|
|
103
|
+
for (const t of trajectories) {
|
|
104
|
+
const distilled = distillAndSerialise(t.raw);
|
|
105
|
+
totalRawBytes += t.raw.length;
|
|
106
|
+
totalDistBytes += distilled.length;
|
|
107
|
+
rawEmbs.push(await embedder.embed(t.raw));
|
|
108
|
+
distEmbs.push(await embedder.embed(distilled));
|
|
109
|
+
}
|
|
110
|
+
const embedMs = performance.now() - tEmbed0;
|
|
111
|
+
|
|
112
|
+
// For each (query, gold) pair: rank all trajectories by cosine to the query
|
|
113
|
+
// embedding. MRR = mean of 1 / rank-of-gold across all queries.
|
|
114
|
+
const tQuery0 = performance.now();
|
|
115
|
+
let rrRaw = 0, rrDist = 0;
|
|
116
|
+
const perQuery = [];
|
|
117
|
+
for (let i = 0; i < trajectories.length; i++) {
|
|
118
|
+
const q = await embedder.embed(trajectories[i].query);
|
|
119
|
+
const rankRaw = rankOf(q, rawEmbs, i);
|
|
120
|
+
const rankDist = rankOf(q, distEmbs, i);
|
|
121
|
+
rrRaw += 1 / rankRaw;
|
|
122
|
+
rrDist += 1 / rankDist;
|
|
123
|
+
perQuery.push({ id: trajectories[i].id, rankRaw, rankDist });
|
|
124
|
+
}
|
|
125
|
+
const queryMs = performance.now() - tQuery0;
|
|
126
|
+
|
|
127
|
+
const N = trajectories.length;
|
|
128
|
+
const mrrRaw = rrRaw / N;
|
|
129
|
+
const mrrDist = rrDist / N;
|
|
130
|
+
const delta = mrrDist - mrrRaw;
|
|
131
|
+
const compression = totalRawBytes / Math.max(1, totalDistBytes);
|
|
132
|
+
|
|
133
|
+
const summary = {
|
|
134
|
+
runAt: new Date().toISOString(),
|
|
135
|
+
benchmark: 'trajectory-mrr',
|
|
136
|
+
embedder: embedder.name,
|
|
137
|
+
corpusVersion: corpus.version,
|
|
138
|
+
corpusSize: N,
|
|
139
|
+
mrr: {
|
|
140
|
+
raw: Number(mrrRaw.toFixed(4)),
|
|
141
|
+
distilled: Number(mrrDist.toFixed(4)),
|
|
142
|
+
delta: Number(delta.toFixed(4)),
|
|
143
|
+
paperReference: { raw: 0.745, distilled: 0.759, delta: 0.014, source: 'arXiv:2603.13017' },
|
|
144
|
+
},
|
|
145
|
+
compression: {
|
|
146
|
+
totalRawBytes,
|
|
147
|
+
totalDistBytes,
|
|
148
|
+
ratio: Number(compression.toFixed(2)),
|
|
149
|
+
paperReference: { tokensRaw: 371, tokensDistilled: 38, ratio: 9.76, source: 'arXiv:2603.13017' },
|
|
150
|
+
},
|
|
151
|
+
latencyMs: {
|
|
152
|
+
embedAll: Number(embedMs.toFixed(2)),
|
|
153
|
+
queryAll: Number(queryMs.toFixed(2)),
|
|
154
|
+
perDistill: Number((embedMs / (2 * N)).toFixed(4)),
|
|
155
|
+
},
|
|
156
|
+
distilledIsBetter: delta > 0,
|
|
157
|
+
perQueryRanks: perQuery,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
if (process.env.BENCH_JSON) {
|
|
161
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
162
|
+
} else {
|
|
163
|
+
console.log(`# Trajectory MRR benchmark (#2241 §Structured Distillation)`);
|
|
164
|
+
console.log(`Embedder: ${embedder.name}`);
|
|
165
|
+
console.log(`Corpus: N=${N} (v${corpus.version})`);
|
|
166
|
+
console.log('');
|
|
167
|
+
console.log('| Metric | Raw | Distilled | Δ |');
|
|
168
|
+
console.log('|---|---:|---:|---:|');
|
|
169
|
+
console.log(`| MRR | ${summary.mrr.raw} | ${summary.mrr.distilled} | ${summary.mrr.delta >= 0 ? '+' : ''}${summary.mrr.delta} |`);
|
|
170
|
+
console.log(`| Total bytes | ${totalRawBytes} | ${totalDistBytes} | ${compression.toFixed(2)}× compression |`);
|
|
171
|
+
console.log('');
|
|
172
|
+
console.log(`Distilled is ${summary.distilledIsBetter ? 'BETTER ✅' : 'WORSE ❌'} than raw on this corpus.`);
|
|
173
|
+
console.log(`Paper (arXiv:2603.13017): MRR raw 0.745 → distilled 0.759 (Δ +0.014); compression ~9.76× (371→38 tokens).`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!process.env.BENCH_NO_WRITE) {
|
|
177
|
+
mkdirSync(RUNS_DIR, { recursive: true });
|
|
178
|
+
const stamp = summary.runAt.replace(/[:.]/g, '-');
|
|
179
|
+
writeFileSync(join(RUNS_DIR, `trajectory-mrr-${stamp}.json`), JSON.stringify(summary, null, 2));
|
|
180
|
+
writeFileSync(join(RUNS_DIR, 'trajectory-mrr-latest.json'), JSON.stringify(summary, null, 2));
|
|
181
|
+
if (!process.env.BENCH_JSON) console.log(`\nWrote ${join(RUNS_DIR, `trajectory-mrr-${stamp}.json`)}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function rankOf(query, embeddings, goldIndex) {
|
|
186
|
+
const scored = embeddings.map((e, i) => ({ i, score: cosine(query, e) }));
|
|
187
|
+
scored.sort((a, b) => b.score - a.score);
|
|
188
|
+
return scored.findIndex((s) => s.i === goldIndex) + 1;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
main().catch((err) => { console.error(err); process.exit(1); });
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// pretrain-from-github.mjs — pretrain ruflo's self-learning system from its
|
|
3
|
+
// own GitHub history (commits + issues). Each commit/issue becomes one
|
|
4
|
+
// trajectory through the SONA + EWC++ pipeline; Structured Distillation
|
|
5
|
+
// (ADR-076) compresses each into the 4-field schema before embedding.
|
|
6
|
+
//
|
|
7
|
+
// What this proves:
|
|
8
|
+
// - globalStats.{trajectoriesRecorded, patternsLearned, signalsProcessed}
|
|
9
|
+
// all move from a measured before to a measured after.
|
|
10
|
+
// - neural_patterns.patternCount grows.
|
|
11
|
+
// - memory-bridge entries grow.
|
|
12
|
+
// - The unified-stats aggregator's consistency block stays clean.
|
|
13
|
+
// - Every item gets a learningPath of 'trajectory-pipeline' (not 'recorded-only').
|
|
14
|
+
//
|
|
15
|
+
// Usage:
|
|
16
|
+
// node scripts/pretrain-from-github.mjs # 50 commits + 30 issues
|
|
17
|
+
// COMMITS=200 ISSUES=100 node scripts/pretrain-from-github.mjs
|
|
18
|
+
// SOURCE=git node scripts/pretrain-from-github.mjs # git only, skip gh
|
|
19
|
+
// BENCH_JSON=1 node scripts/pretrain-from-github.mjs # machine-readable
|
|
20
|
+
// BENCH_NO_WRITE=1 node scripts/pretrain-from-github.mjs # don't write a run JSON
|
|
21
|
+
//
|
|
22
|
+
// Repro from a fresh checkout:
|
|
23
|
+
// git clone https://github.com/ruvnet/ruflo && cd ruflo
|
|
24
|
+
// npm install && ( cd v3/@claude-flow/cli && npx tsc -b )
|
|
25
|
+
// node v3/@claude-flow/cli/scripts/pretrain-from-github.mjs
|
|
26
|
+
|
|
27
|
+
import { execSync } from 'node:child_process';
|
|
28
|
+
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
import { dirname, join, resolve } from 'node:path';
|
|
31
|
+
import { performance } from 'node:perf_hooks';
|
|
32
|
+
|
|
33
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
const CLI_ROOT = resolve(SCRIPT_DIR, '..');
|
|
35
|
+
const REPO_ROOT = resolve(SCRIPT_DIR, '../../../..');
|
|
36
|
+
const RUNS_DIR = join(REPO_ROOT, 'docs', 'benchmarks', 'runs');
|
|
37
|
+
|
|
38
|
+
const COMMITS = Number(process.env.COMMITS) || 50;
|
|
39
|
+
const ISSUES = Number(process.env.ISSUES) || 30;
|
|
40
|
+
const SOURCE = process.env.SOURCE || 'all'; // 'all' | 'git' | 'issues'
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Harvesters
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
function harvestCommits(n) {
|
|
47
|
+
if (SOURCE === 'issues') return [];
|
|
48
|
+
const fmt = '%H%x00%s%x00%b%x01';
|
|
49
|
+
const raw = execSync(
|
|
50
|
+
`git log --pretty=format:'${fmt}' -n ${n} 2>/dev/null`,
|
|
51
|
+
{ cwd: REPO_ROOT, encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024 },
|
|
52
|
+
);
|
|
53
|
+
const entries = [];
|
|
54
|
+
for (const block of raw.split('\x01')) {
|
|
55
|
+
if (!block.trim()) continue;
|
|
56
|
+
const [sha, subject, body] = block.split('\x00');
|
|
57
|
+
if (!sha || !subject) continue;
|
|
58
|
+
entries.push({
|
|
59
|
+
source: 'commit',
|
|
60
|
+
id: `commit-${sha.slice(0, 12)}`,
|
|
61
|
+
subject: subject.trim(),
|
|
62
|
+
body: (body || '').trim(),
|
|
63
|
+
// The "verdict" of a commit isn't directly known. Treat all commits as
|
|
64
|
+
// success — we're learning from intent, not outcome. Failures are
|
|
65
|
+
// captured separately via the issue stream (closed issues are
|
|
66
|
+
// success; open issues are partial).
|
|
67
|
+
verdict: 'success',
|
|
68
|
+
content: `${subject.trim()}\n\n${(body || '').trim()}`.slice(0, 8192),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return entries;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function harvestIssues(n) {
|
|
75
|
+
if (SOURCE === 'git') return [];
|
|
76
|
+
try {
|
|
77
|
+
const raw = execSync(
|
|
78
|
+
`gh issue list --repo ruvnet/ruflo --state all --limit ${n} --json number,title,body,state,closedAt 2>/dev/null`,
|
|
79
|
+
{ cwd: REPO_ROOT, encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024 },
|
|
80
|
+
);
|
|
81
|
+
const items = JSON.parse(raw);
|
|
82
|
+
return items.map((i) => ({
|
|
83
|
+
source: 'issue',
|
|
84
|
+
id: `issue-${i.number}`,
|
|
85
|
+
subject: i.title,
|
|
86
|
+
body: (i.body || '').slice(0, 8192),
|
|
87
|
+
// closed = success outcome; open = partial (in-progress).
|
|
88
|
+
verdict: i.state === 'CLOSED' ? 'success' : 'partial',
|
|
89
|
+
content: `${i.title}\n\n${(i.body || '').slice(0, 8192)}`,
|
|
90
|
+
}));
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error(`gh issue harvest skipped (${err.message?.slice(0, 60)}). Set SOURCE=git to silence.`);
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Main
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
async function main() {
|
|
102
|
+
const intel = await import(join(CLI_ROOT, 'dist/src/memory/intelligence.js'));
|
|
103
|
+
const neural = await import(join(CLI_ROOT, 'dist/src/mcp-tools/neural-tools.js'));
|
|
104
|
+
const { distillAndSerialise } = await import(join(CLI_ROOT, 'dist/src/memory/structured-distill.js'));
|
|
105
|
+
|
|
106
|
+
// §1 — record the baseline (no clear; we want to learn ON TOP of whatever
|
|
107
|
+
// history the user already has).
|
|
108
|
+
const unified0 = await intel.getUnifiedLearningStats();
|
|
109
|
+
const before = {
|
|
110
|
+
trajectoriesRecorded: unified0.global.trajectoriesRecorded,
|
|
111
|
+
patternsLearned: unified0.global.patternsLearned,
|
|
112
|
+
signalsProcessed: unified0.global.signalsProcessed,
|
|
113
|
+
neuralPatternCount: unified0.neuralPatterns.patternCount,
|
|
114
|
+
memoryBridgeTotal: unified0.memoryBridge.totalEntries,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// §2 — harvest
|
|
118
|
+
const tHarvest0 = performance.now();
|
|
119
|
+
const commits = harvestCommits(COMMITS);
|
|
120
|
+
const issues = harvestIssues(ISSUES);
|
|
121
|
+
const items = [...commits, ...issues];
|
|
122
|
+
const harvestMs = performance.now() - tHarvest0;
|
|
123
|
+
|
|
124
|
+
if (!process.env.BENCH_JSON) {
|
|
125
|
+
console.log(`# Pretrain from ruflo GitHub history`);
|
|
126
|
+
console.log(`Harvested: ${commits.length} commits + ${issues.length} issues = ${items.length} trajectories (${harvestMs.toFixed(0)} ms)`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// §3 — feed each item through the trajectory pipeline.
|
|
130
|
+
const tFeed0 = performance.now();
|
|
131
|
+
let trained = 0;
|
|
132
|
+
let failed = 0;
|
|
133
|
+
const failures = [];
|
|
134
|
+
for (const item of items) {
|
|
135
|
+
try {
|
|
136
|
+
const distilled = distillAndSerialise(item.content);
|
|
137
|
+
await intel.recordTrajectory(
|
|
138
|
+
[{
|
|
139
|
+
type: 'result',
|
|
140
|
+
content: distilled,
|
|
141
|
+
metadata: {
|
|
142
|
+
source: item.source,
|
|
143
|
+
id: item.id,
|
|
144
|
+
subject: item.subject.slice(0, 200),
|
|
145
|
+
},
|
|
146
|
+
timestamp: Date.now(),
|
|
147
|
+
}],
|
|
148
|
+
item.verdict,
|
|
149
|
+
);
|
|
150
|
+
trained++;
|
|
151
|
+
} catch (err) {
|
|
152
|
+
failed++;
|
|
153
|
+
if (failures.length < 5) failures.push({ id: item.id, error: String(err.message).slice(0, 120) });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const feedMs = performance.now() - tFeed0;
|
|
157
|
+
intel.flushIntelligenceStats();
|
|
158
|
+
|
|
159
|
+
// §4 — also seed the neural store directly from the same items so
|
|
160
|
+
// `neural_patterns list` reflects them (closes the "globalStats moved but
|
|
161
|
+
// neural_patterns didn't" consistency note from ADR-075).
|
|
162
|
+
const tSeed0 = performance.now();
|
|
163
|
+
const neuralItems = items.map((item) => ({
|
|
164
|
+
name: item.subject.slice(0, 200),
|
|
165
|
+
type: item.source === 'commit' ? 'history-commit' : 'history-issue',
|
|
166
|
+
content: distillAndSerialise(item.content),
|
|
167
|
+
metadata: { source: item.source, id: item.id, verdict: item.verdict },
|
|
168
|
+
}));
|
|
169
|
+
const seedResult = await neural.storeNeuralPatterns(neuralItems);
|
|
170
|
+
const seedMs = performance.now() - tSeed0;
|
|
171
|
+
|
|
172
|
+
// §5 — read the after-counters via the unified aggregator (this is what
|
|
173
|
+
// hooks_intelligence_unified-stats would return for a live caller).
|
|
174
|
+
const unified1 = await intel.getUnifiedLearningStats();
|
|
175
|
+
const after = {
|
|
176
|
+
trajectoriesRecorded: unified1.global.trajectoriesRecorded,
|
|
177
|
+
patternsLearned: unified1.global.patternsLearned,
|
|
178
|
+
signalsProcessed: unified1.global.signalsProcessed,
|
|
179
|
+
neuralPatternCount: unified1.neuralPatterns.patternCount,
|
|
180
|
+
memoryBridgeTotal: unified1.memoryBridge.totalEntries,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const deltas = Object.fromEntries(
|
|
184
|
+
Object.keys(after).map((k) => [k, after[k] - before[k]]),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const summary = {
|
|
188
|
+
runAt: new Date().toISOString(),
|
|
189
|
+
benchmark: 'pretrain-from-github',
|
|
190
|
+
source: SOURCE,
|
|
191
|
+
config: { COMMITS, ISSUES },
|
|
192
|
+
harvest: {
|
|
193
|
+
commits: commits.length,
|
|
194
|
+
issues: issues.length,
|
|
195
|
+
total: items.length,
|
|
196
|
+
harvestMs: Number(harvestMs.toFixed(2)),
|
|
197
|
+
},
|
|
198
|
+
feed: {
|
|
199
|
+
trained,
|
|
200
|
+
failed,
|
|
201
|
+
avgLatencyMs: items.length > 0 ? Number((feedMs / items.length).toFixed(2)) : 0,
|
|
202
|
+
totalMs: Number(feedMs.toFixed(2)),
|
|
203
|
+
sampleFailures: failures,
|
|
204
|
+
},
|
|
205
|
+
seedNeuralStore: {
|
|
206
|
+
stored: seedResult.stored,
|
|
207
|
+
total: seedResult.total,
|
|
208
|
+
seedMs: Number(seedMs.toFixed(2)),
|
|
209
|
+
},
|
|
210
|
+
before,
|
|
211
|
+
after,
|
|
212
|
+
deltas,
|
|
213
|
+
consistency: unified1.consistency,
|
|
214
|
+
passed:
|
|
215
|
+
trained === items.length &&
|
|
216
|
+
deltas.trajectoriesRecorded >= items.length &&
|
|
217
|
+
deltas.neuralPatternCount >= items.length,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
if (process.env.BENCH_JSON) {
|
|
221
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
222
|
+
} else {
|
|
223
|
+
console.log('');
|
|
224
|
+
console.log('| Counter | Before | After | Δ |');
|
|
225
|
+
console.log('|---|---:|---:|---:|');
|
|
226
|
+
for (const k of Object.keys(after)) {
|
|
227
|
+
console.log(`| ${k} | ${before[k]} | ${after[k]} | +${deltas[k]} |`);
|
|
228
|
+
}
|
|
229
|
+
console.log('');
|
|
230
|
+
console.log(`Trained via trajectory pipeline: ${trained}/${items.length}`);
|
|
231
|
+
console.log(`Failed: ${failed}`);
|
|
232
|
+
console.log(`Avg latency per trajectory: ${summary.feed.avgLatencyMs} ms`);
|
|
233
|
+
console.log(`Neural store seeded: ${seedResult.stored}/${seedResult.total}`);
|
|
234
|
+
console.log(`Overall: ${summary.passed ? '✅ PASSED' : '⚠️ partial'}`);
|
|
235
|
+
if (unified1.consistency.notes.length > 0) {
|
|
236
|
+
console.log(`\nConsistency notes:`);
|
|
237
|
+
for (const n of unified1.consistency.notes) console.log(` • ${n}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!process.env.BENCH_NO_WRITE) {
|
|
242
|
+
mkdirSync(RUNS_DIR, { recursive: true });
|
|
243
|
+
const stamp = summary.runAt.replace(/[:.]/g, '-');
|
|
244
|
+
writeFileSync(join(RUNS_DIR, `pretrain-from-github-${stamp}.json`), JSON.stringify(summary, null, 2));
|
|
245
|
+
writeFileSync(join(RUNS_DIR, 'pretrain-from-github-latest.json'), JSON.stringify(summary, null, 2));
|
|
246
|
+
if (!process.env.BENCH_JSON) console.log(`\nWrote ${join(RUNS_DIR, `pretrain-from-github-${stamp}.json`)}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ONNX runtime keeps a worker thread alive — force exit so this can be used
|
|
250
|
+
// as a CI step or chained with other scripts.
|
|
251
|
+
process.exit(summary.passed ? 0 : 1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
main().catch((err) => { console.error(err); process.exit(1); });
|