@claude-flow/cli 3.32.33 → 3.32.35
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/helpers.manifest.json +2 -2
- package/catalog-manifest.json +4 -4
- package/dist/src/commands/agent.js +49 -1
- package/dist/src/commands/hooks.js +24 -0
- package/dist/src/commands/metaharness.js +4 -0
- package/dist/src/commands/swarm.js +61 -5
- package/dist/src/config-adapter.js +1 -0
- package/dist/src/index.js +3 -2
- package/dist/src/mcp-tools/agent-tools.js +10 -0
- package/dist/src/mcp-tools/hooks-tools.js +86 -52
- package/dist/src/mcp-tools/memory-tools.js +14 -2
- package/dist/src/mcp-tools/metaharness-tools.js +7 -0
- package/dist/src/mcp-tools/swarm-tools.d.ts +16 -0
- package/dist/src/mcp-tools/swarm-tools.js +172 -7
- package/dist/src/memory/memory-bridge.d.ts +42 -0
- package/dist/src/memory/memory-bridge.js +144 -40
- package/dist/src/memory/memory-initializer.d.ts +9 -1
- package/dist/src/memory/memory-initializer.js +95 -31
- package/dist/src/services/daemon-autostart.js +7 -4
- package/dist/src/services/flywheel-receipt.d.ts +3 -0
- package/dist/src/services/flywheel-receipt.js +1 -0
- package/dist/src/services/harness-flywheel-runtime.d.ts +6 -0
- package/dist/src/services/harness-flywheel-runtime.js +19 -19
- package/dist/src/services/harness-flywheel.d.ts +2 -0
- package/dist/src/services/harness-flywheel.js +1 -0
- package/dist/src/services/harness-project-anchor.d.ts +23 -0
- package/dist/src/services/harness-project-anchor.js +129 -0
- package/dist/src/services/learned-routing.d.ts +34 -0
- package/dist/src/services/learned-routing.js +85 -0
- package/dist/src/services/pheromone-adaptive.d.ts +71 -0
- package/dist/src/services/pheromone-adaptive.js +214 -0
- package/dist/src/services/worker-daemon.js +4 -1
- package/package.json +1 -1
|
@@ -176,6 +176,32 @@ async function getBridge() {
|
|
|
176
176
|
return null;
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Build the WAL-sidecar refusal message, naming the bridge failure when one
|
|
181
|
+
* was recorded.
|
|
182
|
+
*
|
|
183
|
+
* The refusal tells the operator to "restore the native better-sqlite3
|
|
184
|
+
* bridge" but, on its own, gives them no way to discover why it is down —
|
|
185
|
+
* and the usual cause is a latched init failure inside the bridge, not a
|
|
186
|
+
* missing better-sqlite3. Appending the recorded reason turns an unactionable
|
|
187
|
+
* message into a diagnosis.
|
|
188
|
+
*/
|
|
189
|
+
async function walRefusalError(operation) {
|
|
190
|
+
const base = 'memory database has an active native WAL connection '
|
|
191
|
+
+ '(found -wal/-shm sidecar files) — refusing an unsafe sql.js '
|
|
192
|
+
+ `whole-image ${operation}. Retry once the native writer completes, or `
|
|
193
|
+
+ 'restore the native better-sqlite3 bridge.';
|
|
194
|
+
try {
|
|
195
|
+
const bridge = await getBridge();
|
|
196
|
+
const reason = bridge?.getBridgeFailureReason?.();
|
|
197
|
+
if (reason)
|
|
198
|
+
return `${base} Bridge unavailable: ${reason}`;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// Diagnostics must never mask the refusal they annotate.
|
|
202
|
+
}
|
|
203
|
+
return base;
|
|
204
|
+
}
|
|
179
205
|
/**
|
|
180
206
|
* Enhanced schema with pattern confidence, temporal decay, versioning
|
|
181
207
|
* Vector embeddings enabled for semantic search
|
|
@@ -654,6 +680,64 @@ function saveHNSWMetadata() {
|
|
|
654
680
|
// Silently fail - metadata save is best-effort
|
|
655
681
|
}
|
|
656
682
|
}
|
|
683
|
+
export function removeHNSWEntriesByLogicalKey(entries, key, namespace) {
|
|
684
|
+
let removed = 0;
|
|
685
|
+
for (const [id, entry] of entries) {
|
|
686
|
+
if (entry.key === key && (entry.namespace ?? 'default') === namespace) {
|
|
687
|
+
entries.delete(id);
|
|
688
|
+
removed++;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return removed;
|
|
692
|
+
}
|
|
693
|
+
function removeHNSWEntriesByKey(key, namespace) {
|
|
694
|
+
if (!hnswIndex?.entries)
|
|
695
|
+
return 0;
|
|
696
|
+
const removed = removeHNSWEntriesByLogicalKey(hnswIndex.entries, key, namespace);
|
|
697
|
+
if (removed > 0) {
|
|
698
|
+
saveHNSWMetadata();
|
|
699
|
+
rebuildSearchIndex();
|
|
700
|
+
}
|
|
701
|
+
return removed;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Remove HNSW metadata whose authoritative SQLite row is no longer active.
|
|
705
|
+
* Persistent graph nodes may remain physically allocated, but without metadata
|
|
706
|
+
* they cannot resolve into search results; the next rebuild repopulates only
|
|
707
|
+
* active rows. Returns the number of searchable vectors invalidated.
|
|
708
|
+
*/
|
|
709
|
+
export async function reconcileHNSWIndex(dbPath) {
|
|
710
|
+
if (!hnswIndex?.entries)
|
|
711
|
+
return 0;
|
|
712
|
+
const effectivePath = dbPath
|
|
713
|
+
? path.resolve(dbPath)
|
|
714
|
+
: path.join(getMemoryRoot(), 'memory.db');
|
|
715
|
+
if (!fs.existsSync(effectivePath))
|
|
716
|
+
return 0;
|
|
717
|
+
try {
|
|
718
|
+
const initSqlJs = (await import('sql.js')).default;
|
|
719
|
+
const SQL = await initSqlJs();
|
|
720
|
+
const db = new SQL.Database(readFileMaybeEncrypted(effectivePath, null));
|
|
721
|
+
const rows = db.exec(`SELECT id FROM memory_entries WHERE status = 'active'`);
|
|
722
|
+
const active = new Set((rows[0]?.values ?? []).map((row) => String(row[0])));
|
|
723
|
+
db.close();
|
|
724
|
+
let removed = 0;
|
|
725
|
+
for (const id of hnswIndex.entries.keys()) {
|
|
726
|
+
if (!active.has(id)) {
|
|
727
|
+
hnswIndex.entries.delete(id);
|
|
728
|
+
removed++;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (removed > 0) {
|
|
732
|
+
saveHNSWMetadata();
|
|
733
|
+
rebuildSearchIndex();
|
|
734
|
+
}
|
|
735
|
+
return removed;
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
return 0;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
657
741
|
/**
|
|
658
742
|
* Add entry to HNSW index (with automatic persistence)
|
|
659
743
|
*/
|
|
@@ -2354,6 +2438,9 @@ export async function storeEntry(options) {
|
|
|
2354
2438
|
// Keep HNSW index in sync with bridge-stored entries
|
|
2355
2439
|
if (bridgeResult.rawEmbedding && bridgeResult.success) {
|
|
2356
2440
|
const ns = options.namespace || 'default';
|
|
2441
|
+
// Upsert/resurrection may allocate a new row id. Remove every older
|
|
2442
|
+
// vector for the logical (namespace,key), not merely the newest id.
|
|
2443
|
+
removeHNSWEntriesByKey(options.key, ns);
|
|
2357
2444
|
await addToHNSWIndex(bridgeResult.id, bridgeResult.rawEmbedding, {
|
|
2358
2445
|
id: bridgeResult.id,
|
|
2359
2446
|
key: options.key,
|
|
@@ -2382,10 +2469,7 @@ export async function storeEntry(options) {
|
|
|
2382
2469
|
return {
|
|
2383
2470
|
success: false,
|
|
2384
2471
|
id: '',
|
|
2385
|
-
error:
|
|
2386
|
-
'(found -wal/-shm sidecar files) — refusing an unsafe sql.js ' +
|
|
2387
|
-
'whole-image write. Retry once the native writer completes, or ' +
|
|
2388
|
-
'restore the native better-sqlite3 bridge.',
|
|
2472
|
+
error: await walRefusalError('write'),
|
|
2389
2473
|
};
|
|
2390
2474
|
}
|
|
2391
2475
|
// Ensure schema has all required columns (migration for older DBs)
|
|
@@ -2462,6 +2546,8 @@ export async function storeEntry(options) {
|
|
|
2462
2546
|
// Add to HNSW index for faster future searches
|
|
2463
2547
|
if (embeddingJson) {
|
|
2464
2548
|
const embResult = JSON.parse(embeddingJson);
|
|
2549
|
+
if (upsert)
|
|
2550
|
+
removeHNSWEntriesByKey(key, namespace);
|
|
2465
2551
|
await addToHNSWIndex(id, embResult, {
|
|
2466
2552
|
id,
|
|
2467
2553
|
key,
|
|
@@ -2868,10 +2954,7 @@ export async function getEntry(options) {
|
|
|
2868
2954
|
return {
|
|
2869
2955
|
success: false,
|
|
2870
2956
|
found: false,
|
|
2871
|
-
error:
|
|
2872
|
-
'(found -wal/-shm sidecar files) — refusing an unsafe sql.js ' +
|
|
2873
|
-
'whole-image read/write. Retry once the native writer completes, ' +
|
|
2874
|
-
'or restore the native better-sqlite3 bridge.',
|
|
2957
|
+
error: await walRefusalError('read/write'),
|
|
2875
2958
|
};
|
|
2876
2959
|
}
|
|
2877
2960
|
// Ensure schema has all required columns (migration for older DBs)
|
|
@@ -2957,15 +3040,7 @@ export async function deleteEntry(options) {
|
|
|
2957
3040
|
// #1122: Bridge path must also invalidate the in-memory HNSW index.
|
|
2958
3041
|
// Without this, deleted vectors remain as ghost entries in search results.
|
|
2959
3042
|
if (bridgeResult.deleted && hnswIndex?.entries) {
|
|
2960
|
-
|
|
2961
|
-
for (const [id, entry] of hnswIndex.entries) {
|
|
2962
|
-
if (entry?.key === options.key && (entry?.namespace ?? 'default') === (options.namespace ?? 'default')) {
|
|
2963
|
-
hnswIndex.entries.delete(id);
|
|
2964
|
-
break;
|
|
2965
|
-
}
|
|
2966
|
-
}
|
|
2967
|
-
saveHNSWMetadata();
|
|
2968
|
-
rebuildSearchIndex();
|
|
3043
|
+
removeHNSWEntriesByKey(options.key, options.namespace ?? 'default');
|
|
2969
3044
|
}
|
|
2970
3045
|
return bridgeResult;
|
|
2971
3046
|
}
|
|
@@ -2994,10 +3069,7 @@ export async function deleteEntry(options) {
|
|
|
2994
3069
|
key,
|
|
2995
3070
|
namespace,
|
|
2996
3071
|
remainingEntries: 0,
|
|
2997
|
-
error:
|
|
2998
|
-
'(found -wal/-shm sidecar files) — refusing an unsafe sql.js ' +
|
|
2999
|
-
'whole-image write. Retry once the native writer completes, or ' +
|
|
3000
|
-
'restore the native better-sqlite3 bridge.',
|
|
3072
|
+
error: await walRefusalError('write'),
|
|
3001
3073
|
};
|
|
3002
3074
|
}
|
|
3003
3075
|
// Ensure schema has all required columns (migration for older DBs)
|
|
@@ -3035,8 +3107,6 @@ export async function deleteEntry(options) {
|
|
|
3035
3107
|
error: `Key '${key}' not found in namespace '${namespace}'`
|
|
3036
3108
|
};
|
|
3037
3109
|
}
|
|
3038
|
-
// Capture the entry ID for HNSW cleanup
|
|
3039
|
-
const entryId = String(checkResult[0].values[0][0]);
|
|
3040
3110
|
// Delete the entry (soft delete by setting status to 'deleted')
|
|
3041
3111
|
// Also null out the embedding to clean up vector data from SQLite
|
|
3042
3112
|
db.run(`
|
|
@@ -3058,14 +3128,8 @@ export async function deleteEntry(options) {
|
|
|
3058
3128
|
// Clean up in-memory HNSW index so ghost vectors don't appear in searches.
|
|
3059
3129
|
// Remove the entry from the HNSW entries map and invalidate the index.
|
|
3060
3130
|
// The next search will rebuild the HNSW index from the remaining DB rows.
|
|
3061
|
-
if (hnswIndex?.entries)
|
|
3062
|
-
|
|
3063
|
-
saveHNSWMetadata();
|
|
3064
|
-
// Invalidate the HNSW index so it rebuilds from DB on next search.
|
|
3065
|
-
// We can't surgically remove a vector from the HNSW graph, so we
|
|
3066
|
-
// clear the entire index; it will be lazily rebuilt from SQLite.
|
|
3067
|
-
rebuildSearchIndex();
|
|
3068
|
-
}
|
|
3131
|
+
if (hnswIndex?.entries)
|
|
3132
|
+
removeHNSWEntriesByKey(key, namespace);
|
|
3069
3133
|
return {
|
|
3070
3134
|
success: true,
|
|
3071
3135
|
deleted: true,
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* - single-instance: only starts when no live daemon holds the pidfile, and
|
|
9
9
|
* the spawned `daemon start` independently enforces single-instance via its
|
|
10
10
|
* own lock + checkExistingDaemon() — so a race spawns at most one survivor,
|
|
11
|
-
* - bounded lifetime: the daemon self-terminates on TTL/idle (12h
|
|
12
|
-
*
|
|
11
|
+
* - bounded lifetime: the daemon self-terminates on TTL/idle (12h hard TTL,
|
|
12
|
+
* 30m idle default; RUFLO_DAEMON_TTL_SECS / RUFLO_DAEMON_IDLE_SECS) —
|
|
13
|
+
* auto-start never means "runs forever",
|
|
13
14
|
* - opt-out: RUFLO_DAEMON_AUTOSTART=0|false|no disables it entirely, OR a
|
|
14
15
|
* project-local `daemon.autostart: false` in claude-flow.config.json —
|
|
15
16
|
* the file-based opt-out exists because the env var only reaches a
|
|
@@ -85,8 +86,10 @@ export function ensureDaemonRunning(projectRoot, opts = {}) {
|
|
|
85
86
|
try {
|
|
86
87
|
if (autostartDisabled(projectRoot))
|
|
87
88
|
return { started: false, reason: 'disabled (RUFLO_DAEMON_AUTOSTART=0 or project config)' };
|
|
88
|
-
//
|
|
89
|
-
|
|
89
|
+
// `.claude/` belongs to Claude Code and is present in many repositories
|
|
90
|
+
// that have never initialized Ruflo. Only Ruflo's own state directory is
|
|
91
|
+
// an authorization signal for spawning a detached background process.
|
|
92
|
+
if (!fs.existsSync(path.join(projectRoot, '.claude-flow'))) {
|
|
90
93
|
return { started: false, reason: 'not a ruflo project' };
|
|
91
94
|
}
|
|
92
95
|
const alive = (opts.isAlive ?? isDaemonAlive)(projectRoot);
|
|
@@ -56,6 +56,8 @@ export interface FlywheelReceiptPayload {
|
|
|
56
56
|
gateVersion: string;
|
|
57
57
|
policySchemaVersion: string;
|
|
58
58
|
safetyEnvelopeRef: string;
|
|
59
|
+
/** Hash-pinned human relevance anchor used for this evaluation (#2840). */
|
|
60
|
+
anchorRef?: string;
|
|
59
61
|
requestedProposer: 'auto' | ProposerName;
|
|
60
62
|
effectiveProposer: ProposerName;
|
|
61
63
|
proposerSubstitution?: string;
|
|
@@ -86,6 +88,7 @@ export interface CreateReceiptInput {
|
|
|
86
88
|
gateVersion?: string;
|
|
87
89
|
policySchemaVersion?: string;
|
|
88
90
|
safetyEnvelopeRef: string;
|
|
91
|
+
anchorRef?: string;
|
|
89
92
|
requestedProposer?: 'auto' | ProposerName;
|
|
90
93
|
effectiveProposer?: ProposerName;
|
|
91
94
|
proposerSubstitution?: string;
|
|
@@ -206,6 +206,7 @@ export function createFlywheelReceipt(input) {
|
|
|
206
206
|
gateVersion: input.gateVersion ?? statistics.ruleVersion,
|
|
207
207
|
policySchemaVersion: input.policySchemaVersion ?? 'ruflo.retrieval-policy/v1',
|
|
208
208
|
safetyEnvelopeRef: input.safetyEnvelopeRef,
|
|
209
|
+
...(input.anchorRef ? { anchorRef: input.anchorRef } : {}),
|
|
209
210
|
requestedProposer: input.requestedProposer ?? 'local',
|
|
210
211
|
effectiveProposer: input.effectiveProposer ?? 'local',
|
|
211
212
|
...(input.proposerSubstitution ? { proposerSubstitution: input.proposerSubstitution } : {}),
|
|
@@ -39,6 +39,9 @@ export declare function runFlywheelWorker(projectRoot: string, opts?: {
|
|
|
39
39
|
allowSubstitutionPromotion?: boolean;
|
|
40
40
|
maxConcurrency?: number;
|
|
41
41
|
evaluationTimeoutMs?: number;
|
|
42
|
+
anchorPath?: string;
|
|
43
|
+
anchorHash?: string;
|
|
44
|
+
anchorManifestPath?: string;
|
|
42
45
|
}): Promise<FlywheelResult>;
|
|
43
46
|
/**
|
|
44
47
|
* Run ONE live COMPOUNDING generation against the persisted lineage (ADR-176
|
|
@@ -52,5 +55,8 @@ export declare function runFlywheelGenerationWorker(projectRoot: string, opts?:
|
|
|
52
55
|
sample?: number;
|
|
53
56
|
optInOverride?: boolean;
|
|
54
57
|
now?: number;
|
|
58
|
+
anchorPath?: string;
|
|
59
|
+
anchorHash?: string;
|
|
60
|
+
anchorManifestPath?: string;
|
|
55
61
|
}): Promise<GenerationResult>;
|
|
56
62
|
//# sourceMappingURL=harness-flywheel-runtime.d.ts.map
|
|
@@ -7,23 +7,10 @@
|
|
|
7
7
|
import { harnessLoopOptedIn } from './harness-worker.js';
|
|
8
8
|
import { DEFAULT_CONFIG, retrievalPolicyNeighbors, runFlywheelTick, } from './harness-flywheel.js';
|
|
9
9
|
import { runFlywheelGeneration, checkServedChampionDrift } from './harness-flywheel-generations.js';
|
|
10
|
-
import {
|
|
10
|
+
import { loadEffectiveFlywheelAnchor } from './harness-project-anchor.js';
|
|
11
11
|
import { proposeFlywheelCandidates, } from './flywheel-proposer.js';
|
|
12
12
|
import { sha256Ref } from './flywheel-receipt.js';
|
|
13
13
|
import { evaluatePolicyRequest } from './policy-runtime.js';
|
|
14
|
-
/** The human-labeled ADR-081 anchor — the never-regress relevance set. */
|
|
15
|
-
const ANCHOR = [
|
|
16
|
-
['how was the Opus model alias fixed', ['opus 4.8', 'opus alias', 'opus model alias', '#2232']],
|
|
17
|
-
['self-learning wiring task-completed pretrain', ['self-learning', 'adr-074', 'self learning', '#2245', 'task-completed']],
|
|
18
|
-
['deterministic codemod engine var-to-const', ['deterministic tier-1 codemod', 'adr-143', 'codemod', 'var-to-const']],
|
|
19
|
-
['MCP server orphan leak parent-death', ['mcp orphan', 'mcp servers orphan', 'parent-death', '#2234', 'orphan on every claude']],
|
|
20
|
-
['unified learning stats aggregator', ['unified learning-stats', 'adr-075', 'unified learning stats']],
|
|
21
|
-
['structured distillation 4-field schema', ['structured distillation', 'adr-076', '4-field schema']],
|
|
22
|
-
['SQL injection migrate.ts table identifier', ['sql injection', 'shell injection', 'migrate.ts', 'agentdb', 'cve']],
|
|
23
|
-
['recall@k HNSW benchmark harness', ['hnsw', 'memory-recall', 'benchmark suite', 'recall@k', 'benchmark intelligence']],
|
|
24
|
-
['Q-learning encoder keyword block', ['q-state encoder', 'route q-state', 'keyword block', '#2239', 'q-encoder']],
|
|
25
|
-
['security hardening crypto random IDs', ['cwe-347', 'crypto.randomuuid', 'security fix', 'random id', 'crypto random']],
|
|
26
|
-
].map(([q, labels], i) => ({ id: `q${String(i).padStart(2, '0')}`, input: { id: `q${String(i).padStart(2, '0')}`, q: q }, expected: labels }));
|
|
27
14
|
/**
|
|
28
15
|
* The ADR-322 retrieval safety envelope.
|
|
29
16
|
*
|
|
@@ -99,6 +86,11 @@ export async function runFlywheelWorker(projectRoot, opts = {}) {
|
|
|
99
86
|
...(applier.activeChampion(projectRoot)?.params ?? {}),
|
|
100
87
|
};
|
|
101
88
|
const safetyEnvelope = retrievalSafetyEnvelope(opts.safetyEnvelopeRef);
|
|
89
|
+
const anchor = loadEffectiveFlywheelAnchor(projectRoot, {
|
|
90
|
+
anchorPath: opts.anchorPath ?? process.env.RUFLO_FLYWHEEL_ANCHOR_PATH,
|
|
91
|
+
anchorHash: opts.anchorHash ?? process.env.RUFLO_FLYWHEEL_ANCHOR_HASH,
|
|
92
|
+
manifestPath: opts.anchorManifestPath ?? process.env.RUFLO_FLYWHEEL_ANCHOR_MANIFEST,
|
|
93
|
+
});
|
|
102
94
|
// CLI flag opts.proposer takes precedence over RUFLO_FLYWHEEL_PROPOSER.
|
|
103
95
|
const proposerMode = opts.proposer
|
|
104
96
|
?? (process.env.RUFLO_FLYWHEEL_PROPOSER ?? 'auto');
|
|
@@ -134,7 +126,8 @@ export async function runFlywheelWorker(projectRoot, opts = {}) {
|
|
|
134
126
|
const r = await tool.handler({ action: 'search', query, mode: 'hybrid', limit: 5, rerank: false, ...cfg });
|
|
135
127
|
return (r.results || []).slice(0, 5).map((m) => ({ id: m?.id ?? '', name: m?.name ?? '' }));
|
|
136
128
|
},
|
|
137
|
-
anchorTasks:
|
|
129
|
+
anchorTasks: anchor.tasks,
|
|
130
|
+
anchorRef: anchor.anchorRef,
|
|
138
131
|
activeParams: () => baseline,
|
|
139
132
|
sample: opts.sample ?? 40,
|
|
140
133
|
now: opts.now,
|
|
@@ -172,16 +165,23 @@ export async function runFlywheelGenerationWorker(projectRoot, opts = {}) {
|
|
|
172
165
|
const tool = neural.neuralTools.find((t) => t.name === 'neural_patterns');
|
|
173
166
|
if (!tool)
|
|
174
167
|
return { ran: false, reason: 'neural_patterns tool unavailable', generation: 0 };
|
|
175
|
-
|
|
176
|
-
|
|
168
|
+
const anchor = loadEffectiveFlywheelAnchor(projectRoot, {
|
|
169
|
+
anchorPath: opts.anchorPath ?? process.env.RUFLO_FLYWHEEL_ANCHOR_PATH,
|
|
170
|
+
anchorHash: opts.anchorHash ?? process.env.RUFLO_FLYWHEEL_ANCHOR_HASH,
|
|
171
|
+
manifestPath: opts.anchorManifestPath ?? process.env.RUFLO_FLYWHEEL_ANCHOR_MANIFEST,
|
|
172
|
+
});
|
|
177
173
|
const deps = {
|
|
178
174
|
getPatterns: () => neural.getStorePatterns(),
|
|
179
175
|
search: async (query, cfg) => {
|
|
180
176
|
const r = await tool.handler({ action: 'search', query, mode: 'hybrid', limit: 5, rerank: false, ...cfg });
|
|
181
177
|
return (r.results || []).slice(0, 5).map((m) => ({ id: m?.id ?? '', name: m?.name ?? '' }));
|
|
182
178
|
},
|
|
183
|
-
anchorTasks:
|
|
184
|
-
|
|
179
|
+
anchorTasks: anchor.tasks.map((task) => ({
|
|
180
|
+
id: task.id,
|
|
181
|
+
q: task.input.q,
|
|
182
|
+
labels: task.expected,
|
|
183
|
+
})),
|
|
184
|
+
humanEvalHash: anchor.anchorRef,
|
|
185
185
|
sample: opts.sample ?? 120,
|
|
186
186
|
now: opts.now ?? Date.now(),
|
|
187
187
|
};
|
|
@@ -30,6 +30,8 @@ export interface FlywheelDeps {
|
|
|
30
30
|
lineageId?: string;
|
|
31
31
|
evaluationRunId?: string;
|
|
32
32
|
safetyEnvelopeRef?: string;
|
|
33
|
+
/** Hash of the project-specific human-labelled objective. */
|
|
34
|
+
anchorRef?: string;
|
|
33
35
|
requestedProposer?: 'auto' | 'local' | 'darwin';
|
|
34
36
|
effectiveProposer?: 'local' | 'darwin';
|
|
35
37
|
proposerSubstitution?: string;
|
|
@@ -210,6 +210,7 @@ export async function evaluateFlywheelCandidate(projectRoot, deps) {
|
|
|
210
210
|
expectedLedgerHead: txState.ledgerHead,
|
|
211
211
|
candidatePolicy: candidate,
|
|
212
212
|
safetyEnvelopeRef,
|
|
213
|
+
anchorRef: deps.anchorRef,
|
|
213
214
|
requestedProposer: deps.requestedProposer ?? 'local',
|
|
214
215
|
effectiveProposer: deps.effectiveProposer ?? 'local',
|
|
215
216
|
proposerSubstitution: deps.proposerSubstitution,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AnchorTask } from './harness-flywheel.js';
|
|
2
|
+
export declare const PROJECT_ANCHOR_SCHEMA = "ruflo.flywheel-anchor/v1";
|
|
3
|
+
export declare const PROJECT_ANCHOR_MANIFEST_SCHEMA = "ruflo.flywheel-anchor-manifest/v1";
|
|
4
|
+
export declare const DEFAULT_PROJECT_ANCHOR_MANIFEST: string;
|
|
5
|
+
export interface ProjectAnchorManifest {
|
|
6
|
+
schemaVersion: typeof PROJECT_ANCHOR_MANIFEST_SCHEMA;
|
|
7
|
+
path: string;
|
|
8
|
+
sha256: string;
|
|
9
|
+
}
|
|
10
|
+
export interface FlywheelAnchorSelection {
|
|
11
|
+
version: string;
|
|
12
|
+
anchorRef: string;
|
|
13
|
+
tasks: AnchorTask[];
|
|
14
|
+
source: 'project' | 'ruflo-built-in';
|
|
15
|
+
path?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface LoadFlywheelAnchorOptions {
|
|
18
|
+
anchorPath?: string;
|
|
19
|
+
anchorHash?: string;
|
|
20
|
+
manifestPath?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function loadEffectiveFlywheelAnchor(projectRoot: string, options?: LoadFlywheelAnchorOptions): FlywheelAnchorSelection;
|
|
23
|
+
//# sourceMappingURL=harness-project-anchor.d.ts.map
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-local, hash-pinned flywheel anchors (#2840).
|
|
3
|
+
*
|
|
4
|
+
* Downstream repositories must be evaluated against their own labelled
|
|
5
|
+
* retrieval tasks. Silently using Ruflo's development-history benchmark makes
|
|
6
|
+
* the objective flat and indistinguishable from "already optimal", so foreign
|
|
7
|
+
* projects fail closed unless they supply a pinned anchor manifest.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readFileSync, realpathSync, } from 'node:fs';
|
|
10
|
+
import { dirname, isAbsolute, join, relative, resolve, sep, } from 'node:path';
|
|
11
|
+
import { FROZEN_HUMAN_EVAL_HASH, loadFrozenHumanEval, humanEvalHash, } from './harness-frozen-eval.js';
|
|
12
|
+
export const PROJECT_ANCHOR_SCHEMA = 'ruflo.flywheel-anchor/v1';
|
|
13
|
+
export const PROJECT_ANCHOR_MANIFEST_SCHEMA = 'ruflo.flywheel-anchor-manifest/v1';
|
|
14
|
+
export const DEFAULT_PROJECT_ANCHOR_MANIFEST = join('.claude', 'eval', 'flywheel-anchor.manifest.json');
|
|
15
|
+
function normalizeHash(value) {
|
|
16
|
+
const trimmed = value.trim().toLowerCase();
|
|
17
|
+
return trimmed.startsWith('sha256:') ? trimmed : `sha256:${trimmed}`;
|
|
18
|
+
}
|
|
19
|
+
function containedPath(projectRoot, requested) {
|
|
20
|
+
const root = realpathSync(resolve(projectRoot));
|
|
21
|
+
const absolute = isAbsolute(requested) ? resolve(requested) : resolve(root, requested);
|
|
22
|
+
const lexical = relative(root, absolute);
|
|
23
|
+
if (lexical === '..' || lexical.startsWith(`..${sep}`) || isAbsolute(lexical)) {
|
|
24
|
+
throw new Error('flywheel anchor path must stay inside project root');
|
|
25
|
+
}
|
|
26
|
+
const actual = realpathSync(absolute);
|
|
27
|
+
const physical = relative(root, actual);
|
|
28
|
+
if (physical === '..' || physical.startsWith(`..${sep}`) || isAbsolute(physical)) {
|
|
29
|
+
throw new Error('flywheel anchor symlink escapes project root');
|
|
30
|
+
}
|
|
31
|
+
return actual;
|
|
32
|
+
}
|
|
33
|
+
function parseTasks(path) {
|
|
34
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
35
|
+
if (parsed.schemaVersion && parsed.schemaVersion !== PROJECT_ANCHOR_SCHEMA) {
|
|
36
|
+
throw new Error(`unsupported flywheel anchor schema: ${parsed.schemaVersion}`);
|
|
37
|
+
}
|
|
38
|
+
if (!Array.isArray(parsed.tasks) || parsed.tasks.length < 4) {
|
|
39
|
+
throw new Error('project flywheel anchor requires at least 4 labelled tasks');
|
|
40
|
+
}
|
|
41
|
+
const ids = new Set();
|
|
42
|
+
for (const [index, task] of parsed.tasks.entries()) {
|
|
43
|
+
if (!task || typeof task.id !== 'string' || !/^[A-Za-z0-9._-]{1,128}$/.test(task.id)) {
|
|
44
|
+
throw new Error(`invalid anchor task id at index ${index}`);
|
|
45
|
+
}
|
|
46
|
+
if (ids.has(task.id))
|
|
47
|
+
throw new Error(`duplicate anchor task id: ${task.id}`);
|
|
48
|
+
ids.add(task.id);
|
|
49
|
+
if (typeof task.q !== 'string' || task.q.trim().length === 0) {
|
|
50
|
+
throw new Error(`anchor task ${task.id} has no query`);
|
|
51
|
+
}
|
|
52
|
+
if (!Array.isArray(task.labels) || task.labels.length === 0 || task.labels.some((label) => typeof label !== 'string' || !label.trim())) {
|
|
53
|
+
throw new Error(`anchor task ${task.id} requires non-empty string labels`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { version: parsed.version ?? 'project-anchor-v1', tasks: parsed.tasks };
|
|
57
|
+
}
|
|
58
|
+
function toSelection(path, expectedHash) {
|
|
59
|
+
const parsed = parseTasks(path);
|
|
60
|
+
const actualHash = humanEvalHash(parsed.tasks);
|
|
61
|
+
if (actualHash !== normalizeHash(expectedHash)) {
|
|
62
|
+
throw new Error(`project flywheel anchor hash mismatch (got ${actualHash}, pinned ${normalizeHash(expectedHash)})`);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
version: parsed.version,
|
|
66
|
+
anchorRef: actualHash,
|
|
67
|
+
source: 'project',
|
|
68
|
+
path,
|
|
69
|
+
tasks: parsed.tasks.map((task) => ({
|
|
70
|
+
id: task.id,
|
|
71
|
+
input: { id: task.id, q: task.q },
|
|
72
|
+
expected: task.labels,
|
|
73
|
+
})),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function isRufloRepository(projectRoot) {
|
|
77
|
+
try {
|
|
78
|
+
const pkg = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8'));
|
|
79
|
+
const repository = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url;
|
|
80
|
+
return ['claude-flow', 'ruflo', '@claude-flow/cli'].includes(pkg.name ?? '')
|
|
81
|
+
&& /github\.com[/:]ruvnet\/(?:ruflo|claude-flow)(?:\.git)?$/i.test(repository ?? '');
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export function loadEffectiveFlywheelAnchor(projectRoot, options = {}) {
|
|
88
|
+
const root = resolve(projectRoot);
|
|
89
|
+
if (!!options.anchorPath !== !!options.anchorHash) {
|
|
90
|
+
throw new Error('anchorPath and anchorHash must be supplied together');
|
|
91
|
+
}
|
|
92
|
+
if (options.anchorPath && options.anchorHash) {
|
|
93
|
+
return toSelection(containedPath(root, options.anchorPath), options.anchorHash);
|
|
94
|
+
}
|
|
95
|
+
const manifestCandidate = options.manifestPath ?? DEFAULT_PROJECT_ANCHOR_MANIFEST;
|
|
96
|
+
const manifestPath = isAbsolute(manifestCandidate)
|
|
97
|
+
? manifestCandidate
|
|
98
|
+
: resolve(root, manifestCandidate);
|
|
99
|
+
if (existsSync(manifestPath)) {
|
|
100
|
+
const containedManifest = containedPath(root, manifestPath);
|
|
101
|
+
const manifest = JSON.parse(readFileSync(containedManifest, 'utf8'));
|
|
102
|
+
if (manifest.schemaVersion !== PROJECT_ANCHOR_MANIFEST_SCHEMA) {
|
|
103
|
+
throw new Error(`unsupported flywheel anchor manifest schema: ${manifest.schemaVersion}`);
|
|
104
|
+
}
|
|
105
|
+
if (typeof manifest.path !== 'string' || typeof manifest.sha256 !== 'string') {
|
|
106
|
+
throw new Error('flywheel anchor manifest requires path and sha256');
|
|
107
|
+
}
|
|
108
|
+
// Manifest-relative paths are easier to relocate while remaining
|
|
109
|
+
// repository-contained; project-relative paths remain supported.
|
|
110
|
+
const manifestRelative = resolve(dirname(containedManifest), manifest.path);
|
|
111
|
+
const requested = existsSync(manifestRelative) ? manifestRelative : resolve(root, manifest.path);
|
|
112
|
+
return toSelection(containedPath(root, requested), manifest.sha256);
|
|
113
|
+
}
|
|
114
|
+
if (isRufloRepository(root) || process.env.RUFLO_FLYWHEEL_ALLOW_BUILTIN_ANCHOR === '1') {
|
|
115
|
+
const frozen = loadFrozenHumanEval();
|
|
116
|
+
return {
|
|
117
|
+
version: frozen.version,
|
|
118
|
+
anchorRef: FROZEN_HUMAN_EVAL_HASH,
|
|
119
|
+
source: 'ruflo-built-in',
|
|
120
|
+
tasks: frozen.tasks.map((task) => ({
|
|
121
|
+
id: task.id,
|
|
122
|
+
input: { id: task.id, q: task.q },
|
|
123
|
+
expected: task.labels,
|
|
124
|
+
})),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
throw new Error(`project-local flywheel anchor required; create ${DEFAULT_PROJECT_ANCHOR_MANIFEST} or pass anchorPath + anchorHash`);
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=harness-project-anchor.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discriminative routing-pattern learner (#2839).
|
|
3
|
+
*
|
|
4
|
+
* Outcomes are labelled by the agent that actually completed the task. The
|
|
5
|
+
* previous implementation collapsed every successful task for an agent into
|
|
6
|
+
* one insertion-ordered Set and kept the first 50 words forever. This module
|
|
7
|
+
* ranks terms by within-agent support, cross-agent rarity, outcome quality,
|
|
8
|
+
* and discriminative share. Generic terms therefore lose to stable,
|
|
9
|
+
* agent-specific evidence, and later outcomes can replace earlier noise.
|
|
10
|
+
*/
|
|
11
|
+
export interface LearnedRoutingOutcome {
|
|
12
|
+
task: string;
|
|
13
|
+
agent: string;
|
|
14
|
+
success: boolean;
|
|
15
|
+
quality: number;
|
|
16
|
+
keywords: string[];
|
|
17
|
+
timestamp: string;
|
|
18
|
+
}
|
|
19
|
+
export interface LearnedRoutingPattern {
|
|
20
|
+
keywords: string[];
|
|
21
|
+
agents: string[];
|
|
22
|
+
source: 'learned';
|
|
23
|
+
support: number;
|
|
24
|
+
reliability: number;
|
|
25
|
+
}
|
|
26
|
+
export interface LearnedRoutingOptions {
|
|
27
|
+
maxKeywords?: number;
|
|
28
|
+
minAgentOutcomes?: number;
|
|
29
|
+
minKeywordSupport?: number;
|
|
30
|
+
minOutcomeQuality?: number;
|
|
31
|
+
minDiscriminativeShare?: number;
|
|
32
|
+
}
|
|
33
|
+
export declare function buildLearnedRoutingPatterns(outcomes: LearnedRoutingOutcome[], options?: LearnedRoutingOptions): Record<string, LearnedRoutingPattern>;
|
|
34
|
+
//# sourceMappingURL=learned-routing.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discriminative routing-pattern learner (#2839).
|
|
3
|
+
*
|
|
4
|
+
* Outcomes are labelled by the agent that actually completed the task. The
|
|
5
|
+
* previous implementation collapsed every successful task for an agent into
|
|
6
|
+
* one insertion-ordered Set and kept the first 50 words forever. This module
|
|
7
|
+
* ranks terms by within-agent support, cross-agent rarity, outcome quality,
|
|
8
|
+
* and discriminative share. Generic terms therefore lose to stable,
|
|
9
|
+
* agent-specific evidence, and later outcomes can replace earlier noise.
|
|
10
|
+
*/
|
|
11
|
+
const finiteUnit = (value) => Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0;
|
|
12
|
+
export function buildLearnedRoutingPatterns(outcomes, options = {}) {
|
|
13
|
+
const maxKeywords = Math.max(1, Math.min(options.maxKeywords ?? 50, 100));
|
|
14
|
+
const minAgentOutcomes = Math.max(1, options.minAgentOutcomes ?? 2);
|
|
15
|
+
const minKeywordSupport = Math.max(1, options.minKeywordSupport ?? 2);
|
|
16
|
+
const minOutcomeQuality = finiteUnit(options.minOutcomeQuality ?? 0.65);
|
|
17
|
+
const minDiscriminativeShare = finiteUnit(options.minDiscriminativeShare ?? 0.6);
|
|
18
|
+
const accepted = outcomes.filter((outcome) => outcome.success
|
|
19
|
+
&& typeof outcome.agent === 'string'
|
|
20
|
+
&& outcome.agent.length > 0
|
|
21
|
+
&& finiteUnit(outcome.quality) >= minOutcomeQuality
|
|
22
|
+
&& Array.isArray(outcome.keywords)
|
|
23
|
+
&& outcome.keywords.length > 0);
|
|
24
|
+
const agents = [...new Set(accepted.map((outcome) => outcome.agent))].sort();
|
|
25
|
+
if (agents.length === 0)
|
|
26
|
+
return {};
|
|
27
|
+
const agentOutcomes = new Map();
|
|
28
|
+
const evidence = new Map();
|
|
29
|
+
const globalSupport = new Map();
|
|
30
|
+
const agentDocumentFrequency = new Map();
|
|
31
|
+
for (const agent of agents) {
|
|
32
|
+
const rows = accepted.filter((outcome) => outcome.agent === agent);
|
|
33
|
+
agentOutcomes.set(agent, rows);
|
|
34
|
+
const perKeyword = new Map();
|
|
35
|
+
for (const row of rows) {
|
|
36
|
+
const unique = new Set(row.keywords.map((keyword) => keyword.trim().toLowerCase()).filter(Boolean));
|
|
37
|
+
for (const keyword of unique) {
|
|
38
|
+
const current = perKeyword.get(keyword) ?? { support: 0, qualityTotal: 0 };
|
|
39
|
+
current.support++;
|
|
40
|
+
current.qualityTotal += finiteUnit(row.quality);
|
|
41
|
+
perKeyword.set(keyword, current);
|
|
42
|
+
globalSupport.set(keyword, (globalSupport.get(keyword) ?? 0) + 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
evidence.set(agent, perKeyword);
|
|
46
|
+
for (const keyword of perKeyword.keys()) {
|
|
47
|
+
agentDocumentFrequency.set(keyword, (agentDocumentFrequency.get(keyword) ?? 0) + 1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const patterns = {};
|
|
51
|
+
for (const agent of agents) {
|
|
52
|
+
const rows = agentOutcomes.get(agent) ?? [];
|
|
53
|
+
if (rows.length < minAgentOutcomes)
|
|
54
|
+
continue;
|
|
55
|
+
const ranked = [...(evidence.get(agent) ?? new Map()).entries()]
|
|
56
|
+
.filter(([, item]) => item.support >= minKeywordSupport)
|
|
57
|
+
.map(([keyword, item]) => {
|
|
58
|
+
const totalSupport = globalSupport.get(keyword) ?? item.support;
|
|
59
|
+
const discriminativeShare = item.support / Math.max(1, totalSupport);
|
|
60
|
+
const documentFrequency = agentDocumentFrequency.get(keyword) ?? 1;
|
|
61
|
+
const idf = Math.log(1 + agents.length / documentFrequency);
|
|
62
|
+
const withinAgentSupport = item.support / rows.length;
|
|
63
|
+
const meanQuality = item.qualityTotal / item.support;
|
|
64
|
+
return {
|
|
65
|
+
keyword,
|
|
66
|
+
discriminativeShare,
|
|
67
|
+
score: withinAgentSupport * meanQuality * idf * discriminativeShare,
|
|
68
|
+
};
|
|
69
|
+
})
|
|
70
|
+
.filter((item) => item.discriminativeShare >= minDiscriminativeShare)
|
|
71
|
+
.sort((a, b) => b.score - a.score || a.keyword.localeCompare(b.keyword))
|
|
72
|
+
.slice(0, maxKeywords);
|
|
73
|
+
if (ranked.length === 0)
|
|
74
|
+
continue;
|
|
75
|
+
patterns[`learned-${agent}`] = {
|
|
76
|
+
keywords: ranked.map((item) => item.keyword),
|
|
77
|
+
agents: [agent],
|
|
78
|
+
source: 'learned',
|
|
79
|
+
support: rows.length,
|
|
80
|
+
reliability: rows.reduce((sum, row) => sum + finiteUnit(row.quality), 0) / rows.length,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return patterns;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=learned-routing.js.map
|