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,762 @@
|
|
|
1
|
+
"""PR Review Workflow
|
|
2
|
+
|
|
3
|
+
A comprehensive PR review workflow that combines CodeReviewCrew and
|
|
4
|
+
SecurityAuditCrew for thorough code and security analysis.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Runs both crews in parallel for speed
|
|
8
|
+
- Merges findings from code quality and security perspectives
|
|
9
|
+
- Provides unified verdict and risk assessment
|
|
10
|
+
- Graceful fallback if crews are unavailable
|
|
11
|
+
|
|
12
|
+
Copyright 2025 Smart-AI-Memory
|
|
13
|
+
Licensed under Fair Source License 0.9
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import logging
|
|
18
|
+
import time
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class PRReviewResult:
|
|
26
|
+
"""Result from PRReviewWorkflow execution."""
|
|
27
|
+
|
|
28
|
+
success: bool
|
|
29
|
+
verdict: str # "approve", "approve_with_suggestions", "request_changes", "reject"
|
|
30
|
+
code_quality_score: float
|
|
31
|
+
security_risk_score: float
|
|
32
|
+
combined_score: float
|
|
33
|
+
code_review: dict | None
|
|
34
|
+
security_audit: dict | None
|
|
35
|
+
all_findings: list[dict]
|
|
36
|
+
code_findings: list[dict]
|
|
37
|
+
security_findings: list[dict]
|
|
38
|
+
critical_count: int
|
|
39
|
+
high_count: int
|
|
40
|
+
blockers: list[str]
|
|
41
|
+
warnings: list[str]
|
|
42
|
+
recommendations: list[str]
|
|
43
|
+
summary: str
|
|
44
|
+
agents_used: list[str]
|
|
45
|
+
duration_seconds: float
|
|
46
|
+
cost: float = 0.0 # Total cost from code review and security audit crews
|
|
47
|
+
metadata: dict = field(default_factory=dict)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PRReviewWorkflow:
|
|
51
|
+
"""Combined code review + security audit for comprehensive PR analysis.
|
|
52
|
+
|
|
53
|
+
Runs CodeReviewCrew and SecurityAuditCrew in parallel for maximum
|
|
54
|
+
speed while providing thorough analysis from both perspectives.
|
|
55
|
+
|
|
56
|
+
Usage:
|
|
57
|
+
workflow = PRReviewWorkflow()
|
|
58
|
+
result = await workflow.execute(
|
|
59
|
+
diff="...",
|
|
60
|
+
files_changed=["src/main.py"],
|
|
61
|
+
target_path="./src",
|
|
62
|
+
)
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
provider: str = "anthropic",
|
|
68
|
+
use_code_crew: bool = True,
|
|
69
|
+
use_security_crew: bool = True,
|
|
70
|
+
parallel: bool = True,
|
|
71
|
+
code_crew_config: dict | None = None,
|
|
72
|
+
security_crew_config: dict | None = None,
|
|
73
|
+
):
|
|
74
|
+
"""Initialize the workflow.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
provider: LLM provider to use (anthropic, openai, etc.)
|
|
78
|
+
use_code_crew: Enable CodeReviewCrew
|
|
79
|
+
use_security_crew: Enable SecurityAuditCrew
|
|
80
|
+
parallel: Run crews in parallel (recommended)
|
|
81
|
+
code_crew_config: Configuration for CodeReviewCrew
|
|
82
|
+
security_crew_config: Configuration for SecurityAuditCrew
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
self.provider = provider
|
|
86
|
+
self.use_code_crew = use_code_crew
|
|
87
|
+
self.use_security_crew = use_security_crew
|
|
88
|
+
self.parallel = parallel
|
|
89
|
+
|
|
90
|
+
# Map "hybrid" to a real provider for crews (they don't understand "hybrid")
|
|
91
|
+
crew_provider = "anthropic" if provider == "hybrid" else provider
|
|
92
|
+
|
|
93
|
+
# Inject provider into crew configs
|
|
94
|
+
self.code_crew_config = {"provider": crew_provider, **(code_crew_config or {})}
|
|
95
|
+
self.security_crew_config = {"provider": crew_provider, **(security_crew_config or {})}
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def for_comprehensive_review(cls) -> "PRReviewWorkflow":
|
|
99
|
+
"""Factory for comprehensive PR review with all crews."""
|
|
100
|
+
return cls(
|
|
101
|
+
use_code_crew=True,
|
|
102
|
+
use_security_crew=True,
|
|
103
|
+
parallel=True,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def for_security_focused(cls) -> "PRReviewWorkflow":
|
|
108
|
+
"""Factory for security-focused review."""
|
|
109
|
+
return cls(
|
|
110
|
+
use_code_crew=False,
|
|
111
|
+
use_security_crew=True,
|
|
112
|
+
parallel=False,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def for_code_quality_focused(cls) -> "PRReviewWorkflow":
|
|
117
|
+
"""Factory for code quality-focused review."""
|
|
118
|
+
return cls(
|
|
119
|
+
use_code_crew=True,
|
|
120
|
+
use_security_crew=False,
|
|
121
|
+
parallel=False,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
async def execute(
|
|
125
|
+
self,
|
|
126
|
+
diff: str | None = None,
|
|
127
|
+
files_changed: list[str] | None = None,
|
|
128
|
+
target_path: str = ".",
|
|
129
|
+
target: str | None = None, # Alias for target_path (compatibility)
|
|
130
|
+
context: dict | None = None,
|
|
131
|
+
) -> PRReviewResult:
|
|
132
|
+
"""Execute comprehensive PR review with both crews.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
diff: PR diff content (auto-generated from git if not provided)
|
|
136
|
+
files_changed: List of changed files
|
|
137
|
+
target_path: Path to codebase for security audit
|
|
138
|
+
target: Alias for target_path (for CLI compatibility)
|
|
139
|
+
context: Additional context
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
PRReviewResult with combined analysis
|
|
143
|
+
|
|
144
|
+
"""
|
|
145
|
+
start_time = time.time()
|
|
146
|
+
files_changed = files_changed or []
|
|
147
|
+
context = context or {}
|
|
148
|
+
|
|
149
|
+
# Support 'target' as alias for 'target_path'
|
|
150
|
+
if target and target_path == ".":
|
|
151
|
+
target_path = target
|
|
152
|
+
|
|
153
|
+
# Auto-generate diff from git if not provided
|
|
154
|
+
if not diff:
|
|
155
|
+
import subprocess
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
# Get diff of staged and unstaged changes
|
|
159
|
+
git_result = subprocess.run(
|
|
160
|
+
["git", "diff", "HEAD"],
|
|
161
|
+
check=False,
|
|
162
|
+
cwd=target_path,
|
|
163
|
+
capture_output=True,
|
|
164
|
+
text=True,
|
|
165
|
+
timeout=30,
|
|
166
|
+
)
|
|
167
|
+
diff = git_result.stdout or ""
|
|
168
|
+
if not diff:
|
|
169
|
+
# Try getting diff against main/master
|
|
170
|
+
for branch in ["main", "master"]:
|
|
171
|
+
git_result = subprocess.run(
|
|
172
|
+
["git", "diff", branch],
|
|
173
|
+
check=False,
|
|
174
|
+
cwd=target_path,
|
|
175
|
+
capture_output=True,
|
|
176
|
+
text=True,
|
|
177
|
+
timeout=30,
|
|
178
|
+
)
|
|
179
|
+
if git_result.stdout:
|
|
180
|
+
diff = git_result.stdout
|
|
181
|
+
break
|
|
182
|
+
if not diff:
|
|
183
|
+
diff = "(No diff available - no changes detected)"
|
|
184
|
+
except Exception:
|
|
185
|
+
diff = "(Could not generate diff from git)"
|
|
186
|
+
|
|
187
|
+
# Initialize result collectors
|
|
188
|
+
code_review: dict | None = None
|
|
189
|
+
security_audit: dict | None = None
|
|
190
|
+
code_findings: list[dict] = []
|
|
191
|
+
security_findings: list[dict] = []
|
|
192
|
+
blockers: list[str] = []
|
|
193
|
+
warnings: list[str] = []
|
|
194
|
+
recommendations: list[str] = []
|
|
195
|
+
agents_used: list[str] = []
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
if self.parallel and self.use_code_crew and self.use_security_crew:
|
|
199
|
+
# Run both crews in parallel
|
|
200
|
+
code_review, security_audit = await self._run_parallel(
|
|
201
|
+
diff,
|
|
202
|
+
files_changed,
|
|
203
|
+
target_path,
|
|
204
|
+
)
|
|
205
|
+
else:
|
|
206
|
+
# Run sequentially
|
|
207
|
+
if self.use_code_crew:
|
|
208
|
+
code_review = await self._run_code_review(diff, files_changed)
|
|
209
|
+
if self.use_security_crew:
|
|
210
|
+
security_audit = await self._run_security_audit(target_path)
|
|
211
|
+
|
|
212
|
+
# Collect findings and costs from code review
|
|
213
|
+
total_cost = 0.0
|
|
214
|
+
if code_review:
|
|
215
|
+
code_findings = code_review.get("findings", [])
|
|
216
|
+
agents_used.extend(code_review.get("agents_used", []))
|
|
217
|
+
for f in code_findings:
|
|
218
|
+
if f.get("suggestion"):
|
|
219
|
+
recommendations.append(f["suggestion"])
|
|
220
|
+
# Accumulate cost from code review (if tracked by crew)
|
|
221
|
+
total_cost += code_review.get("cost", 0.0)
|
|
222
|
+
|
|
223
|
+
# Collect findings and costs from security audit
|
|
224
|
+
if security_audit:
|
|
225
|
+
security_findings = security_audit.get("findings", [])
|
|
226
|
+
agents_used.extend(security_audit.get("agents_used", []))
|
|
227
|
+
for f in security_findings:
|
|
228
|
+
if f.get("remediation"):
|
|
229
|
+
recommendations.append(f["remediation"])
|
|
230
|
+
# Accumulate cost from security audit (if tracked by crew)
|
|
231
|
+
total_cost += security_audit.get("cost", 0.0)
|
|
232
|
+
|
|
233
|
+
# Combine all findings
|
|
234
|
+
all_findings = self._merge_findings(code_findings, security_findings)
|
|
235
|
+
|
|
236
|
+
# Count by severity
|
|
237
|
+
critical_count = len([f for f in all_findings if f.get("severity") == "critical"])
|
|
238
|
+
high_count = len([f for f in all_findings if f.get("severity") == "high"])
|
|
239
|
+
|
|
240
|
+
# Determine blockers
|
|
241
|
+
if critical_count > 0:
|
|
242
|
+
blockers.append(f"{critical_count} critical issue(s) must be fixed")
|
|
243
|
+
if high_count > 3:
|
|
244
|
+
blockers.append(f"{high_count} high severity issues exceed threshold")
|
|
245
|
+
|
|
246
|
+
# Calculate scores
|
|
247
|
+
code_quality_score = self._get_code_quality_score(code_review)
|
|
248
|
+
security_risk_score = self._get_security_risk_score(security_audit)
|
|
249
|
+
combined_score = self._calculate_combined_score(code_quality_score, security_risk_score)
|
|
250
|
+
|
|
251
|
+
# Determine verdict
|
|
252
|
+
verdict = self._determine_verdict(code_review, security_audit, combined_score, blockers)
|
|
253
|
+
|
|
254
|
+
# Generate summary
|
|
255
|
+
summary = self._generate_summary(
|
|
256
|
+
verdict,
|
|
257
|
+
code_quality_score,
|
|
258
|
+
security_risk_score,
|
|
259
|
+
len(all_findings),
|
|
260
|
+
critical_count,
|
|
261
|
+
high_count,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Check for warnings
|
|
265
|
+
if not code_review and self.use_code_crew:
|
|
266
|
+
warnings.append("CodeReviewCrew unavailable - code review limited")
|
|
267
|
+
if not security_audit and self.use_security_crew:
|
|
268
|
+
warnings.append("SecurityAuditCrew unavailable - security audit limited")
|
|
269
|
+
|
|
270
|
+
duration = time.time() - start_time
|
|
271
|
+
|
|
272
|
+
result = PRReviewResult(
|
|
273
|
+
success=True,
|
|
274
|
+
verdict=verdict,
|
|
275
|
+
code_quality_score=code_quality_score,
|
|
276
|
+
security_risk_score=security_risk_score,
|
|
277
|
+
combined_score=combined_score,
|
|
278
|
+
code_review=code_review,
|
|
279
|
+
security_audit=security_audit,
|
|
280
|
+
all_findings=all_findings,
|
|
281
|
+
code_findings=code_findings,
|
|
282
|
+
security_findings=security_findings,
|
|
283
|
+
critical_count=critical_count,
|
|
284
|
+
high_count=high_count,
|
|
285
|
+
blockers=blockers,
|
|
286
|
+
warnings=warnings,
|
|
287
|
+
recommendations=recommendations[:15], # Top 15
|
|
288
|
+
summary=summary,
|
|
289
|
+
agents_used=list(dict.fromkeys(agents_used)), # Deduplicate (preserves order)
|
|
290
|
+
duration_seconds=duration,
|
|
291
|
+
cost=total_cost,
|
|
292
|
+
metadata={
|
|
293
|
+
"files_changed": len(files_changed),
|
|
294
|
+
"total_findings": len(all_findings),
|
|
295
|
+
"code_crew_enabled": self.use_code_crew,
|
|
296
|
+
"security_crew_enabled": self.use_security_crew,
|
|
297
|
+
"parallel_execution": self.parallel,
|
|
298
|
+
},
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# Add formatted report for human readability
|
|
302
|
+
result.metadata["formatted_report"] = format_pr_review_report(result)
|
|
303
|
+
return result
|
|
304
|
+
|
|
305
|
+
except Exception as e:
|
|
306
|
+
logger.error(f"PRReviewWorkflow failed: {e}")
|
|
307
|
+
duration = time.time() - start_time
|
|
308
|
+
return PRReviewResult(
|
|
309
|
+
success=False,
|
|
310
|
+
verdict="reject",
|
|
311
|
+
code_quality_score=0.0,
|
|
312
|
+
security_risk_score=100.0,
|
|
313
|
+
combined_score=0.0,
|
|
314
|
+
code_review=code_review,
|
|
315
|
+
security_audit=security_audit,
|
|
316
|
+
all_findings=[],
|
|
317
|
+
code_findings=[],
|
|
318
|
+
security_findings=[],
|
|
319
|
+
critical_count=0,
|
|
320
|
+
high_count=0,
|
|
321
|
+
blockers=[f"Review failed: {e!s}"],
|
|
322
|
+
warnings=[],
|
|
323
|
+
recommendations=[],
|
|
324
|
+
summary=f"PR review failed with error: {e!s}",
|
|
325
|
+
agents_used=[],
|
|
326
|
+
duration_seconds=duration,
|
|
327
|
+
cost=0.0,
|
|
328
|
+
metadata={"error": str(e)},
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
async def _run_parallel(
|
|
332
|
+
self,
|
|
333
|
+
diff: str,
|
|
334
|
+
files_changed: list[str],
|
|
335
|
+
target_path: str,
|
|
336
|
+
) -> tuple[dict | None, dict | None]:
|
|
337
|
+
"""Run both crews in parallel."""
|
|
338
|
+
code_task = asyncio.create_task(self._run_code_review(diff, files_changed))
|
|
339
|
+
security_task = asyncio.create_task(self._run_security_audit(target_path))
|
|
340
|
+
|
|
341
|
+
results = await asyncio.gather(code_task, security_task, return_exceptions=True)
|
|
342
|
+
|
|
343
|
+
code_review: dict | None = results[0] if isinstance(results[0], dict) else None
|
|
344
|
+
security_audit: dict | None = results[1] if isinstance(results[1], dict) else None
|
|
345
|
+
|
|
346
|
+
if isinstance(results[0], Exception):
|
|
347
|
+
logger.warning(f"Code review failed: {results[0]}")
|
|
348
|
+
if isinstance(results[1], Exception):
|
|
349
|
+
logger.warning(f"Security audit failed: {results[1]}")
|
|
350
|
+
|
|
351
|
+
return code_review, security_audit
|
|
352
|
+
|
|
353
|
+
async def _run_code_review(
|
|
354
|
+
self,
|
|
355
|
+
diff: str,
|
|
356
|
+
files_changed: list[str],
|
|
357
|
+
) -> dict | None:
|
|
358
|
+
"""Run CodeReviewCrew."""
|
|
359
|
+
try:
|
|
360
|
+
from .code_review_adapters import (
|
|
361
|
+
_check_crew_available,
|
|
362
|
+
_get_crew_review,
|
|
363
|
+
crew_report_to_workflow_format,
|
|
364
|
+
)
|
|
365
|
+
except ImportError:
|
|
366
|
+
logger.info("CodeReviewCrew adapters not installed")
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
if not _check_crew_available():
|
|
370
|
+
logger.info("CodeReviewCrew not available")
|
|
371
|
+
return None
|
|
372
|
+
|
|
373
|
+
report = await _get_crew_review(
|
|
374
|
+
diff=diff,
|
|
375
|
+
files_changed=files_changed,
|
|
376
|
+
config=self.code_crew_config,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
if report:
|
|
380
|
+
return crew_report_to_workflow_format(report)
|
|
381
|
+
return None
|
|
382
|
+
|
|
383
|
+
async def _run_security_audit(
|
|
384
|
+
self,
|
|
385
|
+
target_path: str,
|
|
386
|
+
) -> dict | None:
|
|
387
|
+
"""Run SecurityAuditCrew."""
|
|
388
|
+
try:
|
|
389
|
+
from .security_adapters import (
|
|
390
|
+
_check_crew_available,
|
|
391
|
+
_get_crew_audit,
|
|
392
|
+
crew_report_to_workflow_format,
|
|
393
|
+
)
|
|
394
|
+
except ImportError:
|
|
395
|
+
logger.info("SecurityAuditCrew adapters not installed")
|
|
396
|
+
return None
|
|
397
|
+
|
|
398
|
+
if not _check_crew_available():
|
|
399
|
+
logger.info("SecurityAuditCrew not available")
|
|
400
|
+
return None
|
|
401
|
+
|
|
402
|
+
report = await _get_crew_audit(
|
|
403
|
+
target=target_path,
|
|
404
|
+
config=self.security_crew_config,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
if report:
|
|
408
|
+
return crew_report_to_workflow_format(report)
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
def _merge_findings(
|
|
412
|
+
self,
|
|
413
|
+
code_findings: list[dict],
|
|
414
|
+
security_findings: list[dict],
|
|
415
|
+
) -> list[dict]:
|
|
416
|
+
"""Merge and deduplicate findings from both sources."""
|
|
417
|
+
# Tag findings with source
|
|
418
|
+
for f in code_findings:
|
|
419
|
+
f["source"] = "code_review"
|
|
420
|
+
for f in security_findings:
|
|
421
|
+
f["source"] = "security_audit"
|
|
422
|
+
|
|
423
|
+
# Combine and deduplicate by (file, line, type)
|
|
424
|
+
all_findings = code_findings + security_findings
|
|
425
|
+
seen = set()
|
|
426
|
+
unique = []
|
|
427
|
+
|
|
428
|
+
for f in all_findings:
|
|
429
|
+
key = (f.get("file"), f.get("line"), f.get("type") or f.get("title"))
|
|
430
|
+
if key not in seen:
|
|
431
|
+
seen.add(key)
|
|
432
|
+
unique.append(f)
|
|
433
|
+
|
|
434
|
+
# Sort by severity (critical first)
|
|
435
|
+
severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
|
|
436
|
+
unique.sort(key=lambda f: severity_order.get(f.get("severity", "medium"), 2))
|
|
437
|
+
|
|
438
|
+
return unique
|
|
439
|
+
|
|
440
|
+
def _get_code_quality_score(self, code_review: dict | None) -> float:
|
|
441
|
+
"""Extract code quality score from review."""
|
|
442
|
+
if code_review:
|
|
443
|
+
return float(code_review.get("quality_score", 85.0))
|
|
444
|
+
return 85.0 # Default if no review
|
|
445
|
+
|
|
446
|
+
def _get_security_risk_score(self, security_audit: dict | None) -> float:
|
|
447
|
+
"""Extract security risk score from audit."""
|
|
448
|
+
if security_audit:
|
|
449
|
+
return float(security_audit.get("risk_score", 20.0))
|
|
450
|
+
return 20.0 # Default if no audit
|
|
451
|
+
|
|
452
|
+
def _calculate_combined_score(
|
|
453
|
+
self,
|
|
454
|
+
code_quality: float,
|
|
455
|
+
security_risk: float,
|
|
456
|
+
) -> float:
|
|
457
|
+
"""Calculate combined score.
|
|
458
|
+
|
|
459
|
+
Higher is better. Combines code quality (0-100, higher=better)
|
|
460
|
+
with security risk (0-100, lower=better).
|
|
461
|
+
"""
|
|
462
|
+
# Convert security risk to "safety score" (invert)
|
|
463
|
+
security_safety = 100.0 - security_risk
|
|
464
|
+
|
|
465
|
+
# Weighted average: security is slightly more important
|
|
466
|
+
combined = (code_quality * 0.45) + (security_safety * 0.55)
|
|
467
|
+
return max(0.0, min(100.0, combined))
|
|
468
|
+
|
|
469
|
+
def _determine_verdict(
|
|
470
|
+
self,
|
|
471
|
+
code_review: dict | None,
|
|
472
|
+
security_audit: dict | None,
|
|
473
|
+
combined_score: float,
|
|
474
|
+
blockers: list[str],
|
|
475
|
+
) -> str:
|
|
476
|
+
"""Determine final PR verdict."""
|
|
477
|
+
verdicts = []
|
|
478
|
+
|
|
479
|
+
# Code review verdict
|
|
480
|
+
if code_review:
|
|
481
|
+
code_verdict = code_review.get("verdict", "approve")
|
|
482
|
+
verdicts.append(code_verdict)
|
|
483
|
+
|
|
484
|
+
# Security risk-based verdict
|
|
485
|
+
if security_audit:
|
|
486
|
+
risk = security_audit.get("risk_score", 0)
|
|
487
|
+
if risk >= 70:
|
|
488
|
+
verdicts.append("reject")
|
|
489
|
+
elif risk >= 50:
|
|
490
|
+
verdicts.append("request_changes")
|
|
491
|
+
elif risk >= 30:
|
|
492
|
+
verdicts.append("approve_with_suggestions")
|
|
493
|
+
|
|
494
|
+
# Score-based verdict
|
|
495
|
+
if combined_score < 50:
|
|
496
|
+
verdicts.append("reject")
|
|
497
|
+
elif combined_score < 70:
|
|
498
|
+
verdicts.append("request_changes")
|
|
499
|
+
elif combined_score < 85:
|
|
500
|
+
verdicts.append("approve_with_suggestions")
|
|
501
|
+
else:
|
|
502
|
+
verdicts.append("approve")
|
|
503
|
+
|
|
504
|
+
# Blockers force request_changes at minimum
|
|
505
|
+
if blockers:
|
|
506
|
+
verdicts.append("request_changes")
|
|
507
|
+
|
|
508
|
+
# Return most severe verdict
|
|
509
|
+
priority = ["reject", "request_changes", "approve_with_suggestions", "approve"]
|
|
510
|
+
for v in priority:
|
|
511
|
+
if v in verdicts:
|
|
512
|
+
return v
|
|
513
|
+
|
|
514
|
+
return "approve"
|
|
515
|
+
|
|
516
|
+
def _generate_summary(
|
|
517
|
+
self,
|
|
518
|
+
verdict: str,
|
|
519
|
+
code_quality: float,
|
|
520
|
+
security_risk: float,
|
|
521
|
+
total_findings: int,
|
|
522
|
+
critical_count: int,
|
|
523
|
+
high_count: int,
|
|
524
|
+
) -> str:
|
|
525
|
+
"""Generate human-readable summary."""
|
|
526
|
+
verdict_text = {
|
|
527
|
+
"approve": "PR is ready to merge",
|
|
528
|
+
"approve_with_suggestions": "PR can be merged with minor improvements",
|
|
529
|
+
"request_changes": "PR requires changes before merging",
|
|
530
|
+
"reject": "PR has critical issues and should not be merged",
|
|
531
|
+
}.get(verdict, "Unknown status")
|
|
532
|
+
|
|
533
|
+
summary_parts = [verdict_text]
|
|
534
|
+
|
|
535
|
+
if total_findings > 0:
|
|
536
|
+
findings_text = f"{total_findings} finding(s)"
|
|
537
|
+
if critical_count > 0:
|
|
538
|
+
findings_text += f" ({critical_count} critical)"
|
|
539
|
+
elif high_count > 0:
|
|
540
|
+
findings_text += f" ({high_count} high)"
|
|
541
|
+
summary_parts.append(findings_text)
|
|
542
|
+
|
|
543
|
+
summary_parts.append(f"Code quality: {code_quality:.0f}/100")
|
|
544
|
+
summary_parts.append(f"Security risk: {security_risk:.0f}/100")
|
|
545
|
+
|
|
546
|
+
return ". ".join(summary_parts) + "."
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
# CLI entry point
|
|
550
|
+
def main():
|
|
551
|
+
"""Run PRReviewWorkflow from command line."""
|
|
552
|
+
import argparse
|
|
553
|
+
|
|
554
|
+
parser = argparse.ArgumentParser(description="PR Review Workflow")
|
|
555
|
+
parser.add_argument("--diff", "-d", help="PR diff content")
|
|
556
|
+
parser.add_argument("--target", "-t", default=".", help="Target path for security audit")
|
|
557
|
+
parser.add_argument("--files", "-f", nargs="*", default=[], help="Changed files")
|
|
558
|
+
parser.add_argument("--parallel/--sequential", dest="parallel", default=True)
|
|
559
|
+
parser.add_argument("--code-only", action="store_true", help="Only run code review")
|
|
560
|
+
parser.add_argument("--security-only", action="store_true", help="Only run security audit")
|
|
561
|
+
|
|
562
|
+
args = parser.parse_args()
|
|
563
|
+
|
|
564
|
+
async def run():
|
|
565
|
+
if args.code_only:
|
|
566
|
+
workflow = PRReviewWorkflow.for_code_quality_focused()
|
|
567
|
+
elif args.security_only:
|
|
568
|
+
workflow = PRReviewWorkflow.for_security_focused()
|
|
569
|
+
else:
|
|
570
|
+
workflow = PRReviewWorkflow(parallel=args.parallel)
|
|
571
|
+
|
|
572
|
+
result = await workflow.execute(
|
|
573
|
+
diff=args.diff or "",
|
|
574
|
+
files_changed=args.files,
|
|
575
|
+
target_path=args.target,
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
print("\n" + "=" * 60)
|
|
579
|
+
print("PR REVIEW RESULTS")
|
|
580
|
+
print("=" * 60)
|
|
581
|
+
print(f"\nVerdict: {result.verdict.upper()}")
|
|
582
|
+
print(f"\n{result.summary}")
|
|
583
|
+
print(f"\nDuration: {result.duration_seconds * 1000:.0f}ms")
|
|
584
|
+
|
|
585
|
+
if result.agents_used:
|
|
586
|
+
print(f"\nAgents Used: {', '.join(result.agents_used)}")
|
|
587
|
+
|
|
588
|
+
print(f"\nFindings: {len(result.all_findings)} total")
|
|
589
|
+
print(f" Code: {len(result.code_findings)}")
|
|
590
|
+
print(f" Security: {len(result.security_findings)}")
|
|
591
|
+
print(f" Critical: {result.critical_count}")
|
|
592
|
+
print(f" High: {result.high_count}")
|
|
593
|
+
|
|
594
|
+
if result.blockers:
|
|
595
|
+
print("\nBlockers:")
|
|
596
|
+
for b in result.blockers:
|
|
597
|
+
print(f" - {b}")
|
|
598
|
+
|
|
599
|
+
if result.warnings:
|
|
600
|
+
print("\nWarnings:")
|
|
601
|
+
for w in result.warnings:
|
|
602
|
+
print(f" - {w}")
|
|
603
|
+
|
|
604
|
+
if result.recommendations[:5]:
|
|
605
|
+
print("\nTop Recommendations:")
|
|
606
|
+
for r in result.recommendations[:5]:
|
|
607
|
+
print(f" - {r[:80]}...")
|
|
608
|
+
|
|
609
|
+
asyncio.run(run())
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def format_pr_review_report(result: PRReviewResult) -> str:
|
|
613
|
+
"""Format PR review result as a human-readable report.
|
|
614
|
+
|
|
615
|
+
Args:
|
|
616
|
+
result: The PRReviewResult dataclass
|
|
617
|
+
|
|
618
|
+
Returns:
|
|
619
|
+
Formatted report string
|
|
620
|
+
|
|
621
|
+
"""
|
|
622
|
+
lines = []
|
|
623
|
+
|
|
624
|
+
# Header with verdict
|
|
625
|
+
verdict_emoji = {
|
|
626
|
+
"approve": "✅",
|
|
627
|
+
"approve_with_suggestions": "🟡",
|
|
628
|
+
"request_changes": "🟠",
|
|
629
|
+
"reject": "🔴",
|
|
630
|
+
}
|
|
631
|
+
emoji = verdict_emoji.get(result.verdict, "⚪")
|
|
632
|
+
|
|
633
|
+
lines.append("=" * 60)
|
|
634
|
+
lines.append("PR REVIEW REPORT")
|
|
635
|
+
lines.append("=" * 60)
|
|
636
|
+
lines.append("")
|
|
637
|
+
|
|
638
|
+
# Verdict banner
|
|
639
|
+
lines.append("-" * 60)
|
|
640
|
+
lines.append(f"{emoji} VERDICT: {result.verdict.upper().replace('_', ' ')}")
|
|
641
|
+
lines.append("-" * 60)
|
|
642
|
+
lines.append("")
|
|
643
|
+
|
|
644
|
+
# Scores
|
|
645
|
+
lines.append("-" * 60)
|
|
646
|
+
lines.append("SCORES")
|
|
647
|
+
lines.append("-" * 60)
|
|
648
|
+
|
|
649
|
+
# Code quality score with visual bar
|
|
650
|
+
cq_score = result.code_quality_score
|
|
651
|
+
cq_bar = "█" * int(cq_score / 10) + "░" * (10 - int(cq_score / 10))
|
|
652
|
+
lines.append(f"Code Quality: [{cq_bar}] {cq_score:.0f}/100")
|
|
653
|
+
|
|
654
|
+
# Security risk (inverted - lower is better)
|
|
655
|
+
sr_score = result.security_risk_score
|
|
656
|
+
sr_bar = "█" * int(sr_score / 10) + "░" * (10 - int(sr_score / 10))
|
|
657
|
+
risk_label = "LOW" if sr_score < 30 else "MEDIUM" if sr_score < 60 else "HIGH"
|
|
658
|
+
lines.append(f"Security Risk: [{sr_bar}] {sr_score:.0f}/100 ({risk_label})")
|
|
659
|
+
|
|
660
|
+
# Combined score
|
|
661
|
+
combined = result.combined_score
|
|
662
|
+
combined_bar = "█" * int(combined / 10) + "░" * (10 - int(combined / 10))
|
|
663
|
+
lines.append(f"Combined Score: [{combined_bar}] {combined:.0f}/100")
|
|
664
|
+
lines.append("")
|
|
665
|
+
|
|
666
|
+
# Summary
|
|
667
|
+
if result.summary:
|
|
668
|
+
lines.append("-" * 60)
|
|
669
|
+
lines.append("SUMMARY")
|
|
670
|
+
lines.append("-" * 60)
|
|
671
|
+
# Word wrap summary
|
|
672
|
+
words = result.summary.split()
|
|
673
|
+
current_line = ""
|
|
674
|
+
for word in words:
|
|
675
|
+
if len(current_line) + len(word) + 1 <= 58:
|
|
676
|
+
current_line += (" " if current_line else "") + word
|
|
677
|
+
else:
|
|
678
|
+
lines.append(current_line)
|
|
679
|
+
current_line = word
|
|
680
|
+
if current_line:
|
|
681
|
+
lines.append(current_line)
|
|
682
|
+
lines.append("")
|
|
683
|
+
|
|
684
|
+
# Blockers
|
|
685
|
+
if result.blockers:
|
|
686
|
+
lines.append("-" * 60)
|
|
687
|
+
lines.append("🚫 BLOCKERS (must fix before merge)")
|
|
688
|
+
lines.append("-" * 60)
|
|
689
|
+
for blocker in result.blockers:
|
|
690
|
+
lines.append(f" • {blocker}")
|
|
691
|
+
lines.append("")
|
|
692
|
+
|
|
693
|
+
# Findings summary
|
|
694
|
+
if result.all_findings:
|
|
695
|
+
lines.append("-" * 60)
|
|
696
|
+
lines.append("FINDINGS")
|
|
697
|
+
lines.append("-" * 60)
|
|
698
|
+
lines.append(f"Total: {len(result.all_findings)}")
|
|
699
|
+
lines.append(f" 🔴 Critical: {result.critical_count}")
|
|
700
|
+
lines.append(f" 🟠 High: {result.high_count}")
|
|
701
|
+
lines.append(f" Code Issues: {len(result.code_findings)}")
|
|
702
|
+
lines.append(f" Security Issues: {len(result.security_findings)}")
|
|
703
|
+
lines.append("")
|
|
704
|
+
|
|
705
|
+
# Show top critical/high findings
|
|
706
|
+
critical_high = [
|
|
707
|
+
f for f in result.all_findings if f.get("severity") in ("critical", "high")
|
|
708
|
+
]
|
|
709
|
+
if critical_high:
|
|
710
|
+
lines.append("Top Issues:")
|
|
711
|
+
for i, finding in enumerate(critical_high[:5], 1):
|
|
712
|
+
severity = finding.get("severity", "unknown")
|
|
713
|
+
title = finding.get("title", finding.get("message", "Unknown issue"))
|
|
714
|
+
emoji = "🔴" if severity == "critical" else "🟠"
|
|
715
|
+
if len(title) > 50:
|
|
716
|
+
title = title[:47] + "..."
|
|
717
|
+
lines.append(f" {emoji} {i}. {title}")
|
|
718
|
+
if len(critical_high) > 5:
|
|
719
|
+
lines.append(f" ... and {len(critical_high) - 5} more critical/high issues")
|
|
720
|
+
lines.append("")
|
|
721
|
+
|
|
722
|
+
# Warnings
|
|
723
|
+
if result.warnings:
|
|
724
|
+
lines.append("-" * 60)
|
|
725
|
+
lines.append("⚠️ WARNINGS")
|
|
726
|
+
lines.append("-" * 60)
|
|
727
|
+
for warning in result.warnings:
|
|
728
|
+
lines.append(f" • {warning}")
|
|
729
|
+
lines.append("")
|
|
730
|
+
|
|
731
|
+
# Recommendations
|
|
732
|
+
if result.recommendations:
|
|
733
|
+
lines.append("-" * 60)
|
|
734
|
+
lines.append("RECOMMENDATIONS")
|
|
735
|
+
lines.append("-" * 60)
|
|
736
|
+
for i, rec in enumerate(result.recommendations[:5], 1):
|
|
737
|
+
if len(rec) > 55:
|
|
738
|
+
rec = rec[:52] + "..."
|
|
739
|
+
lines.append(f" {i}. {rec}")
|
|
740
|
+
if len(result.recommendations) > 5:
|
|
741
|
+
lines.append(f" ... and {len(result.recommendations) - 5} more")
|
|
742
|
+
lines.append("")
|
|
743
|
+
|
|
744
|
+
# Agents used
|
|
745
|
+
if result.agents_used:
|
|
746
|
+
lines.append("-" * 60)
|
|
747
|
+
lines.append("AGENTS USED")
|
|
748
|
+
lines.append("-" * 60)
|
|
749
|
+
lines.append(f" {', '.join(result.agents_used)}")
|
|
750
|
+
lines.append("")
|
|
751
|
+
|
|
752
|
+
# Footer
|
|
753
|
+
lines.append("=" * 60)
|
|
754
|
+
duration_ms = result.duration_seconds * 1000
|
|
755
|
+
lines.append(f"Review completed in {duration_ms:.0f}ms | Cost: ${result.cost:.4f}")
|
|
756
|
+
lines.append("=" * 60)
|
|
757
|
+
|
|
758
|
+
return "\n".join(lines)
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
if __name__ == "__main__":
|
|
762
|
+
main()
|