moflo 4.7.8 → 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 (47) hide show
  1. package/bin/hooks.mjs +23 -20
  2. package/bin/session-start-launcher.mjs +88 -3
  3. package/package.json +1 -1
  4. package/src/@claude-flow/cli/dist/src/commands/daemon.js +42 -95
  5. package/src/@claude-flow/cli/dist/src/commands/doctor.js +11 -5
  6. package/src/@claude-flow/cli/dist/src/config/moflo-config.d.ts +5 -0
  7. package/src/@claude-flow/cli/dist/src/config/moflo-config.js +16 -0
  8. package/src/@claude-flow/cli/dist/src/init/executor.js +74 -0
  9. package/src/@claude-flow/cli/dist/src/services/daemon-lock.d.ts +39 -0
  10. package/src/@claude-flow/cli/dist/src/services/daemon-lock.js +213 -0
  11. package/src/@claude-flow/cli/package.json +1 -1
  12. package/.claude/helpers/README.md +0 -97
  13. package/.claude/helpers/adr-compliance.sh +0 -186
  14. package/.claude/helpers/aggressive-microcompact.mjs +0 -36
  15. package/.claude/helpers/auto-commit.sh +0 -178
  16. package/.claude/helpers/checkpoint-manager.sh +0 -251
  17. package/.claude/helpers/context-persistence-hook.mjs +0 -1979
  18. package/.claude/helpers/daemon-manager.sh +0 -252
  19. package/.claude/helpers/ddd-tracker.sh +0 -144
  20. package/.claude/helpers/github-safe.js +0 -106
  21. package/.claude/helpers/github-setup.sh +0 -28
  22. package/.claude/helpers/guidance-hook.sh +0 -13
  23. package/.claude/helpers/guidance-hooks.sh +0 -102
  24. package/.claude/helpers/health-monitor.sh +0 -108
  25. package/.claude/helpers/learning-hooks.sh +0 -329
  26. package/.claude/helpers/learning-optimizer.sh +0 -127
  27. package/.claude/helpers/learning-service.mjs +0 -1211
  28. package/.claude/helpers/memory.cjs +0 -84
  29. package/.claude/helpers/metrics-db.mjs +0 -492
  30. package/.claude/helpers/patch-aggressive-prune.mjs +0 -184
  31. package/.claude/helpers/pattern-consolidator.sh +0 -86
  32. package/.claude/helpers/perf-worker.sh +0 -160
  33. package/.claude/helpers/quick-start.sh +0 -19
  34. package/.claude/helpers/router.cjs +0 -62
  35. package/.claude/helpers/security-scanner.sh +0 -127
  36. package/.claude/helpers/session.cjs +0 -125
  37. package/.claude/helpers/setup-mcp.sh +0 -18
  38. package/.claude/helpers/standard-checkpoint-hooks.sh +0 -189
  39. package/.claude/helpers/swarm-comms.sh +0 -353
  40. package/.claude/helpers/swarm-hooks.sh +0 -761
  41. package/.claude/helpers/swarm-monitor.sh +0 -211
  42. package/.claude/helpers/sync-v3-metrics.sh +0 -245
  43. package/.claude/helpers/update-v3-progress.sh +0 -166
  44. package/.claude/helpers/v3-quick-status.sh +0 -58
  45. package/.claude/helpers/v3.sh +0 -111
  46. package/.claude/helpers/validate-v3-config.sh +0 -216
  47. package/.claude/helpers/worker-manager.sh +0 -170
package/bin/hooks.mjs CHANGED
@@ -306,10 +306,10 @@ async function main() {
306
306
  }
307
307
 
308
308
  case 'daemon-start': {
309
- if (!isDaemonRunning()) {
309
+ if (!isDaemonLockHeld()) {
310
310
  await runClaudeFlow('daemon', ['start', '--quiet']);
311
311
  } else {
312
- log('info', 'Daemon already running, skipping start');
312
+ log('info', 'Daemon already running (lock held), skipping start');
313
313
  }
314
314
  break;
315
315
  }
@@ -479,34 +479,37 @@ function runBackgroundTraining() {
479
479
  spawnWindowless('node', [localCli, 'neural', 'optimize'], 'neural optimize');
480
480
  }
481
481
 
482
- // Check if daemon is already running via PID file.
483
- // Returns true if a live daemon process exists, false otherwise.
484
- function isDaemonRunning() {
485
- const pidFile = resolve(projectRoot, '.claude-flow', 'daemon.pid');
486
- if (existsSync(pidFile)) {
487
- try {
488
- const pid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);
489
- if (pid && !isNaN(pid)) {
490
- process.kill(pid, 0); // signal 0 = check if process exists, doesn't kill
491
- return true;
492
- }
493
- } catch {
494
- // Process doesn't exist — stale PID file
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;
495
494
  }
495
+ } catch {
496
+ // Dead process or corrupt file — lock is stale
496
497
  }
497
498
  return false;
498
499
  }
