moflo 4.7.7 → 4.8.0
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/statusline.cjs +34 -26
- package/.claude/settings.json +2 -2
- package/README.md +1 -1
- package/bin/hooks.mjs +33 -3
- package/bin/session-start-launcher.mjs +88 -3
- package/package.json +3 -5
- package/src/@claude-flow/cli/README.md +1 -1
- package/src/@claude-flow/cli/dist/src/commands/daemon.js +42 -95
- package/src/@claude-flow/cli/dist/src/commands/doctor.js +11 -5
- package/src/@claude-flow/cli/dist/src/commands/init.js +0 -145
- package/src/@claude-flow/cli/dist/src/config/moflo-config.d.ts +5 -0
- package/src/@claude-flow/cli/dist/src/config/moflo-config.js +16 -0
- package/src/@claude-flow/cli/dist/src/config-adapter.d.ts +1 -1
- package/src/@claude-flow/cli/dist/src/init/executor.js +74 -7
- package/src/@claude-flow/cli/dist/src/init/mcp-generator.d.ts +3 -4
- package/src/@claude-flow/cli/dist/src/init/mcp-generator.js +65 -22
- package/src/@claude-flow/cli/dist/src/init/types.d.ts +0 -4
- package/src/@claude-flow/cli/dist/src/init/types.js +0 -5
- package/src/@claude-flow/cli/dist/src/mcp-server.js +36 -0
- package/src/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +6 -0
- package/src/@claude-flow/cli/dist/src/memory/memory-bridge.js +66 -0
- package/src/@claude-flow/cli/dist/src/memory/memory-initializer.js +52 -1
- package/src/@claude-flow/cli/dist/src/services/daemon-lock.d.ts +39 -0
- package/src/@claude-flow/cli/dist/src/services/daemon-lock.js +213 -0
- package/src/@claude-flow/cli/package.json +2 -6
- package/.claude/helpers/README.md +0 -97
- package/.claude/helpers/adr-compliance.sh +0 -186
- package/.claude/helpers/aggressive-microcompact.mjs +0 -36
- package/.claude/helpers/auto-commit.sh +0 -178
- package/.claude/helpers/checkpoint-manager.sh +0 -251
- package/.claude/helpers/context-persistence-hook.mjs +0 -1979
- package/.claude/helpers/daemon-manager.sh +0 -252
- package/.claude/helpers/ddd-tracker.sh +0 -144
- package/.claude/helpers/github-safe.js +0 -106
- package/.claude/helpers/github-setup.sh +0 -28
- package/.claude/helpers/guidance-hook.sh +0 -13
- package/.claude/helpers/guidance-hooks.sh +0 -102
- package/.claude/helpers/health-monitor.sh +0 -108
- package/.claude/helpers/learning-hooks.sh +0 -329
- package/.claude/helpers/learning-optimizer.sh +0 -127
- package/.claude/helpers/learning-service.mjs +0 -1211
- package/.claude/helpers/memory.cjs +0 -84
- package/.claude/helpers/metrics-db.mjs +0 -492
- package/.claude/helpers/patch-aggressive-prune.mjs +0 -184
- package/.claude/helpers/pattern-consolidator.sh +0 -86
- package/.claude/helpers/perf-worker.sh +0 -160
- package/.claude/helpers/quick-start.sh +0 -19
- package/.claude/helpers/router.cjs +0 -62
- package/.claude/helpers/security-scanner.sh +0 -127
- package/.claude/helpers/session.cjs +0 -125
- package/.claude/helpers/setup-mcp.sh +0 -18
- package/.claude/helpers/standard-checkpoint-hooks.sh +0 -189
- package/.claude/helpers/swarm-comms.sh +0 -353
- package/.claude/helpers/swarm-hooks.sh +0 -761
- package/.claude/helpers/swarm-monitor.sh +0 -211
- package/.claude/helpers/sync-v3-metrics.sh +0 -245
- package/.claude/helpers/update-v3-progress.sh +0 -166
- package/.claude/helpers/v3-quick-status.sh +0 -58
- package/.claude/helpers/v3.sh +0 -111
- package/.claude/helpers/validate-v3-config.sh +0 -216
- package/.claude/helpers/worker-manager.sh +0 -170
|
@@ -330,25 +330,33 @@ function getSecurityStatus() {
|
|
|
330
330
|
}
|
|
331
331
|
|
|
332
332
|
// Swarm status (pure file reads, NO ps aux)
|
|
333
|
+
// Metrics files older than 5 minutes are considered stale (swarm no longer running)
|
|
333
334
|
function getSwarmStatus() {
|
|
335
|
+
const STALE_MS = 5 * 60_000;
|
|
336
|
+
const now = Date.now();
|
|
337
|
+
|
|
334
338
|
const activityData = readJSON(path.join(CWD, '.claude-flow', 'metrics', 'swarm-activity.json'));
|
|
335
339
|
if (activityData?.swarm) {
|
|
336
|
-
const
|
|
340
|
+
const ts = activityData.timestamp ? new Date(activityData.timestamp).getTime() : 0;
|
|
341
|
+
const stale = (now - ts) > STALE_MS;
|
|
342
|
+
const count = stale ? 0 : (activityData.swarm.agent_count || 0);
|
|
337
343
|
return {
|
|
338
344
|
activeAgents: Math.min(count, CONFIG.maxAgents),
|
|
339
345
|
maxAgents: CONFIG.maxAgents,
|
|
340
|
-
coordinationActive: activityData.swarm.coordination_active || activityData.swarm.active || false,
|
|
346
|
+
coordinationActive: stale ? false : (activityData.swarm.coordination_active || activityData.swarm.active || false),
|
|
341
347
|
};
|
|
342
348
|
}
|
|
343
349
|
|
|
344
350
|
const progressData = readJSON(path.join(CWD, '.claude-flow', 'metrics', 'v3-progress.json'));
|
|
345
351
|
if (progressData?.swarm) {
|
|
346
|
-
const
|
|
352
|
+
const ts = progressData.timestamp ? new Date(progressData.timestamp).getTime() : 0;
|
|
353
|
+
const stale = (now - ts) > STALE_MS;
|
|
354
|
+
const count = stale ? 0 : (progressData.swarm.activeAgents || progressData.swarm.agent_count || 0);
|
|
347
355
|
const max = progressData.swarm.totalAgents || CONFIG.maxAgents;
|
|
348
356
|
return {
|
|
349
357
|
activeAgents: Math.min(count, max),
|
|
350
358
|
maxAgents: max,
|
|
351
|
-
coordinationActive: progressData.swarm.active || (count > 0),
|
|
359
|
+
coordinationActive: stale ? false : (progressData.swarm.active || (count > 0)),
|
|
352
360
|
};
|
|
353
361
|
}
|
|
354
362
|
|
|
@@ -461,44 +469,44 @@ function getHooksStatus() {
|
|
|
461
469
|
return { enabled, total };
|
|
462
470
|
}
|
|
463
471
|
|
|
464
|
-
// AgentDB stats —
|
|
472
|
+
// AgentDB stats — reads from cache file written by embedding/memory operations.
|
|
473
|
+
// No subprocess spawning. Falls back to DB file size estimate if cache is missing.
|
|
465
474
|
function getAgentDBStats() {
|
|
466
475
|
let vectorCount = 0;
|
|
467
476
|
let dbSizeKB = 0;
|
|
468
477
|
let namespaces = 0;
|
|
469
478
|
let hasHnsw = false;
|
|
470
|
-
let dbPath = null;
|
|
471
479
|
|
|
480
|
+
// Read cached stats (written by memory store/embed/rebuild commands)
|
|
481
|
+
const cachePaths = [
|
|
482
|
+
path.join(CWD, '.claude-flow', 'vector-stats.json'),
|
|
483
|
+
path.join(CWD, '.swarm', 'vector-stats.json'),
|
|
484
|
+
];
|
|
485
|
+
for (const cp of cachePaths) {
|
|
486
|
+
const cached = readJSON(cp);
|
|
487
|
+
if (cached && typeof cached.vectorCount === 'number') {
|
|
488
|
+
vectorCount = cached.vectorCount;
|
|
489
|
+
dbSizeKB = cached.dbSizeKB || 0;
|
|
490
|
+
namespaces = cached.namespaces || 0;
|
|
491
|
+
hasHnsw = cached.hasHnsw || false;
|
|
492
|
+
return { vectorCount, dbSizeKB, namespaces, hasHnsw };
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Fallback: estimate from DB file size (no subprocess)
|
|
472
497
|
const dbFiles = [
|
|
473
498
|
path.join(CWD, '.swarm', 'memory.db'),
|
|
474
499
|
path.join(CWD, '.claude-flow', 'memory.db'),
|
|
475
500
|
path.join(CWD, '.claude', 'memory.db'),
|
|
476
501
|
path.join(CWD, 'data', 'memory.db'),
|
|
477
502
|
];
|
|
478
|
-
|
|
479
503
|
for (const f of dbFiles) {
|
|
480
504
|
const stat = safeStat(f);
|
|
481
505
|
if (stat) {
|
|
482
|
-
dbSizeKB = stat.size / 1024;
|
|
483
|
-
dbPath = f;
|
|
484
|
-
break;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Try to get real count from sqlite (fast — single COUNT query)
|
|
489
|
-
if (dbPath) {
|
|
490
|
-
const countOutput = safeExec(`node -e "const S=require('sql.js');const f=require('fs');S().then(Q=>{const d=new Q.Database(f.readFileSync('${dbPath.replace(/\\/g, '/')}'));const s=d.prepare('SELECT COUNT(*) as c FROM memory_entries WHERE status=\\"active\\" AND embedding IS NOT NULL');s.step();console.log(JSON.stringify(s.getAsObject()));s.free();const n=d.prepare('SELECT COUNT(DISTINCT namespace) as n FROM memory_entries WHERE status=\\"active\\"');n.step();console.log(JSON.stringify(n.getAsObject()));n.free();d.close();})"`, 3000);
|
|
491
|
-
if (countOutput) {
|
|
492
|
-
try {
|
|
493
|
-
const lines = countOutput.trim().split('\n');
|
|
494
|
-
vectorCount = JSON.parse(lines[0]).c || 0;
|
|
495
|
-
namespaces = lines[1] ? JSON.parse(lines[1]).n || 0 : 0;
|
|
496
|
-
} catch { /* fall back to estimate */ }
|
|
497
|
-
}
|
|
498
|
-
// Fallback to file size estimate if query failed
|
|
499
|
-
if (vectorCount === 0) {
|
|
506
|
+
dbSizeKB = Math.floor(stat.size / 1024);
|
|
500
507
|
vectorCount = Math.floor(dbSizeKB / 2);
|
|
501
508
|
namespaces = 1;
|
|
509
|
+
break;
|
|
502
510
|
}
|
|
503
511
|
}
|
|
504
512
|
|
|
@@ -513,7 +521,7 @@ function getAgentDBStats() {
|
|
|
513
521
|
}
|
|
514
522
|
}
|
|
515
523
|
|
|
516
|
-
return { vectorCount, dbSizeKB
|
|
524
|
+
return { vectorCount, dbSizeKB, namespaces, hasHnsw };
|
|
517
525
|
}
|
|
518
526
|
|
|
519
527
|
// Test stats (count files only — NO reading file contents)
|
package/.claude/settings.json
CHANGED
package/README.md
CHANGED
package/bin/hooks.mjs
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { spawn } from 'child_process';
|
|
23
|
-
import { existsSync, appendFileSync } from 'fs';
|
|
23
|
+
import { existsSync, appendFileSync, readFileSync } from 'fs';
|
|
24
24
|
import { resolve, dirname } from 'path';
|
|
25
25
|
import { fileURLToPath } from 'url';
|
|
26
26
|
|
|
@@ -306,7 +306,11 @@ async function main() {
|
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
case 'daemon-start': {
|
|
309
|
-
|
|
309
|
+
if (!isDaemonLockHeld()) {
|
|
310
|
+
await runClaudeFlow('daemon', ['start', '--quiet']);
|
|
311
|
+
} else {
|
|
312
|
+
log('info', 'Daemon already running (lock held), skipping start');
|
|
313
|
+
}
|
|
310
314
|
break;
|
|
311
315
|
}
|
|
312
316
|
|
|
@@ -475,8 +479,34 @@ function runBackgroundTraining() {
|
|
|
475
479
|
spawnWindowless('node', [localCli, 'neural', 'optimize'], 'neural optimize');
|
|
476
480
|
}
|
|
477
481
|
|
|
478
|
-
//
|
|
482
|
+
// Check if daemon lock exists — fast pre-check to avoid spawning a Node process
|
|
483
|
+
// just to have it bail out via the atomic lock in daemon.ts.
|
|
484
|
+
// Uses daemon.lock (atomic wx-based) instead of the old daemon.pid (TOCTOU-vulnerable).
|
|
485
|
+
function isDaemonLockHeld() {
|
|
486
|
+
const lockFile = resolve(projectRoot, '.claude-flow', 'daemon.lock');
|
|
487
|
+
if (!existsSync(lockFile)) return false;
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
const data = JSON.parse(readFileSync(lockFile, 'utf-8'));
|
|
491
|
+
if (typeof data.pid === 'number' && data.pid > 0) {
|
|
492
|
+
process.kill(data.pid, 0); // check if alive
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
} catch {
|
|
496
|
+
// Dead process or corrupt file — lock is stale
|
|
497
|
+
}
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Run daemon start in background (non-blocking) — skip if already running
|
|
479
502
|
function runDaemonStartBackground() {
|
|
503
|
+
// Fast check: if daemon lock is held by a live process, skip spawning entirely.
|
|
504
|
+
// This avoids zombie Node processes from subagents that all fire SessionStart.
|
|
505
|
+
if (isDaemonLockHeld()) {
|
|
506
|
+
log('info', 'Daemon already running (lock held), skipping start');
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
480
510
|
const localCli = getLocalCliPath();
|
|
481
511
|
if (!localCli) {
|
|
482
512
|
log('warn', 'Local CLI not found, skipping daemon start');
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { spawn } from 'child_process';
|
|
11
|
-
import { existsSync } from 'fs';
|
|
11
|
+
import { existsSync, readFileSync, copyFileSync } from 'fs';
|
|
12
12
|
import { resolve, dirname } from 'path';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
14
|
|
|
@@ -47,7 +47,92 @@ try {
|
|
|
47
47
|
// Non-fatal - workflow gate will use defaults
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
// ── 3.
|
|
50
|
+
// ── 3. Auto-sync scripts and helpers on version change ───────────────────────
|
|
51
|
+
// Controlled by `auto_update.enabled` in moflo.yaml (default: true).
|
|
52
|
+
// When moflo is upgraded (npm install), scripts and helpers may be stale.
|
|
53
|
+
// Detect version change and sync from source before running hooks.
|
|
54
|
+
let autoUpdateConfig = { enabled: true, scripts: true, helpers: true };
|
|
55
|
+
try {
|
|
56
|
+
const mofloYaml = resolve(projectRoot, 'moflo.yaml');
|
|
57
|
+
if (existsSync(mofloYaml)) {
|
|
58
|
+
const yamlContent = readFileSync(mofloYaml, 'utf-8');
|
|
59
|
+
// Simple YAML parsing for auto_update block (avoids js-yaml dependency)
|
|
60
|
+
const enabledMatch = yamlContent.match(/auto_update:\s*\n\s+enabled:\s*(true|false)/);
|
|
61
|
+
const scriptsMatch = yamlContent.match(/auto_update:\s*\n(?:\s+\w+:.*\n)*?\s+scripts:\s*(true|false)/);
|
|
62
|
+
const helpersMatch = yamlContent.match(/auto_update:\s*\n(?:\s+\w+:.*\n)*?\s+helpers:\s*(true|false)/);
|
|
63
|
+
if (enabledMatch) autoUpdateConfig.enabled = enabledMatch[1] === 'true';
|
|
64
|
+
if (scriptsMatch) autoUpdateConfig.scripts = scriptsMatch[1] === 'true';
|
|
65
|
+
if (helpersMatch) autoUpdateConfig.helpers = helpersMatch[1] === 'true';
|
|
66
|
+
}
|
|
67
|
+
} catch { /* non-fatal — use defaults (all true) */ }
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const mofloPkgPath = resolve(projectRoot, 'node_modules/moflo/package.json');
|
|
71
|
+
const versionStampPath = resolve(projectRoot, '.claude-flow', 'moflo-version');
|
|
72
|
+
if (autoUpdateConfig.enabled && existsSync(mofloPkgPath)) {
|
|
73
|
+
const installedVersion = JSON.parse(readFileSync(mofloPkgPath, 'utf-8')).version;
|
|
74
|
+
let cachedVersion = '';
|
|
75
|
+
try { cachedVersion = readFileSync(versionStampPath, 'utf-8').trim(); } catch {}
|
|
76
|
+
|
|
77
|
+
if (installedVersion !== cachedVersion) {
|
|
78
|
+
// Version changed — sync scripts from bin/
|
|
79
|
+
if (autoUpdateConfig.scripts) {
|
|
80
|
+
const binDir = resolve(projectRoot, 'node_modules/moflo/bin');
|
|
81
|
+
const scriptsDir = resolve(projectRoot, '.claude/scripts');
|
|
82
|
+
const scriptFiles = [
|
|
83
|
+
'hooks.mjs', 'session-start-launcher.mjs', 'index-guidance.mjs',
|
|
84
|
+
'build-embeddings.mjs', 'generate-code-map.mjs', 'semantic-search.mjs',
|
|
85
|
+
];
|
|
86
|
+
for (const file of scriptFiles) {
|
|
87
|
+
const src = resolve(binDir, file);
|
|
88
|
+
const dest = resolve(scriptsDir, file);
|
|
89
|
+
if (existsSync(src)) {
|
|
90
|
+
try { copyFileSync(src, dest); } catch { /* non-fatal */ }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Sync helpers from source .claude/helpers/
|
|
96
|
+
if (autoUpdateConfig.helpers) {
|
|
97
|
+
const sourceHelpersDir = resolve(projectRoot, 'node_modules/moflo/src/@claude-flow/cli/.claude/helpers');
|
|
98
|
+
const helpersDir = resolve(projectRoot, '.claude/helpers');
|
|
99
|
+
const helperFiles = [
|
|
100
|
+
'auto-memory-hook.mjs', 'statusline.cjs', 'pre-commit', 'post-commit',
|
|
101
|
+
];
|
|
102
|
+
for (const file of helperFiles) {
|
|
103
|
+
const src = resolve(sourceHelpersDir, file);
|
|
104
|
+
const dest = resolve(helpersDir, file);
|
|
105
|
+
if (existsSync(src)) {
|
|
106
|
+
try { copyFileSync(src, dest); } catch { /* non-fatal */ }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Also sync generated helpers via upgrade CLI (hook-handler.cjs, gate.cjs, etc.)
|
|
111
|
+
// These are generated, not shipped, so we trigger an upgrade in the background
|
|
112
|
+
const localCli = resolve(projectRoot, 'node_modules/moflo/src/@claude-flow/cli/bin/cli.js');
|
|
113
|
+
if (existsSync(localCli)) {
|
|
114
|
+
try {
|
|
115
|
+
const proc = spawn('node', [localCli, 'init', '--upgrade', '--quiet'], {
|
|
116
|
+
cwd: projectRoot, stdio: 'ignore', detached: true, windowsHide: true,
|
|
117
|
+
});
|
|
118
|
+
proc.unref();
|
|
119
|
+
} catch { /* non-fatal */ }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Write version stamp
|
|
124
|
+
try {
|
|
125
|
+
const cfDir = resolve(projectRoot, '.claude-flow');
|
|
126
|
+
if (!existsSync(cfDir)) mkdirSync(cfDir, { recursive: true });
|
|
127
|
+
writeFileSync(versionStampPath, installedVersion);
|
|
128
|
+
} catch {}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
// Non-fatal — scripts will still work, just may be stale
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── 4. Spawn background tasks ───────────────────────────────────────────────
|
|
51
136
|
const localCli = resolve(projectRoot, 'node_modules/moflo/src/@claude-flow/cli/bin/cli.js');
|
|
52
137
|
const hasLocalCli = existsSync(localCli);
|
|
53
138
|
|
|
@@ -59,5 +144,5 @@ if (existsSync(hooksScript)) {
|
|
|
59
144
|
|
|
60
145
|
// Patches are now baked into moflo@4.0.0 source — no runtime patching needed.
|
|
61
146
|
|
|
62
|
-
// ──
|
|
147
|
+
// ── 5. Done — exit immediately ──────────────────────────────────────────────
|
|
63
148
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.8.0",
|
|
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",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"dev": "tsx watch src/index.ts",
|
|
50
50
|
"build": "tsc",
|
|
51
51
|
"build:ts": "cd src/@claude-flow/cli && npm run build || true",
|
|
52
|
-
"test": "vitest",
|
|
52
|
+
"test": "vitest run",
|
|
53
53
|
"test:ui": "vitest --ui",
|
|
54
54
|
"test:security": "vitest run src/__tests__/security/",
|
|
55
55
|
"lint": "cd src/@claude-flow/cli && npm run lint || true",
|
|
@@ -67,7 +67,6 @@
|
|
|
67
67
|
"zod": "^3.22.4"
|
|
68
68
|
},
|
|
69
69
|
"optionalDependencies": {
|
|
70
|
-
"@claude-flow/codex": "^3.0.0-alpha.8",
|
|
71
70
|
"@claude-flow/plugin-gastown-bridge": "^0.1.3",
|
|
72
71
|
"@ruvector/attention": "^0.1.3",
|
|
73
72
|
"@ruvector/core": "^0.1.30",
|
|
@@ -81,14 +80,13 @@
|
|
|
81
80
|
"hono": ">=4.11.4"
|
|
82
81
|
},
|
|
83
82
|
"devDependencies": {
|
|
84
|
-
"@openai/codex": "^0.98.0",
|
|
85
83
|
"@types/bcrypt": "^5.0.2",
|
|
86
84
|
"@types/node": "^20.0.0",
|
|
87
85
|
"eslint": "^8.0.0",
|
|
88
86
|
"moflo": "^4.7.4",
|
|
89
87
|
"tsx": "^4.21.0",
|
|
90
88
|
"typescript": "^5.0.0",
|
|
91
|
-
"vitest": "^
|
|
89
|
+
"vitest": "^4.0.0"
|
|
92
90
|
},
|
|
93
91
|
"engines": {
|
|
94
92
|
"node": ">=20.0.0"
|
|
@@ -4,6 +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
8
|
import { spawn } from 'child_process';
|
|
8
9
|
import { fileURLToPath } from 'url';
|
|
9
10
|
import { dirname, join, resolve } from 'path';
|
|
@@ -64,60 +65,26 @@ const startCommand = {
|
|
|
64
65
|
config.resourceThresholds = thresholds;
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
|
-
// Check if background daemon already running (skip if we ARE the daemon process)
|
|
68
|
-
if (!isDaemonProcess) {
|
|
69
|
-
const bgPid = getBackgroundDaemonPid(projectRoot);
|
|
70
|
-
if (bgPid && isProcessRunning(bgPid)) {
|
|
71
|
-
if (!quiet) {
|
|
72
|
-
output.printWarning(`Daemon already running in background (PID: ${bgPid})`);
|
|
73
|
-
}
|
|
74
|
-
return { success: true };
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
68
|
// Background mode (default): fork a detached process
|
|
78
69
|
if (!foreground) {
|
|
79
70
|
return startBackgroundDaemon(projectRoot, quiet, rawMaxCpu, rawMinMem);
|
|
80
71
|
}
|
|
81
72
|
// Foreground mode: run in current process (blocks terminal)
|
|
82
73
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (fs.existsSync(pidFile)) {
|
|
91
|
-
try {
|
|
92
|
-
const existingPid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
93
|
-
if (!isNaN(existingPid) && existingPid !== process.pid) {
|
|
94
|
-
try {
|
|
95
|
-
process.kill(existingPid, 0); // Check if alive
|
|
96
|
-
// Another daemon is running — exit silently
|
|
97
|
-
if (!quiet) {
|
|
98
|
-
output.printWarning(`Daemon already running (PID: ${existingPid})`);
|
|
99
|
-
}
|
|
100
|
-
return { success: true };
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
// Process not running — stale PID file, continue startup
|
|
104
|
-
}
|
|
74
|
+
// Acquire atomic daemon lock (prevents duplicate daemons)
|
|
75
|
+
// Skip lock acquisition if we're the spawned child — parent already holds it
|
|
76
|
+
if (!isDaemonProcess) {
|
|
77
|
+
const lockResult = acquireDaemonLock(projectRoot);
|
|
78
|
+
if (!lockResult.acquired) {
|
|
79
|
+
if (!quiet) {
|
|
80
|
+
output.printWarning(`Daemon already running (PID: ${lockResult.holder})`);
|
|
105
81
|
}
|
|
106
|
-
|
|
107
|
-
catch {
|
|
108
|
-
// Can't read PID file — continue startup
|
|
82
|
+
return { success: true };
|
|
109
83
|
}
|
|
110
84
|
}
|
|
111
|
-
//
|
|
112
|
-
fs.writeFileSync(pidFile, String(process.pid));
|
|
113
|
-
// Clean up PID file on exit
|
|
85
|
+
// Clean up lock file on exit
|
|
114
86
|
const cleanup = () => {
|
|
115
|
-
|
|
116
|
-
if (fs.existsSync(pidFile)) {
|
|
117
|
-
fs.unlinkSync(pidFile);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
catch { /* ignore */ }
|
|
87
|
+
releaseDaemonLock(projectRoot, process.pid, true);
|
|
121
88
|
};
|
|
122
89
|
process.on('exit', cleanup);
|
|
123
90
|
process.on('SIGINT', () => { cleanup(); process.exit(0); });
|
|
@@ -219,12 +186,18 @@ async function startBackgroundDaemon(projectRoot, quiet, maxCpuLoad, minFreeMemo
|
|
|
219
186
|
const resolvedRoot = resolve(projectRoot);
|
|
220
187
|
validatePath(resolvedRoot, 'Project root');
|
|
221
188
|
const stateDir = join(resolvedRoot, '.claude-flow');
|
|
222
|
-
const pidFile = join(stateDir, 'daemon.pid');
|
|
223
189
|
const logFile = join(stateDir, 'daemon.log');
|
|
224
190
|
// Validate all paths
|
|
225
191
|
validatePath(stateDir, 'State directory');
|
|
226
|
-
validatePath(pidFile, 'PID file');
|
|
227
192
|
validatePath(logFile, 'Log file');
|
|
193
|
+
// Acquire atomic lock BEFORE spawning to prevent races
|
|
194
|
+
const lockResult = acquireDaemonLock(resolvedRoot);
|
|
195
|
+
if (!lockResult.acquired) {
|
|
196
|
+
if (!quiet) {
|
|
197
|
+
output.printWarning(`Daemon already running in background (PID: ${lockResult.holder})`);
|
|
198
|
+
}
|
|
199
|
+
return { success: true };
|
|
200
|
+
}
|
|
228
201
|
// Ensure state directory exists
|
|
229
202
|
if (!fs.existsSync(stateDir)) {
|
|
230
203
|
fs.mkdirSync(stateDir, { recursive: true });
|
|
@@ -273,16 +246,21 @@ async function startBackgroundDaemon(projectRoot, quiet, maxCpuLoad, minFreeMemo
|
|
|
273
246
|
// Get PID from spawned process directly (no shell echo needed)
|
|
274
247
|
const pid = child.pid;
|
|
275
248
|
if (!pid || pid <= 0) {
|
|
249
|
+
// Release lock — spawn failed, no daemon running
|
|
250
|
+
releaseDaemonLock(resolvedRoot, process.pid, true);
|
|
276
251
|
output.printError('Failed to get daemon PID');
|
|
277
252
|
return { success: false, exitCode: 1 };
|
|
278
253
|
}
|
|
279
|
-
// Unref BEFORE
|
|
254
|
+
// Unref BEFORE updating lock — prevents race where parent exits
|
|
280
255
|
// but child hasn't fully detached yet (fixes macOS daemon death #1283)
|
|
281
256
|
child.unref();
|
|
282
257
|
// Small delay to let the child process fully detach on macOS
|
|
283
258
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
284
|
-
//
|
|
285
|
-
|
|
259
|
+
// Update the lock file with the child's PID (parent acquired it, child owns it)
|
|
260
|
+
// We force-release our lock and re-acquire with the child PID so the lock
|
|
261
|
+
// accurately reflects the running daemon process.
|
|
262
|
+
releaseDaemonLock(resolvedRoot, process.pid, true);
|
|
263
|
+
acquireDaemonLock(resolvedRoot, pid);
|
|
286
264
|
if (!quiet) {
|
|
287
265
|
output.printSuccess(`Daemon started in background (PID: ${pid})`);
|
|
288
266
|
output.printInfo(`Logs: ${logFile}`);
|
|
@@ -326,76 +304,45 @@ const stopCommand = {
|
|
|
326
304
|
},
|
|
327
305
|
};
|
|
328
306
|
/**
|
|
329
|
-
* Kill background daemon process using
|
|
307
|
+
* Kill background daemon process using lock file
|
|
330
308
|
*/
|
|
331
309
|
async function killBackgroundDaemon(projectRoot) {
|
|
332
|
-
const
|
|
333
|
-
if (!
|
|
310
|
+
const holderPid = getDaemonLockHolder(projectRoot);
|
|
311
|
+
if (!holderPid) {
|
|
312
|
+
// No live daemon — clean up any stale lock
|
|
313
|
+
releaseDaemonLock(projectRoot, 0, true);
|
|
334
314
|
return false;
|
|
335
315
|
}
|
|
336
316
|
try {
|
|
337
|
-
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
338
|
-
if (isNaN(pid)) {
|
|
339
|
-
fs.unlinkSync(pidFile);
|
|
340
|
-
return false;
|
|
341
|
-
}
|
|
342
|
-
// Check if process is running
|
|
343
|
-
try {
|
|
344
|
-
process.kill(pid, 0); // Signal 0 = check if alive
|
|
345
|
-
}
|
|
346
|
-
catch {
|
|
347
|
-
// Process not running, clean up stale PID file
|
|
348
|
-
fs.unlinkSync(pidFile);
|
|
349
|
-
return false;
|
|
350
|
-
}
|
|
351
317
|
// Kill the process
|
|
352
|
-
process.kill(
|
|
318
|
+
process.kill(holderPid, 'SIGTERM');
|
|
353
319
|
// Wait a moment then force kill if needed
|
|
354
320
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
355
321
|
try {
|
|
356
|
-
process.kill(
|
|
322
|
+
process.kill(holderPid, 0);
|
|
357
323
|
// Still alive, force kill
|
|
358
|
-
process.kill(
|
|
324
|
+
process.kill(holderPid, 'SIGKILL');
|
|
359
325
|
}
|
|
360
326
|
catch {
|
|
361
327
|
// Process terminated
|
|
362
328
|
}
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
fs.unlinkSync(pidFile);
|
|
366
|
-
}
|
|
329
|
+
// Release lock
|
|
330
|
+
releaseDaemonLock(projectRoot, holderPid, true);
|
|
367
331
|
return true;
|
|
368
332
|
}
|
|
369
333
|
catch (error) {
|
|
370
|
-
// Clean up
|
|
371
|
-
|
|
372
|
-
fs.unlinkSync(pidFile);
|
|
373
|
-
}
|
|
334
|
+
// Clean up lock on any error
|
|
335
|
+
releaseDaemonLock(projectRoot, 0, true);
|
|
374
336
|
return false;
|
|
375
337
|
}
|
|
376
338
|
}
|
|
377
|
-
|
|
378
|
-
* Get PID of background daemon from PID file
|
|
379
|
-
*/
|
|
339
|
+
// Legacy aliases — delegate to daemon-lock module
|
|
380
340
|
function getBackgroundDaemonPid(projectRoot) {
|
|
381
|
-
|
|
382
|
-
if (!fs.existsSync(pidFile)) {
|
|
383
|
-
return null;
|
|
384
|
-
}
|
|
385
|
-
try {
|
|
386
|
-
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
387
|
-
return isNaN(pid) ? null : pid;
|
|
388
|
-
}
|
|
389
|
-
catch {
|
|
390
|
-
return null;
|
|
391
|
-
}
|
|
341
|
+
return getDaemonLockHolder(projectRoot);
|
|
392
342
|
}
|
|
393
|
-
/**
|
|
394
|
-
* Check if a process is running
|
|
395
|
-
*/
|
|
396
343
|
function isProcessRunning(pid) {
|
|
397
344
|
try {
|
|
398
|
-
process.kill(pid, 0);
|
|
345
|
+
process.kill(pid, 0);
|
|
399
346
|
return true;
|
|
400
347
|
}
|
|
401
348
|
catch {
|
|
@@ -93,17 +93,23 @@ async function checkConfigFile() {
|
|
|
93
93
|
// Check daemon status
|
|
94
94
|
async function checkDaemonStatus() {
|
|
95
95
|
try {
|
|
96
|
-
const
|
|
97
|
-
if (existsSync(
|
|
98
|
-
const pid = readFileSync(pidFile, 'utf8').trim();
|
|
96
|
+
const lockFile = '.claude-flow/daemon.lock';
|
|
97
|
+
if (existsSync(lockFile)) {
|
|
99
98
|
try {
|
|
100
|
-
|
|
99
|
+
const data = JSON.parse(readFileSync(lockFile, 'utf8'));
|
|
100
|
+
const pid = data.pid;
|
|
101
|
+
process.kill(pid, 0); // Check if process exists
|
|
101
102
|
return { name: 'Daemon Status', status: 'pass', message: `Running (PID: ${pid})` };
|
|
102
103
|
}
|
|
103
104
|
catch {
|
|
104
|
-
return { name: 'Daemon Status', status: 'warn', message: 'Stale
|
|
105
|
+
return { name: 'Daemon Status', status: 'warn', message: 'Stale lock file', fix: 'rm .claude-flow/daemon.lock && claude-flow daemon start' };
|
|
105
106
|
}
|
|
106
107
|
}
|
|
108
|
+
// Also check legacy PID file
|
|
109
|
+
const pidFile = '.claude-flow/daemon.pid';
|
|
110
|
+
if (existsSync(pidFile)) {
|
|
111
|
+
return { name: 'Daemon Status', status: 'warn', message: 'Legacy PID file found', fix: 'rm .claude-flow/daemon.pid && claude-flow daemon start' };
|
|
112
|
+
}
|
|
107
113
|
return { name: 'Daemon Status', status: 'warn', message: 'Not running', fix: 'claude-flow daemon start' };
|
|
108
114
|
}
|
|
109
115
|
catch {
|