moflo 4.12.3 → 4.12.4-rc.2

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 (98) hide show
  1. package/.claude/agents/core/coder.md +2 -3
  2. package/.claude/agents/core/researcher.md +3 -4
  3. package/.claude/agents/core/reviewer.md +6 -8
  4. package/.claude/guidance/shipped/moflo-spell-engine.md +13 -10
  5. package/.claude/helpers/gate-hook.mjs +67 -2
  6. package/.claude/helpers/gate.cjs +173 -12
  7. package/.claude/helpers/hook-handler.cjs +0 -0
  8. package/.claude/helpers/prompt-hook.mjs +0 -0
  9. package/.claude/skills/connector-builder/SKILL.md +5 -4
  10. package/.claude/skills/fl/phases.md +1 -1
  11. package/.claude/skills/fl/sdd.md +1 -1
  12. package/.claude/skills/spell-builder/SKILL.md +5 -4
  13. package/.claude/skills/spell-builder/architecture.md +4 -4
  14. package/.claude/skills/spell-builder/connectors/github-cli/README.md +22 -17
  15. package/.claude/skills/spell-builder/connectors/http/README.md +18 -11
  16. package/.claude/skills/spell-builder/connectors/local-outlook/README.md +8 -7
  17. package/.claude/skills/spell-builder/connectors/playwright/README.md +10 -8
  18. package/.claude/skills/spell-builder/steps/agent/README.md +25 -21
  19. package/.claude/skills/verify/SKILL.md +24 -3
  20. package/bin/gate-hook.mjs +67 -2
  21. package/bin/gate.cjs +173 -12
  22. package/bin/generate-code-map.mjs +70 -17
  23. package/bin/index-all.mjs +38 -0
  24. package/bin/index-guidance.mjs +8 -41
  25. package/bin/index-patterns.mjs +2 -2
  26. package/bin/index-tests.mjs +6 -4
  27. package/bin/lib/guidance-config.mjs +119 -0
  28. package/bin/lib/index-fingerprint.mjs +0 -0
  29. package/dist/src/cli/commands/agent.js +1 -89
  30. package/dist/src/cli/commands/analyze.js +1 -1
  31. package/dist/src/cli/commands/benchmark.js +11 -8
  32. package/dist/src/cli/commands/claims.js +2 -2
  33. package/dist/src/cli/commands/completions.js +1 -1
  34. package/dist/src/cli/commands/config.js +256 -96
  35. package/dist/src/cli/commands/deployment.js +6 -6
  36. package/dist/src/cli/commands/diagnose.js +1 -1
  37. package/dist/src/cli/commands/doctor-checks-config.js +18 -22
  38. package/dist/src/cli/commands/doctor-checks-deep.js +1 -1
  39. package/dist/src/cli/commands/doctor-fixes.js +36 -11
  40. package/dist/src/cli/commands/doctor.js +1 -1
  41. package/dist/src/cli/commands/embeddings.js +64 -23
  42. package/dist/src/cli/commands/github.js +1 -1
  43. package/dist/src/cli/commands/guidance.js +10 -10
  44. package/dist/src/cli/commands/hive-mind.js +8 -70
  45. package/dist/src/cli/commands/hooks.js +32 -219
  46. package/dist/src/cli/commands/mcp.js +5 -2
  47. package/dist/src/cli/commands/memory.js +59 -46
  48. package/dist/src/cli/commands/neural.js +21 -21
  49. package/dist/src/cli/commands/performance.js +14 -7
  50. package/dist/src/cli/commands/plugins.js +2 -2
  51. package/dist/src/cli/commands/providers.js +2 -2
  52. package/dist/src/cli/commands/retire.js +1 -1
  53. package/dist/src/cli/commands/route.js +1 -1
  54. package/dist/src/cli/commands/security.js +4 -4
  55. package/dist/src/cli/commands/status.js +18 -13
  56. package/dist/src/cli/commands/task.js +8 -0
  57. package/dist/src/cli/config/cli-config-store.js +326 -0
  58. package/dist/src/cli/hooks/workers/index.js +15 -9
  59. package/dist/src/cli/index.js +43 -33
  60. package/dist/src/cli/init/claudemd-generator.js +3 -3
  61. package/dist/src/cli/init/helpers-generator.js +203 -20
  62. package/dist/src/cli/init/moflo-yaml-template.js +11 -3
  63. package/dist/src/cli/init/settings-generator.js +10 -2
  64. package/dist/src/cli/mcp-client.js +12 -2
  65. package/dist/src/cli/mcp-server.js +5 -23
  66. package/dist/src/cli/mcp-tools/analysis-tools.js +103 -0
  67. package/dist/src/cli/mcp-tools/coverage-tools.js +206 -0
  68. package/dist/src/cli/mcp-tools/hive-mind-tools.js +71 -0
  69. package/dist/src/cli/mcp-tools/hooks-tools.js +25 -9
  70. package/dist/src/cli/mcp-tools/memory-admin-tools.js +538 -0
  71. package/dist/src/cli/mcp-tools/memory-tools.js +26 -5
  72. package/dist/src/cli/mcp-tools/neural-tools.js +56 -87
  73. package/dist/src/cli/mcp-tools/performance-tools.js +76 -39
  74. package/dist/src/cli/mcp-tools/progress-tools.js +142 -0
  75. package/dist/src/cli/mcp-tools/security-tools.js +1 -1
  76. package/dist/src/cli/mcp-tools/session-tools.js +116 -0
  77. package/dist/src/cli/mcp-tools/synthetic.js +74 -0
  78. package/dist/src/cli/mcp-tools/system-tools.js +126 -78
  79. package/dist/src/cli/mcp-tools/task-tools.js +101 -1
  80. package/dist/src/cli/movector/coverage-router.js +48 -6
  81. package/dist/src/cli/movector/diff-classifier.js +8 -6
  82. package/dist/src/cli/movector/flash-attention.js +1 -1
  83. package/dist/src/cli/movector/vector-db.js +1 -1
  84. package/dist/src/cli/parser.js +81 -12
  85. package/dist/src/cli/services/daemon-lock.js +5 -2
  86. package/dist/src/cli/services/hook-block-hash.js +3 -1
  87. package/dist/src/cli/services/moflo-version.js +62 -0
  88. package/dist/src/cli/services/movector-training.js +1 -1
  89. package/dist/src/cli/services/spell-gate.js +6 -4
  90. package/dist/src/cli/services/worker-daemon.js +37 -7
  91. package/dist/src/cli/shared/utils/load-average.js +59 -0
  92. package/dist/src/cli/spells/commands/agent-command.js +55 -25
  93. package/dist/src/cli/suggest.js +1 -1
  94. package/dist/src/cli/swarm/swarm-persistence.js +112 -0
  95. package/dist/src/cli/swarm/unified-coordinator.js +118 -3
  96. package/dist/src/cli/version.js +1 -1
  97. package/package.json +5 -3
  98. package/dist/src/cli/mcp-tools/github-tools.js +0 -318
