@claude-flow/cli 3.32.3 → 3.32.5
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/helpers/helpers.manifest.json +2 -2
- package/catalog-manifest.json +2 -2
- package/dist/src/agenticow/speculative-exploration.d.ts +148 -0
- package/dist/src/agenticow/speculative-exploration.js +218 -0
- package/dist/src/appliance/gguf-engine.d.ts +91 -0
- package/dist/src/appliance/gguf-engine.js +425 -0
- package/dist/src/appliance/ruvllm-bridge.d.ts +102 -0
- package/dist/src/appliance/ruvllm-bridge.js +292 -0
- package/dist/src/appliance/rvfa-builder.d.ts +44 -0
- package/dist/src/appliance/rvfa-builder.js +329 -0
- package/dist/src/appliance/rvfa-distribution.d.ts +97 -0
- package/dist/src/appliance/rvfa-distribution.js +370 -0
- package/dist/src/appliance/rvfa-format.d.ts +111 -0
- package/dist/src/appliance/rvfa-format.js +393 -0
- package/dist/src/appliance/rvfa-runner.d.ts +69 -0
- package/dist/src/appliance/rvfa-runner.js +237 -0
- package/dist/src/appliance/rvfa-signing.d.ts +123 -0
- package/dist/src/appliance/rvfa-signing.js +347 -0
- package/dist/src/auth/client.d.ts +89 -0
- package/dist/src/auth/client.js +242 -0
- package/dist/src/auth/constants.d.ts +7 -0
- package/dist/src/auth/constants.js +7 -0
- package/dist/src/auth/scopes.d.ts +14 -0
- package/dist/src/auth/scopes.js +21 -0
- package/dist/src/auth/security-bridge.d.ts +36 -0
- package/dist/src/auth/security-bridge.js +42 -0
- package/dist/src/auth/session.d.ts +20 -0
- package/dist/src/auth/session.js +32 -0
- package/dist/src/auth/state.d.ts +19 -0
- package/dist/src/auth/state.js +53 -0
- package/dist/src/auth/types.d.ts +27 -0
- package/dist/src/auth/types.js +11 -0
- package/dist/src/autopilot-state.d.ts +77 -0
- package/dist/src/autopilot-state.js +271 -0
- package/dist/src/benchmarks/gaia-agent-planning.smoke.d.ts +18 -0
- package/dist/src/benchmarks/gaia-agent-planning.smoke.js +253 -0
- package/dist/src/benchmarks/gaia-agent.d.ts +198 -0
- package/dist/src/benchmarks/gaia-agent.js +651 -0
- package/dist/src/benchmarks/gaia-causal-memory.d.ts +133 -0
- package/dist/src/benchmarks/gaia-causal-memory.js +281 -0
- package/dist/src/benchmarks/gaia-causal-memory.smoke.d.ts +22 -0
- package/dist/src/benchmarks/gaia-causal-memory.smoke.js +300 -0
- package/dist/src/benchmarks/gaia-convergence.d.ts +138 -0
- package/dist/src/benchmarks/gaia-convergence.js +260 -0
- package/dist/src/benchmarks/gaia-convergence.smoke.d.ts +19 -0
- package/dist/src/benchmarks/gaia-convergence.smoke.js +246 -0
- package/dist/src/benchmarks/gaia-critic.d.ts +123 -0
- package/dist/src/benchmarks/gaia-critic.js +312 -0
- package/dist/src/benchmarks/gaia-critic.smoke.d.ts +21 -0
- package/dist/src/benchmarks/gaia-critic.smoke.js +327 -0
- package/dist/src/benchmarks/gaia-decomposer.d.ts +125 -0
- package/dist/src/benchmarks/gaia-decomposer.js +350 -0
- package/dist/src/benchmarks/gaia-decomposer.smoke.d.ts +21 -0
- package/dist/src/benchmarks/gaia-decomposer.smoke.js +228 -0
- package/dist/src/benchmarks/gaia-e2e-smoke.d.ts +27 -0
- package/dist/src/benchmarks/gaia-e2e-smoke.js +136 -0
- package/dist/src/benchmarks/gaia-extract.smoke.d.ts +45 -0
- package/dist/src/benchmarks/gaia-extract.smoke.js +242 -0
- package/dist/src/benchmarks/gaia-hardness/features.d.ts +46 -0
- package/dist/src/benchmarks/gaia-hardness/features.js +170 -0
- package/dist/src/benchmarks/gaia-hardness/predictor.d.ts +105 -0
- package/dist/src/benchmarks/gaia-hardness/predictor.js +260 -0
- package/dist/src/benchmarks/gaia-hardness/predictor.smoke.d.ts +20 -0
- package/dist/src/benchmarks/gaia-hardness/predictor.smoke.js +235 -0
- package/dist/src/benchmarks/gaia-hardness/train-data-loader.d.ts +51 -0
- package/dist/src/benchmarks/gaia-hardness/train-data-loader.js +179 -0
- package/dist/src/benchmarks/gaia-judge.d.ts +88 -0
- package/dist/src/benchmarks/gaia-judge.js +437 -0
- package/dist/src/benchmarks/gaia-loader.d.ts +87 -0
- package/dist/src/benchmarks/gaia-loader.js +326 -0
- package/dist/src/benchmarks/gaia-tools/file_read.d.ts +35 -0
- package/dist/src/benchmarks/gaia-tools/file_read.js +403 -0
- package/dist/src/benchmarks/gaia-tools/grounded_query.d.ts +126 -0
- package/dist/src/benchmarks/gaia-tools/grounded_query.js +225 -0
- package/dist/src/benchmarks/gaia-tools/index.d.ts +32 -0
- package/dist/src/benchmarks/gaia-tools/index.js +36 -0
- package/dist/src/benchmarks/gaia-tools/types.d.ts +62 -0
- package/dist/src/benchmarks/gaia-tools/types.js +12 -0
- package/dist/src/benchmarks/gaia-tools/web_search.d.ts +30 -0
- package/dist/src/benchmarks/gaia-tools/web_search.js +210 -0
- package/dist/src/benchmarks/gaia-voting.d.ts +88 -0
- package/dist/src/benchmarks/gaia-voting.js +297 -0
- package/dist/src/benchmarks/gaia-voting.smoke.d.ts +20 -0
- package/dist/src/benchmarks/gaia-voting.smoke.js +332 -0
- package/dist/src/benchmarks/pretrain/index.d.ts +58 -0
- package/dist/src/benchmarks/pretrain/index.js +404 -0
- package/dist/src/business-pods/bbs-budget-tracker.d.ts +139 -0
- package/dist/src/business-pods/bbs-budget-tracker.js +358 -0
- package/dist/src/business-pods/domain-affinity-policy.d.ts +47 -0
- package/dist/src/business-pods/domain-affinity-policy.js +65 -0
- package/dist/src/business-pods/pod-schema.d.ts +96 -0
- package/dist/src/business-pods/pod-schema.js +225 -0
- package/dist/src/commands/advisor.d.ts +15 -0
- package/dist/src/commands/advisor.js +94 -0
- package/dist/src/commands/agent-wasm.d.ts +14 -0
- package/dist/src/commands/agent-wasm.js +333 -0
- package/dist/src/commands/agent.d.ts +8 -0
- package/dist/src/commands/agent.js +927 -0
- package/dist/src/commands/analyze.d.ts +19 -0
- package/dist/src/commands/analyze.js +2047 -0
- package/dist/src/commands/announcements.d.ts +17 -0
- package/dist/src/commands/announcements.js +0 -0
- package/dist/src/commands/appliance-advanced.d.ts +9 -0
- package/dist/src/commands/appliance-advanced.js +215 -0
- package/dist/src/commands/appliance.d.ts +8 -0
- package/dist/src/commands/appliance.js +404 -0
- package/dist/src/commands/auth.d.ts +15 -0
- package/dist/src/commands/auth.js +244 -0
- package/dist/src/commands/autopilot.d.ts +15 -0
- package/dist/src/commands/autopilot.js +407 -0
- package/dist/src/commands/benchmark.d.ts +10 -0
- package/dist/src/commands/benchmark.js +460 -0
- package/dist/src/commands/claims.d.ts +10 -0
- package/dist/src/commands/claims.js +620 -0
- package/dist/src/commands/cleanup.d.ts +13 -0
- package/dist/src/commands/cleanup.js +250 -0
- package/dist/src/commands/completions.d.ts +10 -0
- package/dist/src/commands/completions.js +539 -0
- package/dist/src/commands/config.d.ts +8 -0
- package/dist/src/commands/config.js +428 -0
- package/dist/src/commands/daemon.d.ts +59 -0
- package/dist/src/commands/daemon.js +1733 -0
- package/dist/src/commands/deployment.d.ts +10 -0
- package/dist/src/commands/deployment.js +672 -0
- package/dist/src/commands/doctor.d.ts +10 -0
- package/dist/src/commands/doctor.js +1902 -0
- package/dist/src/commands/eject.d.ts +33 -0
- package/dist/src/commands/eject.js +195 -0
- package/dist/src/commands/embeddings.d.ts +18 -0
- package/dist/src/commands/embeddings.js +1623 -0
- package/dist/src/commands/funnel.d.ts +18 -0
- package/dist/src/commands/funnel.js +302 -0
- package/dist/src/commands/gaia-bench.d.ts +40 -0
- package/dist/src/commands/gaia-bench.js +597 -0
- package/dist/src/commands/guidance.d.ts +8 -0
- package/dist/src/commands/guidance.js +556 -0
- package/dist/src/commands/hive-mind.d.ts +11 -0
- package/dist/src/commands/hive-mind.js +1319 -0
- package/dist/src/commands/hooks.d.ts +8 -0
- package/dist/src/commands/hooks.js +4606 -0
- package/dist/src/commands/index.d.ts +118 -0
- package/dist/src/commands/index.js +366 -0
- package/dist/src/commands/init.d.ts +13 -0
- package/dist/src/commands/init.js +1297 -0
- package/dist/src/commands/issues.d.ts +21 -0
- package/dist/src/commands/issues.js +567 -0
- package/dist/src/commands/mcp.d.ts +11 -0
- package/dist/src/commands/mcp.js +732 -0
- package/dist/src/commands/memory-backup.d.ts +11 -0
- package/dist/src/commands/memory-backup.js +46 -0
- package/dist/src/commands/memory-distill.d.ts +27 -0
- package/dist/src/commands/memory-distill.js +374 -0
- package/dist/src/commands/memory.d.ts +8 -0
- package/dist/src/commands/memory.js +1596 -0
- package/dist/src/commands/metaharness.d.ts +39 -0
- package/dist/src/commands/metaharness.js +215 -0
- package/dist/src/commands/migrate.d.ts +8 -0
- package/dist/src/commands/migrate.js +742 -0
- package/dist/src/commands/neural.d.ts +10 -0
- package/dist/src/commands/neural.js +4331 -0
- package/dist/src/commands/performance.d.ts +10 -0
- package/dist/src/commands/performance.js +583 -0
- package/dist/src/commands/plugins.d.ts +11 -0
- package/dist/src/commands/plugins.js +826 -0
- package/dist/src/commands/process.d.ts +10 -0
- package/dist/src/commands/process.js +694 -0
- package/dist/src/commands/progress.d.ts +11 -0
- package/dist/src/commands/progress.js +259 -0
- package/dist/src/commands/providers.d.ts +10 -0
- package/dist/src/commands/providers.js +502 -0
- package/dist/src/commands/proxy-lifecycle.d.ts +20 -0
- package/dist/src/commands/proxy-lifecycle.js +277 -0
- package/dist/src/commands/proxy.d.ts +21 -0
- package/dist/src/commands/proxy.js +403 -0
- package/dist/src/commands/route.d.ts +16 -0
- package/dist/src/commands/route.js +822 -0
- package/dist/src/commands/ruvector/backup.d.ts +11 -0
- package/dist/src/commands/ruvector/backup.js +747 -0
- package/dist/src/commands/ruvector/benchmark.d.ts +11 -0
- package/dist/src/commands/ruvector/benchmark.js +490 -0
- package/dist/src/commands/ruvector/import.d.ts +18 -0
- package/dist/src/commands/ruvector/import.js +373 -0
- package/dist/src/commands/ruvector/index.d.ts +29 -0
- package/dist/src/commands/ruvector/index.js +129 -0
- package/dist/src/commands/ruvector/init.d.ts +11 -0
- package/dist/src/commands/ruvector/init.js +467 -0
- package/dist/src/commands/ruvector/migrate.d.ts +11 -0
- package/dist/src/commands/ruvector/migrate.js +498 -0
- package/dist/src/commands/ruvector/optimize.d.ts +11 -0
- package/dist/src/commands/ruvector/optimize.js +505 -0
- package/dist/src/commands/ruvector/pg-utils.d.ts +14 -0
- package/dist/src/commands/ruvector/pg-utils.js +41 -0
- package/dist/src/commands/ruvector/setup.d.ts +18 -0
- package/dist/src/commands/ruvector/setup.js +765 -0
- package/dist/src/commands/ruvector/status.d.ts +11 -0
- package/dist/src/commands/ruvector/status.js +479 -0
- package/dist/src/commands/security.d.ts +10 -0
- package/dist/src/commands/security.js +1012 -0
- package/dist/src/commands/session.d.ts +8 -0
- package/dist/src/commands/session.js +757 -0
- package/dist/src/commands/settings.d.ts +19 -0
- package/dist/src/commands/settings.js +180 -0
- package/dist/src/commands/spinner.d.ts +16 -0
- package/dist/src/commands/spinner.js +329 -0
- package/dist/src/commands/start.d.ts +8 -0
- package/dist/src/commands/start.js +418 -0
- package/dist/src/commands/status.d.ts +8 -0
- package/dist/src/commands/status.js +608 -0
- package/dist/src/commands/swarm.d.ts +8 -0
- package/dist/src/commands/swarm.js +891 -0
- package/dist/src/commands/task.d.ts +8 -0
- package/dist/src/commands/task.js +675 -0
- package/dist/src/commands/transfer-store.d.ts +13 -0
- package/dist/src/commands/transfer-store.js +428 -0
- package/dist/src/commands/update.d.ts +8 -0
- package/dist/src/commands/update.js +276 -0
- package/dist/src/commands/verify.d.ts +19 -0
- package/dist/src/commands/verify.js +261 -0
- package/dist/src/commands/version.d.ts +42 -0
- package/dist/src/commands/version.js +106 -0
- package/dist/src/commands/workflow.d.ts +8 -0
- package/dist/src/commands/workflow.js +617 -0
- package/dist/src/config/harness-feedback-applier.d.ts +50 -0
- package/dist/src/config/harness-feedback-applier.js +122 -0
- package/dist/src/config/proven-config-refresh.d.ts +39 -0
- package/dist/src/config/proven-config-refresh.js +154 -0
- package/dist/src/config/proven-config-rvfa.d.ts +23 -0
- package/dist/src/config/proven-config-rvfa.js +73 -0
- package/dist/src/config/proven-config.d.ts +86 -0
- package/dist/src/config/proven-config.js +176 -0
- package/dist/src/config-adapter.d.ts +15 -0
- package/dist/src/config-adapter.js +186 -0
- package/dist/src/encryption/vault.d.ts +94 -0
- package/dist/src/encryption/vault.js +172 -0
- package/dist/src/fs-secure.d.ts +86 -0
- package/dist/src/fs-secure.js +133 -0
- package/dist/src/funnel/advisor-tip.d.ts +58 -0
- package/dist/src/funnel/advisor-tip.js +92 -0
- package/dist/src/funnel/attribution.d.ts +37 -0
- package/dist/src/funnel/attribution.js +101 -0
- package/dist/src/funnel/consent.d.ts +22 -0
- package/dist/src/funnel/consent.js +58 -0
- package/dist/src/funnel/credit-errors.d.ts +31 -0
- package/dist/src/funnel/credit-errors.js +88 -0
- package/dist/src/funnel/credit-notifier.d.ts +44 -0
- package/dist/src/funnel/credit-notifier.js +74 -0
- package/dist/src/funnel/disclosure.d.ts +47 -0
- package/dist/src/funnel/disclosure.js +109 -0
- package/dist/src/funnel/enrollment.d.ts +36 -0
- package/dist/src/funnel/enrollment.js +64 -0
- package/dist/src/funnel/environment.d.ts +17 -0
- package/dist/src/funnel/environment.js +39 -0
- package/dist/src/funnel/event-transport.d.ts +51 -0
- package/dist/src/funnel/event-transport.js +199 -0
- package/dist/src/funnel/events.d.ts +42 -0
- package/dist/src/funnel/events.js +150 -0
- package/dist/src/funnel/index.d.ts +21 -0
- package/dist/src/funnel/index.js +21 -0
- package/dist/src/funnel/insights.d.ts +50 -0
- package/dist/src/funnel/insights.js +120 -0
- package/dist/src/funnel/local-signals.d.ts +20 -0
- package/dist/src/funnel/local-signals.js +77 -0
- package/dist/src/funnel/message-transport.d.ts +51 -0
- package/dist/src/funnel/message-transport.js +149 -0
- package/dist/src/funnel/messages.d.ts +55 -0
- package/dist/src/funnel/messages.js +160 -0
- package/dist/src/funnel/payout.d.ts +40 -0
- package/dist/src/funnel/payout.js +60 -0
- package/dist/src/funnel/power-saver-notifier.d.ts +44 -0
- package/dist/src/funnel/power-saver-notifier.js +92 -0
- package/dist/src/funnel/precedence.d.ts +16 -0
- package/dist/src/funnel/precedence.js +85 -0
- package/dist/src/funnel/promo.d.ts +41 -0
- package/dist/src/funnel/promo.js +144 -0
- package/dist/src/funnel/rate-limit-notifier.d.ts +55 -0
- package/dist/src/funnel/rate-limit-notifier.js +102 -0
- package/dist/src/funnel/rotation.d.ts +19 -0
- package/dist/src/funnel/rotation.js +70 -0
- package/dist/src/funnel/state.d.ts +13 -0
- package/dist/src/funnel/state.js +52 -0
- package/dist/src/funnel/toggle-cooldown.d.ts +17 -0
- package/dist/src/funnel/toggle-cooldown.js +32 -0
- package/dist/src/funnel/types.d.ts +98 -0
- package/dist/src/funnel/types.js +26 -0
- package/dist/src/index.d.ts +81 -0
- package/dist/src/index.js +609 -0
- package/dist/src/infrastructure/in-memory-repositories.d.ts +68 -0
- package/dist/src/infrastructure/in-memory-repositories.js +264 -0
- package/dist/src/init/claudemd-generator.d.ts +16 -0
- package/dist/src/init/claudemd-generator.js +368 -0
- package/dist/src/init/executor.d.ts +41 -0
- package/dist/src/init/executor.js +2142 -0
- package/dist/src/init/helper-refresh.d.ts +79 -0
- package/dist/src/init/helper-refresh.js +347 -0
- package/dist/src/init/helper-signing.d.ts +37 -0
- package/dist/src/init/helper-signing.js +67 -0
- package/dist/src/init/helpers-generator.d.ts +87 -0
- package/dist/src/init/helpers-generator.js +1456 -0
- package/dist/src/init/index.d.ts +13 -0
- package/dist/src/init/index.js +15 -0
- package/dist/src/init/mcp-generator.d.ts +26 -0
- package/dist/src/init/mcp-generator.js +126 -0
- package/dist/src/init/memory-package-resolver.d.ts +53 -0
- package/dist/src/init/memory-package-resolver.js +118 -0
- package/dist/src/init/settings-generator.d.ts +14 -0
- package/dist/src/init/settings-generator.js +462 -0
- package/dist/src/init/statusline-generator.d.ts +28 -0
- package/dist/src/init/statusline-generator.js +177 -0
- package/dist/src/init/types.d.ts +331 -0
- package/dist/src/init/types.js +276 -0
- package/dist/src/log-filters.d.ts +46 -0
- package/dist/src/log-filters.js +107 -0
- package/dist/src/mcp-client.d.ts +92 -0
- package/dist/src/mcp-client.js +397 -0
- package/dist/src/mcp-server.d.ts +163 -0
- package/dist/src/mcp-server.js +751 -0
- package/dist/src/mcp-tools/agent-execute-core.d.ts +115 -0
- package/dist/src/mcp-tools/agent-execute-core.js +587 -0
- package/dist/src/mcp-tools/agent-tools.d.ts +9 -0
- package/dist/src/mcp-tools/agent-tools.js +837 -0
- package/dist/src/mcp-tools/agentbbs-tools.d.ts +28 -0
- package/dist/src/mcp-tools/agentbbs-tools.js +394 -0
- package/dist/src/mcp-tools/agentdb-tools.d.ts +35 -0
- package/dist/src/mcp-tools/agentdb-tools.js +1473 -0
- package/dist/src/mcp-tools/agenticow-loader.d.ts +59 -0
- package/dist/src/mcp-tools/agenticow-loader.js +105 -0
- package/dist/src/mcp-tools/agenticow-speculate-tools.d.ts +24 -0
- package/dist/src/mcp-tools/agenticow-speculate-tools.js +209 -0
- package/dist/src/mcp-tools/agenticow-tools.d.ts +36 -0
- package/dist/src/mcp-tools/agenticow-tools.js +360 -0
- package/dist/src/mcp-tools/analyze-tools.d.ts +38 -0
- package/dist/src/mcp-tools/analyze-tools.js +346 -0
- package/dist/src/mcp-tools/auto-install.d.ts +83 -0
- package/dist/src/mcp-tools/auto-install.js +131 -0
- package/dist/src/mcp-tools/autopilot-tools.d.ts +12 -0
- package/dist/src/mcp-tools/autopilot-tools.js +231 -0
- package/dist/src/mcp-tools/browser-intent-tools.d.ts +162 -0
- package/dist/src/mcp-tools/browser-intent-tools.js +548 -0
- package/dist/src/mcp-tools/browser-session-tools.d.ts +27 -0
- package/dist/src/mcp-tools/browser-session-tools.js +398 -0
- package/dist/src/mcp-tools/browser-tools.d.ts +21 -0
- package/dist/src/mcp-tools/browser-tools.js +760 -0
- package/dist/src/mcp-tools/business-pod-tools.d.ts +20 -0
- package/dist/src/mcp-tools/business-pod-tools.js +169 -0
- package/dist/src/mcp-tools/claims-tools.d.ts +12 -0
- package/dist/src/mcp-tools/claims-tools.js +863 -0
- package/dist/src/mcp-tools/config-tools.d.ts +8 -0
- package/dist/src/mcp-tools/config-tools.js +411 -0
- package/dist/src/mcp-tools/coordination-tools.d.ts +13 -0
- package/dist/src/mcp-tools/coordination-tools.js +729 -0
- package/dist/src/mcp-tools/daa-tools.d.ts +13 -0
- package/dist/src/mcp-tools/daa-tools.js +534 -0
- package/dist/src/mcp-tools/embeddings-tools.d.ts +9 -0
- package/dist/src/mcp-tools/embeddings-tools.js +904 -0
- package/dist/src/mcp-tools/github-tools.d.ts +9 -0
- package/dist/src/mcp-tools/github-tools.js +659 -0
- package/dist/src/mcp-tools/guidance-tools.d.ts +15 -0
- package/dist/src/mcp-tools/guidance-tools.js +639 -0
- package/dist/src/mcp-tools/hive-mind-tools.d.ts +8 -0
- package/dist/src/mcp-tools/hive-mind-tools.js +953 -0
- package/dist/src/mcp-tools/hooks-tools.d.ts +65 -0
- package/dist/src/mcp-tools/hooks-tools.js +4836 -0
- package/dist/src/mcp-tools/http-fetch-tools.d.ts +55 -0
- package/dist/src/mcp-tools/http-fetch-tools.js +329 -0
- package/dist/src/mcp-tools/index.d.ts +34 -0
- package/dist/src/mcp-tools/index.js +40 -0
- package/dist/src/mcp-tools/managed-agent-tools.d.ts +22 -0
- package/dist/src/mcp-tools/managed-agent-tools.js +357 -0
- package/dist/src/mcp-tools/memory-tools.d.ts +14 -0
- package/dist/src/mcp-tools/memory-tools.js +1330 -0
- package/dist/src/mcp-tools/metaharness-tools.d.ts +51 -0
- package/dist/src/mcp-tools/metaharness-tools.js +684 -0
- package/dist/src/mcp-tools/neural-tools.d.ts +54 -0
- package/dist/src/mcp-tools/neural-tools.js +1168 -0
- package/dist/src/mcp-tools/performance-tools.d.ts +16 -0
- package/dist/src/mcp-tools/performance-tools.js +675 -0
- package/dist/src/mcp-tools/progress-tools.d.ts +14 -0
- package/dist/src/mcp-tools/progress-tools.js +348 -0
- package/dist/src/mcp-tools/request-tracker.d.ts +17 -0
- package/dist/src/mcp-tools/request-tracker.js +27 -0
- package/dist/src/mcp-tools/ruvllm-tools.d.ts +9 -0
- package/dist/src/mcp-tools/ruvllm-tools.js +355 -0
- package/dist/src/mcp-tools/security-tools.d.ts +18 -0
- package/dist/src/mcp-tools/security-tools.js +556 -0
- package/dist/src/mcp-tools/session-tools.d.ts +8 -0
- package/dist/src/mcp-tools/session-tools.js +517 -0
- package/dist/src/mcp-tools/swarm-tools.d.ts +37 -0
- package/dist/src/mcp-tools/swarm-tools.js +390 -0
- package/dist/src/mcp-tools/system-tools.d.ts +13 -0
- package/dist/src/mcp-tools/system-tools.js +688 -0
- package/dist/src/mcp-tools/task-tools.d.ts +8 -0
- package/dist/src/mcp-tools/task-tools.js +487 -0
- package/dist/src/mcp-tools/terminal-tools.d.ts +8 -0
- package/dist/src/mcp-tools/terminal-tools.js +306 -0
- package/dist/src/mcp-tools/testgen-tools.d.ts +26 -0
- package/dist/src/mcp-tools/testgen-tools.js +168 -0
- package/dist/src/mcp-tools/tool-loop-guardrail.d.ts +31 -0
- package/dist/src/mcp-tools/tool-loop-guardrail.js +71 -0
- package/dist/src/mcp-tools/transfer-tools.d.ts +14 -0
- package/dist/src/mcp-tools/transfer-tools.js +447 -0
- package/dist/src/mcp-tools/types.d.ts +8 -0
- package/dist/src/mcp-tools/types.js +8 -0
- package/dist/src/mcp-tools/validate-input.d.ts +9 -0
- package/dist/src/mcp-tools/validate-input.js +9 -0
- package/dist/src/mcp-tools/wasm-agent-tools.d.ts +13 -0
- package/dist/src/mcp-tools/wasm-agent-tools.js +840 -0
- package/dist/src/mcp-tools/workflow-tools.d.ts +8 -0
- package/dist/src/mcp-tools/workflow-tools.js +884 -0
- package/dist/src/memory/bge-embedder.d.ts +25 -0
- package/dist/src/memory/bge-embedder.js +121 -0
- package/dist/src/memory/cross-encoder-rerank.d.ts +33 -0
- package/dist/src/memory/cross-encoder-rerank.js +123 -0
- package/dist/src/memory/embedding-policy.d.ts +21 -0
- package/dist/src/memory/embedding-policy.js +30 -0
- package/dist/src/memory/embedding-quantization.d.ts +62 -0
- package/dist/src/memory/embedding-quantization.js +156 -0
- package/dist/src/memory/ewc-consolidation.d.ts +305 -0
- package/dist/src/memory/ewc-consolidation.js +611 -0
- package/dist/src/memory/graph-edge-writer.d.ts +95 -0
- package/dist/src/memory/graph-edge-writer.js +217 -0
- package/dist/src/memory/hybrid-retrieval.d.ts +77 -0
- package/dist/src/memory/hybrid-retrieval.js +192 -0
- package/dist/src/memory/intelligence.d.ts +405 -0
- package/dist/src/memory/intelligence.js +1316 -0
- package/dist/src/memory/lucene-bm25.d.ts +19 -0
- package/dist/src/memory/lucene-bm25.js +308 -0
- package/dist/src/memory/memory-bridge.d.ts +537 -0
- package/dist/src/memory/memory-bridge.js +2460 -0
- package/dist/src/memory/memory-initializer.d.ts +556 -0
- package/dist/src/memory/memory-initializer.js +3021 -0
- package/dist/src/memory/neural-package-bridge.d.ts +48 -0
- package/dist/src/memory/neural-package-bridge.js +87 -0
- package/dist/src/memory/rabitq-index.d.ts +60 -0
- package/dist/src/memory/rabitq-index.js +242 -0
- package/dist/src/memory/sona-optimizer.d.ts +267 -0
- package/dist/src/memory/sona-optimizer.js +779 -0
- package/dist/src/memory/structured-distill.d.ts +48 -0
- package/dist/src/memory/structured-distill.js +125 -0
- package/dist/src/output.d.ts +9 -0
- package/dist/src/output.js +9 -0
- package/dist/src/parser.d.ts +89 -0
- package/dist/src/parser.js +516 -0
- package/dist/src/plugins/manager.d.ts +133 -0
- package/dist/src/plugins/manager.js +415 -0
- package/dist/src/plugins/store/discovery.d.ts +99 -0
- package/dist/src/plugins/store/discovery.js +1224 -0
- package/dist/src/plugins/store/index.d.ts +76 -0
- package/dist/src/plugins/store/index.js +141 -0
- package/dist/src/plugins/store/search.d.ts +46 -0
- package/dist/src/plugins/store/search.js +230 -0
- package/dist/src/plugins/store/types.d.ts +279 -0
- package/dist/src/plugins/store/types.js +7 -0
- package/dist/src/plugins/tests/demo-plugin-store.d.ts +7 -0
- package/dist/src/plugins/tests/demo-plugin-store.js +126 -0
- package/dist/src/plugins/tests/standalone-test.d.ts +12 -0
- package/dist/src/plugins/tests/standalone-test.js +188 -0
- package/dist/src/plugins/tests/test-plugin-store.d.ts +7 -0
- package/dist/src/plugins/tests/test-plugin-store.js +206 -0
- package/dist/src/production/circuit-breaker.d.ts +101 -0
- package/dist/src/production/circuit-breaker.js +241 -0
- package/dist/src/production/error-handler.d.ts +92 -0
- package/dist/src/production/error-handler.js +299 -0
- package/dist/src/production/index.d.ts +23 -0
- package/dist/src/production/index.js +18 -0
- package/dist/src/production/monitoring.d.ts +161 -0
- package/dist/src/production/monitoring.js +356 -0
- package/dist/src/production/rate-limiter.d.ts +80 -0
- package/dist/src/production/rate-limiter.js +201 -0
- package/dist/src/production/retry.d.ts +48 -0
- package/dist/src/production/retry.js +179 -0
- package/dist/src/prompt.d.ts +44 -0
- package/dist/src/prompt.js +501 -0
- package/dist/src/proxy/install.d.ts +29 -0
- package/dist/src/proxy/install.js +135 -0
- package/dist/src/proxy/lifecycle.d.ts +61 -0
- package/dist/src/proxy/lifecycle.js +249 -0
- package/dist/src/proxy/paths.d.ts +34 -0
- package/dist/src/proxy/paths.js +70 -0
- package/dist/src/proxy/release.d.ts +47 -0
- package/dist/src/proxy/release.js +138 -0
- package/dist/src/proxy/token-bridge.d.ts +5 -0
- package/dist/src/proxy/token-bridge.js +61 -0
- package/dist/src/proxy/verify.d.ts +44 -0
- package/dist/src/proxy/verify.js +68 -0
- package/dist/src/runtime/headless.d.ts +60 -0
- package/dist/src/runtime/headless.js +284 -0
- package/dist/src/runtime/parent-death-watchdog.d.ts +42 -0
- package/dist/src/runtime/parent-death-watchdog.js +70 -0
- package/dist/src/ruvector/agent-wasm.d.ts +228 -0
- package/dist/src/ruvector/agent-wasm.js +463 -0
- package/dist/src/ruvector/ast-analyzer.d.ts +67 -0
- package/dist/src/ruvector/ast-analyzer.js +277 -0
- package/dist/src/ruvector/codemods/engine.d.ts +45 -0
- package/dist/src/ruvector/codemods/engine.js +291 -0
- package/dist/src/ruvector/codemods/scope-analysis.d.ts +29 -0
- package/dist/src/ruvector/codemods/scope-analysis.js +162 -0
- package/dist/src/ruvector/coverage-router.d.ts +160 -0
- package/dist/src/ruvector/coverage-router.js +531 -0
- package/dist/src/ruvector/coverage-tools.d.ts +33 -0
- package/dist/src/ruvector/coverage-tools.js +157 -0
- package/dist/src/ruvector/diff-classifier.d.ts +175 -0
- package/dist/src/ruvector/diff-classifier.js +699 -0
- package/dist/src/ruvector/diskann-backend.d.ts +78 -0
- package/dist/src/ruvector/diskann-backend.js +310 -0
- package/dist/src/ruvector/enhanced-model-router.d.ts +172 -0
- package/dist/src/ruvector/enhanced-model-router.js +577 -0
- package/dist/src/ruvector/graph-analyzer.d.ts +187 -0
- package/dist/src/ruvector/graph-analyzer.js +929 -0
- package/dist/src/ruvector/graph-backend.d.ts +79 -0
- package/dist/src/ruvector/graph-backend.js +220 -0
- package/dist/src/ruvector/index.d.ts +38 -0
- package/dist/src/ruvector/index.js +86 -0
- package/dist/src/ruvector/lora-adapter.d.ts +292 -0
- package/dist/src/ruvector/lora-adapter.js +710 -0
- package/dist/src/ruvector/model-prices.d.ts +50 -0
- package/dist/src/ruvector/model-prices.js +72 -0
- package/dist/src/ruvector/model-router.d.ts +408 -0
- package/dist/src/ruvector/model-router.js +1162 -0
- package/dist/src/ruvector/neural-router.d.ts +182 -0
- package/dist/src/ruvector/neural-router.js +825 -0
- package/dist/src/ruvector/output-verifier.d.ts +83 -0
- package/dist/src/ruvector/output-verifier.js +277 -0
- package/dist/src/ruvector/q-learning-router.d.ts +227 -0
- package/dist/src/ruvector/q-learning-router.js +721 -0
- package/dist/src/ruvector/router-calibrator.d.ts +65 -0
- package/dist/src/ruvector/router-calibrator.js +120 -0
- package/dist/src/ruvector/router-parallel-recorder.d.ts +127 -0
- package/dist/src/ruvector/router-parallel-recorder.js +183 -0
- package/dist/src/ruvector/router-trajectory.d.ts +194 -0
- package/dist/src/ruvector/router-trajectory.js +281 -0
- package/dist/src/ruvector/run-transcript-recorder.d.ts +154 -0
- package/dist/src/ruvector/run-transcript-recorder.js +209 -0
- package/dist/src/ruvector/ruvllm-wasm.d.ts +179 -0
- package/dist/src/ruvector/ruvllm-wasm.js +379 -0
- package/dist/src/ruvector/semantic-router.d.ts +77 -0
- package/dist/src/ruvector/semantic-router.js +178 -0
- package/dist/src/ruvector/task-embedder.d.ts +56 -0
- package/dist/src/ruvector/task-embedder.js +237 -0
- package/dist/src/ruvector/trajectory-tree.d.ts +113 -0
- package/dist/src/ruvector/trajectory-tree.js +237 -0
- package/dist/src/ruvector/vector-db.d.ts +73 -0
- package/dist/src/ruvector/vector-db.js +301 -0
- package/dist/src/ruvector/wasm-embedder.d.ts +13 -0
- package/dist/src/ruvector/wasm-embedder.js +143 -0
- package/dist/src/security/builtin-aidefence.d.ts +34 -0
- package/dist/src/security/builtin-aidefence.js +86 -0
- package/dist/src/services/agentic-flow-bridge.d.ts +50 -0
- package/dist/src/services/agentic-flow-bridge.js +95 -0
- package/dist/src/services/ai-job-dedup.d.ts +61 -0
- package/dist/src/services/ai-job-dedup.js +136 -0
- package/dist/src/services/checkpoint-gate.d.ts +140 -0
- package/dist/src/services/checkpoint-gate.js +223 -0
- package/dist/src/services/claim-service.d.ts +204 -0
- package/dist/src/services/claim-service.js +818 -0
- package/dist/src/services/config-file-manager.d.ts +37 -0
- package/dist/src/services/config-file-manager.js +224 -0
- package/dist/src/services/container-worker-pool.d.ts +204 -0
- package/dist/src/services/container-worker-pool.js +589 -0
- package/dist/src/services/daemon-autostart.d.ts +17 -0
- package/dist/src/services/daemon-autostart.js +102 -0
- package/dist/src/services/distill-oracle.d.ts +190 -0
- package/dist/src/services/distill-oracle.js +349 -0
- package/dist/src/services/distill-tuning.d.ts +111 -0
- package/dist/src/services/distill-tuning.js +510 -0
- package/dist/src/services/evolve-proof.d.ts +253 -0
- package/dist/src/services/evolve-proof.js +343 -0
- package/dist/src/services/fable-harness.d.ts +208 -0
- package/dist/src/services/fable-harness.js +388 -0
- package/dist/src/services/git-workspace-identity.d.ts +42 -0
- package/dist/src/services/git-workspace-identity.js +99 -0
- package/dist/src/services/global-ai-budget.d.ts +135 -0
- package/dist/src/services/global-ai-budget.js +415 -0
- package/dist/src/services/harness-benchmark.d.ts +68 -0
- package/dist/src/services/harness-benchmark.js +94 -0
- package/dist/src/services/harness-canary.d.ts +60 -0
- package/dist/src/services/harness-canary.js +69 -0
- package/dist/src/services/harness-corpus-harvester.d.ts +51 -0
- package/dist/src/services/harness-corpus-harvester.js +74 -0
- package/dist/src/services/harness-flywheel-generations.d.ts +115 -0
- package/dist/src/services/harness-flywheel-generations.js +360 -0
- package/dist/src/services/harness-flywheel-runtime.d.ts +25 -0
- package/dist/src/services/harness-flywheel-runtime.js +92 -0
- package/dist/src/services/harness-flywheel.d.ts +48 -0
- package/dist/src/services/harness-flywheel.js +207 -0
- package/dist/src/services/harness-frozen-eval.d.ts +22 -0
- package/dist/src/services/harness-frozen-eval.js +66 -0
- package/dist/src/services/harness-hosts.d.ts +40 -0
- package/dist/src/services/harness-hosts.js +88 -0
- package/dist/src/services/harness-improvement-ledger.d.ts +63 -0
- package/dist/src/services/harness-improvement-ledger.js +101 -0
- package/dist/src/services/harness-loop.d.ts +53 -0
- package/dist/src/services/harness-loop.js +85 -0
- package/dist/src/services/harness-qualification.d.ts +66 -0
- package/dist/src/services/harness-qualification.js +143 -0
- package/dist/src/services/harness-replay.d.ts +37 -0
- package/dist/src/services/harness-replay.js +92 -0
- package/dist/src/services/harness-verify.d.ts +33 -0
- package/dist/src/services/harness-verify.js +26 -0
- package/dist/src/services/harness-worker.d.ts +23 -0
- package/dist/src/services/harness-worker.js +66 -0
- package/dist/src/services/headless-worker-executor.d.ts +358 -0
- package/dist/src/services/headless-worker-executor.js +1269 -0
- package/dist/src/services/index.d.ts +13 -0
- package/dist/src/services/index.js +11 -0
- package/dist/src/services/memory-backup.d.ts +53 -0
- package/dist/src/services/memory-backup.js +198 -0
- package/dist/src/services/memory-distillation.d.ts +41 -0
- package/dist/src/services/memory-distillation.js +277 -0
- package/dist/src/services/native-training.d.ts +68 -0
- package/dist/src/services/native-training.js +141 -0
- package/dist/src/services/registry-api.d.ts +58 -0
- package/dist/src/services/registry-api.js +146 -0
- package/dist/src/services/repo-supervisor.d.ts +70 -0
- package/dist/src/services/repo-supervisor.js +228 -0
- package/dist/src/services/ruvector-training.d.ts +222 -0
- package/dist/src/services/ruvector-training.js +693 -0
- package/dist/src/services/swarm-memory-branches.d.ts +135 -0
- package/dist/src/services/swarm-memory-branches.js +213 -0
- package/dist/src/services/weight-eft.d.ts +305 -0
- package/dist/src/services/weight-eft.js +296 -0
- package/dist/src/services/worker-daemon.d.ts +439 -0
- package/dist/src/services/worker-daemon.js +1865 -0
- package/dist/src/services/worker-queue.d.ts +194 -0
- package/dist/src/services/worker-queue.js +513 -0
- package/dist/src/services/workspace-lease.d.ts +55 -0
- package/dist/src/services/workspace-lease.js +191 -0
- package/dist/src/suggest.d.ts +53 -0
- package/dist/src/suggest.js +200 -0
- package/dist/src/transfer/anonymization/index.d.ts +25 -0
- package/dist/src/transfer/anonymization/index.js +175 -0
- package/dist/src/transfer/deploy-seraphine.d.ts +13 -0
- package/dist/src/transfer/deploy-seraphine.js +205 -0
- package/dist/src/transfer/export.d.ts +25 -0
- package/dist/src/transfer/export.js +113 -0
- package/dist/src/transfer/index.d.ts +12 -0
- package/dist/src/transfer/index.js +31 -0
- package/dist/src/transfer/ipfs/client.d.ts +109 -0
- package/dist/src/transfer/ipfs/client.js +307 -0
- package/dist/src/transfer/ipfs/upload.d.ts +95 -0
- package/dist/src/transfer/ipfs/upload.js +413 -0
- package/dist/src/transfer/models/seraphine.d.ts +72 -0
- package/dist/src/transfer/models/seraphine.js +373 -0
- package/dist/src/transfer/serialization/cfp.d.ts +49 -0
- package/dist/src/transfer/serialization/cfp.js +183 -0
- package/dist/src/transfer/storage/gcs.d.ts +82 -0
- package/dist/src/transfer/storage/gcs.js +272 -0
- package/dist/src/transfer/storage/index.d.ts +6 -0
- package/dist/src/transfer/storage/index.js +6 -0
- package/dist/src/transfer/store/discovery.d.ts +84 -0
- package/dist/src/transfer/store/discovery.js +382 -0
- package/dist/src/transfer/store/download.d.ts +70 -0
- package/dist/src/transfer/store/download.js +334 -0
- package/dist/src/transfer/store/index.d.ts +84 -0
- package/dist/src/transfer/store/index.js +153 -0
- package/dist/src/transfer/store/publish.d.ts +76 -0
- package/dist/src/transfer/store/publish.js +294 -0
- package/dist/src/transfer/store/registry.d.ts +58 -0
- package/dist/src/transfer/store/registry.js +285 -0
- package/dist/src/transfer/store/search.d.ts +54 -0
- package/dist/src/transfer/store/search.js +232 -0
- package/dist/src/transfer/store/tests/standalone-test.d.ts +12 -0
- package/dist/src/transfer/store/tests/standalone-test.js +190 -0
- package/dist/src/transfer/store/types.d.ts +193 -0
- package/dist/src/transfer/store/types.js +6 -0
- package/dist/src/transfer/test-seraphine.d.ts +6 -0
- package/dist/src/transfer/test-seraphine.js +105 -0
- package/dist/src/transfer/tests/test-store.d.ts +7 -0
- package/dist/src/transfer/tests/test-store.js +214 -0
- package/dist/src/transfer/types.d.ts +245 -0
- package/dist/src/transfer/types.js +6 -0
- package/dist/src/types.d.ts +13 -0
- package/dist/src/types.js +13 -0
- package/dist/src/update/checker.d.ts +34 -0
- package/dist/src/update/checker.js +191 -0
- package/dist/src/update/executor.d.ts +33 -0
- package/dist/src/update/executor.js +217 -0
- package/dist/src/update/index.d.ts +33 -0
- package/dist/src/update/index.js +64 -0
- package/dist/src/update/rate-limiter.d.ts +20 -0
- package/dist/src/update/rate-limiter.js +96 -0
- package/dist/src/update/validator.d.ts +17 -0
- package/dist/src/update/validator.js +123 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1865 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Daemon Service
|
|
3
|
+
* Node.js-based background worker system that auto-runs like shell daemons
|
|
4
|
+
*
|
|
5
|
+
* Workers:
|
|
6
|
+
* - map: Codebase mapping (5 min interval)
|
|
7
|
+
* - audit: Security analysis (10 min interval)
|
|
8
|
+
* - optimize: Performance optimization (15 min interval)
|
|
9
|
+
* - consolidate: Memory distillation — memory_entries -> episodes/reasoning_patterns/
|
|
10
|
+
* causal_edges (30 min interval, ADR-174; RUFLO_DAEMON_NO_DISTILL=1 / --no-distill to skip)
|
|
11
|
+
* - testgaps: Test coverage analysis (20 min interval)
|
|
12
|
+
*/
|
|
13
|
+
import { EventEmitter } from 'events';
|
|
14
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync, unlinkSync, renameSync } from 'fs';
|
|
15
|
+
import { cpus } from 'os';
|
|
16
|
+
import { join } from 'path';
|
|
17
|
+
import { HeadlessWorkerExecutor, isHeadlessWorker, } from './headless-worker-executor.js';
|
|
18
|
+
// ADR-174 M3: the consolidate worker below drives the real DISTILL/CONSOLIDATE
|
|
19
|
+
// pass instead of writing a hardcoded { patternsConsolidated: 0 } stub. The
|
|
20
|
+
// service itself is owned/frozen elsewhere (memory-distillation.ts) — it is
|
|
21
|
+
// incremental (rowid cursor), non-destructive, transactional, and
|
|
22
|
+
// quick_check-gated, so it's safe to call unconditionally on every tick.
|
|
23
|
+
import { runDistillation, defaultMemoryDbPath } from './memory-distillation.js';
|
|
24
|
+
import { backupMemoryDb } from './memory-backup.js';
|
|
25
|
+
import { resolveGitWorkspaceIdentity } from './git-workspace-identity.js';
|
|
26
|
+
import { getWorkspaceLeaseRegistry } from './workspace-lease.js';
|
|
27
|
+
import { getRepoSupervisorRegistry } from './repo-supervisor.js';
|
|
28
|
+
// Default worker configurations with improved intervals (P0 fix: map 5min -> 15min)
|
|
29
|
+
const DEFAULT_WORKERS = [
|
|
30
|
+
{ type: 'map', intervalMs: 15 * 60 * 1000, offsetMs: 0, priority: 'normal', description: 'Codebase mapping', enabled: true },
|
|
31
|
+
{ type: 'audit', intervalMs: 10 * 60 * 1000, offsetMs: 2 * 60 * 1000, priority: 'critical', description: 'Security analysis', enabled: true },
|
|
32
|
+
{ type: 'optimize', intervalMs: 15 * 60 * 1000, offsetMs: 4 * 60 * 1000, priority: 'high', description: 'Performance optimization', enabled: true },
|
|
33
|
+
{ type: 'consolidate', intervalMs: 30 * 60 * 1000, offsetMs: 6 * 60 * 1000, priority: 'low', description: 'Memory distillation (ADR-174)', enabled: true },
|
|
34
|
+
{ type: 'testgaps', intervalMs: 20 * 60 * 1000, offsetMs: 8 * 60 * 1000, priority: 'normal', description: 'Test coverage analysis', enabled: true },
|
|
35
|
+
{ type: 'backup', intervalMs: 24 * 60 * 60 * 1000, offsetMs: 10 * 60 * 1000, priority: 'low', description: 'Nightly memory DB backup (WAL-safe, rotated)', enabled: true },
|
|
36
|
+
{ type: 'harness', intervalMs: 6 * 60 * 60 * 1000, offsetMs: 12 * 60 * 1000, priority: 'low', description: 'Self-optimizing harness loop (opt-in RUFLO_HARNESS_LOOP, $0-default)', enabled: true },
|
|
37
|
+
{ type: 'predict', intervalMs: 10 * 60 * 1000, offsetMs: 0, priority: 'low', description: 'Predictive preloading', enabled: false },
|
|
38
|
+
{ type: 'document', intervalMs: 60 * 60 * 1000, offsetMs: 0, priority: 'low', description: 'Auto-documentation', enabled: false },
|
|
39
|
+
];
|
|
40
|
+
// Worker timeout — must exceed the longest per-worker headless timeout (15 min for audit/refactor).
|
|
41
|
+
// Previously 5 min, which caused orphan processes when daemon timeout fired before executor timeout (#1117).
|
|
42
|
+
const DEFAULT_WORKER_TIMEOUT_MS = 16 * 60 * 1000;
|
|
43
|
+
// ADR-174 M3: hard cap on rows the `consolidate` worker distills in a single
|
|
44
|
+
// tick. The distillation service is cursor-driven (per-namespace rowid) and
|
|
45
|
+
// batches in transactions of `batchSize`, so a capped `maxEntries` guarantees
|
|
46
|
+
// this worker returns well within DEFAULT_WORKER_TIMEOUT_MS even against a
|
|
47
|
+
// large backlog — the cursor just picks up where it left off next tick
|
|
48
|
+
// (every 30 min per DEFAULT_WORKERS), draining an arbitrarily large backlog
|
|
49
|
+
// over several ticks instead of blocking on one.
|
|
50
|
+
const CONSOLIDATE_MAX_ENTRIES_PER_TICK = 1000;
|
|
51
|
+
const CONSOLIDATE_BATCH_SIZE = 200;
|
|
52
|
+
// ADR-174 M5: platform-default distillation config, chosen by the M4 self-
|
|
53
|
+
// optimization grid-search (scripts/tune-distill.mjs) on the real ~7.9k-entry
|
|
54
|
+
// corpus. Winner: batchSize=200, dedupDistance=0.2 (held-out MRR@10 0.753 vs
|
|
55
|
+
// 0.749 baseline — measured on-par, not a large uplift; the payoff is the
|
|
56
|
+
// populated substrate + trainable model). Override per-run via `memory distill`.
|
|
57
|
+
const CONSOLIDATE_DEDUP_DISTANCE = 0.2;
|
|
58
|
+
// ADR-174 M3 opt-out: set to skip the real distillation pass entirely (e.g.
|
|
59
|
+
// constrained CI hosts, or a user who wants the daemon's other workers but
|
|
60
|
+
// not this one without touching persisted worker-enabled state). Mirrors the
|
|
61
|
+
// `--no-distill` flag on `daemon start` (see commands/daemon.ts), which sets
|
|
62
|
+
// this env var on the forked/foreground daemon process.
|
|
63
|
+
const NO_DISTILL_ENV = 'RUFLO_DAEMON_NO_DISTILL';
|
|
64
|
+
// #2356 — Self-terminating lifecycle defaults. A background daemon with no
|
|
65
|
+
// upper bound on its lifetime runs until the box reboots; in the field this
|
|
66
|
+
// leaked tens of thousands of headless `claude --print` sweeps over many days
|
|
67
|
+
// (one observed daemon ran 19 days). A 12h default age cap (matching the
|
|
68
|
+
// pacphi/ruflo-machine-ref kit's proven value) heals a forgotten daemon within
|
|
69
|
+
// half a day; set RUFLO_DAEMON_TTL_SECS=0 (or `--ttl 0`) to opt out. Idle
|
|
70
|
+
// shutdown is opt-in (0 = disabled) since a legitimately quiet daemon is not a leak.
|
|
71
|
+
const DEFAULT_DAEMON_TTL_MS = 12 * 60 * 60 * 1000;
|
|
72
|
+
const DEFAULT_DAEMON_IDLE_SHUTDOWN_MS = 0;
|
|
73
|
+
/**
|
|
74
|
+
* Read a non-negative seconds value from an env var and return it as ms.
|
|
75
|
+
* Unlike the `parseInt(x) || default` idiom used elsewhere, an explicit `0`
|
|
76
|
+
* is honored (it disables the corresponding limit) rather than falling back
|
|
77
|
+
* to the default. Invalid / negative / absent values fall back.
|
|
78
|
+
*/
|
|
79
|
+
function readEnvSecsAsMs(name, defaultMs) {
|
|
80
|
+
const raw = process.env[name];
|
|
81
|
+
if (raw === undefined || raw.trim() === '')
|
|
82
|
+
return defaultMs;
|
|
83
|
+
const secs = Number.parseInt(raw, 10);
|
|
84
|
+
if (!Number.isFinite(secs) || secs < 0)
|
|
85
|
+
return defaultMs;
|
|
86
|
+
return secs * 1000;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Worker Daemon - Manages background workers with Node.js
|
|
90
|
+
*/
|
|
91
|
+
export class WorkerDaemon extends EventEmitter {
|
|
92
|
+
config;
|
|
93
|
+
workers = new Map();
|
|
94
|
+
timers = new Map();
|
|
95
|
+
// #1845: separate timer for the MCP-dispatch queue poller. Kept off
|
|
96
|
+
// the per-worker map so stop() clears both kinds without confusion.
|
|
97
|
+
queuePollTimer;
|
|
98
|
+
// #2356: separate timer that enforces the daemon's max-age TTL + idle
|
|
99
|
+
// shutdown. Cleared in stop() alongside the worker/queue timers.
|
|
100
|
+
lifecycleTimer;
|
|
101
|
+
running = false;
|
|
102
|
+
startedAt;
|
|
103
|
+
projectRoot;
|
|
104
|
+
runningWorkers = new Set(); // Track concurrent workers
|
|
105
|
+
pendingWorkers = []; // Queue for deferred workers
|
|
106
|
+
// Headless execution support
|
|
107
|
+
headlessExecutor = null;
|
|
108
|
+
headlessAvailable = false;
|
|
109
|
+
// #2251 — Promise that resolves once initHeadlessExecutor() has finished
|
|
110
|
+
// probing `claude --version` and constructed the executor. The constructor
|
|
111
|
+
// kicks off init fire-and-forget; without awaiting this on the trigger
|
|
112
|
+
// path, `ruflo daemon trigger -w <worker>` runs before headlessAvailable
|
|
113
|
+
// is set and falls through to the local stub in ~2ms.
|
|
114
|
+
headlessInitPromise = Promise.resolve();
|
|
115
|
+
// Preserve the original constructor config so we can detect explicit overrides
|
|
116
|
+
// during state restoration (R1: constructor config takes priority over stale state)
|
|
117
|
+
originalConfig;
|
|
118
|
+
// #2661 root-fix — resolved once (git identity doesn't change at runtime)
|
|
119
|
+
// and reused for lease heartbeats + supervisor election, both gated on
|
|
120
|
+
// aiWorkersEnabled since they only matter for the recurring AI schedule.
|
|
121
|
+
gitIdentity = null;
|
|
122
|
+
lastSupervisorRenewalMs = 0;
|
|
123
|
+
constructor(projectRoot, config) {
|
|
124
|
+
super();
|
|
125
|
+
this.projectRoot = projectRoot;
|
|
126
|
+
this.originalConfig = config;
|
|
127
|
+
const claudeFlowDir = join(projectRoot, '.claude-flow');
|
|
128
|
+
// Read daemon config from .claude-flow/config.json (Layer B)
|
|
129
|
+
const fileConfig = this.readDaemonConfigFromFile(claudeFlowDir);
|
|
130
|
+
// CPU-proportional smart default instead of hardcoded 2.0
|
|
131
|
+
const cpuCount = WorkerDaemon.getEffectiveCpuCount();
|
|
132
|
+
let smartMaxCpuLoad = Math.max(cpuCount * 0.8, 2.0); // Floor of 2.0 for single-CPU machines
|
|
133
|
+
// #2110 — WSL2 reports `/proc/loadavg` values that include Windows-side
|
|
134
|
+
// process counts mapped into the Linux kernel. Real load on a 4-CPU
|
|
135
|
+
// WSL2 host can be 200-400 even when the Linux side is idle. The
|
|
136
|
+
// default gate of `cpuCount * 0.8` always trips, deferring every
|
|
137
|
+
// worker as "CPU load too high" while the daemon reports healthy.
|
|
138
|
+
// Bump the floor to 1000 when WSL is detected so the gate is
|
|
139
|
+
// effectively disabled (real load on Linux side rarely exceeds 100
|
|
140
|
+
// even under heavy contention).
|
|
141
|
+
if (WorkerDaemon.isWslEnvironment()) {
|
|
142
|
+
smartMaxCpuLoad = Math.max(smartMaxCpuLoad, 1000);
|
|
143
|
+
}
|
|
144
|
+
// Platform-aware default: macOS os.freemem() excludes reclaimable file cache,
|
|
145
|
+
// so reported "free" is much lower than actually available memory.
|
|
146
|
+
// Linux reports available memory (including reclaimable cache) more accurately.
|
|
147
|
+
const defaultMinFreeMemory = process.platform === 'darwin' ? 5 : 10;
|
|
148
|
+
// Priority: constructor arg > config.json > smart default
|
|
149
|
+
// For resourceThresholds, merge field-by-field so partial overrides
|
|
150
|
+
// (e.g. only --max-cpu-load) still pick up defaults for other fields.
|
|
151
|
+
this.config = {
|
|
152
|
+
autoStart: config?.autoStart ?? fileConfig.autoStart ?? false,
|
|
153
|
+
logDir: config?.logDir ?? join(claudeFlowDir, 'logs'),
|
|
154
|
+
stateFile: config?.stateFile ?? join(claudeFlowDir, 'daemon-state.json'),
|
|
155
|
+
maxConcurrent: config?.maxConcurrent ?? fileConfig.maxConcurrent ?? 2,
|
|
156
|
+
workerTimeoutMs: config?.workerTimeoutMs ?? fileConfig.workerTimeoutMs ?? DEFAULT_WORKER_TIMEOUT_MS,
|
|
157
|
+
resourceThresholds: {
|
|
158
|
+
maxCpuLoad: config?.resourceThresholds?.maxCpuLoad ?? fileConfig.maxCpuLoad ?? smartMaxCpuLoad,
|
|
159
|
+
minFreeMemoryPercent: config?.resourceThresholds?.minFreeMemoryPercent ?? fileConfig.minFreeMemoryPercent ?? defaultMinFreeMemory,
|
|
160
|
+
},
|
|
161
|
+
// #2356 — precedence: constructor arg > config.json (daemon.ttlSecs) >
|
|
162
|
+
// env (RUFLO_DAEMON_TTL_SECS) > built-in default. readEnvSecsAsMs folds
|
|
163
|
+
// env-or-default and honors an explicit 0 (disable).
|
|
164
|
+
ttlMs: config?.ttlMs ?? fileConfig.ttlMs ?? readEnvSecsAsMs('RUFLO_DAEMON_TTL_SECS', DEFAULT_DAEMON_TTL_MS),
|
|
165
|
+
idleShutdownMs: config?.idleShutdownMs ?? fileConfig.idleShutdownMs ?? readEnvSecsAsMs('RUFLO_DAEMON_IDLE_SECS', DEFAULT_DAEMON_IDLE_SHUTDOWN_MS),
|
|
166
|
+
// #2661 — AI workers are opt-in: flag > config.json > env > OFF.
|
|
167
|
+
// Deliberately NOT restored from daemon-state.json (initializeWorkerStates
|
|
168
|
+
// whitelist) so a stale state file can never resurrect consent.
|
|
169
|
+
aiWorkersEnabled: config?.aiWorkersEnabled
|
|
170
|
+
?? fileConfig.aiWorkersEnabled
|
|
171
|
+
?? (process.env.RUFLO_DAEMON_AI_WORKERS === '1'),
|
|
172
|
+
workers: config?.workers ?? DEFAULT_WORKERS,
|
|
173
|
+
};
|
|
174
|
+
// Setup graceful shutdown handlers
|
|
175
|
+
this.setupShutdownHandlers();
|
|
176
|
+
// #1855: install crash handlers so uncaught exceptions and unhandled
|
|
177
|
+
// rejections don't leak the PID file or orphan child processes.
|
|
178
|
+
this.installCrashHandlers();
|
|
179
|
+
// Ensure directories exist
|
|
180
|
+
if (!existsSync(claudeFlowDir)) {
|
|
181
|
+
mkdirSync(claudeFlowDir, { recursive: true });
|
|
182
|
+
}
|
|
183
|
+
if (!existsSync(this.config.logDir)) {
|
|
184
|
+
mkdirSync(this.config.logDir, { recursive: true });
|
|
185
|
+
}
|
|
186
|
+
// Initialize worker states
|
|
187
|
+
this.initializeWorkerStates();
|
|
188
|
+
// Initialize headless executor (async, non-blocking) — capture the
|
|
189
|
+
// promise so the trigger path (#2251) can await it before checking
|
|
190
|
+
// `headlessAvailable`. Scheduled fires hit a long-running daemon and
|
|
191
|
+
// are unaffected; the on-demand `trigger` path was racing this init.
|
|
192
|
+
this.headlessInitPromise = this.initHeadlessExecutor().catch((err) => {
|
|
193
|
+
this.log('warn', `Headless executor init failed: ${err}`);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Initialize headless executor if Claude Code is available
|
|
198
|
+
*/
|
|
199
|
+
async initHeadlessExecutor() {
|
|
200
|
+
// #2661 — scheduled AI workers require explicit consent. Without it,
|
|
201
|
+
// don't even probe `claude --version`: headlessAvailable stays false,
|
|
202
|
+
// every worker runs its $0 local path, and a default install produces
|
|
203
|
+
// zero autonomous Claude launches regardless of worktree count.
|
|
204
|
+
if (!this.config.aiWorkersEnabled) {
|
|
205
|
+
this.log('info', 'AI workers disabled (default) - all workers run local-only. Enable with `daemon start --headless`, daemon.aiWorkers.enabled=true, or RUFLO_DAEMON_AI_WORKERS=1 (#2661)');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
this.headlessExecutor = new HeadlessWorkerExecutor(this.projectRoot, {
|
|
210
|
+
maxConcurrent: this.config.maxConcurrent,
|
|
211
|
+
});
|
|
212
|
+
this.headlessAvailable = await this.headlessExecutor.isAvailable();
|
|
213
|
+
if (this.headlessAvailable) {
|
|
214
|
+
this.log('info', 'Claude Code headless mode available - AI workers enabled');
|
|
215
|
+
// Forward headless executor events. #1855: also snapshot the
|
|
216
|
+
// active child PIDs to disk on every transition so the next
|
|
217
|
+
// lifetime can reap orphans after a hard crash.
|
|
218
|
+
this.headlessExecutor.on('execution:start', (data) => {
|
|
219
|
+
this.writeChildrenSnapshot();
|
|
220
|
+
this.emit('headless:start', data);
|
|
221
|
+
});
|
|
222
|
+
this.headlessExecutor.on('execution:complete', (data) => {
|
|
223
|
+
this.writeChildrenSnapshot();
|
|
224
|
+
this.emit('headless:complete', data);
|
|
225
|
+
});
|
|
226
|
+
this.headlessExecutor.on('execution:error', (data) => {
|
|
227
|
+
this.writeChildrenSnapshot();
|
|
228
|
+
this.emit('headless:error', data);
|
|
229
|
+
});
|
|
230
|
+
this.headlessExecutor.on('output', (data) => {
|
|
231
|
+
this.emit('headless:output', data);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
this.log('info', 'Claude Code not found - AI workers will run in local fallback mode');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
this.log('warn', `Failed to initialize headless executor: ${error}`);
|
|
240
|
+
this.headlessAvailable = false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Check if headless execution is available
|
|
245
|
+
*/
|
|
246
|
+
isHeadlessAvailable() {
|
|
247
|
+
return this.headlessAvailable;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Get headless executor instance
|
|
251
|
+
*/
|
|
252
|
+
getHeadlessExecutor() {
|
|
253
|
+
return this.headlessExecutor;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Detect effective CPU count for the current environment.
|
|
257
|
+
*
|
|
258
|
+
* Inside Docker / K8s containers, os.cpus().length reports the HOST cpu
|
|
259
|
+
* count, not the container limit (Node.js #28762 — wontfix). We read
|
|
260
|
+
* cgroup v2 / v1 quota files first so the maxCpuLoad threshold stays
|
|
261
|
+
* meaningful under resource-limited containers.
|
|
262
|
+
*/
|
|
263
|
+
/**
|
|
264
|
+
* #2110 — detect WSL2 / WSL1 so the CPU-load gate can use a sane
|
|
265
|
+
* default. `/proc/loadavg` on WSL maps in Windows-side process counts
|
|
266
|
+
* and routinely reports values 100-1000x larger than real Linux load.
|
|
267
|
+
*
|
|
268
|
+
* Detection order:
|
|
269
|
+
* 1. `WSL_DISTRO_NAME` env var (set by Microsoft's WSL launcher)
|
|
270
|
+
* 2. `WSL_INTEROP` env var (set by recent WSL2)
|
|
271
|
+
* 3. `/proc/sys/kernel/osrelease` contains "microsoft" or "WSL"
|
|
272
|
+
* (kernel build marker; survives env stripping)
|
|
273
|
+
*/
|
|
274
|
+
static isWslEnvironment() {
|
|
275
|
+
if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP)
|
|
276
|
+
return true;
|
|
277
|
+
try {
|
|
278
|
+
const osrelease = readFileSync('/proc/sys/kernel/osrelease', 'utf8').toLowerCase();
|
|
279
|
+
if (osrelease.includes('microsoft') || osrelease.includes('wsl'))
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
catch { /* not on Linux or /proc inaccessible */ }
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
static getEffectiveCpuCount() {
|
|
286
|
+
// 1. Try cgroup v2: /sys/fs/cgroup/cpu.max
|
|
287
|
+
try {
|
|
288
|
+
const cpuMax = readFileSync('/sys/fs/cgroup/cpu.max', 'utf8').trim();
|
|
289
|
+
const [quotaStr, periodStr] = cpuMax.split(' ');
|
|
290
|
+
if (quotaStr !== 'max') {
|
|
291
|
+
const quota = parseInt(quotaStr, 10);
|
|
292
|
+
const period = parseInt(periodStr, 10);
|
|
293
|
+
if (quota > 0 && period > 0)
|
|
294
|
+
return Math.ceil(quota / period);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch { /* not in cgroup v2 */ }
|
|
298
|
+
// 2. Try cgroup v1: /sys/fs/cgroup/cpu/cpu.cfs_quota_us
|
|
299
|
+
try {
|
|
300
|
+
const quota = parseInt(readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_quota_us', 'utf8').trim(), 10);
|
|
301
|
+
const period = parseInt(readFileSync('/sys/fs/cgroup/cpu/cpu.cfs_period_us', 'utf8').trim(), 10);
|
|
302
|
+
if (quota > 0 && period > 0)
|
|
303
|
+
return Math.ceil(quota / period);
|
|
304
|
+
}
|
|
305
|
+
catch { /* not in cgroup v1 */ }
|
|
306
|
+
// 3. Fallback to os.cpus().length
|
|
307
|
+
return cpus().length || 1;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Read daemon-specific config from .claude-flow/config.{json,yaml,yml}.
|
|
311
|
+
* Supports dot-notation keys like 'daemon.resourceThresholds.maxCpuLoad'.
|
|
312
|
+
* #1844: prefer JSON when both exist (existing behavior) but fall back
|
|
313
|
+
* to YAML so operators using the v3 canonical YAML format aren't silently
|
|
314
|
+
* ignored. The chosen path is logged at info level.
|
|
315
|
+
*/
|
|
316
|
+
readDaemonConfigFromFile(claudeFlowDir) {
|
|
317
|
+
const jsonPath = join(claudeFlowDir, 'config.json');
|
|
318
|
+
const yamlPath = join(claudeFlowDir, 'config.yaml');
|
|
319
|
+
const ymlPath = join(claudeFlowDir, 'config.yml');
|
|
320
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
321
|
+
let raw;
|
|
322
|
+
let chosenPath;
|
|
323
|
+
if (existsSync(jsonPath)) {
|
|
324
|
+
try {
|
|
325
|
+
raw = JSON.parse(readFileSync(jsonPath, 'utf-8'));
|
|
326
|
+
chosenPath = jsonPath;
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return {};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
else if (existsSync(yamlPath) || existsSync(ymlPath)) {
|
|
333
|
+
const yPath = existsSync(yamlPath) ? yamlPath : ymlPath;
|
|
334
|
+
try {
|
|
335
|
+
// Lazy-load yaml so the daemon doesn't hard-require it; if the
|
|
336
|
+
// dep isn't installed, fall back to the previous warn-only path.
|
|
337
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
338
|
+
const yamlMod = require('yaml');
|
|
339
|
+
const parsed = yamlMod.parse(readFileSync(yPath, 'utf-8'));
|
|
340
|
+
if (parsed && typeof parsed === 'object') {
|
|
341
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
342
|
+
raw = parsed;
|
|
343
|
+
chosenPath = yPath;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
this.log('warn', `Found ${yPath} but yaml parser unavailable. Install \`yaml\` or convert to JSON. Falling back to defaults.`);
|
|
348
|
+
return {};
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (!raw || !chosenPath) {
|
|
352
|
+
return {};
|
|
353
|
+
}
|
|
354
|
+
this.log('info', `Daemon config loaded from ${chosenPath}`);
|
|
355
|
+
try {
|
|
356
|
+
// Support both flat keys at root and nested under scopes.project
|
|
357
|
+
const cfg = raw?.scopes?.project ?? raw;
|
|
358
|
+
const rawCpuLoad = cfg['daemon.resourceThresholds.maxCpuLoad'] ?? raw['daemon.resourceThresholds.maxCpuLoad'];
|
|
359
|
+
const rawMinMem = cfg['daemon.resourceThresholds.minFreeMemoryPercent'] ?? raw['daemon.resourceThresholds.minFreeMemoryPercent'];
|
|
360
|
+
const rawMaxConcurrent = cfg['daemon.maxConcurrent'] ?? raw['daemon.maxConcurrent'];
|
|
361
|
+
const rawTimeout = cfg['daemon.workerTimeoutMs'] ?? raw['daemon.workerTimeoutMs'];
|
|
362
|
+
// #2356 — lifecycle limits are configured in SECONDS in config.json
|
|
363
|
+
// (`daemon.ttlSecs` / `daemon.idleSecs`) for parity with the CLI flag
|
|
364
|
+
// and env var; stored internally as ms. An explicit 0 disables.
|
|
365
|
+
const rawTtl = cfg['daemon.ttlSecs'] ?? raw['daemon.ttlSecs'];
|
|
366
|
+
const rawIdle = cfg['daemon.idleSecs'] ?? raw['daemon.idleSecs'];
|
|
367
|
+
// #2661 — explicit opt-in for scheduled AI workers.
|
|
368
|
+
const rawAiEnabled = cfg['daemon.aiWorkers.enabled'] ?? raw['daemon.aiWorkers.enabled'];
|
|
369
|
+
return {
|
|
370
|
+
autoStart: typeof raw['daemon.autoStart'] === 'boolean' ? raw['daemon.autoStart'] : undefined,
|
|
371
|
+
maxConcurrent: (typeof rawMaxConcurrent === 'number' && rawMaxConcurrent > 0) ? rawMaxConcurrent : undefined,
|
|
372
|
+
workerTimeoutMs: (typeof rawTimeout === 'number' && rawTimeout > 0) ? rawTimeout : undefined,
|
|
373
|
+
maxCpuLoad: (typeof rawCpuLoad === 'number' && rawCpuLoad > 0 && rawCpuLoad < 1000) ? rawCpuLoad : undefined,
|
|
374
|
+
minFreeMemoryPercent: (typeof rawMinMem === 'number' && rawMinMem >= 0 && rawMinMem <= 100) ? rawMinMem : undefined,
|
|
375
|
+
ttlMs: (typeof rawTtl === 'number' && rawTtl >= 0) ? rawTtl * 1000 : undefined,
|
|
376
|
+
idleShutdownMs: (typeof rawIdle === 'number' && rawIdle >= 0) ? rawIdle * 1000 : undefined,
|
|
377
|
+
aiWorkersEnabled: typeof rawAiEnabled === 'boolean' ? rawAiEnabled : undefined,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return {};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Setup graceful shutdown handlers
|
|
386
|
+
*/
|
|
387
|
+
setupShutdownHandlers() {
|
|
388
|
+
const shutdown = async () => {
|
|
389
|
+
this.log('info', 'Received shutdown signal, stopping daemon...');
|
|
390
|
+
await this.stop();
|
|
391
|
+
process.exit(0);
|
|
392
|
+
};
|
|
393
|
+
process.on('SIGTERM', shutdown);
|
|
394
|
+
process.on('SIGINT', shutdown);
|
|
395
|
+
process.on('SIGHUP', shutdown);
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* #1855: install crash handlers for uncaught exceptions and unhandled
|
|
399
|
+
* rejections. Without these, a thrown error from any timer callback,
|
|
400
|
+
* worker logic path, or transitive import crashes the daemon process
|
|
401
|
+
* silently — the PID file leaks and any in-flight child processes
|
|
402
|
+
* orphan. With these, we log a structured crash record, run stop()
|
|
403
|
+
* to clean up, then exit 1 so the process actually dies (otherwise
|
|
404
|
+
* Node would crash anyway after the handler returns).
|
|
405
|
+
*/
|
|
406
|
+
installCrashHandlers() {
|
|
407
|
+
const onCrash = (kind, err) => {
|
|
408
|
+
// Best-effort logging; never throw from inside the crash handler.
|
|
409
|
+
try {
|
|
410
|
+
this.writeCrashRecord(kind, err);
|
|
411
|
+
}
|
|
412
|
+
catch { /* nothing more we can do */ }
|
|
413
|
+
try {
|
|
414
|
+
// Synchronous stop — don't await; the process is dying. Just
|
|
415
|
+
// remove the PID file and snapshot state so the next start
|
|
416
|
+
// sees a clean slate.
|
|
417
|
+
this.removePidFile();
|
|
418
|
+
this.saveState();
|
|
419
|
+
// Snapshot any in-flight child PIDs one last time so the next
|
|
420
|
+
// lifetime can reap them.
|
|
421
|
+
this.writeChildrenSnapshot();
|
|
422
|
+
}
|
|
423
|
+
catch { /* ignore */ }
|
|
424
|
+
// Exit non-zero so supervisors / shells see the failure.
|
|
425
|
+
process.exit(1);
|
|
426
|
+
};
|
|
427
|
+
process.on('uncaughtException', (err) => onCrash('uncaughtException', err));
|
|
428
|
+
process.on('unhandledRejection', (err) => onCrash('unhandledRejection', err));
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Append a structured crash record to .claude-flow/logs/crash.log.
|
|
432
|
+
* Inspectable by hand or via `ruflo daemon status` follow-ups.
|
|
433
|
+
*/
|
|
434
|
+
writeCrashRecord(kind, err) {
|
|
435
|
+
const logDir = this.config.logDir;
|
|
436
|
+
if (!existsSync(logDir))
|
|
437
|
+
mkdirSync(logDir, { recursive: true });
|
|
438
|
+
const crashLog = join(logDir, 'crash.log');
|
|
439
|
+
const ts = new Date().toISOString();
|
|
440
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
441
|
+
const stack = err instanceof Error && err.stack ? err.stack : '<no stack>';
|
|
442
|
+
const record = `[${ts}] [${kind}] pid=${process.pid} ${message}\n${stack}\n---\n`;
|
|
443
|
+
appendFileSync(crashLog, record, 'utf-8');
|
|
444
|
+
this.log('warn', `Daemon crashed (${kind}): ${message} — see ${crashLog}`);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Path to the on-disk children registry — list of headless worker
|
|
448
|
+
* child PIDs the daemon currently owns. #1855: written on every
|
|
449
|
+
* execution:start / :complete / :error transition; read by the next
|
|
450
|
+
* lifetime to reap orphans after a hard crash.
|
|
451
|
+
*/
|
|
452
|
+
get childrenFile() {
|
|
453
|
+
return join(this.projectRoot, '.claude-flow', 'daemon-children.json');
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* #1856: detect workers that were mid-flight when the previous daemon
|
|
457
|
+
* lifetime ended. A mid-flight worker has `lastStartedAt > lastRun`
|
|
458
|
+
* (started after the last successful completion). On crash recovery
|
|
459
|
+
* we count these as failures so the run-counter math stays consistent
|
|
460
|
+
* (`runCount === successCount + failureCount`). Workers naturally
|
|
461
|
+
* retry at their next scheduled interval; we deliberately don't
|
|
462
|
+
* immediately re-run because the failure may have been deterministic.
|
|
463
|
+
*/
|
|
464
|
+
detectMidFlightFailures() {
|
|
465
|
+
let detected = 0;
|
|
466
|
+
for (const [type, state] of this.workers.entries()) {
|
|
467
|
+
const startedAt = state.lastStartedAt?.getTime() ?? 0;
|
|
468
|
+
const lastRunAt = state.lastRun?.getTime() ?? 0;
|
|
469
|
+
// started after the last successful completion → was mid-flight
|
|
470
|
+
if (startedAt > 0 && startedAt > lastRunAt) {
|
|
471
|
+
state.failureCount++;
|
|
472
|
+
state.isRunning = false;
|
|
473
|
+
// Don't bump runCount — it was already incremented at start
|
|
474
|
+
this.log('info', `Worker ${type} was mid-flight at last crash (started ${state.lastStartedAt?.toISOString()}); counted as failure, will retry at next scheduled interval`);
|
|
475
|
+
detected++;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (detected > 0) {
|
|
479
|
+
this.saveState();
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Snapshot the currently-active headless worker child PIDs to disk.
|
|
484
|
+
* Best-effort; failures don't propagate.
|
|
485
|
+
*/
|
|
486
|
+
writeChildrenSnapshot() {
|
|
487
|
+
if (!this.headlessExecutor)
|
|
488
|
+
return;
|
|
489
|
+
try {
|
|
490
|
+
const pids = this.headlessExecutor.getActiveChildPids();
|
|
491
|
+
const dir = join(this.projectRoot, '.claude-flow');
|
|
492
|
+
if (!existsSync(dir))
|
|
493
|
+
mkdirSync(dir, { recursive: true });
|
|
494
|
+
writeFileSync(this.childrenFile, JSON.stringify({ pids, daemonPid: process.pid, timestamp: new Date().toISOString() }, null, 2), 'utf-8');
|
|
495
|
+
}
|
|
496
|
+
catch { /* best-effort */ }
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* #1855: reap orphan headless worker children left behind by a
|
|
500
|
+
* previous crashed lifetime. Reads `.claude-flow/daemon-children.json`,
|
|
501
|
+
* SIGTERMs any PID still alive that doesn't belong to the current
|
|
502
|
+
* daemon, then truncates the file. Called at the top of `start()`
|
|
503
|
+
* so the next lifetime starts with a clean process tree.
|
|
504
|
+
*/
|
|
505
|
+
reapOrphanedChildren() {
|
|
506
|
+
const file = this.childrenFile;
|
|
507
|
+
if (!existsSync(file))
|
|
508
|
+
return;
|
|
509
|
+
let snapshot;
|
|
510
|
+
try {
|
|
511
|
+
snapshot = JSON.parse(readFileSync(file, 'utf-8'));
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
try {
|
|
515
|
+
unlinkSync(file);
|
|
516
|
+
}
|
|
517
|
+
catch { /* ignore */ }
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const pids = Array.isArray(snapshot.pids) ? snapshot.pids : [];
|
|
521
|
+
let reaped = 0;
|
|
522
|
+
for (const pid of pids) {
|
|
523
|
+
if (typeof pid !== 'number' || pid <= 0)
|
|
524
|
+
continue;
|
|
525
|
+
if (pid === process.pid)
|
|
526
|
+
continue; // never our own PID
|
|
527
|
+
try {
|
|
528
|
+
process.kill(pid, 0); // is alive?
|
|
529
|
+
process.kill(pid, 'SIGTERM');
|
|
530
|
+
reaped++;
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
533
|
+
// already dead — fine
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (reaped > 0) {
|
|
537
|
+
this.log('info', `Reaped ${reaped} orphan headless worker child(ren) from previous lifetime`);
|
|
538
|
+
}
|
|
539
|
+
try {
|
|
540
|
+
unlinkSync(file);
|
|
541
|
+
}
|
|
542
|
+
catch { /* ignore */ }
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Check if system resources allow worker execution
|
|
546
|
+
*/
|
|
547
|
+
async canRunWorker() {
|
|
548
|
+
const os = await import('os');
|
|
549
|
+
const cpuLoad = os.loadavg()[0];
|
|
550
|
+
const totalMem = os.totalmem();
|
|
551
|
+
const freeMem = os.freemem();
|
|
552
|
+
const freePercent = (freeMem / totalMem) * 100;
|
|
553
|
+
if (cpuLoad > this.config.resourceThresholds.maxCpuLoad) {
|
|
554
|
+
return { allowed: false, reason: `CPU load too high: ${cpuLoad.toFixed(2)}` };
|
|
555
|
+
}
|
|
556
|
+
if (freePercent < this.config.resourceThresholds.minFreeMemoryPercent) {
|
|
557
|
+
return { allowed: false, reason: `Memory too low: ${freePercent.toFixed(1)}% free` };
|
|
558
|
+
}
|
|
559
|
+
return { allowed: true };
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Process pending workers queue
|
|
563
|
+
*
|
|
564
|
+
* When executeWorkerWithConcurrencyControl defers a worker (returns null),
|
|
565
|
+
* we break immediately to avoid a busy-wait loop — the deferred worker is
|
|
566
|
+
* already back on the pendingWorkers queue by that point. If no workers are
|
|
567
|
+
* currently running when we break, we schedule a backoff retry so the queue
|
|
568
|
+
* does not get permanently stuck.
|
|
569
|
+
*/
|
|
570
|
+
async processPendingWorkers() {
|
|
571
|
+
while (this.pendingWorkers.length > 0 && this.runningWorkers.size < this.config.maxConcurrent) {
|
|
572
|
+
const workerType = this.pendingWorkers.shift();
|
|
573
|
+
const workerConfig = this.config.workers.find(w => w.type === workerType);
|
|
574
|
+
if (workerConfig) {
|
|
575
|
+
const result = await this.executeWorkerWithConcurrencyControl(workerConfig);
|
|
576
|
+
if (result === null) {
|
|
577
|
+
// Worker was deferred (resource pressure or concurrency limit).
|
|
578
|
+
// Break to avoid tight-looping — the next executeWorker() completion
|
|
579
|
+
// will call processPendingWorkers() again via the finally block.
|
|
580
|
+
if (this.runningWorkers.size === 0) {
|
|
581
|
+
// No workers running means nobody will trigger the finally-block
|
|
582
|
+
// callback, so schedule a backoff retry to avoid a stuck queue.
|
|
583
|
+
setTimeout(() => this.processPendingWorkers(), 30_000).unref();
|
|
584
|
+
}
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
initializeWorkerStates() {
|
|
591
|
+
// Try to restore state from file
|
|
592
|
+
if (existsSync(this.config.stateFile)) {
|
|
593
|
+
try {
|
|
594
|
+
const saved = JSON.parse(readFileSync(this.config.stateFile, 'utf-8'));
|
|
595
|
+
// CRITICAL: Restore worker config (including enabled flag) from saved state
|
|
596
|
+
// This fixes #950: daemon enable command not persisting worker state
|
|
597
|
+
if (saved.config?.workers && Array.isArray(saved.config.workers)) {
|
|
598
|
+
for (const savedWorker of saved.config.workers) {
|
|
599
|
+
const workerConfig = this.config.workers.find(w => w.type === savedWorker.type);
|
|
600
|
+
if (workerConfig && typeof savedWorker.enabled === 'boolean') {
|
|
601
|
+
workerConfig.enabled = savedWorker.enabled;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
// Restore resourceThresholds, maxConcurrent, workerTimeoutMs from saved state
|
|
606
|
+
// Only restore if valid numeric values within sane ranges
|
|
607
|
+
if (saved.config?.resourceThresholds && !this.originalConfig?.resourceThresholds) {
|
|
608
|
+
const rt = saved.config.resourceThresholds;
|
|
609
|
+
if (typeof rt.maxCpuLoad === 'number' && rt.maxCpuLoad > 0 && rt.maxCpuLoad < 1000) {
|
|
610
|
+
this.config.resourceThresholds.maxCpuLoad = rt.maxCpuLoad;
|
|
611
|
+
}
|
|
612
|
+
if (typeof rt.minFreeMemoryPercent === 'number' && rt.minFreeMemoryPercent >= 0 && rt.minFreeMemoryPercent <= 100) {
|
|
613
|
+
this.config.resourceThresholds.minFreeMemoryPercent = rt.minFreeMemoryPercent;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (typeof saved.config?.maxConcurrent === 'number' && saved.config.maxConcurrent > 0) {
|
|
617
|
+
this.config.maxConcurrent = saved.config.maxConcurrent;
|
|
618
|
+
}
|
|
619
|
+
if (typeof saved.config?.workerTimeoutMs === 'number' && saved.config.workerTimeoutMs > 0) {
|
|
620
|
+
this.config.workerTimeoutMs = saved.config.workerTimeoutMs;
|
|
621
|
+
}
|
|
622
|
+
// Restore worker runtime states (runCount, successCount, etc.)
|
|
623
|
+
if (saved.workers) {
|
|
624
|
+
for (const [type, state] of Object.entries(saved.workers)) {
|
|
625
|
+
const savedState = state;
|
|
626
|
+
const lastRunValue = savedState.lastRun;
|
|
627
|
+
const lastStartedAtValue = savedState.lastStartedAt;
|
|
628
|
+
this.workers.set(type, {
|
|
629
|
+
runCount: savedState.runCount || 0,
|
|
630
|
+
successCount: savedState.successCount || 0,
|
|
631
|
+
failureCount: savedState.failureCount || 0,
|
|
632
|
+
averageDurationMs: savedState.averageDurationMs || 0,
|
|
633
|
+
lastRun: lastRunValue ? new Date(lastRunValue) : undefined,
|
|
634
|
+
lastStartedAt: lastStartedAtValue ? new Date(lastStartedAtValue) : undefined,
|
|
635
|
+
nextRun: undefined,
|
|
636
|
+
isRunning: false,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
catch {
|
|
642
|
+
// Ignore parse errors, start fresh
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
// Initialize any missing workers
|
|
646
|
+
for (const workerConfig of this.config.workers) {
|
|
647
|
+
if (!this.workers.has(workerConfig.type)) {
|
|
648
|
+
this.workers.set(workerConfig.type, {
|
|
649
|
+
runCount: 0,
|
|
650
|
+
successCount: 0,
|
|
651
|
+
failureCount: 0,
|
|
652
|
+
averageDurationMs: 0,
|
|
653
|
+
isRunning: false,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Get the PID file path for singleton enforcement (#1395 Bug 3).
|
|
660
|
+
*/
|
|
661
|
+
get pidFile() {
|
|
662
|
+
return join(this.projectRoot, '.claude-flow', 'daemon.pid');
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Check if another daemon instance is already running.
|
|
666
|
+
* Returns the existing PID if alive, or null if no daemon is running.
|
|
667
|
+
*
|
|
668
|
+
* #1853: ignore self-PID matches. The detached-spawn path in
|
|
669
|
+
* `commands/daemon.ts` writes the child's PID into the file as a
|
|
670
|
+
* fallback after a 500ms wait. If the child reaches `start()` slower
|
|
671
|
+
* than the parent's 500ms wait (observed on Node 25 / macOS 26), the
|
|
672
|
+
* child reads its own PID back from the file and concludes "another
|
|
673
|
+
* daemon is already running" — so it exits before scheduling workers
|
|
674
|
+
* and `daemon status` reports STOPPED forever. A daemon process is
|
|
675
|
+
* never "another instance" of itself; treat self-match as absence.
|
|
676
|
+
*/
|
|
677
|
+
checkExistingDaemon() {
|
|
678
|
+
if (!existsSync(this.pidFile))
|
|
679
|
+
return null;
|
|
680
|
+
try {
|
|
681
|
+
const pid = parseInt(readFileSync(this.pidFile, 'utf-8').trim(), 10);
|
|
682
|
+
if (isNaN(pid))
|
|
683
|
+
return null;
|
|
684
|
+
// #1853: a PID file containing our own PID is not "another daemon".
|
|
685
|
+
// Treat as absent so the start() path proceeds normally.
|
|
686
|
+
if (pid === process.pid)
|
|
687
|
+
return null;
|
|
688
|
+
// Check if process is alive (signal 0 = existence check)
|
|
689
|
+
process.kill(pid, 0);
|
|
690
|
+
return pid; // Process is alive
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
// Process is dead — clean up stale PID file
|
|
694
|
+
try {
|
|
695
|
+
unlinkSync(this.pidFile);
|
|
696
|
+
}
|
|
697
|
+
catch { /* ignore */ }
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Write PID file for singleton enforcement.
|
|
703
|
+
*/
|
|
704
|
+
writePidFile() {
|
|
705
|
+
const dir = join(this.projectRoot, '.claude-flow');
|
|
706
|
+
if (!existsSync(dir))
|
|
707
|
+
mkdirSync(dir, { recursive: true });
|
|
708
|
+
writeFileSync(this.pidFile, String(process.pid), 'utf-8');
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Remove PID file on shutdown.
|
|
712
|
+
*/
|
|
713
|
+
removePidFile() {
|
|
714
|
+
try {
|
|
715
|
+
unlinkSync(this.pidFile);
|
|
716
|
+
}
|
|
717
|
+
catch { /* ignore */ }
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Start the daemon and all enabled workers
|
|
721
|
+
*/
|
|
722
|
+
async start() {
|
|
723
|
+
if (this.running) {
|
|
724
|
+
this.emit('warning', 'Daemon already running');
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
// PID singleton enforcement (#1395 Bug 3): prevent daemon accumulation
|
|
728
|
+
const existingPid = this.checkExistingDaemon();
|
|
729
|
+
if (existingPid !== null) {
|
|
730
|
+
this.log('info', `Daemon already running (PID: ${existingPid}), skipping start`);
|
|
731
|
+
this.emit('warning', `Daemon already running (PID: ${existingPid})`);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
// #1855: reap orphan headless worker children left by a previous
|
|
735
|
+
// crashed lifetime, BEFORE we mark ourselves running and start
|
|
736
|
+
// accepting new work. The children file from the prior daemon's
|
|
737
|
+
// last-snapshot is the authoritative list.
|
|
738
|
+
this.reapOrphanedChildren();
|
|
739
|
+
// #1856: detect workers that were mid-flight at the previous crash
|
|
740
|
+
// and count them as failures so runCount/successCount/failureCount
|
|
741
|
+
// stay consistent. Workers retry naturally at their next scheduled
|
|
742
|
+
// interval — we don't immediately re-run them, which avoids a
|
|
743
|
+
// freshly-recovered daemon hammering the same code path that just
|
|
744
|
+
// killed it.
|
|
745
|
+
this.detectMidFlightFailures();
|
|
746
|
+
this.running = true;
|
|
747
|
+
this.startedAt = new Date();
|
|
748
|
+
this.writePidFile();
|
|
749
|
+
this.emit('started', { pid: process.pid, startedAt: this.startedAt });
|
|
750
|
+
// #2661 root-fix — resolve repository identity and register/attempt
|
|
751
|
+
// election immediately at start, not just on the first 60s lifecycle
|
|
752
|
+
// tick, so `daemon status` reflects supervisor state right away. Only
|
|
753
|
+
// meaningful when AI workers are enabled (see field doc comment).
|
|
754
|
+
if (this.config.aiWorkersEnabled) {
|
|
755
|
+
this.gitIdentity = resolveGitWorkspaceIdentity(this.projectRoot);
|
|
756
|
+
void this.renewLeaseAndSupervisor();
|
|
757
|
+
}
|
|
758
|
+
// Schedule all enabled workers
|
|
759
|
+
for (const workerConfig of this.config.workers) {
|
|
760
|
+
if (workerConfig.enabled) {
|
|
761
|
+
this.scheduleWorker(workerConfig);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
// #1845: poll the MCP-dispatch queue directory so workers requested
|
|
765
|
+
// via mcp__hooks_worker-dispatch (in a separate process) actually
|
|
766
|
+
// execute here. Previously the dispatch wrote to a process-local Map
|
|
767
|
+
// that the daemon could never see.
|
|
768
|
+
this.queuePollTimer = setInterval(() => {
|
|
769
|
+
void this.processDispatchQueue();
|
|
770
|
+
}, 5_000);
|
|
771
|
+
if (typeof this.queuePollTimer.unref === 'function') {
|
|
772
|
+
this.queuePollTimer.unref();
|
|
773
|
+
}
|
|
774
|
+
// #2356: self-terminating lifecycle. Without an upper bound on lifetime a
|
|
775
|
+
// forgotten daemon keeps dispatching headless worker sweeps for days.
|
|
776
|
+
this.startLifecycleMonitor();
|
|
777
|
+
// Save state
|
|
778
|
+
this.saveState();
|
|
779
|
+
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}%)`);
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* #1845: ingest queue entries written by mcp__hooks_worker-dispatch.
|
|
783
|
+
* Each entry is a JSON file at `.claude-flow/daemon-queue/<id>.json`
|
|
784
|
+
* with `{ workerId, trigger, context, enqueuedAt }`. We move processed
|
|
785
|
+
* files to `.claude-flow/daemon-queue/.processed/` so the daemon never
|
|
786
|
+
* re-runs the same dispatch and operators can inspect history.
|
|
787
|
+
*/
|
|
788
|
+
async processDispatchQueue() {
|
|
789
|
+
if (!this.running)
|
|
790
|
+
return;
|
|
791
|
+
const queueDir = join(this.projectRoot, '.claude-flow', 'daemon-queue');
|
|
792
|
+
if (!existsSync(queueDir))
|
|
793
|
+
return;
|
|
794
|
+
let entries;
|
|
795
|
+
try {
|
|
796
|
+
const fs = await import('fs');
|
|
797
|
+
entries = fs.readdirSync(queueDir).filter((n) => n.endsWith('.json'));
|
|
798
|
+
}
|
|
799
|
+
catch {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
if (entries.length === 0)
|
|
803
|
+
return;
|
|
804
|
+
const fs = await import('fs');
|
|
805
|
+
const processedDir = join(queueDir, '.processed');
|
|
806
|
+
if (!existsSync(processedDir)) {
|
|
807
|
+
try {
|
|
808
|
+
fs.mkdirSync(processedDir, { recursive: true });
|
|
809
|
+
}
|
|
810
|
+
catch { /* race ok */ }
|
|
811
|
+
}
|
|
812
|
+
for (const entry of entries) {
|
|
813
|
+
const src = join(queueDir, entry);
|
|
814
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
815
|
+
let payload;
|
|
816
|
+
try {
|
|
817
|
+
payload = JSON.parse(fs.readFileSync(src, 'utf-8'));
|
|
818
|
+
}
|
|
819
|
+
catch {
|
|
820
|
+
// Malformed entry — quarantine so we don't loop on it
|
|
821
|
+
try {
|
|
822
|
+
fs.renameSync(src, join(processedDir, `bad-${entry}`));
|
|
823
|
+
}
|
|
824
|
+
catch { /* nothing more we can do */ }
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
const trigger = payload?.trigger;
|
|
828
|
+
const workerId = payload?.workerId;
|
|
829
|
+
if (!trigger || !this.config.workers.some((w) => w.type === trigger)) {
|
|
830
|
+
try {
|
|
831
|
+
fs.renameSync(src, join(processedDir, `unknown-${entry}`));
|
|
832
|
+
}
|
|
833
|
+
catch { /* ok */ }
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
try {
|
|
837
|
+
this.log('info', `Dequeued ${trigger}${workerId ? ` (id=${workerId})` : ''} from MCP dispatch queue`);
|
|
838
|
+
await this.triggerWorker(trigger);
|
|
839
|
+
}
|
|
840
|
+
catch (err) {
|
|
841
|
+
this.log('warn', `Queued worker ${trigger} failed: ${err.message}`);
|
|
842
|
+
}
|
|
843
|
+
finally {
|
|
844
|
+
try {
|
|
845
|
+
fs.renameSync(src, join(processedDir, entry));
|
|
846
|
+
}
|
|
847
|
+
catch { /* ignore */ }
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Stop the daemon and all workers
|
|
853
|
+
*/
|
|
854
|
+
async stop() {
|
|
855
|
+
if (!this.running) {
|
|
856
|
+
this.emit('warning', 'Daemon not running');
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
// Clear all timers (convert to array to avoid iterator issues)
|
|
860
|
+
const timerEntries = Array.from(this.timers.entries());
|
|
861
|
+
for (const [type, timer] of timerEntries) {
|
|
862
|
+
clearTimeout(timer);
|
|
863
|
+
this.log('info', `Stopped worker: ${type}`);
|
|
864
|
+
}
|
|
865
|
+
this.timers.clear();
|
|
866
|
+
// #1845: stop the MCP-dispatch queue poller too.
|
|
867
|
+
if (this.queuePollTimer) {
|
|
868
|
+
clearInterval(this.queuePollTimer);
|
|
869
|
+
this.queuePollTimer = undefined;
|
|
870
|
+
}
|
|
871
|
+
// #2356: stop the TTL/idle lifecycle monitor.
|
|
872
|
+
if (this.lifecycleTimer) {
|
|
873
|
+
clearInterval(this.lifecycleTimer);
|
|
874
|
+
this.lifecycleTimer = undefined;
|
|
875
|
+
}
|
|
876
|
+
// #2661 — reap in-flight headless `claude --print` children. They run
|
|
877
|
+
// detached (own process group on POSIX) and would otherwise outlive the
|
|
878
|
+
// daemon; `daemon stop --all` relies on SIGTERM → this path to cancel
|
|
879
|
+
// active Claude process groups.
|
|
880
|
+
if (this.headlessExecutor) {
|
|
881
|
+
try {
|
|
882
|
+
this.headlessExecutor.cancelAll();
|
|
883
|
+
}
|
|
884
|
+
catch { /* best-effort */ }
|
|
885
|
+
}
|
|
886
|
+
// #2661 root-fix — release this worktree's lease and, if held,
|
|
887
|
+
// supervisor status, so a sibling worktree's daemon can take over the
|
|
888
|
+
// schedule within its next tick instead of waiting out the 3-minute
|
|
889
|
+
// supervisor staleness window. Best-effort — a graceful release is an
|
|
890
|
+
// optimization; the staleness timeout is what actually bounds a crash.
|
|
891
|
+
if (this.config.aiWorkersEnabled && this.gitIdentity) {
|
|
892
|
+
const { repositoryId, worktreeRoot } = this.gitIdentity;
|
|
893
|
+
try {
|
|
894
|
+
await getRepoSupervisorRegistry().release(repositoryId, worktreeRoot);
|
|
895
|
+
await getWorkspaceLeaseRegistry().release(repositoryId, worktreeRoot);
|
|
896
|
+
}
|
|
897
|
+
catch { /* best-effort */ }
|
|
898
|
+
}
|
|
899
|
+
this.running = false;
|
|
900
|
+
this.removePidFile();
|
|
901
|
+
this.saveState();
|
|
902
|
+
this.emit('stopped', { stoppedAt: new Date() });
|
|
903
|
+
this.log('info', 'Daemon stopped');
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* #2356 — Self-terminating lifecycle monitor. A daemon with no upper bound
|
|
907
|
+
* on its lifetime is the documented root cause of multi-day token leaks:
|
|
908
|
+
* each interval worker spawns a headless `claude --print` sweep, so a daemon
|
|
909
|
+
* left running for days dispatches tens of thousands of sessions invisibly.
|
|
910
|
+
* This timer enforces a max age (`ttlMs`) and an optional idle window
|
|
911
|
+
* (`idleShutdownMs`), shutting the daemon down gracefully when either trips.
|
|
912
|
+
* Checked once a minute and `unref()`'d so it never keeps the process alive
|
|
913
|
+
* on its own. A no-op when both limits are disabled (0).
|
|
914
|
+
*/
|
|
915
|
+
startLifecycleMonitor() {
|
|
916
|
+
const ttlMs = this.config.ttlMs;
|
|
917
|
+
const idleMs = this.config.idleShutdownMs;
|
|
918
|
+
// #2661 — unlike ttl/idle (both optional), the workspace-removal check
|
|
919
|
+
// always runs, so the monitor is no longer skipped when both limits are
|
|
920
|
+
// disabled. A daemon whose worktree was deleted must not keep running.
|
|
921
|
+
const CHECK_INTERVAL_MS = 60_000;
|
|
922
|
+
this.lifecycleTimer = setInterval(() => {
|
|
923
|
+
if (!this.running)
|
|
924
|
+
return;
|
|
925
|
+
const reason = this.lifecycleShutdownReason(Date.now());
|
|
926
|
+
if (reason) {
|
|
927
|
+
void this.selfShutdown(reason);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
// #2661 root-fix — renew this worktree's lease + supervisor status on
|
|
931
|
+
// the same cadence. Both windows (15min lease TTL, 3min supervisor
|
|
932
|
+
// staleness) comfortably outlive a single missed 60s tick.
|
|
933
|
+
if (this.config.aiWorkersEnabled) {
|
|
934
|
+
void this.renewLeaseAndSupervisor();
|
|
935
|
+
}
|
|
936
|
+
}, CHECK_INTERVAL_MS);
|
|
937
|
+
if (typeof this.lifecycleTimer.unref === 'function') {
|
|
938
|
+
this.lifecycleTimer.unref();
|
|
939
|
+
}
|
|
940
|
+
const parts = ['workspace-removal'];
|
|
941
|
+
if (ttlMs > 0)
|
|
942
|
+
parts.push(`ttl=${Math.round(ttlMs / 1000)}s`);
|
|
943
|
+
if (idleMs > 0)
|
|
944
|
+
parts.push(`idle=${Math.round(idleMs / 1000)}s`);
|
|
945
|
+
this.log('info', `Lifecycle monitor active (${parts.join(', ')})`);
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Decide whether the daemon should self-shutdown, and why. Extracted from
|
|
949
|
+
* the lifecycle timer so it is testable without racing a 60s interval or
|
|
950
|
+
* calling process.exit().
|
|
951
|
+
*
|
|
952
|
+
* #2661 (invariant 6, containment form): a removed worktree makes its
|
|
953
|
+
* daemon ineligible within one check interval — the daemon detects that
|
|
954
|
+
* its workspace directory is gone and shuts down instead of continuing to
|
|
955
|
+
* schedule jobs against a deleted tree. The full lease architecture
|
|
956
|
+
* (supervisor-dispatched jobs, heartbeats) is follow-up work; this stops
|
|
957
|
+
* the leak where recreated/removed worktrees leave schedulers behind.
|
|
958
|
+
*/
|
|
959
|
+
lifecycleShutdownReason(now) {
|
|
960
|
+
if (!existsSync(this.projectRoot)) {
|
|
961
|
+
return 'workspace directory removed (#2661)';
|
|
962
|
+
}
|
|
963
|
+
const ttlMs = this.config.ttlMs;
|
|
964
|
+
const idleMs = this.config.idleShutdownMs;
|
|
965
|
+
const startedMs = this.startedAt?.getTime() ?? now;
|
|
966
|
+
if (ttlMs > 0 && now - startedMs >= ttlMs) {
|
|
967
|
+
return `max age ${Math.round(ttlMs / 1000)}s reached`;
|
|
968
|
+
}
|
|
969
|
+
if (idleMs > 0) {
|
|
970
|
+
const lastActivity = this.lastWorkerActivityMs() ?? startedMs;
|
|
971
|
+
if (now - lastActivity >= idleMs) {
|
|
972
|
+
return `idle for ${Math.round(idleMs / 1000)}s (no worker activity)`;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Most recent worker start/finish time across all workers (epoch ms), or
|
|
979
|
+
* null if no worker has ever started. Used for idle-shutdown detection.
|
|
980
|
+
*/
|
|
981
|
+
lastWorkerActivityMs() {
|
|
982
|
+
let latest = null;
|
|
983
|
+
for (const state of this.workers.values()) {
|
|
984
|
+
for (const t of [state.lastRun, state.lastStartedAt]) {
|
|
985
|
+
if (t) {
|
|
986
|
+
const ms = t.getTime();
|
|
987
|
+
if (latest === null || ms > latest)
|
|
988
|
+
latest = ms;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return latest;
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Graceful self-shutdown triggered by the lifecycle monitor. Mirrors the
|
|
996
|
+
* signal-handler path (`stop()` then `process.exit(0)`) because the
|
|
997
|
+
* foreground keep-alive in the daemon command is a *ref'd* `setInterval`
|
|
998
|
+
* that would otherwise hold the process open after `stop()` clears the
|
|
999
|
+
* service timers — leaving a zombie that reports stopped but never exits.
|
|
1000
|
+
*/
|
|
1001
|
+
async selfShutdown(reason) {
|
|
1002
|
+
this.log('info', `Daemon self-shutdown: ${reason}`);
|
|
1003
|
+
this.emit('self-shutdown', { reason });
|
|
1004
|
+
try {
|
|
1005
|
+
await this.stop();
|
|
1006
|
+
}
|
|
1007
|
+
catch { /* best-effort — we are exiting regardless */ }
|
|
1008
|
+
process.exit(0);
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* #2661 root-fix — heartbeat this worktree's lease and attempt/renew
|
|
1012
|
+
* repository-supervisor election. Best-effort: any failure here must
|
|
1013
|
+
* never affect worker scheduling correctness (the budget ledger remains
|
|
1014
|
+
* the hard invariant regardless of supervisor state) — a daemon that
|
|
1015
|
+
* can't safely coordinate simply falls back to "not supervisor" and runs
|
|
1016
|
+
* its cheap local-only workers, same as any other lease-only worktree.
|
|
1017
|
+
*/
|
|
1018
|
+
async renewLeaseAndSupervisor() {
|
|
1019
|
+
if (!this.gitIdentity)
|
|
1020
|
+
return;
|
|
1021
|
+
const { repositoryId, worktreeRoot } = this.gitIdentity;
|
|
1022
|
+
try {
|
|
1023
|
+
await getWorkspaceLeaseRegistry().heartbeat(repositoryId, worktreeRoot);
|
|
1024
|
+
await getRepoSupervisorRegistry().electOrRenew(repositoryId, worktreeRoot);
|
|
1025
|
+
this.lastSupervisorRenewalMs = Date.now();
|
|
1026
|
+
}
|
|
1027
|
+
catch { /* best-effort — see docstring */ }
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Cheap read-only check: does THIS daemon currently hold repository
|
|
1031
|
+
* supervisor status? Used to gate recurring headless (`claude --print`)
|
|
1032
|
+
* execution — see repo-supervisor.ts's module doc comment for the full
|
|
1033
|
+
* rationale. Never true when AI workers are disabled or the git identity
|
|
1034
|
+
* couldn't be resolved (e.g. a non-git directory).
|
|
1035
|
+
*/
|
|
1036
|
+
isRepositorySupervisor() {
|
|
1037
|
+
if (!this.config.aiWorkersEnabled || !this.gitIdentity)
|
|
1038
|
+
return false;
|
|
1039
|
+
return getRepoSupervisorRegistry().isSupervisor(this.gitIdentity.repositoryId, this.gitIdentity.worktreeRoot);
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Get daemon status
|
|
1043
|
+
*/
|
|
1044
|
+
getStatus() {
|
|
1045
|
+
let supervisor = null;
|
|
1046
|
+
if (this.config.aiWorkersEnabled && this.gitIdentity) {
|
|
1047
|
+
const { repositoryId, worktreeRoot } = this.gitIdentity;
|
|
1048
|
+
supervisor = {
|
|
1049
|
+
repositoryId,
|
|
1050
|
+
isSupervisor: getRepoSupervisorRegistry().isSupervisor(repositoryId, worktreeRoot),
|
|
1051
|
+
record: getRepoSupervisorRegistry().getRecord(repositoryId),
|
|
1052
|
+
activeLeases: getWorkspaceLeaseRegistry().listActive(repositoryId).length,
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
return {
|
|
1056
|
+
running: this.running,
|
|
1057
|
+
pid: process.pid,
|
|
1058
|
+
startedAt: this.startedAt,
|
|
1059
|
+
workers: new Map(this.workers),
|
|
1060
|
+
config: this.config,
|
|
1061
|
+
supervisor,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Schedule a worker to run at intervals with staggered start
|
|
1066
|
+
*/
|
|
1067
|
+
scheduleWorker(workerConfig) {
|
|
1068
|
+
const state = this.workers.get(workerConfig.type);
|
|
1069
|
+
const internalConfig = workerConfig;
|
|
1070
|
+
const staggerOffset = internalConfig.offsetMs || 0;
|
|
1071
|
+
// Calculate initial delay with stagger offset
|
|
1072
|
+
let initialDelay = staggerOffset;
|
|
1073
|
+
if (state.lastRun) {
|
|
1074
|
+
const timeSinceLastRun = Date.now() - state.lastRun.getTime();
|
|
1075
|
+
initialDelay = Math.max(staggerOffset, workerConfig.intervalMs - timeSinceLastRun);
|
|
1076
|
+
}
|
|
1077
|
+
state.nextRun = new Date(Date.now() + initialDelay);
|
|
1078
|
+
const runAndReschedule = async () => {
|
|
1079
|
+
if (!this.running)
|
|
1080
|
+
return;
|
|
1081
|
+
// Use concurrency-controlled execution (P0 fix)
|
|
1082
|
+
await this.executeWorkerWithConcurrencyControl(workerConfig);
|
|
1083
|
+
// Reschedule
|
|
1084
|
+
if (this.running) {
|
|
1085
|
+
const timer = setTimeout(runAndReschedule, workerConfig.intervalMs);
|
|
1086
|
+
this.timers.set(workerConfig.type, timer);
|
|
1087
|
+
state.nextRun = new Date(Date.now() + workerConfig.intervalMs);
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
// Schedule first run with stagger offset
|
|
1091
|
+
const timer = setTimeout(runAndReschedule, initialDelay);
|
|
1092
|
+
this.timers.set(workerConfig.type, timer);
|
|
1093
|
+
this.log('info', `Scheduled ${workerConfig.type} (interval: ${workerConfig.intervalMs / 1000}s, first run in ${initialDelay / 1000}s)`);
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Execute a worker with concurrency control (P0 fix)
|
|
1097
|
+
*/
|
|
1098
|
+
async executeWorkerWithConcurrencyControl(workerConfig) {
|
|
1099
|
+
// Check concurrency limit
|
|
1100
|
+
if (this.runningWorkers.size >= this.config.maxConcurrent) {
|
|
1101
|
+
this.log('info', `Worker ${workerConfig.type} deferred: max concurrent (${this.config.maxConcurrent}) reached`);
|
|
1102
|
+
this.pendingWorkers.push(workerConfig.type);
|
|
1103
|
+
this.emit('worker:deferred', { type: workerConfig.type, reason: 'max_concurrent' });
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
// Check resource availability
|
|
1107
|
+
const resourceCheck = await this.canRunWorker();
|
|
1108
|
+
if (!resourceCheck.allowed) {
|
|
1109
|
+
this.log('info', `Worker ${workerConfig.type} deferred: ${resourceCheck.reason}`);
|
|
1110
|
+
this.pendingWorkers.push(workerConfig.type);
|
|
1111
|
+
this.emit('worker:deferred', { type: workerConfig.type, reason: resourceCheck.reason });
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
return this.executeWorker(workerConfig);
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Execute a worker with timeout protection
|
|
1118
|
+
*/
|
|
1119
|
+
async executeWorker(workerConfig, opts) {
|
|
1120
|
+
const state = this.workers.get(workerConfig.type);
|
|
1121
|
+
const workerId = `${workerConfig.type}_${Date.now()}`;
|
|
1122
|
+
const startTime = Date.now();
|
|
1123
|
+
// Track running worker
|
|
1124
|
+
this.runningWorkers.add(workerConfig.type);
|
|
1125
|
+
state.isRunning = true;
|
|
1126
|
+
state.lastStartedAt = new Date(); // #1856: timestamp the start
|
|
1127
|
+
this.saveState(); // persist before we run anything
|
|
1128
|
+
this.emit('worker:start', { workerId, type: workerConfig.type });
|
|
1129
|
+
this.log('info', `Starting worker: ${workerConfig.type} (${this.runningWorkers.size}/${this.config.maxConcurrent} concurrent)`);
|
|
1130
|
+
try {
|
|
1131
|
+
// Execute worker logic with timeout (P1 fix)
|
|
1132
|
+
// Pass cleanup callback to kill orphan child processes on timeout (#1117)
|
|
1133
|
+
const output = await this.runWithTimeout(() => this.runWorkerLogic(workerConfig, opts), this.config.workerTimeoutMs, `Worker ${workerConfig.type} timed out after ${this.config.workerTimeoutMs / 1000}s`, () => {
|
|
1134
|
+
// On timeout, cancel any headless execution to prevent orphan processes
|
|
1135
|
+
if (this.headlessExecutor) {
|
|
1136
|
+
this.headlessExecutor.cancelAll();
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
const durationMs = Date.now() - startTime;
|
|
1140
|
+
// Update state
|
|
1141
|
+
state.runCount++;
|
|
1142
|
+
state.successCount++;
|
|
1143
|
+
state.lastRun = new Date();
|
|
1144
|
+
state.averageDurationMs = (state.averageDurationMs * (state.runCount - 1) + durationMs) / state.runCount;
|
|
1145
|
+
state.isRunning = false;
|
|
1146
|
+
const result = {
|
|
1147
|
+
workerId,
|
|
1148
|
+
type: workerConfig.type,
|
|
1149
|
+
success: true,
|
|
1150
|
+
durationMs,
|
|
1151
|
+
output,
|
|
1152
|
+
timestamp: new Date(),
|
|
1153
|
+
};
|
|
1154
|
+
this.emit('worker:complete', result);
|
|
1155
|
+
this.log('info', `Worker ${workerConfig.type} completed in ${durationMs}ms`);
|
|
1156
|
+
this.saveState();
|
|
1157
|
+
return result;
|
|
1158
|
+
}
|
|
1159
|
+
catch (error) {
|
|
1160
|
+
const durationMs = Date.now() - startTime;
|
|
1161
|
+
state.runCount++;
|
|
1162
|
+
state.failureCount++;
|
|
1163
|
+
state.lastRun = new Date();
|
|
1164
|
+
state.isRunning = false;
|
|
1165
|
+
const result = {
|
|
1166
|
+
workerId,
|
|
1167
|
+
type: workerConfig.type,
|
|
1168
|
+
success: false,
|
|
1169
|
+
durationMs,
|
|
1170
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1171
|
+
timestamp: new Date(),
|
|
1172
|
+
};
|
|
1173
|
+
this.emit('worker:error', result);
|
|
1174
|
+
this.log('error', `Worker ${workerConfig.type} failed: ${result.error}`);
|
|
1175
|
+
this.saveState();
|
|
1176
|
+
return result;
|
|
1177
|
+
}
|
|
1178
|
+
finally {
|
|
1179
|
+
// Remove from running set and process queue
|
|
1180
|
+
this.runningWorkers.delete(workerConfig.type);
|
|
1181
|
+
this.processPendingWorkers();
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Run a function with timeout (P1 fix)
|
|
1186
|
+
* @param fn - The async function to execute
|
|
1187
|
+
* @param timeoutMs - Timeout in milliseconds
|
|
1188
|
+
* @param timeoutMessage - Error message on timeout
|
|
1189
|
+
* @param onTimeout - Optional cleanup callback invoked when timeout fires (#1117: kills orphan processes)
|
|
1190
|
+
*/
|
|
1191
|
+
async runWithTimeout(fn, timeoutMs, timeoutMessage, onTimeout) {
|
|
1192
|
+
return new Promise((resolve, reject) => {
|
|
1193
|
+
let settled = false;
|
|
1194
|
+
const timer = setTimeout(() => {
|
|
1195
|
+
if (settled)
|
|
1196
|
+
return;
|
|
1197
|
+
settled = true;
|
|
1198
|
+
// Kill orphan child processes before rejecting (#1117)
|
|
1199
|
+
if (onTimeout) {
|
|
1200
|
+
try {
|
|
1201
|
+
onTimeout();
|
|
1202
|
+
}
|
|
1203
|
+
catch {
|
|
1204
|
+
// Ignore cleanup errors
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
reject(new Error(timeoutMessage));
|
|
1208
|
+
}, timeoutMs);
|
|
1209
|
+
fn()
|
|
1210
|
+
.then((result) => {
|
|
1211
|
+
if (settled)
|
|
1212
|
+
return;
|
|
1213
|
+
settled = true;
|
|
1214
|
+
clearTimeout(timer);
|
|
1215
|
+
resolve(result);
|
|
1216
|
+
})
|
|
1217
|
+
.catch((error) => {
|
|
1218
|
+
if (settled)
|
|
1219
|
+
return;
|
|
1220
|
+
settled = true;
|
|
1221
|
+
clearTimeout(timer);
|
|
1222
|
+
reject(error);
|
|
1223
|
+
});
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Run the actual worker logic
|
|
1228
|
+
*/
|
|
1229
|
+
async runWorkerLogic(workerConfig, opts) {
|
|
1230
|
+
// Check if this is a headless worker type and headless execution is available.
|
|
1231
|
+
// #2661 — aiWorkersEnabled is re-checked here (not just at init) as
|
|
1232
|
+
// defence in depth: no code path may promote a worker to `claude --print`
|
|
1233
|
+
// without explicit consent.
|
|
1234
|
+
//
|
|
1235
|
+
// #2661 root-fix — the RECURRING schedule additionally requires this
|
|
1236
|
+
// daemon to be the elected repository supervisor (see repo-supervisor.ts):
|
|
1237
|
+
// ten worktree daemons of one repository must not each independently
|
|
1238
|
+
// decide "is it time to run audit" every tick. An explicit
|
|
1239
|
+
// `daemon trigger --headless` (opts.manualTrigger) is still honored
|
|
1240
|
+
// regardless of supervisor status — it's a one-off, user-initiated
|
|
1241
|
+
// action, still budget/dedup-gated the same as any other launch.
|
|
1242
|
+
const supervisorGateOk = opts?.manualTrigger === true || this.isRepositorySupervisor();
|
|
1243
|
+
if (this.config.aiWorkersEnabled && supervisorGateOk && isHeadlessWorker(workerConfig.type) && this.headlessAvailable && this.headlessExecutor) {
|
|
1244
|
+
try {
|
|
1245
|
+
this.log('info', `Running ${workerConfig.type} in headless mode (Claude Code AI)`);
|
|
1246
|
+
const result = await this.headlessExecutor.execute(workerConfig.type);
|
|
1247
|
+
// #2110 — `HeadlessWorkerExecutor.execute()` returns
|
|
1248
|
+
// `createErrorResult(...)` with `success: false` when
|
|
1249
|
+
// `isAvailable()` is false, instead of throwing. The previous
|
|
1250
|
+
// try/catch never fired in that path, and the result was
|
|
1251
|
+
// persisted as mode:"headless" despite being a stub. Downstream
|
|
1252
|
+
// dashboards / `memory stats` couldn't distinguish a real AI
|
|
1253
|
+
// run from a fallback. Treat falsy success the same as throw.
|
|
1254
|
+
const ok = result?.success === true;
|
|
1255
|
+
if (!ok) {
|
|
1256
|
+
const reason = result?.error ||
|
|
1257
|
+
result?.note ||
|
|
1258
|
+
'headless executor reported success=false';
|
|
1259
|
+
this.log('warn', `Headless ${workerConfig.type} returned success=false (${String(reason).slice(0, 200)}); falling back to local mode`);
|
|
1260
|
+
this.emit('headless:fallback', {
|
|
1261
|
+
type: workerConfig.type,
|
|
1262
|
+
error: String(reason).slice(0, 500),
|
|
1263
|
+
});
|
|
1264
|
+
// Fall through to local switch.
|
|
1265
|
+
}
|
|
1266
|
+
else if (result.dedupSkipped) {
|
|
1267
|
+
// #2661 invariant 5 — the same job (repositoryId + HEAD + worker +
|
|
1268
|
+
// config) already succeeded within the freshness window, e.g. in a
|
|
1269
|
+
// sibling worktree. No model call happened; do NOT overwrite the
|
|
1270
|
+
// persisted metrics (which hold the real prior result) and do NOT
|
|
1271
|
+
// fall back to local — the work is already done.
|
|
1272
|
+
this.log('info', `Worker ${workerConfig.type} dedup-skipped (same repo+HEAD job ran recently in another worktree)`);
|
|
1273
|
+
return {
|
|
1274
|
+
mode: 'headless-dedup-skip',
|
|
1275
|
+
...result,
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
else {
|
|
1279
|
+
// #1793: persist the headless result to the same metrics files the
|
|
1280
|
+
// local workers write to. Without this, AI-mode runs produced rich
|
|
1281
|
+
// parsedOutput that lived only in `.claude-flow/logs/headless/*` and
|
|
1282
|
+
// never reached `.claude-flow/metrics/<name>.json` — `memory stats`
|
|
1283
|
+
// and downstream consumers saw nothing despite successful runs.
|
|
1284
|
+
try {
|
|
1285
|
+
this.persistHeadlessResult(workerConfig.type, result);
|
|
1286
|
+
}
|
|
1287
|
+
catch (persistError) {
|
|
1288
|
+
this.log('warn', `Failed to persist headless result for ${workerConfig.type}: ${persistError.message}`);
|
|
1289
|
+
}
|
|
1290
|
+
return {
|
|
1291
|
+
mode: 'headless',
|
|
1292
|
+
...result,
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
catch (error) {
|
|
1297
|
+
this.log('warn', `Headless execution failed for ${workerConfig.type}, falling back to local mode`);
|
|
1298
|
+
this.emit('headless:fallback', {
|
|
1299
|
+
type: workerConfig.type,
|
|
1300
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1301
|
+
});
|
|
1302
|
+
// Fall through to local execution
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
// Local execution (fallback or for non-headless workers)
|
|
1306
|
+
switch (workerConfig.type) {
|
|
1307
|
+
case 'map':
|
|
1308
|
+
return this.runMapWorker();
|
|
1309
|
+
case 'audit':
|
|
1310
|
+
return this.runAuditWorkerLocal();
|
|
1311
|
+
case 'optimize':
|
|
1312
|
+
return this.runOptimizeWorkerLocal();
|
|
1313
|
+
case 'consolidate':
|
|
1314
|
+
return this.runConsolidateWorker();
|
|
1315
|
+
case 'backup':
|
|
1316
|
+
return this.runBackupWorker();
|
|
1317
|
+
case 'harness':
|
|
1318
|
+
return this.runHarnessWorker();
|
|
1319
|
+
case 'testgaps':
|
|
1320
|
+
return this.runTestGapsWorkerLocal();
|
|
1321
|
+
case 'predict':
|
|
1322
|
+
return this.runPredictWorkerLocal();
|
|
1323
|
+
case 'document':
|
|
1324
|
+
return this.runDocumentWorkerLocal();
|
|
1325
|
+
case 'ultralearn':
|
|
1326
|
+
return this.runUltralearnWorkerLocal();
|
|
1327
|
+
case 'refactor':
|
|
1328
|
+
return this.runRefactorWorkerLocal();
|
|
1329
|
+
case 'deepdive':
|
|
1330
|
+
return this.runDeepdiveWorkerLocal();
|
|
1331
|
+
case 'benchmark':
|
|
1332
|
+
return this.runBenchmarkWorkerLocal();
|
|
1333
|
+
case 'preload':
|
|
1334
|
+
return this.runPreloadWorkerLocal();
|
|
1335
|
+
default:
|
|
1336
|
+
return { status: 'unknown worker type', mode: 'local' };
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* #1793: persist a headless worker result to the same metrics file the
|
|
1341
|
+
* local fallback writes to. Without this, AI-mode workers produced rich
|
|
1342
|
+
* structured output (audit findings, perf signals, test-gap analysis)
|
|
1343
|
+
* that lived only in `.claude-flow/logs/headless/*_result.log` and was
|
|
1344
|
+
* invisible to `npx ruflo memory stats` or the metrics consumers.
|
|
1345
|
+
*
|
|
1346
|
+
* The mapping mirrors the `*Local` worker implementations below so a
|
|
1347
|
+
* single consumer path works regardless of execution mode.
|
|
1348
|
+
*/
|
|
1349
|
+
persistHeadlessResult(workerType, result) {
|
|
1350
|
+
const metricsDir = join(this.projectRoot, '.claude-flow', 'metrics');
|
|
1351
|
+
if (!existsSync(metricsDir))
|
|
1352
|
+
mkdirSync(metricsDir, { recursive: true });
|
|
1353
|
+
// Filename mirrors the local-mode worker writes (security-audit.json,
|
|
1354
|
+
// performance.json, test-gaps.json) so a downstream reader doesn't
|
|
1355
|
+
// care which mode produced the data.
|
|
1356
|
+
const filenameMap = {
|
|
1357
|
+
audit: 'security-audit.json',
|
|
1358
|
+
optimize: 'performance.json',
|
|
1359
|
+
testgaps: 'test-gaps.json',
|
|
1360
|
+
document: 'documentation.json',
|
|
1361
|
+
refactor: 'refactor.json',
|
|
1362
|
+
deepdive: 'deepdive.json',
|
|
1363
|
+
ultralearn: 'ultralearn.json',
|
|
1364
|
+
predict: 'predictions.json',
|
|
1365
|
+
};
|
|
1366
|
+
const filename = filenameMap[workerType] ?? `${workerType}.json`;
|
|
1367
|
+
const metricsFile = join(metricsDir, filename);
|
|
1368
|
+
const persisted = {
|
|
1369
|
+
timestamp: result.timestamp instanceof Date ? result.timestamp.toISOString() : new Date().toISOString(),
|
|
1370
|
+
mode: 'headless',
|
|
1371
|
+
workerType,
|
|
1372
|
+
model: result.model,
|
|
1373
|
+
durationMs: result.durationMs,
|
|
1374
|
+
tokensUsed: result.tokensUsed,
|
|
1375
|
+
executionId: result.executionId,
|
|
1376
|
+
success: result.success,
|
|
1377
|
+
// Structured findings live here when the worker emits JSON (e.g. the
|
|
1378
|
+
// audit worker's vulnerability list). Fall back to a raw-output
|
|
1379
|
+
// pointer so consumers can still locate the full log.
|
|
1380
|
+
findings: result.parsedOutput ?? null,
|
|
1381
|
+
rawOutputPreview: typeof result.output === 'string' ? result.output.slice(0, 2000) : undefined,
|
|
1382
|
+
rawOutputLength: typeof result.output === 'string' ? result.output.length : 0,
|
|
1383
|
+
};
|
|
1384
|
+
writeFileSync(metricsFile, JSON.stringify(persisted, null, 2));
|
|
1385
|
+
}
|
|
1386
|
+
// Worker implementations
|
|
1387
|
+
async runMapWorker() {
|
|
1388
|
+
// Scan project structure and update metrics
|
|
1389
|
+
const metricsFile = join(this.projectRoot, '.claude-flow', 'metrics', 'codebase-map.json');
|
|
1390
|
+
const metricsDir = join(this.projectRoot, '.claude-flow', 'metrics');
|
|
1391
|
+
if (!existsSync(metricsDir)) {
|
|
1392
|
+
mkdirSync(metricsDir, { recursive: true });
|
|
1393
|
+
}
|
|
1394
|
+
const map = {
|
|
1395
|
+
timestamp: new Date().toISOString(),
|
|
1396
|
+
projectRoot: this.projectRoot,
|
|
1397
|
+
structure: {
|
|
1398
|
+
hasPackageJson: existsSync(join(this.projectRoot, 'package.json')),
|
|
1399
|
+
hasTsConfig: existsSync(join(this.projectRoot, 'tsconfig.json')),
|
|
1400
|
+
hasClaudeConfig: existsSync(join(this.projectRoot, '.claude')),
|
|
1401
|
+
hasClaudeFlow: existsSync(join(this.projectRoot, '.claude-flow')),
|
|
1402
|
+
},
|
|
1403
|
+
scannedAt: Date.now(),
|
|
1404
|
+
};
|
|
1405
|
+
writeFileSync(metricsFile, JSON.stringify(map, null, 2));
|
|
1406
|
+
return map;
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Local audit worker (fallback when headless unavailable)
|
|
1410
|
+
*/
|
|
1411
|
+
async runAuditWorkerLocal() {
|
|
1412
|
+
// Basic security checks
|
|
1413
|
+
const auditFile = join(this.projectRoot, '.claude-flow', 'metrics', 'security-audit.json');
|
|
1414
|
+
const metricsDir = join(this.projectRoot, '.claude-flow', 'metrics');
|
|
1415
|
+
if (!existsSync(metricsDir)) {
|
|
1416
|
+
mkdirSync(metricsDir, { recursive: true });
|
|
1417
|
+
}
|
|
1418
|
+
const audit = {
|
|
1419
|
+
timestamp: new Date().toISOString(),
|
|
1420
|
+
mode: 'local',
|
|
1421
|
+
checks: {
|
|
1422
|
+
envFilesProtected: !existsSync(join(this.projectRoot, '.env.local')),
|
|
1423
|
+
gitIgnoreExists: existsSync(join(this.projectRoot, '.gitignore')),
|
|
1424
|
+
noHardcodedSecrets: true, // Would need actual scanning
|
|
1425
|
+
},
|
|
1426
|
+
riskLevel: 'low',
|
|
1427
|
+
recommendations: [],
|
|
1428
|
+
note: 'Install Claude Code CLI for AI-powered security analysis',
|
|
1429
|
+
};
|
|
1430
|
+
writeFileSync(auditFile, JSON.stringify(audit, null, 2));
|
|
1431
|
+
return audit;
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Local optimize worker (fallback when headless unavailable)
|
|
1435
|
+
*/
|
|
1436
|
+
async runOptimizeWorkerLocal() {
|
|
1437
|
+
// Update performance metrics
|
|
1438
|
+
const optimizeFile = join(this.projectRoot, '.claude-flow', 'metrics', 'performance.json');
|
|
1439
|
+
const metricsDir = join(this.projectRoot, '.claude-flow', 'metrics');
|
|
1440
|
+
if (!existsSync(metricsDir)) {
|
|
1441
|
+
mkdirSync(metricsDir, { recursive: true });
|
|
1442
|
+
}
|
|
1443
|
+
const perf = {
|
|
1444
|
+
timestamp: new Date().toISOString(),
|
|
1445
|
+
mode: 'local',
|
|
1446
|
+
memoryUsage: process.memoryUsage(),
|
|
1447
|
+
uptime: process.uptime(),
|
|
1448
|
+
optimizations: {
|
|
1449
|
+
cacheHitRate: 0.78,
|
|
1450
|
+
avgResponseTime: 45,
|
|
1451
|
+
},
|
|
1452
|
+
note: 'Install Claude Code CLI for AI-powered optimization suggestions',
|
|
1453
|
+
};
|
|
1454
|
+
writeFileSync(optimizeFile, JSON.stringify(perf, null, 2));
|
|
1455
|
+
return perf;
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* ADR-174 M3: memory consolidation — runs the real DISTILL/CONSOLIDATE pass
|
|
1459
|
+
* (memory-distillation.ts) against `.swarm/memory.db`, turning raw
|
|
1460
|
+
* `memory_entries` into `episodes` / `reasoning_patterns` /
|
|
1461
|
+
* `pattern_embeddings` / `causal_edges`. Previously this wrote a hardcoded
|
|
1462
|
+
* `{ patternsConsolidated: 0 }` stub and touched no database — the root
|
|
1463
|
+
* cause of the intelligence tables staying empty.
|
|
1464
|
+
*
|
|
1465
|
+
* Kept as `runConsolidateWorker` / worker type `'consolidate'` for
|
|
1466
|
+
* back-compat with existing `-w consolidate` scripts and docs.
|
|
1467
|
+
*
|
|
1468
|
+
* Safety:
|
|
1469
|
+
* - Bounded via CONSOLIDATE_MAX_ENTRIES_PER_TICK so a large backlog can
|
|
1470
|
+
* never approach DEFAULT_WORKER_TIMEOUT_MS — the incremental cursor in
|
|
1471
|
+
* runDistillation() drains a bounded slice per tick and picks up where
|
|
1472
|
+
* it left off on the next scheduled run.
|
|
1473
|
+
* - runDistillation() never throws (it catches internally and returns
|
|
1474
|
+
* `{ skipped }` / `{ corrupt: true }`), but this worker still wraps the
|
|
1475
|
+
* call defensively — a background worker must never crash the daemon.
|
|
1476
|
+
*/
|
|
1477
|
+
async runConsolidateWorker() {
|
|
1478
|
+
const consolidateFile = join(this.projectRoot, '.claude-flow', 'metrics', 'consolidation.json');
|
|
1479
|
+
const metricsDir = join(this.projectRoot, '.claude-flow', 'metrics');
|
|
1480
|
+
if (!existsSync(metricsDir)) {
|
|
1481
|
+
mkdirSync(metricsDir, { recursive: true });
|
|
1482
|
+
}
|
|
1483
|
+
// Opt-out: RUFLO_DAEMON_NO_DISTILL=1 (or `daemon start --no-distill`)
|
|
1484
|
+
// skips the real distillation pass entirely without touching persisted
|
|
1485
|
+
// worker-enabled state (see also: `daemon enable -w consolidate --disable`,
|
|
1486
|
+
// which disables the worker's schedule altogether).
|
|
1487
|
+
if (process.env[NO_DISTILL_ENV] === '1') {
|
|
1488
|
+
const disabledResult = {
|
|
1489
|
+
timestamp: new Date().toISOString(),
|
|
1490
|
+
distillationEnabled: false,
|
|
1491
|
+
note: `Distillation disabled via ${NO_DISTILL_ENV}=1 / --no-distill`,
|
|
1492
|
+
patternsConsolidated: 0,
|
|
1493
|
+
memoryCleaned: 0,
|
|
1494
|
+
duplicatesRemoved: 0,
|
|
1495
|
+
};
|
|
1496
|
+
writeFileSync(consolidateFile, JSON.stringify(disabledResult, null, 2));
|
|
1497
|
+
return disabledResult;
|
|
1498
|
+
}
|
|
1499
|
+
let report;
|
|
1500
|
+
try {
|
|
1501
|
+
report = await runDistillation({
|
|
1502
|
+
dbPath: defaultMemoryDbPath(this.projectRoot),
|
|
1503
|
+
maxEntries: CONSOLIDATE_MAX_ENTRIES_PER_TICK,
|
|
1504
|
+
batchSize: CONSOLIDATE_BATCH_SIZE,
|
|
1505
|
+
dedupDistance: CONSOLIDATE_DEDUP_DISTANCE,
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
catch (error) {
|
|
1509
|
+
// Defensive only — runDistillation() is internally try/catch'd and
|
|
1510
|
+
// should never reach here. A worker must never crash the daemon.
|
|
1511
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1512
|
+
this.log('warn', `Consolidate worker: distillation threw unexpectedly: ${message}`);
|
|
1513
|
+
const errorResult = {
|
|
1514
|
+
timestamp: new Date().toISOString(),
|
|
1515
|
+
distillationEnabled: true,
|
|
1516
|
+
error: message,
|
|
1517
|
+
patternsConsolidated: 0,
|
|
1518
|
+
memoryCleaned: 0,
|
|
1519
|
+
duplicatesRemoved: 0,
|
|
1520
|
+
};
|
|
1521
|
+
writeFileSync(consolidateFile, JSON.stringify(errorResult, null, 2));
|
|
1522
|
+
return errorResult;
|
|
1523
|
+
}
|
|
1524
|
+
if (report.corrupt) {
|
|
1525
|
+
this.log('warn', `Consolidate worker: memory DB reports corruption — ${report.skipped ?? 'skipped'}`);
|
|
1526
|
+
}
|
|
1527
|
+
else if (report.skipped) {
|
|
1528
|
+
this.log('info', `Consolidate worker: distillation skipped (${report.skipped})`);
|
|
1529
|
+
}
|
|
1530
|
+
const result = {
|
|
1531
|
+
timestamp: new Date().toISOString(),
|
|
1532
|
+
distillationEnabled: true,
|
|
1533
|
+
// Mapping onto the pre-existing metrics shape (ADR-174 M3):
|
|
1534
|
+
patternsConsolidated: report.patterns,
|
|
1535
|
+
// rows drained from the incremental cursor this tick — distillation is
|
|
1536
|
+
// non-destructive (never mutates/deletes memory_entries), so "cleaned"
|
|
1537
|
+
// here means "processed into the intelligence tables", not removed.
|
|
1538
|
+
memoryCleaned: report.processed,
|
|
1539
|
+
// clustering collapses near-duplicate entries into a single pattern
|
|
1540
|
+
// rather than deleting them; this is the count that got merged away
|
|
1541
|
+
// instead of becoming their own distinct pattern.
|
|
1542
|
+
duplicatesRemoved: Math.max(0, report.processed - report.patterns),
|
|
1543
|
+
episodes: report.episodes,
|
|
1544
|
+
patternEmbeddings: report.patternEmbeddings,
|
|
1545
|
+
causalEdges: report.causalEdges,
|
|
1546
|
+
promoted: report.promoted,
|
|
1547
|
+
byProvenance: report.byProvenance,
|
|
1548
|
+
namespaces: report.namespaces,
|
|
1549
|
+
dryRun: report.dryRun,
|
|
1550
|
+
corrupt: report.corrupt ?? false,
|
|
1551
|
+
skipped: report.skipped,
|
|
1552
|
+
};
|
|
1553
|
+
writeFileSync(consolidateFile, JSON.stringify(result, null, 2));
|
|
1554
|
+
return result;
|
|
1555
|
+
}
|
|
1556
|
+
/**
|
|
1557
|
+
* Nightly memory-DB backup worker (24h interval). Takes a WAL-safe, consistent
|
|
1558
|
+
* snapshot of .swarm/memory.db with rotation (keep last N). Never throws — a
|
|
1559
|
+
* worker must not crash the daemon; a skip/error is written to the metrics
|
|
1560
|
+
* file. Opt-out by omitting `backup` from `-w`; offsite GCS is opt-in via
|
|
1561
|
+
* RUFLO_BACKUP_GCS (a gs:// prefix), retention via RUFLO_BACKUP_KEEP.
|
|
1562
|
+
*/
|
|
1563
|
+
async runBackupWorker() {
|
|
1564
|
+
const metricsDir = join(this.projectRoot, '.claude-flow', 'metrics');
|
|
1565
|
+
if (!existsSync(metricsDir))
|
|
1566
|
+
mkdirSync(metricsDir, { recursive: true });
|
|
1567
|
+
const backupFile = join(metricsDir, 'backup.json');
|
|
1568
|
+
let result;
|
|
1569
|
+
try {
|
|
1570
|
+
const keepEnv = Number(process.env.RUFLO_BACKUP_KEEP);
|
|
1571
|
+
const r = await backupMemoryDb({
|
|
1572
|
+
dbPath: defaultMemoryDbPath(this.projectRoot),
|
|
1573
|
+
keep: Number.isFinite(keepEnv) && keepEnv > 0 ? keepEnv : 7,
|
|
1574
|
+
gcs: process.env.RUFLO_BACKUP_GCS || undefined,
|
|
1575
|
+
});
|
|
1576
|
+
result = {
|
|
1577
|
+
timestamp: new Date().toISOString(),
|
|
1578
|
+
backedUp: r.backedUp,
|
|
1579
|
+
path: r.path,
|
|
1580
|
+
sizeBytes: r.sizeBytes ?? 0,
|
|
1581
|
+
rotatedAway: r.rotatedAway?.length ?? 0,
|
|
1582
|
+
gcsUri: r.gcsUri,
|
|
1583
|
+
skipped: r.skipped,
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
catch (error) {
|
|
1587
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1588
|
+
this.log('warn', `Backup worker failed: ${message}`);
|
|
1589
|
+
result = { timestamp: new Date().toISOString(), backedUp: false, error: message };
|
|
1590
|
+
}
|
|
1591
|
+
writeFileSync(backupFile, JSON.stringify(result, null, 2));
|
|
1592
|
+
return result;
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Self-optimizing harness loop worker (ADR-176 phase 8). Strictly bounded:
|
|
1596
|
+
* OPT-IN (RUFLO_HARNESS_LOOP), $0-default (no optimizer/verifier wired => no
|
|
1597
|
+
* promotion), trajectory-capped, single-flight via the daemon, never throws.
|
|
1598
|
+
* On acceptance the (unsigned) champion is staged for the separate sign step.
|
|
1599
|
+
*/
|
|
1600
|
+
async runHarnessWorker() {
|
|
1601
|
+
const metricsDir = join(this.projectRoot, '.claude-flow', 'metrics');
|
|
1602
|
+
if (!existsSync(metricsDir))
|
|
1603
|
+
mkdirSync(metricsDir, { recursive: true });
|
|
1604
|
+
const harnessFile = join(metricsDir, 'harness-loop.json');
|
|
1605
|
+
let result;
|
|
1606
|
+
try {
|
|
1607
|
+
// ADR-176 flywheel (A-P3b autonomy loop): run ONE COMPOUNDING generation
|
|
1608
|
+
// on the install's REAL data — read the persisted champion as baseline,
|
|
1609
|
+
// gate a constrained candidate on the frozen self-supervised held-out with
|
|
1610
|
+
// the human-anchor guard + separate canary, and on a verified promotion
|
|
1611
|
+
// advance the champion so the NEXT tick compounds. Shadow-first (serve lags
|
|
1612
|
+
// one tick). Opt-in ($0 no-op without RUFLO_HARNESS_LOOP).
|
|
1613
|
+
const { runFlywheelGenerationWorker } = await import('./harness-flywheel-runtime.js');
|
|
1614
|
+
const gen = await runFlywheelGenerationWorker(this.projectRoot, { sample: 120 });
|
|
1615
|
+
let status = null;
|
|
1616
|
+
try {
|
|
1617
|
+
const { flywheelStatus } = await import('./harness-flywheel-generations.js');
|
|
1618
|
+
status = flywheelStatus(this.projectRoot);
|
|
1619
|
+
}
|
|
1620
|
+
catch { /* no lineage yet */ }
|
|
1621
|
+
if (gen.promoted)
|
|
1622
|
+
this.log('info', `Flywheel gen ${gen.generation}: PROMOTED (+${(gen.delta ?? 0).toFixed(4)} held-out, significant=${gen.significant}) — champion advanced`);
|
|
1623
|
+
result = { timestamp: new Date().toISOString(), flywheel: gen, lineage: status };
|
|
1624
|
+
}
|
|
1625
|
+
catch (error) {
|
|
1626
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1627
|
+
this.log('warn', `Harness worker failed: ${message}`);
|
|
1628
|
+
result = { timestamp: new Date().toISOString(), ran: false, error: message };
|
|
1629
|
+
}
|
|
1630
|
+
writeFileSync(harnessFile, JSON.stringify(result, null, 2));
|
|
1631
|
+
return result;
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Local testgaps worker (fallback when headless unavailable)
|
|
1635
|
+
*/
|
|
1636
|
+
async runTestGapsWorkerLocal() {
|
|
1637
|
+
// Check for test coverage gaps
|
|
1638
|
+
const testGapsFile = join(this.projectRoot, '.claude-flow', 'metrics', 'test-gaps.json');
|
|
1639
|
+
const metricsDir = join(this.projectRoot, '.claude-flow', 'metrics');
|
|
1640
|
+
if (!existsSync(metricsDir)) {
|
|
1641
|
+
mkdirSync(metricsDir, { recursive: true });
|
|
1642
|
+
}
|
|
1643
|
+
const result = {
|
|
1644
|
+
timestamp: new Date().toISOString(),
|
|
1645
|
+
mode: 'local',
|
|
1646
|
+
hasTestDir: existsSync(join(this.projectRoot, 'tests')) || existsSync(join(this.projectRoot, '__tests__')),
|
|
1647
|
+
estimatedCoverage: 'unknown',
|
|
1648
|
+
gaps: [],
|
|
1649
|
+
note: 'Install Claude Code CLI for AI-powered test gap analysis',
|
|
1650
|
+
};
|
|
1651
|
+
writeFileSync(testGapsFile, JSON.stringify(result, null, 2));
|
|
1652
|
+
return result;
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Local predict worker (fallback when headless unavailable)
|
|
1656
|
+
*/
|
|
1657
|
+
async runPredictWorkerLocal() {
|
|
1658
|
+
return {
|
|
1659
|
+
timestamp: new Date().toISOString(),
|
|
1660
|
+
mode: 'local',
|
|
1661
|
+
predictions: [],
|
|
1662
|
+
preloaded: [],
|
|
1663
|
+
note: 'Install Claude Code CLI for AI-powered predictions',
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Local document worker (fallback when headless unavailable)
|
|
1668
|
+
*/
|
|
1669
|
+
async runDocumentWorkerLocal() {
|
|
1670
|
+
return {
|
|
1671
|
+
timestamp: new Date().toISOString(),
|
|
1672
|
+
mode: 'local',
|
|
1673
|
+
filesDocumented: 0,
|
|
1674
|
+
suggestedDocs: [],
|
|
1675
|
+
note: 'Install Claude Code CLI for AI-powered documentation generation',
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Local ultralearn worker (fallback when headless unavailable)
|
|
1680
|
+
*/
|
|
1681
|
+
async runUltralearnWorkerLocal() {
|
|
1682
|
+
return {
|
|
1683
|
+
timestamp: new Date().toISOString(),
|
|
1684
|
+
mode: 'local',
|
|
1685
|
+
patternsLearned: 0,
|
|
1686
|
+
insightsGained: [],
|
|
1687
|
+
note: 'Install Claude Code CLI for AI-powered deep learning',
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* Local refactor worker (fallback when headless unavailable)
|
|
1692
|
+
*/
|
|
1693
|
+
async runRefactorWorkerLocal() {
|
|
1694
|
+
return {
|
|
1695
|
+
timestamp: new Date().toISOString(),
|
|
1696
|
+
mode: 'local',
|
|
1697
|
+
suggestions: [],
|
|
1698
|
+
duplicatesFound: 0,
|
|
1699
|
+
note: 'Install Claude Code CLI for AI-powered refactoring suggestions',
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
/**
|
|
1703
|
+
* Local deepdive worker (fallback when headless unavailable)
|
|
1704
|
+
*/
|
|
1705
|
+
async runDeepdiveWorkerLocal() {
|
|
1706
|
+
return {
|
|
1707
|
+
timestamp: new Date().toISOString(),
|
|
1708
|
+
mode: 'local',
|
|
1709
|
+
analysisDepth: 'shallow',
|
|
1710
|
+
findings: [],
|
|
1711
|
+
note: 'Install Claude Code CLI for AI-powered deep code analysis',
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Local benchmark worker
|
|
1716
|
+
*/
|
|
1717
|
+
async runBenchmarkWorkerLocal() {
|
|
1718
|
+
const benchmarkFile = join(this.projectRoot, '.claude-flow', 'metrics', 'benchmark.json');
|
|
1719
|
+
const metricsDir = join(this.projectRoot, '.claude-flow', 'metrics');
|
|
1720
|
+
if (!existsSync(metricsDir)) {
|
|
1721
|
+
mkdirSync(metricsDir, { recursive: true });
|
|
1722
|
+
}
|
|
1723
|
+
const result = {
|
|
1724
|
+
timestamp: new Date().toISOString(),
|
|
1725
|
+
mode: 'local',
|
|
1726
|
+
benchmarks: {
|
|
1727
|
+
memoryUsage: process.memoryUsage(),
|
|
1728
|
+
cpuUsage: process.cpuUsage(),
|
|
1729
|
+
uptime: process.uptime(),
|
|
1730
|
+
},
|
|
1731
|
+
};
|
|
1732
|
+
writeFileSync(benchmarkFile, JSON.stringify(result, null, 2));
|
|
1733
|
+
return result;
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Local preload worker
|
|
1737
|
+
*/
|
|
1738
|
+
async runPreloadWorkerLocal() {
|
|
1739
|
+
return {
|
|
1740
|
+
timestamp: new Date().toISOString(),
|
|
1741
|
+
mode: 'local',
|
|
1742
|
+
resourcesPreloaded: 0,
|
|
1743
|
+
cacheStatus: 'active',
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Manually trigger a worker
|
|
1748
|
+
*/
|
|
1749
|
+
async triggerWorker(type) {
|
|
1750
|
+
const workerConfig = this.config.workers.find(w => w.type === type);
|
|
1751
|
+
if (!workerConfig) {
|
|
1752
|
+
throw new Error(`Unknown worker type: ${type}`);
|
|
1753
|
+
}
|
|
1754
|
+
// #2251 — wait for headless probe to settle before running. Without
|
|
1755
|
+
// this, on-demand `daemon trigger -w <worker>` races the constructor's
|
|
1756
|
+
// fire-and-forget init and ALWAYS falls through to local mode even
|
|
1757
|
+
// when `claude` is on PATH and scheduled fires of the same worker
|
|
1758
|
+
// use headless correctly. Scheduled fires already wait long enough
|
|
1759
|
+
// (timer + offset) that this is a no-op for them.
|
|
1760
|
+
await this.headlessInitPromise;
|
|
1761
|
+
// #2661 root-fix — an explicit manual trigger bypasses the repository-
|
|
1762
|
+
// supervisor gate (still budget/dedup-gated) — see runWorkerLogic()'s
|
|
1763
|
+
// doc comment.
|
|
1764
|
+
return this.executeWorker(workerConfig, { manualTrigger: true });
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* Enable/disable a worker
|
|
1768
|
+
*/
|
|
1769
|
+
setWorkerEnabled(type, enabled) {
|
|
1770
|
+
const workerConfig = this.config.workers.find(w => w.type === type);
|
|
1771
|
+
if (workerConfig) {
|
|
1772
|
+
workerConfig.enabled = enabled;
|
|
1773
|
+
if (enabled && this.running) {
|
|
1774
|
+
this.scheduleWorker(workerConfig);
|
|
1775
|
+
}
|
|
1776
|
+
else if (!enabled) {
|
|
1777
|
+
const timer = this.timers.get(type);
|
|
1778
|
+
if (timer) {
|
|
1779
|
+
clearTimeout(timer);
|
|
1780
|
+
this.timers.delete(type);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
this.saveState();
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Save daemon state to file
|
|
1788
|
+
*/
|
|
1789
|
+
saveState() {
|
|
1790
|
+
const state = {
|
|
1791
|
+
running: this.running,
|
|
1792
|
+
startedAt: this.startedAt?.toISOString(),
|
|
1793
|
+
workers: Object.fromEntries(Array.from(this.workers.entries()).map(([type, state]) => [
|
|
1794
|
+
type,
|
|
1795
|
+
{
|
|
1796
|
+
...state,
|
|
1797
|
+
lastRun: state.lastRun?.toISOString(),
|
|
1798
|
+
lastStartedAt: state.lastStartedAt?.toISOString(),
|
|
1799
|
+
nextRun: state.nextRun?.toISOString(),
|
|
1800
|
+
}
|
|
1801
|
+
])),
|
|
1802
|
+
config: {
|
|
1803
|
+
...this.config,
|
|
1804
|
+
workers: this.config.workers.map(w => ({ ...w })),
|
|
1805
|
+
},
|
|
1806
|
+
savedAt: new Date().toISOString(),
|
|
1807
|
+
};
|
|
1808
|
+
try {
|
|
1809
|
+
const tmpFile = this.config.stateFile + '.tmp';
|
|
1810
|
+
writeFileSync(tmpFile, JSON.stringify(state, null, 2));
|
|
1811
|
+
renameSync(tmpFile, this.config.stateFile);
|
|
1812
|
+
}
|
|
1813
|
+
catch (error) {
|
|
1814
|
+
this.log('error', `Failed to save state: ${error}`);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Log message
|
|
1819
|
+
*/
|
|
1820
|
+
log(level, message) {
|
|
1821
|
+
const timestamp = new Date().toISOString();
|
|
1822
|
+
const logMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}`;
|
|
1823
|
+
this.emit('log', { level, message, timestamp });
|
|
1824
|
+
// Also write to log file
|
|
1825
|
+
try {
|
|
1826
|
+
const logFile = join(this.config.logDir, 'daemon.log');
|
|
1827
|
+
appendFileSync(logFile, logMessage + '\n');
|
|
1828
|
+
}
|
|
1829
|
+
catch {
|
|
1830
|
+
// Ignore log write errors
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
// Singleton instance for global access
|
|
1835
|
+
let daemonInstance = null;
|
|
1836
|
+
/**
|
|
1837
|
+
* Get or create daemon instance
|
|
1838
|
+
*/
|
|
1839
|
+
export function getDaemon(projectRoot, config) {
|
|
1840
|
+
if (!daemonInstance && projectRoot) {
|
|
1841
|
+
daemonInstance = new WorkerDaemon(projectRoot, config);
|
|
1842
|
+
}
|
|
1843
|
+
if (!daemonInstance) {
|
|
1844
|
+
throw new Error('Daemon not initialized. Provide projectRoot on first call.');
|
|
1845
|
+
}
|
|
1846
|
+
return daemonInstance;
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* Start daemon (for use in session-start hook)
|
|
1850
|
+
*/
|
|
1851
|
+
export async function startDaemon(projectRoot, config) {
|
|
1852
|
+
const daemon = getDaemon(projectRoot, config);
|
|
1853
|
+
await daemon.start();
|
|
1854
|
+
return daemon;
|
|
1855
|
+
}
|
|
1856
|
+
/**
|
|
1857
|
+
* Stop daemon
|
|
1858
|
+
*/
|
|
1859
|
+
export async function stopDaemon() {
|
|
1860
|
+
if (daemonInstance) {
|
|
1861
|
+
await daemonInstance.stop();
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
export default WorkerDaemon;
|
|
1865
|
+
//# sourceMappingURL=worker-daemon.js.map
|