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.
- package/.claude/agents/core/coder.md +2 -3
- package/.claude/agents/core/researcher.md +3 -4
- package/.claude/agents/core/reviewer.md +6 -8
- package/.claude/guidance/shipped/moflo-spell-engine.md +13 -10
- package/.claude/helpers/gate-hook.mjs +67 -2
- package/.claude/helpers/gate.cjs +173 -12
- package/.claude/helpers/hook-handler.cjs +0 -0
- package/.claude/helpers/prompt-hook.mjs +0 -0
- package/.claude/skills/connector-builder/SKILL.md +5 -4
- package/.claude/skills/fl/phases.md +1 -1
- package/.claude/skills/fl/sdd.md +1 -1
- package/.claude/skills/spell-builder/SKILL.md +5 -4
- package/.claude/skills/spell-builder/architecture.md +4 -4
- package/.claude/skills/spell-builder/connectors/github-cli/README.md +22 -17
- package/.claude/skills/spell-builder/connectors/http/README.md +18 -11
- package/.claude/skills/spell-builder/connectors/local-outlook/README.md +8 -7
- package/.claude/skills/spell-builder/connectors/playwright/README.md +10 -8
- package/.claude/skills/spell-builder/steps/agent/README.md +25 -21
- package/.claude/skills/verify/SKILL.md +24 -3
- package/bin/gate-hook.mjs +67 -2
- package/bin/gate.cjs +173 -12
- package/bin/generate-code-map.mjs +70 -17
- package/bin/index-all.mjs +38 -0
- package/bin/index-guidance.mjs +8 -41
- package/bin/index-patterns.mjs +2 -2
- package/bin/index-tests.mjs +6 -4
- package/bin/lib/guidance-config.mjs +119 -0
- package/bin/lib/index-fingerprint.mjs +0 -0
- package/dist/src/cli/commands/agent.js +1 -89
- package/dist/src/cli/commands/analyze.js +1 -1
- package/dist/src/cli/commands/benchmark.js +11 -8
- package/dist/src/cli/commands/claims.js +2 -2
- package/dist/src/cli/commands/completions.js +1 -1
- package/dist/src/cli/commands/config.js +256 -96
- package/dist/src/cli/commands/deployment.js +6 -6
- package/dist/src/cli/commands/diagnose.js +1 -1
- package/dist/src/cli/commands/doctor-checks-config.js +18 -22
- package/dist/src/cli/commands/doctor-checks-deep.js +1 -1
- package/dist/src/cli/commands/doctor-fixes.js +36 -11
- package/dist/src/cli/commands/doctor.js +1 -1
- package/dist/src/cli/commands/embeddings.js +64 -23
- package/dist/src/cli/commands/github.js +1 -1
- package/dist/src/cli/commands/guidance.js +10 -10
- package/dist/src/cli/commands/hive-mind.js +8 -70
- package/dist/src/cli/commands/hooks.js +32 -219
- package/dist/src/cli/commands/mcp.js +5 -2
- package/dist/src/cli/commands/memory.js +59 -46
- package/dist/src/cli/commands/neural.js +21 -21
- package/dist/src/cli/commands/performance.js +14 -7
- package/dist/src/cli/commands/plugins.js +2 -2
- package/dist/src/cli/commands/providers.js +2 -2
- package/dist/src/cli/commands/retire.js +1 -1
- package/dist/src/cli/commands/route.js +1 -1
- package/dist/src/cli/commands/security.js +4 -4
- package/dist/src/cli/commands/status.js +18 -13
- package/dist/src/cli/commands/task.js +8 -0
- package/dist/src/cli/config/cli-config-store.js +326 -0
- package/dist/src/cli/hooks/workers/index.js +15 -9
- package/dist/src/cli/index.js +43 -33
- package/dist/src/cli/init/claudemd-generator.js +3 -3
- package/dist/src/cli/init/helpers-generator.js +203 -20
- package/dist/src/cli/init/moflo-yaml-template.js +11 -3
- package/dist/src/cli/init/settings-generator.js +10 -2
- package/dist/src/cli/mcp-client.js +12 -2
- package/dist/src/cli/mcp-server.js +5 -23
- package/dist/src/cli/mcp-tools/analysis-tools.js +103 -0
- package/dist/src/cli/mcp-tools/coverage-tools.js +206 -0
- package/dist/src/cli/mcp-tools/hive-mind-tools.js +71 -0
- package/dist/src/cli/mcp-tools/hooks-tools.js +25 -9
- package/dist/src/cli/mcp-tools/memory-admin-tools.js +538 -0
- package/dist/src/cli/mcp-tools/memory-tools.js +26 -5
- package/dist/src/cli/mcp-tools/neural-tools.js +56 -87
- package/dist/src/cli/mcp-tools/performance-tools.js +76 -39
- package/dist/src/cli/mcp-tools/progress-tools.js +142 -0
- package/dist/src/cli/mcp-tools/security-tools.js +1 -1
- package/dist/src/cli/mcp-tools/session-tools.js +116 -0
- package/dist/src/cli/mcp-tools/synthetic.js +74 -0
- package/dist/src/cli/mcp-tools/system-tools.js +126 -78
- package/dist/src/cli/mcp-tools/task-tools.js +101 -1
- package/dist/src/cli/movector/coverage-router.js +48 -6
- package/dist/src/cli/movector/diff-classifier.js +8 -6
- package/dist/src/cli/movector/flash-attention.js +1 -1
- package/dist/src/cli/movector/vector-db.js +1 -1
- package/dist/src/cli/parser.js +81 -12
- package/dist/src/cli/services/daemon-lock.js +5 -2
- package/dist/src/cli/services/hook-block-hash.js +3 -1
- package/dist/src/cli/services/moflo-version.js +62 -0
- package/dist/src/cli/services/movector-training.js +1 -1
- package/dist/src/cli/services/spell-gate.js +6 -4
- package/dist/src/cli/services/worker-daemon.js +37 -7
- package/dist/src/cli/shared/utils/load-average.js +59 -0
- package/dist/src/cli/spells/commands/agent-command.js +55 -25
- package/dist/src/cli/suggest.js +1 -1
- package/dist/src/cli/swarm/swarm-persistence.js +112 -0
- package/dist/src/cli/swarm/unified-coordinator.js +118 -3
- package/dist/src/cli/version.js +1 -1
- package/package.json +5 -3
- 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',
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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[
|
|
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 =
|
|
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 */ }
|
package/bin/index-guidance.mjs
CHANGED
|
@@ -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
|
-
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
const
|
|
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
|
package/bin/index-patterns.mjs
CHANGED
|
@@ -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
|
}
|
package/bin/index-tests.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
22
|
-
{ name: 'warmup', short: 'w', type: 'number', description: 'Warmup iterations', default:
|
|
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
|
-
|
|
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:
|
|
86
|
-
{ name: 'dimension', short: 'd', type: 'number', description: 'Embedding dimension', default:
|
|
87
|
-
{ name: 'vectors', short: 'n', type: 'number', description: 'Number of test vectors', default:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
369
|
+
output.writeln(output.dim('Created with ❤️ by cielolimitada.com'));
|
|
370
370
|
return { success: true };
|
|
371
371
|
},
|
|
372
372
|
};
|