@@ -168,7 +168,7 @@ function readCodeMapConfig() {
168
168
  const defaults = {
169
169
  directories: ['src'],
170
170
  extensions: [
171
- '.ts', '.tsx', '.js', '.mjs', '.jsx', // JS/TS
171
+ '.ts', '.tsx', '.js', '.mjs', '.cjs', '.jsx', // JS/TS
172
172
  '.py', '.pyi', // Python
173
173
  '.go', // Go
174
174
  '.java', '.kt', '.kts', // JVM
@@ -210,6 +210,18 @@ function readCodeMapConfig() {
210
210
  } catch { return defaults; }
211
211
  }
212
212
 
213
+ /**
214
+ * Extension of `p`, lowercased.
215
+ *
216
+ * Rule #1: NTFS (Windows) and APFS (macOS) are case-insensitive by default, so a
217
+ * file committed as `Foo.MJS` is routine there. `extname()` returns the literal
218
+ * `.MJS` on every platform — including Linux — so an exact compare against a
219
+ * lowercase list silently skips the file everywhere. Normalise both sides. (#1337)
220
+ */
221
+ function normExt(p) {
222
+ return extname(p).toLowerCase();
223
+ }
224
+
213
225
  /** Walk a directory tree collecting source files (filesystem fallback). */
214
226
  function walkDir(dir, extensions, excludeSet, maxDepth = 8, depth = 0) {
215
227
  if (depth > maxDepth) return [];
@@ -225,8 +237,7 @@ function walkDir(dir, extensions, excludeSet, maxDepth = 8, depth = 0) {
225
237
  if (entry.isDirectory()) {
226
238
  results.push(...walkDir(rel, extensions, excludeSet, maxDepth, depth + 1));
227
239
  } else if (entry.isFile()) {
228
- const ext = extname(entry.name);
229
- if (extensions.has(ext)) results.push(rel);
240
+ if (extensions.has(normExt(entry.name))) results.push(rel);
230
241
  }
231
242
  }
232
243
  return results;
@@ -234,11 +245,18 @@ function walkDir(dir, extensions, excludeSet, maxDepth = 8, depth = 0) {
234
245
 
235
246
  function getSourceFiles() {
236
247
  const config = readCodeMapConfig();
237
- const extSet = new Set(config.extensions);
248
+ // Lowercase the configured list too, so `extensions: [".MJS"]` in a consumer's
249
+ // moflo.yaml still matches the normalised extension of a scanned file.
250
+ const extSet = new Set(config.extensions.map(e => e.toLowerCase()));
238
251
  const excludeSet = new Set(config.exclude);
239
252
 
240
- // Build git glob patterns from configured extensions
241
- const gitGlobArgs = config.extensions.map(ext => `*${ext}`);
253
+ // Build git glob patterns from configured extensions.
254
+ // `:(icase)` is required, not cosmetic: git pathspec globs are case-sensitive
255
+ // regardless of `core.ignorecase` (verified on git 2.43 with the setting both
256
+ // on and off), so a bare `*.mjs` misses a committed `Foo.MJS` on every
257
+ // platform. This is the primary enumeration path — normalising `extname()` in
258
+ // the filesystem fallback alone would leave it broken. (#1337)
259
+ const gitGlobArgs = config.extensions.map(ext => `:(icase)*${ext}`);
242
260
 
243
261
  // Try git ls-files first (fast, respects .gitignore)
244
262
  try {
@@ -415,19 +433,48 @@ const EXT_TO_LANG = {
415
433
 
416
434
  const ENTITY_DECORATOR = /@Entity\s*\(/;
417
435
 
418
- function extractTypes(filePath) {
436
+ /**
437
+ * Dispatch-command string literals (`case 'check-before-done':`).
438
+ *
439
+ * Symbol extraction alone leaves command routers unsearchable: in `bin/gate.cjs`
440
+ * the gate names exist only as `case` labels, so the one token a agent would
441
+ * actually query on ("check-before-done") never reached the index. Indexing the
442
+ * file made it present but still unfindable. (#1337)
443
+ *
444
+ * Deliberately narrow — `case` labels only, and only those bearing an internal
445
+ * separator, so it adds routing vocabulary without pulling in arbitrary strings.
446
+ */
447
+ const DISPATCH_CASE = /\bcase\s+['"`]([A-Za-z][\w.:-]{2,60})['"`]\s*:/g;
448
+ const MAX_COMMANDS = 40;
449
+
450
+ function extractDispatchCommands(content) {
451
+ const found = new Set();
452
+ for (const m of content.matchAll(DISPATCH_CASE)) {
453
+ // Internal separator required: skips bare enum-ish words (`case 'open':`)
454
+ // while keeping real command names (`check-before-pr`, `hooks:post-edit`).
455
+ if (/[.:-]/.test(m[1])) found.add(m[1]);
456
+ if (found.size >= MAX_COMMANDS) break;
457
+ }
458
+ return [...found];
459
+ }
460
+
461
+ /**
462
+ * Read a source file once and extract both its type declarations and its
463
+ * dispatch-command literals. Single read: `generateFileEntries` used to
464
+ * re-open every file for commands, doubling I/O across the whole tree.
465
+ */
466
+ function extractFileFacts(filePath) {
419
467
  const fullPath = resolve(projectRoot, filePath);
420
- if (!existsSync(fullPath)) return [];
468
+ if (!existsSync(fullPath)) return { types: [], commands: [] };
421
469
 
422
470
  let content;
423
471
  try {
424
472
  content = readFileSync(fullPath, 'utf-8');
425
473
  } catch {
426
- return [];
474
+ return { types: [], commands: [] };
427
475
  }
428
476
 
429
- const ext = extname(filePath);
430
- const lang = EXT_TO_LANG[ext] || 'ts';
477
+ const lang = EXT_TO_LANG[normExt(filePath)] || 'ts';
431
478
  const patterns = LANG_PATTERNS[lang] || LANG_PATTERNS.ts;
432
479
 
433
480
  const lines = content.split('\n');
@@ -471,7 +518,7 @@ function extractTypes(filePath) {
471
518
  }
472
519
  }
473
520
 
474
- return types;
521
+ return { types, commands: extractDispatchCommands(content) };
475
522
  }
476
523
 
477
524
  function detectKind(line, name, lang) {
@@ -516,7 +563,6 @@ function getDirDescription(dirName) {
516
563
  }
517
564
 
518
565
  function detectLanguage(filePath) {
519
- const ext = extname(filePath);
520
566
  const langMap = {
521
567
  '.tsx': 'React/TypeScript', '.jsx': 'React/JavaScript',
522
568
  '.ts': 'TypeScript', '.mjs': 'ESM', '.cjs': 'CommonJS', '.js': 'JavaScript',
@@ -530,7 +576,7 @@ function detectLanguage(filePath) {
530
576
  '.php': 'PHP',
531
577
  '.c': 'C', '.h': 'C/C++ Header', '.cpp': 'C++', '.hpp': 'C++ Header', '.cc': 'C++',
532
578
  };
533
- return langMap[ext] || 'Unknown';
579
+ return langMap[normExt(filePath)] || 'Unknown';
534
580
  }
535
581
 
536
582
  // ---------------------------------------------------------------------------
@@ -714,7 +760,7 @@ function generateTypeIndex(allTypes) {
714
760
  * This enables precise semantic search: a query for "CompanyAuditLog" will match
715
761
  * the specific file entry rather than being diluted across a batch of 80 types.
716
762
  */
717
- function generateFileEntries(typesByFile) {
763
+ function generateFileEntries(typesByFile, commandsByFile) {
718
764
  const chunks = [];
719
765
 
720
766
  for (const [filePath, types] of Object.entries(typesByFile)) {
@@ -744,6 +790,11 @@ function generateFileEntries(typesByFile) {
744
790
  content += '\nSource file (no detected exports)\n';
745
791
  }
746
792
 
793
+ const commands = commandsByFile[filePath];
794
+ if (commands && commands.length > 0) {
795
+ content += `\nHandles commands: ${commands.join(', ')}\n`;
796
+ }
797
+
747
798
  // Build tags for filtering
748
799
  const tags = ['file', project];
749
800
  if (types.some(t => t.isEntity)) tags.push('entity');
@@ -833,17 +884,19 @@ async function main() {
833
884
  const typesByProject = {};
834
885
  const typesByDir = {};
835
886
  const typesByFile = {};
887
+ const commandsByFile = {};
836
888
 
837
889
  for (const file of files) {
838
890
  const project = getProjectName(file);
839
891
  if (!filesByProject[project]) filesByProject[project] = [];
840
892
  filesByProject[project].push(file);
841
893
 
842
- const types = extractTypes(file);
894
+ const { types, commands } = extractFileFacts(file);
843
895
 
844
896
  // Track ALL files for file-level entries (not just those with types)
845
897
  // This ensures plain JS projects without explicit exports still get indexed
846
898
  typesByFile[file] = types;
899
+ if (commands.length > 0) commandsByFile[file] = commands;
847
900
 
848
901
  for (const t of types) {
849
902
  allTypes.push(t);
@@ -866,7 +919,7 @@ async function main() {
866
919
  const dirChunks = generateDirectoryDetails(typesByDir);
867
920
  const ifaceChunks = generateInterfaceMaps(allTypes);
868
921
  const typeIdxChunks = generateTypeIndex(allTypes);
869
- const fileChunks = generateFileEntries(typesByFile);
922
+ const fileChunks = generateFileEntries(typesByFile, commandsByFile);
870
923
 
871
924
  const allChunks = [...projectChunks, ...dirChunks, ...ifaceChunks, ...typeIdxChunks, ...fileChunks];
872
925
 
package/bin/index-all.mjs CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  computeStepFingerprint,
25
25
  saveStepFingerprint,
26
26
  cleanupLegacyFingerprint,
27
+ FORCE_ENV,
27
28
  } from './lib/index-fingerprint.mjs';
28
29
  import { resolveMofloBin } from './lib/resolve-bin.mjs';
29
30
 
@@ -42,6 +43,43 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
42
43
  // during development but gets synced to .claude/scripts/ in consumer projects,
43
44
  // so __dirname-relative paths break. findProjectRoot() (lib/moflo-paths.mjs)
44
45
  // works in both locations and resolves identically to the TS bridge.
46
+ /**
47
+ * Parse argv (#1323). This script previously read NO arguments at all, so
48
+ * `index-all.mjs --force` was accepted and did nothing — a no-op that looks
49
+ * exactly like a successful forced reindex, which is how guidance staleness
50
+ * got misdiagnosed in the first place. `--force` is now an alias for the
51
+ * documented `FLO_FORCE_INDEX=1`, and anything unrecognised exits non-zero
52
+ * rather than being silently dropped.
53
+ */
54
+ function parseArgs(argv) {
55
+ const unknown = [];
56
+ let force = false;
57
+ for (const arg of argv) {
58
+ if (arg === '--force' || arg === '-f') force = true;
59
+ else if (arg === '--help' || arg === '-h') return { help: true, force, unknown };
60
+ else unknown.push(arg);
61
+ }
62
+ return { help: false, force, unknown };
63
+ }
64
+
65
+ const USAGE = `Usage: index-all.mjs [--force] [--help]
66
+
67
+ --force, -f Bypass every per-step gate (equivalent to ${FORCE_ENV}=1)
68
+ --help, -h Show this message
69
+ `;
70
+
71
+ const cliArgs = parseArgs(process.argv.slice(2));
72
+ if (cliArgs.help) {
73
+ process.stdout.write(USAGE);
74
+ process.exit(0);
75
+ }
76
+ if (cliArgs.unknown.length > 0) {
77
+ process.stderr.write(`index-all: unknown argument(s): ${cliArgs.unknown.join(', ')}\n${USAGE}`);
78
+ process.exit(2);
79
+ }
80
+ // Set before any gate decision is computed — decideStepGate reads the env var.
81
+ if (cliArgs.force) process.env[FORCE_ENV] = '1';
82
+
45
83
  const projectRoot = findProjectRoot();
46
84
  const LOG_PATH = resolve(projectRoot, '.moflo', 'logs', 'hooks.log');
47
85
  try { mkdirSync(dirname(LOG_PATH), { recursive: true }); } catch { /* best effort */ }
@@ -30,6 +30,7 @@ import { openBackend } from './lib/get-backend.mjs';
30
30
  import { applyIncrementalChunks } from './lib/incremental-write.mjs';
31
31
  import { resolveMofloBin } from './lib/resolve-bin.mjs';
32
32
  import { createProcessManager } from './lib/process-manager.mjs';
33
+ import { readGuidanceConfig } from './lib/guidance-config.mjs';
33
34
 
34
35
 
35
36
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -49,47 +50,13 @@ const DB_PATH = memoryDbPath(projectRoot);
49
50
  function loadGuidanceDirs() {
50
51
  const dirs = [];
51
52
 
52
- // 1. Read moflo.yaml / moflo.config.json for user-configured directories
53
- let configDirs = null;
54
- // #1294 — configurable SDD spec/plan location (default .moflo/specs). Parsed
55
- // here alongside the guidance dirs so step 6 can honor it and skip a
56
- // double-index when it sits inside a guidance dir.
57
- let specsDirConfig = null;
58
- const yamlPath = resolve(projectRoot, 'moflo.yaml');
59
- const jsonPath = resolve(projectRoot, 'moflo.config.json');
60
-
61
- if (existsSync(yamlPath)) {
62
- try {
63
- const content = readFileSync(yamlPath, 'utf-8');
64
- // sdd.specs_dir (snake_case or camelCase, quoted or bare). Two-step:
65
- // isolate the top-level `sdd:` block (up to the next column-0 key, blank
66
- // lines included — a naive contiguous-line regex would break on natural
67
- // whitespace between keys, #1294 review), then find specs_dir within it.
68
- const sddBlock = content.match(/(?:^|\n)[ \t]*sdd:[ \t]*\n([\s\S]*?)(?=\n\S|$)/);
69
- if (sddBlock) {
70
- const m = sddBlock[1].match(/(?:^|\n)[ \t]+specs_?[dD]ir:[ \t]*["']?([^"'\n#]+)/);
71
- if (m && m[1].trim()) specsDirConfig = m[1].trim();
72
- }
73
- // Simple YAML array extraction — avoids needing js-yaml at runtime
74
- // Matches: guidance:\n directories:\n - .claude/guidance\n - docs/guides
75
- const guidanceBlock = content.match(/guidance:\s*\n\s+directories:\s*\n((?:\s+-\s+.+\n?)+)/);
76
- if (guidanceBlock) {
77
- const items = guidanceBlock[1].match(/-\s+(.+)/g);
78
- if (items && items.length > 0) {
79
- configDirs = items.map(item => item.replace(/^-\s+/, '').trim());
80
- }
81
- }
82
- } catch { /* ignore parse errors, fall through to defaults */ }
83
- } else if (existsSync(jsonPath)) {
84
- try {
85
- const raw = JSON.parse(readFileSync(jsonPath, 'utf-8'));
86
- if (raw.guidance?.directories && Array.isArray(raw.guidance.directories)) {
87
- configDirs = raw.guidance.directories;
88
- }
89
- const sd = raw.sdd?.specs_dir ?? raw.sdd?.specsDir;
90
- if (typeof sd === 'string' && sd.trim()) specsDirConfig = sd.trim();
91
- } catch { /* ignore parse errors */ }
92
- }
53
+ // 1. Read moflo.yaml / moflo.config.json for user-configured directories.
54
+ // Delegated to lib/guidance-config.mjs so the step GATE fingerprints
55
+ // exactly the directories this indexer walks — they disagreed before
56
+ // #1323, which left every non-default directory permanently stale.
57
+ // #1294 — specs_dir comes from the same read so step 6 can honor it and
58
+ // skip a double-index when it sits inside a guidance dir.
59
+ const { directories: configDirs, specsDir: specsDirConfig } = readGuidanceConfig(projectRoot);
93
60
 
94
61
  // Use config dirs or fall back to defaults
95
62
  // Each directory gets a unique prefix derived from its path to avoid key collisions
@@ -48,7 +48,7 @@ const statsOnly = args.includes('--stats');
48
48
  function log(msg) { console.log(`[index-patterns] ${msg}`); }
49
49
  function debug(msg) { if (verbose) console.log(`[index-patterns] ${msg}`); }
50
50
 
51
- const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.py', '.go', '.rs']);
51
+ const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs']);
52
52
  const EXCLUDE_DIRS = new Set([
53
53
  'node_modules', 'dist', 'build', '.next', 'coverage',
54
54
  '.claude', '.swarm', '.moflo', '.git', 'template',
@@ -123,7 +123,7 @@ function collectSourceFiles(dir, maxDepth = 8, depth = 0) {
123
123
  const fullPath = resolve(dir, entry.name);
124
124
  if (entry.isDirectory()) {
125
125
  files.push(...collectSourceFiles(fullPath, maxDepth, depth + 1));
126
- } else if (entry.isFile() && SOURCE_EXTENSIONS.has(extname(entry.name))) {
126
+ } else if (entry.isFile() && SOURCE_EXTENSIONS.has(extname(entry.name).toLowerCase())) {
127
127
  files.push(fullPath);
128
128
  }
129
129
  }
@@ -58,7 +58,7 @@ const TEST_FILE_PATTERNS = [
58
58
  /\.test-\w+\.\w+$/,
59
59
  ];
60
60
 
61
- const TEST_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs']);
61
+ const TEST_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
62
62
 
63
63
  const EXCLUDE_DIRS = new Set([
64
64
  'node_modules', 'dist', 'build', '.next', 'coverage',
@@ -193,8 +193,10 @@ function getTestFiles() {
193
193
  // Strategy 1: git ls-files for tracked test files
194
194
  let gitFiles = [];
195
195
  try {
196
+ // `:(icase)` — git pathspec globs are case-sensitive regardless of
197
+ // core.ignorecase, so `Foo.Test.mjs` would otherwise be skipped (#1337).
196
198
  const raw = execFileSync(
197
- 'git', ['ls-files', '--', '*.test.*', '*.spec.*', '*.test-*'],
199
+ 'git', ['ls-files', '--', ':(icase)*.test.*', ':(icase)*.spec.*', ':(icase)*.test-*'],
198
200
  { cwd: projectRoot, encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, windowsHide: true }
199
201
  ).trim();
200
202
 
@@ -207,7 +209,7 @@ function getTestFiles() {
207
209
  if (f.startsWith(ex + '/')) return false;
208
210
  }
209
211
  // Only include recognized extensions
210
- const ext = extname(f);
212
+ const ext = extname(f).toLowerCase();
211
213
  return TEST_EXTENSIONS.has(ext);
212
214
  });
213
215
  }
@@ -239,7 +241,7 @@ function walkTestFiles(dir, results) {
239
241
  walkTestFiles(resolve(dir, entry.name), results);
240
242
  }
241
243
  } else if (entry.isFile()) {
242
- const ext = extname(entry.name);
244
+ const ext = extname(entry.name).toLowerCase();
243
245
  if (!TEST_EXTENSIONS.has(ext)) continue;
244
246
 
245
247
  // Include if it matches test patterns OR if it's inside a test directory
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Single source of truth for reading the `guidance:` block out of a project's
3
+ * moflo.yaml / moflo.config.json.
4
+ *
5
+ * Why this is shared rather than parsed per-caller (#1323): the guidance
6
+ * INDEXER honoured `guidance.directories` while the guidance step GATE
7
+ * fingerprinted a hardcoded `.claude/guidance`. Any configured directory
8
+ * beyond that default was indexed but never fingerprinted, so edits there
9
+ * never invalidated the gate and the guidance namespace went stale
10
+ * indefinitely — with embeddings regenerated from the same stale rows, so
11
+ * nothing downstream detected the drift. Two components disagreeing about one
12
+ * config key is the bug; duplicating the parse a third time would re-create it.
13
+ *
14
+ * Contract: never throws. A missing, unreadable, or malformed config yields
15
+ * `{ directories: null, specsDir: null }` and callers apply their own defaults.
16
+ */
17
+
18
+ import { existsSync, readFileSync } from 'fs';
19
+ import { resolve } from 'path';
20
+
21
+ /** Default guidance directories when nothing is configured. */
22
+ export const DEFAULT_GUIDANCE_DIRS = ['.claude/guidance', 'docs/guides'];
23
+
24
+ // Parsing is cheap but the gate calls this twice per step (pre + post), and
25
+ // `index-all` runs several steps per session-start. Cache per project root.
26
+ const _configCache = new Map();
27
+
28
+ function parseYaml(content) {
29
+ let directories = null;
30
+ let specsDir = null;
31
+
32
+ // sdd.specs_dir (snake_case or camelCase, quoted or bare). Two-step: isolate
33
+ // the top-level `sdd:` block (up to the next column-0 key, blank lines
34
+ // included — a naive contiguous-line regex breaks on whitespace between
35
+ // keys, #1294 review), then find specs_dir within it.
36
+ const sddBlock = content.match(/(?:^|\n)[ \t]*sdd:[ \t]*\n([\s\S]*?)(?=\n\S|$)/);
37
+ if (sddBlock) {
38
+ const m = sddBlock[1].match(/(?:^|\n)[ \t]+specs_?[dD]ir:[ \t]*["']?([^"'\n#]+)/);
39
+ if (m && m[1].trim()) specsDir = m[1].trim();
40
+ }
41
+
42
+ // Simple YAML array extraction — avoids needing js-yaml at runtime.
43
+ // Matches: guidance:\n directories:\n - .claude/guidance\n - docs/guides
44
+ const guidanceBlock = content.match(/guidance:\s*\n\s+directories:\s*\n((?:\s+-\s+.+\n?)+)/);
45
+ if (guidanceBlock) {
46
+ const items = guidanceBlock[1].match(/-\s+(.+)/g);
47
+ if (items && items.length > 0) {
48
+ const parsed = items
49
+ .map(item => item.replace(/^-\s+/, '').trim())
50
+ // Strip inline comments and surrounding quotes; drop empties so a
51
+ // malformed entry can't produce a directory that resolves to the root.
52
+ .map(d => d.replace(/\s+#.*$/, '').replace(/^["']|["']$/g, '').trim())
53
+ .filter(Boolean);
54
+ if (parsed.length > 0) directories = parsed;
55
+ }
56
+ }
57
+
58
+ return { directories, specsDir };
59
+ }
60
+
61
+ function parseJson(raw) {
62
+ let directories = null;
63
+ let specsDir = null;
64
+
65
+ if (Array.isArray(raw?.guidance?.directories)) {
66
+ const parsed = raw.guidance.directories
67
+ .filter(d => typeof d === 'string')
68
+ .map(d => d.trim())
69
+ .filter(Boolean);
70
+ if (parsed.length > 0) directories = parsed;
71
+ }
72
+
73
+ const sd = raw?.sdd?.specs_dir ?? raw?.sdd?.specsDir;
74
+ if (typeof sd === 'string' && sd.trim()) specsDir = sd.trim();
75
+
76
+ return { directories, specsDir };
77
+ }
78
+
79
+ /**
80
+ * Read the guidance config for `projectRoot`.
81
+ *
82
+ * @returns {{ directories: string[] | null, specsDir: string | null }}
83
+ * `directories` is null when unset or malformed — callers decide the default.
84
+ */
85
+ export function readGuidanceConfig(projectRoot) {
86
+ const cached = _configCache.get(projectRoot);
87
+ if (cached) return cached;
88
+
89
+ let result = { directories: null, specsDir: null };
90
+ try {
91
+ const yamlPath = resolve(projectRoot, 'moflo.yaml');
92
+ const jsonPath = resolve(projectRoot, 'moflo.config.json');
93
+ if (existsSync(yamlPath)) {
94
+ result = parseYaml(readFileSync(yamlPath, 'utf-8'));
95
+ } else if (existsSync(jsonPath)) {
96
+ result = parseJson(JSON.parse(readFileSync(jsonPath, 'utf-8')));
97
+ }
98
+ } catch {
99
+ // Malformed config must not break indexing or gating — fall back to
100
+ // defaults rather than throwing out of a session-start hook.
101
+ }
102
+
103
+ _configCache.set(projectRoot, result);
104
+ return result;
105
+ }
106
+
107
+ /**
108
+ * Guidance directories for `projectRoot`, with defaults applied.
109
+ * Always returns a non-empty array of repo-relative paths.
110
+ */
111
+ export function resolveGuidanceDirs(projectRoot) {
112
+ const { directories } = readGuidanceConfig(projectRoot);
113
+ return directories && directories.length > 0 ? directories : [...DEFAULT_GUIDANCE_DIRS];
114
+ }
115
+
116
+ /** Test seam — drop memoised config (the file is read once per process). */
117
+ export function _resetGuidanceConfigCache() {
118
+ _configCache.clear();
119
+ }
Binary file
@@ -690,94 +690,6 @@ const healthCommand = {
690
690
  }
691
691
  }
692
692
  };
693
- // Agent logs subcommand
694
- const logsCommand = {
695
- name: 'logs',
696
- description: 'Show agent activity logs',
697
- options: [
698
- {
699
- name: 'id',
700
- short: 'i',
701
- description: 'Agent ID',
702
- type: 'string'
703
- },
704
- {
705
- name: 'tail',
706
- short: 'n',
707
- description: 'Number of recent entries',
708
- type: 'number',
709
- default: 50
710
- },
711
- {
712
- name: 'level',
713
- short: 'l',
714
- description: 'Minimum log level',
715
- type: 'string',
716
- choices: ['debug', 'info', 'warn', 'error'],
717
- default: 'info'
718
- },
719
- {
720
- name: 'follow',
721
- short: 'f',
722
- description: 'Follow log output',
723
- type: 'boolean',
724
- default: false
725
- },
726
- {
727
- name: 'since',
728
- description: 'Show logs since (e.g., "1h", "30m")',
729
- type: 'string'
730
- }
731
- ],
732
- examples: [
733
- { command: 'flo agent logs -i agent-001', description: 'Show agent logs' },
734
- { command: 'flo agent logs -i agent-001 -f', description: 'Follow agent logs' },
735
- { command: 'flo agent logs -l error --since 1h', description: 'Show errors from last hour' }
736
- ],
737
- action: async (ctx) => {
738
- const agentId = ctx.args[0] || ctx.flags.id;
739
- const tail = ctx.flags.tail;
740
- const level = ctx.flags.level;
741
- if (!agentId) {
742
- output.printError('Agent ID is required. Use --id or -i');
743
- return { success: false, exitCode: 1 };
744
- }
745
- try {
746
- const result = await callMCPTool('agent_logs', {
747
- agentId,
748
- tail,
749
- level,
750
- since: ctx.flags.since,
751
- });
752
- if (ctx.flags.format === 'json') {
753
- output.printJson(result);
754
- return { success: true, data: result };
755
- }
756
- output.writeln();
757
- output.writeln(output.bold(`Logs for ${agentId}`));
758
- output.writeln(output.dim(`Showing ${result.entries.length} of ${result.total} entries`));
759
- output.writeln();
760
- for (const entry of result.entries) {
761
- const time = new Date(entry.timestamp).toLocaleTimeString();
762
- const levelStr = formatLogLevel(entry.level);
763
- output.writeln(`${output.dim(time)} ${levelStr} ${entry.message}`);
764
- if (entry.context && Object.keys(entry.context).length > 0) {
765
- output.writeln(output.dim(` ${JSON.stringify(entry.context)}`));
766
- }
767
- }
768
- return { success: true, data: result };
769
- }
770
- catch (error) {
771
- if (error instanceof MCPClientError) {
772
- output.printError(`Logs error: ${error.message}`);
773
- }
774
- else {
775
- output.printError(`Unexpected error: ${String(error)}`);
776
- }
777
- return { success: false, exitCode: 1 };
778
- }
779
- }
780
- };
781
693
  function formatLogLevel(level) {
782
694
  switch (level) {
783
695
  case 'debug':
@@ -796,7 +708,7 @@ function formatLogLevel(level) {
796
708
  export const agentCommand = {
797
709
  name: 'agent',
798
710
  description: 'Agent management commands',
799
- subcommands: [spawnCommand, listCommand, statusCommand, stopCommand, metricsCommand, poolCommand, healthCommand, logsCommand],
711
+ subcommands: [spawnCommand, listCommand, statusCommand, stopCommand, metricsCommand, poolCommand, healthCommand],
800
712
  options: [],
801
713
  examples: [
802
714
  { command: 'flo agent spawn -t coder', description: 'Spawn a coder agent' },
@@ -11,7 +11,7 @@
11
11
  * - Module communities using Louvain algorithm
12
12
  * - Circular dependency detection
13
13
  *
14
- * Created with motailz.com
14
+ * Created with cielolimitada.com
15
15
  */
16
16
  import { output } from '../output.js';
17
17
  import { callMCPTool, MCPClientError } from '../mcp-client.js';
@@ -18,11 +18,14 @@ const pretrainCommand = {
18
18
  name: 'pretrain',
19
19
  description: 'Benchmark self-learning pre-training system (SONA, EWC++, MoE)',
20
20
  options: [
21
- { name: 'iterations', short: 'i', type: 'number', description: 'Benchmark iterations', default: '100' },
22
- { name: 'warmup', short: 'w', type: 'number', description: 'Warmup iterations', default: '10' },
21
+ { name: 'iterations', short: 'i', type: 'number', description: 'Benchmark iterations', default: 100 },
22
+ { name: 'warmup', short: 'w', type: 'number', description: 'Warmup iterations', default: 10 },
23
23
  { name: 'output', short: 'o', type: 'string', description: 'Output format: text, json', default: 'text' },
24
24
  { name: 'save', short: 's', type: 'string', description: 'Save results to file' },
25
- { name: 'verbose', short: 'v', type: 'boolean', description: 'Verbose output', default: 'false' },
25
+ // Boolean default, not the string 'false' — this option shadows the global
26
+ // --verbose, so its default is the one the parser applies, and 'false' is
27
+ // a truthy string that would turn verbose ON by default.
28
+ { name: 'verbose', short: 'v', type: 'boolean', description: 'Verbose output', default: false },
26
29
  ],
27
30
  examples: [
28
31
  { command: 'flo benchmark pretrain', description: 'Run pre-training benchmarks' },
@@ -82,9 +85,9 @@ const neuralCommand = {
82
85
  name: 'neural',
83
86
  description: 'Benchmark neural operations (embeddings, WASM, Flash Attention)',
84
87
  options: [
85
- { name: 'iterations', short: 'i', type: 'number', description: 'Benchmark iterations', default: '100' },
86
- { name: 'dimension', short: 'd', type: 'number', description: 'Embedding dimension', default: '384' },
87
- { name: 'vectors', short: 'n', type: 'number', description: 'Number of test vectors', default: '1000' },
88
+ { name: 'iterations', short: 'i', type: 'number', description: 'Benchmark iterations', default: 100 },
89
+ { name: 'dimension', short: 'd', type: 'number', description: 'Embedding dimension', default: 384 },
90
+ { name: 'vectors', short: 'n', type: 'number', description: 'Number of test vectors', default: 1000 },
88
91
  { name: 'output', short: 'o', type: 'string', description: 'Output format: text, json', default: 'text' },
89
92
  ],
90
93
  examples: [
@@ -242,7 +245,7 @@ const memoryCommand = {
242
245
  name: 'memory',
243
246
  description: 'Benchmark memory operations (HNSW search, store, retrieve)',
244
247
  options: [
245
- { name: 'iterations', short: 'i', type: 'number', description: 'Benchmark iterations', default: '100' },
248
+ { name: 'iterations', short: 'i', type: 'number', description: 'Benchmark iterations', default: 100 },
246
249
  { name: 'output', short: 'o', type: 'string', description: 'Output format: text, json', default: 'text' },
247
250
  ],
248
251
  examples: [
@@ -356,7 +359,7 @@ const allCommand = {
356
359
  name: 'all',
357
360
  description: 'Run all benchmark suites',
358
361
  options: [
359
- { name: 'iterations', short: 'i', type: 'number', description: 'Benchmark iterations', default: '50' },
362
+ { name: 'iterations', short: 'i', type: 'number', description: 'Benchmark iterations', default: 50 },
360
363
  { name: 'output', short: 'o', type: 'string', description: 'Output format: text, json', default: 'text' },
361
364
  { name: 'save', short: 's', type: 'string', description: 'Save results to file' },
362
365
  ],
@@ -2,7 +2,7 @@
2
2
  * V3 CLI Claims Command
3
3
  * Claims-based authorization, permissions, and access control
4
4
  *
5
- * Created with ❤️ by motailz.com
5
+ * Created with ❤️ by cielolimitada.com
6
6
  */
7
7
  import { output } from '../output.js';
8
8
  import os from 'os';
@@ -366,7 +366,7 @@ export const claimsCommand = {
366
366
  'admin:* - Administrative operations',
367
367
  ]);
368
368
  output.writeln();
369
- output.writeln(output.dim('Created with ❤️ by motailz.com'));
369
+ output.writeln(output.dim('Created with ❤️ by cielolimitada.com'));
370
370
  return { success: true };
371
371
  },
372
372
  };