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.
Files changed (61) hide show
  1. package/.claude/helpers/statusline.cjs +34 -26
  2. package/.claude/settings.json +2 -2
  3. package/README.md +1 -1
  4. package/bin/hooks.mjs +33 -3
  5. package/bin/session-start-launcher.mjs +88 -3
  6. package/package.json +3 -5
  7. package/src/@claude-flow/cli/README.md +1 -1
  8. package/src/@claude-flow/cli/dist/src/commands/daemon.js +42 -95
  9. package/src/@claude-flow/cli/dist/src/commands/doctor.js +11 -5
  10. package/src/@claude-flow/cli/dist/src/commands/init.js +0 -145
  11. package/src/@claude-flow/cli/dist/src/config/moflo-config.d.ts +5 -0
  12. package/src/@claude-flow/cli/dist/src/config/moflo-config.js +16 -0
  13. package/src/@claude-flow/cli/dist/src/config-adapter.d.ts +1 -1
  14. package/src/@claude-flow/cli/dist/src/init/executor.js +74 -7
  15. package/src/@claude-flow/cli/dist/src/init/mcp-generator.d.ts +3 -4
  16. package/src/@claude-flow/cli/dist/src/init/mcp-generator.js +65 -22
  17. package/src/@claude-flow/cli/dist/src/init/types.d.ts +0 -4
  18. package/src/@claude-flow/cli/dist/src/init/types.js +0 -5
  19. package/src/@claude-flow/cli/dist/src/mcp-server.js +36 -0
  20. package/src/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +6 -0
  21. package/src/@claude-flow/cli/dist/src/memory/memory-bridge.js +66 -0
  22. package/src/@claude-flow/cli/dist/src/memory/memory-initializer.js +52 -1
  23. package/src/@claude-flow/cli/dist/src/services/daemon-lock.d.ts +39 -0
  24. package/src/@claude-flow/cli/dist/src/services/daemon-lock.js +213 -0
  25. package/src/@claude-flow/cli/package.json +2 -6
  26. package/.claude/helpers/README.md +0 -97
  27. package/.claude/helpers/adr-compliance.sh +0 -186
  28. package/.claude/helpers/aggressive-microcompact.mjs +0 -36
  29. package/.claude/helpers/auto-commit.sh +0 -178
  30. package/.claude/helpers/checkpoint-manager.sh +0 -251
  31. package/.claude/helpers/context-persistence-hook.mjs +0 -1979
  32. package/.claude/helpers/daemon-manager.sh +0 -252
  33. package/.claude/helpers/ddd-tracker.sh +0 -144
  34. package/.claude/helpers/github-safe.js +0 -106
  35. package/.claude/helpers/github-setup.sh +0 -28
  36. package/.claude/helpers/guidance-hook.sh +0 -13
  37. package/.claude/helpers/guidance-hooks.sh +0 -102
  38. package/.claude/helpers/health-monitor.sh +0 -108
  39. package/.claude/helpers/learning-hooks.sh +0 -329
  40. package/.claude/helpers/learning-optimizer.sh +0 -127
  41. package/.claude/helpers/learning-service.mjs +0 -1211
  42. package/.claude/helpers/memory.cjs +0 -84
  43. package/.claude/helpers/metrics-db.mjs +0 -492
  44. package/.claude/helpers/patch-aggressive-prune.mjs +0 -184
  45. package/.claude/helpers/pattern-consolidator.sh +0 -86
  46. package/.claude/helpers/perf-worker.sh +0 -160
  47. package/.claude/helpers/quick-start.sh +0 -19
  48. package/.claude/helpers/router.cjs +0 -62
  49. package/.claude/helpers/security-scanner.sh +0 -127
  50. package/.claude/helpers/session.cjs +0 -125
  51. package/.claude/helpers/setup-mcp.sh +0 -18
  52. package/.claude/helpers/standard-checkpoint-hooks.sh +0 -189
  53. package/.claude/helpers/swarm-comms.sh +0 -353
  54. package/.claude/helpers/swarm-hooks.sh +0 -761
  55. package/.claude/helpers/swarm-monitor.sh +0 -211
  56. package/.claude/helpers/sync-v3-metrics.sh +0 -245
  57. package/.claude/helpers/update-v3-progress.sh +0 -166
  58. package/.claude/helpers/v3-quick-status.sh +0 -58
  59. package/.claude/helpers/v3.sh +0 -111
  60. package/.claude/helpers/validate-v3-config.sh +0 -216
  61. 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 count = activityData.swarm.agent_count || 0;
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 count = progressData.swarm.activeAgents || progressData.swarm.agent_count || 0;
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 — queries real count from sqlite when possible, falls back to file size estimate
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: Math.floor(dbSizeKB), namespaces, hasHnsw };
524
+ return { vectorCount, dbSizeKB, namespaces, hasHnsw };
517
525
  }
