moflo 4.8.41 → 4.8.43

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.
@@ -176,6 +176,15 @@ function countNamespace(db) {
176
176
  return count;
177
177
  }
178
178
 
179
+ function countMissingEmbeddings(db) {
180
+ const stmt = db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE namespace = ? AND (embedding IS NULL OR embedding = '')`);
181
+ stmt.bind([NAMESPACE]);
182
+ let count = 0;
183
+ if (stmt.step()) count = stmt.getAsObject().cnt;
184
+ stmt.free();
185
+ return count;
186
+ }
187
+
179
188
  // ---------------------------------------------------------------------------
180
189
  // Source file enumeration — git ls-files with filesystem fallback
181
190
  // ---------------------------------------------------------------------------
@@ -184,7 +193,18 @@ function countNamespace(db) {
184
193
  function readCodeMapConfig() {
185
194
  const defaults = {
186
195
  directories: ['src'],
187
- extensions: ['.ts', '.tsx', '.js', '.mjs', '.jsx'],
196
+ extensions: [
197
+ '.ts', '.tsx', '.js', '.mjs', '.jsx', // JS/TS
198
+ '.py', '.pyi', // Python
199
+ '.go', // Go
200
+ '.java', '.kt', '.kts', // JVM
201
+ '.cs', // C#
202
+ '.rs', // Rust
203
+ '.rb', // Ruby
204
+ '.swift', // Swift
205
+ '.php', // PHP
206
+ '.c', '.h', '.cpp', '.hpp', '.cc', // C/C++
207
+ ],
188
208
  exclude: [...EXCLUDE_DIRS],
189
209
  };
190
210
  try {
@@ -239,10 +259,17 @@ function walkDir(dir, extensions, excludeSet, maxDepth = 8, depth = 0) {
239
259
  }
240
260
 
241
261
  function getSourceFiles() {
262
+ const config = readCodeMapConfig();
263
+ const extSet = new Set(config.extensions);
264
+ const excludeSet = new Set(config.exclude);
265
+
266
+ // Build git glob patterns from configured extensions
267
+ const gitGlobs = config.extensions.map(ext => `"*${ext}"`).join(' ');
268
+
242
269
  // Try git ls-files first (fast, respects .gitignore)
243
270
  try {
244
271
  const raw = execSync(
245
- `git ls-files -- "*.ts" "*.tsx" "*.js" "*.mjs" "*.jsx"`,
272
+ `git ls-files -- ${gitGlobs}`,
246
273
  { cwd: projectRoot, encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 }
247
274
  ).trim();
248
275
 
@@ -261,9 +288,6 @@ function getSourceFiles() {
261
288
 
262
289
  // Fallback: walk configured directories from moflo.yaml
263
290
  log('git ls-files returned no files — falling back to filesystem walk');
264
- const config = readCodeMapConfig();
265
- const extSet = new Set(config.extensions);
266
- const excludeSet = new Set(config.exclude);
267
291
  const files = [];
268
292
 
269
293
  for (const dir of config.directories) {
@@ -288,17 +312,135 @@ function isUnchanged(currentHash) {
288
312
  }
289
313
 
290
314
  // ---------------------------------------------------------------------------
291
- // Type extraction (regex-based, no AST)
315
+ // Type extraction (regex-based, no AST) — multi-language
292
316
  // ---------------------------------------------------------------------------
293
317
 
294
- const TS_PATTERNS = [
295
- /^export\s+(?:default\s+)?(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+([\w.]+))?(?:\s+implements\s+([\w,\s.]+))?/,
296
- /^export\s+(?:default\s+)?interface\s+(\w+)(?:\s+extends\s+([\w,\s.]+))?/,
297
- /^export\s+(?:default\s+)?type\s+(\w+)\s*[=<]/,
298
- /^export\s+(?:const\s+)?enum\s+(\w+)/,
299
- /^export\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)/,
300
- /^export\s+(?:default\s+)?const\s+(\w+)\s*[=:]/,
301
- ];
318
+ // Per-language extraction patterns: each entry is [regex, kindOverride?]
319
+ // Group 1 = name, Group 2 = base/extends (optional), Group 3 = implements (optional)
320
+ const LANG_PATTERNS = {
321
+ // JS/TS — require `export` keyword
322
+ ts: [
323
+ [/^export\s+(?:default\s+)?(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+([\w.]+))?(?:\s+implements\s+([\w,\s.]+))?/],
324
+ [/^export\s+(?:default\s+)?interface\s+(\w+)(?:\s+extends\s+([\w,\s.]+))?/],
325
+ [/^export\s+(?:default\s+)?type\s+(\w+)\s*[=<]/],
326
+ [/^export\s+(?:const\s+)?enum\s+(\w+)/],
327
+ [/^export\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)/],
328
+ [/^export\s+(?:default\s+)?const\s+(\w+)\s*[=:]/],
329
+ // CommonJS / plain JS (no export keyword)
330
+ [/^(?:module\.exports\s*=\s*)?class\s+(\w+)(?:\s+extends\s+([\w.]+))?/],
331
+ [/^(?:async\s+)?function\s+(\w+)\s*\(/],
332
+ [/^const\s+(\w+)\s*=\s*(?:async\s+)?\(?.*\)?\s*=>/],
333
+ [/^(?:var|let|const)\s+(\w+)\s*=\s*require\s*\(/],
334
+ ],
335
+
336
+ // Python — class/def at module level
337
+ py: [
338
+ [/^class\s+(\w+)(?:\(([^)]+)\))?:/],
339
+ [/^(?:async\s+)?def\s+(\w+)\s*\(/],
340
+ [/^(\w+)\s*:\s*TypeAlias\s*=/, 'type'],
341
+ [/^(\w+)\s*=\s*(?:TypeVar|NewType|NamedTuple|dataclass)\s*\(/, 'type'],
342
+ ],
343
+
344
+ // Go — top-level type/func/var declarations
345
+ go: [
346
+ [/^type\s+(\w+)\s+struct\b/, 'struct'],
347
+ [/^type\s+(\w+)\s+interface\b/, 'interface'],
348
+ [/^type\s+(\w+)\s+/, 'type'],
349
+ [/^func\s+(\w+)\s*\(/],
350
+ [/^func\s+\([^)]+\)\s+(\w+)\s*\(/, 'method'],
351
+ [/^var\s+(\w+)\s+/, 'var'],
352
+ [/^const\s+(\w+)\s+/, 'const'],
353
+ ],
354
+
355
+ // Java/Kotlin
356
+ java: [
357
+ [/^(?:public|protected|private|abstract|static|final|sealed|open|\s)*class\s+(\w+)(?:\s+extends\s+([\w.]+))?(?:\s+implements\s+([\w,\s.]+))?/],
358
+ [/^(?:public|protected|private|abstract|static|sealed|\s)*interface\s+(\w+)(?:\s+extends\s+([\w,\s.]+))?/],
359
+ [/^(?:public|protected|private|abstract|static|\s)*enum\s+(\w+)/],
360
+ [/^(?:public|protected|private|abstract|static|\s)*@?interface\s+(\w+)/, 'annotation'],
361
+ [/^(?:public|protected|private|abstract|static|final|synchronized|\s)*(?:[\w<>\[\],\s]+)\s+(\w+)\s*\(/, 'method'],
362
+ [/^(?:data\s+)?class\s+(\w+)(?:\s*:\s*([\w.]+))?/, 'class'], // Kotlin
363
+ [/^(?:fun|suspend\s+fun)\s+(\w+)\s*\(/], // Kotlin
364
+ [/^object\s+(\w+)/, 'object'], // Kotlin
365
+ ],
366
+
367
+ // C#
368
+ cs: [
369
+ [/^(?:public|protected|private|internal|abstract|static|sealed|partial|\s)*class\s+(\w+)(?:\s*:\s*([\w.,\s<>]+))?/],
370
+ [/^(?:public|protected|private|internal|abstract|static|\s)*interface\s+(\w+)(?:\s*:\s*([\w.,\s<>]+))?/],
371
+ [/^(?:public|protected|private|internal|abstract|static|\s)*enum\s+(\w+)/],
372
+ [/^(?:public|protected|private|internal|abstract|static|\s)*struct\s+(\w+)/],
373
+ [/^(?:public|protected|private|internal|abstract|static|\s)*record\s+(\w+)/],
374
+ [/^(?:public|protected|private|internal|abstract|static|\s)*delegate\s+\S+\s+(\w+)\s*\(/, 'delegate'],
375
+ [/^namespace\s+([\w.]+)/, 'namespace'],
376
+ ],
377
+
378
+ // Rust
379
+ rs: [
380
+ [/^pub(?:\([\w]+\))?\s+struct\s+(\w+)/, 'struct'],
381
+ [/^pub(?:\([\w]+\))?\s+enum\s+(\w+)/],
382
+ [/^pub(?:\([\w]+\))?\s+trait\s+(\w+)(?:\s*:\s*([\w\s+]+))?/, 'trait'],
383
+ [/^pub(?:\([\w]+\))?\s+(?:async\s+)?fn\s+(\w+)/],
384
+ [/^pub(?:\([\w]+\))?\s+type\s+(\w+)\s*=/, 'type'],
385
+ [/^pub(?:\([\w]+\))?\s+mod\s+(\w+)/, 'module'],
386
+ [/^impl(?:<[^>]+>)?\s+(\w+)/, 'impl'],
387
+ [/^struct\s+(\w+)/, 'struct'],
388
+ [/^enum\s+(\w+)/],
389
+ [/^trait\s+(\w+)/, 'trait'],
390
+ [/^(?:async\s+)?fn\s+(\w+)/],
391
+ ],
392
+
393
+ // Ruby
394
+ rb: [
395
+ [/^class\s+(\w+)(?:\s*<\s*([\w:]+))?/],
396
+ [/^module\s+(\w+)/, 'module'],
397
+ [/^def\s+(self\.)?(\w+)/, 'method'],
398
+ ],
399
+
400
+ // Swift
401
+ swift: [
402
+ [/^(?:public|open|internal|fileprivate|private|\s)*(?:final\s+)?class\s+(\w+)(?:\s*:\s*([\w,\s]+))?/],
403
+ [/^(?:public|open|internal|fileprivate|private|\s)*protocol\s+(\w+)(?:\s*:\s*([\w,\s]+))?/, 'protocol'],
404
+ [/^(?:public|open|internal|fileprivate|private|\s)*struct\s+(\w+)/, 'struct'],
405
+ [/^(?:public|open|internal|fileprivate|private|\s)*enum\s+(\w+)/],
406
+ [/^(?:public|open|internal|fileprivate|private|\s)*func\s+(\w+)\s*\(/],
407
+ [/^(?:public|open|internal|fileprivate|private|\s)*typealias\s+(\w+)/, 'type'],
408
+ ],
409
+
410
+ // PHP
411
+ php: [
412
+ [/^(?:abstract\s+|final\s+)?class\s+(\w+)(?:\s+extends\s+([\w\\]+))?(?:\s+implements\s+([\w\\,\s]+))?/],
413
+ [/^interface\s+(\w+)(?:\s+extends\s+([\w\\,\s]+))?/],
414
+ [/^trait\s+(\w+)/, 'trait'],
415
+ [/^enum\s+(\w+)/],
416
+ [/^(?:public|protected|private|static|\s)*function\s+(\w+)\s*\(/],
417
+ ],
418
+
419
+ // C/C++
420
+ c: [
421
+ [/^(?:typedef\s+)?struct\s+(\w+)/, 'struct'],
422
+ [/^(?:typedef\s+)?enum\s+(\w+)/],
423
+ [/^(?:typedef\s+)?union\s+(\w+)/, 'union'],
424
+ [/^class\s+(\w+)(?:\s*:\s*(?:public|protected|private)\s+([\w:]+))?/],
425
+ [/^namespace\s+(\w+)/, 'namespace'],
426
+ [/^template\s*<[^>]*>\s*class\s+(\w+)/, 'template-class'],
427
+ [/^(?:static\s+|inline\s+|extern\s+)*(?:[\w*&:<>,\s]+)\s+(\w+)\s*\(/, 'function'],
428
+ ],
429
+ };
430
+
431
+ // Map file extensions to language keys
432
+ const EXT_TO_LANG = {
433
+ '.ts': 'ts', '.tsx': 'ts', '.js': 'ts', '.mjs': 'ts', '.jsx': 'ts', '.cjs': 'ts',
434
+ '.py': 'py', '.pyi': 'py',
435
+ '.go': 'go',
436
+ '.java': 'java', '.kt': 'java', '.kts': 'java',
437
+ '.cs': 'cs',
438
+ '.rs': 'rs',
439
+ '.rb': 'rb',
440
+ '.swift': 'swift',
441
+ '.php': 'php',
442
+ '.c': 'c', '.h': 'c', '.cpp': 'c', '.hpp': 'c', '.cc': 'c',
443
+ };
302
444
 
303
445
  const ENTITY_DECORATOR = /@Entity\s*\(/;
304
446
 
@@ -313,6 +455,10 @@ function extractTypes(filePath) {
313
455
  return [];
314
456
  }
315
457
 
458
+ const ext = extname(filePath);
459
+ const lang = EXT_TO_LANG[ext] || 'ts';
460
+ const patterns = LANG_PATTERNS[lang] || LANG_PATTERNS.ts;
461
+
316
462
  const lines = content.split('\n');
317
463
  const types = [];
318
464
  const seen = new Set();
@@ -326,15 +472,18 @@ function extractTypes(filePath) {
326
472
  continue;
327
473
  }
328
474
 
329
- for (const pattern of TS_PATTERNS) {
475
+ for (const [pattern, kindOverride] of patterns) {
330
476
  const m = line.match(pattern);
331
- if (m && m[1] && !seen.has(m[1])) {
332
- seen.add(m[1]);
333
- const kind = detectKind(line, m[1]);
477
+ if (m) {
478
+ // Ruby's def self.name captures differently — normalize
479
+ const name = m[1] === 'self.' ? m[2] : m[1];
480
+ if (!name || seen.has(name)) continue;
481
+ seen.add(name);
482
+ const kind = kindOverride || detectKind(line, name, lang);
334
483
  const bases = (m[2] || '').trim();
335
484
  const implements_ = (m[3] || '').trim();
336
485
  types.push({
337
- name: m[1],
486
+ name,
338
487
  kind,
339
488
  bases: bases || null,
340
489
  implements: implements_ || null,
@@ -354,12 +503,16 @@ function extractTypes(filePath) {
354
503
  return types;
355
504
  }
356
505
 
357
- function detectKind(line, name) {
506
+ function detectKind(line, name, lang) {
358
507
  if (/\bclass\b/.test(line)) return 'class';
359
508
  if (/\binterface\b/.test(line)) return 'interface';
360
509
  if (/\btype\b/.test(line)) return 'type';
361
510
  if (/\benum\b/.test(line)) return 'enum';
362
- if (/\bfunction\b/.test(line)) return 'function';
511
+ if (/\bstruct\b/.test(line)) return 'struct';
512
+ if (/\btrait\b/.test(line)) return 'trait';
513
+ if (/\bprotocol\b/.test(line)) return 'protocol';
514
+ if (/\bmodule\b/.test(line)) return 'module';
515
+ if (/\bfunction\b/.test(line) || /\bfunc\b/.test(line) || /\bdef\b/.test(line) || /\bfn\b/.test(line)) return 'function';
363
516
  if (/\bconst\b/.test(line)) return 'const';
364
517
  return 'export';
365
518
  }
@@ -393,10 +546,20 @@ function getDirDescription(dirName) {
393
546
 
394
547
  function detectLanguage(filePath) {
395
548
  const ext = extname(filePath);
396
- if (ext === '.tsx' || ext === '.jsx') return 'tsx';
397
- if (ext === '.ts') return 'ts';
398
- if (ext === '.mjs') return 'esm';
399
- return 'js';
549
+ const langMap = {
550
+ '.tsx': 'React/TypeScript', '.jsx': 'React/JavaScript',
551
+ '.ts': 'TypeScript', '.mjs': 'ESM', '.cjs': 'CommonJS', '.js': 'JavaScript',
552
+ '.py': 'Python', '.pyi': 'Python',
553
+ '.go': 'Go',
554
+ '.java': 'Java', '.kt': 'Kotlin', '.kts': 'Kotlin',
555
+ '.cs': 'C#',
556
+ '.rs': 'Rust',
557
+ '.rb': 'Ruby',
558
+ '.swift': 'Swift',
559
+ '.php': 'PHP',
560
+ '.c': 'C', '.h': 'C/C++ Header', '.cpp': 'C++', '.hpp': 'C++ Header', '.cc': 'C++',
561
+ };
562
+ return langMap[ext] || 'Unknown';
400
563
  }
401
564
 
402
565
  // ---------------------------------------------------------------------------
@@ -441,16 +604,16 @@ function generateProjectOverviews(filesByProject, typesByProject) {
441
604
  }
442
605
 
443
606
  function detectProjectLang(files) {
444
- let tsx = 0, ts = 0, js = 0;
607
+ const counts = {};
445
608
  for (const f of files) {
446
- const ext = extname(f);
447
- if (ext === '.tsx' || ext === '.jsx') tsx++;
448
- else if (ext === '.ts') ts++;
449
- else js++;
609
+ const lang = detectLanguage(f);
610
+ counts[lang] = (counts[lang] || 0) + 1;
450
611
  }
451
- if (tsx > ts && tsx > js) return 'React/TypeScript';
452
- if (ts >= js) return 'TypeScript';
453
- return 'JavaScript';
612
+ // Return the dominant language, or list top 2 if mixed
613
+ const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
614
+ if (sorted.length === 0) return 'Unknown';
615
+ if (sorted.length === 1 || sorted[0][1] > sorted[1][1] * 2) return sorted[0][0];
616
+ return `${sorted[0][0]}/${sorted[1][0]}`;
454
617
  }
455
618
 
456
619
  function generateDirectoryDetails(typesByDir) {
@@ -584,8 +747,6 @@ function generateFileEntries(typesByFile) {
584
747
  const chunks = [];
585
748
 
586
749
  for (const [filePath, types] of Object.entries(typesByFile)) {
587
- if (types.length === 0) continue;
588
-
589
750
  const project = getProjectName(filePath);
590
751
  const dir = getDirectory(filePath);
591
752
  const dirDesc = getDirDescription(dir);
@@ -596,23 +757,34 @@ function generateFileEntries(typesByFile) {
596
757
  let content = `# ${fileName} (${filePath})\n`;
597
758
  content += `Project: ${project} | Language: ${lang}\n`;
598
759
  if (dirDesc) content += `Directory: ${dirDesc}\n`;
599
- content += '\nExported types:\n';
600
760
 
601
- for (const t of types) {
602
- let line = ` ${t.kind} ${t.name}`;
603
- if (t.isEntity) line += ' [MikroORM entity]';
604
- if (t.bases) line += ` extends ${t.bases}`;
605
- if (t.implements) line += ` implements ${t.implements}`;
606
- content += line + '\n';
761
+ if (types.length > 0) {
762
+ content += '\nExported types:\n';
763
+ for (const t of types) {
764
+ let line = ` ${t.kind} ${t.name}`;
765
+ if (t.isEntity) line += ' [MikroORM entity]';
766
+ if (t.bases) line += ` extends ${t.bases}`;
767
+ if (t.implements) line += ` implements ${t.implements}`;
768
+ content += line + '\n';
769
+ }
770
+ } else {
771
+ // For files without detected exports, include a summary line
772
+ // so the file is still discoverable via semantic search
773
+ content += '\nSource file (no detected exports)\n';
607
774
  }
608
775
 
609
776
  // Build tags for filtering
610
777
  const tags = ['file', project];
611
778
  if (types.some(t => t.isEntity)) tags.push('entity');
612
779
  if (types.some(t => t.kind === 'interface')) tags.push('interface');
613
- if (filePath.includes('/services/')) tags.push('service');
614
- if (filePath.includes('/routes/')) tags.push('route');
615
- if (filePath.includes('/middleware/')) tags.push('middleware');
780
+ // Use path separator pattern that works cross-platform
781
+ if (filePath.includes('/services/') || filePath.includes('\\services\\')) tags.push('service');
782
+ if (filePath.includes('/routes/') || filePath.includes('\\routes\\')) tags.push('route');
783
+ if (filePath.includes('/middleware/') || filePath.includes('\\middleware\\')) tags.push('middleware');
784
+ if (filePath.includes('/components/') || filePath.includes('\\components\\')) tags.push('component');
785
+ if (filePath.includes('/hooks/') || filePath.includes('\\hooks\\')) tags.push('hook');
786
+ if (filePath.includes('/api/') || filePath.includes('\\api\\')) tags.push('api');
787
+ if (filePath.includes('/utils/') || filePath.includes('\\utils\\')) tags.push('util');
616
788
 
617
789
  chunks.push({
618
790
  key: `file:${filePath}`,
@@ -668,9 +840,15 @@ async function main() {
668
840
  if (isUnchanged(currentHash)) {
669
841
  const db = await getDb();
670
842
  const count = countNamespace(db);
843
+ const missing = countMissingEmbeddings(db);
671
844
  db.close();
672
845
  if (count > 0) {
673
- log(`Skipping — file list unchanged (${count} chunks in DB, hash ${currentHash.slice(0, 12)}...)`);
846
+ if (missing > 0 && !skipEmbeddings) {
847
+ log(`File list unchanged but ${missing}/${count} entries missing embeddings — generating...`);
848
+ await runEmbeddings();
849
+ } else {
850
+ log(`Skipping — file list unchanged (${count} chunks in DB, hash ${currentHash.slice(0, 12)}...)`);
851
+ }
674
852
  return;
675
853
  }
676
854
  log('File list unchanged but no chunks in DB — forcing regeneration');
@@ -691,10 +869,9 @@ async function main() {
691
869
 
692
870
  const types = extractTypes(file);
693
871
 
694
- // Track types per file for file-level entries
695
- if (types.length > 0) {
696
- typesByFile[file] = types;
697
- }
872
+ // Track ALL files for file-level entries (not just those with types)
873
+ // This ensures plain JS projects without explicit exports still get indexed
874
+ typesByFile[file] = types;
698
875
 
699
876
  for (const t of types) {
700
877
  allTypes.push(t);
@@ -748,25 +925,28 @@ async function main() {
748
925
 
749
926
  // 7. Generate embeddings inline (not detached — ensures Xenova runs reliably)
750
927
  if (!skipEmbeddings) {
751
- // Prefer moflo's own bin script, fall back to project's .claude/scripts/
752
- const embedCandidates = [
753
- resolve(dirname(fileURLToPath(import.meta.url)), 'build-embeddings.mjs'),
754
- resolve(projectRoot, '.claude/scripts/build-embeddings.mjs'),
755
- ];
756
- const embedScript = embedCandidates.find(p => existsSync(p));
757
- if (embedScript) {
758
- log('Generating embeddings for code-map...');
759
- try {
760
- execSync(`node "${embedScript}" --namespace code-map`, {
761
- cwd: projectRoot,
762
- stdio: 'inherit',
763
- timeout: 120000,
764
- windowsHide: true,
765
- });
766
- } catch (err) {
767
- log(`Warning: embedding generation failed: ${err.message?.split('\n')[0]}`);
768
- }
769
- }
928
+ await runEmbeddings();
929
+ }
930
+ }
931
+
932
+ async function runEmbeddings() {
933
+ const embedCandidates = [
934
+ resolve(dirname(fileURLToPath(import.meta.url)), 'build-embeddings.mjs'),
935
+ resolve(projectRoot, '.claude/scripts/build-embeddings.mjs'),
936
+ ];
937
+ const embedScript = embedCandidates.find(p => existsSync(p));
938
+ if (!embedScript) return;
939
+
940
+ log('Generating embeddings for code-map...');
941
+ try {
942
+ execSync(`node "${embedScript}" --namespace code-map`, {
943
+ cwd: projectRoot,
944
+ stdio: 'inherit',
945
+ timeout: 120000,
946
+ windowsHide: true,
947
+ });
948
+ } catch (err) {
949
+ log(`Warning: embedding generation failed: ${err.message?.split('\n')[0]}`);
770
950
  }
771
951
  }
772
952
 
@@ -27,7 +27,22 @@ import { createProcessManager } from './lib/process-manager.mjs';
27
27
 
28
28
  const __filename = fileURLToPath(import.meta.url);
29
29
  const __dirname = dirname(__filename);
30
- const projectRoot = resolve(__dirname, '../..');
30
+
31
+ // Detect project root by walking up from cwd to find package.json.
32
+ // IMPORTANT: Do NOT use resolve(__dirname, '..') or '../..' — this script lives
33
+ // in bin/ during development but gets synced to .claude/scripts/ in consumer
34
+ // projects, so __dirname-relative paths break. findProjectRoot() works everywhere.
35
+ function findProjectRoot() {
36
+ let dir = process.cwd();
37
+ const root = resolve(dir, '/');
38
+ while (dir !== root) {
39
+ if (existsSync(resolve(dir, 'package.json'))) return dir;
40
+ dir = dirname(dir);
41
+ }
42
+ return process.cwd();
43
+ }
44
+
45
+ const projectRoot = findProjectRoot();
31
46
  const logFile = resolve(projectRoot, '.swarm/hooks.log');
32
47
  const pm = createProcessManager(projectRoot);
33
48
 
@@ -260,21 +275,14 @@ async function main() {
260
275
  case 'session-start': {
261
276
  // Start daemon quietly in background (no DB writes)
262
277
  runDaemonStartBackground();
263
- // Run pretrain in background (writes to patterns namespace, fast)
264
- runBackgroundPretrain();
278
+ // Initialize embeddings engine (must run before indexers that generate embeddings)
279
+ runEmbeddingsInitBackground();
265
280
  // Run all DB-writing indexers SEQUENTIALLY in a single background process.
266
- // This avoids sql.js last-write-wins concurrency (issue #78).
267
- // Chain: guidance code-map tests HNSW rebuild (must be last).
268
- const indexAllScript = resolve(__dirname, 'index-all.mjs');
269
- if (existsSync(indexAllScript)) {
270
- spawnWindowless('node', [indexAllScript], 'sequential indexing chain');
271
- } else {
272
- // Fallback to parallel if index-all.mjs not available
273
- runIndexGuidanceBackground();
274
- runCodeMapBackground();
275
- runTestIndexBackground();
276
- runHNSWRebuildBackground();
277
- }
281
+ // This avoids sql.js last-write-wins concurrency (#78) and ensures
282
+ // HNSW rebuild runs after all indexers finish (#81).
283
+ // Chain: guidance code-map → tests → patterns → pretrain → HNSW rebuild.
284
+ spawnWindowless('node', [resolve(__dirname, 'index-all.mjs')], 'sequential indexing chain');
285
+ // Neural patterns now loaded by moflo core routing — no external patching.
278
286
  break;
279
287
  }
280
288
 
@@ -469,55 +477,6 @@ function runIndexGuidanceBackground(specificFile = null) {
469
477
  spawnWindowless('node', indexArgs, desc);
470
478
  }
471
479
 
472
- // Run structural code map generator in background (non-blocking)
473
- function runCodeMapBackground() {
474
- // Check auto_index.code_map flag in moflo.yaml (default: true)
475
- const yamlPath = resolve(projectRoot, 'moflo.yaml');
476
- if (existsSync(yamlPath)) {
477
- try {
478
- const content = readFileSync(yamlPath, 'utf-8');
479
- const match = content.match(/auto_index:\s*\n(?:.*\n)*?\s+code_map:\s*(true|false)/);
480
- if (match && match[1] === 'false') {
481
- log('info', 'Code map generation disabled (auto_index.code_map: false)');
482
- return;
483
- }
484
- } catch { /* ignore, proceed with indexing */ }
485
- }
486
-
487
- const codeMapScript = resolveBinOrLocal('flo-codemap', 'generate-code-map.mjs');
488
-
489
- if (!codeMapScript) {
490
- log('warn', 'Code map generator not found (checked npm bin + .claude/scripts/)');
491
- return;
492
- }
493
-
494
- spawnWindowless('node', [codeMapScript], 'background code map generation');
495
- }
496
-
497
- // Run test file indexer in background (non-blocking)
498
- function runTestIndexBackground() {
499
- // Check auto_index.tests flag in moflo.yaml (default: true)
500
- const yamlPath = resolve(projectRoot, 'moflo.yaml');
501
- if (existsSync(yamlPath)) {
502
- try {
503
- const content = readFileSync(yamlPath, 'utf-8');
504
- const match = content.match(/auto_index:\s*\n(?:.*\n)*?\s+tests:\s*(true|false)/);
505
- if (match && match[1] === 'false') {
506
- log('info', 'Test indexing disabled (auto_index.tests: false)');
507
- return;
508
- }
509
- } catch { /* ignore, proceed with indexing */ }
510
- }
511
-
512
- const testIndexScript = resolveBinOrLocal('flo-testmap', 'index-tests.mjs');
513
-
514
- if (!testIndexScript) {
515
- log('info', 'Test indexer not found (checked npm bin + .claude/scripts/)');
516
- return;
517
- }
518
-
519
- spawnWindowless('node', [testIndexScript], 'background test indexing');
520
- }
521
480
 
522
481
  // Run ReasoningBank + MicroLoRA training + EWC++ consolidation in background (non-blocking)
523
482
  function runBackgroundTraining() {
@@ -625,29 +584,19 @@ function runDaemonStartBackground() {
625
584
  spawnWindowless('node', [localCli, 'daemon', 'start', '--quiet'], 'daemon');
626
585
  }
627
586
 
628
- // Run pretrain in background on session start (non-blocking)
629
- function runBackgroundPretrain() {
630
- const localCli = getLocalCliPath();
631
- if (!localCli) {
632
- log('warn', 'Local CLI not found, skipping background pretrain');
633
- return;
634
- }
635
587
 
636
- spawnWindowless('node', [localCli, 'hooks', 'pretrain'], 'background pretrain');
637
- }
638
-
639
- // Force HNSW rebuild in background to ensure all processes use identical fresh index
640
- // This fixes the issue where spawned agents return different search results than CLI/MCP
641
- function runHNSWRebuildBackground() {
588
+ // Initialize embeddings ONNX engine on session start (non-blocking)
589
+ function runEmbeddingsInitBackground() {
642
590
  const localCli = getLocalCliPath();
643
591
  if (!localCli) {
644
- log('warn', 'Local CLI not found, skipping HNSW rebuild');
592
+ log('warn', 'Local CLI not found, skipping embeddings init');
645
593
  return;
646
594
  }
647
595
 
648
- spawnWindowless('node', [localCli, 'memory', 'rebuild', '--force'], 'HNSW rebuild');
596
+ spawnWindowless('node', [localCli, 'embeddings', 'init'], 'embeddings init');
649
597
  }
650
598
 
599
+
651
600
  // Neural pattern application — now handled by moflo core routing (learned patterns
652
601
  // loaded from routing-outcomes.json by hooks-tools.ts getSemanticRouter).
653
602
  // No external patch script needed.