empathy-framework 2.4.0__py3-none-any.whl → 3.8.2__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.
Files changed (329) hide show
  1. coach_wizards/__init__.py +13 -12
  2. coach_wizards/accessibility_wizard.py +12 -12
  3. coach_wizards/api_wizard.py +12 -12
  4. coach_wizards/base_wizard.py +26 -20
  5. coach_wizards/cicd_wizard.py +15 -13
  6. coach_wizards/code_reviewer_README.md +60 -0
  7. coach_wizards/code_reviewer_wizard.py +180 -0
  8. coach_wizards/compliance_wizard.py +12 -12
  9. coach_wizards/database_wizard.py +12 -12
  10. coach_wizards/debugging_wizard.py +12 -12
  11. coach_wizards/documentation_wizard.py +12 -12
  12. coach_wizards/generate_wizards.py +1 -2
  13. coach_wizards/localization_wizard.py +101 -19
  14. coach_wizards/migration_wizard.py +12 -12
  15. coach_wizards/monitoring_wizard.py +12 -12
  16. coach_wizards/observability_wizard.py +12 -12
  17. coach_wizards/performance_wizard.py +12 -12
  18. coach_wizards/prompt_engineering_wizard.py +661 -0
  19. coach_wizards/refactoring_wizard.py +12 -12
  20. coach_wizards/scaling_wizard.py +12 -12
  21. coach_wizards/security_wizard.py +12 -12
  22. coach_wizards/testing_wizard.py +12 -12
  23. empathy_framework-3.8.2.dist-info/METADATA +1176 -0
  24. empathy_framework-3.8.2.dist-info/RECORD +333 -0
  25. empathy_framework-3.8.2.dist-info/entry_points.txt +22 -0
  26. {empathy_framework-2.4.0.dist-info → empathy_framework-3.8.2.dist-info}/top_level.txt +5 -1
  27. empathy_healthcare_plugin/__init__.py +1 -2
  28. empathy_healthcare_plugin/monitors/__init__.py +9 -0
  29. empathy_healthcare_plugin/monitors/clinical_protocol_monitor.py +315 -0
  30. empathy_healthcare_plugin/monitors/monitoring/__init__.py +44 -0
  31. empathy_healthcare_plugin/monitors/monitoring/protocol_checker.py +300 -0
  32. empathy_healthcare_plugin/monitors/monitoring/protocol_loader.py +214 -0
  33. empathy_healthcare_plugin/monitors/monitoring/sensor_parsers.py +306 -0
  34. empathy_healthcare_plugin/monitors/monitoring/trajectory_analyzer.py +389 -0
  35. empathy_llm_toolkit/__init__.py +7 -7
  36. empathy_llm_toolkit/agent_factory/__init__.py +53 -0
  37. empathy_llm_toolkit/agent_factory/adapters/__init__.py +85 -0
  38. empathy_llm_toolkit/agent_factory/adapters/autogen_adapter.py +312 -0
  39. empathy_llm_toolkit/agent_factory/adapters/crewai_adapter.py +454 -0
  40. empathy_llm_toolkit/agent_factory/adapters/haystack_adapter.py +298 -0
  41. empathy_llm_toolkit/agent_factory/adapters/langchain_adapter.py +362 -0
  42. empathy_llm_toolkit/agent_factory/adapters/langgraph_adapter.py +333 -0
  43. empathy_llm_toolkit/agent_factory/adapters/native.py +228 -0
  44. empathy_llm_toolkit/agent_factory/adapters/wizard_adapter.py +426 -0
  45. empathy_llm_toolkit/agent_factory/base.py +305 -0
  46. empathy_llm_toolkit/agent_factory/crews/__init__.py +67 -0
  47. empathy_llm_toolkit/agent_factory/crews/code_review.py +1113 -0
  48. empathy_llm_toolkit/agent_factory/crews/health_check.py +1246 -0
  49. empathy_llm_toolkit/agent_factory/crews/refactoring.py +1128 -0
  50. empathy_llm_toolkit/agent_factory/crews/security_audit.py +1018 -0
  51. empathy_llm_toolkit/agent_factory/decorators.py +286 -0
  52. empathy_llm_toolkit/agent_factory/factory.py +558 -0
  53. empathy_llm_toolkit/agent_factory/framework.py +192 -0
  54. empathy_llm_toolkit/agent_factory/memory_integration.py +324 -0
  55. empathy_llm_toolkit/agent_factory/resilient.py +320 -0
  56. empathy_llm_toolkit/claude_memory.py +14 -15
  57. empathy_llm_toolkit/cli/__init__.py +8 -0
  58. empathy_llm_toolkit/cli/sync_claude.py +487 -0
  59. empathy_llm_toolkit/code_health.py +186 -28
  60. empathy_llm_toolkit/config/__init__.py +29 -0
  61. empathy_llm_toolkit/config/unified.py +295 -0
  62. empathy_llm_toolkit/contextual_patterns.py +11 -12
  63. empathy_llm_toolkit/core.py +168 -53
  64. empathy_llm_toolkit/git_pattern_extractor.py +17 -13
  65. empathy_llm_toolkit/levels.py +6 -13
  66. empathy_llm_toolkit/pattern_confidence.py +14 -18
  67. empathy_llm_toolkit/pattern_resolver.py +10 -12
  68. empathy_llm_toolkit/pattern_summary.py +16 -14
  69. empathy_llm_toolkit/providers.py +194 -28
  70. empathy_llm_toolkit/routing/__init__.py +32 -0
  71. empathy_llm_toolkit/routing/model_router.py +362 -0
  72. empathy_llm_toolkit/security/IMPLEMENTATION_SUMMARY.md +413 -0
  73. empathy_llm_toolkit/security/PHASE2_COMPLETE.md +384 -0
  74. empathy_llm_toolkit/security/PHASE2_SECRETS_DETECTOR_COMPLETE.md +271 -0
  75. empathy_llm_toolkit/security/QUICK_REFERENCE.md +316 -0
  76. empathy_llm_toolkit/security/README.md +262 -0
  77. empathy_llm_toolkit/security/__init__.py +62 -0
  78. empathy_llm_toolkit/security/audit_logger.py +929 -0
  79. empathy_llm_toolkit/security/audit_logger_example.py +152 -0
  80. empathy_llm_toolkit/security/pii_scrubber.py +640 -0
  81. empathy_llm_toolkit/security/secrets_detector.py +678 -0
  82. empathy_llm_toolkit/security/secrets_detector_example.py +304 -0
  83. empathy_llm_toolkit/security/secure_memdocs.py +1192 -0
  84. empathy_llm_toolkit/security/secure_memdocs_example.py +278 -0
  85. empathy_llm_toolkit/session_status.py +20 -22
  86. empathy_llm_toolkit/state.py +28 -21
  87. empathy_llm_toolkit/wizards/__init__.py +38 -0
  88. empathy_llm_toolkit/wizards/base_wizard.py +364 -0
  89. empathy_llm_toolkit/wizards/customer_support_wizard.py +190 -0
  90. empathy_llm_toolkit/wizards/healthcare_wizard.py +362 -0
  91. empathy_llm_toolkit/wizards/patient_assessment_README.md +64 -0
  92. empathy_llm_toolkit/wizards/patient_assessment_wizard.py +193 -0
  93. empathy_llm_toolkit/wizards/technology_wizard.py +194 -0
  94. empathy_os/__init__.py +125 -84
  95. empathy_os/adaptive/__init__.py +13 -0
  96. empathy_os/adaptive/task_complexity.py +127 -0
  97. empathy_os/{monitoring.py → agent_monitoring.py} +28 -28
  98. empathy_os/cache/__init__.py +117 -0
  99. empathy_os/cache/base.py +166 -0
  100. empathy_os/cache/dependency_manager.py +253 -0
  101. empathy_os/cache/hash_only.py +248 -0
  102. empathy_os/cache/hybrid.py +390 -0
  103. empathy_os/cache/storage.py +282 -0
  104. empathy_os/cli.py +1516 -70
  105. empathy_os/cli_unified.py +597 -0
  106. empathy_os/config/__init__.py +63 -0
  107. empathy_os/config/xml_config.py +239 -0
  108. empathy_os/config.py +95 -37
  109. empathy_os/coordination.py +72 -68
  110. empathy_os/core.py +94 -107
  111. empathy_os/cost_tracker.py +74 -55
  112. empathy_os/dashboard/__init__.py +15 -0
  113. empathy_os/dashboard/server.py +743 -0
  114. empathy_os/discovery.py +17 -14
  115. empathy_os/emergence.py +21 -22
  116. empathy_os/exceptions.py +18 -30
  117. empathy_os/feedback_loops.py +30 -33
  118. empathy_os/levels.py +32 -35
  119. empathy_os/leverage_points.py +31 -32
  120. empathy_os/logging_config.py +19 -16
  121. empathy_os/memory/__init__.py +195 -0
  122. empathy_os/memory/claude_memory.py +466 -0
  123. empathy_os/memory/config.py +224 -0
  124. empathy_os/memory/control_panel.py +1298 -0
  125. empathy_os/memory/edges.py +179 -0
  126. empathy_os/memory/graph.py +567 -0
  127. empathy_os/memory/long_term.py +1194 -0
  128. empathy_os/memory/nodes.py +179 -0
  129. empathy_os/memory/redis_bootstrap.py +540 -0
  130. empathy_os/memory/security/__init__.py +31 -0
  131. empathy_os/memory/security/audit_logger.py +930 -0
  132. empathy_os/memory/security/pii_scrubber.py +640 -0
  133. empathy_os/memory/security/secrets_detector.py +678 -0
  134. empathy_os/memory/short_term.py +2119 -0
  135. empathy_os/memory/storage/__init__.py +15 -0
  136. empathy_os/memory/summary_index.py +583 -0
  137. empathy_os/memory/unified.py +619 -0
  138. empathy_os/metrics/__init__.py +12 -0
  139. empathy_os/metrics/prompt_metrics.py +190 -0
  140. empathy_os/models/__init__.py +136 -0
  141. empathy_os/models/__main__.py +13 -0
  142. empathy_os/models/cli.py +655 -0
  143. empathy_os/models/empathy_executor.py +354 -0
  144. empathy_os/models/executor.py +252 -0
  145. empathy_os/models/fallback.py +671 -0
  146. empathy_os/models/provider_config.py +563 -0
  147. empathy_os/models/registry.py +382 -0
  148. empathy_os/models/tasks.py +302 -0
  149. empathy_os/models/telemetry.py +548 -0
  150. empathy_os/models/token_estimator.py +378 -0
  151. empathy_os/models/validation.py +274 -0
  152. empathy_os/monitoring/__init__.py +52 -0
  153. empathy_os/monitoring/alerts.py +23 -0
  154. empathy_os/monitoring/alerts_cli.py +268 -0
  155. empathy_os/monitoring/multi_backend.py +271 -0
  156. empathy_os/monitoring/otel_backend.py +363 -0
  157. empathy_os/optimization/__init__.py +19 -0
  158. empathy_os/optimization/context_optimizer.py +272 -0
  159. empathy_os/pattern_library.py +30 -29
  160. empathy_os/persistence.py +35 -37
  161. empathy_os/platform_utils.py +261 -0
  162. empathy_os/plugins/__init__.py +28 -0
  163. empathy_os/plugins/base.py +361 -0
  164. empathy_os/plugins/registry.py +268 -0
  165. empathy_os/project_index/__init__.py +30 -0
  166. empathy_os/project_index/cli.py +335 -0
  167. empathy_os/project_index/crew_integration.py +430 -0
  168. empathy_os/project_index/index.py +425 -0
  169. empathy_os/project_index/models.py +501 -0
  170. empathy_os/project_index/reports.py +473 -0
  171. empathy_os/project_index/scanner.py +538 -0
  172. empathy_os/prompts/__init__.py +61 -0
  173. empathy_os/prompts/config.py +77 -0
  174. empathy_os/prompts/context.py +177 -0
  175. empathy_os/prompts/parser.py +285 -0
  176. empathy_os/prompts/registry.py +313 -0
  177. empathy_os/prompts/templates.py +208 -0
  178. empathy_os/redis_config.py +144 -58
  179. empathy_os/redis_memory.py +79 -77
  180. empathy_os/resilience/__init__.py +56 -0
  181. empathy_os/resilience/circuit_breaker.py +256 -0
  182. empathy_os/resilience/fallback.py +179 -0
  183. empathy_os/resilience/health.py +300 -0
  184. empathy_os/resilience/retry.py +209 -0
  185. empathy_os/resilience/timeout.py +135 -0
  186. empathy_os/routing/__init__.py +43 -0
  187. empathy_os/routing/chain_executor.py +433 -0
  188. empathy_os/routing/classifier.py +217 -0
  189. empathy_os/routing/smart_router.py +234 -0
  190. empathy_os/routing/wizard_registry.py +307 -0
  191. empathy_os/templates.py +19 -14
  192. empathy_os/trust/__init__.py +28 -0
  193. empathy_os/trust/circuit_breaker.py +579 -0
  194. empathy_os/trust_building.py +67 -58
  195. empathy_os/validation/__init__.py +19 -0
  196. empathy_os/validation/xml_validator.py +281 -0
  197. empathy_os/wizard_factory_cli.py +170 -0
  198. empathy_os/{workflows.py → workflow_commands.py} +131 -37
  199. empathy_os/workflows/__init__.py +360 -0
  200. empathy_os/workflows/base.py +1660 -0
  201. empathy_os/workflows/bug_predict.py +962 -0
  202. empathy_os/workflows/code_review.py +960 -0
  203. empathy_os/workflows/code_review_adapters.py +310 -0
  204. empathy_os/workflows/code_review_pipeline.py +720 -0
  205. empathy_os/workflows/config.py +600 -0
  206. empathy_os/workflows/dependency_check.py +648 -0
  207. empathy_os/workflows/document_gen.py +1069 -0
  208. empathy_os/workflows/documentation_orchestrator.py +1205 -0
  209. empathy_os/workflows/health_check.py +679 -0
  210. empathy_os/workflows/keyboard_shortcuts/__init__.py +39 -0
  211. empathy_os/workflows/keyboard_shortcuts/generators.py +386 -0
  212. empathy_os/workflows/keyboard_shortcuts/parsers.py +414 -0
  213. empathy_os/workflows/keyboard_shortcuts/prompts.py +295 -0
  214. empathy_os/workflows/keyboard_shortcuts/schema.py +193 -0
  215. empathy_os/workflows/keyboard_shortcuts/workflow.py +505 -0
  216. empathy_os/workflows/manage_documentation.py +804 -0
  217. empathy_os/workflows/new_sample_workflow1.py +146 -0
  218. empathy_os/workflows/new_sample_workflow1_README.md +150 -0
  219. empathy_os/workflows/perf_audit.py +687 -0
  220. empathy_os/workflows/pr_review.py +748 -0
  221. empathy_os/workflows/progress.py +445 -0
  222. empathy_os/workflows/progress_server.py +322 -0
  223. empathy_os/workflows/refactor_plan.py +693 -0
  224. empathy_os/workflows/release_prep.py +808 -0
  225. empathy_os/workflows/research_synthesis.py +404 -0
  226. empathy_os/workflows/secure_release.py +585 -0
  227. empathy_os/workflows/security_adapters.py +297 -0
  228. empathy_os/workflows/security_audit.py +1046 -0
  229. empathy_os/workflows/step_config.py +234 -0
  230. empathy_os/workflows/test5.py +125 -0
  231. empathy_os/workflows/test5_README.md +158 -0
  232. empathy_os/workflows/test_gen.py +1855 -0
  233. empathy_os/workflows/test_lifecycle.py +526 -0
  234. empathy_os/workflows/test_maintenance.py +626 -0
  235. empathy_os/workflows/test_maintenance_cli.py +590 -0
  236. empathy_os/workflows/test_maintenance_crew.py +821 -0
  237. empathy_os/workflows/xml_enhanced_crew.py +285 -0
  238. empathy_software_plugin/__init__.py +1 -2
  239. empathy_software_plugin/cli/__init__.py +120 -0
  240. empathy_software_plugin/cli/inspect.py +362 -0
  241. empathy_software_plugin/cli.py +49 -27
  242. empathy_software_plugin/plugin.py +4 -8
  243. empathy_software_plugin/wizards/__init__.py +42 -0
  244. empathy_software_plugin/wizards/advanced_debugging_wizard.py +392 -0
  245. empathy_software_plugin/wizards/agent_orchestration_wizard.py +511 -0
  246. empathy_software_plugin/wizards/ai_collaboration_wizard.py +503 -0
  247. empathy_software_plugin/wizards/ai_context_wizard.py +441 -0
  248. empathy_software_plugin/wizards/ai_documentation_wizard.py +503 -0
  249. empathy_software_plugin/wizards/base_wizard.py +288 -0
  250. empathy_software_plugin/wizards/book_chapter_wizard.py +519 -0
  251. empathy_software_plugin/wizards/code_review_wizard.py +606 -0
  252. empathy_software_plugin/wizards/debugging/__init__.py +50 -0
  253. empathy_software_plugin/wizards/debugging/bug_risk_analyzer.py +414 -0
  254. empathy_software_plugin/wizards/debugging/config_loaders.py +442 -0
  255. empathy_software_plugin/wizards/debugging/fix_applier.py +469 -0
  256. empathy_software_plugin/wizards/debugging/language_patterns.py +383 -0
  257. empathy_software_plugin/wizards/debugging/linter_parsers.py +470 -0
  258. empathy_software_plugin/wizards/debugging/verification.py +369 -0
  259. empathy_software_plugin/wizards/enhanced_testing_wizard.py +537 -0
  260. empathy_software_plugin/wizards/memory_enhanced_debugging_wizard.py +816 -0
  261. empathy_software_plugin/wizards/multi_model_wizard.py +501 -0
  262. empathy_software_plugin/wizards/pattern_extraction_wizard.py +422 -0
  263. empathy_software_plugin/wizards/pattern_retriever_wizard.py +400 -0
  264. empathy_software_plugin/wizards/performance/__init__.py +9 -0
  265. empathy_software_plugin/wizards/performance/bottleneck_detector.py +221 -0
  266. empathy_software_plugin/wizards/performance/profiler_parsers.py +278 -0
  267. empathy_software_plugin/wizards/performance/trajectory_analyzer.py +429 -0
  268. empathy_software_plugin/wizards/performance_profiling_wizard.py +305 -0
  269. empathy_software_plugin/wizards/prompt_engineering_wizard.py +425 -0
  270. empathy_software_plugin/wizards/rag_pattern_wizard.py +461 -0
  271. empathy_software_plugin/wizards/security/__init__.py +32 -0
  272. empathy_software_plugin/wizards/security/exploit_analyzer.py +290 -0
  273. empathy_software_plugin/wizards/security/owasp_patterns.py +241 -0
  274. empathy_software_plugin/wizards/security/vulnerability_scanner.py +604 -0
  275. empathy_software_plugin/wizards/security_analysis_wizard.py +322 -0
  276. empathy_software_plugin/wizards/security_learning_wizard.py +740 -0
  277. empathy_software_plugin/wizards/tech_debt_wizard.py +726 -0
  278. empathy_software_plugin/wizards/testing/__init__.py +27 -0
  279. empathy_software_plugin/wizards/testing/coverage_analyzer.py +459 -0
  280. empathy_software_plugin/wizards/testing/quality_analyzer.py +531 -0
  281. empathy_software_plugin/wizards/testing/test_suggester.py +533 -0
  282. empathy_software_plugin/wizards/testing_wizard.py +274 -0
  283. hot_reload/README.md +473 -0
  284. hot_reload/__init__.py +62 -0
  285. hot_reload/config.py +84 -0
  286. hot_reload/integration.py +228 -0
  287. hot_reload/reloader.py +298 -0
  288. hot_reload/watcher.py +179 -0
  289. hot_reload/websocket.py +176 -0
  290. scaffolding/README.md +589 -0
  291. scaffolding/__init__.py +35 -0
  292. scaffolding/__main__.py +14 -0
  293. scaffolding/cli.py +240 -0
  294. test_generator/__init__.py +38 -0
  295. test_generator/__main__.py +14 -0
  296. test_generator/cli.py +226 -0
  297. test_generator/generator.py +325 -0
  298. test_generator/risk_analyzer.py +216 -0
  299. workflow_patterns/__init__.py +33 -0
  300. workflow_patterns/behavior.py +249 -0
  301. workflow_patterns/core.py +76 -0
  302. workflow_patterns/output.py +99 -0
  303. workflow_patterns/registry.py +255 -0
  304. workflow_patterns/structural.py +288 -0
  305. workflow_scaffolding/__init__.py +11 -0
  306. workflow_scaffolding/__main__.py +12 -0
  307. workflow_scaffolding/cli.py +206 -0
  308. workflow_scaffolding/generator.py +265 -0
  309. agents/code_inspection/patterns/inspection/recurring_B112.json +0 -18
  310. agents/code_inspection/patterns/inspection/recurring_F541.json +0 -16
  311. agents/code_inspection/patterns/inspection/recurring_FORMAT.json +0 -25
  312. agents/code_inspection/patterns/inspection/recurring_bug_20250822_def456.json +0 -16
  313. agents/code_inspection/patterns/inspection/recurring_bug_20250915_abc123.json +0 -16
  314. agents/code_inspection/patterns/inspection/recurring_bug_20251212_3c5b9951.json +0 -16
  315. agents/code_inspection/patterns/inspection/recurring_bug_20251212_97c0f72f.json +0 -16
  316. agents/code_inspection/patterns/inspection/recurring_bug_20251212_a0871d53.json +0 -16
  317. agents/code_inspection/patterns/inspection/recurring_bug_20251212_a9b6ec41.json +0 -16
  318. agents/code_inspection/patterns/inspection/recurring_bug_null_001.json +0 -16
  319. agents/code_inspection/patterns/inspection/recurring_builtin.json +0 -16
  320. agents/compliance_anticipation_agent.py +0 -1427
  321. agents/epic_integration_wizard.py +0 -541
  322. agents/trust_building_behaviors.py +0 -891
  323. empathy_framework-2.4.0.dist-info/METADATA +0 -485
  324. empathy_framework-2.4.0.dist-info/RECORD +0 -102
  325. empathy_framework-2.4.0.dist-info/entry_points.txt +0 -6
  326. empathy_llm_toolkit/htmlcov/status.json +0 -1
  327. empathy_llm_toolkit/security/htmlcov/status.json +0 -1
  328. {empathy_framework-2.4.0.dist-info → empathy_framework-3.8.2.dist-info}/WHEEL +0 -0
  329. {empathy_framework-2.4.0.dist-info → empathy_framework-3.8.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,960 @@
1
+ """Code Review Workflow
2
+
3
+ A tiered code analysis pipeline:
4
+ 1. Haiku: Classify change type (cheap, fast)
5
+ 2. Sonnet: Security scan + bug pattern matching
6
+ 3. Opus: Architectural review (conditional on complexity)
7
+
8
+ Copyright 2025 Smart-AI-Memory
9
+ Licensed under Fair Source License 0.9
10
+ """
11
+
12
+ from typing import Any
13
+
14
+ from .base import BaseWorkflow, ModelTier
15
+ from .step_config import WorkflowStepConfig
16
+
17
+ # Define step configurations for executor-based execution
18
+ CODE_REVIEW_STEPS = {
19
+ "architect_review": WorkflowStepConfig(
20
+ name="architect_review",
21
+ task_type="architectural_decision", # Premium tier task
22
+ tier_hint="premium",
23
+ description="Comprehensive architectural code review",
24
+ max_tokens=3000,
25
+ ),
26
+ }
27
+
28
+
29
+ class CodeReviewWorkflow(BaseWorkflow):
30
+ """Multi-tier code review workflow.
31
+
32
+ Uses cheap models for classification, capable models for security
33
+ and bug scanning, and premium models only for complex architectural
34
+ reviews (10+ files or core module changes).
35
+
36
+ Usage:
37
+ workflow = CodeReviewWorkflow()
38
+ result = await workflow.execute(
39
+ diff="...",
40
+ files_changed=["src/main.py", "tests/test_main.py"],
41
+ is_core_module=False
42
+ )
43
+ """
44
+
45
+ name = "code-review"
46
+ description = "Tiered code analysis with conditional premium review"
47
+ stages = ["classify", "scan", "architect_review"]
48
+ tier_map = {
49
+ "classify": ModelTier.CHEAP,
50
+ "scan": ModelTier.CAPABLE,
51
+ "architect_review": ModelTier.PREMIUM,
52
+ }
53
+
54
+ def __init__(
55
+ self,
56
+ file_threshold: int = 10,
57
+ core_modules: list[str] | None = None,
58
+ use_crew: bool = True,
59
+ crew_config: dict | None = None,
60
+ **kwargs: Any,
61
+ ):
62
+ """Initialize workflow.
63
+
64
+ Args:
65
+ file_threshold: Number of files above which premium review is used.
66
+ core_modules: List of module paths considered "core" (trigger premium).
67
+ use_crew: Enable CodeReviewCrew for comprehensive 5-agent analysis (default: True).
68
+ crew_config: Configuration dict for CodeReviewCrew.
69
+
70
+ """
71
+ super().__init__(**kwargs)
72
+ self.file_threshold = file_threshold
73
+ self.core_modules = core_modules or [
74
+ "src/core/",
75
+ "src/security/",
76
+ "src/auth/",
77
+ "empathy_os/core.py",
78
+ "empathy_os/security/",
79
+ ]
80
+ self.use_crew = use_crew
81
+ self.crew_config = crew_config or {}
82
+ self._needs_architect_review: bool = False
83
+ self._change_type: str = "unknown"
84
+ self._crew: Any = None
85
+ self._crew_available = False
86
+
87
+ # Dynamically configure stages based on crew setting
88
+ if use_crew:
89
+ self.stages = ["classify", "crew_review", "scan", "architect_review"]
90
+ self.tier_map = {
91
+ "classify": ModelTier.CHEAP,
92
+ "crew_review": ModelTier.CAPABLE, # Changed from PREMIUM to CAPABLE
93
+ "scan": ModelTier.CAPABLE,
94
+ "architect_review": ModelTier.PREMIUM,
95
+ }
96
+
97
+ async def _initialize_crew(self) -> None:
98
+ """Initialize the CodeReviewCrew."""
99
+ if self._crew is not None:
100
+ return
101
+
102
+ try:
103
+ import logging
104
+
105
+ from empathy_llm_toolkit.agent_factory.crews.code_review import CodeReviewCrew
106
+
107
+ self._crew = CodeReviewCrew()
108
+ self._crew_available = True
109
+ logging.getLogger(__name__).info("CodeReviewCrew initialized successfully")
110
+ except ImportError as e:
111
+ import logging
112
+
113
+ logging.getLogger(__name__).warning(f"CodeReviewCrew not available: {e}")
114
+ self._crew_available = False
115
+
116
+ def should_skip_stage(self, stage_name: str, input_data: Any) -> tuple[bool, str | None]:
117
+ """Skip stages when appropriate."""
118
+ # Skip all stages after classify if there was an input error
119
+ if isinstance(input_data, dict) and input_data.get("error"):
120
+ if stage_name != "classify":
121
+ return True, "Skipped due to input validation error"
122
+
123
+ # Skip crew review if crew is not available
124
+ if stage_name == "crew_review" and not self._crew_available:
125
+ return True, "CodeReviewCrew not available"
126
+
127
+ # Skip architectural review if change is simple
128
+ if stage_name == "architect_review" and not self._needs_architect_review:
129
+ return True, "Simple change - architectural review not needed"
130
+ return False, None
131
+
132
+ def _gather_project_context(self) -> str:
133
+ """Gather project context for project-level reviews.
134
+
135
+ Reads project metadata and key files to provide context to the LLM.
136
+ Returns formatted project context string, or empty string if no context found.
137
+ """
138
+ import os
139
+ from pathlib import Path
140
+
141
+ context_parts = []
142
+ cwd = Path.cwd()
143
+
144
+ # Get project name from directory or config files
145
+ project_name = cwd.name
146
+ context_parts.append(f"# Project: {project_name}")
147
+ context_parts.append(f"# Path: {cwd}")
148
+ context_parts.append("")
149
+
150
+ # Check for pyproject.toml
151
+ pyproject = cwd / "pyproject.toml"
152
+ if pyproject.exists():
153
+ try:
154
+ content = pyproject.read_text()[:2000]
155
+ context_parts.append("## pyproject.toml")
156
+ context_parts.append("```toml")
157
+ context_parts.append(content)
158
+ context_parts.append("```")
159
+ context_parts.append("")
160
+ except OSError:
161
+ pass
162
+
163
+ # Check for package.json
164
+ package_json = cwd / "package.json"
165
+ if package_json.exists():
166
+ try:
167
+ content = package_json.read_text()[:2000]
168
+ context_parts.append("## package.json")
169
+ context_parts.append("```json")
170
+ context_parts.append(content)
171
+ context_parts.append("```")
172
+ context_parts.append("")
173
+ except OSError:
174
+ pass
175
+
176
+ # Check for README
177
+ for readme_name in ["README.md", "README.rst", "README.txt", "README"]:
178
+ readme = cwd / readme_name
179
+ if readme.exists():
180
+ try:
181
+ content = readme.read_text()[:3000]
182
+ context_parts.append(f"## {readme_name}")
183
+ context_parts.append(content)
184
+ context_parts.append("")
185
+ break
186
+ except OSError:
187
+ pass
188
+
189
+ # Get directory structure (top 2 levels)
190
+ context_parts.append("## Project Structure")
191
+ context_parts.append("```")
192
+ try:
193
+ for root, dirs, files in os.walk(cwd):
194
+ # Skip hidden and common ignored directories
195
+ dirs[:] = [
196
+ d
197
+ for d in dirs
198
+ if not d.startswith(".")
199
+ and d
200
+ not in (
201
+ "node_modules",
202
+ "__pycache__",
203
+ "venv",
204
+ ".venv",
205
+ "dist",
206
+ "build",
207
+ ".git",
208
+ ".tox",
209
+ ".pytest_cache",
210
+ ".mypy_cache",
211
+ "htmlcov",
212
+ )
213
+ ]
214
+ level = root.replace(str(cwd), "").count(os.sep)
215
+ if level < 2:
216
+ indent = " " * level
217
+ folder_name = os.path.basename(root) or project_name
218
+ context_parts.append(f"{indent}{folder_name}/")
219
+ # Show key files at this level
220
+ key_files = [
221
+ f
222
+ for f in files
223
+ if f.endswith(
224
+ (".py", ".ts", ".js", ".json", ".yaml", ".yml", ".toml", ".md"),
225
+ )
226
+ and not f.startswith(".")
227
+ ][:10]
228
+ for f in key_files:
229
+ context_parts.append(f"{indent} {f}")
230
+ if level >= 2:
231
+ break
232
+ except OSError:
233
+ context_parts.append("(Unable to read directory structure)")
234
+ context_parts.append("```")
235
+
236
+ # Return empty if we only have the header
237
+ if len(context_parts) <= 3:
238
+ return ""
239
+
240
+ return "\n".join(context_parts)
241
+
242
+ async def run_stage(
243
+ self,
244
+ stage_name: str,
245
+ tier: ModelTier,
246
+ input_data: Any,
247
+ ) -> tuple[Any, int, int]:
248
+ """Execute a code review stage."""
249
+ if stage_name == "classify":
250
+ return await self._classify(input_data, tier)
251
+ if stage_name == "crew_review":
252
+ return await self._crew_review(input_data, tier)
253
+ if stage_name == "scan":
254
+ return await self._scan(input_data, tier)
255
+ if stage_name == "architect_review":
256
+ return await self._architect_review(input_data, tier)
257
+ raise ValueError(f"Unknown stage: {stage_name}")
258
+
259
+ async def _classify(self, input_data: dict, tier: ModelTier) -> tuple[dict, int, int]:
260
+ """Classify the type of change."""
261
+ diff = input_data.get("diff", "")
262
+ target = input_data.get("target", "")
263
+ files_changed = input_data.get("files_changed", [])
264
+
265
+ # If target provided instead of diff, use it as the code to review
266
+ code_to_review = diff or target
267
+
268
+ # Handle project-level review when target is "." or empty
269
+ if not code_to_review or code_to_review.strip() in (".", "", "./"):
270
+ # Gather project context for project-level review
271
+ project_context = self._gather_project_context()
272
+ if not project_context:
273
+ # Return early with helpful error message if no context found
274
+ return (
275
+ {
276
+ "classification": "ERROR: No code provided for review",
277
+ "error": True,
278
+ "error_message": (
279
+ "No code was provided for review. Please ensure you:\n"
280
+ "1. Have a file open in the editor, OR\n"
281
+ "2. Select a specific file to review, OR\n"
282
+ '3. Provide code content directly via --input \'{"diff": "..."}\'\n\n'
283
+ "Tip: Use 'Select File...' option in the workflow picker."
284
+ ),
285
+ "change_type": "none",
286
+ "files_changed": [],
287
+ "file_count": 0,
288
+ "needs_architect_review": False,
289
+ "is_core_module": False,
290
+ "code_to_review": "",
291
+ },
292
+ 0,
293
+ 0,
294
+ )
295
+ code_to_review = project_context
296
+ # Mark as project-level review
297
+ input_data["is_project_review"] = True
298
+
299
+ system = """You are a code review classifier. Analyze the code and classify:
300
+ 1. Change type: bug_fix, feature, refactor, docs, test, config, or security
301
+ 2. Complexity: low, medium, high
302
+ 3. Risk level: low, medium, high
303
+
304
+ Respond with a brief classification summary."""
305
+
306
+ user_message = f"""Classify this code change:
307
+
308
+ Files: {", ".join(files_changed) if files_changed else "Not specified"}
309
+
310
+ Code:
311
+ {code_to_review[:4000]}"""
312
+
313
+ response, input_tokens, output_tokens = await self._call_llm(
314
+ tier,
315
+ system,
316
+ user_message,
317
+ max_tokens=500,
318
+ )
319
+
320
+ # Parse response to determine if architect review needed
321
+ is_high_complexity = "high" in response.lower() and (
322
+ "complexity" in response.lower() or "risk" in response.lower()
323
+ )
324
+ is_core = (
325
+ any(any(core in f for core in self.core_modules) for f in files_changed)
326
+ if files_changed
327
+ else False
328
+ )
329
+
330
+ self._needs_architect_review = (
331
+ len(files_changed) >= self.file_threshold
332
+ or is_core
333
+ or is_high_complexity
334
+ or input_data.get("is_core_module", False)
335
+ )
336
+
337
+ return (
338
+ {
339
+ "classification": response,
340
+ "change_type": "feature", # Will be refined by LLM
341
+ "files_changed": files_changed,
342
+ "file_count": len(files_changed),
343
+ "needs_architect_review": self._needs_architect_review,
344
+ "is_core_module": is_core,
345
+ "code_to_review": code_to_review,
346
+ },
347
+ input_tokens,
348
+ output_tokens,
349
+ )
350
+
351
+ async def _crew_review(self, input_data: dict, tier: ModelTier) -> tuple[dict, int, int]:
352
+ """Run CodeReviewCrew for comprehensive 5-agent analysis.
353
+
354
+ This stage uses the CodeReviewCrew (Review Lead, Security Analyst,
355
+ Architecture Reviewer, Quality Analyst, Performance Reviewer) for
356
+ deep code analysis with memory graph integration.
357
+
358
+ Falls back gracefully if CodeReviewCrew is not available.
359
+ """
360
+ await self._initialize_crew()
361
+
362
+ from .code_review_adapters import (
363
+ _check_crew_available,
364
+ _get_crew_review,
365
+ crew_report_to_workflow_format,
366
+ )
367
+
368
+ # Get code to review
369
+ diff = input_data.get("diff", "") or input_data.get("code_to_review", "")
370
+ files_changed = input_data.get("files_changed", [])
371
+
372
+ # Check if crew is available
373
+ if not self._crew_available or not _check_crew_available():
374
+ return (
375
+ {
376
+ "crew_review": {
377
+ "available": False,
378
+ "fallback": True,
379
+ "reason": "CodeReviewCrew not installed or failed to initialize",
380
+ },
381
+ **input_data,
382
+ },
383
+ 0,
384
+ 0,
385
+ )
386
+
387
+ # Run CodeReviewCrew
388
+ report = await _get_crew_review(
389
+ diff=diff,
390
+ files_changed=files_changed,
391
+ config=self.crew_config,
392
+ )
393
+
394
+ if report is None:
395
+ return (
396
+ {
397
+ "crew_review": {
398
+ "available": True,
399
+ "fallback": True,
400
+ "reason": "CodeReviewCrew review failed or timed out",
401
+ },
402
+ **input_data,
403
+ },
404
+ 0,
405
+ 0,
406
+ )
407
+
408
+ # Convert crew report to workflow format
409
+ crew_results = crew_report_to_workflow_format(report)
410
+
411
+ # Update needs_architect_review based on crew findings
412
+ has_blocking = crew_results.get("has_blocking_issues", False)
413
+ critical_count = len(crew_results.get("assessment", {}).get("critical_findings", []))
414
+ high_count = len(crew_results.get("assessment", {}).get("high_findings", []))
415
+
416
+ if has_blocking or critical_count > 0 or high_count > 2:
417
+ self._needs_architect_review = True
418
+
419
+ crew_review_result = {
420
+ "available": True,
421
+ "fallback": False,
422
+ "findings": crew_results.get("findings", []),
423
+ "finding_count": crew_results.get("finding_count", 0),
424
+ "verdict": crew_results.get("verdict", "approve"),
425
+ "quality_score": crew_results.get("quality_score", 100),
426
+ "has_blocking_issues": has_blocking,
427
+ "critical_count": critical_count,
428
+ "high_count": high_count,
429
+ "summary": crew_results.get("summary", ""),
430
+ "agents_used": crew_results.get("agents_used", []),
431
+ "memory_graph_hits": crew_results.get("memory_graph_hits", 0),
432
+ "review_duration_seconds": crew_results.get("review_duration_seconds", 0),
433
+ }
434
+
435
+ # Estimate tokens (crew uses internal LLM calls)
436
+ input_tokens = len(diff) // 4
437
+ output_tokens = len(str(crew_review_result)) // 4
438
+
439
+ return (
440
+ {
441
+ "crew_review": crew_review_result,
442
+ "needs_architect_review": self._needs_architect_review,
443
+ **input_data,
444
+ },
445
+ input_tokens,
446
+ output_tokens,
447
+ )
448
+
449
+ async def _scan(self, input_data: dict, tier: ModelTier) -> tuple[dict, int, int]:
450
+ """Security scan and bug pattern matching.
451
+
452
+ When external_audit_results is provided in input_data (e.g., from
453
+ SecurityAuditCrew), these findings are merged with the LLM analysis
454
+ and can trigger architect_review if critical issues are found.
455
+ """
456
+ code_to_review = input_data.get("code_to_review", input_data.get("diff", ""))
457
+ classification = input_data.get("classification", "")
458
+ files_changed = input_data.get("files_changed", input_data.get("files", []))
459
+
460
+ # Check for external audit results (e.g., from SecurityAuditCrew)
461
+ external_audit = input_data.get("external_audit_results")
462
+
463
+ system = """You are a security and code quality expert. Analyze the code for:
464
+
465
+ 1. SECURITY ISSUES (OWASP Top 10):
466
+ - SQL Injection, XSS, Command Injection
467
+ - Hardcoded secrets, API keys, passwords
468
+ - Insecure deserialization
469
+ - Authentication/authorization flaws
470
+
471
+ 2. BUG PATTERNS:
472
+ - Null/undefined references
473
+ - Resource leaks
474
+ - Race conditions
475
+ - Error handling issues
476
+
477
+ 3. CODE QUALITY:
478
+ - Code smells
479
+ - Maintainability issues
480
+ - Performance concerns
481
+
482
+ For each issue found, provide:
483
+ - Severity (critical/high/medium/low)
484
+ - Location (if identifiable)
485
+ - Description
486
+ - Recommendation
487
+
488
+ Be thorough but focused on actionable findings."""
489
+
490
+ # If external audit provided, include it in the prompt for context
491
+ external_context = ""
492
+ if external_audit:
493
+ external_summary = external_audit.get("summary", "")
494
+ external_findings = external_audit.get("findings", [])
495
+ if external_summary or external_findings:
496
+ # Build findings list efficiently (avoid O(n²) string concat)
497
+ finding_lines = []
498
+ for finding in external_findings[:10]: # Top 10
499
+ sev = finding.get("severity", "unknown").upper()
500
+ title = finding.get("title", "N/A")
501
+ desc = finding.get("description", "")[:100]
502
+ finding_lines.append(f"- [{sev}] {title}: {desc}")
503
+
504
+ external_context = f"""
505
+
506
+ ## External Security Audit Results
507
+ Summary: {external_summary}
508
+
509
+ Findings ({len(external_findings)} total):
510
+ {chr(10).join(finding_lines)}
511
+
512
+ Verify these findings and identify additional issues."""
513
+
514
+ user_message = f"""Review this code for security and quality issues:
515
+
516
+ Previous classification: {classification}
517
+ {external_context}
518
+ Code to review:
519
+ {code_to_review[:6000]}"""
520
+
521
+ response, input_tokens, output_tokens = await self._call_llm(
522
+ tier,
523
+ system,
524
+ user_message,
525
+ max_tokens=2048,
526
+ )
527
+
528
+ # Extract structured findings from LLM response
529
+ llm_findings = self._extract_findings_from_response(
530
+ response=response,
531
+ files_changed=files_changed or [],
532
+ code_context=code_to_review[:1000], # First 1000 chars for context
533
+ )
534
+
535
+ # Check if critical issues found in LLM response
536
+ has_critical = "critical" in response.lower() or "high" in response.lower()
537
+
538
+ # Merge external audit findings if provided
539
+ security_findings: list[dict] = []
540
+ external_has_critical = False
541
+
542
+ if external_audit:
543
+ merged_response, security_findings, external_has_critical = self._merge_external_audit(
544
+ response,
545
+ external_audit,
546
+ )
547
+ response = merged_response
548
+ has_critical = has_critical or external_has_critical
549
+
550
+ # Combine LLM findings with security findings
551
+ all_findings = llm_findings + security_findings
552
+
553
+ # Calculate summary statistics
554
+ summary: dict[str, Any] = {
555
+ "total_findings": len(all_findings),
556
+ "by_severity": {},
557
+ "by_category": {},
558
+ "files_affected": list({f.get("file", "") for f in all_findings if f.get("file")}),
559
+ }
560
+
561
+ # Count by severity
562
+ for finding in all_findings:
563
+ sev = finding.get("severity", "info")
564
+ summary["by_severity"][sev] = summary["by_severity"].get(sev, 0) + 1
565
+
566
+ # Count by category
567
+ for finding in all_findings:
568
+ cat = finding.get("category", "other")
569
+ summary["by_category"][cat] = summary["by_category"].get(cat, 0) + 1
570
+
571
+ # Add helpful message if no findings
572
+ if len(all_findings) == 0:
573
+ summary["message"] = (
574
+ "No security or quality issues found in scan. "
575
+ "Code will proceed to architectural review."
576
+ )
577
+
578
+ return (
579
+ {
580
+ "scan_results": response,
581
+ "findings": all_findings, # NEW: structured findings for UI
582
+ "summary": summary, # NEW: summary statistics
583
+ "security_findings": security_findings, # Keep for backward compat
584
+ "bug_patterns": [],
585
+ "quality_issues": [],
586
+ "has_critical_issues": has_critical,
587
+ "security_score": 70 if has_critical else 90,
588
+ "needs_architect_review": input_data.get("needs_architect_review", False)
589
+ or has_critical,
590
+ "code_to_review": code_to_review,
591
+ "classification": classification,
592
+ "external_audit_included": external_audit is not None,
593
+ "external_audit_risk_score": (
594
+ external_audit.get("risk_score", 0) if external_audit else 0
595
+ ),
596
+ },
597
+ input_tokens,
598
+ output_tokens,
599
+ )
600
+
601
+ def _merge_external_audit(
602
+ self,
603
+ llm_response: str,
604
+ external_audit: dict,
605
+ ) -> tuple[str, list, bool]:
606
+ """Merge external SecurityAuditCrew results into scan output.
607
+
608
+ Args:
609
+ llm_response: Response from LLM security scan
610
+ external_audit: External audit dict (from SecurityAuditCrew.to_dict())
611
+
612
+ Returns:
613
+ Tuple of (merged_response, security_findings, has_critical)
614
+
615
+ """
616
+ findings = external_audit.get("findings", [])
617
+ summary = external_audit.get("summary", "")
618
+ risk_score = external_audit.get("risk_score", 0)
619
+
620
+ # Check for critical/high findings
621
+ has_critical = any(f.get("severity") in ("critical", "high") for f in findings)
622
+
623
+ # Build merged response
624
+ merged_sections = [llm_response]
625
+
626
+ if summary or findings:
627
+ # Build crew section efficiently (avoid O(n²) string concat)
628
+ parts = ["\n\n## SecurityAuditCrew Analysis\n"]
629
+ if summary:
630
+ parts.append(f"\n{summary}\n")
631
+
632
+ parts.append(f"\n**Risk Score**: {risk_score}/100\n")
633
+
634
+ if findings:
635
+ critical = [f for f in findings if f.get("severity") == "critical"]
636
+ high = [f for f in findings if f.get("severity") == "high"]
637
+
638
+ if critical:
639
+ parts.append("\n### Critical Findings\n")
640
+ for f in critical:
641
+ title = f"- **{f.get('title', 'N/A')}**"
642
+ if f.get("file"):
643
+ title += f" ({f.get('file')}:{f.get('line', '?')})"
644
+ parts.append(title)
645
+ parts.append(f"\n {f.get('description', '')[:200]}\n")
646
+ if f.get("remediation"):
647
+ parts.append(f" *Fix*: {f.get('remediation')[:150]}\n")
648
+
649
+ if high:
650
+ parts.append("\n### High Severity Findings\n")
651
+ for f in high[:5]: # Top 5
652
+ title = f"- **{f.get('title', 'N/A')}**"
653
+ if f.get("file"):
654
+ title += f" ({f.get('file')}:{f.get('line', '?')})"
655
+ parts.append(title)
656
+ parts.append(f"\n {f.get('description', '')[:150]}\n")
657
+
658
+ merged_sections.append("".join(parts))
659
+
660
+ return "\n".join(merged_sections), findings, has_critical
661
+
662
+ async def _architect_review(self, input_data: dict, tier: ModelTier) -> tuple[dict, int, int]:
663
+ """Deep architectural review.
664
+
665
+ Supports XML-enhanced prompts when enabled in workflow config.
666
+ """
667
+ code_to_review = input_data.get("code_to_review", "")
668
+ scan_results = input_data.get("scan_results", "")
669
+ classification = input_data.get("classification", "")
670
+
671
+ # Build input payload
672
+ input_payload = f"""Classification: {classification}
673
+
674
+ Security Scan Results:
675
+ {scan_results[:2000]}
676
+
677
+ Code:
678
+ {code_to_review[:4000]}"""
679
+
680
+ # Check if XML prompts are enabled
681
+ if self._is_xml_enabled():
682
+ user_message = self._render_xml_prompt(
683
+ role="senior software architect",
684
+ goal="Perform comprehensive code review with architectural assessment",
685
+ instructions=[
686
+ "Assess design patterns used (or missing)",
687
+ "Evaluate SOLID principles compliance",
688
+ "Check separation of concerns",
689
+ "Analyze coupling and cohesion",
690
+ "Provide specific improvement recommendations with examples",
691
+ "Suggest refactoring and testing improvements",
692
+ "Provide verdict: approve, approve_with_suggestions, or reject",
693
+ ],
694
+ constraints=[
695
+ "Be specific and actionable",
696
+ "Reference file locations where possible",
697
+ "Prioritize issues by impact",
698
+ ],
699
+ input_type="code",
700
+ input_payload=input_payload,
701
+ )
702
+ system = None
703
+ else:
704
+ system = """You are a senior software architect. Provide a comprehensive review:
705
+
706
+ 1. ARCHITECTURAL ASSESSMENT:
707
+ - Design patterns used (or missing)
708
+ - SOLID principles compliance
709
+ - Separation of concerns
710
+ - Coupling and cohesion
711
+
712
+ 2. RECOMMENDATIONS:
713
+ - Specific improvements with examples
714
+ - Refactoring suggestions
715
+ - Testing recommendations
716
+
717
+ 3. VERDICT:
718
+ - APPROVE: Code is production-ready
719
+ - APPROVE_WITH_SUGGESTIONS: Minor improvements recommended
720
+ - REQUEST_CHANGES: Issues must be addressed
721
+ - REJECT: Fundamental problems
722
+
723
+ Provide actionable, specific feedback."""
724
+
725
+ user_message = f"""Perform an architectural review:
726
+
727
+ {input_payload}"""
728
+
729
+ # Try executor-based execution first (Phase 3 pattern)
730
+ if self._executor is not None or self._api_key:
731
+ try:
732
+ step = CODE_REVIEW_STEPS["architect_review"]
733
+ response, input_tokens, output_tokens, cost = await self.run_step_with_executor(
734
+ step=step,
735
+ prompt=user_message,
736
+ system=system,
737
+ )
738
+ except Exception:
739
+ # Fall back to legacy _call_llm if executor fails
740
+ response, input_tokens, output_tokens = await self._call_llm(
741
+ tier,
742
+ system or "",
743
+ user_message,
744
+ max_tokens=3000,
745
+ )
746
+ else:
747
+ # Legacy path for backward compatibility
748
+ response, input_tokens, output_tokens = await self._call_llm(
749
+ tier,
750
+ system or "",
751
+ user_message,
752
+ max_tokens=3000,
753
+ )
754
+
755
+ # Parse XML response if enforcement is enabled
756
+ parsed_data = self._parse_xml_response(response)
757
+
758
+ # Determine verdict from response or parsed data
759
+ verdict = "approve_with_suggestions"
760
+ if parsed_data.get("xml_parsed"):
761
+ extra = parsed_data.get("_parsed_response")
762
+ if extra and hasattr(extra, "extra"):
763
+ parsed_verdict = extra.extra.get("verdict", "").lower()
764
+ if parsed_verdict in [
765
+ "approve",
766
+ "approve_with_suggestions",
767
+ "request_changes",
768
+ "reject",
769
+ ]:
770
+ verdict = parsed_verdict
771
+
772
+ if verdict == "approve_with_suggestions":
773
+ # Fall back to text parsing
774
+ if "REQUEST_CHANGES" in response.upper() or "REJECT" in response.upper():
775
+ verdict = "request_changes"
776
+ elif "APPROVE" in response.upper() and "SUGGESTIONS" not in response.upper():
777
+ verdict = "approve"
778
+
779
+ result: dict = {
780
+ "architectural_review": response,
781
+ "verdict": verdict,
782
+ "recommendations": [],
783
+ "model_tier_used": tier.value,
784
+ }
785
+
786
+ # Merge parsed XML data if available
787
+ if parsed_data.get("xml_parsed"):
788
+ result.update(
789
+ {
790
+ "xml_parsed": True,
791
+ "summary": parsed_data.get("summary"),
792
+ "findings": parsed_data.get("findings", []),
793
+ "checklist": parsed_data.get("checklist", []),
794
+ },
795
+ )
796
+
797
+ # Add formatted report for human readability
798
+ formatted_report = format_code_review_report(result, input_data)
799
+ result["formatted_report"] = formatted_report
800
+
801
+ # Also add as top-level display_output for better UX
802
+ result["display_output"] = formatted_report
803
+
804
+ return (result, input_tokens, output_tokens)
805
+
806
+
807
+ def format_code_review_report(result: dict, input_data: dict) -> str:
808
+ """Format code review output as a human-readable report.
809
+
810
+ Args:
811
+ result: The architect_review stage result
812
+ input_data: Input data from previous stages
813
+
814
+ Returns:
815
+ Formatted report string
816
+
817
+ """
818
+ lines = []
819
+
820
+ # Check for input validation error
821
+ if input_data.get("error"):
822
+ lines.append("=" * 60)
823
+ lines.append("CODE REVIEW - INPUT ERROR")
824
+ lines.append("=" * 60)
825
+ lines.append("")
826
+ lines.append(input_data.get("error_message", "No code provided for review."))
827
+ lines.append("")
828
+ lines.append("=" * 60)
829
+ return "\n".join(lines)
830
+
831
+ # Header
832
+ verdict = result.get("verdict", "unknown").upper().replace("_", " ")
833
+ verdict_icon = {
834
+ "APPROVE": "✅",
835
+ "APPROVE WITH SUGGESTIONS": "🔶",
836
+ "REQUEST CHANGES": "⚠️",
837
+ "REJECT": "❌",
838
+ }.get(verdict, "❓")
839
+
840
+ lines.append("=" * 60)
841
+ lines.append("CODE REVIEW REPORT")
842
+ lines.append("=" * 60)
843
+ lines.append("")
844
+ lines.append(f"Verdict: {verdict_icon} {verdict}")
845
+ lines.append("")
846
+
847
+ # Classification summary
848
+ classification = input_data.get("classification", "")
849
+ if classification:
850
+ lines.append("-" * 60)
851
+ lines.append("CLASSIFICATION")
852
+ lines.append("-" * 60)
853
+ lines.append(classification[:500])
854
+ lines.append("")
855
+
856
+ # Security scan results
857
+ has_critical = input_data.get("has_critical_issues", False)
858
+ security_score = input_data.get("security_score", 100)
859
+ security_icon = "🔴" if has_critical else ("🟡" if security_score < 90 else "🟢")
860
+
861
+ lines.append("-" * 60)
862
+ lines.append("SECURITY ANALYSIS")
863
+ lines.append("-" * 60)
864
+ lines.append(f"Security Score: {security_icon} {security_score}/100")
865
+ lines.append(f"Critical Issues: {'Yes' if has_critical else 'No'}")
866
+ lines.append("")
867
+
868
+ # Security findings
869
+ security_findings = input_data.get("security_findings", [])
870
+ if security_findings:
871
+ lines.append("Security Findings:")
872
+ for finding in security_findings[:10]:
873
+ severity = finding.get("severity", "unknown").upper()
874
+ title = finding.get("title", "N/A")
875
+ sev_icon = {"CRITICAL": "🔴", "HIGH": "🟠", "MEDIUM": "🟡", "LOW": "🟢"}.get(
876
+ severity,
877
+ "⚪",
878
+ )
879
+ lines.append(f" {sev_icon} [{severity}] {title}")
880
+ lines.append("")
881
+
882
+ # Scan results summary
883
+ scan_results = input_data.get("scan_results", "")
884
+ if scan_results:
885
+ lines.append("Scan Summary:")
886
+ # Truncate scan results for readability
887
+ summary = scan_results[:800]
888
+ if len(scan_results) > 800:
889
+ summary += "..."
890
+ lines.append(summary)
891
+ lines.append("")
892
+
893
+ # Architectural review
894
+ arch_review = result.get("architectural_review", "")
895
+ if arch_review:
896
+ lines.append("-" * 60)
897
+ lines.append("ARCHITECTURAL REVIEW")
898
+ lines.append("-" * 60)
899
+ lines.append(arch_review)
900
+ lines.append("")
901
+
902
+ # Recommendations
903
+ recommendations = result.get("recommendations", [])
904
+ if recommendations:
905
+ lines.append("-" * 60)
906
+ lines.append("RECOMMENDATIONS")
907
+ lines.append("-" * 60)
908
+ for i, rec in enumerate(recommendations, 1):
909
+ lines.append(f"{i}. {rec}")
910
+ lines.append("")
911
+
912
+ # Crew review results (if available)
913
+ crew_review = input_data.get("crew_review", {})
914
+ if crew_review and crew_review.get("available") and not crew_review.get("fallback"):
915
+ lines.append("-" * 60)
916
+ lines.append("CREW REVIEW ANALYSIS")
917
+ lines.append("-" * 60)
918
+ lines.append(f"Quality Score: {crew_review.get('quality_score', 'N/A')}/100")
919
+ lines.append(f"Finding Count: {crew_review.get('finding_count', 0)}")
920
+ agents = crew_review.get("agents_used", [])
921
+ if agents:
922
+ lines.append(f"Agents Used: {', '.join(agents)}")
923
+ summary = crew_review.get("summary", "")
924
+ if summary:
925
+ lines.append(f"Summary: {summary[:300]}")
926
+ lines.append("")
927
+
928
+ # Check if we have any meaningful content to show
929
+ content_sections = [
930
+ input_data.get("classification"),
931
+ input_data.get("security_findings"),
932
+ input_data.get("scan_results"),
933
+ result.get("architectural_review"),
934
+ result.get("recommendations"),
935
+ ]
936
+ has_content = any(content_sections)
937
+
938
+ # If no content was generated, add a helpful message
939
+ if not has_content and len(lines) < 15: # Just header/footer, no real content
940
+ lines.append("-" * 60)
941
+ lines.append("NO ISSUES FOUND")
942
+ lines.append("-" * 60)
943
+ lines.append("")
944
+ lines.append("The code review workflow completed but found no issues to report.")
945
+ lines.append("This could mean:")
946
+ lines.append(" • No code was provided for review (check input parameters)")
947
+ lines.append(" • The code is clean and follows best practices")
948
+ lines.append(" • The workflow needs configuration (check .empathy/workflows.yaml)")
949
+ lines.append("")
950
+ lines.append("Tip: Try running with a specific file or diff:")
951
+ lines.append(' empathy workflow run code-review --input \'{"target": "path/to/file.py"}\'')
952
+ lines.append("")
953
+
954
+ # Footer
955
+ lines.append("=" * 60)
956
+ model_tier = result.get("model_tier_used", "unknown")
957
+ lines.append(f"Review completed using {model_tier} tier model")
958
+ lines.append("=" * 60)
959
+
960
+ return "\n".join(lines)