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.
- package/.claude/helpers/gate.cjs +21 -5
- package/.claude/skills/eldar/SKILL.md +305 -0
- package/.claude/skills/fl/phases.md +18 -2
- package/.claude/skills/simplify/SKILL.md +35 -48
- package/README.md +25 -0
- package/bin/gate.cjs +21 -5
- package/bin/hooks.mjs +2 -2
- package/bin/index-guidance.mjs +14 -24
- package/bin/index-patterns.mjs +13 -10
- package/bin/session-start-launcher.mjs +64 -10
- package/bin/simplify-classify.cjs +211 -0
- package/dist/src/cli/commands/doctor-checks-config.js +246 -0
- package/dist/src/cli/commands/doctor-checks-deep.js +14 -0
- package/dist/src/cli/commands/doctor-checks-intelligence.js +197 -0
- package/dist/src/cli/commands/doctor-checks-memory.js +207 -0
- package/dist/src/cli/commands/doctor-checks-platform.js +138 -0
- package/dist/src/cli/commands/doctor-checks-runtime.js +170 -0
- package/dist/src/cli/commands/doctor-fixes.js +165 -0
- package/dist/src/cli/commands/doctor-registry.js +109 -0
- package/dist/src/cli/commands/doctor-render.js +203 -0
- package/dist/src/cli/commands/doctor-types.js +9 -0
- package/dist/src/cli/commands/doctor-version.js +134 -0
- package/dist/src/cli/commands/doctor-zombies.js +201 -0
- package/dist/src/cli/commands/doctor.js +35 -1657
- package/dist/src/cli/init/helpers-generator.js +21 -5
- package/dist/src/cli/init/moflo-init.js +20 -268
- package/dist/src/cli/init/moflo-yaml-template.js +370 -0
- package/dist/src/cli/mcp-tools/hooks-tools.js +3 -1
- package/dist/src/cli/movector/model-router.js +66 -20
- package/dist/src/cli/services/hook-block-hash.js +23 -2
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
- 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
|
|
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
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
:
|
|
298
|
-
|
|
299
|
-
|
|
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
|