518
526
 
519
527
  // Test stats (count files only — NO reading file contents)
@@ -275,9 +275,9 @@
275
275
  },
276
276
  "mcpServers": {
277
277
  "claude-flow": {
278
- "command": "npx",
278
+ "command": "node",
279
279
  "args": [
280
- "moflo",
280
+ "bin/cli.js",
281
281
  "mcp",
282
282
  "start"
283
283
  ]
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="https://raw.githubusercontent.com/eric-cielo/moflo/main/docs/Moflo_md.png?v=5" alt="MoFlo" />
2
+ <img src="https://raw.githubusercontent.com/eric-cielo/moflo/main/docs/Moflo_md.png?v=6" alt="MoFlo" />
3
3
  </p>
4
4
 
5
5
  # MoFlo
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
- await runClaudeFlow('daemon', ['start', '--quiet']);
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
- // Run daemon start in background (non-blocking)
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. Spawn background tasks ───────────────────────────────────────────────
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
- // ── 4. Done — exit immediately ──────────────────────────────────────────────
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.7.7",
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": "^1.0.0"
89
+ "vitest": "^4.0.0"
92
90
  },
93
91
  "engines": {
94
92
  "node": ">=20.0.0"
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="https://raw.githubusercontent.com/eric-cielo/moflo/main/docs/Moflo_md.png?v=5" alt="MoFlo" />
2
+ <img src="https://raw.githubusercontent.com/eric-cielo/moflo/main/docs/Moflo_md.png?v=6" alt="MoFlo" />
3
3
  </p>
4
4
 
5
5
  # MoFlo
@@ -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
- const stateDir = join(projectRoot, '.claude-flow');
84
- const pidFile = join(stateDir, 'daemon.pid');
85
- // Ensure state directory exists
86
- if (!fs.existsSync(stateDir)) {
87
- fs.mkdirSync(stateDir, { recursive: true });
88
- }
89
- // Check if another foreground daemon is already running (prevents duplicate daemons)
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
- // Write PID file for foreground mode
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
- try {
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 writing PID file — prevents race where parent exits
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
- // Save PID only after child is detached
285
- fs.writeFileSync(pidFile, String(pid));
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 PID file
307
+ * Kill background daemon process using lock file
330
308
  */
331
309
  async function killBackgroundDaemon(projectRoot) {
332
- const pidFile = join(projectRoot, '.claude-flow', 'daemon.pid');
333
- if (!fs.existsSync(pidFile)) {
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(pid, 'SIGTERM');
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(pid, 0);
322
+ process.kill(holderPid, 0);
357
323
  // Still alive, force kill
358
- process.kill(pid, 'SIGKILL');
324
+ process.kill(holderPid, 'SIGKILL');
359
325
  }
360
326
  catch {
361
327
  // Process terminated
362
328
  }
363
- // Clean up PID file
364
- if (fs.existsSync(pidFile)) {
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 PID file on any error
371
- if (fs.existsSync(pidFile)) {
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
- const pidFile = join(projectRoot, '.claude-flow', 'daemon.pid');
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); // Signal 0 = check if alive
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 pidFile = '.claude-flow/daemon.pid';
97
- if (existsSync(pidFile)) {
98
- const pid = readFileSync(pidFile, 'utf8').trim();
96
+ const lockFile = '.claude-flow/daemon.lock';
97
+ if (existsSync(lockFile)) {
99
98
  try {
100
- process.kill(parseInt(pid, 10), 0); // Check if process exists
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 PID file', fix: 'rm .claude-flow/daemon.pid && claude-flow daemon start' };
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 {