moflo 4.8.1 → 4.8.3
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/README.md +15 -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/diagnose.d.ts +16 -0
- package/src/@claude-flow/cli/dist/src/commands/diagnose.js +503 -0
- package/src/@claude-flow/cli/dist/src/commands/doctor.js +10 -10
- package/src/@claude-flow/cli/dist/src/commands/index.d.ts +1 -0
- package/src/@claude-flow/cli/dist/src/commands/index.js +7 -0
- package/src/@claude-flow/cli/dist/src/commands/memory.js +165 -2
- 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/README.md
CHANGED
|
@@ -124,7 +124,11 @@ npx flo memory code-map # Index your code structure
|
|
|
124
124
|
npx flo doctor # Verify everything works
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
Both indexes run automatically at session start after this, so you only need to run them manually on first setup or after major structural changes.
|
|
127
|
+
Both indexes run automatically at session start after this, so you only need to run them manually on first setup or after major structural changes. To reindex everything at once:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
npx flo memory refresh # Reindex all content, rebuild embeddings, cleanup, vacuum
|
|
131
|
+
```
|
|
128
132
|
|
|
129
133
|
## The `/flo` Skill
|
|
130
134
|
|
|
@@ -217,6 +221,7 @@ flo memory search -q "auth patterns" # Semantic search
|
|
|
217
221
|
flo memory index-guidance # Index guidance docs
|
|
218
222
|
flo memory code-map # Index code structure
|
|
219
223
|
flo memory rebuild-index # Regenerate all embeddings
|
|
224
|
+
flo memory refresh # Reindex all + rebuild + cleanup + vacuum
|
|
220
225
|
flo memory stats # Show statistics
|
|
221
226
|
```
|
|
222
227
|
|
|
@@ -238,11 +243,19 @@ flo gate prompt-reminder # Context bracket tracking
|
|
|
238
243
|
flo gate session-reset # Reset workflow state
|
|
239
244
|
```
|
|
240
245
|
|
|
246
|
+
### Diagnostics
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
flo doctor # Quick health check (environment, deps, config)
|
|
250
|
+
flo diagnose # Full integration test (memory, swarm, hive, hooks, neural)
|
|
251
|
+
flo diagnose --suite memory # Run only memory tests
|
|
252
|
+
flo diagnose --json # JSON output for CI/automation
|
|
253
|
+
```
|
|
254
|
+
|
|
241
255
|
### System
|
|
242
256
|
|
|
243
257
|
```bash
|
|
244
258
|
flo init # Initialize project (one-time setup)
|
|
245
|
-
flo doctor # Health check
|
|
246
259
|
flo --version # Show version
|
|
247
260
|
```
|
|
248
261
|
|
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.3",
|
|
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}`);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V3 CLI Diagnose Command
|
|
3
|
+
* Full integration test suite — runs non-destructively in the destination project.
|
|
4
|
+
*
|
|
5
|
+
* Unlike `doctor` (which checks environment health), `diagnose` exercises
|
|
6
|
+
* every subsystem end-to-end: memory CRUD, swarm lifecycle, hive-mind,
|
|
7
|
+
* task management, hooks, config, neural, and init idempotency.
|
|
8
|
+
*
|
|
9
|
+
* All test data is cleaned up after each test — no code or state is left behind.
|
|
10
|
+
*
|
|
11
|
+
* Created with motailz.com
|
|
12
|
+
*/
|
|
13
|
+
import type { Command } from '../types.js';
|
|
14
|
+
export declare const diagnoseCommand: Command;
|
|
15
|
+
export default diagnoseCommand;
|
|
16
|
+
//# sourceMappingURL=diagnose.d.ts.map
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V3 CLI Diagnose Command
|
|
3
|
+
* Full integration test suite — runs non-destructively in the destination project.
|
|
4
|
+
*
|
|
5
|
+
* Unlike `doctor` (which checks environment health), `diagnose` exercises
|
|
6
|
+
* every subsystem end-to-end: memory CRUD, swarm lifecycle, hive-mind,
|
|
7
|
+
* task management, hooks, config, neural, and init idempotency.
|
|
8
|
+
*
|
|
9
|
+
* All test data is cleaned up after each test — no code or state is left behind.
|
|
10
|
+
*
|
|
11
|
+
* Created with motailz.com
|
|
12
|
+
*/
|
|
13
|
+
import { output } from '../output.js';
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Helpers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
const DIAG_NAMESPACE = '__moflo_diagnose__';
|
|
18
|
+
const DIAG_KEY = '__diag_test_entry__';
|
|
19
|
+
async function timed(name, fn) {
|
|
20
|
+
const t0 = performance.now();
|
|
21
|
+
try {
|
|
22
|
+
const r = await fn();
|
|
23
|
+
return { name, ...r, duration: performance.now() - t0 };
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
return { name, status: 'fail', message: err instanceof Error ? err.message : String(err), duration: performance.now() - t0 };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Lazy-load memory functions to avoid pulling in WASM at import time.
|
|
31
|
+
*/
|
|
32
|
+
async function getMemFns() {
|
|
33
|
+
const { storeEntry, searchEntries, listEntries, getEntry, deleteEntry, initializeMemoryDatabase, checkMemoryInitialization, } = await import('../memory/memory-initializer.js');
|
|
34
|
+
return { storeEntry, searchEntries, listEntries, getEntry, deleteEntry, initializeMemoryDatabase, checkMemoryInitialization };
|
|
35
|
+
}
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Individual diagnostic tests
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
function testMemoryInit() {
|
|
40
|
+
return () => timed('Memory Init', async () => {
|
|
41
|
+
const { initializeMemoryDatabase, checkMemoryInitialization } = await getMemFns();
|
|
42
|
+
const status = await checkMemoryInitialization();
|
|
43
|
+
if (!status.initialized) {
|
|
44
|
+
await initializeMemoryDatabase({ force: false, verbose: false });
|
|
45
|
+
const recheck = await checkMemoryInitialization();
|
|
46
|
+
if (!recheck.initialized)
|
|
47
|
+
return { status: 'fail', message: 'Could not initialize memory database' };
|
|
48
|
+
}
|
|
49
|
+
return { status: 'pass', message: `Initialized (v${status.version || '3.0.0'})` };
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function testMemoryStore() {
|
|
53
|
+
return () => timed('Memory Store', async () => {
|
|
54
|
+
const { storeEntry, deleteEntry } = await getMemFns();
|
|
55
|
+
// Clean up any leftover from a previous run
|
|
56
|
+
try {
|
|
57
|
+
await deleteEntry({ key: DIAG_KEY, namespace: DIAG_NAMESPACE });
|
|
58
|
+
}
|
|
59
|
+
catch { /* ignore */ }
|
|
60
|
+
const result = await storeEntry({
|
|
61
|
+
key: DIAG_KEY,
|
|
62
|
+
value: 'diagnose test value — safe to delete',
|
|
63
|
+
namespace: DIAG_NAMESPACE,
|
|
64
|
+
generateEmbeddingFlag: true,
|
|
65
|
+
tags: ['diagnose'],
|
|
66
|
+
upsert: true,
|
|
67
|
+
});
|
|
68
|
+
if (!result.success)
|
|
69
|
+
return { status: 'fail', message: result.error || 'store failed' };
|
|
70
|
+
const dims = result.embedding?.dimensions;
|
|
71
|
+
return { status: 'pass', message: `Stored with ${dims ?? 0}-dim vector` };
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function testMemoryRetrieve() {
|
|
75
|
+
return () => timed('Memory Retrieve', async () => {
|
|
76
|
+
const { getEntry } = await getMemFns();
|
|
77
|
+
const result = await getEntry({ key: DIAG_KEY, namespace: DIAG_NAMESPACE });
|
|
78
|
+
if (!result.found)
|
|
79
|
+
return { status: 'fail', message: 'Entry not found after store' };
|
|
80
|
+
if (!result.entry?.content?.includes('diagnose test value'))
|
|
81
|
+
return { status: 'fail', message: 'Content mismatch' };
|
|
82
|
+
return { status: 'pass', message: 'Retrieved and verified' };
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function testMemorySearch() {
|
|
86
|
+
return () => timed('Memory Search', async () => {
|
|
87
|
+
const { searchEntries } = await getMemFns();
|
|
88
|
+
const result = await searchEntries({
|
|
89
|
+
query: 'diagnose test value safe delete',
|
|
90
|
+
namespace: DIAG_NAMESPACE,
|
|
91
|
+
limit: 5,
|
|
92
|
+
threshold: 0.1,
|
|
93
|
+
});
|
|
94
|
+
if (!result.results || result.results.length === 0)
|
|
95
|
+
return { status: 'fail', message: 'No search results returned' };
|
|
96
|
+
const top = result.results[0];
|
|
97
|
+
return { status: 'pass', message: `Top hit: ${top.key} (score ${top.score.toFixed(2)})` };
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function testMemoryList() {
|
|
101
|
+
return () => timed('Memory List', async () => {
|
|
102
|
+
const { listEntries } = await getMemFns();
|
|
103
|
+
const result = await listEntries({ namespace: DIAG_NAMESPACE, limit: 50 });
|
|
104
|
+
if (result.total === 0)
|
|
105
|
+
return { status: 'fail', message: 'No entries in diagnose namespace' };
|
|
106
|
+
return { status: 'pass', message: `${result.total} entries found` };
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function testMemoryDelete() {
|
|
110
|
+
return () => timed('Memory Delete', async () => {
|
|
111
|
+
const { deleteEntry, listEntries } = await getMemFns();
|
|
112
|
+
const result = await deleteEntry({ key: DIAG_KEY, namespace: DIAG_NAMESPACE });
|
|
113
|
+
if (!result.deleted)
|
|
114
|
+
return { status: 'fail', message: 'Delete returned false' };
|
|
115
|
+
// Verify it's gone
|
|
116
|
+
const list = await listEntries({ namespace: DIAG_NAMESPACE, limit: 50 });
|
|
117
|
+
if (list.total !== 0)
|
|
118
|
+
return { status: 'fail', message: `${list.total} entries still remain after delete` };
|
|
119
|
+
return { status: 'pass', message: 'Deleted and verified' };
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function testSwarmLifecycle() {
|
|
123
|
+
return () => timed('Swarm Lifecycle', async () => {
|
|
124
|
+
// Use dynamic import to the MCP tools which contain the swarm logic
|
|
125
|
+
let swarmId;
|
|
126
|
+
try {
|
|
127
|
+
const { swarmTools } = await import('../mcp-tools/swarm-tools.js');
|
|
128
|
+
const tools = swarmTools;
|
|
129
|
+
const initTool = tools.find(t => t.name === 'swarm_init');
|
|
130
|
+
if (!initTool)
|
|
131
|
+
return { status: 'skip', message: 'swarm_init tool not found' };
|
|
132
|
+
const initResult = await initTool.handler({
|
|
133
|
+
topology: 'hierarchical',
|
|
134
|
+
maxAgents: 4,
|
|
135
|
+
strategy: 'specialized',
|
|
136
|
+
});
|
|
137
|
+
swarmId = initResult?.swarmId;
|
|
138
|
+
if (!swarmId)
|
|
139
|
+
return { status: 'fail', message: 'No swarm ID returned' };
|
|
140
|
+
// Spawn an agent
|
|
141
|
+
const spawnTool = tools.find(t => t.name === 'agent_spawn');
|
|
142
|
+
if (spawnTool) {
|
|
143
|
+
await spawnTool.handler({ type: 'coder', name: '__diag_agent__' });
|
|
144
|
+
}
|
|
145
|
+
// Status
|
|
146
|
+
const statusTool = tools.find(t => t.name === 'swarm_status');
|
|
147
|
+
if (statusTool) {
|
|
148
|
+
await statusTool.handler({});
|
|
149
|
+
}
|
|
150
|
+
// Stop the agent and swarm
|
|
151
|
+
const agentListTool = tools.find(t => t.name === 'agent_list');
|
|
152
|
+
if (agentListTool) {
|
|
153
|
+
const agents = await agentListTool.handler({});
|
|
154
|
+
const agentList = (agents?.agents ?? []);
|
|
155
|
+
const stopAgentTool = tools.find(t => t.name === 'agent_stop');
|
|
156
|
+
if (stopAgentTool) {
|
|
157
|
+
for (const a of agentList) {
|
|
158
|
+
if (a.id)
|
|
159
|
+
await stopAgentTool.handler({ agentId: a.id });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { status: 'pass', message: `Swarm ${swarmId} — init/spawn/status/stop OK` };
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
return { status: 'fail', message: err instanceof Error ? err.message : String(err) };
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function testHiveMindLifecycle() {
|
|
171
|
+
return () => timed('Hive-Mind Lifecycle', async () => {
|
|
172
|
+
try {
|
|
173
|
+
const { hiveMindTools } = await import('../mcp-tools/hive-mind-tools.js');
|
|
174
|
+
const tools = hiveMindTools;
|
|
175
|
+
const initTool = tools.find(t => t.name === 'hive-mind_init');
|
|
176
|
+
if (!initTool)
|
|
177
|
+
return { status: 'skip', message: 'hive-mind_init tool not found' };
|
|
178
|
+
const initResult = await initTool.handler({
|
|
179
|
+
topology: 'hierarchical-mesh',
|
|
180
|
+
consensus: 'raft',
|
|
181
|
+
maxAgents: 4,
|
|
182
|
+
});
|
|
183
|
+
const hiveId = initResult?.hiveId;
|
|
184
|
+
if (!hiveId)
|
|
185
|
+
return { status: 'fail', message: 'No hive ID returned' };
|
|
186
|
+
// Spawn a worker
|
|
187
|
+
const spawnTool = tools.find(t => t.name === 'hive-mind_spawn');
|
|
188
|
+
if (spawnTool) {
|
|
189
|
+
await spawnTool.handler({ role: 'worker', name: '__diag_worker__' });
|
|
190
|
+
}
|
|
191
|
+
// Status
|
|
192
|
+
const statusTool = tools.find(t => t.name === 'hive-mind_status');
|
|
193
|
+
if (statusTool) {
|
|
194
|
+
await statusTool.handler({});
|
|
195
|
+
}
|
|
196
|
+
// Shutdown
|
|
197
|
+
const shutdownTool = tools.find(t => t.name === 'hive-mind_shutdown');
|
|
198
|
+
if (shutdownTool) {
|
|
199
|
+
await shutdownTool.handler({});
|
|
200
|
+
}
|
|
201
|
+
return { status: 'pass', message: `Hive ${hiveId} — init/spawn/status/shutdown OK` };
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
return { status: 'fail', message: err instanceof Error ? err.message : String(err) };
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function testTaskLifecycle() {
|
|
209
|
+
return () => timed('Task Lifecycle', async () => {
|
|
210
|
+
try {
|
|
211
|
+
const { taskTools } = await import('../mcp-tools/task-tools.js');
|
|
212
|
+
const tools = taskTools;
|
|
213
|
+
const createTool = tools.find(t => t.name === 'task_create');
|
|
214
|
+
if (!createTool)
|
|
215
|
+
return { status: 'skip', message: 'task_create tool not found' };
|
|
216
|
+
const createResult = await createTool.handler({
|
|
217
|
+
type: 'implementation',
|
|
218
|
+
description: '__moflo_diagnose__ test task — safe to delete',
|
|
219
|
+
});
|
|
220
|
+
const taskId = createResult?.taskId;
|
|
221
|
+
if (!taskId)
|
|
222
|
+
return { status: 'fail', message: 'No task ID returned' };
|
|
223
|
+
// List tasks
|
|
224
|
+
const listTool = tools.find(t => t.name === 'task_list');
|
|
225
|
+
if (listTool) {
|
|
226
|
+
const list = await listTool.handler({});
|
|
227
|
+
const tasks = (list?.tasks ?? []);
|
|
228
|
+
if (tasks.length === 0)
|
|
229
|
+
return { status: 'fail', message: 'Task list empty after create' };
|
|
230
|
+
}
|
|
231
|
+
return { status: 'pass', message: `Task ${taskId} — create/list OK` };
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
return { status: 'fail', message: err instanceof Error ? err.message : String(err) };
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
function testHooksRouting() {
|
|
239
|
+
return () => timed('Hooks Routing', async () => {
|
|
240
|
+
try {
|
|
241
|
+
const { hooksTools } = await import('../mcp-tools/hooks-tools.js');
|
|
242
|
+
const tools = hooksTools;
|
|
243
|
+
const routeTool = tools.find(t => t.name === 'hooks_route');
|
|
244
|
+
if (!routeTool)
|
|
245
|
+
return { status: 'skip', message: 'hooks_route tool not found' };
|
|
246
|
+
const result = await routeTool.handler({
|
|
247
|
+
task: 'add user authentication with OAuth',
|
|
248
|
+
});
|
|
249
|
+
const primary = result?.primaryAgent;
|
|
250
|
+
const agent = primary?.type;
|
|
251
|
+
const confidence = primary?.confidence;
|
|
252
|
+
if (!agent)
|
|
253
|
+
return { status: 'fail', message: 'No agent recommendation returned' };
|
|
254
|
+
return { status: 'pass', message: `Routed to ${agent} (${confidence}% confidence)` };
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
return { status: 'fail', message: err instanceof Error ? err.message : String(err) };
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function testConfigShow() {
|
|
262
|
+
return () => timed('Config Show', async () => {
|
|
263
|
+
try {
|
|
264
|
+
const { existsSync, readFileSync } = await import('fs');
|
|
265
|
+
const yamlPaths = [
|
|
266
|
+
'moflo.yaml',
|
|
267
|
+
'.claude-flow/config.yaml',
|
|
268
|
+
'.claude-flow/config.yml',
|
|
269
|
+
];
|
|
270
|
+
for (const p of yamlPaths) {
|
|
271
|
+
if (existsSync(p)) {
|
|
272
|
+
const content = readFileSync(p, 'utf-8');
|
|
273
|
+
if (content.length > 10) {
|
|
274
|
+
return { status: 'pass', message: `${p} (${content.length} bytes)` };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const jsonPaths = [
|
|
279
|
+
'.claude-flow/config.json',
|
|
280
|
+
'claude-flow.config.json',
|
|
281
|
+
];
|
|
282
|
+
for (const p of jsonPaths) {
|
|
283
|
+
if (existsSync(p)) {
|
|
284
|
+
JSON.parse(readFileSync(p, 'utf-8'));
|
|
285
|
+
return { status: 'pass', message: `${p} (valid JSON)` };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return { status: 'fail', message: 'No config file found' };
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
return { status: 'fail', message: err instanceof Error ? err.message : String(err) };
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
function testInitIdempotency() {
|
|
296
|
+
return () => timed('Init Idempotency', async () => {
|
|
297
|
+
try {
|
|
298
|
+
const { existsSync } = await import('fs');
|
|
299
|
+
// Verify that key init artifacts exist (but don't re-run init to avoid side effects)
|
|
300
|
+
const expected = [
|
|
301
|
+
'.claude/settings.json',
|
|
302
|
+
'.claude/agents',
|
|
303
|
+
'.claude/skills/flo/SKILL.md',
|
|
304
|
+
];
|
|
305
|
+
const missing = expected.filter(p => !existsSync(p));
|
|
306
|
+
if (missing.length > 0) {
|
|
307
|
+
return { status: 'fail', message: `Missing: ${missing.join(', ')}` };
|
|
308
|
+
}
|
|
309
|
+
return { status: 'pass', message: `${expected.length} artifacts verified` };
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
return { status: 'fail', message: err instanceof Error ? err.message : String(err) };
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
function testNeuralStatus() {
|
|
317
|
+
return () => timed('Neural Status', async () => {
|
|
318
|
+
try {
|
|
319
|
+
const { neuralTools } = await import('../mcp-tools/neural-tools.js');
|
|
320
|
+
const tools = neuralTools;
|
|
321
|
+
const statusTool = tools.find(t => t.name === 'neural_status');
|
|
322
|
+
if (!statusTool)
|
|
323
|
+
return { status: 'skip', message: 'neural_status tool not found' };
|
|
324
|
+
const result = await statusTool.handler({});
|
|
325
|
+
const components = result?.components;
|
|
326
|
+
if (!components || components.length === 0) {
|
|
327
|
+
// Still pass if we got a response — neural may not be fully loaded
|
|
328
|
+
return { status: 'pass', message: 'Neural subsystem responded' };
|
|
329
|
+
}
|
|
330
|
+
const active = components.filter(c => c.status === 'Active' || c.status === 'Available' || c.status === 'Loaded');
|
|
331
|
+
return { status: 'pass', message: `${active.length}/${components.length} components active` };
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
// Neural is optional — don't fail the whole suite
|
|
335
|
+
return { status: 'skip', message: `Neural not available: ${err instanceof Error ? err.message : String(err)}` };
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
function testMcpParity() {
|
|
340
|
+
return () => timed('MCP Tools Available', async () => {
|
|
341
|
+
try {
|
|
342
|
+
const { memoryTools: memTools } = await import('../mcp-tools/memory-tools.js');
|
|
343
|
+
const expectedMemTools = ['memory_store', 'memory_retrieve', 'memory_search', 'memory_delete', 'memory_list', 'memory_stats'];
|
|
344
|
+
const found = expectedMemTools.filter(name => memTools.find(t => t.name === name));
|
|
345
|
+
const missing = expectedMemTools.filter(name => !memTools.find(t => t.name === name));
|
|
346
|
+
if (missing.length > 0) {
|
|
347
|
+
return { status: 'fail', message: `Missing MCP tools: ${missing.join(', ')}` };
|
|
348
|
+
}
|
|
349
|
+
return { status: 'pass', message: `${found.length} memory tools registered` };
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
return { status: 'fail', message: err instanceof Error ? err.message : String(err) };
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// Main command
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
export const diagnoseCommand = {
|
|
360
|
+
name: 'diagnose',
|
|
361
|
+
description: 'Full integration test suite — exercises all subsystems non-destructively',
|
|
362
|
+
aliases: ['diag'],
|
|
363
|
+
options: [
|
|
364
|
+
{
|
|
365
|
+
name: 'suite',
|
|
366
|
+
short: 's',
|
|
367
|
+
description: 'Run specific suite: memory, swarm, hive, task, hooks, config, neural, mcp, init, all',
|
|
368
|
+
type: 'string',
|
|
369
|
+
default: 'all',
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: 'verbose',
|
|
373
|
+
short: 'v',
|
|
374
|
+
description: 'Show detailed output for each test',
|
|
375
|
+
type: 'boolean',
|
|
376
|
+
default: false,
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
name: 'json',
|
|
380
|
+
description: 'Output results as JSON',
|
|
381
|
+
type: 'boolean',
|
|
382
|
+
default: false,
|
|
383
|
+
},
|
|
384
|
+
],
|
|
385
|
+
examples: [
|
|
386
|
+
{ command: 'moflo diagnose', description: 'Run full integration diagnostics' },
|
|
387
|
+
{ command: 'moflo diagnose --suite memory', description: 'Run only memory tests' },
|
|
388
|
+
{ command: 'moflo diagnose --json', description: 'Output results as JSON' },
|
|
389
|
+
{ command: 'moflo diag', description: 'Alias for diagnose' },
|
|
390
|
+
],
|
|
391
|
+
action: async (ctx) => {
|
|
392
|
+
const suite = ctx.flags.suite || 'all';
|
|
393
|
+
const verbose = ctx.flags.verbose;
|
|
394
|
+
const jsonOutput = ctx.flags.json;
|
|
395
|
+
if (!jsonOutput) {
|
|
396
|
+
output.writeln();
|
|
397
|
+
output.writeln(output.bold('MoFlo Diagnose'));
|
|
398
|
+
output.writeln(output.dim('Full integration test suite — all test data is cleaned up'));
|
|
399
|
+
output.writeln(output.dim('─'.repeat(60)));
|
|
400
|
+
output.writeln();
|
|
401
|
+
}
|
|
402
|
+
// Build test list based on suite filter
|
|
403
|
+
const suites = {
|
|
404
|
+
memory: [
|
|
405
|
+
testMemoryInit(),
|
|
406
|
+
testMemoryStore(),
|
|
407
|
+
testMemoryRetrieve(),
|
|
408
|
+
testMemorySearch(),
|
|
409
|
+
testMemoryList(),
|
|
410
|
+
testMemoryDelete(),
|
|
411
|
+
],
|
|
412
|
+
swarm: [testSwarmLifecycle()],
|
|
413
|
+
hive: [testHiveMindLifecycle()],
|
|
414
|
+
task: [testTaskLifecycle()],
|
|
415
|
+
hooks: [testHooksRouting()],
|
|
416
|
+
config: [testConfigShow()],
|
|
417
|
+
neural: [testNeuralStatus()],
|
|
418
|
+
mcp: [testMcpParity()],
|
|
419
|
+
init: [testInitIdempotency()],
|
|
420
|
+
};
|
|
421
|
+
let tests;
|
|
422
|
+
if (suite === 'all') {
|
|
423
|
+
tests = Object.values(suites).flat();
|
|
424
|
+
}
|
|
425
|
+
else if (suites[suite]) {
|
|
426
|
+
tests = suites[suite];
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
const valid = Object.keys(suites).join(', ');
|
|
430
|
+
output.writeln(output.error(`Unknown suite "${suite}". Valid: ${valid}, all`));
|
|
431
|
+
return { success: false, exitCode: 1 };
|
|
432
|
+
}
|
|
433
|
+
// Run tests sequentially (some depend on prior state, e.g. memory store → retrieve)
|
|
434
|
+
const results = [];
|
|
435
|
+
const spinner = output.createSpinner({ text: 'Running diagnostics...', spinner: 'dots' });
|
|
436
|
+
if (!jsonOutput)
|
|
437
|
+
spinner.start();
|
|
438
|
+
for (const test of tests) {
|
|
439
|
+
const result = await test();
|
|
440
|
+
results.push(result);
|
|
441
|
+
if (!jsonOutput) {
|
|
442
|
+
spinner.stop();
|
|
443
|
+
const icon = result.status === 'pass' ? output.success('✓')
|
|
444
|
+
: result.status === 'skip' ? output.dim('○')
|
|
445
|
+
: output.error('✗');
|
|
446
|
+
const dur = result.duration < 1000
|
|
447
|
+
? `${result.duration.toFixed(0)}ms`
|
|
448
|
+
: `${(result.duration / 1000).toFixed(1)}s`;
|
|
449
|
+
output.writeln(`${icon} ${result.name}: ${result.message} ${output.dim(`(${dur})`)}`);
|
|
450
|
+
spinner.start();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (!jsonOutput)
|
|
454
|
+
spinner.stop();
|
|
455
|
+
// Summary
|
|
456
|
+
const passed = results.filter(r => r.status === 'pass').length;
|
|
457
|
+
const failed = results.filter(r => r.status === 'fail').length;
|
|
458
|
+
const skipped = results.filter(r => r.status === 'skip').length;
|
|
459
|
+
const totalTime = results.reduce((s, r) => s + r.duration, 0);
|
|
460
|
+
if (jsonOutput) {
|
|
461
|
+
const out = {
|
|
462
|
+
passed,
|
|
463
|
+
failed,
|
|
464
|
+
skipped,
|
|
465
|
+
total: results.length,
|
|
466
|
+
totalTime: `${totalTime.toFixed(0)}ms`,
|
|
467
|
+
results: results.map(r => ({
|
|
468
|
+
name: r.name,
|
|
469
|
+
status: r.status,
|
|
470
|
+
message: r.message,
|
|
471
|
+
duration: `${r.duration.toFixed(0)}ms`,
|
|
472
|
+
})),
|
|
473
|
+
};
|
|
474
|
+
output.writeln(JSON.stringify(out, null, 2));
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
output.writeln();
|
|
478
|
+
output.writeln(output.dim('─'.repeat(60)));
|
|
479
|
+
output.writeln();
|
|
480
|
+
const parts = [
|
|
481
|
+
output.success(`${passed} passed`),
|
|
482
|
+
failed > 0 ? output.error(`${failed} failed`) : null,
|
|
483
|
+
skipped > 0 ? output.dim(`${skipped} skipped`) : null,
|
|
484
|
+
].filter(Boolean);
|
|
485
|
+
output.writeln(`${output.bold('Results:')} ${parts.join(', ')} ${output.dim(`(${(totalTime / 1000).toFixed(1)}s)`)}`);
|
|
486
|
+
if (failed > 0) {
|
|
487
|
+
output.writeln();
|
|
488
|
+
output.writeln(output.error('Some tests failed. Run with --verbose or fix the issues above.'));
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
output.writeln();
|
|
492
|
+
output.writeln(output.success('All systems operational.'));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
success: failed === 0,
|
|
497
|
+
exitCode: failed > 0 ? 1 : 0,
|
|
498
|
+
data: { passed, failed, skipped, total: results.length, results },
|
|
499
|
+
};
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
export default diagnoseCommand;
|
|
503
|
+
//# sourceMappingURL=diagnose.js.map
|
|
@@ -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';
|
|
@@ -29,6 +29,7 @@ export { ruvectorCommand } from './ruvector/index.js';
|
|
|
29
29
|
export { hiveMindCommand } from './hive-mind.js';
|
|
30
30
|
export { guidanceCommand } from './guidance.js';
|
|
31
31
|
export { applianceCommand } from './appliance.js';
|
|
32
|
+
export { diagnoseCommand } from './diagnose.js';
|
|
32
33
|
export declare function getConfigCommand(): Promise<Command | undefined>;
|
|
33
34
|
export declare function getMigrateCommand(): Promise<Command | undefined>;
|
|
34
35
|
export declare function getWorkflowCommand(): Promise<Command | undefined>;
|
|
@@ -55,6 +55,8 @@ const commandLoaders = {
|
|
|
55
55
|
update: () => import('./update.js'),
|
|
56
56
|
// RuVector PostgreSQL Bridge
|
|
57
57
|
ruvector: () => import('./ruvector/index.js'),
|
|
58
|
+
// Full integration diagnostics
|
|
59
|
+
diagnose: () => import('./diagnose.js'),
|
|
58
60
|
// Benchmark Suite (Pre-training, Neural, Memory)
|
|
59
61
|
benchmark: () => import('./benchmark.js'),
|
|
60
62
|
// Guidance Control Plane
|
|
@@ -134,6 +136,7 @@ import updateCommand from './update.js';
|
|
|
134
136
|
import { processCommand } from './process.js';
|
|
135
137
|
import { guidanceCommand } from './guidance.js';
|
|
136
138
|
import { applianceCommand } from './appliance.js';
|
|
139
|
+
import { diagnoseCommand } from './diagnose.js';
|
|
137
140
|
// Pre-populate cache with core commands
|
|
138
141
|
loadedCommands.set('init', initCommand);
|
|
139
142
|
loadedCommands.set('start', startCommand);
|
|
@@ -154,6 +157,7 @@ loadedCommands.set('security', securityCommand);
|
|
|
154
157
|
loadedCommands.set('ruvector', ruvectorCommand);
|
|
155
158
|
loadedCommands.set('hive-mind', hiveMindCommand);
|
|
156
159
|
loadedCommands.set('guidance', guidanceCommand);
|
|
160
|
+
loadedCommands.set('diagnose', diagnoseCommand);
|
|
157
161
|
// =============================================================================
|
|
158
162
|
// Exports (maintain backwards compatibility)
|
|
159
163
|
// =============================================================================
|
|
@@ -178,6 +182,7 @@ export { ruvectorCommand } from './ruvector/index.js';
|
|
|
178
182
|
export { hiveMindCommand } from './hive-mind.js';
|
|
179
183
|
export { guidanceCommand } from './guidance.js';
|
|
180
184
|
export { applianceCommand } from './appliance.js';
|
|
185
|
+
export { diagnoseCommand } from './diagnose.js';
|
|
181
186
|
// Lazy-loaded command re-exports (for backwards compatibility, but async-only)
|
|
182
187
|
export async function getConfigCommand() { return loadCommand('config'); }
|
|
183
188
|
export async function getMigrateCommand() { return loadCommand('migrate'); }
|
|
@@ -227,6 +232,7 @@ export const commands = [
|
|
|
227
232
|
ruvectorCommand,
|
|
228
233
|
hiveMindCommand,
|
|
229
234
|
guidanceCommand,
|
|
235
|
+
diagnoseCommand,
|
|
230
236
|
];
|
|
231
237
|
/**
|
|
232
238
|
* Commands organized by category for help display
|
|
@@ -256,6 +262,7 @@ export const commandsByCategory = {
|
|
|
256
262
|
utility: [
|
|
257
263
|
configCommand,
|
|
258
264
|
doctorCommand,
|
|
265
|
+
diagnoseCommand,
|
|
259
266
|
daemonCommand,
|
|
260
267
|
completionsCommand,
|
|
261
268
|
migrateCommand,
|
|
@@ -2384,11 +2384,173 @@ const codeMapCommand = {
|
|
|
2384
2384
|
return { success: true };
|
|
2385
2385
|
}
|
|
2386
2386
|
};
|
|
2387
|
+
// refresh subcommand — reindex everything + vacuum
|
|
2388
|
+
const refreshCommand = {
|
|
2389
|
+
name: 'refresh',
|
|
2390
|
+
description: 'Reindex all guidance and code, rebuild embeddings, clean up expired entries, and vacuum the database',
|
|
2391
|
+
options: [
|
|
2392
|
+
{
|
|
2393
|
+
name: 'skip-guidance',
|
|
2394
|
+
description: 'Skip guidance reindexing',
|
|
2395
|
+
type: 'boolean',
|
|
2396
|
+
default: false,
|
|
2397
|
+
},
|
|
2398
|
+
{
|
|
2399
|
+
name: 'skip-code-map',
|
|
2400
|
+
description: 'Skip code map regeneration',
|
|
2401
|
+
type: 'boolean',
|
|
2402
|
+
default: false,
|
|
2403
|
+
},
|
|
2404
|
+
{
|
|
2405
|
+
name: 'skip-cleanup',
|
|
2406
|
+
description: 'Skip expired entry cleanup',
|
|
2407
|
+
type: 'boolean',
|
|
2408
|
+
default: false,
|
|
2409
|
+
},
|
|
2410
|
+
{
|
|
2411
|
+
name: 'verbose',
|
|
2412
|
+
short: 'v',
|
|
2413
|
+
description: 'Verbose output',
|
|
2414
|
+
type: 'boolean',
|
|
2415
|
+
default: false,
|
|
2416
|
+
},
|
|
2417
|
+
],
|
|
2418
|
+
examples: [
|
|
2419
|
+
{ command: 'flo memory refresh', description: 'Full reindex + vacuum' },
|
|
2420
|
+
{ command: 'flo memory refresh --skip-code-map', description: 'Reindex guidance only + vacuum' },
|
|
2421
|
+
],
|
|
2422
|
+
action: async (ctx) => {
|
|
2423
|
+
const skipGuidance = ctx.flags['skip-guidance'];
|
|
2424
|
+
const skipCodeMap = ctx.flags['skip-code-map'];
|
|
2425
|
+
const skipCleanup = ctx.flags['skip-cleanup'];
|
|
2426
|
+
output.writeln();
|
|
2427
|
+
output.writeln(output.bold('MoFlo Memory Refresh'));
|
|
2428
|
+
output.writeln(output.dim('Reindex all content, rebuild embeddings, clean up, and vacuum'));
|
|
2429
|
+
output.writeln(output.dim('─'.repeat(60)));
|
|
2430
|
+
output.writeln();
|
|
2431
|
+
const t0 = performance.now();
|
|
2432
|
+
const steps = [];
|
|
2433
|
+
// Helper to run a subcommand action
|
|
2434
|
+
const runStep = async (name, skip, action) => {
|
|
2435
|
+
if (skip) {
|
|
2436
|
+
steps.push({ name, status: 'skip', message: 'Skipped', duration: 0 });
|
|
2437
|
+
output.writeln(`${output.dim('○')} ${name}: ${output.dim('Skipped')}`);
|
|
2438
|
+
return;
|
|
2439
|
+
}
|
|
2440
|
+
const stepStart = performance.now();
|
|
2441
|
+
try {
|
|
2442
|
+
const result = await action();
|
|
2443
|
+
const dur = performance.now() - stepStart;
|
|
2444
|
+
const success = result === undefined || result.success;
|
|
2445
|
+
steps.push({ name, status: success ? 'pass' : 'fail', message: success ? 'Done' : (result?.message || 'Failed'), duration: dur });
|
|
2446
|
+
const icon = success ? output.success('✓') : output.error('✗');
|
|
2447
|
+
const durStr = dur < 1000 ? `${dur.toFixed(0)}ms` : `${(dur / 1000).toFixed(1)}s`;
|
|
2448
|
+
output.writeln(`${icon} ${name} ${output.dim(`(${durStr})`)}`);
|
|
2449
|
+
}
|
|
2450
|
+
catch (err) {
|
|
2451
|
+
const dur = performance.now() - stepStart;
|
|
2452
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2453
|
+
steps.push({ name, status: 'fail', message: msg, duration: dur });
|
|
2454
|
+
output.writeln(`${output.error('✗')} ${name}: ${msg}`);
|
|
2455
|
+
}
|
|
2456
|
+
};
|
|
2457
|
+
// Build a fake context with force flag for subcommand calls
|
|
2458
|
+
const forceCtx = {
|
|
2459
|
+
args: [],
|
|
2460
|
+
flags: { force: true, _: [], 'no-embeddings': false, overlap: 20 },
|
|
2461
|
+
cwd: ctx.cwd,
|
|
2462
|
+
interactive: false,
|
|
2463
|
+
};
|
|
2464
|
+
// Step 1: Index guidance
|
|
2465
|
+
await runStep('Index Guidance', skipGuidance, async () => {
|
|
2466
|
+
return indexGuidanceCommand.action(forceCtx);
|
|
2467
|
+
});
|
|
2468
|
+
// Step 2: Code map
|
|
2469
|
+
await runStep('Code Map', skipCodeMap, async () => {
|
|
2470
|
+
const codeMapCtx = {
|
|
2471
|
+
args: [],
|
|
2472
|
+
flags: { force: true, _: [], stats: false },
|
|
2473
|
+
cwd: ctx.cwd,
|
|
2474
|
+
interactive: false,
|
|
2475
|
+
};
|
|
2476
|
+
return codeMapCommand.action(codeMapCtx);
|
|
2477
|
+
});
|
|
2478
|
+
// Step 3: Rebuild embeddings
|
|
2479
|
+
await runStep('Rebuild Embeddings', false, async () => {
|
|
2480
|
+
const rebuildCtx = {
|
|
2481
|
+
args: [],
|
|
2482
|
+
flags: { force: true, _: [] },
|
|
2483
|
+
cwd: ctx.cwd,
|
|
2484
|
+
interactive: false,
|
|
2485
|
+
};
|
|
2486
|
+
return rebuildIndexCommand.action(rebuildCtx);
|
|
2487
|
+
});
|
|
2488
|
+
// Step 4: Cleanup expired entries (direct SQL — avoids MCP dependency)
|
|
2489
|
+
await runStep('Cleanup Expired', skipCleanup, async () => {
|
|
2490
|
+
const { db, dbPath } = await openDb(ctx.cwd);
|
|
2491
|
+
try {
|
|
2492
|
+
const now = Date.now();
|
|
2493
|
+
const result = db.run(`DELETE FROM memory_entries WHERE expires_at IS NOT NULL AND expires_at > 0 AND expires_at < ?`, [now]);
|
|
2494
|
+
const deleted = db.getRowsModified();
|
|
2495
|
+
if (deleted > 0) {
|
|
2496
|
+
saveAndCloseDb(db, dbPath);
|
|
2497
|
+
output.writeln(output.dim(` Removed ${deleted} expired entries`));
|
|
2498
|
+
}
|
|
2499
|
+
else {
|
|
2500
|
+
db.close();
|
|
2501
|
+
output.writeln(output.dim(' No expired entries found'));
|
|
2502
|
+
}
|
|
2503
|
+
return { success: true };
|
|
2504
|
+
}
|
|
2505
|
+
catch (err) {
|
|
2506
|
+
try {
|
|
2507
|
+
db.close();
|
|
2508
|
+
}
|
|
2509
|
+
catch { /* ignore */ }
|
|
2510
|
+
throw err;
|
|
2511
|
+
}
|
|
2512
|
+
});
|
|
2513
|
+
// Step 5: VACUUM the database
|
|
2514
|
+
await runStep('Vacuum Database', false, async () => {
|
|
2515
|
+
const { db, dbPath } = await openDb(ctx.cwd);
|
|
2516
|
+
try {
|
|
2517
|
+
db.run('VACUUM');
|
|
2518
|
+
saveAndCloseDb(db, dbPath);
|
|
2519
|
+
return { success: true };
|
|
2520
|
+
}
|
|
2521
|
+
catch (err) {
|
|
2522
|
+
try {
|
|
2523
|
+
db.close();
|
|
2524
|
+
}
|
|
2525
|
+
catch { /* ignore */ }
|
|
2526
|
+
throw err;
|
|
2527
|
+
}
|
|
2528
|
+
});
|
|
2529
|
+
// Summary
|
|
2530
|
+
const totalTime = performance.now() - t0;
|
|
2531
|
+
const passed = steps.filter(s => s.status === 'pass').length;
|
|
2532
|
+
const failed = steps.filter(s => s.status === 'fail').length;
|
|
2533
|
+
const skipped = steps.filter(s => s.status === 'skip').length;
|
|
2534
|
+
output.writeln();
|
|
2535
|
+
output.writeln(output.dim('─'.repeat(60)));
|
|
2536
|
+
const parts = [
|
|
2537
|
+
output.success(`${passed} done`),
|
|
2538
|
+
failed > 0 ? output.error(`${failed} failed`) : null,
|
|
2539
|
+
skipped > 0 ? output.dim(`${skipped} skipped`) : null,
|
|
2540
|
+
].filter(Boolean);
|
|
2541
|
+
const durStr = totalTime < 1000 ? `${totalTime.toFixed(0)}ms` : `${(totalTime / 1000).toFixed(1)}s`;
|
|
2542
|
+
output.writeln(`${output.bold('Refresh complete:')} ${parts.join(', ')} ${output.dim(`(${durStr})`)}`);
|
|
2543
|
+
if (failed > 0) {
|
|
2544
|
+
return { success: false, exitCode: 1 };
|
|
2545
|
+
}
|
|
2546
|
+
return { success: true };
|
|
2547
|
+
},
|
|
2548
|
+
};
|
|
2387
2549
|
// Main memory command
|
|
2388
2550
|
export const memoryCommand = {
|
|
2389
2551
|
name: 'memory',
|
|
2390
2552
|
description: 'Memory management commands',
|
|
2391
|
-
subcommands: [initMemoryCommand, storeCommand, retrieveCommand, searchCommand, listCommand, deleteCommand, statsCommand, configureCommand, cleanupCommand, compressCommand, exportCommand, importCommand, indexGuidanceCommand, rebuildIndexCommand, codeMapCommand],
|
|
2553
|
+
subcommands: [initMemoryCommand, storeCommand, retrieveCommand, searchCommand, listCommand, deleteCommand, statsCommand, configureCommand, cleanupCommand, compressCommand, exportCommand, importCommand, indexGuidanceCommand, rebuildIndexCommand, codeMapCommand, refreshCommand],
|
|
2392
2554
|
options: [],
|
|
2393
2555
|
examples: [
|
|
2394
2556
|
{ command: 'claude-flow memory store -k "key" -v "value"', description: 'Store data' },
|
|
@@ -2417,7 +2579,8 @@ export const memoryCommand = {
|
|
|
2417
2579
|
`${output.highlight('import')} - Import from file`,
|
|
2418
2580
|
`${output.highlight('index-guidance')} - Index .claude/guidance/ files with RAG segments`,
|
|
2419
2581
|
`${output.highlight('rebuild-index')} - Regenerate embeddings for memory entries`,
|
|
2420
|
-
`${output.highlight('code-map')} - Generate structural code map
|
|
2582
|
+
`${output.highlight('code-map')} - Generate structural code map`,
|
|
2583
|
+
`${output.highlight('refresh')} - Reindex all content, rebuild embeddings, cleanup, and vacuum`
|
|
2421
2584
|
]);
|
|
2422
2585
|
return { success: true };
|
|
2423
2586
|
}
|
|
@@ -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",
|