moflo 4.8.2 → 4.8.4
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 +113 -25
- package/README.md +10 -2
- package/bin/hooks.mjs +58 -11
- package/bin/index-guidance.mjs +27 -5
- package/package.json +2 -2
- package/src/@claude-flow/cli/dist/src/commands/daemon.js +25 -15
- package/src/@claude-flow/cli/dist/src/commands/doctor.js +49 -27
- package/src/@claude-flow/cli/dist/src/services/daemon-lock.d.ts +7 -0
- package/src/@claude-flow/cli/dist/src/services/daemon-lock.js +26 -0
- package/src/@claude-flow/cli/package.json +1 -1
- package/.claude/workflow-state.json +0 -5
package/.claude/helpers/gate.cjs
CHANGED
|
@@ -39,21 +39,119 @@ function loadGateConfig() {
|
|
|
39
39
|
var config = loadGateConfig();
|
|
40
40
|
var command = process.argv[2];
|
|
41
41
|
|
|
42
|
-
var EXEMPT = ['.claude/', '.claude\\', 'CLAUDE.md', 'MEMORY.md', 'workflow-state', 'node_modules'];
|
|
43
42
|
var DANGEROUS = ['rm -rf /', 'format c:', 'del /s /q c:\\', ':(){:|:&};:', 'mkfs.', '> /dev/sda'];
|
|
44
43
|
var DIRECTIVE_RE = /^(yes|no|yeah|yep|nope|sure|ok|okay|correct|right|exactly|perfect)\b/i;
|
|
45
44
|
var TASK_RE = /\b(fix|bug|error|implement|add|create|build|write|refactor|debug|test|feature|issue|security|optimi)\b/i;
|
|
46
45
|
|
|
46
|
+
// Deny a tool call cleanly via structured JSON (no "hook error" noise).
|
|
47
|
+
// Exit 0 + permissionDecision:"deny" is the Claude Code way to block a tool.
|
|
48
|
+
function blockTool(reason) {
|
|
49
|
+
console.log(JSON.stringify({
|
|
50
|
+
hookSpecificOutput: {
|
|
51
|
+
hookEventName: 'PreToolUse',
|
|
52
|
+
permissionDecision: 'deny',
|
|
53
|
+
permissionDecisionReason: reason
|
|
54
|
+
}
|
|
55
|
+
}));
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Determine if a Grep/Glob target is a mechanical/administrative search
|
|
60
|
+
// that should bypass the memory-first gate. The idea: if memory/guidance
|
|
61
|
+
// wouldn't improve the search outcome, don't block it.
|
|
62
|
+
//
|
|
63
|
+
// Strategy: path is the strongest signal. When a path clearly points to
|
|
64
|
+
// tooling/deps/tests, allow it. When it points to source/docs/scripts,
|
|
65
|
+
// block it (require memory). Pattern-based rules only kick in when there's
|
|
66
|
+
// no path or when the path is neutral.
|
|
67
|
+
function isMechanicalSearch() {
|
|
68
|
+
var searchPath = (process.env.TOOL_INPUT_path || '').replace(/\\/g, '/').toLowerCase();
|
|
69
|
+
var pattern = (process.env.TOOL_INPUT_pattern || '').toLowerCase();
|
|
70
|
+
var filePath = (process.env.TOOL_INPUT_file_path || '').replace(/\\/g, '/').toLowerCase();
|
|
71
|
+
var anyPath = searchPath || filePath;
|
|
72
|
+
|
|
73
|
+
// --- PATH-BASED RULES (strongest signal, checked first) ---
|
|
74
|
+
|
|
75
|
+
if (anyPath) {
|
|
76
|
+
// Always mechanical: dependencies, tooling internals, CI, test dirs
|
|
77
|
+
var mechanicalPaths = [
|
|
78
|
+
'node_modules/', '.claude/', '.claude-flow/', '.swarm/', '.github/',
|
|
79
|
+
'tests/', 'test/', 'config/', 'examples/',
|
|
80
|
+
];
|
|
81
|
+
for (var i = 0; i < mechanicalPaths.length; i++) {
|
|
82
|
+
if (anyPath.indexOf(mechanicalPaths[i]) >= 0) return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Targeting a specific config/meta file by path extension
|
|
86
|
+
if (/\.(json|yaml|yml|toml|lock|env|cjs|mjs)$/i.test(anyPath)) return true;
|
|
87
|
+
|
|
88
|
+
// If path points to source, docs, or scripts — these are knowledge-rich.
|
|
89
|
+
// Do NOT fall through to pattern-based exemptions; the path is authoritative.
|
|
90
|
+
// (Still allow test-file glob patterns even within source dirs.)
|
|
91
|
+
var knowledgePaths = [
|
|
92
|
+
'src/', 'back-office/', 'front-office/', 'docs/', 'scripts/', 'lib/',
|
|
93
|
+
];
|
|
94
|
+
var inKnowledgePath = false;
|
|
95
|
+
for (var k = 0; k < knowledgePaths.length; k++) {
|
|
96
|
+
if (anyPath.indexOf(knowledgePaths[k]) >= 0) { inKnowledgePath = true; break; }
|
|
97
|
+
}
|
|
98
|
+
if (inKnowledgePath) {
|
|
99
|
+
// Exception: searching for test/spec files within source is structural
|
|
100
|
+
if (/\*\*?[/\\]?\*?\.(test|spec)\.(ts|js|tsx|jsx)\b/i.test(pattern)) return true;
|
|
101
|
+
// Everything else in a knowledge path requires memory
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- PATTERN-BASED RULES (no path, or path is neutral) ---
|
|
107
|
+
|
|
108
|
+
// Glob patterns looking for config/build/tooling files by extension
|
|
109
|
+
if (/\*\*?[/\\]?\*?\.(json|yaml|yml|toml|lock|env|config|cjs|mjs)\b/i.test(pattern)) return true;
|
|
110
|
+
|
|
111
|
+
// Glob patterns for specific config filenames (eslintrc, Dockerfile, etc.)
|
|
112
|
+
if (/\*\*?[/\\]?\*?\.?(eslint|prettier|babel|stylelint|editor|git|docker|nginx|jest|vitest|vite|webpack|rollup|esbuild|tsconfig|browserslist)/i.test(pattern)) return true;
|
|
113
|
+
|
|
114
|
+
// Glob patterns for lock files and test files (structural lookups)
|
|
115
|
+
if (/\*\*?[/\\]?\*?[\w-]*[-.]lock\b/i.test(pattern)) return true;
|
|
116
|
+
if (/\*\*?[/\\]?\*?\.(test|spec)\.(ts|js|tsx|jsx)\b/i.test(pattern)) return true;
|
|
117
|
+
|
|
118
|
+
// Config/tooling name searches (bare names without a path).
|
|
119
|
+
// Only exempt if ALL tokens in a pipe-separated pattern are config names.
|
|
120
|
+
// "webpack|vite" = exempt. "webpack|merchant" = NOT exempt.
|
|
121
|
+
var CONFIG_NAME = /^\.?(eslint|prettier|babel|stylelint|editor|gitignore|gitattributes|dockerignore|dockerfile|docker-compose|nginx|jest|vitest|vite|webpack|rollup|esbuild|tsconfig|changelog|license|makefile|procfile|browserslist|commitlint|husky|lint-staged)\b/i;
|
|
122
|
+
var tokens = pattern.split(/[|,\s]+/).filter(function(t) { return t.length > 0; });
|
|
123
|
+
if (tokens.length > 0 && tokens.every(function(t) { return CONFIG_NAME.test(t.trim()); })) return true;
|
|
124
|
+
|
|
125
|
+
// Known tooling/meta file names as substrings (but avoid false matches like "process.env")
|
|
126
|
+
var toolingNames = [
|
|
127
|
+
'claude.md', 'memory.md', 'workflow-state', '.mcp.json',
|
|
128
|
+
'package.json', 'package-lock', 'daemon.lock', 'moflo.yaml',
|
|
129
|
+
];
|
|
130
|
+
var target = pattern + ' ' + anyPath;
|
|
131
|
+
for (var j = 0; j < toolingNames.length; j++) {
|
|
132
|
+
if (target.indexOf(toolingNames[j]) >= 0) return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Env file lookups (but NOT "process.env" which is source code searching)
|
|
136
|
+
if (/^\.env\b/.test(pattern) || /\*\*?[/\\]?\.env/.test(pattern)) return true;
|
|
137
|
+
|
|
138
|
+
// Git/process/system-level pattern searches
|
|
139
|
+
if (/^(git\b|pid|daemon|lock|wmic|tasklist|powershell|ps\s)/i.test(pattern)) return true;
|
|
140
|
+
|
|
141
|
+
// CI/CD folder exploration
|
|
142
|
+
if (/\.github/i.test(pattern)) return true;
|
|
143
|
+
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
47
147
|
switch (command) {
|
|
48
148
|
case 'check-before-agent': {
|
|
49
149
|
var s = readState();
|
|
50
150
|
if (config.task_create_first && !s.tasksCreated) {
|
|
51
|
-
|
|
52
|
-
process.exit(1);
|
|
151
|
+
blockTool('Call TaskCreate before spawning agents. Task tool is blocked until then.');
|
|
53
152
|
}
|
|
54
153
|
if (config.memory_first && !s.memorySearched) {
|
|
55
|
-
|
|
56
|
-
process.exit(1);
|
|
154
|
+
blockTool('Search memory before spawning agents. Use mcp__claude-flow__memory_search first.');
|
|
57
155
|
}
|
|
58
156
|
break;
|
|
59
157
|
}
|
|
@@ -61,31 +159,21 @@ switch (command) {
|
|
|
61
159
|
if (!config.memory_first) break;
|
|
62
160
|
var s = readState();
|
|
63
161
|
if (s.memorySearched || !s.memoryRequired) break;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (now - last > 2000) {
|
|
69
|
-
s.lastBlockedAt = new Date(now).toISOString();
|
|
70
|
-
writeState(s);
|
|
71
|
-
console.log('BLOCKED: Search memory before exploring files.');
|
|
72
|
-
}
|
|
73
|
-
process.exit(1);
|
|
162
|
+
if (isMechanicalSearch()) break;
|
|
163
|
+
s.lastBlockedAt = new Date().toISOString();
|
|
164
|
+
writeState(s);
|
|
165
|
+
blockTool('Search memory before exploring files. Use mcp__claude-flow__memory_search with namespace "code-map", "patterns", "knowledge", or "guidance".');
|
|
74
166
|
}
|
|
75
167
|
case 'check-before-read': {
|
|
76
168
|
if (!config.memory_first) break;
|
|
77
169
|
var s = readState();
|
|
78
170
|
if (s.memorySearched || !s.memoryRequired) break;
|
|
79
|
-
var fp = process.env.TOOL_INPUT_file_path || '';
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
writeState(s);
|
|
86
|
-
console.log('BLOCKED: Search memory before reading guidance files.');
|
|
87
|
-
}
|
|
88
|
-
process.exit(1);
|
|
171
|
+
var fp = (process.env.TOOL_INPUT_file_path || '').replace(/\\/g, '/');
|
|
172
|
+
// Block reads of guidance files (that's exactly what memory indexes)
|
|
173
|
+
if (fp.indexOf('.claude/guidance/') < 0) break;
|
|
174
|
+
s.lastBlockedAt = new Date().toISOString();
|
|
175
|
+
writeState(s);
|
|
176
|
+
blockTool('Search memory before reading guidance files. Use mcp__claude-flow__memory_search with namespace "guidance".');
|
|
89
177
|
}
|
|
90
178
|
case 'record-task-created': {
|
|
91
179
|
var s = readState();
|
package/README.md
CHANGED
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
# MoFlo
|
|
6
6
|
|
|
7
|
-
**⚠️ MoFlo is experimental software. APIs, commands, and behavior may change without notice.**
|
|
8
|
-
|
|
9
7
|
**An opinionated fork of [Ruflo/Claude Flow](https://github.com/ruvnet/ruflo), optimized for local development.**
|
|
10
8
|
|
|
11
9
|
MoFlo adds automatic code and guidance cataloging along with memory gating on top of the original Ruflo/Claude Flow orchestration engine. Where the upstream project provides raw building blocks, MoFlo ships opinionated defaults — workflow gates that enforce memory-first patterns, semantic indexing that runs at session start, and learned routing that improves over time — so you get a productive setup from `flo init` without manual tuning.
|
|
@@ -477,6 +475,16 @@ When `flo init` runs, it appends a workflow section to your CLAUDE.md that teach
|
|
|
477
475
|
|
|
478
476
|
MoFlo builds on top of the full [Ruflo/Claude Flow](https://github.com/ruvnet/ruflo) engine. For detailed documentation on the underlying capabilities — swarm topologies, hive-mind consensus, HNSW vector search, neural routing, MCP server internals, and more — check out the [Ruflo repository](https://github.com/ruvnet/ruflo).
|
|
479
477
|
|
|
478
|
+
## Why I Made This
|
|
479
|
+
|
|
480
|
+
[Ruflo/Claude Flow](https://github.com/ruvnet/ruflo) is an incredible piece of work. The engineering that [rUv](https://github.com/ruvnet) and the contributors have put into it — swarm topologies, hive-mind consensus, HNSW vector search, neural routing, and so much more — makes it one of the most comprehensive agent orchestration frameworks available. It's a massive, versatile toolbox built to support a wide range of scenarios: distributed systems, multi-agent swarms, enterprise orchestration, research workflows, and beyond.
|
|
481
|
+
|
|
482
|
+
My use case was just one of those many scenarios: day-to-day local coding, enhancing my normal Claude Code experience on a single project. Claude Flow absolutely supports this — it's all in there — but because the project serves so many different needs, I found myself spending time configuring and tailoring things for my specific workflow each time I pulled in updates. That's not a shortcoming of the project; it's the natural trade-off of a tool designed to be that flexible and powerful.
|
|
483
|
+
|
|
484
|
+
So I forked the excellent foundation and narrowed the focus to my particular corner of it. I baked in the defaults I kept setting manually, added automatic indexing and memory gating at session start, and tuned the out-of-box experience so that `npm install` and `flo init` gets you straight to coding.
|
|
485
|
+
|
|
486
|
+
If you're exploring the full breadth of what agent orchestration can do, go use [Ruflo/Claude Flow](https://github.com/ruvnet/ruflo) directly — it's the real deal. But if your needs are similar to mine — a focused, opinionated local dev setup that just works — then hopefully MoFlo saves you the same configuration time it saves me.
|
|
487
|
+
|
|
480
488
|
## License
|
|
481
489
|
|
|
482
490
|
MIT (inherited from [Ruflo/Claude Flow](https://github.com/ruvnet/ruflo))
|
package/bin/hooks.mjs
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { spawn } from 'child_process';
|
|
23
|
-
import { existsSync, appendFileSync, readFileSync } from 'fs';
|
|
23
|
+
import { existsSync, appendFileSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'fs';
|
|
24
24
|
import { resolve, dirname } from 'path';
|
|
25
25
|
import { fileURLToPath } from 'url';
|
|
26
26
|
|
|
@@ -306,10 +306,13 @@ async function main() {
|
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
case 'daemon-start': {
|
|
309
|
-
if (
|
|
310
|
-
await runClaudeFlow('daemon', ['start', '--quiet']);
|
|
311
|
-
} else {
|
|
309
|
+
if (isDaemonLockHeld()) {
|
|
312
310
|
log('info', 'Daemon already running (lock held), skipping start');
|
|
311
|
+
} else if (isDaemonSpawnRecent()) {
|
|
312
|
+
log('info', 'Daemon spawn debounced (recent attempt), skipping');
|
|
313
|
+
} else {
|
|
314
|
+
touchSpawnStamp();
|
|
315
|
+
await runClaudeFlow('daemon', ['start', '--quiet']);
|
|
313
316
|
}
|
|
314
317
|
break;
|
|
315
318
|
}
|
|
@@ -479,17 +482,30 @@ function runBackgroundTraining() {
|
|
|
479
482
|
spawnWindowless('node', [localCli, 'neural', 'optimize'], 'neural optimize');
|
|
480
483
|
}
|
|
481
484
|
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
|
|
485
|
+
// Delegate to daemon-lock.js for proper PID + command-line verification.
|
|
486
|
+
// Falls back to a naive kill(0) check if the import fails (e.g. dist not built).
|
|
487
|
+
let _getDaemonLockHolder = null;
|
|
488
|
+
try {
|
|
489
|
+
const daemonLockPath = resolve(__dirname, '..', 'src', '@claude-flow', 'cli', 'dist', 'src', 'services', 'daemon-lock.js');
|
|
490
|
+
if (existsSync(daemonLockPath)) {
|
|
491
|
+
const mod = await import('file://' + daemonLockPath.replace(/\\/g, '/'));
|
|
492
|
+
_getDaemonLockHolder = mod.getDaemonLockHolder;
|
|
493
|
+
}
|
|
494
|
+
} catch { /* fallback below */ }
|
|
495
|
+
|
|
485
496
|
function isDaemonLockHeld() {
|
|
497
|
+
// Prefer the real daemon-lock module (PID + command-line verification)
|
|
498
|
+
if (_getDaemonLockHolder) {
|
|
499
|
+
return _getDaemonLockHolder(projectRoot) !== null;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Fallback: naive PID check (only if daemon-lock.js unavailable)
|
|
486
503
|
const lockFile = resolve(projectRoot, '.claude-flow', 'daemon.lock');
|
|
487
504
|
if (!existsSync(lockFile)) return false;
|
|
488
|
-
|
|
489
505
|
try {
|
|
490
506
|
const data = JSON.parse(readFileSync(lockFile, 'utf-8'));
|
|
491
507
|
if (typeof data.pid === 'number' && data.pid > 0) {
|
|
492
|
-
process.kill(data.pid, 0);
|
|
508
|
+
process.kill(data.pid, 0);
|
|
493
509
|
return true;
|
|
494
510
|
}
|
|
495
511
|
} catch {
|
|
@@ -498,21 +514,52 @@ function isDaemonLockHeld() {
|
|
|
498
514
|
return false;
|
|
499
515
|
}
|
|
500
516
|
|
|
517
|
+
// Debounce file — prevents thundering-herd spawns when multiple hooks fire
|
|
518
|
+
// within the same second (e.g. subagents each triggering SessionStart).
|
|
519
|
+
const SPAWN_DEBOUNCE_MS = 30_000;
|
|
520
|
+
const SPAWN_STAMP_FILE = resolve(projectRoot, '.claude-flow', 'daemon-spawn.stamp');
|
|
521
|
+
|
|
522
|
+
function isDaemonSpawnRecent() {
|
|
523
|
+
try {
|
|
524
|
+
if (existsSync(SPAWN_STAMP_FILE)) {
|
|
525
|
+
const age = Date.now() - statSync(SPAWN_STAMP_FILE).mtimeMs;
|
|
526
|
+
return age < SPAWN_DEBOUNCE_MS;
|
|
527
|
+
}
|
|
528
|
+
} catch { /* non-fatal */ }
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function touchSpawnStamp() {
|
|
533
|
+
try {
|
|
534
|
+
const dir = resolve(projectRoot, '.claude-flow');
|
|
535
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
536
|
+
writeFileSync(SPAWN_STAMP_FILE, String(Date.now()));
|
|
537
|
+
} catch { /* non-fatal */ }
|
|
538
|
+
}
|
|
539
|
+
|
|
501
540
|
// Run daemon start in background (non-blocking) — skip if already running
|
|
502
541
|
function runDaemonStartBackground() {
|
|
503
|
-
//
|
|
504
|
-
// This avoids zombie Node processes from subagents that all fire SessionStart.
|
|
542
|
+
// 1. Check if a live daemon already holds the lock
|
|
505
543
|
if (isDaemonLockHeld()) {
|
|
506
544
|
log('info', 'Daemon already running (lock held), skipping start');
|
|
507
545
|
return;
|
|
508
546
|
}
|
|
509
547
|
|
|
548
|
+
// 2. Debounce: skip if we spawned recently (prevents thundering herd)
|
|
549
|
+
if (isDaemonSpawnRecent()) {
|
|
550
|
+
log('info', 'Daemon spawn debounced (recent attempt), skipping');
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
510
554
|
const localCli = getLocalCliPath();
|
|
511
555
|
if (!localCli) {
|
|
512
556
|
log('warn', 'Local CLI not found, skipping daemon start');
|
|
513
557
|
return;
|
|
514
558
|
}
|
|
515
559
|
|
|
560
|
+
// 3. Write stamp BEFORE spawning so concurrent callers see it immediately
|
|
561
|
+
touchSpawnStamp();
|
|
562
|
+
|
|
516
563
|
spawnWindowless('node', [localCli, 'daemon', 'start', '--quiet'], 'daemon');
|
|
517
564
|
}
|
|
518
565
|
|
package/bin/index-guidance.mjs
CHANGED
|
@@ -635,6 +635,29 @@ function indexFile(db, filePath, keyPrefix) {
|
|
|
635
635
|
}
|
|
636
636
|
}
|
|
637
637
|
|
|
638
|
+
/**
|
|
639
|
+
* Recursively collect all .md files under a directory.
|
|
640
|
+
* Skips node_modules, .git, and other non-content directories.
|
|
641
|
+
*/
|
|
642
|
+
function walkMdFiles(dir) {
|
|
643
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.next']);
|
|
644
|
+
const files = [];
|
|
645
|
+
|
|
646
|
+
function walk(current) {
|
|
647
|
+
if (!existsSync(current)) return;
|
|
648
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
649
|
+
if (entry.isDirectory()) {
|
|
650
|
+
if (!SKIP_DIRS.has(entry.name)) walk(resolve(current, entry.name));
|
|
651
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
652
|
+
files.push(resolve(current, entry.name));
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
walk(dir);
|
|
658
|
+
return files;
|
|
659
|
+
}
|
|
660
|
+
|
|
638
661
|
function indexDirectory(db, dirConfig) {
|
|
639
662
|
const dirPath = dirConfig.absolute ? dirConfig.path : resolve(projectRoot, dirConfig.path);
|
|
640
663
|
const results = [];
|
|
@@ -644,13 +667,12 @@ function indexDirectory(db, dirConfig) {
|
|
|
644
667
|
return results;
|
|
645
668
|
}
|
|
646
669
|
|
|
647
|
-
const allMdFiles =
|
|
648
|
-
const
|
|
649
|
-
? allMdFiles.filter(f => dirConfig.fileFilter.includes(f))
|
|
670
|
+
const allMdFiles = walkMdFiles(dirPath);
|
|
671
|
+
const filtered = dirConfig.fileFilter
|
|
672
|
+
? allMdFiles.filter(f => dirConfig.fileFilter.includes(basename(f)))
|
|
650
673
|
: allMdFiles;
|
|
651
674
|
|
|
652
|
-
for (const
|
|
653
|
-
const filePath = resolve(dirPath, file);
|
|
675
|
+
for (const filePath of filtered) {
|
|
654
676
|
const result = indexFile(db, filePath, dirConfig.prefix);
|
|
655
677
|
results.push(result);
|
|
656
678
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.4",
|
|
4
4
|
"description": "MoFlo — AI agent orchestration for Claude Code. Forked from ruflo/claude-flow with patches applied to source, plus feature-level orchestration.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"@types/bcrypt": "^5.0.2",
|
|
84
84
|
"@types/node": "^20.19.37",
|
|
85
85
|
"eslint": "^8.0.0",
|
|
86
|
-
"moflo": "^4.8.
|
|
86
|
+
"moflo": "^4.8.2",
|
|
87
87
|
"tsx": "^4.21.0",
|
|
88
88
|
"typescript": "^5.9.3",
|
|
89
89
|
"vitest": "^4.0.0"
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { output } from '../output.js';
|
|
6
6
|
import { getDaemon, startDaemon, stopDaemon } from '../services/worker-daemon.js';
|
|
7
|
-
import { acquireDaemonLock, releaseDaemonLock, getDaemonLockHolder } from '../services/daemon-lock.js';
|
|
7
|
+
import { acquireDaemonLock, releaseDaemonLock, getDaemonLockHolder, transferDaemonLock } from '../services/daemon-lock.js';
|
|
8
8
|
import { spawn } from 'child_process';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
10
|
import { dirname, join, resolve } from 'path';
|
|
@@ -71,16 +71,16 @@ const startCommand = {
|
|
|
71
71
|
}
|
|
72
72
|
// Foreground mode: run in current process (blocks terminal)
|
|
73
73
|
try {
|
|
74
|
-
// Acquire atomic daemon lock (prevents duplicate daemons)
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
return { success: true };
|
|
74
|
+
// Acquire atomic daemon lock (prevents duplicate daemons).
|
|
75
|
+
// Always acquire here — even when spawned as a child (CLAUDE_FLOW_DAEMON=1)
|
|
76
|
+
// because on Windows the parent's child.pid is the shell PID (cmd.exe),
|
|
77
|
+
// not the actual node process. The child must write its own real PID.
|
|
78
|
+
const lockResult = acquireDaemonLock(projectRoot);
|
|
79
|
+
if (!lockResult.acquired) {
|
|
80
|
+
if (!quiet) {
|
|
81
|
+
output.printWarning(`Daemon already running (PID: ${lockResult.holder})`);
|
|
83
82
|
}
|
|
83
|
+
return { success: true };
|
|
84
84
|
}
|
|
85
85
|
// Clean up lock file on exit
|
|
86
86
|
const cleanup = () => {
|
|
@@ -256,11 +256,21 @@ async function startBackgroundDaemon(projectRoot, quiet, maxCpuLoad, minFreeMemo
|
|
|
256
256
|
child.unref();
|
|
257
257
|
// Small delay to let the child process fully detach on macOS
|
|
258
258
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
259
|
+
// On Windows with shell: true, child.pid is the cmd.exe shell PID, not the
|
|
260
|
+
// actual node daemon. The child will acquire the lock itself with its real PID
|
|
261
|
+
// (see foreground start path). Release the parent's lock so the child can take it.
|
|
262
|
+
//
|
|
263
|
+
// On POSIX (no shell), child.pid IS the real daemon PID, so we can transfer
|
|
264
|
+
// atomically to avoid any gap where the lock is absent.
|
|
265
|
+
if (isWin) {
|
|
266
|
+
releaseDaemonLock(resolvedRoot, process.pid, true);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
if (!transferDaemonLock(resolvedRoot, pid)) {
|
|
270
|
+
releaseDaemonLock(resolvedRoot, process.pid, true);
|
|
271
|
+
acquireDaemonLock(resolvedRoot, pid);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
264
274
|
if (!quiet) {
|
|
265
275
|
output.printSuccess(`Daemon started in background (PID: ${pid})`);
|
|
266
276
|
output.printInfo(`Logs: ${logFile}`);
|
|
@@ -91,20 +91,20 @@ async function checkConfigFile() {
|
|
|
91
91
|
}
|
|
92
92
|
return { name: 'Config File', status: 'warn', message: 'No config file (using defaults)', fix: 'claude-flow config init' };
|
|
93
93
|
}
|
|
94
|
-
// Check daemon status
|
|
94
|
+
// Check daemon status — delegates to daemon-lock module for proper
|
|
95
|
+
// PID + command-line verification (avoids Windows PID-recycling false positives).
|
|
95
96
|
async function checkDaemonStatus() {
|
|
96
97
|
try {
|
|
98
|
+
const holderPid = getDaemonLockHolder(process.cwd());
|
|
99
|
+
if (holderPid) {
|
|
100
|
+
return { name: 'Daemon Status', status: 'pass', message: `Running (PID: ${holderPid})` };
|
|
101
|
+
}
|
|
102
|
+
// getDaemonLockHolder auto-cleans stale locks, but check for legacy PID file
|
|
97
103
|
const lockFile = '.claude-flow/daemon.lock';
|
|
98
104
|
if (existsSync(lockFile)) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
process.kill(pid, 0); // Check if process exists
|
|
103
|
-
return { name: 'Daemon Status', status: 'pass', message: `Running (PID: ${pid})` };
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
return { name: 'Daemon Status', status: 'warn', message: 'Stale lock file', fix: 'rm .claude-flow/daemon.lock && claude-flow daemon start' };
|
|
107
|
-
}
|
|
105
|
+
// Lock exists but holder is null — getDaemonLockHolder already cleaned it,
|
|
106
|
+
// but if it persists it means cleanup failed (permissions, etc.)
|
|
107
|
+
return { name: 'Daemon Status', status: 'warn', message: 'Stale lock file', fix: 'rm .claude-flow/daemon.lock && claude-flow daemon start' };
|
|
108
108
|
}
|
|
109
109
|
// Also check legacy PID file
|
|
110
110
|
const pidFile = '.claude-flow/daemon.pid';
|
|
@@ -407,47 +407,69 @@ async function checkAgenticFlow() {
|
|
|
407
407
|
return { name: 'agentic-flow', status: 'warn', message: 'Check failed' };
|
|
408
408
|
}
|
|
409
409
|
}
|
|
410
|
-
//
|
|
410
|
+
// Check whether a given PID is still running.
|
|
411
|
+
// Uses signal 0 which works cross-platform (Windows, Linux, macOS) without
|
|
412
|
+
// needing PowerShell or /proc — Node handles the platform abstraction.
|
|
413
|
+
function isProcessAlive(pid) {
|
|
414
|
+
try {
|
|
415
|
+
process.kill(pid, 0);
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// Find and optionally kill orphaned moflo/claude-flow node processes.
|
|
423
|
+
// A process is only "orphaned" if its parent is no longer alive — meaning
|
|
424
|
+
// nothing will clean it up. MCP servers spawned by a live Claude Code session
|
|
425
|
+
// have a live parent (claude.exe) and must not be flagged.
|
|
411
426
|
async function findZombieProcesses(kill = false) {
|
|
412
427
|
const legitimatePid = getDaemonLockHolder(process.cwd());
|
|
413
428
|
const currentPid = process.pid;
|
|
414
429
|
const parentPid = process.ppid;
|
|
415
430
|
const found = [];
|
|
416
431
|
let killed = 0;
|
|
432
|
+
// Collect candidates as { pid, ppid } so we can check parent liveness
|
|
433
|
+
const candidates = [];
|
|
417
434
|
try {
|
|
418
435
|
if (process.platform === 'win32') {
|
|
419
|
-
// Windows:
|
|
420
|
-
const result = execSync('powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"Name=\'node.exe\'\\" | Select-Object ProcessId,CommandLine | Format-Table -AutoSize -Wrap"', { encoding: 'utf-8', timeout: 10000, windowsHide: true });
|
|
436
|
+
// Windows: include ParentProcessId so we can verify orphan status
|
|
437
|
+
const result = execSync('powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"Name=\'node.exe\'\\" | Select-Object ProcessId,ParentProcessId,CommandLine | Format-Table -AutoSize -Wrap"', { encoding: 'utf-8', timeout: 10000, windowsHide: true });
|
|
421
438
|
const lines = result.split('\n');
|
|
422
439
|
for (const line of lines) {
|
|
423
440
|
if (/moflo|claude-flow|flo\s+(hooks|gate|mcp|daemon)/i.test(line)) {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if (pid === currentPid || pid === parentPid || pid === legitimatePid)
|
|
429
|
-
continue;
|
|
430
|
-
found.push(pid);
|
|
441
|
+
// Format-Table columns: ProcessId ParentProcessId CommandLine...
|
|
442
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)/);
|
|
443
|
+
if (match) {
|
|
444
|
+
candidates.push({ pid: parseInt(match[1], 10), ppid: parseInt(match[2], 10) });
|
|
431
445
|
}
|
|
432
446
|
}
|
|
433
447
|
}
|
|
434
448
|
}
|
|
435
449
|
else {
|
|
436
|
-
// Unix/macOS: use ps
|
|
437
|
-
const result = execSync('ps
|
|
450
|
+
// Unix/macOS: use ps with explicit PID+PPID columns for reliable parsing
|
|
451
|
+
const result = execSync('ps -eo pid,ppid,command | grep -E "node.*(moflo|claude-flow)" | grep -v grep', { encoding: 'utf-8', timeout: 5000 });
|
|
438
452
|
const lines = result.trim().split('\n');
|
|
439
453
|
for (const line of lines) {
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
found.push(pid);
|
|
454
|
+
const match = line.trim().match(/^(\d+)\s+(\d+)/);
|
|
455
|
+
if (match) {
|
|
456
|
+
candidates.push({ pid: parseInt(match[1], 10), ppid: parseInt(match[2], 10) });
|
|
457
|
+
}
|
|
445
458
|
}
|
|
446
459
|
}
|
|
447
460
|
}
|
|
448
461
|
catch {
|
|
449
462
|
// No matches found (grep exits non-zero) or command failed
|
|
450
463
|
}
|
|
464
|
+
// Filter: skip known-good PIDs and processes whose parent is still alive.
|
|
465
|
+
// A live parent (e.g. claude.exe for MCP servers) means the process is managed, not orphaned.
|
|
466
|
+
for (const { pid, ppid } of candidates) {
|
|
467
|
+
if (pid === currentPid || pid === parentPid || pid === legitimatePid)
|
|
468
|
+
continue;
|
|
469
|
+
if (isProcessAlive(ppid))
|
|
470
|
+
continue;
|
|
471
|
+
found.push(pid);
|
|
472
|
+
}
|
|
451
473
|
if (kill && found.length > 0) {
|
|
452
474
|
for (const pid of found) {
|
|
453
475
|
try {
|
|
@@ -31,6 +31,13 @@ export declare function acquireDaemonLock(projectRoot: string, pid?: number): {
|
|
|
31
31
|
* Release the daemon lock. Only removes if we own it (or force = true).
|
|
32
32
|
*/
|
|
33
33
|
export declare function releaseDaemonLock(projectRoot: string, pid?: number, force?: boolean): void;
|
|
34
|
+
/**
|
|
35
|
+
* Atomically transfer the daemon lock to a new PID (e.g. parent → child).
|
|
36
|
+
*
|
|
37
|
+
* Overwrites the lock file in-place so there is no window where the lock
|
|
38
|
+
* is absent. Only succeeds if the lock is currently held by `fromPid`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function transferDaemonLock(projectRoot: string, newPid: number, fromPid?: number): boolean;
|
|
34
41
|
/**
|
|
35
42
|
* Check if the daemon lock is currently held by a live daemon.
|
|
36
43
|
* Returns the holder PID or null.
|
|
@@ -79,6 +79,32 @@ export function releaseDaemonLock(projectRoot, pid = process.pid, force = false)
|
|
|
79
79
|
safeUnlink(lock);
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Atomically transfer the daemon lock to a new PID (e.g. parent → child).
|
|
84
|
+
*
|
|
85
|
+
* Overwrites the lock file in-place so there is no window where the lock
|
|
86
|
+
* is absent. Only succeeds if the lock is currently held by `fromPid`.
|
|
87
|
+
*/
|
|
88
|
+
export function transferDaemonLock(projectRoot, newPid, fromPid = process.pid) {
|
|
89
|
+
const lock = lockPath(projectRoot);
|
|
90
|
+
const existing = readLockPayload(lock);
|
|
91
|
+
if (!existing || existing.pid !== fromPid) {
|
|
92
|
+
return false; // We don't own the lock — can't transfer
|
|
93
|
+
}
|
|
94
|
+
const payload = {
|
|
95
|
+
pid: newPid,
|
|
96
|
+
startedAt: Date.now(),
|
|
97
|
+
label: LOCK_LABEL,
|
|
98
|
+
};
|
|
99
|
+
try {
|
|
100
|
+
// Atomic overwrite — no unlink/recreate gap
|
|
101
|
+
fs.writeFileSync(lock, JSON.stringify(payload));
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
82
108
|
/**
|
|
83
109
|
* Check if the daemon lock is currently held by a live daemon.
|
|
84
110
|
* Returns the holder PID or null.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moflo/cli",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MoFlo CLI — AI agent orchestration with specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|