moflo 4.9.12 → 4.9.14

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 (33) hide show
  1. package/.claude/helpers/gate.cjs +21 -5
  2. package/.claude/skills/eldar/SKILL.md +305 -0
  3. package/.claude/skills/fl/phases.md +18 -2
  4. package/.claude/skills/simplify/SKILL.md +35 -48
  5. package/README.md +25 -0
  6. package/bin/gate.cjs +21 -5
  7. package/bin/hooks.mjs +2 -2
  8. package/bin/index-guidance.mjs +14 -24
  9. package/bin/index-patterns.mjs +13 -10
  10. package/bin/session-start-launcher.mjs +64 -10
  11. package/bin/simplify-classify.cjs +211 -0
  12. package/dist/src/cli/commands/doctor-checks-config.js +246 -0
  13. package/dist/src/cli/commands/doctor-checks-deep.js +14 -0
  14. package/dist/src/cli/commands/doctor-checks-intelligence.js +197 -0
  15. package/dist/src/cli/commands/doctor-checks-memory.js +207 -0
  16. package/dist/src/cli/commands/doctor-checks-platform.js +138 -0
  17. package/dist/src/cli/commands/doctor-checks-runtime.js +170 -0
  18. package/dist/src/cli/commands/doctor-fixes.js +165 -0
  19. package/dist/src/cli/commands/doctor-registry.js +109 -0
  20. package/dist/src/cli/commands/doctor-render.js +203 -0
  21. package/dist/src/cli/commands/doctor-types.js +9 -0
  22. package/dist/src/cli/commands/doctor-version.js +134 -0
  23. package/dist/src/cli/commands/doctor-zombies.js +201 -0
  24. package/dist/src/cli/commands/doctor.js +35 -1657
  25. package/dist/src/cli/init/helpers-generator.js +21 -5
  26. package/dist/src/cli/init/moflo-init.js +20 -268
  27. package/dist/src/cli/init/moflo-yaml-template.js +370 -0
  28. package/dist/src/cli/mcp-tools/hooks-tools.js +3 -1
  29. package/dist/src/cli/movector/model-router.js +66 -20
  30. package/dist/src/cli/services/hook-block-hash.js +23 -2
  31. package/dist/src/cli/version.js +1 -1
  32. package/package.json +2 -2
  33. package/scripts/post-install-bootstrap.mjs +1 -0
