@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.
Files changed (33) hide show
  1. package/.claude/helpers/helpers.manifest.json +2 -2
  2. package/catalog-manifest.json +4 -4
  3. package/dist/src/commands/agent.js +49 -1
  4. package/dist/src/commands/hooks.js +24 -0
  5. package/dist/src/commands/metaharness.js +4 -0
  6. package/dist/src/commands/swarm.js +61 -5
  7. package/dist/src/config-adapter.js +1 -0
  8. package/dist/src/index.js +3 -2
  9. package/dist/src/mcp-tools/agent-tools.js +10 -0
  10. package/dist/src/mcp-tools/hooks-tools.js +86 -52
  11. package/dist/src/mcp-tools/memory-tools.js +14 -2
  12. package/dist/src/mcp-tools/metaharness-tools.js +7 -0
  13. package/dist/src/mcp-tools/swarm-tools.d.ts +16 -0
  14. package/dist/src/mcp-tools/swarm-tools.js +172 -7
  15. package/dist/src/memory/memory-bridge.d.ts +42 -0
  16. package/dist/src/memory/memory-bridge.js +144 -40
  17. package/dist/src/memory/memory-initializer.d.ts +9 -1
  18. package/dist/src/memory/memory-initializer.js +95 -31
  19. package/dist/src/services/daemon-autostart.js +7 -4
  20. package/dist/src/services/flywheel-receipt.d.ts +3 -0
  21. package/dist/src/services/flywheel-receipt.js +1 -0
  22. package/dist/src/services/harness-flywheel-runtime.d.ts +6 -0
  23. package/dist/src/services/harness-flywheel-runtime.js +19 -19
  24. package/dist/src/services/harness-flywheel.d.ts +2 -0
  25. package/dist/src/services/harness-flywheel.js +1 -0
  26. package/dist/src/services/harness-project-anchor.d.ts +23 -0
  27. package/dist/src/services/harness-project-anchor.js +129 -0
  28. package/dist/src/services/learned-routing.d.ts +34 -0
  29. package/dist/src/services/learned-routing.js +85 -0
  30. package/dist/src/services/pheromone-adaptive.d.ts +71 -0
  31. package/dist/src/services/pheromone-adaptive.js +214 -0
  32. package/dist/src/services/worker-daemon.js +4 -1
  33. 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: 'memory database has an active native WAL connection ' +
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: 'memory database has an active native WAL connection ' +
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
- // Remove the entry from the HNSW entries map by key+namespace composite
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: 'memory database has an active native WAL connection ' +
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
- hnswIndex.entries.delete(entryId);
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 default,
12
- * RUFLO_DAEMON_TTL_SECS) auto-start never means "runs forever",
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
- // Only in an initialized project (avoid scaffolding a daemon in a random dir).
89
- if (!fs.existsSync(path.join(projectRoot, '.claude-flow')) && !fs.existsSync(path.join(projectRoot, '.claude'))) {
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 { loadFrozenHumanEval } from './harness-frozen-eval.js';
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: ANCHOR,
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
- // Load the FROZEN, hashed, public human eval set (throws if it has drifted).
176
- const frozen = loadFrozenHumanEval();
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: frozen.tasks,
184
- humanEvalHash: frozen.corpusHash,
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