patchi 0.7.5__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 (496) hide show
  1. patchi/__init__.py +1 -0
  2. patchi/__main__.py +13 -0
  3. patchi/cli/__init__.py +0 -0
  4. patchi/cli/command_families.py +235 -0
  5. patchi/cli/commands/__init__.py +0 -0
  6. patchi/cli/commands/access_cmd.py +65 -0
  7. patchi/cli/commands/agent_stats_cmd.py +273 -0
  8. patchi/cli/commands/agents_cmd.py +187 -0
  9. patchi/cli/commands/ai_cmd.py +308 -0
  10. patchi/cli/commands/assure_cmd.py +538 -0
  11. patchi/cli/commands/audit_cmd.py +286 -0
  12. patchi/cli/commands/auto_cmd.py +110 -0
  13. patchi/cli/commands/blame_cmd.py +53 -0
  14. patchi/cli/commands/chains_cmd.py +327 -0
  15. patchi/cli/commands/charter_cmd.py +173 -0
  16. patchi/cli/commands/chat_cmd.py +597 -0
  17. patchi/cli/commands/check_cmd.py +232 -0
  18. patchi/cli/commands/cleanup_cmd.py +328 -0
  19. patchi/cli/commands/cockpit_cmd.py +208 -0
  20. patchi/cli/commands/commands_cmd.py +160 -0
  21. patchi/cli/commands/cross_repo_cmd.py +113 -0
  22. patchi/cli/commands/deps_cmd.py +110 -0
  23. patchi/cli/commands/dev_check_cmd.py +342 -0
  24. patchi/cli/commands/dev_cmd.py +445 -0
  25. patchi/cli/commands/doctor_cmd.py +457 -0
  26. patchi/cli/commands/findings_cmd.py +187 -0
  27. patchi/cli/commands/fix_cmd.py +648 -0
  28. patchi/cli/commands/fix_review_cmd.py +273 -0
  29. patchi/cli/commands/heatmap_cmd.py +62 -0
  30. patchi/cli/commands/help_cmd.py +159 -0
  31. patchi/cli/commands/hosted_cmd.py +980 -0
  32. patchi/cli/commands/init.py +456 -0
  33. patchi/cli/commands/key_cmd.py +571 -0
  34. patchi/cli/commands/learn_cmd.py +140 -0
  35. patchi/cli/commands/link_cmd.py +104 -0
  36. patchi/cli/commands/log_cmd.py +57 -0
  37. patchi/cli/commands/memory_cmd.py +215 -0
  38. patchi/cli/commands/model_cmd.py +207 -0
  39. patchi/cli/commands/notify_cmd.py +213 -0
  40. patchi/cli/commands/notify_prompts.py +29 -0
  41. patchi/cli/commands/patch_cmd.py +231 -0
  42. patchi/cli/commands/plan_cmd.py +128 -0
  43. patchi/cli/commands/plugins_cmd.py +307 -0
  44. patchi/cli/commands/queue_cmd.py +152 -0
  45. patchi/cli/commands/quick_cmd.py +159 -0
  46. patchi/cli/commands/ready_cmd.py +292 -0
  47. patchi/cli/commands/report_cmd.py +416 -0
  48. patchi/cli/commands/restrict_cmd.py +127 -0
  49. patchi/cli/commands/review_cmd.py +225 -0
  50. patchi/cli/commands/rules_cmd.py +233 -0
  51. patchi/cli/commands/scan_cmd.py +1908 -0
  52. patchi/cli/commands/settings_cmd.py +45 -0
  53. patchi/cli/commands/status_cmd.py +679 -0
  54. patchi/cli/commands/test_cmd.py +1052 -0
  55. patchi/cli/commands/trend_cmd.py +296 -0
  56. patchi/cli/commands/undo_cmd.py +235 -0
  57. patchi/cli/commands/update_cmd.py +345 -0
  58. patchi/cli/commands/verify_cmd.py +56 -0
  59. patchi/cli/commands/vr_cmd.py +440 -0
  60. patchi/cli/commands/watch_cmd.py +304 -0
  61. patchi/cli/commands/web_cmd.py +114 -0
  62. patchi/cli/console.py +101 -0
  63. patchi/cli/display/__init__.py +1 -0
  64. patchi/cli/display/live_audit.py +269 -0
  65. patchi/cli/display/live_progress.py +169 -0
  66. patchi/cli/framework.py +241 -0
  67. patchi/cli/logo.py +128 -0
  68. patchi/cli/main.py +222 -0
  69. patchi/cli/registry.py +1171 -0
  70. patchi/cli/style.py +68 -0
  71. patchi/cli/themes.py +187 -0
  72. patchi/cli/ux.py +444 -0
  73. patchi/core/__init__.py +0 -0
  74. patchi/core/agents/__init__.py +0 -0
  75. patchi/core/agents/api_fuzzer_agent.py +141 -0
  76. patchi/core/agents/attack_agent.py +242 -0
  77. patchi/core/agents/base.py +1012 -0
  78. patchi/core/agents/bug_predictor_agent.py +58 -0
  79. patchi/core/agents/build_tool_validator.py +150 -0
  80. patchi/core/agents/cache.py +192 -0
  81. patchi/core/agents/chaos_agent.py +133 -0
  82. patchi/core/agents/cicd_generator.py +240 -0
  83. patchi/core/agents/comment_scanner.py +280 -0
  84. patchi/core/agents/console_logging_agent.py +156 -0
  85. patchi/core/agents/coordinator.py +858 -0
  86. patchi/core/agents/core_scanner.py +269 -0
  87. patchi/core/agents/coverage_prioritizer.py +181 -0
  88. patchi/core/agents/dead_code_hygiene.py +296 -0
  89. patchi/core/agents/dead_code_scanner.py +692 -0
  90. patchi/core/agents/dependency_scanner.py +725 -0
  91. patchi/core/agents/doc_claim_agent.py +573 -0
  92. patchi/core/agents/duplicate_scanner.py +568 -0
  93. patchi/core/agents/env_scanner.py +410 -0
  94. patchi/core/agents/feature_flag_agent.py +89 -0
  95. patchi/core/agents/frontend_framework_agent.py +133 -0
  96. patchi/core/agents/goroutine_agent.py +82 -0
  97. patchi/core/agents/governor.py +1972 -0
  98. patchi/core/agents/i18n_agent.py +95 -0
  99. patchi/core/agents/license_compliance.py +247 -0
  100. patchi/core/agents/linking_agent.py +251 -0
  101. patchi/core/agents/memory_profiler_agent.py +102 -0
  102. patchi/core/agents/modernization_agent.py +84 -0
  103. patchi/core/agents/mutation_agent.py +174 -0
  104. patchi/core/agents/nl_query_agent.py +74 -0
  105. patchi/core/agents/promise_rejection_agent.py +100 -0
  106. patchi/core/agents/refactoring_agent.py +286 -0
  107. patchi/core/agents/resource_leak_agent.py +91 -0
  108. patchi/core/agents/route_graph_scanner.py +951 -0
  109. patchi/core/agents/run_agent.py +106 -0
  110. patchi/core/agents/rust_unwrap_agent.py +100 -0
  111. patchi/core/agents/sbom_generator.py +284 -0
  112. patchi/core/agents/scanners.py +67 -0
  113. patchi/core/agents/side/__init__.py +1 -0
  114. patchi/core/agents/side/build_agent.py +65 -0
  115. patchi/core/agents/side/format_agent.py +50 -0
  116. patchi/core/agents/side/install_agent.py +63 -0
  117. patchi/core/agents/side_file_scanner.py +549 -0
  118. patchi/core/agents/snapshot_drift_detector.py +146 -0
  119. patchi/core/agents/spa_route_inventory.py +203 -0
  120. patchi/core/agents/test_scanner.py +442 -0
  121. patchi/core/agents/tool_health.py +109 -0
  122. patchi/core/agents/tool_runner.py +162 -0
  123. patchi/core/agents/type_scanner.py +168 -0
  124. patchi/core/agents/ui_scanner.py +558 -0
  125. patchi/core/ai/__init__.py +20 -0
  126. patchi/core/ai/agent_audit.py +153 -0
  127. patchi/core/ai/agent_profiler.py +277 -0
  128. patchi/core/ai/client.py +499 -0
  129. patchi/core/ai/cost_tracker.py +107 -0
  130. patchi/core/ai/model_router.py +318 -0
  131. patchi/core/ai/orchestrator.py +625 -0
  132. patchi/core/ai/prompts.py +681 -0
  133. patchi/core/ai/smart.py +302 -0
  134. patchi/core/ai/tool_executor.py +24 -0
  135. patchi/core/ai/tools/__init__.py +10 -0
  136. patchi/core/ai/tools/executor.py +621 -0
  137. patchi/core/ai/tools/realize.py +1160 -0
  138. patchi/core/ai/tools/registry.py +1577 -0
  139. patchi/core/assurance/__init__.py +33 -0
  140. patchi/core/assurance/builder.py +211 -0
  141. patchi/core/assurance/graph.py +342 -0
  142. patchi/core/assurance/invariants.py +168 -0
  143. patchi/core/atomic.py +61 -0
  144. patchi/core/attackers/__init__.py +17 -0
  145. patchi/core/attackers/api_abuse.py +89 -0
  146. patchi/core/attackers/auth_bypass.py +73 -0
  147. patchi/core/attackers/base.py +91 -0
  148. patchi/core/attackers/business_logic.py +88 -0
  149. patchi/core/attackers/planner.py +108 -0
  150. patchi/core/attackers/priv_escalation.py +88 -0
  151. patchi/core/attackers/recon.py +87 -0
  152. patchi/core/brain/__init__.py +0 -0
  153. patchi/core/brain/ast_utils/__init__.py +81 -0
  154. patchi/core/brain/ast_utils/assignments.py +219 -0
  155. patchi/core/brain/ast_utils/calls.py +139 -0
  156. patchi/core/brain/ast_utils/config.py +132 -0
  157. patchi/core/brain/ast_utils/control_flow.py +74 -0
  158. patchi/core/brain/ast_utils/dead_symbols.py +205 -0
  159. patchi/core/brain/ast_utils/decorators.py +108 -0
  160. patchi/core/brain/ast_utils/helpers.py +144 -0
  161. patchi/core/brain/ast_utils/imports.py +169 -0
  162. patchi/core/brain/ast_utils/scan.py +143 -0
  163. patchi/core/brain/ast_utils/taint.py +310 -0
  164. patchi/core/brain/audit.py +379 -0
  165. patchi/core/brain/baseline.py +195 -0
  166. patchi/core/brain/blast_radius.py +312 -0
  167. patchi/core/brain/body_tags.py +144 -0
  168. patchi/core/brain/brain.py +1161 -0
  169. patchi/core/brain/brain_context.py +393 -0
  170. patchi/core/brain/brain_watcher.py +340 -0
  171. patchi/core/brain/charter.py +473 -0
  172. patchi/core/brain/classifier.py +214 -0
  173. patchi/core/brain/cockpit.py +350 -0
  174. patchi/core/brain/code_query.py +710 -0
  175. patchi/core/brain/contract.py +889 -0
  176. patchi/core/brain/contract_diff.py +155 -0
  177. patchi/core/brain/council.py +800 -0
  178. patchi/core/brain/cross_repo.py +104 -0
  179. patchi/core/brain/dead_code_tools.py +157 -0
  180. patchi/core/brain/doc_validator.py +197 -0
  181. patchi/core/brain/domain_activator.py +2183 -0
  182. patchi/core/brain/enriched_context.py +220 -0
  183. patchi/core/brain/file_corpus.py +220 -0
  184. patchi/core/brain/framework.py +953 -0
  185. patchi/core/brain/freshness.py +273 -0
  186. patchi/core/brain/git_aware.py +206 -0
  187. patchi/core/brain/heatmap.py +104 -0
  188. patchi/core/brain/ignore_parser.py +153 -0
  189. patchi/core/brain/import_graph.py +486 -0
  190. patchi/core/brain/languages.py +397 -0
  191. patchi/core/brain/layered_brain.py +419 -0
  192. patchi/core/brain/learning.py +229 -0
  193. patchi/core/brain/orphaned_endpoints.py +313 -0
  194. patchi/core/brain/personas/__init__.py +648 -0
  195. patchi/core/brain/personas/bad_user.py +165 -0
  196. patchi/core/brain/personas/base.py +349 -0
  197. patchi/core/brain/proactive.py +807 -0
  198. patchi/core/brain/project_context.py +121 -0
  199. patchi/core/brain/project_reader.py +521 -0
  200. patchi/core/brain/reasoning.py +269 -0
  201. patchi/core/brain/route_detector/__init__.py +15 -0
  202. patchi/core/brain/route_detector/base.py +31 -0
  203. patchi/core/brain/route_detector/csharp.py +209 -0
  204. patchi/core/brain/route_detector/file_based.py +81 -0
  205. patchi/core/brain/route_detector/go.py +152 -0
  206. patchi/core/brain/route_detector/java.py +195 -0
  207. patchi/core/brain/route_detector/javascript.py +197 -0
  208. patchi/core/brain/route_detector/php.py +158 -0
  209. patchi/core/brain/route_detector/python.py +193 -0
  210. patchi/core/brain/route_detector/registry.py +25 -0
  211. patchi/core/brain/route_detector/ruby.py +134 -0
  212. patchi/core/brain/route_detector/rust.py +216 -0
  213. patchi/core/brain/route_detector/swift.py +118 -0
  214. patchi/core/brain/route_mapper.py +265 -0
  215. patchi/core/brain/scanner.py +2136 -0
  216. patchi/core/brain/secrets.py +280 -0
  217. patchi/core/brain/symbol_graph.py +1955 -0
  218. patchi/core/brain/team.py +28 -0
  219. patchi/core/brain/trace_log.py +132 -0
  220. patchi/core/brain/type_checker/__init__.py +65 -0
  221. patchi/core/brain/type_checker/base.py +60 -0
  222. patchi/core/brain/type_checker/csharp.py +59 -0
  223. patchi/core/brain/type_checker/dart.py +59 -0
  224. patchi/core/brain/type_checker/go.py +81 -0
  225. patchi/core/brain/type_checker/java.py +75 -0
  226. patchi/core/brain/type_checker/kotlin.py +75 -0
  227. patchi/core/brain/type_checker/php.py +88 -0
  228. patchi/core/brain/type_checker/python.py +87 -0
  229. patchi/core/brain/type_checker/rust.py +81 -0
  230. patchi/core/brain/type_checker/swift.py +59 -0
  231. patchi/core/brain/type_checker/typescript.py +298 -0
  232. patchi/core/brain/understander.py +130 -0
  233. patchi/core/brain/verify.py +379 -0
  234. patchi/core/campaigns/__init__.py +14 -0
  235. patchi/core/campaigns/base.py +62 -0
  236. patchi/core/campaigns/data_flow_abuse.py +153 -0
  237. patchi/core/campaigns/orchestrator.py +140 -0
  238. patchi/core/campaigns/state_transitions.py +178 -0
  239. patchi/core/ci_bundle.py +104 -0
  240. patchi/core/config.py +311 -0
  241. patchi/core/constants.py +245 -0
  242. patchi/core/debug/__init__.py +25 -0
  243. patchi/core/debug/adapters/__init__.py +11 -0
  244. patchi/core/debug/adapters/codelldb.py +379 -0
  245. patchi/core/debug/adapters/node.py +222 -0
  246. patchi/core/debug/adapters/powershell.py +210 -0
  247. patchi/core/debug/adapters/python.py +244 -0
  248. patchi/core/debug/capture.py +210 -0
  249. patchi/core/debug/dap_client.py +268 -0
  250. patchi/core/detector/__init__.py +0 -0
  251. patchi/core/detector/audit.py +194 -0
  252. patchi/core/detector/bus.py +226 -0
  253. patchi/core/detector/dispatcher.py +275 -0
  254. patchi/core/detector/event.py +254 -0
  255. patchi/core/detector/sigma_engine.py +378 -0
  256. patchi/core/detector/triage.py +289 -0
  257. patchi/core/export/sarif.py +113 -0
  258. patchi/core/fix/__init__.py +17 -0
  259. patchi/core/fix/applier.py +574 -0
  260. patchi/core/fix/base.py +510 -0
  261. patchi/core/fix/code_fixer.py +169 -0
  262. patchi/core/fix/dead_code_remover.py +131 -0
  263. patchi/core/fix/fix_agents.py +775 -0
  264. patchi/core/fix/patch.py +444 -0
  265. patchi/core/fix/risk_gate.py +383 -0
  266. patchi/core/fix/security_fixer.py +93 -0
  267. patchi/core/fix/verify_loop.py +204 -0
  268. patchi/core/fuzz/__init__.py +8 -0
  269. patchi/core/fuzz/corpus.py +137 -0
  270. patchi/core/fuzz/input_fuzzer.py +256 -0
  271. patchi/core/fuzz/sequence_fuzzer.py +162 -0
  272. patchi/core/fuzz/state_fuzzer.py +236 -0
  273. patchi/core/health.py +407 -0
  274. patchi/core/hosted/__init__.py +1 -0
  275. patchi/core/hosted/anomaly.py +298 -0
  276. patchi/core/hosted/audit_log.py +102 -0
  277. patchi/core/hosted/compliance_report.py +148 -0
  278. patchi/core/hosted/ip_reputation.py +169 -0
  279. patchi/core/hosted/log_parsers.py +251 -0
  280. patchi/core/hosted/tokens.py +127 -0
  281. patchi/core/hosted/watchlist.py +164 -0
  282. patchi/core/hosted/webhooks.py +156 -0
  283. patchi/core/memory.py +437 -0
  284. patchi/core/notifications/__init__.py +18 -0
  285. patchi/core/notifications/channels.py +192 -0
  286. patchi/core/notifications/digest.py +141 -0
  287. patchi/core/notifications/escalation.py +158 -0
  288. patchi/core/notifications/notifier.py +182 -0
  289. patchi/core/notifications/quiet_hours.py +69 -0
  290. patchi/core/plugins/__init__.py +48 -0
  291. patchi/core/plugins/analyzer.py +326 -0
  292. patchi/core/plugins/analyzers/__init__.py +6 -0
  293. patchi/core/plugins/analyzers/dead_code_analyzer.py +120 -0
  294. patchi/core/plugins/analyzers/secret_scanner.py +158 -0
  295. patchi/core/plugins/analyzers/todo_scanner.py +115 -0
  296. patchi/core/plugins/registry.py +335 -0
  297. patchi/core/queue.py +375 -0
  298. patchi/core/reliability/__init__.py +13 -0
  299. patchi/core/reliability/chaos.py +163 -0
  300. patchi/core/reliability/fault_injector.py +172 -0
  301. patchi/core/reliability/idempotency.py +128 -0
  302. patchi/core/reliability/recovery.py +147 -0
  303. patchi/core/runtime/__init__.py +5 -0
  304. patchi/core/runtime/tracer.py +230 -0
  305. patchi/core/scan_bus.py +156 -0
  306. patchi/core/security/__init__.py +1 -0
  307. patchi/core/security/ai_validator.py +462 -0
  308. patchi/core/security/app_mapper.py +216 -0
  309. patchi/core/security/app_profile.py +464 -0
  310. patchi/core/security/attack_feedback.py +141 -0
  311. patchi/core/security/attack_scenarios/__init__.py +1 -0
  312. patchi/core/security/attack_tree.py +176 -0
  313. patchi/core/security/auth_audit_agent.py +344 -0
  314. patchi/core/security/authz_agent.py +602 -0
  315. patchi/core/security/auto_fix_proactive.py +620 -0
  316. patchi/core/security/auto_fixer.py +660 -0
  317. patchi/core/security/auto_ticket.py +53 -0
  318. patchi/core/security/bandit_agent.py +152 -0
  319. patchi/core/security/blast_radius.py +226 -0
  320. patchi/core/security/browser_tester.py +333 -0
  321. patchi/core/security/business_logic_agent.py +263 -0
  322. patchi/core/security/catch_block_auditor.py +395 -0
  323. patchi/core/security/cdn_cache_agent.py +357 -0
  324. patchi/core/security/chain_analyzer.py +321 -0
  325. patchi/core/security/chain_to_assurance.py +168 -0
  326. patchi/core/security/charter.py +510 -0
  327. patchi/core/security/cloud_waf_detector.py +295 -0
  328. patchi/core/security/codeql_agent.py +233 -0
  329. patchi/core/security/compliance_agent.py +348 -0
  330. patchi/core/security/confidence_gate.py +423 -0
  331. patchi/core/security/container_scanner.py +282 -0
  332. patchi/core/security/crypto_agent.py +378 -0
  333. patchi/core/security/cve_monitor.py +262 -0
  334. patchi/core/security/dast_agent.py +865 -0
  335. patchi/core/security/defectdojo.py +238 -0
  336. patchi/core/security/defenders/__init__.py +75 -0
  337. patchi/core/security/defenders/auth_middleware.py +115 -0
  338. patchi/core/security/defenders/base.py +97 -0
  339. patchi/core/security/defenders/block_ip.py +71 -0
  340. patchi/core/security/defenders/block_ws_origin.py +36 -0
  341. patchi/core/security/defenders/crypto_fix.py +118 -0
  342. patchi/core/security/defenders/enforce_rate_limit.py +108 -0
  343. patchi/core/security/defenders/fix_code.py +93 -0
  344. patchi/core/security/defenders/invalidate_session.py +40 -0
  345. patchi/core/security/defenders/patch_config.py +70 -0
  346. patchi/core/security/defenders/rotate_secret.py +54 -0
  347. patchi/core/security/defenders/suspend_account.py +41 -0
  348. patchi/core/security/defenders/update_dependency.py +160 -0
  349. patchi/core/security/defense_layer.py +217 -0
  350. patchi/core/security/dependency_vulnerability_agent.py +616 -0
  351. patchi/core/security/detection_pipeline.py +289 -0
  352. patchi/core/security/dns_security_agent.py +497 -0
  353. patchi/core/security/domain_activator_v2.py +913 -0
  354. patchi/core/security/domain_loader.py +1004 -0
  355. patchi/core/security/domains/__init__.py +0 -0
  356. patchi/core/security/email_authentication_agent.py +417 -0
  357. patchi/core/security/env_var_validator.py +304 -0
  358. patchi/core/security/evidence.py +100 -0
  359. patchi/core/security/falco_runtime_agent.py +387 -0
  360. patchi/core/security/fix-playbooks/__init__.py +0 -0
  361. patchi/core/security/gated_finding.py +85 -0
  362. patchi/core/security/git_diff_activator.py +367 -0
  363. patchi/core/security/governance.py +208 -0
  364. patchi/core/security/gradual_gate.py +34 -0
  365. patchi/core/security/history.py +315 -0
  366. patchi/core/security/iac_scanner.py +250 -0
  367. patchi/core/security/ignore_expiry.py +111 -0
  368. patchi/core/security/ignore_learner.py +416 -0
  369. patchi/core/security/injection_agent.py +319 -0
  370. patchi/core/security/insecure_randomness_agent.py +288 -0
  371. patchi/core/security/intent_analyzer.py +326 -0
  372. patchi/core/security/jwt_agent.py +453 -0
  373. patchi/core/security/kubernetes_agent.py +638 -0
  374. patchi/core/security/layer2_orchestrator.py +341 -0
  375. patchi/core/security/llm_security_agent.py +252 -0
  376. patchi/core/security/misconfig_agent.py +600 -0
  377. patchi/core/security/mobile_security_agent.py +474 -0
  378. patchi/core/security/network_agent.py +376 -0
  379. patchi/core/security/noise_filter.py +274 -0
  380. patchi/core/security/orchestrator.py +313 -0
  381. patchi/core/security/pattern_context.py +125 -0
  382. patchi/core/security/pattern_loader.py +101 -0
  383. patchi/core/security/pentest/__init__.py +4 -0
  384. patchi/core/security/pentest/base.py +58 -0
  385. patchi/core/security/pentest/dalfox_adapter.py +40 -0
  386. patchi/core/security/pentest/ffuf_adapter.py +48 -0
  387. patchi/core/security/pentest/nuclei_adapter.py +50 -0
  388. patchi/core/security/pentest/registry.py +80 -0
  389. patchi/core/security/pentest/shannon_adapter.py +107 -0
  390. patchi/core/security/pentest/sqlmap_adapter.py +56 -0
  391. patchi/core/security/pentest/zap_adapter.py +52 -0
  392. patchi/core/security/plan_auditor.py +276 -0
  393. patchi/core/security/policy_engine.py +272 -0
  394. patchi/core/security/prechecks.py +172 -0
  395. patchi/core/security/precommit_hook.py +247 -0
  396. patchi/core/security/privacy_agent.py +361 -0
  397. patchi/core/security/push_notification_agent.py +417 -0
  398. patchi/core/security/pysa_agent.py +143 -0
  399. patchi/core/security/reasoning.py +442 -0
  400. patchi/core/security/red_team_agent.py +184 -0
  401. patchi/core/security/red_team_engine.py +1142 -0
  402. patchi/core/security/remediation.py +360 -0
  403. patchi/core/security/request_interceptor.py +294 -0
  404. patchi/core/security/runtime_validator.py +245 -0
  405. patchi/core/security/saml_sso_agent.py +401 -0
  406. patchi/core/security/sast_agent.py +213 -0
  407. patchi/core/security/sast_gate.py +95 -0
  408. patchi/core/security/scheduler.py +287 -0
  409. patchi/core/security/secrets_runtime_agent.py +476 -0
  410. patchi/core/security/security_agents.py +175 -0
  411. patchi/core/security/security_config.py +551 -0
  412. patchi/core/security/security_probe.py +762 -0
  413. patchi/core/security/security_taint.py +1005 -0
  414. patchi/core/security/sensitive_data_agent.py +271 -0
  415. patchi/core/security/service_mesh_agent.py +412 -0
  416. patchi/core/security/session_management_agent.py +245 -0
  417. patchi/core/security/ssrf_agent.py +422 -0
  418. patchi/core/security/supply_chain.py +449 -0
  419. patchi/core/security/threat_model_generator.py +378 -0
  420. patchi/core/security/threat_model_updater.py +196 -0
  421. patchi/core/security/tool_adapters.py +135 -0
  422. patchi/core/security/tool_verify.py +173 -0
  423. patchi/core/security/websocket_security_agent.py +188 -0
  424. patchi/core/snapshot.py +242 -0
  425. patchi/core/tenant.py +386 -0
  426. patchi/core/testing/__init__.py +17 -0
  427. patchi/core/testing/_browser.py +147 -0
  428. patchi/core/testing/ads_agent.py +141 -0
  429. patchi/core/testing/api_contract_agent.py +450 -0
  430. patchi/core/testing/app_discovery_agent.py +641 -0
  431. patchi/core/testing/app_launcher.py +184 -0
  432. patchi/core/testing/browser_test_agent.py +516 -0
  433. patchi/core/testing/e2e_flow_agent.py +354 -0
  434. patchi/core/testing/flake_detector_agent.py +292 -0
  435. patchi/core/testing/gate.py +31 -0
  436. patchi/core/testing/live_test_runner.py +304 -0
  437. patchi/core/testing/live_v2/__init__.py +1 -0
  438. patchi/core/testing/live_v2/browser_pool.py +349 -0
  439. patchi/core/testing/live_v2/browser_test_runner.py +313 -0
  440. patchi/core/testing/live_v2/runner.py +626 -0
  441. patchi/core/testing/live_v2/screenshot_manager.py +474 -0
  442. patchi/core/testing/live_v2/stress_orchestrator.py +523 -0
  443. patchi/core/testing/live_v2/video_recorder.py +303 -0
  444. patchi/core/testing/regression_agent.py +302 -0
  445. patchi/core/testing/security_test_agent.py +253 -0
  446. patchi/core/testing/stress_test_agent.py +203 -0
  447. patchi/core/testing/test_agents.py +50 -0
  448. patchi/core/testing/test_config.py +194 -0
  449. patchi/core/testing/ui_accessibility_agent.py +324 -0
  450. patchi/core/testing/ui_button_agent.py +294 -0
  451. patchi/core/testing/ui_layout_agent.py +330 -0
  452. patchi/core/testing/unit_test_agent.py +1149 -0
  453. patchi/core/testing/visual_regression_agent.py +260 -0
  454. patchi/web/__init__.py +1 -0
  455. patchi/web/api/__init__.py +1 -0
  456. patchi/web/api/brain_map.py +114 -0
  457. patchi/web/api/charts.py +47 -0
  458. patchi/web/api/chat.py +224 -0
  459. patchi/web/api/cicd.py +576 -0
  460. patchi/web/api/dast_evidence.py +49 -0
  461. patchi/web/api/dev_check.py +320 -0
  462. patchi/web/api/fix.py +134 -0
  463. patchi/web/api/guard.py +65 -0
  464. patchi/web/api/hosted.py +128 -0
  465. patchi/web/api/hosted_v2.py +234 -0
  466. patchi/web/api/live_testing.py +699 -0
  467. patchi/web/api/scan.py +582 -0
  468. patchi/web/api/smart.py +111 -0
  469. patchi/web/api/tenant.py +117 -0
  470. patchi/web/api/tools.py +85 -0
  471. patchi/web/api_legacy.py +2401 -0
  472. patchi/web/app.py +310 -0
  473. patchi/web/events.py +483 -0
  474. patchi/web/routes/__init__.py +1 -0
  475. patchi/web/routes/assurance.py +541 -0
  476. patchi/web/routes/brain.py +36 -0
  477. patchi/web/routes/charter.py +227 -0
  478. patchi/web/routes/chat.py +23 -0
  479. patchi/web/routes/dashboard.py +558 -0
  480. patchi/web/routes/dashboard_v2.py +402 -0
  481. patchi/web/routes/findings.py +330 -0
  482. patchi/web/routes/guard.py +35 -0
  483. patchi/web/routes/history.py +126 -0
  484. patchi/web/routes/landing.py +52 -0
  485. patchi/web/routes/live_testing.py +24 -0
  486. patchi/web/routes/review.py +57 -0
  487. patchi/web/routes/self_improvement.py +110 -0
  488. patchi/web/routes/settings.py +49 -0
  489. patchi/web/routes/tokens.py +29 -0
  490. patchi/web/spawn.py +139 -0
  491. patchi/web/ws.py +141 -0
  492. patchi-0.7.5.dist-info/METADATA +315 -0
  493. patchi-0.7.5.dist-info/RECORD +496 -0
  494. patchi-0.7.5.dist-info/WHEEL +5 -0
  495. patchi-0.7.5.dist-info/licenses/LICENSE +60 -0
  496. patchi-0.7.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,648 @@