499
500
 
500
501
  // Run daemon start in background (non-blocking) — skip if already running
501
502
  function runDaemonStartBackground() {
502
- const localCli = getLocalCliPath();
503
- if (!localCli) {
504
- log('warn', 'Local CLI not found, skipping daemon start');
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');
505
507
  return;
506
508
  }
507
509
 
508
- if (isDaemonRunning()) {
509
- log('info', 'Daemon already running, skipping start');
510
+ const localCli = getLocalCliPath();
511
+ if (!localCli) {
512
+ log('warn', 'Local CLI not found, skipping daemon start');
510
513
  return;
511
514
  }
512
515
 
@@ -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.8",
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",
@@ -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 {
@@ -50,6 +50,11 @@ export interface MofloConfig {
50
50
  circuit_breaker: boolean;
51
51
  agent_overrides: Record<string, string>;
52
52
  };
53
+ auto_update: {
54
+ enabled: boolean;
55
+ scripts: boolean;
56
+ helpers: boolean;
57
+ };
53
58
  status_line: {
54
59
  enabled: boolean;
55
60
  branding: string;
@@ -69,6 +69,11 @@ const DEFAULT_CONFIG = {
69
69
  circuit_breaker: true,
70
70
  agent_overrides: {},
71
71
  },
72
+ auto_update: {
73
+ enabled: true,
74
+ scripts: true,
75
+ helpers: true,
76
+ },
72
77
  status_line: {
73
78
  enabled: true,
74
79
  branding: 'Moflo V4',
@@ -159,6 +164,11 @@ function mergeConfig(raw, root) {
159
164
  circuit_breaker: raw.model_routing?.circuit_breaker ?? raw.modelRouting?.circuitBreaker ?? DEFAULT_CONFIG.model_routing.circuit_breaker,
160
165
  agent_overrides: raw.model_routing?.agent_overrides ?? raw.modelRouting?.agentOverrides ?? DEFAULT_CONFIG.model_routing.agent_overrides,
161
166
  },
167
+ auto_update: {
168
+ enabled: raw.auto_update?.enabled ?? raw.autoUpdate?.enabled ?? DEFAULT_CONFIG.auto_update.enabled,
169
+ scripts: raw.auto_update?.scripts ?? raw.autoUpdate?.scripts ?? DEFAULT_CONFIG.auto_update.scripts,
170
+ helpers: raw.auto_update?.helpers ?? raw.autoUpdate?.helpers ?? DEFAULT_CONFIG.auto_update.helpers,
171
+ },
162
172
  status_line: {
163
173
  enabled: raw.status_line?.enabled ?? raw.statusLine?.enabled ?? DEFAULT_CONFIG.status_line.enabled,
164
174
  branding: raw.status_line?.branding ?? raw.statusLine?.branding ?? DEFAULT_CONFIG.status_line.branding,
@@ -314,6 +324,12 @@ model_routing:
314
324
  # security-architect: opus # Always use opus for security
315
325
  # researcher: sonnet # Pin research to sonnet
316
326
 
327
+ # Auto-update on session start (syncs scripts and helpers when moflo version changes)
328
+ auto_update:
329
+ enabled: true # Master toggle for version-change auto-sync
330
+ scripts: true # Sync .claude/scripts/ from moflo bin/
331
+ helpers: true # Sync .claude/helpers/ from moflo source
332
+
317
333
  # Status line items (show/hide individual sections)
318
334
  status_line:
319
335
  enabled: true
@@ -423,6 +423,46 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
423
423
  };
424
424
  fs.writeFileSync(statuslinePath, generateStatuslineScript(upgradeOptions), 'utf-8');
425
425
  }
426
+ // 1b. ALWAYS sync .claude/scripts/ from moflo bin/ (derived files, not user-edited)
427
+ // Scripts contain critical daemon guards, hook logic, etc. that must stay in sync.
428
+ const scriptsDir = path.join(targetDir, '.claude', 'scripts');
429
+ if (!fs.existsSync(scriptsDir)) {
430
+ fs.mkdirSync(scriptsDir, { recursive: true });
431
+ }
432
+ const UPGRADE_SCRIPT_MAP = {
433
+ 'hooks.mjs': 'hooks.mjs',
434
+ 'session-start-launcher.mjs': 'session-start-launcher.mjs',
435
+ 'index-guidance.mjs': 'index-guidance.mjs',
436
+ 'build-embeddings.mjs': 'build-embeddings.mjs',
437
+ 'generate-code-map.mjs': 'generate-code-map.mjs',
438
+ 'semantic-search.mjs': 'semantic-search.mjs',
439
+ };
440
+ const binDir = findMofloBinDir();
441
+ if (binDir) {
442
+ for (const [destName, srcName] of Object.entries(UPGRADE_SCRIPT_MAP)) {
443
+ const srcPath = path.join(binDir, srcName);
444
+ const destPath = path.join(scriptsDir, destName);
445
+ if (!fs.existsSync(srcPath))
446
+ continue;
447
+ try {
448
+ const srcStat = fs.statSync(srcPath);
449
+ const destExists = fs.existsSync(destPath);
450
+ // Always overwrite if source is newer or dest doesn't exist
451
+ if (!destExists || srcStat.mtimeMs > fs.statSync(destPath).mtimeMs) {
452
+ fs.copyFileSync(srcPath, destPath);
453
+ if (destExists) {
454
+ result.updated.push(`.claude/scripts/${destName}`);
455
+ }
456
+ else {
457
+ result.created.push(`.claude/scripts/${destName}`);
458
+ }
459
+ }
460
+ }
461
+ catch {
462
+ // Non-fatal — skip individual script on error
463
+ }
464
+ }
465
+ }
426
466
  // 2. Create MISSING metrics files only (preserve existing data)
427
467
  const metricsDir = path.join(targetDir, '.claude-flow', 'metrics');
428
468
  const securityDir = path.join(targetDir, '.claude-flow', 'security');
@@ -898,6 +938,40 @@ function findSourceHelpersDir(sourceBaseDir) {
898
938
  }
899
939
  return null;
900
940
  }
941
+ /**
942
+ * Find the moflo bin/ directory (source of truth for .claude/scripts/).
943
+ * Uses the same resolution strategies as findSourceHelpersDir.
944
+ */
945
+ function findMofloBinDir() {
946
+ const possiblePaths = [];
947
+ const SENTINEL_FILE = 'hooks.mjs'; // Must exist in valid bin/
948
+ // Strategy 1: require.resolve
949
+ try {
950
+ const esmRequire = createRequire(import.meta.url);
951
+ const pkgJsonPath = esmRequire.resolve('moflo/package.json');
952
+ possiblePaths.push(path.join(path.dirname(pkgJsonPath), 'bin'));
953
+ }
954
+ catch { /* not installed as package */ }
955
+ // Strategy 2: __dirname-based (dist/src/init -> package root -> bin)
956
+ possiblePaths.push(path.resolve(__dirname, '..', '..', '..', 'bin'));
957
+ // Strategy 3: Walk up from __dirname
958
+ let currentDir = __dirname;
959
+ for (let i = 0; i < 10; i++) {
960
+ const parentDir = path.dirname(currentDir);
961
+ if (parentDir === currentDir)
962
+ break;
963
+ possiblePaths.push(path.join(parentDir, 'bin'));
964
+ currentDir = parentDir;
965
+ }
966
+ // Strategy 4: cwd-relative (node_modules/moflo/bin)
967
+ possiblePaths.push(path.join(process.cwd(), 'node_modules', 'moflo', 'bin'));
968
+ for (const p of possiblePaths) {
969
+ if (fs.existsSync(p) && fs.existsSync(path.join(p, SENTINEL_FILE))) {
970
+ return p;
971
+ }
972
+ }
973
+ return null;
974
+ }
901
975
  /**
902
976
  * Write helper scripts
903
977
  */
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Atomic daemon lock — prevents duplicate daemon processes.
3
+ *
4
+ * Uses fs.writeFileSync with { flag: 'wx' } (O_CREAT | O_EXCL) which is
5
+ * atomic on all platforms: the write fails immediately if the file exists,
6
+ * eliminating the TOCTOU race in the old PID-file approach.
7
+ *
8
+ * Also solves Windows PID recycling by storing a label in the lock payload
9
+ * and verifying the process command line before trusting a "live" PID.
10
+ */
11
+ export interface DaemonLockPayload {
12
+ pid: number;
13
+ startedAt: number;
14
+ label: string;
15
+ }
16
+ /** Resolve the lock file path for a project root. */
17
+ export declare function lockPath(projectRoot: string): string;
18
+ /**
19
+ * Try to acquire the daemon lock atomically.
20
+ *
21
+ * @returns `{ acquired: true }` on success,
22
+ * `{ acquired: false, holder: pid }` if another daemon owns the lock.
23
+ */
24
+ export declare function acquireDaemonLock(projectRoot: string, pid?: number): {
25
+ acquired: true;
26
+ } | {
27
+ acquired: false;
28
+ holder: number;
29
+ };
30
+ /**
31
+ * Release the daemon lock. Only removes if we own it (or force = true).
32
+ */
33
+ export declare function releaseDaemonLock(projectRoot: string, pid?: number, force?: boolean): void;
34
+ /**
35
+ * Check if the daemon lock is currently held by a live daemon.
36
+ * Returns the holder PID or null.
37
+ */
38
+ export declare function getDaemonLockHolder(projectRoot: string): number | null;
39
+ //# sourceMappingURL=daemon-lock.d.ts.map