@claude-flow/cli 3.32.34 → 3.32.36
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 +5 -5
- package/.claude/helpers/hook-handler.cjs +30 -0
- package/.claude/helpers/intelligence.cjs +47 -4
- package/.claude/helpers/statusline.cjs +169 -6
- package/bin/cli.js +25 -1
- package/catalog-manifest.json +4 -4
- package/dist/src/commands/agent.js +50 -2
- package/dist/src/commands/doctor.js +159 -11
- package/dist/src/commands/hooks.js +63 -4
- package/dist/src/commands/mcp.js +1 -1
- package/dist/src/commands/memory-distill.js +1 -0
- package/dist/src/commands/metaharness.js +4 -0
- package/dist/src/commands/swarm.js +89 -5
- package/dist/src/config-adapter.js +1 -0
- package/dist/src/index.js +3 -2
- package/dist/src/init/executor.js +16 -10
- package/dist/src/init/helpers-generator.js +15 -4
- package/dist/src/init/statusline-generator.js +18 -5
- package/dist/src/mcp-server.d.ts +25 -0
- package/dist/src/mcp-server.js +57 -2
- package/dist/src/mcp-tools/agent-tools.js +10 -0
- package/dist/src/mcp-tools/embeddings-tools.js +51 -10
- package/dist/src/mcp-tools/hooks-tools.js +120 -60
- 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 +2 -0
- package/dist/src/memory/memory-bridge.js +5 -1
- package/dist/src/memory/memory-initializer.d.ts +9 -1
- package/dist/src/memory/memory-initializer.js +66 -19
- 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/memory-distillation.d.ts +1 -0
- package/dist/src/services/memory-distillation.js +81 -5
- 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 +5 -1
- package/node_modules/@claude-flow/codex/.agents/skills/github-automation/SKILL.md +32 -0
- package/node_modules/@claude-flow/codex/.agents/skills/performance-analysis/SKILL.md +32 -0
- package/node_modules/@claude-flow/codex/dist/dual-mode/cli.d.ts +4 -0
- package/node_modules/@claude-flow/codex/dist/dual-mode/cli.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js +20 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts +4 -0
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js +18 -2
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/generators/skill-md.d.ts +11 -5
- package/node_modules/@claude-flow/codex/dist/generators/skill-md.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/generators/skill-md.js +84 -849
- package/node_modules/@claude-flow/codex/dist/generators/skill-md.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/initializer.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/initializer.js +9 -13
- package/node_modules/@claude-flow/codex/dist/initializer.js.map +1 -1
- package/package.json +1 -1
- package/plugins/ruflo-metaharness/scripts/_harness.mjs +5 -1
- package/plugins/ruflo-metaharness/scripts/_invoke.mjs +12 -13
- package/plugins/ruflo-metaharness/scripts/mcp-scan.mjs +7 -8
- package/plugins/ruflo-metaharness/scripts/smoke.sh +58 -2
- package/plugins/ruflo-metaharness/scripts/test-similarity.mjs +38 -0
- package/plugins/ruflo-metaharness/scripts/threat-model.mjs +4 -1
|
@@ -1840,11 +1840,15 @@ export async function bridgeRecordFeedback(options) {
|
|
|
1840
1840
|
try {
|
|
1841
1841
|
const intelligence = await import('./intelligence.js');
|
|
1842
1842
|
const verdict = options.success ? 'success' : 'failure';
|
|
1843
|
+
const taskContext = options.task?.trim();
|
|
1843
1844
|
const recorded = await intelligence.recordTrajectory([{
|
|
1844
1845
|
type: 'action',
|
|
1845
|
-
|
|
1846
|
+
// Put the task description first so the embedding represents the
|
|
1847
|
+
// work, not just the bookkeeping suffix (#2812).
|
|
1848
|
+
content: `${taskContext ? `${taskContext} — ` : ''}Task ${options.taskId} completed by ${options.agent || 'unknown'} — success=${options.success}, quality=${options.quality.toFixed(3)}`,
|
|
1846
1849
|
metadata: {
|
|
1847
1850
|
taskId: options.taskId,
|
|
1851
|
+
task: taskContext,
|
|
1848
1852
|
agent: options.agent,
|
|
1849
1853
|
quality: options.quality,
|
|
1850
1854
|
duration: options.duration,
|
|
@@ -36,7 +36,7 @@ export declare function resolveDbPath(cliFlag?: string): string;
|
|
|
36
36
|
* Vector embeddings enabled for semantic search
|
|
37
37
|
*/
|
|
38
38
|
export declare const MEMORY_SCHEMA_V3 = "\n-- RuFlo V3 Memory Database\n-- Version: 3.0.0\n-- Features: Pattern learning, vector embeddings, temporal decay, migration tracking\n\nPRAGMA journal_mode = WAL;\nPRAGMA synchronous = NORMAL;\nPRAGMA foreign_keys = ON;\n\n-- ============================================\n-- CORE MEMORY TABLES\n-- ============================================\n\n-- Memory entries (main storage)\nCREATE TABLE IF NOT EXISTS memory_entries (\n id TEXT PRIMARY KEY,\n key TEXT NOT NULL,\n namespace TEXT DEFAULT 'default',\n content TEXT NOT NULL,\n type TEXT DEFAULT 'semantic' CHECK(type IN ('semantic', 'episodic', 'procedural', 'working', 'pattern')),\n\n -- Vector embedding for semantic search (stored as JSON array)\n embedding TEXT,\n embedding_model TEXT DEFAULT 'local',\n embedding_dimensions INTEGER,\n\n -- Metadata\n tags TEXT, -- JSON array\n metadata TEXT, -- JSON object\n owner_id TEXT,\n\n -- ADR-323: who/what produced this entry \u2014 lets shared-namespace retrieval\n -- filter by trust level instead of conflating a user's stated claim with\n -- an agent's own output or a raw tool/system observation.\n provenance_type TEXT DEFAULT 'unknown' CHECK(provenance_type IN (\n 'user_claim', 'agent_output', 'system_observation', 'tool_result', 'unknown'\n )),\n\n -- Timestamps\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),\n expires_at INTEGER,\n last_accessed_at INTEGER,\n\n -- Access tracking for hot/cold detection\n access_count INTEGER DEFAULT 0,\n\n -- Status\n status TEXT DEFAULT 'active' CHECK(status IN ('active', 'archived', 'deleted')),\n\n UNIQUE(namespace, key)\n);\n\n-- Indexes for memory entries\nCREATE INDEX IF NOT EXISTS idx_memory_namespace ON memory_entries(namespace);\nCREATE INDEX IF NOT EXISTS idx_memory_key ON memory_entries(key);\nCREATE INDEX IF NOT EXISTS idx_memory_type ON memory_entries(type);\nCREATE INDEX IF NOT EXISTS idx_memory_status ON memory_entries(status);\nCREATE INDEX IF NOT EXISTS idx_memory_created ON memory_entries(created_at);\nCREATE INDEX IF NOT EXISTS idx_memory_accessed ON memory_entries(last_accessed_at);\nCREATE INDEX IF NOT EXISTS idx_memory_owner ON memory_entries(owner_id);\n\n-- ============================================\n-- PATTERN LEARNING TABLES\n-- ============================================\n\n-- Learned patterns with confidence scoring and versioning\nCREATE TABLE IF NOT EXISTS patterns (\n id TEXT PRIMARY KEY,\n\n -- Pattern identification\n name TEXT NOT NULL,\n pattern_type TEXT NOT NULL CHECK(pattern_type IN (\n 'task-routing', 'error-recovery', 'optimization', 'learning',\n 'coordination', 'prediction', 'code-pattern', 'workflow'\n )),\n\n -- Pattern definition\n condition TEXT NOT NULL, -- Regex or semantic match\n action TEXT NOT NULL, -- What to do when pattern matches\n description TEXT,\n\n -- Confidence scoring (0.0 - 1.0)\n confidence REAL DEFAULT 0.5,\n success_count INTEGER DEFAULT 0,\n failure_count INTEGER DEFAULT 0,\n\n -- Temporal decay\n decay_rate REAL DEFAULT 0.01, -- How fast confidence decays\n half_life_days INTEGER DEFAULT 30, -- Days until confidence halves without use\n\n -- Vector embedding for semantic pattern matching\n embedding TEXT,\n embedding_dimensions INTEGER,\n\n -- Versioning\n version INTEGER DEFAULT 1,\n parent_id TEXT REFERENCES patterns(id),\n\n -- Metadata\n tags TEXT, -- JSON array\n metadata TEXT, -- JSON object\n source TEXT, -- Where the pattern was learned from\n\n -- Timestamps\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),\n last_matched_at INTEGER,\n last_success_at INTEGER,\n last_failure_at INTEGER,\n\n -- Status\n status TEXT DEFAULT 'active' CHECK(status IN ('active', 'archived', 'deprecated', 'experimental'))\n);\n\n-- Indexes for patterns\nCREATE INDEX IF NOT EXISTS idx_patterns_type ON patterns(pattern_type);\nCREATE INDEX IF NOT EXISTS idx_patterns_confidence ON patterns(confidence DESC);\nCREATE INDEX IF NOT EXISTS idx_patterns_status ON patterns(status);\nCREATE INDEX IF NOT EXISTS idx_patterns_last_matched ON patterns(last_matched_at);\n\n-- Pattern evolution history (for versioning)\nCREATE TABLE IF NOT EXISTS pattern_history (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n pattern_id TEXT NOT NULL REFERENCES patterns(id),\n version INTEGER NOT NULL,\n\n -- Snapshot of pattern state\n confidence REAL,\n success_count INTEGER,\n failure_count INTEGER,\n condition TEXT,\n action TEXT,\n\n -- What changed\n change_type TEXT CHECK(change_type IN ('created', 'updated', 'success', 'failure', 'decay', 'merged', 'split')),\n change_reason TEXT,\n\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)\n);\n\nCREATE INDEX IF NOT EXISTS idx_pattern_history_pattern ON pattern_history(pattern_id);\n\n-- ============================================\n-- LEARNING & TRAJECTORY TABLES\n-- ============================================\n\n-- Learning trajectories (SONA integration)\nCREATE TABLE IF NOT EXISTS trajectories (\n id TEXT PRIMARY KEY,\n session_id TEXT,\n\n -- Trajectory state\n status TEXT DEFAULT 'active' CHECK(status IN ('active', 'completed', 'failed', 'abandoned')),\n verdict TEXT CHECK(verdict IN ('success', 'failure', 'partial', NULL)),\n\n -- Context\n task TEXT,\n context TEXT, -- JSON object\n\n -- Metrics\n total_steps INTEGER DEFAULT 0,\n total_reward REAL DEFAULT 0,\n\n -- Timestamps\n started_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),\n ended_at INTEGER,\n\n -- Reference to extracted pattern (if any)\n extracted_pattern_id TEXT REFERENCES patterns(id)\n);\n\n-- Trajectory steps\nCREATE TABLE IF NOT EXISTS trajectory_steps (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n trajectory_id TEXT NOT NULL REFERENCES trajectories(id),\n step_number INTEGER NOT NULL,\n\n -- Step data\n action TEXT NOT NULL,\n observation TEXT,\n reward REAL DEFAULT 0,\n\n -- Metadata\n metadata TEXT, -- JSON object\n\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)\n);\n\nCREATE INDEX IF NOT EXISTS idx_steps_trajectory ON trajectory_steps(trajectory_id);\n\n-- ============================================\n-- MIGRATION STATE TRACKING\n-- ============================================\n\n-- Migration state (for resume capability)\nCREATE TABLE IF NOT EXISTS migration_state (\n id TEXT PRIMARY KEY,\n migration_type TEXT NOT NULL, -- 'v2-to-v3', 'pattern', 'memory', etc.\n\n -- Progress tracking\n status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'in_progress', 'completed', 'failed', 'rolled_back')),\n total_items INTEGER DEFAULT 0,\n processed_items INTEGER DEFAULT 0,\n failed_items INTEGER DEFAULT 0,\n skipped_items INTEGER DEFAULT 0,\n\n -- Current position (for resume)\n current_batch INTEGER DEFAULT 0,\n last_processed_id TEXT,\n\n -- Source/destination info\n source_path TEXT,\n source_type TEXT,\n destination_path TEXT,\n\n -- Backup info\n backup_path TEXT,\n backup_created_at INTEGER,\n\n -- Error tracking\n last_error TEXT,\n errors TEXT, -- JSON array of errors\n\n -- Timestamps\n started_at INTEGER,\n completed_at INTEGER,\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)\n);\n\n-- ============================================\n-- SESSION MANAGEMENT\n-- ============================================\n\n-- Sessions for context persistence\nCREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY,\n\n -- Session state\n state TEXT NOT NULL, -- JSON object with full session state\n status TEXT DEFAULT 'active' CHECK(status IN ('active', 'paused', 'completed', 'expired')),\n\n -- Context\n project_path TEXT,\n branch TEXT,\n\n -- Metrics\n tasks_completed INTEGER DEFAULT 0,\n patterns_learned INTEGER DEFAULT 0,\n\n -- Timestamps\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),\n expires_at INTEGER\n);\n\n-- ============================================\n-- VECTOR INDEX METADATA (for HNSW)\n-- ============================================\n\n-- Track HNSW index state\nCREATE TABLE IF NOT EXISTS vector_indexes (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n\n -- Index configuration\n dimensions INTEGER NOT NULL,\n metric TEXT DEFAULT 'cosine' CHECK(metric IN ('cosine', 'euclidean', 'dot')),\n\n -- HNSW parameters\n hnsw_m INTEGER DEFAULT 16,\n hnsw_ef_construction INTEGER DEFAULT 200,\n hnsw_ef_search INTEGER DEFAULT 100,\n\n -- Quantization\n quantization_type TEXT CHECK(quantization_type IN ('none', 'scalar', 'product')),\n quantization_bits INTEGER DEFAULT 8,\n\n -- Statistics\n total_vectors INTEGER DEFAULT 0,\n last_rebuild_at INTEGER,\n\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)\n);\n\n-- ============================================\n-- GRAPH EDGES (ADR-130 Phase 1)\n-- Unified knowledge graph backend \u2014 sql.js canonical store\n-- ============================================\n\n-- Unified graph edges table (ADR-130)\n-- Node IDs use domain-prefixed format: {domain}:{uuid}\n-- where domain in (mem, agent, task, entity, span, pattern)\nCREATE TABLE IF NOT EXISTS graph_edges (\n id TEXT PRIMARY KEY, -- edge-{uuid}\n source_id TEXT NOT NULL, -- domain-prefixed node ID\n target_id TEXT NOT NULL, -- domain-prefixed node ID\n relation TEXT NOT NULL, -- e.g. \"caused\", \"depends-on\", \"imports\"\n weight REAL DEFAULT 1.0,\n -- Temporal / reliability semantics (ADR-130 \u00A7\"graph that forgets\" property)\n confidence REAL DEFAULT 1.0, -- [0,1]; updated by JUDGE step\n decay_rate REAL DEFAULT 0.0, -- per-day exponential decay applied at read time\n last_reinforced TEXT, -- ISO-8601; set when CONSOLIDATE re-touches edge\n witness_id TEXT, -- FK to verification/witness-fixes.json (ADR-103)\n -- Embedding storage: \"inline:{base64}\" | \"vector_indexes:{id}\" | NULL\n embedding_ref TEXT,\n metadata TEXT, -- JSON blob for plugin-specific fields\n created_at TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_graph_edges_source ON graph_edges (source_id);\nCREATE INDEX IF NOT EXISTS idx_graph_edges_target ON graph_edges (target_id);\nCREATE INDEX IF NOT EXISTS idx_graph_edges_relation ON graph_edges (relation);\nCREATE INDEX IF NOT EXISTS idx_graph_edges_reinforced ON graph_edges (last_reinforced);\n\n-- ============================================\n-- SYSTEM METADATA\n-- ============================================\n\nCREATE TABLE IF NOT EXISTS metadata (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at INTEGER DEFAULT (strftime('%s', 'now') * 1000)\n);\n";
|
|
39
|
-
interface HNSWEntry {
|
|
39
|
+
export interface HNSWEntry {
|
|
40
40
|
id: string;
|
|
41
41
|
key: string;
|
|
42
42
|
namespace: string;
|
|
@@ -57,6 +57,14 @@ export declare function getHNSWIndex(options?: {
|
|
|
57
57
|
dimensions?: number;
|
|
58
58
|
forceRebuild?: boolean;
|
|
59
59
|
}): Promise<HNSWIndex | null>;
|
|
60
|
+
export declare function removeHNSWEntriesByLogicalKey(entries: Map<string, HNSWEntry>, key: string, namespace: string): number;
|
|
61
|
+
/**
|
|
62
|
+
* Remove HNSW metadata whose authoritative SQLite row is no longer active.
|
|
63
|
+
* Persistent graph nodes may remain physically allocated, but without metadata
|
|
64
|
+
* they cannot resolve into search results; the next rebuild repopulates only
|
|
65
|
+
* active rows. Returns the number of searchable vectors invalidated.
|
|
66
|
+
*/
|
|
67
|
+
export declare function reconcileHNSWIndex(dbPath?: string): Promise<number>;
|
|
60
68
|
/**
|
|
61
69
|
* Add entry to HNSW index (with automatic persistence)
|
|
62
70
|
*/
|
|
@@ -680,6 +680,64 @@ function saveHNSWMetadata() {
|
|
|
680
680
|
// Silently fail - metadata save is best-effort
|
|
681
681
|
}
|
|
682
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
|
+
}
|
|
683
741
|
/**
|
|
684
742
|
* Add entry to HNSW index (with automatic persistence)
|
|
685
743
|
*/
|
|
@@ -2380,6 +2438,9 @@ export async function storeEntry(options) {
|
|
|
2380
2438
|
// Keep HNSW index in sync with bridge-stored entries
|
|
2381
2439
|
if (bridgeResult.rawEmbedding && bridgeResult.success) {
|
|
2382
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);
|
|
2383
2444
|
await addToHNSWIndex(bridgeResult.id, bridgeResult.rawEmbedding, {
|
|
2384
2445
|
id: bridgeResult.id,
|
|
2385
2446
|
key: options.key,
|
|
@@ -2485,6 +2546,8 @@ export async function storeEntry(options) {
|
|
|
2485
2546
|
// Add to HNSW index for faster future searches
|
|
2486
2547
|
if (embeddingJson) {
|
|
2487
2548
|
const embResult = JSON.parse(embeddingJson);
|
|
2549
|
+
if (upsert)
|
|
2550
|
+
removeHNSWEntriesByKey(key, namespace);
|
|
2488
2551
|
await addToHNSWIndex(id, embResult, {
|
|
2489
2552
|
id,
|
|
2490
2553
|
key,
|
|
@@ -2977,15 +3040,7 @@ export async function deleteEntry(options) {
|
|
|
2977
3040
|
// #1122: Bridge path must also invalidate the in-memory HNSW index.
|
|
2978
3041
|
// Without this, deleted vectors remain as ghost entries in search results.
|
|
2979
3042
|
if (bridgeResult.deleted && hnswIndex?.entries) {
|
|
2980
|
-
|
|
2981
|
-
for (const [id, entry] of hnswIndex.entries) {
|
|
2982
|
-
if (entry?.key === options.key && (entry?.namespace ?? 'default') === (options.namespace ?? 'default')) {
|
|
2983
|
-
hnswIndex.entries.delete(id);
|
|
2984
|
-
break;
|
|
2985
|
-
}
|
|
2986
|
-
}
|
|
2987
|
-
saveHNSWMetadata();
|
|
2988
|
-
rebuildSearchIndex();
|
|
3043
|
+
removeHNSWEntriesByKey(options.key, options.namespace ?? 'default');
|
|
2989
3044
|
}
|
|
2990
3045
|
return bridgeResult;
|
|
2991
3046
|
}
|
|
@@ -3052,8 +3107,6 @@ export async function deleteEntry(options) {
|
|
|
3052
3107
|
error: `Key '${key}' not found in namespace '${namespace}'`
|
|
3053
3108
|
};
|
|
3054
3109
|
}
|
|
3055
|
-
// Capture the entry ID for HNSW cleanup
|
|
3056
|
-
const entryId = String(checkResult[0].values[0][0]);
|
|
3057
3110
|
// Delete the entry (soft delete by setting status to 'deleted')
|
|
3058
3111
|
// Also null out the embedding to clean up vector data from SQLite
|
|
3059
3112
|
db.run(`
|
|
@@ -3075,14 +3128,8 @@ export async function deleteEntry(options) {
|
|
|
3075
3128
|
// Clean up in-memory HNSW index so ghost vectors don't appear in searches.
|
|
3076
3129
|
// Remove the entry from the HNSW entries map and invalidate the index.
|
|
3077
3130
|
// The next search will rebuild the HNSW index from the remaining DB rows.
|
|
3078
|
-
if (hnswIndex?.entries)
|
|
3079
|
-
|
|
3080
|
-
saveHNSWMetadata();
|
|
3081
|
-
// Invalidate the HNSW index so it rebuilds from DB on next search.
|
|
3082
|
-
// We can't surgically remove a vector from the HNSW graph, so we
|
|
3083
|
-
// clear the entire index; it will be lazily rebuilt from SQLite.
|
|
3084
|
-
rebuildSearchIndex();
|
|
3085
|
-
}
|
|
3131
|
+
if (hnswIndex?.entries)
|
|
3132
|
+
removeHNSWEntriesByKey(key, namespace);
|
|
3086
3133
|
return {
|
|
3087
3134
|
success: true,
|
|
3088
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
|