@@ -279,7 +279,11 @@ var DANGEROUS = ['rm -rf /', 'format c:', 'del /s /q c:\\\\', ':(){:|:&};:', 'mk
279
279
  var DIRECTIVE_RE = /^(yes|no|yeah|yep|nope|sure|ok|okay|correct|right|exactly|perfect)\\b/i;
280
280
  var TASK_RE = /\\b(fix|bug|error|implement|add|create|build|write|refactor|debug|test|feature|issue|security|optimi)\\b/i;
281
281
  var TEST_RUNNER_RE = /(?:^|[^a-z])(?:npm|yarn|pnpm|bun)\\s+(?:run\\s+)?(?:test|t)(?:[:\\s]|$)|\\b(?:npx|pnpx)\\s+(?:vitest|jest|mocha|ava|tap|jasmine|pytest)\\b|(?:^|;|&&|\\|\\|)\\s*(?:vitest|jest|pytest|mocha|jasmine|tap|ava)\\s|\\b(?:cargo|go|deno|dotnet|mvn)\\s+test\\b|\\bgradle\\w*\\s+test\\b/i;
282
- var EDIT_RESET_SKIP_RE = /\\.(md|markdown|txt|rst|adoc|lock|gitignore)$|(?:^|[\\\\\\/])(CHANGELOG(?:\\.md)?|\\.env\\.example|package-lock\\.json|pnpm-lock\\.yaml|yarn\\.lock|bun\\.lockb)$/i;
282
+ var EDIT_RESET_SKIP_BOTH_RE = /\\.(md|markdown|txt|rst|adoc|lock|gitignore)$|(?:^|[\\\\\\/])(CHANGELOG(?:\\.md)?|\\.env\\.example|package-lock\\.json|pnpm-lock\\.yaml|yarn\\.lock|bun\\.lockb)$/i;
283
+ // Test files: invalidate testsRun but preserve simplifyRun (#908) — /simplify
284
+ // already reviewed the production code, touching tests/fixtures doesn't expose
285
+ // new untested surface for code review.
286
+ var EDIT_RESET_SKIP_SIMPLIFY_ONLY_RE = /(?:^|[\\\\\\/])(__tests__|__mocks__|tests?|spec|specs|cypress|e2e|fixtures?)[\\\\\\/]|\\.(test|spec)\\.[mc]?[jt]sx?$|\\.fixture\\.[mc]?[jt]sx?$/i;
283
287
 
284
288
  switch (command) {
285
289
  case 'check-before-agent': {
@@ -370,11 +374,20 @@ switch (command) {
370
374
  }
371
375
  case 'reset-edit-gates': {
372
376
  var fp = process.env.TOOL_INPUT_file_path || '';
373
- if (fp && EDIT_RESET_SKIP_RE.test(fp)) break;
377
+ // Inert files (markdown, lockfiles, CHANGELOG, .env.example): no gate reset.
378
+ if (fp && EDIT_RESET_SKIP_BOTH_RE.test(fp)) break;
374
379
  var s = readState();
375
- if (!s.testsRun && !s.simplifyRun) break;
376
- s.testsRun = false;
377
- s.simplifyRun = false;
380
+ // Test-only edits invalidate testsRun but preserve simplifyRun (#908).
381
+ var isTestOnly = fp && EDIT_RESET_SKIP_SIMPLIFY_ONLY_RE.test(fp);
382
+ var resetTests = s.testsRun;
383
+ var resetSimplify = s.simplifyRun && !isTestOnly;
384
+ if (!resetTests && !resetSimplify) break;
385
+ var gates = [];
386
+ if (resetTests) { s.testsRun = false; gates.push('tests'); }
387
+ if (resetSimplify) { s.simplifyRun = false; gates.push('simplify'); }
388
+ if (fp) {
389
+ s.lastResetBy = { file: fp, at: new Date().toISOString(), gates: gates };
390
+ }
378
391
  writeState(s);
379
392
  break;
380
393
  }
@@ -391,6 +404,9 @@ switch (command) {
391
404
  for (var i = 0; i < missing.length; i++) {
392
405
  process.stderr.write(' - ' + missing[i] + '\\n');
393
406
  }
407
+ if (s.lastResetBy && s.lastResetBy.file) {
408
+ process.stderr.write('Last gate reset: ' + s.lastResetBy.file + ' (' + (s.lastResetBy.gates || []).join(', ') + ')\\n');
409
+ }
394
410
  process.stderr.write('Disable per-gate via moflo.yaml:\\n');
395
411
  process.stderr.write(' gates:\\n testing_gate: false\\n simplify_gate: false\\n learnings_gate: false\\n');
396
412
  process.exit(2);
@@ -14,14 +14,8 @@ import * as path from 'path';
14
14
  import { execSync } from 'child_process';
15
15
  import { locateMofloRootPath } from '../services/moflo-require.js';
16
16
  import { errorDetail } from '../shared/utils/error-detail.js';
17
- // Directories that walkers should never recurse into when discovering project
18
- // structure. The runtime state dirs (.swarm, .moflo) and other generated/
19
- // tooling trees would only produce noise. Hoisted from three identical inline
20
- // copies in this file's discover* helpers.
21
- const WALK_SKIP_DIRS = new Set([
22
- 'node_modules', '.git', 'dist', 'build', 'coverage', '.next', '.reports',
23
- '.swarm', '.moflo', 'packages',
24
- ]);
17
+ import { discoverGuidanceDirs, discoverSrcDirs, discoverTestDirs, detectExtensions, renderMofloYaml, } from './moflo-yaml-template.js';
18
+ export { discoverTestDirs };
25
19
  // ============================================================================
26
20
  // Init
27
21
  // ============================================================================
@@ -39,122 +33,6 @@ function mofloRootJoin(...segments) {
39
33
  const hit = locateMofloRootPath(segments.join('/'));
40
34
  return hit ? [hit] : [];
41
35
  }
42
- /**
43
- * Discover guidance directories by checking top-level candidates AND walking
44
- * the project tree for subproject .claude/guidance dirs (monorepo support).
45
- */
46
- function discoverGuidanceDirs(root) {
47
- const TOP_LEVEL = ['.claude/guidance', 'docs/guides', 'docs', 'architecture', 'adr', '.cursor/rules'];
48
- const found = TOP_LEVEL.filter(d => fs.existsSync(path.join(root, d)));
49
- // Walk up to 3 levels deep looking for .claude/guidance in subprojects
50
- function walk(dir, depth) {
51
- if (depth > 3)
52
- return;
53
- try {
54
- const entries = fs.readdirSync(path.join(root, dir), { withFileTypes: true });
55
- for (const entry of entries) {
56
- if (!entry.isDirectory() || WALK_SKIP_DIRS.has(entry.name))
57
- continue;
58
- const rel = dir ? `${dir}/${entry.name}` : entry.name;
59
- const guidancePath = `${rel}/.claude/guidance`;
60
- if (fs.existsSync(path.join(root, guidancePath))) {
61
- // Verify it has .md files
62
- try {
63
- const files = fs.readdirSync(path.join(root, guidancePath));
64
- if (files.some(f => f.endsWith('.md')))
65
- found.push(guidancePath);
66
- }
67
- catch { /* skip unreadable */ }
68
- }
69
- else {
70
- walk(rel, depth + 1);
71
- }
72
- }
73
- }
74
- catch { /* skip unreadable directories */ }
75
- }
76
- walk('', 0);
77
- return found;
78
- }
79
- /**
80
- * Discover test directories by checking common locations and walking for
81
- * colocated __tests__ dirs. Returns relative paths.
82
- */
83
- export function discoverTestDirs(root) {
84
- const TOP_LEVEL = ['tests', 'test', '__tests__', 'spec', 'e2e'];
85
- const found = TOP_LEVEL.filter(d => fs.existsSync(path.join(root, d)));
86
- // Walk up to 3 levels deep looking for __tests__ dirs inside src
87
- function walk(dir, depth) {
88
- if (depth > 3)
89
- return;
90
- try {
91
- const entries = fs.readdirSync(path.join(root, dir), { withFileTypes: true });
92
- for (const entry of entries) {
93
- if (!entry.isDirectory() || WALK_SKIP_DIRS.has(entry.name))
94
- continue;
95
- const rel = dir ? `${dir}/${entry.name}` : entry.name;
96
- if (entry.name === '__tests__') {
97
- found.push(rel);
98
- }
99
- else {
100
- walk(rel, depth + 1);
101
- }
102
- }
103
- }
104
- catch { /* skip unreadable directories */ }
105
- }
106
- walk('', 0);
107
- return found;
108
- }
109
- /**
110
- * Discover source directories by walking the project tree.
111
- * Finds directories named 'src' (or top-level 'packages', 'lib', etc.)
112
- * that contain .ts/.tsx/.js/.jsx files. Skips node_modules, dist, etc.
113
- */
114
- function discoverSrcDirs(root) {
115
- // Top-level candidates that are always source roots if they exist
116
- const TOP_LEVEL = ['packages', 'lib', 'app', 'apps', 'services', 'server', 'client'];
117
- const found = [];
118
- // Add top-level candidates first
119
- for (const d of TOP_LEVEL) {
120
- if (fs.existsSync(path.join(root, d)))
121
- found.push(d);
122
- }
123
- // Walk up to 3 levels deep looking for 'src' and 'migrations' directories
124
- const SRC_NAMES = new Set(['src', 'migrations']);
125
- function walk(dir, depth) {
126
- if (depth > 3)
127
- return;
128
- try {
129
- const entries = fs.readdirSync(path.join(root, dir), { withFileTypes: true });
130
- for (const entry of entries) {
131
- if (!entry.isDirectory() || WALK_SKIP_DIRS.has(entry.name))
132
- continue;
133
- const rel = dir ? `${dir}/${entry.name}` : entry.name;
134
- if (SRC_NAMES.has(entry.name)) {
135
- // Check it actually has source files
136
- try {
137
- const files = fs.readdirSync(path.join(root, rel));
138
- const hasSource = files.some(f => /\.(ts|tsx|js|jsx)$/.test(f));
139
- if (hasSource)
140
- found.push(rel);
141
- }
142
- catch { /* skip unreadable */ }
143
- }
144
- else {
145
- walk(rel, depth + 1);
146
- }
147
- }
148
- }
149
- catch { /* skip unreadable directories */ }
150
- }
151
- walk('', 0);
152
- // Deduplicate: if 'packages' is found, don't also include 'packages/foo/src'
153
- // since the code-map walker handles subdirs
154
- return found.filter(d => {
155
- return !found.some(other => other !== d && d.startsWith(other + '/'));
156
- });
157
- }
158
36
  /**
159
37
  * Run interactive wizard to collect user preferences.
160
38
  */
@@ -276,151 +154,25 @@ function generateConfig(root, force, answers) {
276
154
  if (fs.existsSync(configPath) && !force) {
277
155
  return { name: 'moflo.yaml', status: 'skipped', detail: 'Already exists (use --force to overwrite)' };
278
156
  }
279
- const projectName = path.basename(root);
280
- const guidanceDirs = answers?.guidanceDirs ?? ['.claude/guidance'];
281
157
  const srcDirs = answers?.srcDirs ?? ['src'];
282
- const testDirs = answers?.testDirs ?? ['tests'];
283
- const gatesEnabled = answers?.gates ?? true;
284
- // Detect languages
285
- const extensions = new Set();
286
- for (const dir of srcDirs) {
287
- const fullDir = path.join(root, dir);
288
- if (fs.existsSync(fullDir)) {
289
- try {
290
- scanExtensions(fullDir, extensions, 0, 3);
291
- }
292
- catch { /* skip */ }
293
- }
294
- }
295
- const detectedExts = extensions.size > 0
296
- ? [...extensions].sort()
297
- : ['.ts', '.tsx', '.js', '.jsx'];
298
- const yaml = `# MoFlo Project Configuration
299
- # Generated by: moflo init
300
- # Docs: https://github.com/eric-cielo/moflo
301
-
302
- project:
303
- name: "${projectName}"
304
-
305
- # Guidance/knowledge docs to index for semantic search
306
- guidance:
307
- directories:
308
- ${guidanceDirs.map(d => ` - ${d}`).join('\n')}
309
- namespace: guidance
310
-
311
- # Source directories for code navigation map
312
- code_map:
313
- directories:
314
- ${srcDirs.map(d => ` - ${d}`).join('\n')}
315
- extensions: [${detectedExts.map(e => `"${e}"`).join(', ')}]
316
- exclude: [node_modules, dist, .next, coverage, build, __pycache__, target, .git]
317
- namespace: code-map
318
-
319
- # Test file discovery and indexing
320
- tests:
321
- directories:
322
- ${testDirs.map(d => ` - ${d}`).join('\n')}
323
- patterns: ["*.test.*", "*.spec.*", "*.test-*"]
324
- extensions: [".ts", ".tsx", ".js", ".jsx"]
325
- exclude: [node_modules, coverage, dist]
326
- namespace: tests
327
-
328
- # Spell gates (enforced via Claude Code hooks)
329
- gates:
330
- memory_first: ${gatesEnabled}
331
- task_create_first: ${gatesEnabled}
332
- context_tracking: ${gatesEnabled}
333
-
334
- # Auto-index on session start
335
- auto_index:
336
- guidance: ${answers?.guidance ?? true}
337
- code_map: ${answers?.codeMap ?? true}
338
- tests: ${answers?.tests ?? true}
339
-
340
- # Memory backend
341
- memory:
342
- backend: sql.js
343
- embedding_model: Xenova/all-MiniLM-L6-v2
344
- namespace: default
345
-
346
- # Hook toggles (all on by default — disable to slim down)
347
- hooks:
348
- pre_edit: true # Track file edits for learning
349
- post_edit: true # Record edit outcomes, train neural patterns
350
- pre_task: true # Get agent routing before task spawn
351
- post_task: true # Record task results for learning
352
- gate: ${gatesEnabled} # Spell gate enforcement (memory-first, task-create-first)
353
- route: true # Intelligent task routing on each prompt
354
- stop_hook: ${answers?.stopHook ?? true} # Session-end persistence and metric export
355
- session_restore: true # Restore session state on start
356
- notification: true # Hook into Claude Code notifications
357
-
358
- # MCP server options
359
- mcp:
360
- tool_defer: deferred # Defer 150+ tool schemas; loaded on demand via ToolSearch
361
- auto_start: false # Auto-start MCP server on session begin
362
-
363
- # Spell step sandboxing (OS-level process isolation for bash steps)
364
- # Platform support: macOS (sandbox-exec), Linux/WSL (bwrap). Windows has no OS sandbox.
365
- # Tiers:
366
- # auto — Use best available sandbox for this platform (recommended when enabled)
367
- # denylist-only — Layer 1 only: block catastrophic commands, no OS isolation
368
- # full — Require full OS isolation; throws if the sandbox tool is unavailable
369
- sandbox:
370
- enabled: false # Set to true to wrap bash steps in an OS sandbox
371
- tier: auto # auto | denylist-only | full
372
-
373
- # Status line display (shown at bottom of Claude Code)
374
- # mode: "compact" (default), "single-line", or "dashboard" (full multi-line)
375
- status_line:
376
- enabled: true
377
- mode: compact
378
- branding: "MoFlo V4"
379
- show_git: true
380
- show_session: true
381
- show_swarm: true
382
- show_mcp: true
383
-
384
- # Model preferences (haiku, sonnet, opus)
385
- # These are static fallbacks. When model_routing.enabled is true (default),
386
- # the dynamic router takes precedence based on task complexity.
387
- models:
388
- default: opus # Model for general tasks (kept high for unknowns)
389
- research: sonnet # Model for research/exploration agents
390
- review: sonnet # Code review never needs opus reasoning
391
- test: sonnet # Model for test-writing agents
392
-
393
- # Intelligent model routing (auto-selects haiku/sonnet/opus per task)
394
- # When enabled, overrides the static model preferences above
395
- # by analyzing task complexity and routing to the cheapest capable model.
396
- model_routing:
397
- enabled: true # Set to false to pin to the static models above
398
- confidence_threshold: 0.85 # Min confidence before escalating to a more capable model
399
- cost_optimization: true # Prefer cheaper models when confidence is high
400
- circuit_breaker: true # Penalize models that fail repeatedly
401
- # Per-agent overrides (set to "inherit" to use routing, or a specific model to pin)
402
- # agent_overrides:
403
- # security-architect: opus # Always use opus for security
404
- # researcher: sonnet # Pin research to sonnet
405
- `;
406
- fs.writeFileSync(configPath, yaml, 'utf-8');
407
- return { name: 'moflo.yaml', status: 'created', detail: `Detected: ${srcDirs.join(', ')} | ${detectedExts.join(', ')}` };
408
- }
409
- function scanExtensions(dir, extensions, depth, maxDepth) {
410
- if (depth > maxDepth)
411
- return;
412
- const entries = fs.readdirSync(dir, { withFileTypes: true });
413
- for (const entry of entries.slice(0, 100)) {
414
- if (entry.isDirectory() && !['node_modules', '.git', 'dist', 'build'].includes(entry.name)) {
415
- scanExtensions(path.join(dir, entry.name), extensions, depth + 1, maxDepth);
416
- }
417
- else if (entry.isFile()) {
418
- const ext = path.extname(entry.name);
419
- if (['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs', '.java', '.kt', '.swift', '.rb', '.cs'].includes(ext)) {
420
- extensions.add(ext);
421
- }
422
- }
423
- }
158
+ const config = {
159
+ projectName: path.basename(root),
160
+ guidanceDirs: answers?.guidanceDirs ?? ['.claude/guidance'],
161
+ srcDirs,
162
+ testDirs: answers?.testDirs ?? ['tests'],
163
+ detectedExts: detectExtensions(root, srcDirs),
164
+ guidance: answers?.guidance ?? true,
165
+ codeMap: answers?.codeMap ?? true,
166
+ tests: answers?.tests ?? true,
167
+ gates: answers?.gates ?? true,
168
+ stopHook: answers?.stopHook ?? true,
169
+ };
170
+ fs.writeFileSync(configPath, renderMofloYaml(config), 'utf-8');
171
+ return {
172
+ name: 'moflo.yaml',
173
+ status: 'created',
174
+ detail: `Detected: ${config.srcDirs.join(', ')} | ${config.detectedExts.join(', ')}`,
175
+ };
424
176
  }
425
177
  // ============================================================================
426
178
  // Step 2: .claude/settings.json hooks