@claude-flow/cli 3.71.3 → 3.72.0

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 (83) hide show
  1. package/.claude/helpers/auto-memory-hook.mjs +17 -0
  2. package/.claude/helpers/helpers.manifest.json +6 -6
  3. package/.claude/helpers/hook-handler.cjs +23 -10
  4. package/.claude/helpers/intelligence.cjs +23 -1
  5. package/.claude/helpers/statusline.cjs +29 -14
  6. package/README.md +18 -3
  7. package/bin/cli.js +13 -18
  8. package/bin/mcp-server.js +13 -18
  9. package/catalog-manifest.json +2 -2
  10. package/dist/src/appliance/rvfa-distribution.js +2 -2
  11. package/dist/src/benchmarks/gaia-critic.d.ts +1 -7
  12. package/dist/src/benchmarks/gaia-critic.js +1 -7
  13. package/dist/src/benchmarks/gaia-decomposer.d.ts +1 -8
  14. package/dist/src/benchmarks/gaia-decomposer.js +1 -8
  15. package/dist/src/commands/agent-wasm.js +2 -2
  16. package/dist/src/commands/analyze.js +1 -1
  17. package/dist/src/commands/daemon.js +1 -1
  18. package/dist/src/commands/doctor.js +3 -2
  19. package/dist/src/commands/guidance.js +9 -9
  20. package/dist/src/commands/hooks.js +13 -8
  21. package/dist/src/commands/init.js +14 -16
  22. package/dist/src/commands/memory.js +4 -3
  23. package/dist/src/commands/neural.js +8 -6
  24. package/dist/src/commands/route.js +1 -1
  25. package/dist/src/commands/ruvector/backup.js +7 -5
  26. package/dist/src/commands/ruvector/benchmark.js +6 -1
  27. package/dist/src/commands/ruvector/import.js +19 -5
  28. package/dist/src/commands/ruvector/init.js +1 -1
  29. package/dist/src/commands/start.js +1 -1
  30. package/dist/src/commands/status.js +1 -1
  31. package/dist/src/commands/task.js +18 -1
  32. package/dist/src/commands/update.js +1 -1
  33. package/dist/src/funnel/local-signals.js +7 -2
  34. package/dist/src/init/helpers-generator.js +16 -1
  35. package/dist/src/mcp-protocol.d.ts +90 -0
  36. package/dist/src/mcp-protocol.js +179 -0
  37. package/dist/src/mcp-server.d.ts +1 -0
  38. package/dist/src/mcp-server.js +20 -22
  39. package/dist/src/mcp-tools/agent-execute-core.js +9 -1
  40. package/dist/src/mcp-tools/agent-tools.d.ts +13 -0
  41. package/dist/src/mcp-tools/agent-tools.js +63 -12
  42. package/dist/src/mcp-tools/agentbbs-federation.d.ts +18 -1
  43. package/dist/src/mcp-tools/agentbbs-federation.js +23 -7
  44. package/dist/src/mcp-tools/browser-argv.d.ts +27 -0
  45. package/dist/src/mcp-tools/browser-argv.js +92 -0
  46. package/dist/src/mcp-tools/browser-session-tools.js +4 -0
  47. package/dist/src/mcp-tools/browser-tools.d.ts +1 -1
  48. package/dist/src/mcp-tools/browser-tools.js +25 -8
  49. package/dist/src/mcp-tools/coordination-tools.js +5 -4
  50. package/dist/src/mcp-tools/embeddings-tools.js +12 -4
  51. package/dist/src/mcp-tools/glob-exclusions.d.ts +6 -0
  52. package/dist/src/mcp-tools/glob-exclusions.js +81 -0
  53. package/dist/src/mcp-tools/hive-mind-tools.js +104 -35
  54. package/dist/src/mcp-tools/hooks-tools.js +12 -5
  55. package/dist/src/mcp-tools/import-limits.d.ts +4 -0
  56. package/dist/src/mcp-tools/import-limits.js +28 -0
  57. package/dist/src/mcp-tools/memory-tools.js +68 -72
  58. package/dist/src/mcp-tools/neural-tools.js +8 -2
  59. package/dist/src/mcp-tools/path-containment.d.ts +34 -0
  60. package/dist/src/mcp-tools/path-containment.js +133 -0
  61. package/dist/src/mcp-tools/performance-tools.js +7 -2
  62. package/dist/src/mcp-tools/record-store.d.ts +15 -0
  63. package/dist/src/mcp-tools/record-store.js +117 -3
  64. package/dist/src/mcp-tools/security-tools.d.ts +9 -0
  65. package/dist/src/mcp-tools/security-tools.js +39 -3
  66. package/dist/src/mcp-tools/session-tools.js +9 -41
  67. package/dist/src/mcp-tools/swarm-tools.js +36 -14
  68. package/dist/src/mcp-tools/task-tools.js +68 -5
  69. package/dist/src/mcp-tools/terminal-tools.js +16 -2
  70. package/dist/src/mcp-tools/validate-input.d.ts +2 -0
  71. package/dist/src/mcp-tools/validate-input.js +14 -0
  72. package/dist/src/mcp-tools/workflow-tools.js +13 -12
  73. package/dist/src/mcp-tools/x-federation-join.js +55 -14
  74. package/dist/src/memory/intelligence.d.ts +11 -0
  75. package/dist/src/memory/intelligence.js +84 -6
  76. package/dist/src/memory/memory-initializer.d.ts +6 -2
  77. package/dist/src/memory/memory-initializer.js +15 -6
  78. package/dist/src/ruvector/model-router.d.ts +6 -1
  79. package/dist/src/ruvector/model-router.js +16 -4
  80. package/dist/src/services/worker-daemon.js +30 -12
  81. package/dist/src/transfer/storage/gcs.js +3 -2
  82. package/package.json +1 -1
  83. package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -0
