@claude-flow/cli 3.71.4 → 3.72.1

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 (124) hide show
  1. package/.claude/helpers/helpers.manifest.json +4 -4
  2. package/.claude/helpers/hook-handler.cjs +23 -10
  3. package/.claude/helpers/statusline.cjs +29 -14
  4. package/.claude/skills/fast-code-edits/SKILL.md +45 -0
  5. package/.claude/skills/graph-mincut/SKILL.md +32 -0
  6. package/.claude/skills/injection-defense/SKILL.md +36 -0
  7. package/.claude/skills/lora-learning/SKILL.md +30 -0
  8. package/.claude/skills/sublinear-solver/SKILL.md +34 -0
  9. package/.claude/skills/vector-quantization/SKILL.md +36 -0
  10. package/README.md +18 -3
  11. package/bin/cli.js +13 -18
  12. package/bin/mcp-server.js +13 -18
  13. package/catalog-manifest.json +4 -4
  14. package/dist/src/appliance/ruvllm-bridge.d.ts +6 -2
  15. package/dist/src/appliance/ruvllm-bridge.js +13 -8
  16. package/dist/src/appliance/rvfa-distribution.js +2 -2
  17. package/dist/src/benchmarks/gaia-critic.d.ts +1 -7
  18. package/dist/src/benchmarks/gaia-critic.js +1 -7
  19. package/dist/src/benchmarks/gaia-decomposer.d.ts +1 -8
  20. package/dist/src/benchmarks/gaia-decomposer.js +1 -8
  21. package/dist/src/commands/agent-wasm.js +2 -2
  22. package/dist/src/commands/analyze.js +1 -1
  23. package/dist/src/commands/daemon.js +1 -1
  24. package/dist/src/commands/doctor.js +3 -2
  25. package/dist/src/commands/guidance.js +9 -9
  26. package/dist/src/commands/hooks.js +13 -8
  27. package/dist/src/commands/init.js +16 -16
  28. package/dist/src/commands/neural.js +87 -77
  29. package/dist/src/commands/route.js +1 -1
  30. package/dist/src/commands/ruvector/backup.js +7 -5
  31. package/dist/src/commands/ruvector/benchmark.js +6 -1
  32. package/dist/src/commands/ruvector/import.js +19 -5
  33. package/dist/src/commands/ruvector/init.js +1 -1
  34. package/dist/src/commands/start.js +1 -1
  35. package/dist/src/commands/status.js +1 -1
  36. package/dist/src/commands/task.js +18 -1
  37. package/dist/src/commands/update.js +1 -1
  38. package/dist/src/funnel/local-signals.js +7 -2
  39. package/dist/src/init/executor.js +13 -0
  40. package/dist/src/init/types.d.ts +6 -0
  41. package/dist/src/init/types.js +3 -0
  42. package/dist/src/mcp-client.js +2 -0
  43. package/dist/src/mcp-protocol.d.ts +90 -0
  44. package/dist/src/mcp-protocol.js +179 -0
  45. package/dist/src/mcp-server.d.ts +1 -0
  46. package/dist/src/mcp-server.js +20 -22
  47. package/dist/src/mcp-tools/agent-execute-core.js +9 -1
  48. package/dist/src/mcp-tools/agent-tools.d.ts +13 -0
  49. package/dist/src/mcp-tools/agent-tools.js +59 -8
  50. package/dist/src/mcp-tools/agentdb-tools.js +34 -2
  51. package/dist/src/mcp-tools/browser-argv.d.ts +9 -0
  52. package/dist/src/mcp-tools/browser-argv.js +33 -0
  53. package/dist/src/mcp-tools/browser-session-tools.js +4 -0
  54. package/dist/src/mcp-tools/browser-tools.js +12 -2
  55. package/dist/src/mcp-tools/coordination-tools.js +5 -4
  56. package/dist/src/mcp-tools/embeddings-tools.js +12 -4
  57. package/dist/src/mcp-tools/fast-apply-tools.d.ts +4 -0
  58. package/dist/src/mcp-tools/fast-apply-tools.js +85 -0
  59. package/dist/src/mcp-tools/glob-exclusions.d.ts +6 -0
  60. package/dist/src/mcp-tools/glob-exclusions.js +81 -0
  61. package/dist/src/mcp-tools/hive-mind-tools.js +104 -35
  62. package/dist/src/mcp-tools/hooks-tools.js +14 -38
  63. package/dist/src/mcp-tools/import-limits.d.ts +4 -0
  64. package/dist/src/mcp-tools/import-limits.js +28 -0
  65. package/dist/src/mcp-tools/memory-tools.js +21 -42
  66. package/dist/src/mcp-tools/metaharness-tools.js +1 -1
  67. package/dist/src/mcp-tools/performance-tools.js +7 -2
  68. package/dist/src/mcp-tools/realpath-containment.d.ts +3 -0
  69. package/dist/src/mcp-tools/realpath-containment.js +39 -0
  70. package/dist/src/mcp-tools/record-store.d.ts +8 -0
  71. package/dist/src/mcp-tools/record-store.js +98 -1
  72. package/dist/src/mcp-tools/security-tools.d.ts +9 -0
  73. package/dist/src/mcp-tools/security-tools.js +39 -3
  74. package/dist/src/mcp-tools/session-tools.js +5 -3
  75. package/dist/src/mcp-tools/swarm-tools.js +35 -17
  76. package/dist/src/mcp-tools/task-tools.js +68 -5
  77. package/dist/src/mcp-tools/terminal-tools.js +9 -0
  78. package/dist/src/mcp-tools/workflow-tools.js +13 -12
  79. package/dist/src/mcp-tools/x-federation-join.js +55 -14
  80. package/dist/src/memory/intelligence.d.ts +11 -0
  81. package/dist/src/memory/intelligence.js +84 -6
  82. package/dist/src/memory/memory-initializer.d.ts +6 -2
  83. package/dist/src/memory/memory-initializer.js +15 -6
  84. package/dist/src/mods/command-registry/schema.d.ts +12 -12
  85. package/dist/src/ruvector/ast-analyzer.d.ts +1 -2
  86. package/dist/src/ruvector/ast-analyzer.js +4 -12
  87. package/dist/src/ruvector/coverage-router.d.ts +1 -2
  88. package/dist/src/ruvector/coverage-router.js +4 -12
  89. package/dist/src/ruvector/diff-classifier.d.ts +1 -2
  90. package/dist/src/ruvector/diff-classifier.js +3 -12
  91. package/dist/src/ruvector/fast-apply.d.ts +46 -0
  92. package/dist/src/ruvector/fast-apply.js +73 -0
  93. package/dist/src/ruvector/graph-analyzer.d.ts +11 -1
  94. package/dist/src/ruvector/graph-analyzer.js +22 -36
  95. package/dist/src/ruvector/mincut-bridge.d.ts +70 -0
  96. package/dist/src/ruvector/mincut-bridge.js +172 -0
  97. package/dist/src/ruvector/model-router.d.ts +6 -1
  98. package/dist/src/ruvector/model-router.js +16 -4
  99. package/dist/src/services/distill-oracle.d.ts +1 -1
  100. package/dist/src/services/distill-oracle.js +2 -2
  101. package/dist/src/services/worker-daemon.js +30 -12
  102. package/dist/src/transfer/storage/gcs.js +3 -2
  103. package/node_modules/@claude-flow/codex/.agents/skills/fast-code-edits/SKILL.md +42 -0
  104. package/node_modules/@claude-flow/codex/.agents/skills/graph-mincut/SKILL.md +42 -0
  105. package/node_modules/@claude-flow/codex/.agents/skills/injection-defense/SKILL.md +45 -0
  106. package/node_modules/@claude-flow/codex/.agents/skills/lora-learning/SKILL.md +39 -0
  107. package/node_modules/@claude-flow/codex/.agents/skills/sublinear-solver/SKILL.md +38 -0
  108. package/node_modules/@claude-flow/codex/.agents/skills/vector-quantization/SKILL.md +49 -0
  109. package/node_modules/@claude-flow/codex/dist/generators/skill-md.d.ts.map +1 -1
  110. package/node_modules/@claude-flow/codex/dist/generators/skill-md.js +6 -0
  111. package/node_modules/@claude-flow/codex/dist/generators/skill-md.js.map +1 -1
  112. package/node_modules/@claude-flow/codex/dist/index.d.ts +3 -3
  113. package/node_modules/@claude-flow/codex/dist/index.js +1 -1
  114. package/node_modules/@claude-flow/codex/dist/templates/index.d.ts.map +1 -1
  115. package/node_modules/@claude-flow/codex/dist/templates/index.js +37 -0
  116. package/node_modules/@claude-flow/codex/dist/templates/index.js.map +1 -1
  117. package/node_modules/@claude-flow/codex/dist/types.d.ts +1 -1
  118. package/node_modules/@claude-flow/codex/dist/types.d.ts.map +1 -1
  119. package/node_modules/@claude-flow/codex/package.json +1 -1
  120. package/package.json +11 -9
  121. package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +2 -1
  122. package/plugins/ruflo-metaharness/scripts/_darwin.mjs +4 -4
  123. package/plugins/ruflo-metaharness/scripts/_harness.mjs +1 -1
  124. package/plugins/ruflo-metaharness/scripts/smoke.sh +4 -4
