claude-flow 3.7.0-alpha.16 → 3.7.0-alpha.18
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/package.json +1 -1
- package/v3/@claude-flow/cli/dist/src/commands/daemon.js +284 -2
- package/v3/@claude-flow/cli/dist/src/commands/hooks.js +14 -14
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +11 -0
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +39 -0
- package/v3/@claude-flow/cli/package.json +1 -1
- package/.claude/scheduled_tasks.lock +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-flow",
|
|
3
|
-
"version": "3.7.0-alpha.
|
|
3
|
+
"version": "3.7.0-alpha.18",
|
|
4
4
|
"description": "Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -391,10 +391,17 @@ async function killBackgroundDaemon(projectRoot) {
|
|
|
391
391
|
}
|
|
392
392
|
}
|
|
393
393
|
/**
|
|
394
|
-
* Kill stale daemon processes not tracked by the PID file (#1551).
|
|
395
|
-
* Uses `ps`
|
|
394
|
+
* Kill stale daemon processes not tracked by the PID file (#1551, #1857).
|
|
395
|
+
* Uses `ps` on POSIX and `tasklist` on Windows to find all daemon
|
|
396
|
+
* processes for this project and kill them.
|
|
396
397
|
*/
|
|
397
398
|
async function killStaleDaemons(projectRoot, quiet) {
|
|
399
|
+
if (process.platform === 'win32') {
|
|
400
|
+
return killStaleDaemonsWindows(projectRoot, quiet);
|
|
401
|
+
}
|
|
402
|
+
return killStaleDaemonsPosix(projectRoot, quiet);
|
|
403
|
+
}
|
|
404
|
+
async function killStaleDaemonsPosix(projectRoot, quiet) {
|
|
398
405
|
try {
|
|
399
406
|
const { execFileSync } = await import('child_process');
|
|
400
407
|
const psOutput = execFileSync('ps', ['-eo', 'pid,command'], { encoding: 'utf-8', timeout: 5000 });
|
|
@@ -430,6 +437,63 @@ async function killStaleDaemons(projectRoot, quiet) {
|
|
|
430
437
|
// ps not available or failed — skip stale cleanup
|
|
431
438
|
}
|
|
432
439
|
}
|
|
440
|
+
/**
|
|
441
|
+
* #1857: Windows replacement for the POSIX `ps -eo pid,command` path.
|
|
442
|
+
* Uses `tasklist /v /fo csv` which returns CSV with the full Window
|
|
443
|
+
* Title column (last field) — Node-spawned daemon processes carry
|
|
444
|
+
* their command line there. Best-effort like the POSIX path: any
|
|
445
|
+
* tooling failure (tasklist missing, parse error, etc.) is swallowed
|
|
446
|
+
* silently so cleanup doesn't break daemon start.
|
|
447
|
+
*/
|
|
448
|
+
async function killStaleDaemonsWindows(projectRoot, quiet) {
|
|
449
|
+
try {
|
|
450
|
+
const { execFileSync } = await import('child_process');
|
|
451
|
+
// /v includes the Window Title; /fo csv uses comma-separated quoted fields
|
|
452
|
+
const out = execFileSync('tasklist', ['/v', '/fo', 'csv', '/nh'], { encoding: 'utf-8', timeout: 5000 });
|
|
453
|
+
const lines = out.split(/\r?\n/);
|
|
454
|
+
const currentPid = process.pid;
|
|
455
|
+
const trackedPid = getBackgroundDaemonPid(projectRoot);
|
|
456
|
+
let killed = 0;
|
|
457
|
+
for (const line of lines) {
|
|
458
|
+
if (!line.trim())
|
|
459
|
+
continue;
|
|
460
|
+
// Match daemon command line markers — the Window Title field
|
|
461
|
+
// typically holds the full invocation. Skip rows that aren't ours.
|
|
462
|
+
if (!line.includes('daemon start --foreground'))
|
|
463
|
+
continue;
|
|
464
|
+
if (!line.includes('claude-flow') && !line.includes('@claude-flow/cli'))
|
|
465
|
+
continue;
|
|
466
|
+
// Parse CSV: tasklist quotes each field, so split on `","`
|
|
467
|
+
const fields = line.split(/","/).map(f => f.replace(/^"|"$/g, ''));
|
|
468
|
+
// fields[0] = Image Name, fields[1] = PID, …
|
|
469
|
+
const pidStr = fields[1];
|
|
470
|
+
const pid = parseInt(pidStr ?? '', 10);
|
|
471
|
+
if (isNaN(pid) || pid === currentPid || pid === trackedPid)
|
|
472
|
+
continue;
|
|
473
|
+
if (!isProcessRunning(pid))
|
|
474
|
+
continue;
|
|
475
|
+
try {
|
|
476
|
+
// taskkill is the Windows equivalent of kill — /pid <n> /f forces.
|
|
477
|
+
// Use SIGTERM-equivalent (no /f) first; the daemon's signal handler
|
|
478
|
+
// catches and cleans up; force-kill is the next start's job.
|
|
479
|
+
execFileSync('taskkill', ['/pid', String(pid), '/t'], { encoding: 'utf-8', timeout: 5000 });
|
|
480
|
+
killed++;
|
|
481
|
+
if (!quiet) {
|
|
482
|
+
output.printWarning(`Killed stale daemon process (PID: ${pid})`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
catch { /* taskkill failed — process may have exited; ignore */ }
|
|
486
|
+
}
|
|
487
|
+
if (killed > 0 && !quiet) {
|
|
488
|
+
output.printInfo(`Cleaned up ${killed} stale daemon process(es)`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
// tasklist not available or failed — skip stale cleanup. Defensive
|
|
493
|
+
// shape matches the POSIX path. Not tested on Windows by the
|
|
494
|
+
// maintainer; please report regressions on the issue tracker.
|
|
495
|
+
}
|
|
496
|
+
}
|
|
433
497
|
/**
|
|
434
498
|
* Get PID of background daemon from PID file
|
|
435
499
|
*/
|
|
@@ -671,6 +735,222 @@ function formatTimeUntil(date) {
|
|
|
671
735
|
return `in ${Math.floor(seconds / 3600)}h`;
|
|
672
736
|
return `in ${Math.floor(seconds / 86400)}d`;
|
|
673
737
|
}
|
|
738
|
+
// #1565: Supervisor installer subcommand. Writes a native auto-restart
|
|
739
|
+
// unit (launchd plist on macOS, systemd-user .service on Linux) so the
|
|
740
|
+
// daemon survives crashes and reboots without requiring the operator
|
|
741
|
+
// to manually run `daemon start` after every failure.
|
|
742
|
+
const installSupervisorCommand = {
|
|
743
|
+
name: 'install-supervisor',
|
|
744
|
+
description: 'Install OS-level auto-restart supervisor (launchd on macOS, systemd-user on Linux)',
|
|
745
|
+
options: [
|
|
746
|
+
{ name: 'force', short: 'f', type: 'boolean', description: 'Overwrite existing unit file', default: 'false' },
|
|
747
|
+
{ name: 'load', type: 'boolean', description: 'Load/enable the unit immediately', default: 'true' },
|
|
748
|
+
{ name: 'dry-run', type: 'boolean', description: 'Print the unit file content without writing', default: 'false' },
|
|
749
|
+
],
|
|
750
|
+
examples: [
|
|
751
|
+
{ command: 'claude-flow daemon install-supervisor', description: 'Install + load (auto-restart enabled)' },
|
|
752
|
+
{ command: 'claude-flow daemon install-supervisor --no-load', description: 'Write unit file but do not enable yet' },
|
|
753
|
+
{ command: 'claude-flow daemon install-supervisor --dry-run', description: 'Preview the unit file' },
|
|
754
|
+
],
|
|
755
|
+
action: async (ctx) => {
|
|
756
|
+
const force = ctx.flags.force === true;
|
|
757
|
+
const load = ctx.flags.load !== false;
|
|
758
|
+
const dryRun = ctx.flags['dry-run'] === true || ctx.flags.dryRun === true;
|
|
759
|
+
const projectRoot = process.cwd();
|
|
760
|
+
const platform = process.platform;
|
|
761
|
+
if (platform === 'win32') {
|
|
762
|
+
output.printError('Windows scheduled-task installer is not yet implemented.');
|
|
763
|
+
output.printInfo('Use Task Scheduler manually, or follow this issue: https://github.com/ruvnet/ruflo/issues/1565');
|
|
764
|
+
return { success: false, exitCode: 1 };
|
|
765
|
+
}
|
|
766
|
+
if (platform !== 'darwin' && platform !== 'linux') {
|
|
767
|
+
output.printError(`Unsupported platform: ${platform}. Supported: darwin (launchd), linux (systemd-user).`);
|
|
768
|
+
return { success: false, exitCode: 1 };
|
|
769
|
+
}
|
|
770
|
+
// Resolve absolute paths the unit file will reference.
|
|
771
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
772
|
+
if (!home) {
|
|
773
|
+
output.printError('HOME/USERPROFILE not set; cannot resolve user unit path.');
|
|
774
|
+
return { success: false, exitCode: 1 };
|
|
775
|
+
}
|
|
776
|
+
const nodeBin = process.execPath;
|
|
777
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
778
|
+
const __dirname = dirname(__filename);
|
|
779
|
+
const cliJs = resolve(join(__dirname, '..', '..', '..', 'bin', 'cli.js'));
|
|
780
|
+
if (!fs.existsSync(cliJs)) {
|
|
781
|
+
output.printError(`CLI not found at: ${cliJs}`);
|
|
782
|
+
return { success: false, exitCode: 1 };
|
|
783
|
+
}
|
|
784
|
+
if (platform === 'darwin') {
|
|
785
|
+
const plistDir = join(home, 'Library', 'LaunchAgents');
|
|
786
|
+
const plistPath = join(plistDir, 'io.ruv.ruflo.daemon.plist');
|
|
787
|
+
const logDir = join(projectRoot, '.claude-flow', 'logs');
|
|
788
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
789
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
790
|
+
<plist version="1.0">
|
|
791
|
+
<dict>
|
|
792
|
+
<key>Label</key><string>io.ruv.ruflo.daemon</string>
|
|
793
|
+
<key>ProgramArguments</key>
|
|
794
|
+
<array>
|
|
795
|
+
<string>${nodeBin}</string>
|
|
796
|
+
<string>${cliJs}</string>
|
|
797
|
+
<string>daemon</string><string>start</string><string>--foreground</string><string>--quiet</string>
|
|
798
|
+
</array>
|
|
799
|
+
<key>WorkingDirectory</key><string>${projectRoot}</string>
|
|
800
|
+
<key>RunAtLoad</key><true/>
|
|
801
|
+
<key>KeepAlive</key>
|
|
802
|
+
<dict>
|
|
803
|
+
<key>SuccessfulExit</key><false/>
|
|
804
|
+
<key>Crashed</key><true/>
|
|
805
|
+
</dict>
|
|
806
|
+
<key>ThrottleInterval</key><integer>10</integer>
|
|
807
|
+
<key>StandardOutPath</key><string>${logDir}/supervisor.out.log</string>
|
|
808
|
+
<key>StandardErrorPath</key><string>${logDir}/supervisor.err.log</string>
|
|
809
|
+
<key>EnvironmentVariables</key>
|
|
810
|
+
<dict>
|
|
811
|
+
<key>CLAUDE_FLOW_DAEMON</key><string>1</string>
|
|
812
|
+
</dict>
|
|
813
|
+
</dict>
|
|
814
|
+
</plist>
|
|
815
|
+
`;
|
|
816
|
+
if (dryRun) {
|
|
817
|
+
output.writeln(plist);
|
|
818
|
+
return { success: true };
|
|
819
|
+
}
|
|
820
|
+
if (fs.existsSync(plistPath) && !force) {
|
|
821
|
+
output.printWarning(`Already installed: ${plistPath}`);
|
|
822
|
+
output.printInfo('Use --force to overwrite.');
|
|
823
|
+
return { success: false, exitCode: 1 };
|
|
824
|
+
}
|
|
825
|
+
if (!fs.existsSync(plistDir))
|
|
826
|
+
fs.mkdirSync(plistDir, { recursive: true });
|
|
827
|
+
if (!fs.existsSync(logDir))
|
|
828
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
829
|
+
fs.writeFileSync(plistPath, plist, 'utf-8');
|
|
830
|
+
output.printSuccess(`Wrote ${plistPath}`);
|
|
831
|
+
if (load) {
|
|
832
|
+
try {
|
|
833
|
+
const { execFileSync } = await import('child_process');
|
|
834
|
+
// unload first in case a previous version is loaded
|
|
835
|
+
try {
|
|
836
|
+
execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8', timeout: 5000 });
|
|
837
|
+
}
|
|
838
|
+
catch { /* ok */ }
|
|
839
|
+
execFileSync('launchctl', ['load', '-w', plistPath], { encoding: 'utf-8', timeout: 5000 });
|
|
840
|
+
output.printSuccess('Supervisor loaded — daemon will auto-restart on crash and survive reboot.');
|
|
841
|
+
}
|
|
842
|
+
catch (err) {
|
|
843
|
+
output.printWarning(`launchctl load failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
844
|
+
output.printInfo(`Run manually: launchctl load -w ${plistPath}`);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
output.printInfo(`Run when ready: launchctl load -w ${plistPath}`);
|
|
849
|
+
}
|
|
850
|
+
return { success: true };
|
|
851
|
+
}
|
|
852
|
+
// Linux: systemd-user
|
|
853
|
+
const unitDir = join(home, '.config', 'systemd', 'user');
|
|
854
|
+
const unitPath = join(unitDir, 'ruflo-daemon.service');
|
|
855
|
+
const unit = `[Unit]
|
|
856
|
+
Description=RuFlo background worker daemon
|
|
857
|
+
After=default.target
|
|
858
|
+
|
|
859
|
+
[Service]
|
|
860
|
+
Type=simple
|
|
861
|
+
WorkingDirectory=${projectRoot}
|
|
862
|
+
Environment=CLAUDE_FLOW_DAEMON=1
|
|
863
|
+
ExecStart=${nodeBin} ${cliJs} daemon start --foreground --quiet
|
|
864
|
+
Restart=on-failure
|
|
865
|
+
RestartSec=10
|
|
866
|
+
# Restart on Crashed (signal) too
|
|
867
|
+
StartLimitIntervalSec=300
|
|
868
|
+
StartLimitBurst=5
|
|
869
|
+
|
|
870
|
+
[Install]
|
|
871
|
+
WantedBy=default.target
|
|
872
|
+
`;
|
|
873
|
+
if (dryRun) {
|
|
874
|
+
output.writeln(unit);
|
|
875
|
+
return { success: true };
|
|
876
|
+
}
|
|
877
|
+
if (fs.existsSync(unitPath) && !force) {
|
|
878
|
+
output.printWarning(`Already installed: ${unitPath}`);
|
|
879
|
+
output.printInfo('Use --force to overwrite.');
|
|
880
|
+
return { success: false, exitCode: 1 };
|
|
881
|
+
}
|
|
882
|
+
if (!fs.existsSync(unitDir))
|
|
883
|
+
fs.mkdirSync(unitDir, { recursive: true });
|
|
884
|
+
fs.writeFileSync(unitPath, unit, 'utf-8');
|
|
885
|
+
output.printSuccess(`Wrote ${unitPath}`);
|
|
886
|
+
if (load) {
|
|
887
|
+
try {
|
|
888
|
+
const { execFileSync } = await import('child_process');
|
|
889
|
+
execFileSync('systemctl', ['--user', 'daemon-reload'], { encoding: 'utf-8', timeout: 5000 });
|
|
890
|
+
execFileSync('systemctl', ['--user', 'enable', '--now', 'ruflo-daemon.service'], { encoding: 'utf-8', timeout: 10000 });
|
|
891
|
+
output.printSuccess('Supervisor enabled — daemon will auto-restart on crash and survive reboot.');
|
|
892
|
+
output.printInfo('Note: requires `loginctl enable-linger $USER` for restart-after-logout on some distros.');
|
|
893
|
+
}
|
|
894
|
+
catch (err) {
|
|
895
|
+
output.printWarning(`systemctl --user enable failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
896
|
+
output.printInfo(`Run manually: systemctl --user daemon-reload && systemctl --user enable --now ruflo-daemon.service`);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
else {
|
|
900
|
+
output.printInfo(`Run when ready: systemctl --user daemon-reload && systemctl --user enable --now ruflo-daemon.service`);
|
|
901
|
+
}
|
|
902
|
+
return { success: true };
|
|
903
|
+
},
|
|
904
|
+
};
|
|
905
|
+
const uninstallSupervisorCommand = {
|
|
906
|
+
name: 'uninstall-supervisor',
|
|
907
|
+
description: 'Remove the auto-restart supervisor unit (launchd on macOS, systemd-user on Linux)',
|
|
908
|
+
options: [],
|
|
909
|
+
action: async () => {
|
|
910
|
+
const platform = process.platform;
|
|
911
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
912
|
+
if (platform === 'darwin') {
|
|
913
|
+
const plistPath = join(home, 'Library', 'LaunchAgents', 'io.ruv.ruflo.daemon.plist');
|
|
914
|
+
try {
|
|
915
|
+
const { execFileSync } = await import('child_process');
|
|
916
|
+
try {
|
|
917
|
+
execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8', timeout: 5000 });
|
|
918
|
+
}
|
|
919
|
+
catch { /* ok */ }
|
|
920
|
+
}
|
|
921
|
+
catch { /* ignore */ }
|
|
922
|
+
if (fs.existsSync(plistPath)) {
|
|
923
|
+
fs.unlinkSync(plistPath);
|
|
924
|
+
output.printSuccess(`Removed ${plistPath}`);
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
output.printInfo(`Not installed: ${plistPath}`);
|
|
928
|
+
}
|
|
929
|
+
return { success: true };
|
|
930
|
+
}
|
|
931
|
+
if (platform === 'linux') {
|
|
932
|
+
const unitPath = join(home, '.config', 'systemd', 'user', 'ruflo-daemon.service');
|
|
933
|
+
try {
|
|
934
|
+
const { execFileSync } = await import('child_process');
|
|
935
|
+
try {
|
|
936
|
+
execFileSync('systemctl', ['--user', 'disable', '--now', 'ruflo-daemon.service'], { encoding: 'utf-8', timeout: 5000 });
|
|
937
|
+
}
|
|
938
|
+
catch { /* ok */ }
|
|
939
|
+
}
|
|
940
|
+
catch { /* ignore */ }
|
|
941
|
+
if (fs.existsSync(unitPath)) {
|
|
942
|
+
fs.unlinkSync(unitPath);
|
|
943
|
+
output.printSuccess(`Removed ${unitPath}`);
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
output.printInfo(`Not installed: ${unitPath}`);
|
|
947
|
+
}
|
|
948
|
+
return { success: true };
|
|
949
|
+
}
|
|
950
|
+
output.printError(`Unsupported platform: ${platform}`);
|
|
951
|
+
return { success: false, exitCode: 1 };
|
|
952
|
+
},
|
|
953
|
+
};
|
|
674
954
|
// Main daemon command
|
|
675
955
|
export const daemonCommand = {
|
|
676
956
|
name: 'daemon',
|
|
@@ -681,6 +961,8 @@ export const daemonCommand = {
|
|
|
681
961
|
statusCommand,
|
|
682
962
|
triggerCommand,
|
|
683
963
|
enableCommand,
|
|
964
|
+
installSupervisorCommand,
|
|
965
|
+
uninstallSupervisorCommand,
|
|
684
966
|
],
|
|
685
967
|
options: [],
|
|
686
968
|
examples: [
|
|
@@ -270,7 +270,7 @@ const preEditCommand = {
|
|
|
270
270
|
],
|
|
271
271
|
action: async (ctx) => {
|
|
272
272
|
// Default file to 'unknown' for backward compatibility (env var may be empty)
|
|
273
|
-
const filePath = ctx.
|
|
273
|
+
const filePath = ctx.flags.file || ctx.args[0] || 'unknown';
|
|
274
274
|
const operation = ctx.flags.operation || 'update';
|
|
275
275
|
output.printInfo(`Analyzing context for: ${output.highlight(filePath)}`);
|
|
276
276
|
try {
|
|
@@ -375,7 +375,7 @@ const postEditCommand = {
|
|
|
375
375
|
],
|
|
376
376
|
action: async (ctx) => {
|
|
377
377
|
// Default file to 'unknown' for backward compatibility (env var may be empty)
|
|
378
|
-
const filePath = ctx.
|
|
378
|
+
const filePath = ctx.flags.file || ctx.args[0] || 'unknown';
|
|
379
379
|
// Default success to true for backward compatibility (PostToolUse = success, PostToolUseFailure = failure)
|
|
380
380
|
const success = ctx.flags.success !== undefined ? ctx.flags.success : true;
|
|
381
381
|
output.printInfo(`Recording outcome for: ${output.highlight(filePath)}`);
|
|
@@ -458,7 +458,7 @@ const preCommandCommand = {
|
|
|
458
458
|
{ command: 'claude-flow hooks pre-command -c "npm install lodash"', description: 'Check package install' }
|
|
459
459
|
],
|
|
460
460
|
action: async (ctx) => {
|
|
461
|
-
const command = ctx.
|
|
461
|
+
const command = ctx.flags.command || ctx.args[0];
|
|
462
462
|
if (!command) {
|
|
463
463
|
output.printError('Command is required. Use --command or -c flag.');
|
|
464
464
|
return { success: false, exitCode: 1 };
|
|
@@ -567,7 +567,7 @@ const postCommandCommand = {
|
|
|
567
567
|
{ command: 'claude-flow hooks post-command -c "npm build" --success false -e 1', description: 'Record failed build' }
|
|
568
568
|
],
|
|
569
569
|
action: async (ctx) => {
|
|
570
|
-
const command = ctx.
|
|
570
|
+
const command = ctx.flags.command || ctx.args[0];
|
|
571
571
|
// Default success to true for backward compatibility
|
|
572
572
|
const success = ctx.flags.success !== undefined ? ctx.flags.success : true;
|
|
573
573
|
if (!command) {
|
|
@@ -639,7 +639,7 @@ const routeCommand = {
|
|
|
639
639
|
{ command: 'claude-flow hooks route -t "Optimize database queries" -K 5', description: 'Get top 5 suggestions' }
|
|
640
640
|
],
|
|
641
641
|
action: async (ctx) => {
|
|
642
|
-
const task = ctx.
|
|
642
|
+
const task = ctx.flags.task || ctx.args[0];
|
|
643
643
|
const topK = ctx.flags.topK || 3;
|
|
644
644
|
if (!task) {
|
|
645
645
|
output.printError('Task description is required. Use --task or -t flag.');
|
|
@@ -751,7 +751,7 @@ const explainCommand = {
|
|
|
751
751
|
{ command: 'claude-flow hooks explain -t "Optimize queries" -a coder --verbose', description: 'Verbose explanation for specific agent' }
|
|
752
752
|
],
|
|
753
753
|
action: async (ctx) => {
|
|
754
|
-
const task = ctx.
|
|
754
|
+
const task = ctx.flags.task || ctx.args[0];
|
|
755
755
|
if (!task) {
|
|
756
756
|
output.printError('Task description is required. Use --task or -t flag.');
|
|
757
757
|
return { success: false, exitCode: 1 };
|
|
@@ -1224,7 +1224,7 @@ const transferFromProjectCommand = {
|
|
|
1224
1224
|
{ command: 'claude-flow hooks transfer from-project -s ../prod --filter security -m 0.9', description: 'Transfer high-confidence security patterns' }
|
|
1225
1225
|
],
|
|
1226
1226
|
action: async (ctx) => {
|
|
1227
|
-
const sourcePath = ctx.
|
|
1227
|
+
const sourcePath = ctx.flags.source || ctx.args[0];
|
|
1228
1228
|
const minConfidence = ctx.flags.minConfidence || 0.7;
|
|
1229
1229
|
if (!sourcePath) {
|
|
1230
1230
|
output.printError('Source project path is required. Use --source or -s flag.');
|
|
@@ -1422,7 +1422,7 @@ const preTaskCommand = {
|
|
|
1422
1422
|
],
|
|
1423
1423
|
action: async (ctx) => {
|
|
1424
1424
|
const taskId = ctx.flags.taskId || `task-${Date.now().toString(36)}`;
|
|
1425
|
-
const description = ctx.
|
|
1425
|
+
const description = ctx.flags.description || ctx.args[0];
|
|
1426
1426
|
if (!description) {
|
|
1427
1427
|
output.printError('Description is required: --description "your task"');
|
|
1428
1428
|
return { success: false, exitCode: 1 };
|
|
@@ -1707,7 +1707,7 @@ const sessionRestoreCommand = {
|
|
|
1707
1707
|
{ command: 'claude-flow hooks session-restore -i session-12345', description: 'Restore specific session' }
|
|
1708
1708
|
],
|
|
1709
1709
|
action: async (ctx) => {
|
|
1710
|
-
const sessionId = ctx.
|
|
1710
|
+
const sessionId = ctx.flags.sessionId || ctx.args[0] || 'latest';
|
|
1711
1711
|
output.printInfo(`Restoring session: ${output.highlight(sessionId)}`);
|
|
1712
1712
|
try {
|
|
1713
1713
|
const result = await callMCPTool('hooks_session-restore', {
|
|
@@ -2519,7 +2519,7 @@ const coverageRouteCommand = {
|
|
|
2519
2519
|
{ command: 'claude-flow hooks coverage-route -t "add tests" --threshold 90', description: 'Route with custom threshold' }
|
|
2520
2520
|
],
|
|
2521
2521
|
action: async (ctx) => {
|
|
2522
|
-
const task = ctx.
|
|
2522
|
+
const task = ctx.flags.task || ctx.args[0];
|
|
2523
2523
|
const threshold = ctx.flags.threshold || 80;
|
|
2524
2524
|
const useRuvector = !ctx.flags['no-ruvector'];
|
|
2525
2525
|
if (!task) {
|
|
@@ -2739,7 +2739,7 @@ const coverageSuggestCommand = {
|
|
|
2739
2739
|
{ command: 'claude-flow hooks coverage-suggest -p src/services --threshold 90', description: 'Stricter threshold' }
|
|
2740
2740
|
],
|
|
2741
2741
|
action: async (ctx) => {
|
|
2742
|
-
const targetPath = ctx.
|
|
2742
|
+
const targetPath = ctx.flags.path || ctx.args[0];
|
|
2743
2743
|
const threshold = ctx.flags.threshold || 80;
|
|
2744
2744
|
const limit = ctx.flags.limit || 20;
|
|
2745
2745
|
if (!targetPath) {
|
|
@@ -3977,7 +3977,7 @@ const modelRouteCommand = {
|
|
|
3977
3977
|
{ command: 'claude-flow hooks model-route -t "architect auth system"', description: 'Route complex task (likely opus)' },
|
|
3978
3978
|
],
|
|
3979
3979
|
action: async (ctx) => {
|
|
3980
|
-
const task = ctx.
|
|
3980
|
+
const task = ctx.flags.task || ctx.args[0];
|
|
3981
3981
|
if (!task) {
|
|
3982
3982
|
output.printError('Task description required. Use --task or -t flag.');
|
|
3983
3983
|
return { success: false, exitCode: 1 };
|
|
@@ -4285,7 +4285,7 @@ const taskCompletedCommand = {
|
|
|
4285
4285
|
{ command: 'claude-flow hooks task-completed -i task-456 --notify-lead --quality 0.95', description: 'Complete with quality score' }
|
|
4286
4286
|
],
|
|
4287
4287
|
action: async (ctx) => {
|
|
4288
|
-
const taskId = ctx.
|
|
4288
|
+
const taskId = ctx.flags.taskId || ctx.args[0];
|
|
4289
4289
|
const trainPatterns = ctx.flags.trainPatterns !== false;
|
|
4290
4290
|
const notifyLead = ctx.flags.notifyLead !== false;
|
|
4291
4291
|
const success = ctx.flags.success !== false;
|
|
@@ -4363,7 +4363,7 @@ const notifyCommand = {
|
|
|
4363
4363
|
{ command: 'claude-flow hooks notify -m "Test failed" -l error', description: 'Send error notification' },
|
|
4364
4364
|
],
|
|
4365
4365
|
action: async (ctx) => {
|
|
4366
|
-
const message = ctx.
|
|
4366
|
+
const message = ctx.flags.message || ctx.args[0];
|
|
4367
4367
|
const level = ctx.flags.level || 'info';
|
|
4368
4368
|
if (!message) {
|
|
4369
4369
|
output.printError('Message is required: --message "your message"');
|
|
@@ -27,6 +27,7 @@ interface WorkerState {
|
|
|
27
27
|
failureCount: number;
|
|
28
28
|
averageDurationMs: number;
|
|
29
29
|
isRunning: boolean;
|
|
30
|
+
lastStartedAt?: Date;
|
|
30
31
|
}
|
|
31
32
|
interface WorkerResult {
|
|
32
33
|
workerId: string;
|
|
@@ -128,6 +129,16 @@ export declare class WorkerDaemon extends EventEmitter {
|
|
|
128
129
|
* lifetime to reap orphans after a hard crash.
|
|
129
130
|
*/
|
|
130
131
|
private get childrenFile();
|
|
132
|
+
/**
|
|
133
|
+
* #1856: detect workers that were mid-flight when the previous daemon
|
|
134
|
+
* lifetime ended. A mid-flight worker has `lastStartedAt > lastRun`
|
|
135
|
+
* (started after the last successful completion). On crash recovery
|
|
136
|
+
* we count these as failures so the run-counter math stays consistent
|
|
137
|
+
* (`runCount === successCount + failureCount`). Workers naturally
|
|
138
|
+
* retry at their next scheduled interval; we deliberately don't
|
|
139
|
+
* immediately re-run because the failure may have been deterministic.
|
|
140
|
+
*/
|
|
141
|
+
private detectMidFlightFailures;
|
|
131
142
|
/**
|
|
132
143
|
* Snapshot the currently-active headless worker child PIDs to disk.
|
|
133
144
|
* Best-effort; failures don't propagate.
|
|
@@ -315,6 +315,33 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
315
315
|
get childrenFile() {
|
|
316
316
|
return join(this.projectRoot, '.claude-flow', 'daemon-children.json');
|
|
317
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* #1856: detect workers that were mid-flight when the previous daemon
|
|
320
|
+
* lifetime ended. A mid-flight worker has `lastStartedAt > lastRun`
|
|
321
|
+
* (started after the last successful completion). On crash recovery
|
|
322
|
+
* we count these as failures so the run-counter math stays consistent
|
|
323
|
+
* (`runCount === successCount + failureCount`). Workers naturally
|
|
324
|
+
* retry at their next scheduled interval; we deliberately don't
|
|
325
|
+
* immediately re-run because the failure may have been deterministic.
|
|
326
|
+
*/
|
|
327
|
+
detectMidFlightFailures() {
|
|
328
|
+
let detected = 0;
|
|
329
|
+
for (const [type, state] of this.workers.entries()) {
|
|
330
|
+
const startedAt = state.lastStartedAt?.getTime() ?? 0;
|
|
331
|
+
const lastRunAt = state.lastRun?.getTime() ?? 0;
|
|
332
|
+
// started after the last successful completion → was mid-flight
|
|
333
|
+
if (startedAt > 0 && startedAt > lastRunAt) {
|
|
334
|
+
state.failureCount++;
|
|
335
|
+
state.isRunning = false;
|
|
336
|
+
// Don't bump runCount — it was already incremented at start
|
|
337
|
+
this.log('info', `Worker ${type} was mid-flight at last crash (started ${state.lastStartedAt?.toISOString()}); counted as failure, will retry at next scheduled interval`);
|
|
338
|
+
detected++;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (detected > 0) {
|
|
342
|
+
this.saveState();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
318
345
|
/**
|
|
319
346
|
* Snapshot the currently-active headless worker child PIDs to disk.
|
|
320
347
|
* Best-effort; failures don't propagate.
|
|
@@ -460,12 +487,14 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
460
487
|
for (const [type, state] of Object.entries(saved.workers)) {
|
|
461
488
|
const savedState = state;
|
|
462
489
|
const lastRunValue = savedState.lastRun;
|
|
490
|
+
const lastStartedAtValue = savedState.lastStartedAt;
|
|
463
491
|
this.workers.set(type, {
|
|
464
492
|
runCount: savedState.runCount || 0,
|
|
465
493
|
successCount: savedState.successCount || 0,
|
|
466
494
|
failureCount: savedState.failureCount || 0,
|
|
467
495
|
averageDurationMs: savedState.averageDurationMs || 0,
|
|
468
496
|
lastRun: lastRunValue ? new Date(lastRunValue) : undefined,
|
|
497
|
+
lastStartedAt: lastStartedAtValue ? new Date(lastStartedAtValue) : undefined,
|
|
469
498
|
nextRun: undefined,
|
|
470
499
|
isRunning: false,
|
|
471
500
|
});
|
|
@@ -570,6 +599,13 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
570
599
|
// accepting new work. The children file from the prior daemon's
|
|
571
600
|
// last-snapshot is the authoritative list.
|
|
572
601
|
this.reapOrphanedChildren();
|
|
602
|
+
// #1856: detect workers that were mid-flight at the previous crash
|
|
603
|
+
// and count them as failures so runCount/successCount/failureCount
|
|
604
|
+
// stay consistent. Workers retry naturally at their next scheduled
|
|
605
|
+
// interval — we don't immediately re-run them, which avoids a
|
|
606
|
+
// freshly-recovered daemon hammering the same code path that just
|
|
607
|
+
// killed it.
|
|
608
|
+
this.detectMidFlightFailures();
|
|
573
609
|
this.running = true;
|
|
574
610
|
this.startedAt = new Date();
|
|
575
611
|
this.writePidFile();
|
|
@@ -764,6 +800,8 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
764
800
|
// Track running worker
|
|
765
801
|
this.runningWorkers.add(workerConfig.type);
|
|
766
802
|
state.isRunning = true;
|
|
803
|
+
state.lastStartedAt = new Date(); // #1856: timestamp the start
|
|
804
|
+
this.saveState(); // persist before we run anything
|
|
767
805
|
this.emit('worker:start', { workerId, type: workerConfig.type });
|
|
768
806
|
this.log('info', `Starting worker: ${workerConfig.type} (${this.runningWorkers.size}/${this.config.maxConcurrent} concurrent)`);
|
|
769
807
|
try {
|
|
@@ -1215,6 +1253,7 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
1215
1253
|
{
|
|
1216
1254
|
...state,
|
|
1217
1255
|
lastRun: state.lastRun?.toISOString(),
|
|
1256
|
+
lastStartedAt: state.lastStartedAt?.toISOString(),
|
|
1218
1257
|
nextRun: state.nextRun?.toISOString(),
|
|
1219
1258
|
}
|
|
1220
1259
|
])),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.7.0-alpha.
|
|
3
|
+
"version": "3.7.0-alpha.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"sessionId":"7c6134bf-6c84-4145-9232-421e80ca3692","pid":85190,"procStart":"Fri May 8 13:13:24 2026","acquiredAt":1778246373999}
|