@@ -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
  }
@@ -90,6 +90,11 @@ export interface ModelRouterConfig {
90
90
  */
91
91
  priorDecay: number;
92
92
  }
93
+ /** Request-local preferences; omitted or false keeps the existing scoring. */
94
+ export interface ModelRoutingPreferences {
95
+ preferCost?: boolean;
96
+ preferSpeed?: boolean;
97
+ }
93
98
  /**
94
99
  * Routing decision result
95
100
  */
@@ -241,7 +246,7 @@ export declare class ModelRouter {
241
246
  * 'bandit-fallback'` (neural was enabled but declined) or
242
247
  * `'heuristic'` (neural was disabled).
243
248
  */
244
- route(task: string, embedding?: number[]): Promise<ModelRoutingResult>;
249
+ route(task: string, embedding?: number[], preferences?: ModelRoutingPreferences): Promise<ModelRoutingResult>;
245
250
  /**
246
251
  * Analyze task complexity
247
252
  */
@@ -395,7 +395,7 @@ export class ModelRouter {
395
395
  * 'bandit-fallback'` (neural was enabled but declined) or
396
396
  * `'heuristic'` (neural was disabled).
397
397
  */
398
- async route(task, embedding) {
398
+ async route(task, embedding, preferences = {}) {
399
399
  const startTime = performance.now();
400
400
  // Analyze task complexity
401
401
  const complexity = this.analyzeComplexity(task, embedding);
@@ -473,7 +473,7 @@ export class ModelRouter {
473
473
  // Silent — neural path is best-effort.
474
474
  }
475
475
  }
476
- const scores = this.computeModelScores(complexity);
476
+ const scores = this.computeModelScores(complexity, preferences);
477
477
  const adjustedScores = this.applyCircuitBreaker(scores);
478
478
  // A/B mode: compute the pure-bandit pick alongside the hybrid pick so
479
479
  // we can log disagreement. Both samples are drawn from the same Beta
@@ -717,17 +717,29 @@ export class ModelRouter {
717
717
  /**
718
718
  * Compute scores for each model
719
719
  */
720
- computeModelScores(complexity) {
720
+ computeModelScores(complexity, preferences) {
721
721
  const { score } = complexity;
722
722
  // Base scoring: inverse relationship with complexity
723
723
  // Low complexity → haiku scores high
724
724
  // High complexity → opus scores high
725
- return {
725
+ const scores = {
726
726
  haiku: Math.max(0, 1 - score * 2), // Drops off quickly as complexity rises
727
727
  sonnet: 1 - Math.abs(score - 0.5) * 2, // Peaks at medium complexity
728
728
  opus: Math.min(1, score * 1.5), // Rises with complexity
729
729
  inherit: 0.1, // Low baseline unless explicitly needed
730
730
  };
731
+ for (const model of ['haiku', 'sonnet', 'opus']) {
732
+ const capabilities = MODEL_CAPABILITIES[model];
733
+ // Preferences must not boost a tier beyond its task capability.
734
+ // Circuit-breaker penalties and uncertainty escalation still apply.
735
+ if (score > capabilities.maxComplexity)
736
+ continue;
737
+ if (preferences.preferCost)
738
+ scores[model] *= 2 - capabilities.costMultiplier;
739
+ if (preferences.preferSpeed)
740
+ scores[model] *= capabilities.speedMultiplier;
741
+ }
742
+ return scores;
731
743
  }
732
744
  /**
733
745
  * Apply circuit breaker adjustments
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { ConfigFileManager } from './config-file-manager.js';
14
14
  import { EventEmitter } from 'events';
15
- import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync, unlinkSync } from 'fs';
15
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync, unlinkSync, renameSync, statSync } from 'fs';
16
16
  import { cpus, homedir } from 'os';
17
17
  import { join } from 'path';
18
18
  import { createRequire } from 'node:module';
@@ -31,6 +31,8 @@ import { getRepoSupervisorRegistry } from './repo-supervisor.js';
31
31
  // Daemon config is read synchronously from the constructor. In an ESM build,
32
32
  // bare `require` is undefined, so use a module-relative require for YAML.
33
33
  const requireYaml = createRequire(import.meta.url);
34
+ const DAEMON_LOG_MAX_BYTES = 5 * 1024 * 1024;
35
+ const DAEMON_LOG_BACKUPS = 3;
34
36
  // Default worker configurations with improved intervals (P0 fix: map 5min -> 15min)
35
37
  const DEFAULT_WORKERS = [
36
38
  { type: 'map', intervalMs: 15 * 60 * 1000, offsetMs: 0, priority: 'normal', description: 'Codebase mapping', enabled: true },
@@ -1543,7 +1545,7 @@ export class WorkerDaemon extends EventEmitter {
1543
1545
  rawOutputPreview: typeof result.output === 'string' ? result.output.slice(0, 2000) : undefined,
1544
1546
  rawOutputLength: typeof result.output === 'string' ? result.output.length : 0,
1545
1547
  };
1546
- writeFileSync(metricsFile, JSON.stringify(persisted, null, 2));
1548
+ writeFileAtomic(metricsFile, Buffer.from(JSON.stringify(persisted, null, 2)));
1547
1549
  }
1548
1550
  // Worker implementations
1549
1551
  async runMapWorker() {
@@ -1564,7 +1566,7 @@ export class WorkerDaemon extends EventEmitter {
1564
1566
  },
1565
1567
  scannedAt: Date.now(),
1566
1568
  };
1567
- writeFileSync(metricsFile, JSON.stringify(map, null, 2));
1569
+ writeFileAtomic(metricsFile, Buffer.from(JSON.stringify(map, null, 2)));
1568
1570
  return map;
1569
1571
  }
1570
1572
  /**
@@ -1596,7 +1598,7 @@ export class WorkerDaemon extends EventEmitter {
1596
1598
  ],
1597
1599
  note: 'Local fallback checks file presence only; it does not scan source contents.',
1598
1600
  };
1599
- writeFileSync(auditFile, JSON.stringify(audit, null, 2));
1601
+ writeFileAtomic(auditFile, Buffer.from(JSON.stringify(audit, null, 2)));
1600
1602
  return audit;
1601
1603
  }
1602
1604
  /**
@@ -1620,7 +1622,7 @@ export class WorkerDaemon extends EventEmitter {
1620
1622
  },
1621
1623
  note: 'Install Claude Code CLI for AI-powered optimization suggestions',
1622
1624
  };
1623
- writeFileSync(optimizeFile, JSON.stringify(perf, null, 2));
1625
+ writeFileAtomic(optimizeFile, Buffer.from(JSON.stringify(perf, null, 2)));
1624
1626
  return perf;
1625
1627
  }
1626
1628
  /**
@@ -1662,7 +1664,7 @@ export class WorkerDaemon extends EventEmitter {
1662
1664
  memoryCleaned: 0,
1663
1665
  duplicatesRemoved: 0,
1664
1666
  };
1665
- writeFileSync(consolidateFile, JSON.stringify(disabledResult, null, 2));
1667
+ writeFileAtomic(consolidateFile, Buffer.from(JSON.stringify(disabledResult, null, 2)));
1666
1668
  return disabledResult;
1667
1669
  }
1668
1670
  let report;
@@ -1689,7 +1691,7 @@ export class WorkerDaemon extends EventEmitter {
1689
1691
  memoryCleaned: 0,
1690
1692
  duplicatesRemoved: 0,
1691
1693
  };
1692
- writeFileSync(consolidateFile, JSON.stringify(errorResult, null, 2));
1694
+ writeFileAtomic(consolidateFile, Buffer.from(JSON.stringify(errorResult, null, 2)));
1693
1695
  return errorResult;
1694
1696
  }
1695
1697
  for (const store of stores) {
@@ -1733,7 +1735,7 @@ export class WorkerDaemon extends EventEmitter {
1733
1735
  skipped: part.skipped,
1734
1736
  })),
1735
1737
  };
1736
- writeFileSync(consolidateFile, JSON.stringify(result, null, 2));
1738
+ writeFileAtomic(consolidateFile, Buffer.from(JSON.stringify(result, null, 2)));
1737
1739
  return result;
1738
1740
  }
1739
1741
  /**
@@ -1775,7 +1777,7 @@ export class WorkerDaemon extends EventEmitter {
1775
1777
  this.log('warn', `Backup worker failed: ${message}`);
1776
1778
  result = { timestamp: new Date().toISOString(), backedUp: false, error: message };
1777
1779
  }
1778
- writeFileSync(backupFile, JSON.stringify(result, null, 2));
1780
+ writeFileAtomic(backupFile, Buffer.from(JSON.stringify(result, null, 2)));
1779
1781
  return result;
1780
1782
  }
1781
1783
  /**
@@ -1814,7 +1816,7 @@ export class WorkerDaemon extends EventEmitter {
1814
1816
  this.log('warn', `Harness worker failed: ${message}`);
1815
1817
  result = { timestamp: new Date().toISOString(), ran: false, error: message };
1816
1818
  }
1817
- writeFileSync(harnessFile, JSON.stringify(result, null, 2));
1819
+ writeFileAtomic(harnessFile, Buffer.from(JSON.stringify(result, null, 2)));
1818
1820
  return result;
1819
1821
  }
1820
1822
  /**
@@ -1835,7 +1837,7 @@ export class WorkerDaemon extends EventEmitter {
1835
1837
  gaps: [],
1836
1838
  note: 'Install Claude Code CLI for AI-powered test gap analysis',
1837
1839
  };
1838
- writeFileSync(testGapsFile, JSON.stringify(result, null, 2));
1840
+ writeFileAtomic(testGapsFile, Buffer.from(JSON.stringify(result, null, 2)));
1839
1841
  return result;
1840
1842
  }
1841
1843
  /**
@@ -1916,7 +1918,7 @@ export class WorkerDaemon extends EventEmitter {
1916
1918
  uptime: process.uptime(),
1917
1919
  },
1918
1920
  };
1919
- writeFileSync(benchmarkFile, JSON.stringify(result, null, 2));
1921
+ writeFileAtomic(benchmarkFile, Buffer.from(JSON.stringify(result, null, 2)));
1920
1922
  return result;
1921
1923
  }
1922
1924
  /**
@@ -2020,6 +2022,22 @@ export class WorkerDaemon extends EventEmitter {
2020
2022
  // Also write to log file
2021
2023
  try {
2022
2024
  const logFile = join(this.config.logDir, 'daemon.log');
2025
+ // Keep three generations. Rotate before the next complete record
2026
+ // crosses 5 MiB; a single oversized record is preserved intact.
2027
+ if (existsSync(logFile)) {
2028
+ const size = statSync(logFile).size;
2029
+ if (size > 0 && size + Buffer.byteLength(logMessage + '\n') > DAEMON_LOG_MAX_BYTES) {
2030
+ const oldest = `${logFile}.${DAEMON_LOG_BACKUPS}`;
2031
+ if (existsSync(oldest))
2032
+ unlinkSync(oldest);
2033
+ for (let generation = DAEMON_LOG_BACKUPS - 1; generation >= 1; generation--) {
2034
+ const previous = `${logFile}.${generation}`;
2035
+ if (existsSync(previous))
2036
+ renameSync(previous, `${logFile}.${generation + 1}`);
2037
+ }
2038
+ renameSync(logFile, `${logFile}.1`);
2039
+ }
2040
+ }
2023
2041
  appendFileSync(logFile, logMessage + '\n');
2024
2042
  }
2025
2043
  catch {
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import * as crypto from 'crypto';
9
9
  import * as fs from 'fs';
10
+ import { tmpdir } from 'node:os';
10
11
  import * as path from 'path';
11
12
  import { execFileSync } from 'child_process';
12
13
  /**
@@ -92,7 +93,7 @@ export async function uploadToGCS(content, options = {}) {
92
93
  }
93
94
  console.log(`[GCS] Uploading to gs://${config.bucket}/${objectPath}...`);
94
95
  // Write content to temp file
95
- const tempDir = process.env.TMPDIR || '/tmp';
96
+ const tempDir = tmpdir();
96
97
  const tempFile = path.join(tempDir, `claude-flow-upload-${Date.now()}.json`);
97
98
  fs.writeFileSync(tempFile, content);
98
99
  try {
@@ -151,7 +152,7 @@ export async function downloadFromGCS(uri, config) {
151
152
  const cfg = config || getGCSConfig();
152
153
  console.log(`[GCS] Downloading from ${uri}...`);
153
154
  // Write to temp file first
154
- const tempDir = process.env.TMPDIR || '/tmp';
155
+ const tempDir = tmpdir();
155
156
  const tempFile = path.join(tempDir, `claude-flow-download-${Date.now()}.json`);
156
157
  try {
157
158
  // Download using gcloud storage cp (array form prevents shell injection)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.71.3",
3
+ "version": "3.72.0",
4
4
  "type": "module",
5
5
  "description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",