@@ -1,14 +1,53 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
- import { join, dirname } from 'node:path';
5
- // ADR-125 precedence: tool args (relayHttp / relayWs / keyFile) take precedence over the
6
- // RUFLO_X_RELAY_HTTP / RUFLO_X_RELAY_WS / RUFLO_NOSTR_KEY_FILE env vars, which precede defaults.
4
+ import { join, dirname, isAbsolute, relative, resolve } from 'node:path';
5
+ // Host configuration precedes defaults. Caller key overrides stay under ~/.ruflo;
6
+ // a caller relayWs must match the configured relay. relayHttp keeps ADR-125 precedence.
7
7
  const HTTP_BASE = (o) => (o || process.env.RUFLO_X_RELAY_HTTP || 'https://relay.ruv.io').replace(/\/$/, '');
8
8
  const RELAY_WS = (o) => o || process.env.RUFLO_X_RELAY_WS || 'wss://relay.ruv.io';
9
9
  const KEY_FILE = () => process.env.RUFLO_NOSTR_KEY_FILE || join(homedir(), '.ruflo', 'nostr.key');
10
10
  const hex = (b) => Buffer.from(b).toString('hex');
11
11
  const unhex = (h) => Uint8Array.from(Buffer.from(h, 'hex'));
12
+ function callerKeyFile(value) {
13
+ // Host configuration is trusted; an MCP argument cannot select a different host file.
14
+ if (value === undefined)
15
+ return KEY_FILE();
16
+ if (typeof value !== 'string' || !value)
17
+ throw new Error('keyFile must name a file under ~/.ruflo');
18
+ const root = resolve(homedir(), '.ruflo');
19
+ const target = resolve(value);
20
+ const rel = relative(root, target);
21
+ if (!rel || rel === '..' || rel.startsWith('../') || rel.startsWith('..\\') || isAbsolute(rel)) {
22
+ throw new Error('keyFile must name a file under ~/.ruflo');
23
+ }
24
+ for (let current = target;; current = dirname(current)) {
25
+ try {
26
+ if (lstatSync(current).isSymbolicLink())
27
+ throw new Error('keyFile must not follow symlinks');
28
+ }
29
+ catch (error) {
30
+ if (error.code !== 'ENOENT')
31
+ throw error;
32
+ }
33
+ if (relative(root, current) === '')
34
+ break;
35
+ }
36
+ return target;
37
+ }
38
+ function configuredRelay(value) {
39
+ const configured = new URL(RELAY_WS());
40
+ if (value !== undefined && typeof value !== 'string')
41
+ throw new Error('relayWs must be the configured relay URL');
42
+ const requested = new URL(value === undefined ? configured.href : value);
43
+ const localTestRelay = Boolean(process.env.RUFLO_X_RELAY_WS)
44
+ && configured.protocol === 'ws:' && ['localhost', '127.0.0.1', '[::1]'].includes(configured.hostname);
45
+ if ((!localTestRelay && configured.protocol !== 'wss:') || requested.href !== configured.href
46
+ || requested.username || requested.password || requested.hash) {
47
+ throw new Error('relayWs must use wss and match the configured relay (explicit loopback ws configuration is allowed)');
48
+ }
49
+ return requested.href;
50
+ }
12
51
  async function loadNostrTools() {
13
52
  try {
14
53
  return (await import('nostr-tools/pure'));
@@ -24,7 +63,7 @@ export function loadOrCreateKey(nt, file = KEY_FILE()) {
24
63
  }
25
64
  mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
26
65
  const sk = nt.generateSecretKey();
27
- writeFileSync(file, hex(sk), { mode: 0o600 });
66
+ writeFileSync(file, hex(sk), { mode: 0o600, flag: 'wx' });
28
67
  return { sk, pubkey: nt.getPublicKey(sk), created: true };
29
68
  }
30
69
  export function nip98Header(nt, sk, url, method, body) {
@@ -80,24 +119,26 @@ export const xFederationJoinTools = [{
80
119
  code: { type: 'string', description: 'Optional private invite code (v2.…), for invite-only relays.' },
81
120
  gatewayUrl: { type: 'string', description: 'Public registration gateway origin; defaults to https://x.ruv.io.' },
82
121
  relayHttp: { type: 'string', description: 'Relay HTTPS base for the claim; takes precedence over RUFLO_X_RELAY_HTTP.' },
83
- relayWs: { type: 'string', description: 'Relay wss URL for NIP-42; takes precedence over RUFLO_X_RELAY_WS.' },
84
- keyFile: { type: 'string', description: 'Key file path; takes precedence over RUFLO_NOSTR_KEY_FILE (default ~/.ruflo/nostr.key).' }
122
+ relayWs: { type: 'string', description: 'Relay wss URL for NIP-42; must match RUFLO_X_RELAY_WS or the default relay.' },
123
+ keyFile: { type: 'string', description: 'Caller-selected file under ~/.ruflo (no symlinks). Omit to use host RUFLO_NOSTR_KEY_FILE or ~/.ruflo/nostr.key.' }
85
124
  }, required: [] },
86
125
  handler: async (input) => {
87
126
  const i = input;
88
- const nt = await loadNostrTools();
89
127
  // Validate input before the optional-dependency check so a bad code fails fast and identically
90
128
  // whether or not nostr-tools is present.
91
129
  if (i.code !== undefined && !/^v2\.[A-Za-z0-9._-]{8,}$/.test(i.code))
92
130
  throw new Error('invite code must look like v2.<token>');
131
+ const registrationBase = i.code ? undefined : secureBase(i.gatewayUrl || process.env.RUFLO_X_GATEWAY_URL || 'https://x.ruv.io');
132
+ const keyFile = callerKeyFile(i.keyFile);
133
+ const relayWs = configuredRelay(i.relayWs);
134
+ const nt = await loadNostrTools();
93
135
  if (!nt)
94
136
  return { degraded: true, reason: 'nostr-tools not installed', hint: 'npm i -g nostr-tools (secp256k1 signing is not in node:crypto)' };
95
- const registrationBase = i.code ? undefined : secureBase(i.gatewayUrl || process.env.RUFLO_X_GATEWAY_URL || 'https://x.ruv.io');
96
- const { sk, pubkey, created } = loadOrCreateKey(nt, i.keyFile);
137
+ const { sk, pubkey, created } = loadOrCreateKey(nt, keyFile);
97
138
  // Existing members need no new grant. In particular never downgrade/re-admit
98
139
  // their key just because they run join again.
99
140
  if (!i.code) {
100
- const existing = await verifyMembership(nt, sk, RELAY_WS(i.relayWs));
141
+ const existing = await verifyMembership(nt, sk, relayWs);
101
142
  if (existing.ok)
102
143
  return { ok: true, pubkey, keyCreated: created, membershipVerified: true, alreadyMember: true };
103
144
  }
@@ -108,7 +149,7 @@ export const xFederationJoinTools = [{
108
149
  r = await fetch(url, { method: 'POST', redirect: 'error', headers: { Authorization: nip98Header(nt, sk, url, 'POST', body), 'Content-Type': 'application/json' }, body, signal: AbortSignal.timeout(20_000) });
109
150
  }
110
151
  catch {
111
- const check = await verifyMembership(nt, sk, RELAY_WS(i.relayWs));
152
+ const check = await verifyMembership(nt, sk, relayWs);
112
153
  if (check.ok)
113
154
  return { ok: true, pubkey, keyCreated: created, membershipVerified: true };
114
155
  throw new Error('Admission response unavailable and relay membership unverified. Keep your local key and retry; do not create another identity.');
@@ -117,12 +158,12 @@ export const xFederationJoinTools = [{
117
158
  if (!r.ok) {
118
159
  // A lost acknowledgment can follow a successful grant. Check with the
119
160
  // relay before declaring failure; never silently generate a second key.
120
- const check = await verifyMembership(nt, sk, RELAY_WS(i.relayWs));
161
+ const check = await verifyMembership(nt, sk, relayWs);
121
162
  if (check.ok)
122
163
  return { ok: true, pubkey, keyCreated: created, membershipVerified: true };
123
164
  throw new Error(`join rejected (${r.status}): ${claim.error ?? claim.message ?? 'unknown'}`);
124
165
  }
125
- const auth = await verifyMembership(nt, sk, RELAY_WS(i.relayWs));
166
+ const auth = await verifyMembership(nt, sk, relayWs);
126
167
  return { ok: auth.ok, pubkey, keyCreated: created, role: claim.role ?? 'member', membershipVerified: auth.ok, ...(auth.ok ? {} : { reason: auth.reason }),
127
168
  next: 'Publish kind-1 events tagged ["t","ruflo-swarm"] — or run `ruflo federation sync` to read the swarm.' };
128
169
  },
@@ -152,6 +152,12 @@ declare class LocalReasoningBank {
152
152
  private persistenceEnabled;
153
153
  private dirty;
154
154
  private saveTimeout;
155
+ /** IDs changed locally since the last successful flush. */
156
+ private dirtyIds;
157
+ /** IDs deleted locally; a disk copy must not be resurrected. */
158
+ private deletedIds;
159
+ /** A local clear() must remove disk entries too. */
160
+ private clearRequested;
155
161
  constructor(options: {
156
162
  maxSize: number;
157
163
  persistence?: boolean;
@@ -168,6 +174,11 @@ declare class LocalReasoningBank {
168
174
  private saveToDisk;
169
175
  /**
170
176
  * Immediately flush patterns to disk
177
+ *
178
+ * Re-reads the file under the cooperating-writer lock and merges only the
179
+ * patterns this process changed, so a long-lived daemon cannot erase patterns
180
+ * written by another hook/process (#4735). Deletions and clear() are carried
181
+ * as tombstones; the write itself is temp-file + rename.
171
182
  */
172
183
  flushToDisk(): void;
173
184
  /**
@@ -337,6 +337,12 @@ class LocalReasoningBank {
337
337
  persistenceEnabled;
338
338
  dirty = false;
339
339
  saveTimeout = null;
340
+ /** IDs changed locally since the last successful flush. */
341
+ dirtyIds = new Set();
342
+ /** IDs deleted locally; a disk copy must not be resurrected. */
343
+ deletedIds = new Set();
344
+ /** A local clear() must remove disk entries too. */
345
+ clearRequested = false;
340
346
  constructor(options) {
341
347
  this.maxSize = options.maxSize;
342
348
  this.persistenceEnabled = options.persistence !== false;
@@ -359,6 +365,7 @@ class LocalReasoningBank {
359
365
  const totalLoaded = data.length;
360
366
  // Group by content to deduplicate
361
367
  const byContent = new Map();
368
+ const removedIds = [];
362
369
  for (const pattern of data) {
363
370
  const key = pattern.content;
364
371
  const existing = byContent.get(key);
@@ -372,10 +379,12 @@ class LocalReasoningBank {
372
379
  (pattern.lastUsedAt ?? 0) > (existing.lastUsedAt ?? 0))) {
373
380
  // Merge: adopt the higher usageCount sum
374
381
  pattern.usageCount = (pattern.usageCount ?? 0) + (existing.usageCount ?? 0);
382
+ removedIds.push(existing.id);
375
383
  byContent.set(key, pattern);
376
384
  }
377
385
  else {
378
386
  existing.usageCount = (existing.usageCount ?? 0) + (pattern.usageCount ?? 0);
387
+ removedIds.push(pattern.id);
379
388
  }
380
389
  }
381
390
  }
@@ -387,15 +396,28 @@ class LocalReasoningBank {
387
396
  const removed = totalLoaded - byContent.size;
388
397
  if (removed > 0) {
389
398
  console.log(`Deduplicated ${removed} patterns (${byContent.size} unique)`);
390
- // Persist the compacted set immediately so the file shrinks on disk
399
+ // Persist the compacted set immediately so the file shrinks on disk.
400
+ // Mark every kept row dirty so this compaction wins over the disk
401
+ // copies, and tombstone the discarded duplicates.
402
+ for (const pattern of byContent.values())
403
+ this.dirtyIds.add(pattern.id);
404
+ for (const id of removedIds)
405
+ this.deletedIds.add(id);
391
406
  this.dirty = true;
392
407
  this.flushToDisk();
393
408
  }
394
409
  }
395
410
  }
396
411
  }
397
- catch {
398
- // Ignore load errors, start fresh
412
+ catch (error) {
413
+ // A corrupt patterns file must be preserved, not silently replaced by
414
+ // an empty bank on the next flush (#4735).
415
+ try {
416
+ const corruptPath = `${getPatternsPath()}.corrupt-${Date.now()}`;
417
+ renameSync(getPatternsPath(), corruptPath);
418
+ console.warn(`[intelligence] Corrupt patterns file moved to ${corruptPath}`);
419
+ }
420
+ catch { /* already absent or rename failed — never overwrite it below */ }
399
421
  }
400
422
  }
401
423
  /**
@@ -415,6 +437,11 @@ class LocalReasoningBank {
415
437
  }
416
438
  /**
417
439
  * Immediately flush patterns to disk
440
+ *
441
+ * Re-reads the file under the cooperating-writer lock and merges only the
442
+ * patterns this process changed, so a long-lived daemon cannot erase patterns
443
+ * written by another hook/process (#4735). Deletions and clear() are carried
444
+ * as tombstones; the write itself is temp-file + rename.
418
445
  */
419
446
  flushToDisk() {
420
447
  if (!this.persistenceEnabled || !this.dirty)
@@ -422,11 +449,52 @@ class LocalReasoningBank {
422
449
  try {
423
450
  ensureDataDir();
424
451
  const path = getPatternsPath();
425
- writeFileSync(path, JSON.stringify(this.patternList, null, 2), 'utf-8');
426
- this.dirty = false;
452
+ withStatsFileLock(path, (canonicalPath) => {
453
+ let diskPatterns = [];
454
+ if (existsSync(canonicalPath)) {
455
+ try {
456
+ const parsed = JSON.parse(readFileSync(canonicalPath, 'utf-8'));
457
+ if (!Array.isArray(parsed))
458
+ throw new Error('patterns.json is not an array');
459
+ diskPatterns = parsed;
460
+ }
461
+ catch {
462
+ // Preserve the corrupt original instead of overwriting it with an
463
+ // empty bank; the new file is written only after this rename.
464
+ try {
465
+ const corruptPath = `${canonicalPath}.corrupt-${Date.now()}`;
466
+ renameSync(canonicalPath, corruptPath);
467
+ console.warn(`[intelligence] Corrupt patterns file moved to ${corruptPath}`);
468
+ }
469
+ catch { /* leave the original in place */ }
470
+ }
471
+ }
472
+ const merged = new Map();
473
+ if (!this.clearRequested) {
474
+ for (const pattern of diskPatterns) {
475
+ if (!this.deletedIds.has(pattern.id))
476
+ merged.set(pattern.id, pattern);
477
+ }
478
+ }
479
+ for (const pattern of this.patternList) {
480
+ if (this.clearRequested || this.dirtyIds.has(pattern.id)) {
481
+ merged.set(pattern.id, pattern);
482
+ }
483
+ }
484
+ const list = [...merged.values()];
485
+ const tmp = `${canonicalPath}.${process.pid}.${Date.now()}.tmp`;
486
+ writeFileSync(tmp, JSON.stringify(list, null, 2), 'utf-8');
487
+ renameSync(tmp, canonicalPath);
488
+ this.patterns = new Map(list.map((pattern) => [pattern.id, pattern]));
489
+ this.patternList = list;
490
+ this.dirty = false;
491
+ this.dirtyIds.clear();
492
+ this.deletedIds.clear();
493
+ this.clearRequested = false;
494
+ });
427
495
  }
428
496
  catch (error) {
429
- // Log but don't throw - persistence failures shouldn't break training
497
+ // Log but don't throw - persistence failures shouldn't break training.
430
498
  console.error('Failed to persist patterns:', error);
431
499
  }
432
500
  }
@@ -459,6 +527,7 @@ class LocalReasoningBank {
459
527
  this.patternList[idx] = stored;
460
528
  }
461
529
  this.patterns.set(pattern.id, stored);
530
+ this.dirtyIds.add(pattern.id);
462
531
  this.saveToDisk();
463
532
  return false;
464
533
  }
@@ -474,6 +543,7 @@ class LocalReasoningBank {
474
543
  }
475
544
  // Keep the Map in sync with the mutated object
476
545
  this.patterns.set(contentDupe.id, contentDupe);
546
+ this.dirtyIds.add(contentDupe.id);
477
547
  this.saveToDisk();
478
548
  return false;
479
549
  }
@@ -482,11 +552,14 @@ class LocalReasoningBank {
482
552
  const oldest = this.patternList.shift();
483
553
  if (oldest) {
484
554
  this.patterns.delete(oldest.id);
555
+ this.deletedIds.add(oldest.id);
556
+ this.dirtyIds.delete(oldest.id);
485
557
  }
486
558
  }
487
559
  this.patternList.push(stored);
488
560
  }
489
561
  this.patterns.set(pattern.id, stored);
562
+ this.dirtyIds.add(stored.id);
490
563
  // Trigger persistence (debounced)
491
564
  this.saveToDisk();
492
565
  return true;
@@ -583,6 +656,8 @@ class LocalReasoningBank {
583
656
  if (idx >= 0) {
584
657
  this.patternList.splice(idx, 1);
585
658
  }
659
+ this.dirtyIds.delete(id);
660
+ this.deletedIds.add(id);
586
661
  this.saveToDisk();
587
662
  return true;
588
663
  }
@@ -592,6 +667,9 @@ class LocalReasoningBank {
592
667
  clear() {
593
668
  this.patterns.clear();
594
669
  this.patternList = [];
670
+ this.dirtyIds.clear();
671
+ this.deletedIds.clear();
672
+ this.clearRequested = true;
595
673
  this.saveToDisk();
596
674
  }
597
675
  }
@@ -361,7 +361,9 @@ export declare function loadEmbeddingModel(options?: {
361
361
  * own embedder is real or stubbed). Set `backend` truthfully by the path that
362
362
  * actually produced the vector. Do NOT change the embedding math.
363
363
  */
364
- export declare function generateEmbedding(text: string): Promise<{
364
+ export declare function generateEmbedding(text: string, options?: {
365
+ normalize?: boolean;
366
+ }): Promise<{
365
367
  embedding: number[];
366
368
  dimensions: number;
367
369
  model: string;
@@ -382,7 +384,9 @@ export declare function generateEmbedding(text: string): Promise<{
382
384
  *
383
385
  * Keeping the local chain as its own export breaks that cycle structurally.
384
386
  */
385
- export declare function generateLocalEmbedding(text: string): Promise<{
387
+ export declare function generateLocalEmbedding(text: string, options?: {
388
+ normalize?: boolean;
389
+ }): Promise<{
386
390
  embedding: number[];
387
391
  dimensions: number;
388
392
  model: string;
@@ -2253,7 +2253,12 @@ async function loadLocalEmbeddingChain(verbose = false, startTime = Date.now())
2253
2253
  * own embedder is real or stubbed). Set `backend` truthfully by the path that
2254
2254
  * actually produced the vector. Do NOT change the embedding math.
2255
2255
  */
2256
- export async function generateEmbedding(text) {
2256
+ export async function generateEmbedding(text, options) {
2257
+ // #4724: AgentDB embed() has no normalize switch and always returns a unit
2258
+ // vector. A raw request has to use the local chain, which can skip L2.
2259
+ if (options?.normalize === false) {
2260
+ return generateLocalEmbedding(text, { normalize: false });
2261
+ }
2257
2262
  // ADR-053: Try AgentDB v3 bridge first
2258
2263
  const bridge = await getBridge();
2259
2264
  if (bridge) {
@@ -2283,7 +2288,9 @@ export async function generateEmbedding(text) {
2283
2288
  *
2284
2289
  * Keeping the local chain as its own export breaks that cycle structurally.
2285
2290
  */
2286
- export async function generateLocalEmbedding(text) {
2291
+ export async function generateLocalEmbedding(text, options) {
2292
+ // Default true keeps the historical unit-norm embedding math.
2293
+ const normalize = options?.normalize !== false;
2287
2294
  // Ensure the LOCAL model is loaded. #3375: this must not go through
2288
2295
  // loadEmbeddingModel(), which is bridge-first — when the bridge answered
2289
2296
  // there, no local model was ever loaded and this function always returned
@@ -2314,7 +2321,7 @@ export async function generateLocalEmbedding(text) {
2314
2321
  // Use ONNX model if available
2315
2322
  if (state.model && typeof state.model === 'function') {
2316
2323
  try {
2317
- const output = await state.model(text, { pooling: 'mean', normalize: true });
2324
+ const output = await state.model(text, { pooling: 'mean', normalize });
2318
2325
  // Handle @xenova/transformers (output.data), ruvector (plain array) and
2319
2326
  // typed-array (ReasoningBank Float32Array, #3114) formats. DataView is an
2320
2327
  // ArrayBuffer view without an iterator (Array.from gives []), so exclude it.
@@ -2338,7 +2345,7 @@ export async function generateLocalEmbedding(text) {
2338
2345
  // Deterministic hash-based fallback (for testing/demo without ONNX).
2339
2346
  // AUDIT #3: backend='mock' — these vectors do NOT carry real semantics.
2340
2347
  (await import('./embedding-policy.js')).enforceNoStub('memory-initializer.generateLocalEmbedding'); // "no stubs" strict mode
2341
- const embedding = generateHashEmbedding(text, state.dimensions);
2348
+ const embedding = generateHashEmbedding(text, state.dimensions, normalize);
2342
2349
  return {
2343
2350
  embedding,
2344
2351
  dimensions: state.dimensions,
@@ -2401,7 +2408,7 @@ export async function generateBatchEmbeddings(texts, options) {
2401
2408
  * Generate deterministic hash-based embedding
2402
2409
  * Not semantic, but deterministic and useful for testing
2403
2410
  */
2404
- function generateHashEmbedding(text, dimensions) {
2411
+ function generateHashEmbedding(text, dimensions, normalize = true) {
2405
2412
  const embedding = new Array(dimensions).fill(0);
2406
2413
  // Simple hash-based approach for reproducibility
2407
2414
  const words = text.toLowerCase().split(/\s+/);
@@ -2413,7 +2420,9 @@ function generateHashEmbedding(text, dimensions) {
2413
2420
  embedding[idx] += Math.sin(charCode * 0.1) * 0.1;
2414
2421
  }
2415
2422
  }
2416
- // Normalize to unit vector
2423
+ // Normalize to unit vector unless the caller asked for the raw vector (#4724).
2424
+ if (!normalize)
2425
+ return embedding;
2417
2426
  const magnitude = Math.sqrt(embedding.reduce((sum, v) => sum + v * v, 0)) || 1;
2418
2427
  return embedding.map(v => v / magnitude);
2419
2428
  }
@@ -192,8 +192,8 @@ export declare const inventorySourceSchema: z.ZodObject<{
192
192
  }, "strict", z.ZodTypeAny, {
193
193
  digest: string;
194
194
  sourcePath: string;
195
- channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
196
195
  package: string;
196
+ channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
197
197
  observedInvocation: string;
198
198
  binding: "markdown-loader" | "mod-register" | "mod-middleware-on-markdown";
199
199
  shippedIn: string[];
@@ -201,8 +201,8 @@ export declare const inventorySourceSchema: z.ZodObject<{
201
201
  }, {
202
202
  digest: string;
203
203
  sourcePath: string;
204
- channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
205
204
  package: string;
205
+ channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
206
206
  observedInvocation: string;
207
207
  binding: "markdown-loader" | "mod-register" | "mod-middleware-on-markdown";
208
208
  shippedIn: string[];
@@ -370,8 +370,8 @@ export declare const catalogEntrySchema: z.ZodObject<{
370
370
  }, "strict", z.ZodTypeAny, {
371
371
  digest: string;
372
372
  sourcePath: string;
373
- channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
374
373
  package: string;
374
+ channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
375
375
  observedInvocation: string;
376
376
  binding: "markdown-loader" | "mod-register" | "mod-middleware-on-markdown";
377
377
  shippedIn: string[];
@@ -379,8 +379,8 @@ export declare const catalogEntrySchema: z.ZodObject<{
379
379
  }, {
380
380
  digest: string;
381
381
  sourcePath: string;
382
- channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
383
382
  package: string;
383
+ channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
384
384
  observedInvocation: string;
385
385
  binding: "markdown-loader" | "mod-register" | "mod-middleware-on-markdown";
386
386
  shippedIn: string[];
@@ -391,8 +391,8 @@ export declare const catalogEntrySchema: z.ZodObject<{
391
391
  sources: {
392
392
  digest: string;
393
393
  sourcePath: string;
394
- channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
395
394
  package: string;
395
+ channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
396
396
  observedInvocation: string;
397
397
  binding: "markdown-loader" | "mod-register" | "mod-middleware-on-markdown";
398
398
  shippedIn: string[];
@@ -432,8 +432,8 @@ export declare const catalogEntrySchema: z.ZodObject<{
432
432
  sources: {
433
433
  digest: string;
434
434
  sourcePath: string;
435
- channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
436
435
  package: string;
436
+ channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
437
437
  observedInvocation: string;
438
438
  binding: "markdown-loader" | "mod-register" | "mod-middleware-on-markdown";
439
439
  shippedIn: string[];
@@ -636,8 +636,8 @@ export declare const commandCatalogSchema: z.ZodObject<{
636
636
  }, "strict", z.ZodTypeAny, {
637
637
  digest: string;
638
638
  sourcePath: string;
639
- channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
640
639
  package: string;
640
+ channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
641
641
  observedInvocation: string;
642
642
  binding: "markdown-loader" | "mod-register" | "mod-middleware-on-markdown";
643
643
  shippedIn: string[];
@@ -645,8 +645,8 @@ export declare const commandCatalogSchema: z.ZodObject<{
645
645
  }, {
646
646
  digest: string;
647
647
  sourcePath: string;
648
- channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
649
648
  package: string;
649
+ channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
650
650
  observedInvocation: string;
651
651
  binding: "markdown-loader" | "mod-register" | "mod-middleware-on-markdown";
652
652
  shippedIn: string[];
@@ -657,8 +657,8 @@ export declare const commandCatalogSchema: z.ZodObject<{
657
657
  sources: {
658
658
  digest: string;
659
659
  sourcePath: string;
660
- channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
661
660
  package: string;
661
+ channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
662
662
  observedInvocation: string;
663
663
  binding: "markdown-loader" | "mod-register" | "mod-middleware-on-markdown";
664
664
  shippedIn: string[];
@@ -698,8 +698,8 @@ export declare const commandCatalogSchema: z.ZodObject<{
698
698
  sources: {
699
699
  digest: string;
700
700
  sourcePath: string;
701
- channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
702
701
  package: string;
702
+ channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
703
703
  observedInvocation: string;
704
704
  binding: "markdown-loader" | "mod-register" | "mod-middleware-on-markdown";
705
705
  shippedIn: string[];
@@ -741,8 +741,8 @@ export declare const commandCatalogSchema: z.ZodObject<{
741
741
  sources: {
742
742
  digest: string;
743
743
  sourcePath: string;
744
- channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
745
744
  package: string;
745
+ channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
746
746
  observedInvocation: string;
747
747
  binding: "markdown-loader" | "mod-register" | "mod-middleware-on-markdown";
748
748
  shippedIn: string[];
@@ -787,8 +787,8 @@ export declare const commandCatalogSchema: z.ZodObject<{
787
787
  sources: {
788
788
  digest: string;
789
789
  sourcePath: string;
790
- channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
791
790
  package: string;
791
+ channel: "repo-project-commands" | "cli-init-template" | "marketplace-plugin" | "source-plugin" | "mod-registration";
792
792
  observedInvocation: string;
793
793
  binding: "markdown-loader" | "mod-register" | "mod-middleware-on-markdown";
794
794
  shippedIn: string[];
@@ -40,8 +40,7 @@ export interface ASTAnalysis {
40
40
  }
41
41
  export declare class ASTAnalyzer {
42
42
  private config;
43
- private ruvectorEngine;
44
- private useNative;
43
+ private readonly useNative;
45
44
  private analysisCache;
46
45
  constructor(config?: Partial<ASTAnalyzerConfig>);
47
46
  initialize(): Promise<void>;
@@ -22,24 +22,16 @@ const LANGUAGE_PATTERNS = {
22
22
  };
23
23
  export class ASTAnalyzer {
24
24
  config;
25
- ruvectorEngine = null;
26
25
  useNative = false;
27
26
  analysisCache = new Map();
28
27
  constructor(config = {}) {
29
28
  this.config = { ...DEFAULT_CONFIG, ...config };
30
29
  }
31
30
  async initialize() {
32
- try {
33
- // @ruvector/ast is optional - gracefully fallback if not installed
34
- const ruvector = await import('@ruvector/ast').catch(() => null);
35
- if (ruvector) {
36
- this.ruvectorEngine = ruvector.createASTAnalyzer?.(this.config);
37
- this.useNative = !!this.ruvectorEngine;
38
- }
39
- }
40
- catch {
41
- this.useNative = false;
42
- }
31
+ // Pure-JS analysis. This used to try `import('@ruvector/ast')`, a package that has
32
+ // never been published (npm 404), so it always fell through to here. ruvector@0.3's
33
+ // ASTParser needs the native tree-sitter grammars and is async-only, so it cannot
34
+ // back this synchronous analyzer.
43
35
  }
44
36
  analyze(code, filePath = 'unknown') {
45
37
  const startTime = performance.now();
@@ -63,8 +63,7 @@ export interface CoverageRouteResult {
63
63
  }
64
64
  export declare class CoverageRouter {
65
65
  private config;
66
- private ruvectorEngine;
67
- private useNative;
66
+ private readonly useNative;
68
67
  private coverageHistory;
69
68
  constructor(config?: Partial<CoverageRouterConfig>);
70
69
  initialize(): Promise<void>;
@@ -37,24 +37,16 @@ const DEFAULT_CONFIG = {
37
37
  };
38
38
  export class CoverageRouter {
39
39
  config;
40
- ruvectorEngine = null;
41
40
  useNative = false;
42
41
  coverageHistory = [];
43
42
  constructor(config = {}) {
44
43
  this.config = { ...DEFAULT_CONFIG, ...config };
45
44
  }
46
45
  async initialize() {
47
- try {
48
- // @ruvector/coverage is optional - gracefully fallback if not installed
49
- const ruvector = await import('@ruvector/coverage').catch(() => null);
50
- if (ruvector) {
51
- this.ruvectorEngine = ruvector.createCoverageRouter?.(this.config);
52
- this.useNative = !!this.ruvectorEngine;
53
- }
54
- }
55
- catch {
56
- this.useNative = false;
57
- }
46
+ // Pure-JS analysis. This used to try `import('@ruvector/coverage')`, a package that
47
+ // has never been published (npm 404), so it always fell through to here.
48
+ // ruvector@0.3's parseIstanbulCoverage is a plain-JS parser equivalent to the parsers
49
+ // below.
58
50
  }
59
51
  parseCoverage(data, format = 'json') {
60
52
  switch (format) {
@@ -49,8 +49,7 @@ export interface DiffAnalysis {
49
49
  }
50
50
  export declare class DiffClassifier {
51
51
  private config;
52
- private ruvectorEngine;
53
- private useNative;
52
+ private readonly useNative;
54
53
  private classificationCache;
55
54
  constructor(config?: Partial<DiffClassifierConfig>);
56
55
  initialize(): Promise<void>;
@@ -60,24 +60,15 @@ function isSecuritySensitivePath(path) {
60
60
  }
61
61
  export class DiffClassifier {
62
62
  config;
63
- ruvectorEngine = null;
64
63
  useNative = false;
65
64
  classificationCache = new Map();
66
65
  constructor(config = {}) {
67
66
  this.config = { ...DEFAULT_CONFIG, ...config };
68
67
  }
69
68
  async initialize() {
70
- try {
71
- // @ruvector/diff is optional - gracefully fallback if not installed
72
- const ruvector = await import('@ruvector/diff').catch(() => null);
73
- if (ruvector) {
74
- this.ruvectorEngine = ruvector.createDiffClassifier?.(this.config);
75
- this.useNative = !!this.ruvectorEngine;
76
- }
77
- }
78
- catch {
79
- this.useNative = false;
80
- }
69
+ // Pure-JS analysis. This used to try `import('@ruvector/diff')`, a package that has
70
+ // never been published (npm 404), so it always fell through to here. ruvector@0.3's
71
+ // parseDiff is a plain-JS parser equivalent to parseDiff() below.
81
72
  }
82
73
  parseDiff(diffContent) {
83
74
  const files = [];