attune-ai 2.0.0__py3-none-any.whl
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.
- attune/__init__.py +358 -0
- attune/adaptive/__init__.py +13 -0
- attune/adaptive/task_complexity.py +127 -0
- attune/agent_monitoring.py +414 -0
- attune/cache/__init__.py +117 -0
- attune/cache/base.py +166 -0
- attune/cache/dependency_manager.py +256 -0
- attune/cache/hash_only.py +251 -0
- attune/cache/hybrid.py +457 -0
- attune/cache/storage.py +285 -0
- attune/cache_monitor.py +356 -0
- attune/cache_stats.py +298 -0
- attune/cli/__init__.py +152 -0
- attune/cli/__main__.py +12 -0
- attune/cli/commands/__init__.py +1 -0
- attune/cli/commands/batch.py +264 -0
- attune/cli/commands/cache.py +248 -0
- attune/cli/commands/help.py +331 -0
- attune/cli/commands/info.py +140 -0
- attune/cli/commands/inspect.py +436 -0
- attune/cli/commands/inspection.py +57 -0
- attune/cli/commands/memory.py +48 -0
- attune/cli/commands/metrics.py +92 -0
- attune/cli/commands/orchestrate.py +184 -0
- attune/cli/commands/patterns.py +207 -0
- attune/cli/commands/profiling.py +202 -0
- attune/cli/commands/provider.py +98 -0
- attune/cli/commands/routing.py +285 -0
- attune/cli/commands/setup.py +96 -0
- attune/cli/commands/status.py +235 -0
- attune/cli/commands/sync.py +166 -0
- attune/cli/commands/tier.py +121 -0
- attune/cli/commands/utilities.py +114 -0
- attune/cli/commands/workflow.py +579 -0
- attune/cli/core.py +32 -0
- attune/cli/parsers/__init__.py +68 -0
- attune/cli/parsers/batch.py +118 -0
- attune/cli/parsers/cache.py +65 -0
- attune/cli/parsers/help.py +41 -0
- attune/cli/parsers/info.py +26 -0
- attune/cli/parsers/inspect.py +66 -0
- attune/cli/parsers/metrics.py +42 -0
- attune/cli/parsers/orchestrate.py +61 -0
- attune/cli/parsers/patterns.py +54 -0
- attune/cli/parsers/provider.py +40 -0
- attune/cli/parsers/routing.py +110 -0
- attune/cli/parsers/setup.py +42 -0
- attune/cli/parsers/status.py +47 -0
- attune/cli/parsers/sync.py +31 -0
- attune/cli/parsers/tier.py +33 -0
- attune/cli/parsers/workflow.py +77 -0
- attune/cli/utils/__init__.py +1 -0
- attune/cli/utils/data.py +242 -0
- attune/cli/utils/helpers.py +68 -0
- attune/cli_legacy.py +3957 -0
- attune/cli_minimal.py +1159 -0
- attune/cli_router.py +437 -0
- attune/cli_unified.py +814 -0
- attune/config/__init__.py +66 -0
- attune/config/xml_config.py +286 -0
- attune/config.py +545 -0
- attune/coordination.py +870 -0
- attune/core.py +1511 -0
- attune/core_modules/__init__.py +15 -0
- attune/cost_tracker.py +626 -0
- attune/dashboard/__init__.py +41 -0
- attune/dashboard/app.py +512 -0
- attune/dashboard/simple_server.py +435 -0
- attune/dashboard/standalone_server.py +547 -0
- attune/discovery.py +306 -0
- attune/emergence.py +306 -0
- attune/exceptions.py +123 -0
- attune/feedback_loops.py +373 -0
- attune/hot_reload/README.md +473 -0
- attune/hot_reload/__init__.py +62 -0
- attune/hot_reload/config.py +83 -0
- attune/hot_reload/integration.py +229 -0
- attune/hot_reload/reloader.py +298 -0
- attune/hot_reload/watcher.py +183 -0
- attune/hot_reload/websocket.py +177 -0
- attune/levels.py +577 -0
- attune/leverage_points.py +441 -0
- attune/logging_config.py +261 -0
- attune/mcp/__init__.py +10 -0
- attune/mcp/server.py +506 -0
- attune/memory/__init__.py +237 -0
- attune/memory/claude_memory.py +469 -0
- attune/memory/config.py +224 -0
- attune/memory/control_panel.py +1290 -0
- attune/memory/control_panel_support.py +145 -0
- attune/memory/cross_session.py +845 -0
- attune/memory/edges.py +179 -0
- attune/memory/encryption.py +159 -0
- attune/memory/file_session.py +770 -0
- attune/memory/graph.py +570 -0
- attune/memory/long_term.py +913 -0
- attune/memory/long_term_types.py +99 -0
- attune/memory/mixins/__init__.py +25 -0
- attune/memory/mixins/backend_init_mixin.py +249 -0
- attune/memory/mixins/capabilities_mixin.py +208 -0
- attune/memory/mixins/handoff_mixin.py +208 -0
- attune/memory/mixins/lifecycle_mixin.py +49 -0
- attune/memory/mixins/long_term_mixin.py +352 -0
- attune/memory/mixins/promotion_mixin.py +109 -0
- attune/memory/mixins/short_term_mixin.py +182 -0
- attune/memory/nodes.py +179 -0
- attune/memory/redis_bootstrap.py +540 -0
- attune/memory/security/__init__.py +31 -0
- attune/memory/security/audit_logger.py +932 -0
- attune/memory/security/pii_scrubber.py +640 -0
- attune/memory/security/secrets_detector.py +678 -0
- attune/memory/short_term.py +2192 -0
- attune/memory/simple_storage.py +302 -0
- attune/memory/storage/__init__.py +15 -0
- attune/memory/storage_backend.py +167 -0
- attune/memory/summary_index.py +583 -0
- attune/memory/types.py +446 -0
- attune/memory/unified.py +182 -0
- attune/meta_workflows/__init__.py +74 -0
- attune/meta_workflows/agent_creator.py +248 -0
- attune/meta_workflows/builtin_templates.py +567 -0
- attune/meta_workflows/cli_commands/__init__.py +56 -0
- attune/meta_workflows/cli_commands/agent_commands.py +321 -0
- attune/meta_workflows/cli_commands/analytics_commands.py +442 -0
- attune/meta_workflows/cli_commands/config_commands.py +232 -0
- attune/meta_workflows/cli_commands/memory_commands.py +182 -0
- attune/meta_workflows/cli_commands/template_commands.py +354 -0
- attune/meta_workflows/cli_commands/workflow_commands.py +382 -0
- attune/meta_workflows/cli_meta_workflows.py +59 -0
- attune/meta_workflows/form_engine.py +292 -0
- attune/meta_workflows/intent_detector.py +409 -0
- attune/meta_workflows/models.py +569 -0
- attune/meta_workflows/pattern_learner.py +738 -0
- attune/meta_workflows/plan_generator.py +384 -0
- attune/meta_workflows/session_context.py +397 -0
- attune/meta_workflows/template_registry.py +229 -0
- attune/meta_workflows/workflow.py +984 -0
- attune/metrics/__init__.py +12 -0
- attune/metrics/collector.py +31 -0
- attune/metrics/prompt_metrics.py +194 -0
- attune/models/__init__.py +172 -0
- attune/models/__main__.py +13 -0
- attune/models/adaptive_routing.py +437 -0
- attune/models/auth_cli.py +444 -0
- attune/models/auth_strategy.py +450 -0
- attune/models/cli.py +655 -0
- attune/models/empathy_executor.py +354 -0
- attune/models/executor.py +257 -0
- attune/models/fallback.py +762 -0
- attune/models/provider_config.py +282 -0
- attune/models/registry.py +472 -0
- attune/models/tasks.py +359 -0
- attune/models/telemetry/__init__.py +71 -0
- attune/models/telemetry/analytics.py +594 -0
- attune/models/telemetry/backend.py +196 -0
- attune/models/telemetry/data_models.py +431 -0
- attune/models/telemetry/storage.py +489 -0
- attune/models/token_estimator.py +420 -0
- attune/models/validation.py +280 -0
- attune/monitoring/__init__.py +52 -0
- attune/monitoring/alerts.py +946 -0
- attune/monitoring/alerts_cli.py +448 -0
- attune/monitoring/multi_backend.py +271 -0
- attune/monitoring/otel_backend.py +362 -0
- attune/optimization/__init__.py +19 -0
- attune/optimization/context_optimizer.py +272 -0
- attune/orchestration/__init__.py +67 -0
- attune/orchestration/agent_templates.py +707 -0
- attune/orchestration/config_store.py +499 -0
- attune/orchestration/execution_strategies.py +2111 -0
- attune/orchestration/meta_orchestrator.py +1168 -0
- attune/orchestration/pattern_learner.py +696 -0
- attune/orchestration/real_tools.py +931 -0
- attune/pattern_cache.py +187 -0
- attune/pattern_library.py +542 -0
- attune/patterns/debugging/all_patterns.json +81 -0
- attune/patterns/debugging/workflow_20260107_1770825e.json +77 -0
- attune/patterns/refactoring_memory.json +89 -0
- attune/persistence.py +564 -0
- attune/platform_utils.py +265 -0
- attune/plugins/__init__.py +28 -0
- attune/plugins/base.py +361 -0
- attune/plugins/registry.py +268 -0
- attune/project_index/__init__.py +32 -0
- attune/project_index/cli.py +335 -0
- attune/project_index/index.py +667 -0
- attune/project_index/models.py +504 -0
- attune/project_index/reports.py +474 -0
- attune/project_index/scanner.py +777 -0
- attune/project_index/scanner_parallel.py +291 -0
- attune/prompts/__init__.py +61 -0
- attune/prompts/config.py +77 -0
- attune/prompts/context.py +177 -0
- attune/prompts/parser.py +285 -0
- attune/prompts/registry.py +313 -0
- attune/prompts/templates.py +208 -0
- attune/redis_config.py +302 -0
- attune/redis_memory.py +799 -0
- attune/resilience/__init__.py +56 -0
- attune/resilience/circuit_breaker.py +256 -0
- attune/resilience/fallback.py +179 -0
- attune/resilience/health.py +300 -0
- attune/resilience/retry.py +209 -0
- attune/resilience/timeout.py +135 -0
- attune/routing/__init__.py +43 -0
- attune/routing/chain_executor.py +433 -0
- attune/routing/classifier.py +217 -0
- attune/routing/smart_router.py +234 -0
- attune/routing/workflow_registry.py +343 -0
- attune/scaffolding/README.md +589 -0
- attune/scaffolding/__init__.py +35 -0
- attune/scaffolding/__main__.py +14 -0
- attune/scaffolding/cli.py +240 -0
- attune/scaffolding/templates/base_wizard.py.jinja2 +121 -0
- attune/scaffolding/templates/coach_wizard.py.jinja2 +321 -0
- attune/scaffolding/templates/domain_wizard.py.jinja2 +408 -0
- attune/scaffolding/templates/linear_flow_wizard.py.jinja2 +203 -0
- attune/socratic/__init__.py +256 -0
- attune/socratic/ab_testing.py +958 -0
- attune/socratic/blueprint.py +533 -0
- attune/socratic/cli.py +703 -0
- attune/socratic/collaboration.py +1114 -0
- attune/socratic/domain_templates.py +924 -0
- attune/socratic/embeddings.py +738 -0
- attune/socratic/engine.py +794 -0
- attune/socratic/explainer.py +682 -0
- attune/socratic/feedback.py +772 -0
- attune/socratic/forms.py +629 -0
- attune/socratic/generator.py +732 -0
- attune/socratic/llm_analyzer.py +637 -0
- attune/socratic/mcp_server.py +702 -0
- attune/socratic/session.py +312 -0
- attune/socratic/storage.py +667 -0
- attune/socratic/success.py +730 -0
- attune/socratic/visual_editor.py +860 -0
- attune/socratic/web_ui.py +958 -0
- attune/telemetry/__init__.py +39 -0
- attune/telemetry/agent_coordination.py +475 -0
- attune/telemetry/agent_tracking.py +367 -0
- attune/telemetry/approval_gates.py +545 -0
- attune/telemetry/cli.py +1231 -0
- attune/telemetry/commands/__init__.py +14 -0
- attune/telemetry/commands/dashboard_commands.py +696 -0
- attune/telemetry/event_streaming.py +409 -0
- attune/telemetry/feedback_loop.py +567 -0
- attune/telemetry/usage_tracker.py +591 -0
- attune/templates.py +754 -0
- attune/test_generator/__init__.py +38 -0
- attune/test_generator/__main__.py +14 -0
- attune/test_generator/cli.py +234 -0
- attune/test_generator/generator.py +355 -0
- attune/test_generator/risk_analyzer.py +216 -0
- attune/test_generator/templates/unit_test.py.jinja2 +272 -0
- attune/tier_recommender.py +384 -0
- attune/tools.py +183 -0
- attune/trust/__init__.py +28 -0
- attune/trust/circuit_breaker.py +579 -0
- attune/trust_building.py +527 -0
- attune/validation/__init__.py +19 -0
- attune/validation/xml_validator.py +281 -0
- attune/vscode_bridge.py +173 -0
- attune/workflow_commands.py +780 -0
- attune/workflow_patterns/__init__.py +33 -0
- attune/workflow_patterns/behavior.py +249 -0
- attune/workflow_patterns/core.py +76 -0
- attune/workflow_patterns/output.py +99 -0
- attune/workflow_patterns/registry.py +255 -0
- attune/workflow_patterns/structural.py +288 -0
- attune/workflows/__init__.py +539 -0
- attune/workflows/autonomous_test_gen.py +1268 -0
- attune/workflows/base.py +2667 -0
- attune/workflows/batch_processing.py +342 -0
- attune/workflows/bug_predict.py +1084 -0
- attune/workflows/builder.py +273 -0
- attune/workflows/caching.py +253 -0
- attune/workflows/code_review.py +1048 -0
- attune/workflows/code_review_adapters.py +312 -0
- attune/workflows/code_review_pipeline.py +722 -0
- attune/workflows/config.py +645 -0
- attune/workflows/dependency_check.py +644 -0
- attune/workflows/document_gen/__init__.py +25 -0
- attune/workflows/document_gen/config.py +30 -0
- attune/workflows/document_gen/report_formatter.py +162 -0
- attune/workflows/document_gen/workflow.py +1426 -0
- attune/workflows/document_manager.py +216 -0
- attune/workflows/document_manager_README.md +134 -0
- attune/workflows/documentation_orchestrator.py +1205 -0
- attune/workflows/history.py +510 -0
- attune/workflows/keyboard_shortcuts/__init__.py +39 -0
- attune/workflows/keyboard_shortcuts/generators.py +391 -0
- attune/workflows/keyboard_shortcuts/parsers.py +416 -0
- attune/workflows/keyboard_shortcuts/prompts.py +295 -0
- attune/workflows/keyboard_shortcuts/schema.py +193 -0
- attune/workflows/keyboard_shortcuts/workflow.py +509 -0
- attune/workflows/llm_base.py +363 -0
- attune/workflows/manage_docs.py +87 -0
- attune/workflows/manage_docs_README.md +134 -0
- attune/workflows/manage_documentation.py +821 -0
- attune/workflows/new_sample_workflow1.py +149 -0
- attune/workflows/new_sample_workflow1_README.md +150 -0
- attune/workflows/orchestrated_health_check.py +849 -0
- attune/workflows/orchestrated_release_prep.py +600 -0
- attune/workflows/output.py +413 -0
- attune/workflows/perf_audit.py +863 -0
- attune/workflows/pr_review.py +762 -0
- attune/workflows/progress.py +785 -0
- attune/workflows/progress_server.py +322 -0
- attune/workflows/progressive/README 2.md +454 -0
- attune/workflows/progressive/README.md +454 -0
- attune/workflows/progressive/__init__.py +82 -0
- attune/workflows/progressive/cli.py +219 -0
- attune/workflows/progressive/core.py +488 -0
- attune/workflows/progressive/orchestrator.py +723 -0
- attune/workflows/progressive/reports.py +520 -0
- attune/workflows/progressive/telemetry.py +274 -0
- attune/workflows/progressive/test_gen.py +495 -0
- attune/workflows/progressive/workflow.py +589 -0
- attune/workflows/refactor_plan.py +694 -0
- attune/workflows/release_prep.py +895 -0
- attune/workflows/release_prep_crew.py +969 -0
- attune/workflows/research_synthesis.py +404 -0
- attune/workflows/routing.py +168 -0
- attune/workflows/secure_release.py +593 -0
- attune/workflows/security_adapters.py +297 -0
- attune/workflows/security_audit.py +1329 -0
- attune/workflows/security_audit_phase3.py +355 -0
- attune/workflows/seo_optimization.py +633 -0
- attune/workflows/step_config.py +234 -0
- attune/workflows/telemetry_mixin.py +269 -0
- attune/workflows/test5.py +125 -0
- attune/workflows/test5_README.md +158 -0
- attune/workflows/test_coverage_boost_crew.py +849 -0
- attune/workflows/test_gen/__init__.py +52 -0
- attune/workflows/test_gen/ast_analyzer.py +249 -0
- attune/workflows/test_gen/config.py +88 -0
- attune/workflows/test_gen/data_models.py +38 -0
- attune/workflows/test_gen/report_formatter.py +289 -0
- attune/workflows/test_gen/test_templates.py +381 -0
- attune/workflows/test_gen/workflow.py +655 -0
- attune/workflows/test_gen.py +54 -0
- attune/workflows/test_gen_behavioral.py +477 -0
- attune/workflows/test_gen_parallel.py +341 -0
- attune/workflows/test_lifecycle.py +526 -0
- attune/workflows/test_maintenance.py +627 -0
- attune/workflows/test_maintenance_cli.py +590 -0
- attune/workflows/test_maintenance_crew.py +840 -0
- attune/workflows/test_runner.py +622 -0
- attune/workflows/tier_tracking.py +531 -0
- attune/workflows/xml_enhanced_crew.py +285 -0
- attune_ai-2.0.0.dist-info/METADATA +1026 -0
- attune_ai-2.0.0.dist-info/RECORD +457 -0
- attune_ai-2.0.0.dist-info/WHEEL +5 -0
- attune_ai-2.0.0.dist-info/entry_points.txt +26 -0
- attune_ai-2.0.0.dist-info/licenses/LICENSE +201 -0
- attune_ai-2.0.0.dist-info/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +101 -0
- attune_ai-2.0.0.dist-info/top_level.txt +5 -0
- attune_healthcare/__init__.py +13 -0
- attune_healthcare/monitors/__init__.py +9 -0
- attune_healthcare/monitors/clinical_protocol_monitor.py +315 -0
- attune_healthcare/monitors/monitoring/__init__.py +44 -0
- attune_healthcare/monitors/monitoring/protocol_checker.py +300 -0
- attune_healthcare/monitors/monitoring/protocol_loader.py +214 -0
- attune_healthcare/monitors/monitoring/sensor_parsers.py +306 -0
- attune_healthcare/monitors/monitoring/trajectory_analyzer.py +389 -0
- attune_llm/README.md +553 -0
- attune_llm/__init__.py +28 -0
- attune_llm/agent_factory/__init__.py +53 -0
- attune_llm/agent_factory/adapters/__init__.py +85 -0
- attune_llm/agent_factory/adapters/autogen_adapter.py +312 -0
- attune_llm/agent_factory/adapters/crewai_adapter.py +483 -0
- attune_llm/agent_factory/adapters/haystack_adapter.py +298 -0
- attune_llm/agent_factory/adapters/langchain_adapter.py +362 -0
- attune_llm/agent_factory/adapters/langgraph_adapter.py +333 -0
- attune_llm/agent_factory/adapters/native.py +228 -0
- attune_llm/agent_factory/adapters/wizard_adapter.py +423 -0
- attune_llm/agent_factory/base.py +305 -0
- attune_llm/agent_factory/crews/__init__.py +67 -0
- attune_llm/agent_factory/crews/code_review.py +1113 -0
- attune_llm/agent_factory/crews/health_check.py +1262 -0
- attune_llm/agent_factory/crews/refactoring.py +1128 -0
- attune_llm/agent_factory/crews/security_audit.py +1018 -0
- attune_llm/agent_factory/decorators.py +287 -0
- attune_llm/agent_factory/factory.py +558 -0
- attune_llm/agent_factory/framework.py +193 -0
- attune_llm/agent_factory/memory_integration.py +328 -0
- attune_llm/agent_factory/resilient.py +320 -0
- attune_llm/agents_md/__init__.py +22 -0
- attune_llm/agents_md/loader.py +218 -0
- attune_llm/agents_md/parser.py +271 -0
- attune_llm/agents_md/registry.py +307 -0
- attune_llm/claude_memory.py +466 -0
- attune_llm/cli/__init__.py +8 -0
- attune_llm/cli/sync_claude.py +487 -0
- attune_llm/code_health.py +1313 -0
- attune_llm/commands/__init__.py +51 -0
- attune_llm/commands/context.py +375 -0
- attune_llm/commands/loader.py +301 -0
- attune_llm/commands/models.py +231 -0
- attune_llm/commands/parser.py +371 -0
- attune_llm/commands/registry.py +429 -0
- attune_llm/config/__init__.py +29 -0
- attune_llm/config/unified.py +291 -0
- attune_llm/context/__init__.py +22 -0
- attune_llm/context/compaction.py +455 -0
- attune_llm/context/manager.py +434 -0
- attune_llm/contextual_patterns.py +361 -0
- attune_llm/core.py +907 -0
- attune_llm/git_pattern_extractor.py +435 -0
- attune_llm/hooks/__init__.py +24 -0
- attune_llm/hooks/config.py +306 -0
- attune_llm/hooks/executor.py +289 -0
- attune_llm/hooks/registry.py +302 -0
- attune_llm/hooks/scripts/__init__.py +39 -0
- attune_llm/hooks/scripts/evaluate_session.py +201 -0
- attune_llm/hooks/scripts/first_time_init.py +285 -0
- attune_llm/hooks/scripts/pre_compact.py +207 -0
- attune_llm/hooks/scripts/session_end.py +183 -0
- attune_llm/hooks/scripts/session_start.py +163 -0
- attune_llm/hooks/scripts/suggest_compact.py +225 -0
- attune_llm/learning/__init__.py +30 -0
- attune_llm/learning/evaluator.py +438 -0
- attune_llm/learning/extractor.py +514 -0
- attune_llm/learning/storage.py +560 -0
- attune_llm/levels.py +227 -0
- attune_llm/pattern_confidence.py +414 -0
- attune_llm/pattern_resolver.py +272 -0
- attune_llm/pattern_summary.py +350 -0
- attune_llm/providers.py +967 -0
- attune_llm/routing/__init__.py +32 -0
- attune_llm/routing/model_router.py +362 -0
- attune_llm/security/IMPLEMENTATION_SUMMARY.md +413 -0
- attune_llm/security/PHASE2_COMPLETE.md +384 -0
- attune_llm/security/PHASE2_SECRETS_DETECTOR_COMPLETE.md +271 -0
- attune_llm/security/QUICK_REFERENCE.md +316 -0
- attune_llm/security/README.md +262 -0
- attune_llm/security/__init__.py +62 -0
- attune_llm/security/audit_logger.py +929 -0
- attune_llm/security/audit_logger_example.py +152 -0
- attune_llm/security/pii_scrubber.py +640 -0
- attune_llm/security/secrets_detector.py +678 -0
- attune_llm/security/secrets_detector_example.py +304 -0
- attune_llm/security/secure_memdocs.py +1192 -0
- attune_llm/security/secure_memdocs_example.py +278 -0
- attune_llm/session_status.py +745 -0
- attune_llm/state.py +246 -0
- attune_llm/utils/__init__.py +5 -0
- attune_llm/utils/tokens.py +349 -0
- attune_software/SOFTWARE_PLUGIN_README.md +57 -0
- attune_software/__init__.py +13 -0
- attune_software/cli/__init__.py +120 -0
- attune_software/cli/inspect.py +362 -0
- attune_software/cli.py +574 -0
- attune_software/plugin.py +188 -0
- workflow_scaffolding/__init__.py +11 -0
- workflow_scaffolding/__main__.py +12 -0
- workflow_scaffolding/cli.py +206 -0
- workflow_scaffolding/generator.py +265 -0
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
"""Orchestrated Health Check Workflow
|
|
2
|
+
|
|
3
|
+
Uses meta-orchestration to perform comprehensive project health assessments
|
|
4
|
+
with configurable depth levels (daily, weekly, release).
|
|
5
|
+
|
|
6
|
+
This workflow demonstrates adaptive agent composition based on execution mode,
|
|
7
|
+
intelligent health scoring, and historical trend tracking.
|
|
8
|
+
|
|
9
|
+
Architecture:
|
|
10
|
+
- MetaOrchestrator selects agents based on mode
|
|
11
|
+
- ParallelStrategy for daily/weekly (fast validation)
|
|
12
|
+
- RefinementStrategy for release (deep multi-stage analysis)
|
|
13
|
+
- Health score calculation with weighted criteria
|
|
14
|
+
- Trend tracking for historical comparisons
|
|
15
|
+
|
|
16
|
+
Modes:
|
|
17
|
+
- daily: Quick parallel check (security, coverage, quality)
|
|
18
|
+
- weekly: Comprehensive parallel (+ performance, docs, dependencies)
|
|
19
|
+
- release: Deep sequential refinement (multi-stage validation)
|
|
20
|
+
|
|
21
|
+
Quality Criteria (weighted):
|
|
22
|
+
- Security: 30% (critical issues, vulnerability count)
|
|
23
|
+
- Coverage: 25% (test coverage percentage)
|
|
24
|
+
- Quality: 20% (code quality score)
|
|
25
|
+
- Performance: 15% (bottleneck count, response times)
|
|
26
|
+
- Documentation: 10% (completeness percentage)
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
>>> workflow = OrchestratedHealthCheckWorkflow(mode="weekly")
|
|
30
|
+
>>> report = await workflow.execute(project_root=".")
|
|
31
|
+
>>> print(f"{report.overall_health_score}/100 ({report.grade})")
|
|
32
|
+
85/100 (B)
|
|
33
|
+
|
|
34
|
+
Copyright 2025 Smart-AI-Memory
|
|
35
|
+
Licensed under Fair Source License 0.9
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
import asyncio
|
|
39
|
+
import json
|
|
40
|
+
import logging
|
|
41
|
+
from dataclasses import dataclass, field
|
|
42
|
+
from datetime import datetime
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
from typing import Any
|
|
45
|
+
|
|
46
|
+
from attune.config import _validate_file_path
|
|
47
|
+
|
|
48
|
+
from ..orchestration.agent_templates import AgentTemplate, get_template
|
|
49
|
+
from ..orchestration.execution_strategies import ParallelStrategy, StrategyResult
|
|
50
|
+
from ..orchestration.meta_orchestrator import MetaOrchestrator
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger(__name__)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class CategoryScore:
|
|
57
|
+
"""Individual category health score.
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
name: Category name (e.g., "Security")
|
|
61
|
+
score: Score 0-100
|
|
62
|
+
weight: Weight in overall score (0-1)
|
|
63
|
+
raw_metrics: Raw metrics from agent
|
|
64
|
+
issues: Issues found
|
|
65
|
+
passed: Whether category passed threshold
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
name: str
|
|
69
|
+
score: float
|
|
70
|
+
weight: float
|
|
71
|
+
raw_metrics: dict[str, Any] = field(default_factory=dict)
|
|
72
|
+
issues: list[str] = field(default_factory=list)
|
|
73
|
+
passed: bool = True
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class HealthCheckReport:
|
|
78
|
+
"""Comprehensive health check report.
|
|
79
|
+
|
|
80
|
+
Attributes:
|
|
81
|
+
overall_health_score: Overall health score 0-100
|
|
82
|
+
grade: Letter grade (A/B/C/D/F)
|
|
83
|
+
category_scores: Scores by category
|
|
84
|
+
issues: All issues found
|
|
85
|
+
recommendations: Actionable recommendations
|
|
86
|
+
trend: Comparison with last check
|
|
87
|
+
execution_time: Total execution time in seconds
|
|
88
|
+
mode: Execution mode (daily/weekly/release)
|
|
89
|
+
timestamp: Report generation time
|
|
90
|
+
agents_executed: Number of agents executed
|
|
91
|
+
success: Whether check completed successfully
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
overall_health_score: float
|
|
95
|
+
grade: str
|
|
96
|
+
category_scores: list[CategoryScore] = field(default_factory=list)
|
|
97
|
+
issues: list[str] = field(default_factory=list)
|
|
98
|
+
recommendations: list[str] = field(default_factory=list)
|
|
99
|
+
trend: str = ""
|
|
100
|
+
execution_time: float = 0.0
|
|
101
|
+
mode: str = "daily"
|
|
102
|
+
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
103
|
+
agents_executed: int = 0
|
|
104
|
+
success: bool = True
|
|
105
|
+
|
|
106
|
+
def to_dict(self) -> dict[str, Any]:
|
|
107
|
+
"""Convert report to dictionary for JSON serialization.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Dictionary representation
|
|
111
|
+
"""
|
|
112
|
+
return {
|
|
113
|
+
"overall_health_score": self.overall_health_score,
|
|
114
|
+
"grade": self.grade,
|
|
115
|
+
"category_scores": [
|
|
116
|
+
{
|
|
117
|
+
"name": cat.name,
|
|
118
|
+
"score": cat.score,
|
|
119
|
+
"weight": cat.weight,
|
|
120
|
+
"raw_metrics": cat.raw_metrics,
|
|
121
|
+
"issues": cat.issues,
|
|
122
|
+
"passed": cat.passed,
|
|
123
|
+
}
|
|
124
|
+
for cat in self.category_scores
|
|
125
|
+
],
|
|
126
|
+
"issues": self.issues,
|
|
127
|
+
"recommendations": self.recommendations,
|
|
128
|
+
"trend": self.trend,
|
|
129
|
+
"execution_time": self.execution_time,
|
|
130
|
+
"mode": self.mode,
|
|
131
|
+
"timestamp": self.timestamp,
|
|
132
|
+
"agents_executed": self.agents_executed,
|
|
133
|
+
"success": self.success,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
def format_console_output(self) -> str:
|
|
137
|
+
"""Format report for console display.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Human-readable formatted report
|
|
141
|
+
"""
|
|
142
|
+
lines = []
|
|
143
|
+
|
|
144
|
+
# Header
|
|
145
|
+
lines.append("=" * 70)
|
|
146
|
+
lines.append("PROJECT HEALTH CHECK REPORT (Meta-Orchestrated)")
|
|
147
|
+
lines.append("=" * 70)
|
|
148
|
+
lines.append("")
|
|
149
|
+
|
|
150
|
+
# Overall health
|
|
151
|
+
grade_emoji = {
|
|
152
|
+
"A": "🏆",
|
|
153
|
+
"B": "✅",
|
|
154
|
+
"C": "⚠️",
|
|
155
|
+
"D": "❌",
|
|
156
|
+
"F": "🚨",
|
|
157
|
+
}
|
|
158
|
+
emoji = grade_emoji.get(self.grade, "")
|
|
159
|
+
|
|
160
|
+
lines.append(
|
|
161
|
+
f"Overall Health: {emoji} {self.overall_health_score:.1f}/100 (Grade {self.grade})"
|
|
162
|
+
)
|
|
163
|
+
lines.append(f"Mode: {self.mode.upper()}")
|
|
164
|
+
lines.append(f"Agents Executed: {self.agents_executed}")
|
|
165
|
+
lines.append(f"Generated: {self.timestamp}")
|
|
166
|
+
lines.append(f"Duration: {self.execution_time:.2f}s")
|
|
167
|
+
|
|
168
|
+
if self.trend:
|
|
169
|
+
lines.append(f"Trend: {self.trend}")
|
|
170
|
+
|
|
171
|
+
lines.append("")
|
|
172
|
+
|
|
173
|
+
# Category scores
|
|
174
|
+
lines.append("-" * 70)
|
|
175
|
+
lines.append("CATEGORY BREAKDOWN")
|
|
176
|
+
lines.append("-" * 70)
|
|
177
|
+
|
|
178
|
+
for category in sorted(self.category_scores, key=lambda x: x.score, reverse=True):
|
|
179
|
+
status = "✅" if category.passed else "❌"
|
|
180
|
+
bar_length = int(category.score / 5) # 0-20 chars
|
|
181
|
+
bar = "█" * bar_length
|
|
182
|
+
lines.append(
|
|
183
|
+
f"{status} {category.name:15} {category.score:5.1f}/100 "
|
|
184
|
+
f"(weight: {category.weight * 100:2.0f}%) {bar}"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Show issues for failing categories
|
|
188
|
+
if category.issues and not category.passed:
|
|
189
|
+
for issue in category.issues[:3]: # Show first 3
|
|
190
|
+
lines.append(f" • {issue}")
|
|
191
|
+
|
|
192
|
+
lines.append("")
|
|
193
|
+
|
|
194
|
+
# Issues summary
|
|
195
|
+
if self.issues:
|
|
196
|
+
lines.append("-" * 70)
|
|
197
|
+
lines.append(f"🚨 ISSUES FOUND ({len(self.issues)})")
|
|
198
|
+
lines.append("-" * 70)
|
|
199
|
+
for issue in self.issues[:10]: # Show first 10
|
|
200
|
+
lines.append(f" • {issue}")
|
|
201
|
+
if len(self.issues) > 10:
|
|
202
|
+
lines.append(f" ... and {len(self.issues) - 10} more")
|
|
203
|
+
lines.append("")
|
|
204
|
+
|
|
205
|
+
# Recommendations
|
|
206
|
+
if self.recommendations:
|
|
207
|
+
lines.append("-" * 70)
|
|
208
|
+
lines.append(f"💡 RECOMMENDATIONS ({len(self.recommendations)})")
|
|
209
|
+
lines.append("-" * 70)
|
|
210
|
+
for rec in self.recommendations:
|
|
211
|
+
lines.append(f" • {rec}")
|
|
212
|
+
lines.append("")
|
|
213
|
+
|
|
214
|
+
lines.append("=" * 70)
|
|
215
|
+
|
|
216
|
+
return "\n".join(lines)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class OrchestratedHealthCheckWorkflow:
|
|
220
|
+
"""Health check workflow using meta-orchestration.
|
|
221
|
+
|
|
222
|
+
This workflow performs comprehensive project health assessment using
|
|
223
|
+
intelligent agent composition based on execution mode.
|
|
224
|
+
|
|
225
|
+
Modes:
|
|
226
|
+
- daily: Fast parallel check (security, coverage, quality) with CHEAP/CAPABLE agents
|
|
227
|
+
- weekly: Comprehensive parallel (adds performance, docs, deps) with all tiers
|
|
228
|
+
- release: Deep sequential refinement with premium agents
|
|
229
|
+
|
|
230
|
+
Health Score Calculation:
|
|
231
|
+
Weighted average of category scores:
|
|
232
|
+
- Security: 30%
|
|
233
|
+
- Coverage: 25%
|
|
234
|
+
- Quality: 20%
|
|
235
|
+
- Performance: 15%
|
|
236
|
+
- Documentation: 10%
|
|
237
|
+
|
|
238
|
+
Example:
|
|
239
|
+
>>> workflow = OrchestratedHealthCheckWorkflow(mode="weekly")
|
|
240
|
+
>>> report = await workflow.execute(project_root=".")
|
|
241
|
+
>>> if report.overall_health_score >= 80:
|
|
242
|
+
... print("Project is healthy!")
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
# Category weights for overall score
|
|
246
|
+
CATEGORY_WEIGHTS = {
|
|
247
|
+
"Security": 0.30,
|
|
248
|
+
"Coverage": 0.25,
|
|
249
|
+
"Quality": 0.20,
|
|
250
|
+
"Performance": 0.15,
|
|
251
|
+
"Documentation": 0.10,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
# Agent sets by mode
|
|
255
|
+
MODE_AGENTS = {
|
|
256
|
+
"daily": [
|
|
257
|
+
"security_auditor",
|
|
258
|
+
"test_coverage_analyzer",
|
|
259
|
+
"code_reviewer",
|
|
260
|
+
],
|
|
261
|
+
"weekly": [
|
|
262
|
+
"security_auditor",
|
|
263
|
+
"test_coverage_analyzer",
|
|
264
|
+
"code_reviewer",
|
|
265
|
+
"performance_optimizer",
|
|
266
|
+
"documentation_writer",
|
|
267
|
+
],
|
|
268
|
+
"release": [
|
|
269
|
+
"security_auditor",
|
|
270
|
+
"test_coverage_analyzer",
|
|
271
|
+
"code_reviewer",
|
|
272
|
+
"performance_optimizer",
|
|
273
|
+
"documentation_writer",
|
|
274
|
+
"architecture_analyst",
|
|
275
|
+
],
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# Grade thresholds
|
|
279
|
+
GRADE_THRESHOLDS = {
|
|
280
|
+
"A": 90.0,
|
|
281
|
+
"B": 80.0,
|
|
282
|
+
"C": 70.0,
|
|
283
|
+
"D": 60.0,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
def __init__(self, mode: str = "daily", project_root: str = ".", **kwargs):
|
|
287
|
+
"""Initialize health check workflow.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
mode: Execution mode ("daily", "weekly", "release")
|
|
291
|
+
project_root: Project root directory
|
|
292
|
+
**kwargs: Extra parameters (ignored, for CLI compatibility)
|
|
293
|
+
|
|
294
|
+
Raises:
|
|
295
|
+
ValueError: If mode is invalid
|
|
296
|
+
"""
|
|
297
|
+
if mode not in self.MODE_AGENTS:
|
|
298
|
+
raise ValueError(
|
|
299
|
+
f"Invalid mode: {mode}. Must be one of {list(self.MODE_AGENTS.keys())}"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
self.mode = mode
|
|
303
|
+
self.project_root = Path(project_root).resolve()
|
|
304
|
+
self.orchestrator = MetaOrchestrator()
|
|
305
|
+
|
|
306
|
+
# Tracking directory
|
|
307
|
+
self.tracking_dir = self.project_root / ".empathy" / "health_tracking"
|
|
308
|
+
self.tracking_dir.mkdir(parents=True, exist_ok=True)
|
|
309
|
+
|
|
310
|
+
logger.info(
|
|
311
|
+
f"OrchestratedHealthCheckWorkflow initialized: mode={mode}, root={project_root}"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
async def execute(
|
|
315
|
+
self,
|
|
316
|
+
project_root: str | None = None,
|
|
317
|
+
context: dict[str, Any] | None = None,
|
|
318
|
+
**kwargs, # Absorb extra parameters from VSCode/CLI (target, etc.)
|
|
319
|
+
) -> HealthCheckReport:
|
|
320
|
+
"""Execute health check workflow.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
project_root: Optional project root (overrides init value)
|
|
324
|
+
context: Additional context for agents
|
|
325
|
+
**kwargs: Extra parameters (ignored, for VSCode/CLI compatibility)
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
HealthCheckReport with comprehensive health assessment
|
|
329
|
+
|
|
330
|
+
Raises:
|
|
331
|
+
ValueError: If project_root is invalid
|
|
332
|
+
"""
|
|
333
|
+
# Map 'target' to 'project_root' for VSCode compatibility
|
|
334
|
+
if "target" in kwargs and not project_root:
|
|
335
|
+
project_root = kwargs["target"]
|
|
336
|
+
if project_root:
|
|
337
|
+
self.project_root = Path(project_root).resolve()
|
|
338
|
+
|
|
339
|
+
if not self.project_root.exists():
|
|
340
|
+
raise ValueError(f"Project root does not exist: {self.project_root}")
|
|
341
|
+
|
|
342
|
+
logger.info(f"Starting health check: mode={self.mode}, root={self.project_root}")
|
|
343
|
+
start_time = asyncio.get_event_loop().time()
|
|
344
|
+
|
|
345
|
+
# Prepare context
|
|
346
|
+
full_context = {
|
|
347
|
+
"project_root": str(self.project_root),
|
|
348
|
+
"mode": self.mode,
|
|
349
|
+
**(context or {}),
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
# Get agents for mode
|
|
353
|
+
agent_ids = self.MODE_AGENTS[self.mode]
|
|
354
|
+
agents = []
|
|
355
|
+
for agent_id in agent_ids:
|
|
356
|
+
template = get_template(agent_id)
|
|
357
|
+
if template:
|
|
358
|
+
agents.append(template)
|
|
359
|
+
else:
|
|
360
|
+
logger.warning(f"Agent template not found: {agent_id}")
|
|
361
|
+
|
|
362
|
+
if not agents:
|
|
363
|
+
raise ValueError(f"No agents available for mode: {self.mode}")
|
|
364
|
+
|
|
365
|
+
logger.info(f"Selected {len(agents)} agents: {[a.id for a in agents]}")
|
|
366
|
+
|
|
367
|
+
# Execute agents based on mode strategy
|
|
368
|
+
# All modes use parallel strategy for reliability and speed
|
|
369
|
+
# (RefinementStrategy had issues with data passing between stages)
|
|
370
|
+
strategy = ParallelStrategy()
|
|
371
|
+
|
|
372
|
+
strategy_result = await strategy.execute(agents, full_context)
|
|
373
|
+
|
|
374
|
+
# Create health report
|
|
375
|
+
report = await self._create_report(strategy_result, agents)
|
|
376
|
+
|
|
377
|
+
# Set execution time
|
|
378
|
+
end_time = asyncio.get_event_loop().time()
|
|
379
|
+
report.execution_time = end_time - start_time
|
|
380
|
+
|
|
381
|
+
# Save to tracking history
|
|
382
|
+
self._save_tracking_history(report)
|
|
383
|
+
|
|
384
|
+
# Save to .attune/health.json for VS Code extension
|
|
385
|
+
self._save_health_json(report)
|
|
386
|
+
|
|
387
|
+
logger.info(
|
|
388
|
+
f"Health check completed: score={report.overall_health_score:.1f}, "
|
|
389
|
+
f"grade={report.grade}, duration={report.execution_time:.2f}s"
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
return report
|
|
393
|
+
|
|
394
|
+
async def _create_report(
|
|
395
|
+
self, strategy_result: StrategyResult, agents: list[AgentTemplate]
|
|
396
|
+
) -> HealthCheckReport:
|
|
397
|
+
"""Create health check report from agent results.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
strategy_result: Results from strategy execution
|
|
401
|
+
agents: Agents that were executed
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
HealthCheckReport with all findings
|
|
405
|
+
"""
|
|
406
|
+
# Extract agent results
|
|
407
|
+
agent_results: dict[str, dict] = {}
|
|
408
|
+
for result in strategy_result.outputs:
|
|
409
|
+
agent_results[result.agent_id] = {
|
|
410
|
+
"success": result.success,
|
|
411
|
+
"output": result.output,
|
|
412
|
+
"confidence": result.confidence,
|
|
413
|
+
"duration": result.duration_seconds,
|
|
414
|
+
"error": result.error,
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
# Calculate category scores
|
|
418
|
+
category_scores = self._calculate_category_scores(agent_results)
|
|
419
|
+
|
|
420
|
+
# Calculate overall health score
|
|
421
|
+
overall_score = self._calculate_overall_score(category_scores)
|
|
422
|
+
|
|
423
|
+
# Assign grade
|
|
424
|
+
grade = self._assign_grade(overall_score)
|
|
425
|
+
|
|
426
|
+
# Collect all issues
|
|
427
|
+
issues = []
|
|
428
|
+
for category in category_scores:
|
|
429
|
+
issues.extend(category.issues)
|
|
430
|
+
|
|
431
|
+
# Generate recommendations
|
|
432
|
+
recommendations = self._generate_recommendations(category_scores)
|
|
433
|
+
|
|
434
|
+
# Get trend comparison
|
|
435
|
+
trend = self._get_trend_comparison(overall_score)
|
|
436
|
+
|
|
437
|
+
return HealthCheckReport(
|
|
438
|
+
overall_health_score=overall_score,
|
|
439
|
+
grade=grade,
|
|
440
|
+
category_scores=category_scores,
|
|
441
|
+
issues=issues,
|
|
442
|
+
recommendations=recommendations,
|
|
443
|
+
trend=trend,
|
|
444
|
+
mode=self.mode,
|
|
445
|
+
agents_executed=len(agents),
|
|
446
|
+
success=strategy_result.success,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
def _calculate_category_scores(self, agent_results: dict[str, dict]) -> list[CategoryScore]:
|
|
450
|
+
"""Calculate health scores for each category.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
agent_results: Results from all agents
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
List of CategoryScore objects
|
|
457
|
+
"""
|
|
458
|
+
scores = []
|
|
459
|
+
|
|
460
|
+
# Security score (from security_auditor)
|
|
461
|
+
security_result = agent_results.get("security_auditor", {}).get("output", {})
|
|
462
|
+
critical_issues = security_result.get("critical_issues", 0)
|
|
463
|
+
high_issues = security_result.get("high_issues", 0)
|
|
464
|
+
medium_issues = security_result.get("medium_issues", 0)
|
|
465
|
+
|
|
466
|
+
security_score = 100.0
|
|
467
|
+
security_issues = []
|
|
468
|
+
|
|
469
|
+
if critical_issues > 0:
|
|
470
|
+
security_score -= critical_issues * 20 # -20 per critical
|
|
471
|
+
security_issues.append(f"{critical_issues} critical security issue(s)")
|
|
472
|
+
if high_issues > 0:
|
|
473
|
+
security_score -= high_issues * 10 # -10 per high
|
|
474
|
+
security_issues.append(f"{high_issues} high severity issue(s)")
|
|
475
|
+
if medium_issues > 0:
|
|
476
|
+
security_score -= medium_issues * 5 # -5 per medium
|
|
477
|
+
security_issues.append(f"{medium_issues} medium severity issue(s)")
|
|
478
|
+
|
|
479
|
+
security_score = max(0.0, security_score)
|
|
480
|
+
|
|
481
|
+
scores.append(
|
|
482
|
+
CategoryScore(
|
|
483
|
+
name="Security",
|
|
484
|
+
score=security_score,
|
|
485
|
+
weight=self.CATEGORY_WEIGHTS["Security"],
|
|
486
|
+
raw_metrics={
|
|
487
|
+
"critical": critical_issues,
|
|
488
|
+
"high": high_issues,
|
|
489
|
+
"medium": medium_issues,
|
|
490
|
+
},
|
|
491
|
+
issues=security_issues,
|
|
492
|
+
passed=critical_issues == 0 and high_issues == 0,
|
|
493
|
+
)
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# Coverage score (from test_coverage_analyzer)
|
|
497
|
+
coverage_result = agent_results.get("test_coverage_analyzer", {}).get("output", {})
|
|
498
|
+
coverage_percent = coverage_result.get("coverage_percent", 0.0)
|
|
499
|
+
|
|
500
|
+
coverage_issues = []
|
|
501
|
+
if coverage_percent < 80.0:
|
|
502
|
+
coverage_issues.append(f"Coverage below 80% ({coverage_percent:.1f}%)")
|
|
503
|
+
|
|
504
|
+
scores.append(
|
|
505
|
+
CategoryScore(
|
|
506
|
+
name="Coverage",
|
|
507
|
+
score=coverage_percent,
|
|
508
|
+
weight=self.CATEGORY_WEIGHTS["Coverage"],
|
|
509
|
+
raw_metrics={"coverage_percent": coverage_percent},
|
|
510
|
+
issues=coverage_issues,
|
|
511
|
+
passed=coverage_percent >= 80.0,
|
|
512
|
+
)
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# Quality score (from code_reviewer)
|
|
516
|
+
quality_result = agent_results.get("code_reviewer", {}).get("output", {})
|
|
517
|
+
quality_score = quality_result.get("quality_score", 0.0)
|
|
518
|
+
# Convert 0-10 scale to 0-100
|
|
519
|
+
quality_score_100 = quality_score * 10
|
|
520
|
+
|
|
521
|
+
quality_issues = []
|
|
522
|
+
if quality_score < 7.0:
|
|
523
|
+
quality_issues.append(f"Quality score below 7 ({quality_score:.1f}/10)")
|
|
524
|
+
|
|
525
|
+
scores.append(
|
|
526
|
+
CategoryScore(
|
|
527
|
+
name="Quality",
|
|
528
|
+
score=quality_score_100,
|
|
529
|
+
weight=self.CATEGORY_WEIGHTS["Quality"],
|
|
530
|
+
raw_metrics={"quality_score": quality_score},
|
|
531
|
+
issues=quality_issues,
|
|
532
|
+
passed=quality_score >= 7.0,
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
# Performance score (from performance_optimizer, if available)
|
|
537
|
+
if "performance_optimizer" in agent_results:
|
|
538
|
+
perf_result = agent_results.get("performance_optimizer", {}).get("output", {})
|
|
539
|
+
bottleneck_count = perf_result.get("bottleneck_count", 0)
|
|
540
|
+
|
|
541
|
+
perf_score = 100.0 - (bottleneck_count * 10) # -10 per bottleneck
|
|
542
|
+
perf_score = max(0.0, perf_score)
|
|
543
|
+
|
|
544
|
+
perf_issues = []
|
|
545
|
+
if bottleneck_count > 0:
|
|
546
|
+
perf_issues.append(f"{bottleneck_count} performance bottleneck(s)")
|
|
547
|
+
|
|
548
|
+
scores.append(
|
|
549
|
+
CategoryScore(
|
|
550
|
+
name="Performance",
|
|
551
|
+
score=perf_score,
|
|
552
|
+
weight=self.CATEGORY_WEIGHTS["Performance"],
|
|
553
|
+
raw_metrics={"bottleneck_count": bottleneck_count},
|
|
554
|
+
issues=perf_issues,
|
|
555
|
+
passed=bottleneck_count <= 2,
|
|
556
|
+
)
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
# Documentation score (from documentation_writer, if available)
|
|
560
|
+
if "documentation_writer" in agent_results:
|
|
561
|
+
docs_result = agent_results.get("documentation_writer", {}).get("output", {})
|
|
562
|
+
doc_coverage = docs_result.get("coverage_percent", 0.0)
|
|
563
|
+
|
|
564
|
+
doc_issues = []
|
|
565
|
+
if doc_coverage < 100.0:
|
|
566
|
+
doc_issues.append(f"Documentation incomplete ({doc_coverage:.1f}%)")
|
|
567
|
+
|
|
568
|
+
scores.append(
|
|
569
|
+
CategoryScore(
|
|
570
|
+
name="Documentation",
|
|
571
|
+
score=doc_coverage,
|
|
572
|
+
weight=self.CATEGORY_WEIGHTS["Documentation"],
|
|
573
|
+
raw_metrics={"coverage_percent": doc_coverage},
|
|
574
|
+
issues=doc_issues,
|
|
575
|
+
passed=doc_coverage >= 90.0,
|
|
576
|
+
)
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
return scores
|
|
580
|
+
|
|
581
|
+
def _calculate_overall_score(self, category_scores: list[CategoryScore]) -> float:
|
|
582
|
+
"""Calculate weighted overall health score.
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
category_scores: Category scores
|
|
586
|
+
|
|
587
|
+
Returns:
|
|
588
|
+
Overall score 0-100
|
|
589
|
+
"""
|
|
590
|
+
total_score = 0.0
|
|
591
|
+
total_weight = 0.0
|
|
592
|
+
|
|
593
|
+
for category in category_scores:
|
|
594
|
+
total_score += category.score * category.weight
|
|
595
|
+
total_weight += category.weight
|
|
596
|
+
|
|
597
|
+
if total_weight == 0:
|
|
598
|
+
return 0.0
|
|
599
|
+
|
|
600
|
+
return total_score / total_weight
|
|
601
|
+
|
|
602
|
+
def _assign_grade(self, score: float) -> str:
|
|
603
|
+
"""Assign letter grade based on score.
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
score: Overall health score 0-100
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
Letter grade (A/B/C/D/F)
|
|
610
|
+
"""
|
|
611
|
+
for grade, threshold in self.GRADE_THRESHOLDS.items():
|
|
612
|
+
if score >= threshold:
|
|
613
|
+
return grade
|
|
614
|
+
return "F"
|
|
615
|
+
|
|
616
|
+
def _generate_recommendations(self, category_scores: list[CategoryScore]) -> list[str]:
|
|
617
|
+
"""Generate actionable recommendations with specific commands.
|
|
618
|
+
|
|
619
|
+
Args:
|
|
620
|
+
category_scores: Category scores
|
|
621
|
+
|
|
622
|
+
Returns:
|
|
623
|
+
List of recommendations with commands to run
|
|
624
|
+
"""
|
|
625
|
+
recommendations = []
|
|
626
|
+
|
|
627
|
+
# Sort categories by score (lowest first)
|
|
628
|
+
sorted_categories = sorted(category_scores, key=lambda x: x.score)
|
|
629
|
+
|
|
630
|
+
for category in sorted_categories:
|
|
631
|
+
if not category.passed:
|
|
632
|
+
if category.name == "Security":
|
|
633
|
+
recommendations.append(f"🔒 Address {len(category.issues)} security issue(s)")
|
|
634
|
+
recommendations.append(" → Run: empathy workflow run security-audit --path .")
|
|
635
|
+
elif category.name == "Coverage":
|
|
636
|
+
recommendations.append(
|
|
637
|
+
f"🧪 Increase test coverage to 80%+ (currently {category.score:.1f}%)"
|
|
638
|
+
)
|
|
639
|
+
recommendations.append(" → Run: pytest --cov=src --cov-report=term-missing")
|
|
640
|
+
recommendations.append(
|
|
641
|
+
" → Or use: empathy workflow run test-gen --path <file>"
|
|
642
|
+
)
|
|
643
|
+
elif category.name == "Quality":
|
|
644
|
+
quality_score = category.raw_metrics.get("quality_score", 0.0)
|
|
645
|
+
recommendations.append(
|
|
646
|
+
f"✨ Improve code quality to 7+ (currently {quality_score:.1f}/10)"
|
|
647
|
+
)
|
|
648
|
+
recommendations.append(" → Run: empathy workflow run code-review --path .")
|
|
649
|
+
recommendations.append(
|
|
650
|
+
" → Or: empathy fix-all (auto-fix lint/format issues)"
|
|
651
|
+
)
|
|
652
|
+
elif category.name == "Performance":
|
|
653
|
+
bottlenecks = category.raw_metrics.get("bottleneck_count", 0)
|
|
654
|
+
recommendations.append(f"⚡ Optimize {bottlenecks} performance bottleneck(s)")
|
|
655
|
+
recommendations.append(" → Run: empathy workflow run perf-audit --path .")
|
|
656
|
+
elif category.name == "Documentation":
|
|
657
|
+
recommendations.append(
|
|
658
|
+
f"📚 Complete documentation (currently {category.score:.1f}%)"
|
|
659
|
+
)
|
|
660
|
+
recommendations.append(" → Run: empathy workflow run doc-gen --path .")
|
|
661
|
+
|
|
662
|
+
# Add general recommendations
|
|
663
|
+
if len(recommendations) == 0:
|
|
664
|
+
recommendations.append("✅ Project health looks good! Keep up the good work.")
|
|
665
|
+
recommendations.append(
|
|
666
|
+
" → Run: empathy orchestrate health-check --mode weekly (for deeper analysis)"
|
|
667
|
+
)
|
|
668
|
+
elif len(recommendations) >= 6: # Multiple issues
|
|
669
|
+
recommendations.append("")
|
|
670
|
+
recommendations.append("💡 Tip: Focus on top priority first for maximum impact")
|
|
671
|
+
recommendations.append(
|
|
672
|
+
" → Rerun: empathy orchestrate health-check --mode daily (to track progress)"
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
return recommendations
|
|
676
|
+
|
|
677
|
+
def _get_trend_comparison(self, current_score: float) -> str:
|
|
678
|
+
"""Compare current score with last check.
|
|
679
|
+
|
|
680
|
+
Args:
|
|
681
|
+
current_score: Current health score
|
|
682
|
+
|
|
683
|
+
Returns:
|
|
684
|
+
Trend description
|
|
685
|
+
"""
|
|
686
|
+
history_file = self.tracking_dir / "history.jsonl"
|
|
687
|
+
|
|
688
|
+
if not history_file.exists():
|
|
689
|
+
return "No historical data"
|
|
690
|
+
|
|
691
|
+
# Read last score from history
|
|
692
|
+
try:
|
|
693
|
+
with history_file.open("r") as f:
|
|
694
|
+
lines = f.readlines()
|
|
695
|
+
if len(lines) < 2:
|
|
696
|
+
return "First baseline established"
|
|
697
|
+
|
|
698
|
+
# Get second-to-last entry (last is current)
|
|
699
|
+
previous_entry = json.loads(lines[-2])
|
|
700
|
+
previous_score = previous_entry.get("overall_health_score", 0.0)
|
|
701
|
+
|
|
702
|
+
delta = current_score - previous_score
|
|
703
|
+
|
|
704
|
+
if abs(delta) < 1.0:
|
|
705
|
+
return f"Stable (~{previous_score:.1f})"
|
|
706
|
+
elif delta > 0:
|
|
707
|
+
return f"Improving (+{delta:.1f} from {previous_score:.1f})"
|
|
708
|
+
else:
|
|
709
|
+
return f"Declining ({delta:.1f} from {previous_score:.1f})"
|
|
710
|
+
|
|
711
|
+
except (json.JSONDecodeError, KeyError, IndexError) as e:
|
|
712
|
+
logger.warning(f"Error reading tracking history: {e}")
|
|
713
|
+
return "Unable to determine trend"
|
|
714
|
+
|
|
715
|
+
def _save_tracking_history(self, report: HealthCheckReport) -> None:
|
|
716
|
+
"""Save health check report to tracking history.
|
|
717
|
+
|
|
718
|
+
Args:
|
|
719
|
+
report: Health check report to save
|
|
720
|
+
"""
|
|
721
|
+
history_file = self.tracking_dir / "history.jsonl"
|
|
722
|
+
|
|
723
|
+
try:
|
|
724
|
+
# Append to history file (JSONL format)
|
|
725
|
+
with history_file.open("a") as f:
|
|
726
|
+
entry = {
|
|
727
|
+
"timestamp": report.timestamp,
|
|
728
|
+
"mode": report.mode,
|
|
729
|
+
"overall_health_score": report.overall_health_score,
|
|
730
|
+
"grade": report.grade,
|
|
731
|
+
"execution_time": report.execution_time,
|
|
732
|
+
"category_scores": [
|
|
733
|
+
{"name": cat.name, "score": cat.score} for cat in report.category_scores
|
|
734
|
+
],
|
|
735
|
+
}
|
|
736
|
+
f.write(json.dumps(entry) + "\n")
|
|
737
|
+
|
|
738
|
+
logger.info(f"Saved health check to tracking history: {history_file}")
|
|
739
|
+
|
|
740
|
+
except OSError as e:
|
|
741
|
+
logger.error(f"Failed to save tracking history: {e}")
|
|
742
|
+
|
|
743
|
+
def _save_health_json(self, report: HealthCheckReport) -> None:
|
|
744
|
+
"""Save health check report to .attune/health.json for VS Code extension.
|
|
745
|
+
|
|
746
|
+
This creates the health.json file that the Empathy VS Code extension
|
|
747
|
+
reads to display the interactive health dashboard.
|
|
748
|
+
|
|
749
|
+
Args:
|
|
750
|
+
report: Health check report to save
|
|
751
|
+
"""
|
|
752
|
+
health_file = self.project_root / ".empathy" / "health.json"
|
|
753
|
+
|
|
754
|
+
try:
|
|
755
|
+
# Ensure .empathy directory exists
|
|
756
|
+
health_file.parent.mkdir(parents=True, exist_ok=True)
|
|
757
|
+
|
|
758
|
+
# Extract metrics from category scores
|
|
759
|
+
lint_errors = 0
|
|
760
|
+
type_errors = 0
|
|
761
|
+
security_high = 0
|
|
762
|
+
security_medium = 0
|
|
763
|
+
security_low = 0
|
|
764
|
+
test_passed = 0
|
|
765
|
+
test_failed = 0
|
|
766
|
+
test_total = 0
|
|
767
|
+
coverage_pct = 0.0
|
|
768
|
+
|
|
769
|
+
for category in report.category_scores:
|
|
770
|
+
if category.name == "Quality":
|
|
771
|
+
# Quality issues often come from lint/type errors
|
|
772
|
+
quality_score = category.raw_metrics.get("quality_score", 10.0)
|
|
773
|
+
# Estimate errors from quality score (10 = perfect, 0 = many errors)
|
|
774
|
+
lint_errors = max(0, int((10 - quality_score) * 5))
|
|
775
|
+
|
|
776
|
+
elif category.name == "Security":
|
|
777
|
+
security_high = category.raw_metrics.get("critical_issues", 0)
|
|
778
|
+
security_medium = category.raw_metrics.get("high_issues", 0)
|
|
779
|
+
security_low = category.raw_metrics.get("medium_issues", 0)
|
|
780
|
+
|
|
781
|
+
elif category.name == "Coverage":
|
|
782
|
+
coverage_pct = category.score
|
|
783
|
+
# Estimate test counts (assuming good coverage means tests are passing)
|
|
784
|
+
if coverage_pct > 70:
|
|
785
|
+
test_total = 100
|
|
786
|
+
test_passed = int(coverage_pct)
|
|
787
|
+
test_failed = test_total - test_passed
|
|
788
|
+
|
|
789
|
+
# Build health data in VS Code extension format
|
|
790
|
+
health_data = {
|
|
791
|
+
"score": int(report.overall_health_score),
|
|
792
|
+
"lint": {"errors": lint_errors, "warnings": 0},
|
|
793
|
+
"types": {"errors": type_errors},
|
|
794
|
+
"security": {
|
|
795
|
+
"high": security_high,
|
|
796
|
+
"medium": security_medium,
|
|
797
|
+
"low": security_low,
|
|
798
|
+
},
|
|
799
|
+
"tests": {
|
|
800
|
+
"passed": test_passed,
|
|
801
|
+
"failed": test_failed,
|
|
802
|
+
"total": test_total,
|
|
803
|
+
"coverage": int(coverage_pct),
|
|
804
|
+
},
|
|
805
|
+
"tech_debt": {"total": 0, "todos": 0, "fixmes": 0, "hacks": 0},
|
|
806
|
+
"timestamp": report.timestamp,
|
|
807
|
+
"mode": report.mode,
|
|
808
|
+
"grade": report.grade,
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
# Write health.json
|
|
812
|
+
validated_health_file = _validate_file_path(str(health_file))
|
|
813
|
+
with validated_health_file.open("w") as f:
|
|
814
|
+
json.dump(health_data, f, indent=2)
|
|
815
|
+
|
|
816
|
+
logger.info(f"Saved health data to {validated_health_file} for VS Code extension")
|
|
817
|
+
|
|
818
|
+
except OSError as e:
|
|
819
|
+
logger.warning(f"Failed to save health.json (file system error): {e}")
|
|
820
|
+
except (TypeError, ValueError) as e:
|
|
821
|
+
logger.error(f"Failed to save health.json (serialization error): {e}")
|
|
822
|
+
except Exception as e: # noqa: BLE001
|
|
823
|
+
# INTENTIONAL: Saving health data should never crash a health check
|
|
824
|
+
logger.warning(f"Failed to save health.json (unexpected error): {e}")
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
async def main():
|
|
828
|
+
"""CLI entry point for orchestrated health check."""
|
|
829
|
+
import sys
|
|
830
|
+
|
|
831
|
+
# Parse arguments
|
|
832
|
+
mode = sys.argv[1] if len(sys.argv) > 1 else "daily"
|
|
833
|
+
project_root = sys.argv[2] if len(sys.argv) > 2 else "."
|
|
834
|
+
|
|
835
|
+
# Create workflow
|
|
836
|
+
workflow = OrchestratedHealthCheckWorkflow(mode=mode, project_root=project_root)
|
|
837
|
+
|
|
838
|
+
# Execute
|
|
839
|
+
report = await workflow.execute()
|
|
840
|
+
|
|
841
|
+
# Print report
|
|
842
|
+
print(report.format_console_output())
|
|
843
|
+
|
|
844
|
+
# Exit with appropriate code
|
|
845
|
+
sys.exit(0 if report.overall_health_score >= 70 else 1)
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
if __name__ == "__main__":
|
|
849
|
+
asyncio.run(main())
|