@claude-flow/cli 3.7.0-alpha.8 → 3.7.0-alpha.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/github/release-manager.md +2 -2
- package/.claude/agents/github/release-swarm.md +1 -1
- package/.claude/agents/github/repo-architect.md +2 -2
- package/.claude/agents/github/swarm-pr.md +1 -1
- package/.claude/agents/github/workflow-automation.md +2 -2
- package/.claude/commands/github/code-review-swarm.md +1 -1
- package/.claude/commands/github/issue-tracker.md +3 -3
- package/.claude/commands/github/release-manager.md +5 -3
- package/.claude/commands/github/release-swarm.md +1 -1
- package/.claude/commands/github/repo-architect.md +2 -2
- package/.claude/commands/github/swarm-issue.md +4 -1
- package/.claude/commands/github/swarm-pr.md +6 -3
- package/.claude/commands/github/sync-coordinator.md +3 -1
- package/.claude/commands/github/workflow-automation.md +2 -2
- package/.claude/helpers/github-safe.js +95 -60
- package/.claude/helpers/github-setup.sh +26 -9
- package/.claude/helpers/hook-handler.cjs +12 -4
- package/.claude/helpers/statusline.cjs +31 -2
- package/.claude/helpers/statusline.js +35 -4
- package/.claude/settings.json +1 -1
- package/.claude/skills/dual-mode/README.md +71 -0
- package/.claude/skills/dual-mode/dual-collect.md +103 -0
- package/.claude/skills/dual-mode/dual-coordinate.md +85 -0
- package/.claude/skills/dual-mode/dual-spawn.md +81 -0
- package/.claude/skills/flow-nexus-neural/SKILL.md +0 -11
- package/.claude/skills/flow-nexus-platform/SKILL.md +2 -5
- package/.claude/skills/flow-nexus-swarm/SKILL.md +0 -6
- package/.claude/skills/github-code-review/SKILL.md +2 -17
- package/.claude/skills/github-multi-repo/SKILL.md +4 -16
- package/.claude/skills/github-project-management/SKILL.md +18 -33
- package/.claude/skills/github-release-management/SKILL.md +4 -21
- package/.claude/skills/github-workflow-automation/SKILL.md +8 -26
- package/.claude/skills/reasoningbank-intelligence/SKILL.md +2 -2
- package/.claude/skills/sparc-methodology/SKILL.md +2 -11
- package/.claude/skills/stream-chain/SKILL.md +0 -3
- package/.claude/skills/swarm-advanced/SKILL.md +2 -5
- package/.claude/skills/swarm-orchestration/SKILL.md +1 -1
- package/.claude/skills/verification-quality/SKILL.md +120 -78
- package/README.md +57 -38
- package/bin/cli.js +15 -2
- package/bin/mcp-server.js +1 -1
- package/dist/src/__probe.d.ts +2 -0
- package/dist/src/__probe.d.ts.map +1 -0
- package/dist/src/__probe.js +5 -0
- package/dist/src/__probe.js.map +1 -0
- package/dist/src/commands/agent-wasm.js +2 -2
- package/dist/src/commands/agent-wasm.js.map +1 -1
- package/dist/src/commands/benchmark-cosign.d.ts +29 -0
- package/dist/src/commands/benchmark-cosign.d.ts.map +1 -0
- package/dist/src/commands/benchmark-cosign.js +222 -0
- package/dist/src/commands/benchmark-cosign.js.map +1 -0
- package/dist/src/commands/benchmark-verify.d.ts +21 -0
- package/dist/src/commands/benchmark-verify.d.ts.map +1 -0
- package/dist/src/commands/benchmark-verify.js +202 -0
- package/dist/src/commands/benchmark-verify.js.map +1 -0
- package/dist/src/commands/daemon.d.ts +20 -0
- package/dist/src/commands/daemon.d.ts.map +1 -1
- package/dist/src/commands/daemon.js +366 -7
- package/dist/src/commands/daemon.js.map +1 -1
- package/dist/src/commands/doctor.d.ts.map +1 -1
- package/dist/src/commands/doctor.js +224 -46
- package/dist/src/commands/doctor.js.map +1 -1
- package/dist/src/commands/embeddings.d.ts.map +1 -1
- package/dist/src/commands/embeddings.js +18 -9
- package/dist/src/commands/embeddings.js.map +1 -1
- package/dist/src/commands/hive-mind.d.ts.map +1 -1
- package/dist/src/commands/hive-mind.js +25 -7
- package/dist/src/commands/hive-mind.js.map +1 -1
- package/dist/src/commands/hooks.d.ts.map +1 -1
- package/dist/src/commands/hooks.js +56 -29
- package/dist/src/commands/hooks.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +21 -1
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/memory.d.ts.map +1 -1
- package/dist/src/commands/memory.js +128 -3
- package/dist/src/commands/memory.js.map +1 -1
- package/dist/src/commands/start.js +1 -1
- package/dist/src/commands/start.js.map +1 -1
- package/dist/src/commands/swarm.js +1 -1
- package/dist/src/commands/swarm.js.map +1 -1
- package/dist/src/commands/task.d.ts.map +1 -1
- package/dist/src/commands/task.js +8 -4
- package/dist/src/commands/task.js.map +1 -1
- package/dist/src/config-adapter.js +1 -1
- package/dist/src/config-adapter.js.map +1 -1
- package/dist/src/index.d.ts +5 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +61 -18
- package/dist/src/index.js.map +1 -1
- package/dist/src/init/claudemd-generator.d.ts.map +1 -1
- package/dist/src/init/claudemd-generator.js +1 -0
- package/dist/src/init/claudemd-generator.js.map +1 -1
- package/dist/src/init/executor.d.ts.map +1 -1
- package/dist/src/init/executor.js +133 -0
- package/dist/src/init/executor.js.map +1 -1
- package/dist/src/init/helpers-generator.d.ts +1 -0
- package/dist/src/init/helpers-generator.d.ts.map +1 -1
- package/dist/src/init/helpers-generator.js +19 -2
- package/dist/src/init/helpers-generator.js.map +1 -1
- package/dist/src/init/mcp-generator.js +4 -4
- package/dist/src/init/mcp-generator.js.map +1 -1
- package/dist/src/init/settings-generator.d.ts.map +1 -1
- package/dist/src/init/settings-generator.js +84 -19
- package/dist/src/init/settings-generator.js.map +1 -1
- package/dist/src/init/statusline-generator.d.ts.map +1 -1
- package/dist/src/init/statusline-generator.js +75 -31
- package/dist/src/init/statusline-generator.js.map +1 -1
- package/dist/src/init/types.d.ts +30 -0
- package/dist/src/init/types.d.ts.map +1 -1
- package/dist/src/init/types.js +18 -5
- package/dist/src/init/types.js.map +1 -1
- package/dist/src/mcp-client.d.ts.map +1 -1
- package/dist/src/mcp-client.js +12 -0
- package/dist/src/mcp-client.js.map +1 -1
- package/dist/src/mcp-server.d.ts.map +1 -1
- package/dist/src/mcp-server.js +38 -5
- package/dist/src/mcp-server.js.map +1 -1
- package/dist/src/mcp-tools/agent-execute-core.d.ts +3 -2
- package/dist/src/mcp-tools/agent-execute-core.d.ts.map +1 -1
- package/dist/src/mcp-tools/agent-execute-core.js +157 -83
- package/dist/src/mcp-tools/agent-execute-core.js.map +1 -1
- package/dist/src/mcp-tools/agent-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/agent-tools.js +119 -11
- package/dist/src/mcp-tools/agent-tools.js.map +1 -1
- package/dist/src/mcp-tools/agentdb-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/agentdb-tools.js +101 -24
- package/dist/src/mcp-tools/agentdb-tools.js.map +1 -1
- package/dist/src/mcp-tools/analyze-tools.js +6 -6
- package/dist/src/mcp-tools/analyze-tools.js.map +1 -1
- package/dist/src/mcp-tools/autopilot-tools.js +10 -10
- package/dist/src/mcp-tools/autopilot-tools.js.map +1 -1
- package/dist/src/mcp-tools/browser-session-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/browser-session-tools.js +18 -7
- package/dist/src/mcp-tools/browser-session-tools.js.map +1 -1
- package/dist/src/mcp-tools/browser-tools.js +23 -23
- package/dist/src/mcp-tools/browser-tools.js.map +1 -1
- package/dist/src/mcp-tools/claims-tools.js +12 -12
- package/dist/src/mcp-tools/claims-tools.js.map +1 -1
- package/dist/src/mcp-tools/config-tools.js +6 -6
- package/dist/src/mcp-tools/config-tools.js.map +1 -1
- package/dist/src/mcp-tools/coordination-tools.js +7 -7
- package/dist/src/mcp-tools/coordination-tools.js.map +1 -1
- package/dist/src/mcp-tools/daa-tools.js +8 -8
- package/dist/src/mcp-tools/daa-tools.js.map +1 -1
- package/dist/src/mcp-tools/embeddings-tools.js +10 -10
- package/dist/src/mcp-tools/embeddings-tools.js.map +1 -1
- package/dist/src/mcp-tools/github-tools.js +5 -5
- package/dist/src/mcp-tools/github-tools.js.map +1 -1
- package/dist/src/mcp-tools/guidance-tools.js +21 -21
- package/dist/src/mcp-tools/guidance-tools.js.map +1 -1
- package/dist/src/mcp-tools/hive-consensus-runtime.d.ts +149 -0
- package/dist/src/mcp-tools/hive-consensus-runtime.d.ts.map +1 -0
- package/dist/src/mcp-tools/hive-consensus-runtime.js +296 -0
- package/dist/src/mcp-tools/hive-consensus-runtime.js.map +1 -0
- package/dist/src/mcp-tools/hive-mind-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/hive-mind-tools.js +53 -9
- package/dist/src/mcp-tools/hive-mind-tools.js.map +1 -1
- package/dist/src/mcp-tools/hooks-tools.d.ts +2 -0
- package/dist/src/mcp-tools/hooks-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/hooks-tools.js +179 -46
- package/dist/src/mcp-tools/hooks-tools.js.map +1 -1
- package/dist/src/mcp-tools/managed-agent-tools.d.ts +22 -0
- package/dist/src/mcp-tools/managed-agent-tools.d.ts.map +1 -0
- package/dist/src/mcp-tools/managed-agent-tools.js +357 -0
- package/dist/src/mcp-tools/managed-agent-tools.js.map +1 -0
- package/dist/src/mcp-tools/memory-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/memory-tools.js +499 -68
- package/dist/src/mcp-tools/memory-tools.js.map +1 -1
- package/dist/src/mcp-tools/neural-tools.js +6 -6
- package/dist/src/mcp-tools/neural-tools.js.map +1 -1
- package/dist/src/mcp-tools/performance-tools.js +6 -6
- package/dist/src/mcp-tools/performance-tools.js.map +1 -1
- package/dist/src/mcp-tools/progress-tools.js +4 -4
- package/dist/src/mcp-tools/progress-tools.js.map +1 -1
- package/dist/src/mcp-tools/ruvllm-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/ruvllm-tools.js +27 -11
- package/dist/src/mcp-tools/ruvllm-tools.js.map +1 -1
- package/dist/src/mcp-tools/security-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/security-tools.js +34 -9
- package/dist/src/mcp-tools/security-tools.js.map +1 -1
- package/dist/src/mcp-tools/session-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/session-tools.js +130 -6
- package/dist/src/mcp-tools/session-tools.js.map +1 -1
- package/dist/src/mcp-tools/swarm-tools.d.ts +28 -0
- package/dist/src/mcp-tools/swarm-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/swarm-tools.js +80 -9
- package/dist/src/mcp-tools/swarm-tools.js.map +1 -1
- package/dist/src/mcp-tools/system-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/system-tools.js +91 -18
- package/dist/src/mcp-tools/system-tools.js.map +1 -1
- package/dist/src/mcp-tools/task-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/task-tools.js +55 -7
- package/dist/src/mcp-tools/task-tools.js.map +1 -1
- package/dist/src/mcp-tools/terminal-tools.js +5 -5
- package/dist/src/mcp-tools/terminal-tools.js.map +1 -1
- package/dist/src/mcp-tools/transfer-tools.js +11 -11
- package/dist/src/mcp-tools/transfer-tools.js.map +1 -1
- package/dist/src/mcp-tools/wasm-agent-tools.js +11 -11
- package/dist/src/mcp-tools/wasm-agent-tools.js.map +1 -1
- package/dist/src/mcp-tools/workflow-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/workflow-tools.js +118 -10
- package/dist/src/mcp-tools/workflow-tools.js.map +1 -1
- package/dist/src/memory/ann-router-registry.d.ts +61 -0
- package/dist/src/memory/ann-router-registry.d.ts.map +1 -0
- package/dist/src/memory/ann-router-registry.js +72 -0
- package/dist/src/memory/ann-router-registry.js.map +1 -0
- package/dist/src/memory/diskann-registry.d.ts +56 -0
- package/dist/src/memory/diskann-registry.d.ts.map +1 -0
- package/dist/src/memory/diskann-registry.js +88 -0
- package/dist/src/memory/diskann-registry.js.map +1 -0
- package/dist/src/memory/memory-bridge.d.ts +4 -0
- package/dist/src/memory/memory-bridge.d.ts.map +1 -1
- package/dist/src/memory/memory-bridge.js +49 -8
- package/dist/src/memory/memory-bridge.js.map +1 -1
- package/dist/src/memory/memory-initializer.d.ts +12 -0
- package/dist/src/memory/memory-initializer.d.ts.map +1 -1
- package/dist/src/memory/memory-initializer.js +98 -19
- package/dist/src/memory/memory-initializer.js.map +1 -1
- package/dist/src/memory/sona-optimizer.d.ts.map +1 -1
- package/dist/src/memory/sona-optimizer.js +3 -0
- package/dist/src/memory/sona-optimizer.js.map +1 -1
- package/dist/src/parser.d.ts +9 -0
- package/dist/src/parser.d.ts.map +1 -1
- package/dist/src/parser.js +11 -0
- package/dist/src/parser.js.map +1 -1
- package/dist/src/plugins/store/discovery.d.ts +15 -4
- package/dist/src/plugins/store/discovery.d.ts.map +1 -1
- package/dist/src/plugins/store/discovery.js +40 -18
- package/dist/src/plugins/store/discovery.js.map +1 -1
- package/dist/src/ruvector/agent-wasm.d.ts.map +1 -1
- package/dist/src/ruvector/agent-wasm.js +4 -1
- package/dist/src/ruvector/agent-wasm.js.map +1 -1
- package/dist/src/ruvector/coverage-tools.js +6 -6
- package/dist/src/ruvector/coverage-tools.js.map +1 -1
- package/dist/src/services/headless-worker-executor.d.ts +20 -1
- package/dist/src/services/headless-worker-executor.d.ts.map +1 -1
- package/dist/src/services/headless-worker-executor.js +91 -13
- package/dist/src/services/headless-worker-executor.js.map +1 -1
- package/dist/src/services/worker-daemon.d.ts +92 -2
- package/dist/src/services/worker-daemon.d.ts.map +1 -1
- package/dist/src/services/worker-daemon.js +431 -16
- package/dist/src/services/worker-daemon.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +12 -8
- package/.claude/agents/core/coder.md +0 -453
- package/.claude/agents/core/researcher.md +0 -369
- package/.claude/agents/core/reviewer.md +0 -520
- package/.claude/agents/core/tester.md +0 -512
- package/.claude/agents/goal/goal-planner.md +0 -73
- package/.claude/agents/v3/adr-architect.md +0 -184
- package/.claude/agents/v3/memory-specialist.md +0 -995
- package/.claude/agents/v3/security-auditor.md +0 -771
- package/.claude/agents/v3/sparc-orchestrator.md +0 -182
- package/.claude/commands/flow-nexus/app-store.md +0 -124
- package/.claude/commands/flow-nexus/challenges.md +0 -120
- package/.claude/commands/flow-nexus/login-registration.md +0 -65
- package/.claude/commands/flow-nexus/neural-network.md +0 -134
- package/.claude/commands/flow-nexus/payments.md +0 -116
- package/.claude/commands/flow-nexus/sandbox.md +0 -83
- package/.claude/commands/flow-nexus/swarm.md +0 -87
- package/.claude/commands/flow-nexus/user-tools.md +0 -152
- package/.claude/commands/flow-nexus/workflow.md +0 -115
- package/.claude/skills/agentic-jujutsu/SKILL.md +0 -645
- package/.claude/skills/aidefence-scan.md +0 -151
- package/.claude/skills/aidefence.yaml +0 -297
- package/.claude/skills/hive-mind-advanced/SKILL.md +0 -712
- package/.claude/skills/performance-analysis/SKILL.md +0 -563
- package/.claude/skills/secure-review.md +0 -181
- package/.claude/skills/worker-benchmarks/skill.md +0 -135
- package/.claude/skills/worker-integration/skill.md +0 -154
- package/dist/src/ruvector/flash-attention.d.ts +0 -195
- package/dist/src/ruvector/flash-attention.d.ts.map +0 -1
- package/dist/src/ruvector/flash-attention.js +0 -643
- package/dist/src/ruvector/flash-attention.js.map +0 -1
- package/dist/src/ruvector/moe-router.d.ts +0 -206
- package/dist/src/ruvector/moe-router.d.ts.map +0 -1
- package/dist/src/ruvector/moe-router.js +0 -626
- package/dist/src/ruvector/moe-router.js.map +0 -1
- package/dist/src/services/event-stream.d.ts +0 -25
- package/dist/src/services/event-stream.d.ts.map +0 -1
- package/dist/src/services/event-stream.js +0 -27
- package/dist/src/services/event-stream.js.map +0 -1
- package/dist/src/services/loop-worker-runner.d.ts +0 -16
- package/dist/src/services/loop-worker-runner.d.ts.map +0 -1
- package/dist/src/services/loop-worker-runner.js +0 -34
- package/dist/src/services/loop-worker-runner.js.map +0 -1
- package/dist/src/services/runtime-capabilities.d.ts +0 -22
- package/dist/src/services/runtime-capabilities.d.ts.map +0 -1
- package/dist/src/services/runtime-capabilities.js +0 -45
- package/dist/src/services/runtime-capabilities.js.map +0 -1
|
@@ -34,6 +34,9 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
34
34
|
config;
|
|
35
35
|
workers = new Map();
|
|
36
36
|
timers = new Map();
|
|
37
|
+
// #1845: separate timer for the MCP-dispatch queue poller. Kept off
|
|
38
|
+
// the per-worker map so stop() clears both kinds without confusion.
|
|
39
|
+
queuePollTimer;
|
|
37
40
|
running = false;
|
|
38
41
|
startedAt;
|
|
39
42
|
projectRoot;
|
|
@@ -54,7 +57,18 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
54
57
|
const fileConfig = this.readDaemonConfigFromFile(claudeFlowDir);
|
|
55
58
|
// CPU-proportional smart default instead of hardcoded 2.0
|
|
56
59
|
const cpuCount = WorkerDaemon.getEffectiveCpuCount();
|
|
57
|
-
|
|
60
|
+
let smartMaxCpuLoad = Math.max(cpuCount * 0.8, 2.0); // Floor of 2.0 for single-CPU machines
|
|
61
|
+
// #2110 — WSL2 reports `/proc/loadavg` values that include Windows-side
|
|
62
|
+
// process counts mapped into the Linux kernel. Real load on a 4-CPU
|
|
63
|
+
// WSL2 host can be 200-400 even when the Linux side is idle. The
|
|
64
|
+
// default gate of `cpuCount * 0.8` always trips, deferring every
|
|
65
|
+
// worker as "CPU load too high" while the daemon reports healthy.
|
|
66
|
+
// Bump the floor to 1000 when WSL is detected so the gate is
|
|
67
|
+
// effectively disabled (real load on Linux side rarely exceeds 100
|
|
68
|
+
// even under heavy contention).
|
|
69
|
+
if (WorkerDaemon.isWslEnvironment()) {
|
|
70
|
+
smartMaxCpuLoad = Math.max(smartMaxCpuLoad, 1000);
|
|
71
|
+
}
|
|
58
72
|
// Platform-aware default: macOS os.freemem() excludes reclaimable file cache,
|
|
59
73
|
// so reported "free" is much lower than actually available memory.
|
|
60
74
|
// Linux reports available memory (including reclaimable cache) more accurately.
|
|
@@ -76,6 +90,9 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
76
90
|
};
|
|
77
91
|
// Setup graceful shutdown handlers
|
|
78
92
|
this.setupShutdownHandlers();
|
|
93
|
+
// #1855: install crash handlers so uncaught exceptions and unhandled
|
|
94
|
+
// rejections don't leak the PID file or orphan child processes.
|
|
95
|
+
this.installCrashHandlers();
|
|
79
96
|
// Ensure directories exist
|
|
80
97
|
if (!existsSync(claudeFlowDir)) {
|
|
81
98
|
mkdirSync(claudeFlowDir, { recursive: true });
|
|
@@ -101,14 +118,19 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
101
118
|
this.headlessAvailable = await this.headlessExecutor.isAvailable();
|
|
102
119
|
if (this.headlessAvailable) {
|
|
103
120
|
this.log('info', 'Claude Code headless mode available - AI workers enabled');
|
|
104
|
-
// Forward headless executor events
|
|
121
|
+
// Forward headless executor events. #1855: also snapshot the
|
|
122
|
+
// active child PIDs to disk on every transition so the next
|
|
123
|
+
// lifetime can reap orphans after a hard crash.
|
|
105
124
|
this.headlessExecutor.on('execution:start', (data) => {
|
|
125
|
+
this.writeChildrenSnapshot();
|
|
106
126
|
this.emit('headless:start', data);
|
|
107
127
|
});
|
|
108
128
|
this.headlessExecutor.on('execution:complete', (data) => {
|
|
129
|
+
this.writeChildrenSnapshot();
|
|
109
130
|
this.emit('headless:complete', data);
|
|
110
131
|
});
|
|
111
132
|
this.headlessExecutor.on('execution:error', (data) => {
|
|
133
|
+
this.writeChildrenSnapshot();
|
|
112
134
|
this.emit('headless:error', data);
|
|
113
135
|
});
|
|
114
136
|
this.headlessExecutor.on('output', (data) => {
|
|
@@ -144,6 +166,28 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
144
166
|
* cgroup v2 / v1 quota files first so the maxCpuLoad threshold stays
|
|
145
167
|
* meaningful under resource-limited containers.
|
|
146
168
|
*/
|
|
169
|
+
/**
|
|
170
|
+
* #2110 — detect WSL2 / WSL1 so the CPU-load gate can use a sane
|
|
171
|
+
* default. `/proc/loadavg` on WSL maps in Windows-side process counts
|
|
172
|
+
* and routinely reports values 100-1000x larger than real Linux load.
|
|
173
|
+
*
|
|
174
|
+
* Detection order:
|
|
175
|
+
* 1. `WSL_DISTRO_NAME` env var (set by Microsoft's WSL launcher)
|
|
176
|
+
* 2. `WSL_INTEROP` env var (set by recent WSL2)
|
|
177
|
+
* 3. `/proc/sys/kernel/osrelease` contains "microsoft" or "WSL"
|
|
178
|
+
* (kernel build marker; survives env stripping)
|
|
179
|
+
*/
|
|
180
|
+
static isWslEnvironment() {
|
|
181
|
+
if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP)
|
|
182
|
+
return true;
|
|
183
|
+
try {
|
|
184
|
+
const osrelease = readFileSync('/proc/sys/kernel/osrelease', 'utf8').toLowerCase();
|
|
185
|
+
if (osrelease.includes('microsoft') || osrelease.includes('wsl'))
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
catch { /* not on Linux or /proc inaccessible */ }
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
147
191
|
static getEffectiveCpuCount() {
|
|
148
192
|
// 1. Try cgroup v2: /sys/fs/cgroup/cpu.max
|
|
149
193
|
try {
|
|
@@ -169,22 +213,52 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
169
213
|
return cpus().length || 1;
|
|
170
214
|
}
|
|
171
215
|
/**
|
|
172
|
-
* Read daemon-specific config from .claude-flow/config.json
|
|
173
|
-
* Supports dot-notation keys like 'daemon.resourceThresholds.maxCpuLoad'
|
|
216
|
+
* Read daemon-specific config from .claude-flow/config.{json,yaml,yml}.
|
|
217
|
+
* Supports dot-notation keys like 'daemon.resourceThresholds.maxCpuLoad'.
|
|
218
|
+
* #1844: prefer JSON when both exist (existing behavior) but fall back
|
|
219
|
+
* to YAML so operators using the v3 canonical YAML format aren't silently
|
|
220
|
+
* ignored. The chosen path is logged at info level.
|
|
174
221
|
*/
|
|
175
222
|
readDaemonConfigFromFile(claudeFlowDir) {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
223
|
+
const jsonPath = join(claudeFlowDir, 'config.json');
|
|
224
|
+
const yamlPath = join(claudeFlowDir, 'config.yaml');
|
|
225
|
+
const ymlPath = join(claudeFlowDir, 'config.yml');
|
|
226
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
227
|
+
let raw;
|
|
228
|
+
let chosenPath;
|
|
229
|
+
if (existsSync(jsonPath)) {
|
|
230
|
+
try {
|
|
231
|
+
raw = JSON.parse(readFileSync(jsonPath, 'utf-8'));
|
|
232
|
+
chosenPath = jsonPath;
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return {};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else if (existsSync(yamlPath) || existsSync(ymlPath)) {
|
|
239
|
+
const yPath = existsSync(yamlPath) ? yamlPath : ymlPath;
|
|
240
|
+
try {
|
|
241
|
+
// Lazy-load yaml so the daemon doesn't hard-require it; if the
|
|
242
|
+
// dep isn't installed, fall back to the previous warn-only path.
|
|
243
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
244
|
+
const yamlMod = require('yaml');
|
|
245
|
+
const parsed = yamlMod.parse(readFileSync(yPath, 'utf-8'));
|
|
246
|
+
if (parsed && typeof parsed === 'object') {
|
|
247
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
248
|
+
raw = parsed;
|
|
249
|
+
chosenPath = yPath;
|
|
250
|
+
}
|
|
183
251
|
}
|
|
252
|
+
catch {
|
|
253
|
+
this.log('warn', `Found ${yPath} but yaml parser unavailable. Install \`yaml\` or convert to JSON. Falling back to defaults.`);
|
|
254
|
+
return {};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (!raw || !chosenPath) {
|
|
184
258
|
return {};
|
|
185
259
|
}
|
|
260
|
+
this.log('info', `Daemon config loaded from ${chosenPath}`);
|
|
186
261
|
try {
|
|
187
|
-
const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
188
262
|
// Support both flat keys at root and nested under scopes.project
|
|
189
263
|
const cfg = raw?.scopes?.project ?? raw;
|
|
190
264
|
const rawCpuLoad = cfg['daemon.resourceThresholds.maxCpuLoad'] ?? raw['daemon.resourceThresholds.maxCpuLoad'];
|
|
@@ -216,6 +290,153 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
216
290
|
process.on('SIGINT', shutdown);
|
|
217
291
|
process.on('SIGHUP', shutdown);
|
|
218
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* #1855: install crash handlers for uncaught exceptions and unhandled
|
|
295
|
+
* rejections. Without these, a thrown error from any timer callback,
|
|
296
|
+
* worker logic path, or transitive import crashes the daemon process
|
|
297
|
+
* silently — the PID file leaks and any in-flight child processes
|
|
298
|
+
* orphan. With these, we log a structured crash record, run stop()
|
|
299
|
+
* to clean up, then exit 1 so the process actually dies (otherwise
|
|
300
|
+
* Node would crash anyway after the handler returns).
|
|
301
|
+
*/
|
|
302
|
+
installCrashHandlers() {
|
|
303
|
+
const onCrash = (kind, err) => {
|
|
304
|
+
// Best-effort logging; never throw from inside the crash handler.
|
|
305
|
+
try {
|
|
306
|
+
this.writeCrashRecord(kind, err);
|
|
307
|
+
}
|
|
308
|
+
catch { /* nothing more we can do */ }
|
|
309
|
+
try {
|
|
310
|
+
// Synchronous stop — don't await; the process is dying. Just
|
|
311
|
+
// remove the PID file and snapshot state so the next start
|
|
312
|
+
// sees a clean slate.
|
|
313
|
+
this.removePidFile();
|
|
314
|
+
this.saveState();
|
|
315
|
+
// Snapshot any in-flight child PIDs one last time so the next
|
|
316
|
+
// lifetime can reap them.
|
|
317
|
+
this.writeChildrenSnapshot();
|
|
318
|
+
}
|
|
319
|
+
catch { /* ignore */ }
|
|
320
|
+
// Exit non-zero so supervisors / shells see the failure.
|
|
321
|
+
process.exit(1);
|
|
322
|
+
};
|
|
323
|
+
process.on('uncaughtException', (err) => onCrash('uncaughtException', err));
|
|
324
|
+
process.on('unhandledRejection', (err) => onCrash('unhandledRejection', err));
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Append a structured crash record to .claude-flow/logs/crash.log.
|
|
328
|
+
* Inspectable by hand or via `ruflo daemon status` follow-ups.
|
|
329
|
+
*/
|
|
330
|
+
writeCrashRecord(kind, err) {
|
|
331
|
+
const logDir = this.config.logDir;
|
|
332
|
+
if (!existsSync(logDir))
|
|
333
|
+
mkdirSync(logDir, { recursive: true });
|
|
334
|
+
const crashLog = join(logDir, 'crash.log');
|
|
335
|
+
const ts = new Date().toISOString();
|
|
336
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
337
|
+
const stack = err instanceof Error && err.stack ? err.stack : '<no stack>';
|
|
338
|
+
const record = `[${ts}] [${kind}] pid=${process.pid} ${message}\n${stack}\n---\n`;
|
|
339
|
+
appendFileSync(crashLog, record, 'utf-8');
|
|
340
|
+
this.log('warn', `Daemon crashed (${kind}): ${message} — see ${crashLog}`);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Path to the on-disk children registry — list of headless worker
|
|
344
|
+
* child PIDs the daemon currently owns. #1855: written on every
|
|
345
|
+
* execution:start / :complete / :error transition; read by the next
|
|
346
|
+
* lifetime to reap orphans after a hard crash.
|
|
347
|
+
*/
|
|
348
|
+
get childrenFile() {
|
|
349
|
+
return join(this.projectRoot, '.claude-flow', 'daemon-children.json');
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* #1856: detect workers that were mid-flight when the previous daemon
|
|
353
|
+
* lifetime ended. A mid-flight worker has `lastStartedAt > lastRun`
|
|
354
|
+
* (started after the last successful completion). On crash recovery
|
|
355
|
+
* we count these as failures so the run-counter math stays consistent
|
|
356
|
+
* (`runCount === successCount + failureCount`). Workers naturally
|
|
357
|
+
* retry at their next scheduled interval; we deliberately don't
|
|
358
|
+
* immediately re-run because the failure may have been deterministic.
|
|
359
|
+
*/
|
|
360
|
+
detectMidFlightFailures() {
|
|
361
|
+
let detected = 0;
|
|
362
|
+
for (const [type, state] of this.workers.entries()) {
|
|
363
|
+
const startedAt = state.lastStartedAt?.getTime() ?? 0;
|
|
364
|
+
const lastRunAt = state.lastRun?.getTime() ?? 0;
|
|
365
|
+
// started after the last successful completion → was mid-flight
|
|
366
|
+
if (startedAt > 0 && startedAt > lastRunAt) {
|
|
367
|
+
state.failureCount++;
|
|
368
|
+
state.isRunning = false;
|
|
369
|
+
// Don't bump runCount — it was already incremented at start
|
|
370
|
+
this.log('info', `Worker ${type} was mid-flight at last crash (started ${state.lastStartedAt?.toISOString()}); counted as failure, will retry at next scheduled interval`);
|
|
371
|
+
detected++;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (detected > 0) {
|
|
375
|
+
this.saveState();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Snapshot the currently-active headless worker child PIDs to disk.
|
|
380
|
+
* Best-effort; failures don't propagate.
|
|
381
|
+
*/
|
|
382
|
+
writeChildrenSnapshot() {
|
|
383
|
+
if (!this.headlessExecutor)
|
|
384
|
+
return;
|
|
385
|
+
try {
|
|
386
|
+
const pids = this.headlessExecutor.getActiveChildPids();
|
|
387
|
+
const dir = join(this.projectRoot, '.claude-flow');
|
|
388
|
+
if (!existsSync(dir))
|
|
389
|
+
mkdirSync(dir, { recursive: true });
|
|
390
|
+
writeFileSync(this.childrenFile, JSON.stringify({ pids, daemonPid: process.pid, timestamp: new Date().toISOString() }, null, 2), 'utf-8');
|
|
391
|
+
}
|
|
392
|
+
catch { /* best-effort */ }
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* #1855: reap orphan headless worker children left behind by a
|
|
396
|
+
* previous crashed lifetime. Reads `.claude-flow/daemon-children.json`,
|
|
397
|
+
* SIGTERMs any PID still alive that doesn't belong to the current
|
|
398
|
+
* daemon, then truncates the file. Called at the top of `start()`
|
|
399
|
+
* so the next lifetime starts with a clean process tree.
|
|
400
|
+
*/
|
|
401
|
+
reapOrphanedChildren() {
|
|
402
|
+
const file = this.childrenFile;
|
|
403
|
+
if (!existsSync(file))
|
|
404
|
+
return;
|
|
405
|
+
let snapshot;
|
|
406
|
+
try {
|
|
407
|
+
snapshot = JSON.parse(readFileSync(file, 'utf-8'));
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
try {
|
|
411
|
+
unlinkSync(file);
|
|
412
|
+
}
|
|
413
|
+
catch { /* ignore */ }
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const pids = Array.isArray(snapshot.pids) ? snapshot.pids : [];
|
|
417
|
+
let reaped = 0;
|
|
418
|
+
for (const pid of pids) {
|
|
419
|
+
if (typeof pid !== 'number' || pid <= 0)
|
|
420
|
+
continue;
|
|
421
|
+
if (pid === process.pid)
|
|
422
|
+
continue; // never our own PID
|
|
423
|
+
try {
|
|
424
|
+
process.kill(pid, 0); // is alive?
|
|
425
|
+
process.kill(pid, 'SIGTERM');
|
|
426
|
+
reaped++;
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
// already dead — fine
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (reaped > 0) {
|
|
433
|
+
this.log('info', `Reaped ${reaped} orphan headless worker child(ren) from previous lifetime`);
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
unlinkSync(file);
|
|
437
|
+
}
|
|
438
|
+
catch { /* ignore */ }
|
|
439
|
+
}
|
|
219
440
|
/**
|
|
220
441
|
* Check if system resources allow worker execution
|
|
221
442
|
*/
|
|
@@ -299,12 +520,14 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
299
520
|
for (const [type, state] of Object.entries(saved.workers)) {
|
|
300
521
|
const savedState = state;
|
|
301
522
|
const lastRunValue = savedState.lastRun;
|
|
523
|
+
const lastStartedAtValue = savedState.lastStartedAt;
|
|
302
524
|
this.workers.set(type, {
|
|
303
525
|
runCount: savedState.runCount || 0,
|
|
304
526
|
successCount: savedState.successCount || 0,
|
|
305
527
|
failureCount: savedState.failureCount || 0,
|
|
306
528
|
averageDurationMs: savedState.averageDurationMs || 0,
|
|
307
529
|
lastRun: lastRunValue ? new Date(lastRunValue) : undefined,
|
|
530
|
+
lastStartedAt: lastStartedAtValue ? new Date(lastStartedAtValue) : undefined,
|
|
308
531
|
nextRun: undefined,
|
|
309
532
|
isRunning: false,
|
|
310
533
|
});
|
|
@@ -337,6 +560,15 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
337
560
|
/**
|
|
338
561
|
* Check if another daemon instance is already running.
|
|
339
562
|
* Returns the existing PID if alive, or null if no daemon is running.
|
|
563
|
+
*
|
|
564
|
+
* #1853: ignore self-PID matches. The detached-spawn path in
|
|
565
|
+
* `commands/daemon.ts` writes the child's PID into the file as a
|
|
566
|
+
* fallback after a 500ms wait. If the child reaches `start()` slower
|
|
567
|
+
* than the parent's 500ms wait (observed on Node 25 / macOS 26), the
|
|
568
|
+
* child reads its own PID back from the file and concludes "another
|
|
569
|
+
* daemon is already running" — so it exits before scheduling workers
|
|
570
|
+
* and `daemon status` reports STOPPED forever. A daemon process is
|
|
571
|
+
* never "another instance" of itself; treat self-match as absence.
|
|
340
572
|
*/
|
|
341
573
|
checkExistingDaemon() {
|
|
342
574
|
if (!existsSync(this.pidFile))
|
|
@@ -345,6 +577,10 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
345
577
|
const pid = parseInt(readFileSync(this.pidFile, 'utf-8').trim(), 10);
|
|
346
578
|
if (isNaN(pid))
|
|
347
579
|
return null;
|
|
580
|
+
// #1853: a PID file containing our own PID is not "another daemon".
|
|
581
|
+
// Treat as absent so the start() path proceeds normally.
|
|
582
|
+
if (pid === process.pid)
|
|
583
|
+
return null;
|
|
348
584
|
// Check if process is alive (signal 0 = existence check)
|
|
349
585
|
process.kill(pid, 0);
|
|
350
586
|
return pid; // Process is alive
|
|
@@ -391,6 +627,18 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
391
627
|
this.emit('warning', `Daemon already running (PID: ${existingPid})`);
|
|
392
628
|
return;
|
|
393
629
|
}
|
|
630
|
+
// #1855: reap orphan headless worker children left by a previous
|
|
631
|
+
// crashed lifetime, BEFORE we mark ourselves running and start
|
|
632
|
+
// accepting new work. The children file from the prior daemon's
|
|
633
|
+
// last-snapshot is the authoritative list.
|
|
634
|
+
this.reapOrphanedChildren();
|
|
635
|
+
// #1856: detect workers that were mid-flight at the previous crash
|
|
636
|
+
// and count them as failures so runCount/successCount/failureCount
|
|
637
|
+
// stay consistent. Workers retry naturally at their next scheduled
|
|
638
|
+
// interval — we don't immediately re-run them, which avoids a
|
|
639
|
+
// freshly-recovered daemon hammering the same code path that just
|
|
640
|
+
// killed it.
|
|
641
|
+
this.detectMidFlightFailures();
|
|
394
642
|
this.running = true;
|
|
395
643
|
this.startedAt = new Date();
|
|
396
644
|
this.writePidFile();
|
|
@@ -401,10 +649,90 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
401
649
|
this.scheduleWorker(workerConfig);
|
|
402
650
|
}
|
|
403
651
|
}
|
|
652
|
+
// #1845: poll the MCP-dispatch queue directory so workers requested
|
|
653
|
+
// via mcp__hooks_worker-dispatch (in a separate process) actually
|
|
654
|
+
// execute here. Previously the dispatch wrote to a process-local Map
|
|
655
|
+
// that the daemon could never see.
|
|
656
|
+
this.queuePollTimer = setInterval(() => {
|
|
657
|
+
void this.processDispatchQueue();
|
|
658
|
+
}, 5_000);
|
|
659
|
+
if (typeof this.queuePollTimer.unref === 'function') {
|
|
660
|
+
this.queuePollTimer.unref();
|
|
661
|
+
}
|
|
404
662
|
// Save state
|
|
405
663
|
this.saveState();
|
|
406
664
|
this.log('info', `Daemon started (PID: ${process.pid}, CPUs: ${cpus().length}, workers: ${this.config.workers.filter(w => w.enabled).length}, maxCpuLoad: ${this.config.resourceThresholds.maxCpuLoad}, minFreeMemoryPercent: ${this.config.resourceThresholds.minFreeMemoryPercent}%)`);
|
|
407
665
|
}
|
|
666
|
+
/**
|
|
667
|
+
* #1845: ingest queue entries written by mcp__hooks_worker-dispatch.
|
|
668
|
+
* Each entry is a JSON file at `.claude-flow/daemon-queue/<id>.json`
|
|
669
|
+
* with `{ workerId, trigger, context, enqueuedAt }`. We move processed
|
|
670
|
+
* files to `.claude-flow/daemon-queue/.processed/` so the daemon never
|
|
671
|
+
* re-runs the same dispatch and operators can inspect history.
|
|
672
|
+
*/
|
|
673
|
+
async processDispatchQueue() {
|
|
674
|
+
if (!this.running)
|
|
675
|
+
return;
|
|
676
|
+
const queueDir = join(this.projectRoot, '.claude-flow', 'daemon-queue');
|
|
677
|
+
if (!existsSync(queueDir))
|
|
678
|
+
return;
|
|
679
|
+
let entries;
|
|
680
|
+
try {
|
|
681
|
+
const fs = await import('fs');
|
|
682
|
+
entries = fs.readdirSync(queueDir).filter((n) => n.endsWith('.json'));
|
|
683
|
+
}
|
|
684
|
+
catch {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
if (entries.length === 0)
|
|
688
|
+
return;
|
|
689
|
+
const fs = await import('fs');
|
|
690
|
+
const processedDir = join(queueDir, '.processed');
|
|
691
|
+
if (!existsSync(processedDir)) {
|
|
692
|
+
try {
|
|
693
|
+
fs.mkdirSync(processedDir, { recursive: true });
|
|
694
|
+
}
|
|
695
|
+
catch { /* race ok */ }
|
|
696
|
+
}
|
|
697
|
+
for (const entry of entries) {
|
|
698
|
+
const src = join(queueDir, entry);
|
|
699
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
700
|
+
let payload;
|
|
701
|
+
try {
|
|
702
|
+
payload = JSON.parse(fs.readFileSync(src, 'utf-8'));
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
// Malformed entry — quarantine so we don't loop on it
|
|
706
|
+
try {
|
|
707
|
+
fs.renameSync(src, join(processedDir, `bad-${entry}`));
|
|
708
|
+
}
|
|
709
|
+
catch { /* nothing more we can do */ }
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
const trigger = payload?.trigger;
|
|
713
|
+
const workerId = payload?.workerId;
|
|
714
|
+
if (!trigger || !this.config.workers.some((w) => w.type === trigger)) {
|
|
715
|
+
try {
|
|
716
|
+
fs.renameSync(src, join(processedDir, `unknown-${entry}`));
|
|
717
|
+
}
|
|
718
|
+
catch { /* ok */ }
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
try {
|
|
722
|
+
this.log('info', `Dequeued ${trigger}${workerId ? ` (id=${workerId})` : ''} from MCP dispatch queue`);
|
|
723
|
+
await this.triggerWorker(trigger);
|
|
724
|
+
}
|
|
725
|
+
catch (err) {
|
|
726
|
+
this.log('warn', `Queued worker ${trigger} failed: ${err.message}`);
|
|
727
|
+
}
|
|
728
|
+
finally {
|
|
729
|
+
try {
|
|
730
|
+
fs.renameSync(src, join(processedDir, entry));
|
|
731
|
+
}
|
|
732
|
+
catch { /* ignore */ }
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
408
736
|
/**
|
|
409
737
|
* Stop the daemon and all workers
|
|
410
738
|
*/
|
|
@@ -420,6 +748,11 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
420
748
|
this.log('info', `Stopped worker: ${type}`);
|
|
421
749
|
}
|
|
422
750
|
this.timers.clear();
|
|
751
|
+
// #1845: stop the MCP-dispatch queue poller too.
|
|
752
|
+
if (this.queuePollTimer) {
|
|
753
|
+
clearInterval(this.queuePollTimer);
|
|
754
|
+
this.queuePollTimer = undefined;
|
|
755
|
+
}
|
|
423
756
|
this.running = false;
|
|
424
757
|
this.removePidFile();
|
|
425
758
|
this.saveState();
|
|
@@ -500,6 +833,8 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
500
833
|
// Track running worker
|
|
501
834
|
this.runningWorkers.add(workerConfig.type);
|
|
502
835
|
state.isRunning = true;
|
|
836
|
+
state.lastStartedAt = new Date(); // #1856: timestamp the start
|
|
837
|
+
this.saveState(); // persist before we run anything
|
|
503
838
|
this.emit('worker:start', { workerId, type: workerConfig.type });
|
|
504
839
|
this.log('info', `Starting worker: ${workerConfig.type} (${this.runningWorkers.size}/${this.config.maxConcurrent} concurrent)`);
|
|
505
840
|
try {
|
|
@@ -607,10 +942,42 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
607
942
|
try {
|
|
608
943
|
this.log('info', `Running ${workerConfig.type} in headless mode (Claude Code AI)`);
|
|
609
944
|
const result = await this.headlessExecutor.execute(workerConfig.type);
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
945
|
+
// #2110 — `HeadlessWorkerExecutor.execute()` returns
|
|
946
|
+
// `createErrorResult(...)` with `success: false` when
|
|
947
|
+
// `isAvailable()` is false, instead of throwing. The previous
|
|
948
|
+
// try/catch never fired in that path, and the result was
|
|
949
|
+
// persisted as mode:"headless" despite being a stub. Downstream
|
|
950
|
+
// dashboards / `memory stats` couldn't distinguish a real AI
|
|
951
|
+
// run from a fallback. Treat falsy success the same as throw.
|
|
952
|
+
const ok = result?.success === true;
|
|
953
|
+
if (!ok) {
|
|
954
|
+
const reason = result?.error ||
|
|
955
|
+
result?.note ||
|
|
956
|
+
'headless executor reported success=false';
|
|
957
|
+
this.log('warn', `Headless ${workerConfig.type} returned success=false (${String(reason).slice(0, 200)}); falling back to local mode`);
|
|
958
|
+
this.emit('headless:fallback', {
|
|
959
|
+
type: workerConfig.type,
|
|
960
|
+
error: String(reason).slice(0, 500),
|
|
961
|
+
});
|
|
962
|
+
// Fall through to local switch.
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
// #1793: persist the headless result to the same metrics files the
|
|
966
|
+
// local workers write to. Without this, AI-mode runs produced rich
|
|
967
|
+
// parsedOutput that lived only in `.claude-flow/logs/headless/*` and
|
|
968
|
+
// never reached `.claude-flow/metrics/<name>.json` — `memory stats`
|
|
969
|
+
// and downstream consumers saw nothing despite successful runs.
|
|
970
|
+
try {
|
|
971
|
+
this.persistHeadlessResult(workerConfig.type, result);
|
|
972
|
+
}
|
|
973
|
+
catch (persistError) {
|
|
974
|
+
this.log('warn', `Failed to persist headless result for ${workerConfig.type}: ${persistError.message}`);
|
|
975
|
+
}
|
|
976
|
+
return {
|
|
977
|
+
mode: 'headless',
|
|
978
|
+
...result,
|
|
979
|
+
};
|
|
980
|
+
}
|
|
614
981
|
}
|
|
615
982
|
catch (error) {
|
|
616
983
|
this.log('warn', `Headless execution failed for ${workerConfig.type}, falling back to local mode`);
|
|
@@ -651,6 +1018,53 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
651
1018
|
return { status: 'unknown worker type', mode: 'local' };
|
|
652
1019
|
}
|
|
653
1020
|
}
|
|
1021
|
+
/**
|
|
1022
|
+
* #1793: persist a headless worker result to the same metrics file the
|
|
1023
|
+
* local fallback writes to. Without this, AI-mode workers produced rich
|
|
1024
|
+
* structured output (audit findings, perf signals, test-gap analysis)
|
|
1025
|
+
* that lived only in `.claude-flow/logs/headless/*_result.log` and was
|
|
1026
|
+
* invisible to `npx ruflo memory stats` or the metrics consumers.
|
|
1027
|
+
*
|
|
1028
|
+
* The mapping mirrors the `*Local` worker implementations below so a
|
|
1029
|
+
* single consumer path works regardless of execution mode.
|
|
1030
|
+
*/
|
|
1031
|
+
persistHeadlessResult(workerType, result) {
|
|
1032
|
+
const metricsDir = join(this.projectRoot, '.claude-flow', 'metrics');
|
|
1033
|
+
if (!existsSync(metricsDir))
|
|
1034
|
+
mkdirSync(metricsDir, { recursive: true });
|
|
1035
|
+
// Filename mirrors the local-mode worker writes (security-audit.json,
|
|
1036
|
+
// performance.json, test-gaps.json) so a downstream reader doesn't
|
|
1037
|
+
// care which mode produced the data.
|
|
1038
|
+
const filenameMap = {
|
|
1039
|
+
audit: 'security-audit.json',
|
|
1040
|
+
optimize: 'performance.json',
|
|
1041
|
+
testgaps: 'test-gaps.json',
|
|
1042
|
+
document: 'documentation.json',
|
|
1043
|
+
refactor: 'refactor.json',
|
|
1044
|
+
deepdive: 'deepdive.json',
|
|
1045
|
+
ultralearn: 'ultralearn.json',
|
|
1046
|
+
predict: 'predictions.json',
|
|
1047
|
+
};
|
|
1048
|
+
const filename = filenameMap[workerType] ?? `${workerType}.json`;
|
|
1049
|
+
const metricsFile = join(metricsDir, filename);
|
|
1050
|
+
const persisted = {
|
|
1051
|
+
timestamp: result.timestamp instanceof Date ? result.timestamp.toISOString() : new Date().toISOString(),
|
|
1052
|
+
mode: 'headless',
|
|
1053
|
+
workerType,
|
|
1054
|
+
model: result.model,
|
|
1055
|
+
durationMs: result.durationMs,
|
|
1056
|
+
tokensUsed: result.tokensUsed,
|
|
1057
|
+
executionId: result.executionId,
|
|
1058
|
+
success: result.success,
|
|
1059
|
+
// Structured findings live here when the worker emits JSON (e.g. the
|
|
1060
|
+
// audit worker's vulnerability list). Fall back to a raw-output
|
|
1061
|
+
// pointer so consumers can still locate the full log.
|
|
1062
|
+
findings: result.parsedOutput ?? null,
|
|
1063
|
+
rawOutputPreview: typeof result.output === 'string' ? result.output.slice(0, 2000) : undefined,
|
|
1064
|
+
rawOutputLength: typeof result.output === 'string' ? result.output.length : 0,
|
|
1065
|
+
};
|
|
1066
|
+
writeFileSync(metricsFile, JSON.stringify(persisted, null, 2));
|
|
1067
|
+
}
|
|
654
1068
|
// Worker implementations
|
|
655
1069
|
async runMapWorker() {
|
|
656
1070
|
// Scan project structure and update metrics
|
|
@@ -893,6 +1307,7 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
893
1307
|
{
|
|
894
1308
|
...state,
|
|
895
1309
|
lastRun: state.lastRun?.toISOString(),
|
|
1310
|
+
lastStartedAt: state.lastStartedAt?.toISOString(),
|
|
896
1311
|
nextRun: state.nextRun?.toISOString(),
|
|
897
1312
|
}
|
|
898
1313
|
])),
|