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.
- package/bin/hooks.mjs +23 -20
- package/bin/session-start-launcher.mjs +88 -3
- package/package.json +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/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/init/executor.js +74 -0
- 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 +1 -1
- 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
package/bin/hooks.mjs
CHANGED
|
@@ -306,10 +306,10 @@ async function main() {
|
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
case 'daemon-start': {
|
|
309
|
-
if (!
|
|
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
|
|
483
|
-
//
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
509
|
-
|
|
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.
|
|
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",
|
|
@@ -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 {
|
|
@@ -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
|