1
+ """
2
+ `p fix` — Propose and apply fixes for all detected issues.
3
+
4
+ Usage:
5
+ p fix — fix entire project
6
+ p fix src/auth — fix specific area
7
+ p fix --dry-run — preview what would be fixed, touch nothing
8
+ p fix --safe-all — apply every non-blocked patch, not just AUTO
9
+
10
+ Flow:
11
+ 1. Check contract_locked (hard block if not confirmed)
12
+ 2. Load findings from last scan (from memory)
13
+ 3. Run relevant fix agents (sequentially — spec mandates non-parallel)
14
+ 4. Collect proposed patches
15
+ 5. For each patch: run through risk gate
16
+ - BLOCK → skip, explain why
17
+ - AUTO → apply immediately (AUTO/AUTOPILOT mode, low risk)
18
+ - REQUIRE_REVIEW → queue for `p review`
19
+ 6. Show summary: applied / queued / blocked
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from pathlib import Path
25
+
26
+ from rich.panel import Panel
27
+ from rich.table import Table
28
+ from rich.text import Text
29
+
30
+ from patchi.cli.console import con
31
+ from patchi.cli.display.live_progress import LiveProgress
32
+ from patchi.core import memory as mem
33
+ from patchi.core.config import require_project_root
34
+ from patchi.core.fix.applier import PatchApplier
35
+ from patchi.core.fix.patch import Patch, PatchState
36
+ from patchi.core.fix.risk_gate import RiskGate
37
+ from patchi.core.fix.verify_loop import run_verify_loop, should_flag_for_review
38
+
39
+
40
+ def run(
41
+ area: str | None = None,
42
+ dry_run: bool = False,
43
+ preview: bool = False,
44
+ safe_all: bool = False,
45
+ root: Path | None = None,
46
+ ) -> None:
47
+ """Entry point for `p fix [area]`."""
48
+ try:
49
+ r = root or require_project_root()
50
+ except RuntimeError as e:
51
+ con.print(f"[red]{e}[/red]")
52
+ return
53
+
54
+ # ── 1. Contract lock check ─────────────────────────────────────────────────
55
+ brain = mem.get_brain(r)
56
+ if not brain.get("contract_locked"):
57
+ con.print()
58
+ con.print(
59
+ Panel(
60
+ "[bold yellow]App contract not yet formed.[/bold yellow]\n\n"
61
+ "[dim]Run [bold]p scan[/bold] first to understand your project,\n"
62
+ "then [bold]p fix[/bold] will work automatically.[/dim]\n",
63
+ border_style="yellow",
64
+ )
65
+ )
66
+ con.print()
67
+ return
68
+
69
+ # ── 2. Load findings from last scan ───────────────────────────────────────
70
+ scan_results = mem.get_scan_results(r)
71
+ all_findings = _collect_findings(scan_results, area)
72
+
73
+ # Annotate findings with framework info for multi-framework awareness
74
+ _annotate_findings_with_framework(all_findings, brain)
75
+
76
+ if not all_findings:
77
+ con.print()
78
+ con.print("[dim]No findings from last scan. Run [bold]p scan[/bold] first.[/dim]")
79
+ con.print()
80
+ return
81
+
82
+ fixable = [f for f in all_findings if f.get("fix_agent")]
83
+ if not fixable:
84
+ con.print()
85
+ con.print("[#4ADE80]✓[/#4ADE80] [dim]No fixable issues found.[/dim]")
86
+ con.print()
87
+ return
88
+
89
+ con.print()
90
+ area_label = f" [dim]→ {area}[/dim]" if area else ""
91
+ con.print(
92
+ f"[bold #C8621A]Fixing{area_label}[/bold #C8621A] "
93
+ f"[dim]{len(fixable)} fixable finding(s)[/dim]"
94
+ )
95
+ con.print()
96
+
97
+ if dry_run or preview:
98
+ _show_dry_run(fixable, r, preview=preview)
99
+ return
100
+
101
+ # ── 3. Run fix agents sequentially ────────────────────────────────────────
102
+ from patchi.cli.ux import format_step, status_icon, status_style
103
+
104
+ fix_phases = [
105
+ "Finding analysis",
106
+ "Fix agent execution",
107
+ "Risk gate evaluation",
108
+ "Patch application",
109
+ ]
110
+ total_steps = len(fix_phases)
111
+ current_step = 0
112
+
113
+ def show_fix_step(step_name: str, status: str = "running"):
114
+ nonlocal current_step
115
+ current_step += 1
116
+ icon = status_icon(status)
117
+ style = status_style(status)
118
+ con.print(format_step(current_step, total_steps, f"[{style}]{icon} {step_name}[/{style}]"))
119
+
120
+ show_fix_step("Finding analysis", "done")
121
+ show_fix_step("Fix agent execution")
122
+
123
+ patches = _run_fix_agents(fixable, r)
124
+
125
+ if not patches:
126
+ show_fix_step("Fix agent execution", "failed")
127
+ con.print("[dim]Fix agents produced no patches.[/dim]")
128
+ con.print(
129
+ "[dim]This may be because no AI key is configured. "
130
+ "Run [bold]p key add[/bold] to add one.[/dim]"
131
+ )
132
+ con.print()
133
+ return
134
+
135
+ show_fix_step("Fix agent execution", "done")
136
+ show_fix_step("Risk gate evaluation")
137
+
138
+ # ── 4. Risk gate + apply/queue ────────────────────────────────────────────
139
+ gate = RiskGate(r)
140
+ applier = PatchApplier(r)
141
+
142
+ try:
143
+ from patchi.core import config as cfg
144
+
145
+ config = cfg.load(r)
146
+ except Exception as e:
147
+ con.print(f"[dim]Config load warning: {e}[/dim]")
148
+ config = {}
149
+ brain = mem.get_brain(r)
150
+
151
+ # Sort patches by blast radius (smallest first) to minimize risk
152
+ patches.sort(key=lambda p: p.blast_radius or 0)
153
+
154
+ applied: list[Patch] = []
155
+ queued: list[Patch] = []
156
+ blocked: list[tuple[Patch, str]] = []
157
+
158
+ for patch in patches:
159
+ # Test-weakening guard: a fix that only changes test files must go to
160
+ # human review, never auto-apply — flag it before the gate so even
161
+ # AUTOPILOT mode queues it.
162
+ if should_flag_for_review(patch):
163
+ patch.requires_review = True
164
+
165
+ gate_result = gate.evaluate(patch)
166
+
167
+ if gate_result.is_blocked:
168
+ blocked.append((patch, gate_result.reason))
169
+ continue
170
+
171
+ # --safe-all: apply everything the gate didn't block, except
172
+ # test-weakening edits (requires_review set above) which still queue.
173
+ if gate_result.is_auto or (
174
+ safe_all and gate_result.needs_review and not patch.requires_review
175
+ ):
176
+ if (patch.source_finding or {}).get("type") == "test_failure":
177
+ # fix → verify → retry loop: apply, re-run the failing test,
178
+ # feed the failure back (up to 2 retries), never weaken tests.
179
+ outcome = run_verify_loop(
180
+ patch,
181
+ root=r,
182
+ config=config,
183
+ brain=brain,
184
+ applier=applier,
185
+ log=lambda msg: con.print(f"[dim]{msg}[/dim]"),
186
+ )
187
+ if outcome.review_required:
188
+ patch.state = PatchState.PENDING
189
+ _save_patch(outcome.patch or patch, r)
190
+ queued.append(outcome.patch or patch)
191
+ con.print(
192
+ f"[yellow]⚠[/yellow] Patch {patch.id} only changes test file(s) — "
193
+ "queued for human review (test-weakening edits are never "
194
+ "auto-applied)."
195
+ )
196
+ elif outcome.applied:
197
+ applied.append(outcome.patch or patch)
198
+ if outcome.verified:
199
+ con.print(
200
+ f"[#4ADE80]✓[/#4ADE80] Patch {patch.id} verified — failing test passes."
201
+ )
202
+ else:
203
+ con.print(
204
+ f"[yellow]⚠[/yellow] Patch {patch.id} applied but could not "
205
+ f"be re-verified: {outcome.reason}"
206
+ )
207
+ else:
208
+ con.print(
209
+ f"[yellow]⚠[/yellow] Patch {patch.id} not applied after "
210
+ f"{outcome.retries_used} retr{'y' if outcome.retries_used == 1 else 'ies'} — "
211
+ f"rolled back. {outcome.reason}"
212
+ )
213
+ else:
214
+ # Save patch to memory first
215
+ _save_patch(patch, r)
216
+ result = applier.apply(patch)
217
+ if result.success:
218
+ applied.append(patch)
219
+ else:
220
+ con.print(
221
+ f"[yellow]⚠[/yellow] Patch {patch.id} applied but tests failed — "
222
+ f"rolled back. {result.error}"
223
+ )
224
+ else:
225
+ # Queue for review
226
+ patch.state = PatchState.PENDING
227
+ _save_patch(patch, r)
228
+ queued.append(patch)
229
+
230
+ # ── 5. Record fix patterns for learning ───────────────────────────────────
231
+ try:
232
+ from patchi.core.brain.learning import record_fix_pattern
233
+
234
+ for patch in applied:
235
+ if patch.findings:
236
+ finding_type = patch.findings[0].get("type", "unknown")
237
+ file_pattern = patch.findings[0].get("file", "")
238
+ record_fix_pattern(
239
+ finding_type=finding_type,
240
+ file_pattern=file_pattern,
241
+ fix_strategy=patch.strategy,
242
+ fix_summary=patch.message[:200],
243
+ root=r,
244
+ )
245
+ except Exception as e:
246
+ con.print(f"[dim]Could not record fix pattern: {e}[/dim]")
247
+
248
+ show_fix_step("Risk gate evaluation", "done")
249
+ show_fix_step("Patch application", "done")
250
+
251
+ # ── 6. Summary ────────────────────────────────────────────────────────────
252
+ _show_summary(applied, queued, blocked)
253
+
254
+ # ── 7. Interactive review prompt ──────────────────────────────────────────
255
+ if queued and not dry_run:
256
+ from patchi.cli.ux import confirm
257
+ con.print()
258
+ if confirm("Review queued patches interactively?", default=False):
259
+ from patchi.cli.commands.fix_review_cmd import run as review_run
260
+ review_run(root=r)
261
+
262
+
263
+ # ── Fix agents runner ──────────────────────────────────────────────────────────
264
+
265
+
266
+ def _run_fix_agents(findings: list[dict], root: Path) -> list[Patch]:
267
+ """Run relevant fix agents sequentially. Returns all proposed patches."""
268
+ import patchi.core.fix.fix_agents # noqa: F401 — trigger registration
269
+ from patchi.core import config as cfg
270
+ from patchi.core.agents.base import AgentGroup, AgentInput, list_agents
271
+
272
+ try:
273
+ config = cfg.load(root)
274
+ except Exception as e:
275
+ con.print(f"[dim]Config load warning: {e}[/dim]")
276
+ config = {}
277
+
278
+ brain = mem.get_brain(root)
279
+
280
+ # Load brain report to include blast radius information
281
+ brain_report = None
282
+ try:
283
+ brain_data = mem.get_brain(root)
284
+ brain_report = type(
285
+ "BrainReport",
286
+ (),
287
+ {
288
+ "blast_radius_map": brain_data.get("blast_radius_map", {}),
289
+ "routes": brain_data.get("routes", []),
290
+ "file_infos": brain_data.get("file_infos", []),
291
+ },
292
+ )()
293
+ except Exception as e:
294
+ con.print(f"[dim]Could not build brain report: {e}[/dim]")
295
+ brain_report = None
296
+
297
+ # Build import graph for fix context
298
+ import_graph = None
299
+ try:
300
+ from patchi.core.brain.import_graph import build_import_graph
301
+
302
+ import_graph = build_import_graph(root)
303
+ # Enrich each finding with import graph context
304
+ for f in findings:
305
+ fpath = f.get("file", "")
306
+ if fpath and import_graph and fpath in import_graph.nodes:
307
+ f["import_context"] = import_graph.format_fix_context(fpath)
308
+ f["transitive_dependents"] = list(import_graph.get_transitive_dependents(fpath))
309
+ except Exception as e:
310
+ con.print(f"[dim]Import graph enrichment error: {e}[/dim]")
311
+
312
+ inp = AgentInput(
313
+ root=root,
314
+ scope=[],
315
+ brain=brain,
316
+ config=config,
317
+ extra={"findings": findings, "brain_report": brain_report}
318
+ if brain_report
319
+ else {"findings": findings},
320
+ )
321
+
322
+ fix_agent_classes = list_agents(AgentGroup.FIX)
323
+ all_patches: list[Patch] = []
324
+
325
+ lp = LiveProgress(con, title="Fix")
326
+ lp.start()
327
+ for agent_cls in fix_agent_classes:
328
+ # Only run agents that have relevant findings
329
+ relevant = [f for f in findings if f.get("fix_agent") == agent_cls.name]
330
+ if not relevant and agent_cls.name not in ("EnvFixer",):
331
+ continue
332
+
333
+ short_name = agent_cls.name.replace("Agent", "").replace("Fixer", "")
334
+ lp.set_progress(len(all_patches) + 1, len(fix_agent_classes), short_name)
335
+ lp.log(f" {short_name}…")
336
+ lp.update()
337
+
338
+ def ai_progress(msg: str):
339
+ lp.log(f" [dim]{msg}[/dim]", "dim")
340
+ lp.update()
341
+
342
+ inp_with_cb = AgentInput(
343
+ root=inp.root,
344
+ scope=inp.scope,
345
+ brain=inp.brain,
346
+ config=inp.config,
347
+ extra=inp.extra,
348
+ on_message=lambda n, msg, s: (lp.log(f" {msg}", s), lp.update()),
349
+ on_ai_progress=ai_progress,
350
+ )
351
+ result = agent_cls().run(inp_with_cb)
352
+ patch_count = len(result.data.get("patches", []))
353
+ lp.log(f" [{patch_count}] patches from {short_name}", "")
354
+
355
+ # Extract patches from result
356
+ for patch_dict in result.data.get("patches", []):
357
+ try:
358
+ patch = Patch.from_dict(patch_dict)
359
+ all_patches.append(patch)
360
+ except Exception as e:
361
+ lp.log(f" [dim]Skipping invalid patch: {e}[/dim]", "dim")
362
+
363
+ lp.update()
364
+
365
+ lp.stop(summary=f"{len(all_patches)} patch(es) from {len(fix_agent_classes)} agent(s)")
366
+ return all_patches
367
+
368
+
369
+ # ── Dry run display ────────────────────────────────────────────────────────────
370
+
371
+
372
+ def _show_dry_run(findings: list[dict], root: Path, preview: bool = False) -> None:
373
+ """Show what would be fixed without doing anything.
374
+
375
+ If preview=True, also show the actual before/after code diff for each patch.
376
+ """
377
+ table = Table(show_header=True, header_style="bold #C8621A", box=None, pad_edge=False)
378
+ table.add_column("Fix Agent", style="bold #F2EDD6", width=20)
379
+ table.add_column("File", style="dim", width=34)
380
+ table.add_column("Issue", width=40)
381
+ table.add_column("Sev", width=8)
382
+
383
+ sev_colors = {
384
+ "critical": "#FF4D6D",
385
+ "high": "#FF8C42",
386
+ "medium": "#FACC15",
387
+ "low": "#4ADE80",
388
+ "info": "dim",
389
+ }
390
+ _SEV_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
391
+
392
+ for f in sorted(findings, key=lambda x: _SEV_ORDER.get(x.get("severity", "info"), 99)):
393
+ sev = f.get("severity", "info")
394
+ table.add_row(
395
+ f.get("fix_agent", "?"),
396
+ f.get("file", "?")[:34],
397
+ f.get("message", "?")[:40],
398
+ Text(sev, style=sev_colors.get(sev, "white")),
399
+ )
400
+
401
+ con.print(
402
+ Panel(
403
+ table,
404
+ title="[bold #C8621A]--dry-run: Would fix[/bold #C8621A]",
405
+ border_style="#2A3D28",
406
+ )
407
+ )
408
+ con.print()
409
+ con.print(f"[dim]{len(findings)} finding(s) would be sent to fix agents.[/dim]")
410
+
411
+ # Run fix agents to generate proposed patches (without applying)
412
+ patches = _run_fix_agents(findings, root)
413
+
414
+ if patches:
415
+ gate = RiskGate(root)
416
+ con.print()
417
+ risk_table = Table(show_header=True, header_style="bold #C8621A", box=None, pad_edge=False)
418
+ risk_table.add_column("File", style="dim", width=34)
419
+ risk_table.add_column("Risk", justify="right", width=8)
420
+ risk_table.add_column("Confidence", justify="right", width=10)
421
+ risk_table.add_column("Action", width=16)
422
+ risk_table.add_column("Description", width=40)
423
+
424
+ files_changed: set[str] = set()
425
+ for patch in sorted(patches, key=lambda p: p.blast_radius or 0):
426
+ gate_result = gate.evaluate(patch)
427
+ action = (
428
+ "BLOCKED"
429
+ if gate_result.is_blocked
430
+ else ("AUTO-APPLY" if gate_result.is_auto else "QUEUE")
431
+ )
432
+ action_color = (
433
+ "#FF4D6D"
434
+ if gate_result.is_blocked
435
+ else ("#4ADE80" if gate_result.is_auto else "#FACC15")
436
+ )
437
+ for fp in patch.affected_paths:
438
+ files_changed.add(fp)
439
+ file_label = patch.affected_paths[0] if patch.affected_paths else "(multiple)"
440
+ risk_table.add_row(
441
+ file_label[:34],
442
+ f"{patch.risk_score:.0f}",
443
+ f"{patch.confidence:.0f}%",
444
+ Text(action, style=action_color),
445
+ (patch.description or "")[:40],
446
+ )
447
+
448
+ con.print(
449
+ Panel(
450
+ risk_table,
451
+ title="[bold #C8621A]--dry-run: Proposed patches[/bold #C8621A]",
452
+ border_style="#2A3D28",
453
+ )
454
+ )
455
+ con.print()
456
+ con.print(
457
+ f"[dim]{len(patches)} patch(es) across {len(files_changed)} file(s) would be generated.[/dim]"
458
+ )
459
+ else:
460
+ con.print("[dim]No patches would be generated by fix agents.[/dim]")
461
+
462
+ # --preview: show before/after code diffs
463
+ if preview and patches:
464
+ _show_preview_diffs(patches, root)
465
+
466
+ hint = "Remove --preview or --dry-run to apply fixes."
467
+ con.print(f"[dim]{hint}[/dim]")
468
+ con.print()
469
+
470
+
471
+ # ── Preview Diffs ──────────────────────────────────────────────────────────────
472
+
473
+
474
+ def _show_preview_diffs(patches: list[Patch], root: Path) -> None:
475
+ """Show before/after code diffs for each proposed patch."""
476
+ con.print()
477
+ con.print("[bold #C8621A]── Preview: Before / After ──[/bold #C8621A]")
478
+ con.print()
479
+
480
+ for i, patch in enumerate(patches, 1):
481
+ file_label = patch.affected_paths[0] if patch.affected_paths else "(unknown)"
482
+ con.print(
483
+ f"[bold]#{i} {file_label}[/bold] "
484
+ f"[dim]{patch.description[:60] if patch.description else ''}[/dim]"
485
+ )
486
+
487
+ # Try to read the current file content and show relevant lines
488
+ for change in (patch.changes or []):
489
+ file_path = change.get("path", "")
490
+ if not file_path:
491
+ continue
492
+
493
+ full_path = root / file_path
494
+ if not full_path.is_file():
495
+ continue
496
+
497
+ # Get line range from the change
498
+ start_line = change.get("line_start", change.get("line", 0))
499
+ end_line = change.get("line_end", start_line)
500
+ new_content = change.get("content", change.get("new_content", ""))
501
+
502
+ if not new_content and not start_line:
503
+ continue
504
+
505
+ try:
506
+ lines = full_path.read_text(encoding="utf-8", errors="replace").splitlines()
507
+ except Exception:
508
+ continue
509
+
510
+ # Show context: 3 lines before, the changed area, 3 lines after
511
+ ctx_start = max(0, (start_line or 1) - 4)
512
+ ctx_end = min(len(lines), (end_line or start_line or len(lines)) + 3)
513
+
514
+ if start_line and end_line:
515
+ # Show the diff
516
+ con.print(f" [dim]Lines {start_line}-{end_line}:[/dim]")
517
+ for ln in range(ctx_start, ctx_end):
518
+ line_num = ln + 1
519
+ marker = "-" if start_line <= line_num <= end_line else " "
520
+ color = "red" if marker == "-" else "dim"
521
+ con.print(f" [{color}]{marker} {line_num:4d} │ {lines[ln][:80]}[/{color}]")
522
+
523
+ if new_content:
524
+ con.print(" [green]+ After:[/green]")
525
+ for new_line in new_content.splitlines()[:10]:
526
+ con.print(f" [green]+ │ {new_line[:80]}[/green]")
527
+ elif new_content:
528
+ # New file or block
529
+ con.print(" [green]+ New content:[/green]")
530
+ for new_line in new_content.splitlines()[:15]:
531
+ con.print(f" [green]+ │ {new_line[:80]}[/green]")
532
+
533
+ con.print()
534
+
535
+
536
+ # ── Summary ────────────────────────────────────────────────────────────────────
537
+
538
+
539
+ def _show_summary(
540
+ applied: list[Patch],
541
+ queued: list[Patch],
542
+ blocked: list[tuple[Patch, str]],
543
+ ) -> None:
544
+ con.print()
545
+
546
+ if applied:
547
+ con.print(
548
+ f"[#4ADE80]✓ Applied:[/#4ADE80] {len(applied)} fix(es) passed tests and are live."
549
+ )
550
+ for p in applied:
551
+ con.print(f" [dim]● {p.id}[/dim] {p.description[:60]}")
552
+
553
+ if queued:
554
+ con.print()
555
+ con.print(
556
+ f"[yellow]● Queued for review:[/yellow] {len(queued)} fix(es) need your approval."
557
+ )
558
+ con.print(" [dim]Run [bold]p review[/bold] to see diffs and approve or reject.[/dim]")
559
+
560
+ if blocked:
561
+ con.print()
562
+ con.print(f"[red]✗ Blocked:[/red] {len(blocked)} fix(es) could not proceed.")
563
+ for _, reason in blocked[:3]:
564
+ con.print(f" [dim]{reason[:80]}[/dim]")
565
+
566
+ if not applied and not queued and not blocked:
567
+ con.print("[dim]No patches were generated.[/dim]")
568
+
569
+ con.print()
570
+
571
+
572
+ # ── Multi-framework annotation ─────────────────────────────────────────────────
573
+
574
+
575
+ def _build_file_framework_map(brain: dict) -> dict[str, str]:
576
+ """Build a file path → framework name map from brain routes and framework list.
577
+
578
+ Routes carry a ``framework`` field (set by RouteMapper). For files that
579
+ appear in no route we fall back to the first detected framework for that
580
+ file's language.
581
+ """
582
+ mapping: dict[str, str] = {}
583
+
584
+ # 1. Route-based mapping (most precise — per-file per-route)
585
+ for route in brain.get("routes", []):
586
+ if isinstance(route, dict):
587
+ fw = route.get("framework", "") or ""
588
+ fp = route.get("file", "") or ""
589
+ else:
590
+ fw = getattr(route, "framework", "") or ""
591
+ fp = getattr(route, "file", "") or ""
592
+ if fw and fp:
593
+ mapping[fp] = fw
594
+
595
+ # 2. Language-based fallback for files without routes
596
+ detected_frameworks = brain.get("frameworks", [])
597
+ if not mapping and detected_frameworks:
598
+ first_fw = ""
599
+ if isinstance(detected_frameworks[0], dict):
600
+ first_fw = detected_frameworks[0].get("name", "")
601
+ elif isinstance(detected_frameworks[0], str):
602
+ first_fw = detected_frameworks[0]
603
+ if first_fw:
604
+ for fi in brain.get("file_infos", []):
605
+ fp = (
606
+ fi.path
607
+ if hasattr(fi, "path")
608
+ else (fi.get("path") if isinstance(fi, dict) else "")
609
+ )
610
+ if fp and fp not in mapping:
611
+ mapping[fp] = first_fw
612
+
613
+ return mapping
614
+
615
+
616
+ def _annotate_findings_with_framework(findings: list[dict], brain: dict) -> None:
617
+ """Annotate each finding with the ``framework`` key so fix agents can
618
+ produce framework-aware prompts."""
619
+ fw_map = _build_file_framework_map(brain)
620
+ for f in findings:
621
+ fp = f.get("file", "")
622
+ if fp in fw_map:
623
+ f["framework"] = fw_map[fp]
624
+
625
+
626
+ # ── Helpers ────────────────────────────────────────────────────────────────────
627
+
628
+
629
+ def _collect_findings(scan_results: dict, area: str | None) -> list[dict]:
630
+ """Collect all findings from the last agent scan results."""
631
+ all_findings: list[dict] = []
632
+ for agent_name, data in scan_results.items():
633
+ if agent_name == "Brain":
634
+ continue
635
+ findings = data.get("findings", [])
636
+ for f in findings:
637
+ if area and not f.get("file", "").startswith(area):
638
+ continue
639
+ all_findings.append(f)
640
+ return all_findings
641
+
642
+
643
+ def _save_patch(patch: Patch, root: Path) -> None:
644
+ """Save a patch to memory."""
645
+ try:
646
+ mem.save_patch(patch.to_dict(), root)
647
+ except Exception as e:
648
+ con.print(f"[dim]Could not save patch {patch.id}: {e}[/dim]")