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,207 @@
1
+ """
2
+ `p model` — Manage Patchi's local Ollama model integration.
3
+
4
+ Subcommands:
5
+ p model set <model_name> — Set the local model Patchi uses
6
+ p model list — List available models from Ollama
7
+ p model status — Show current model + Ollama connection health
8
+
9
+ Ollama API base: http://localhost:11434
10
+ All network calls time out in 5s — they are informational checks, not blockers.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ # ── Subcommand runners ─────────────────────────────────────────────────────────
16
+ import logging
17
+ import time
18
+ from pathlib import Path
19
+
20
+ from rich.table import Table
21
+
22
+ from patchi.cli.console import con
23
+ from patchi.core.config import require_project_root
24
+ from patchi.core.constants import OLLAMA_BASE_URL
25
+
26
+ _log = logging.getLogger("patchi.cli.model_cmd")
27
+
28
+
29
+ def run_set(model_name: str, root: Path | None = None) -> None:
30
+ """Set the local model Patchi will use."""
31
+ try:
32
+ r = root or require_project_root()
33
+ except RuntimeError as e:
34
+ con.print(f"[red]{e}[/red]")
35
+ return
36
+
37
+ from patchi.core import config as cfg
38
+
39
+ cfg.set_value("ai.local_model_name", model_name, r)
40
+ con.print()
41
+ con.print(f"[#4ADE80]✓[/#4ADE80] Local model set to [bold]{model_name}[/bold]")
42
+ con.print()
43
+
44
+ # Quick availability check
45
+ ok, note = _model_available(model_name)
46
+ if ok:
47
+ con.print(f" [dim]✓ {note}[/dim]")
48
+ else:
49
+ con.print(f" [#FACC15]⚠ {note}[/#FACC15]")
50
+ con.print()
51
+
52
+
53
+ def run_list(root: Path | None = None) -> None:
54
+ """List models available from local Ollama instance."""
55
+ con.print()
56
+ con.print("[bold #C8621A]Local Models[/bold #C8621A] [dim]from Ollama (localhost:11434)[/dim]")
57
+ con.print()
58
+
59
+ try:
60
+ r = root or require_project_root()
61
+ from patchi.core import config as cfg
62
+
63
+ current_model = cfg.load(r).get("ai", {}).get("local_model_name", "")
64
+ except Exception as e:
65
+ _log.warning("run_list failed: %s", e)
66
+ current_model = ""
67
+
68
+ models = _fetch_models()
69
+
70
+ if models is None:
71
+ con.print("[#FACC15]⚠ Ollama is not running.[/#FACC15]")
72
+ con.print("[dim]Start it with: ollama serve[/dim]")
73
+ con.print()
74
+ return
75
+
76
+ if not models:
77
+ con.print("[dim]No models pulled yet.[/dim]")
78
+ con.print("[dim]Try: ollama pull llama3.2[/dim]")
79
+ con.print()
80
+ return
81
+
82
+ table = Table(show_header=True, header_style="bold #C8621A", box=None, pad_edge=False)
83
+ table.add_column("Model", style="bold #F2EDD6", width=32)
84
+ table.add_column("Size", justify="right", width=10)
85
+ table.add_column("Modified", width=22)
86
+ table.add_column("Active", width=8)
87
+
88
+ for m in sorted(models, key=lambda x: x.get("name", "")):
89
+ name = m.get("name", "?")
90
+ size = _fmt_size(m.get("size", 0))
91
+ mod = m.get("modified_at", "")[:19].replace("T", " ")
92
+ active = (
93
+ "[#4ADE80]●[/#4ADE80]"
94
+ if name.split(":")[0] == (current_model or "").split(":")[0]
95
+ else ""
96
+ )
97
+ table.add_row(name, size, mod, active)
98
+
99
+ con.print(table)
100
+ con.print()
101
+
102
+
103
+ def run_status(root: Path | None = None) -> None:
104
+ """Show Ollama connection health and current model."""
105
+ con.print()
106
+ con.print("[bold #C8621A]Model Status[/bold #C8621A]")
107
+ con.print()
108
+
109
+ try:
110
+ r = root or require_project_root()
111
+ from patchi.core import config as cfg
112
+
113
+ config = cfg.load(r)
114
+ local_model = config.get("ai", {}).get("local_model_name")
115
+ except RuntimeError:
116
+ local_model = None
117
+
118
+ # ── Ollama health ─────────────────────────────────────────────────────────
119
+ ollama_ok, ollama_note = _check_ollama()
120
+ marker = "[#4ADE80]●[/#4ADE80]" if ollama_ok else "[#FF4D6D]●[/#FF4D6D]"
121
+ con.print(f" {marker} Ollama [dim]{ollama_note}[/dim]")
122
+
123
+ # ── Configured model ──────────────────────────────────────────────────────
124
+ if local_model:
125
+ avail_ok, avail_note = _model_available(local_model)
126
+ marker2 = "[#4ADE80]●[/#4ADE80]" if avail_ok else "[#FACC15]●[/#FACC15]"
127
+ con.print(f" {marker2} Model [bold]{local_model}[/bold] [dim]{avail_note}[/dim]")
128
+
129
+ # Benchmark ping if available
130
+ if ollama_ok:
131
+ ms = _ping_model(local_model)
132
+ if ms is not None:
133
+ con.print(f" [dim] Response time: {ms}ms[/dim]")
134
+ else:
135
+ con.print(" [dim]— No local model configured. Run: p model set <name>[/dim]")
136
+
137
+ con.print()
138
+
139
+
140
+ # ── Ollama API helpers ─────────────────────────────────────────────────────────
141
+
142
+
143
+ def _fetch_models() -> list[dict] | None:
144
+ """Return list of pulled model objects, or None if Ollama is unreachable."""
145
+ try:
146
+ import httpx
147
+
148
+ resp = httpx.get(f"{OLLAMA_BASE_URL}/api/tags", timeout=4.0)
149
+ if resp.status_code == 200:
150
+ return resp.json().get("models", [])
151
+ return []
152
+ except Exception as e:
153
+ _log.warning("_fetch_models failed: %s", e)
154
+ return None
155
+
156
+
157
+ def _check_ollama() -> tuple[bool, str]:
158
+ try:
159
+ import httpx
160
+
161
+ t0 = time.monotonic()
162
+ resp = httpx.get(f"{OLLAMA_BASE_URL}/", timeout=4.0)
163
+ ms = int((time.monotonic() - t0) * 1000)
164
+ if resp.status_code in (200, 404):
165
+ return True, f"Running — localhost:11434 ({ms}ms)"
166
+ return False, f"HTTP {resp.status_code}"
167
+ except Exception as e:
168
+ _log.debug("_check_ollama failed: %s", e)
169
+ return False, "Not running — start with: ollama serve"
170
+
171
+
172
+ def _model_available(model_name: str) -> tuple[bool, str]:
173
+ models = _fetch_models()
174
+ if models is None:
175
+ return False, "Ollama not running"
176
+ stem = model_name.split(":")[0]
177
+ if any(m.get("name", "").split(":")[0] == stem for m in models):
178
+ return True, "Model is available locally"
179
+ return False, f"Not pulled — run: ollama pull {model_name}"
180
+
181
+
182
+ def _ping_model(model_name: str) -> int | None:
183
+ """Send a minimal generation request to measure first-token latency."""
184
+ try:
185
+ import httpx
186
+
187
+ t0 = time.monotonic()
188
+ resp = httpx.post(
189
+ f"{OLLAMA_BASE_URL}/api/generate",
190
+ json={"model": model_name, "prompt": "hi", "stream": False, "max_tokens": 1},
191
+ timeout=15.0,
192
+ )
193
+ ms = int((time.monotonic() - t0) * 1000)
194
+ if resp.status_code == 200:
195
+ return ms
196
+ return None
197
+ except Exception as e:
198
+ _log.debug("_ping_model failed: %s", e)
199
+ return None
200
+
201
+
202
+ def _fmt_size(size_bytes: int) -> str:
203
+ if size_bytes >= 1_073_741_824:
204
+ return f"{size_bytes / 1_073_741_824:.1f} GB"
205
+ if size_bytes >= 1_048_576:
206
+ return f"{size_bytes / 1_048_576:.0f} MB"
207
+ return f"{size_bytes} B"
@@ -0,0 +1,213 @@
1
+ """
2
+ `p notify` — manage notification channels.
3
+ list | add <type> | remove <name> | test [name] | ack <id> | flush | pending
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+
13
+ import patchi.core.config as config
14
+ from patchi.cli.commands.notify_prompts import CHANNEL_SETUP_FIELDS, SEVERITY_CHOICES
15
+ from patchi.core.notifications.channels import ChannelType, from_config
16
+ from patchi.core.notifications.notifier import Notifier
17
+
18
+ console = Console()
19
+
20
+
21
+ def run_notify(args) -> None:
22
+ sub = getattr(args, "notify_cmd", None)
23
+
24
+ if sub == "list" or sub is None:
25
+ _cmd_list(args)
26
+ elif sub == "add":
27
+ _cmd_add(args)
28
+ elif sub == "remove":
29
+ _cmd_remove(args)
30
+ elif sub == "test":
31
+ _cmd_test(args)
32
+ elif sub == "ack":
33
+ _cmd_ack(args)
34
+ elif sub == "flush":
35
+ _cmd_flush(args)
36
+ elif sub == "pending":
37
+ _cmd_pending(args)
38
+
39
+
40
+ def _cmd_list(args) -> None:
41
+ root = _require_root()
42
+ cfg = config.load(root)
43
+ channels = cfg.get("notifications", [])
44
+
45
+ if not channels:
46
+ console.print("[dim]No notification channels configured.[/dim]")
47
+ console.print("Add one with: [bold]p notify add slack[/bold]")
48
+ return
49
+
50
+ table = Table(show_header=True, header_style="bold", box=None, pad_edge=False)
51
+ table.add_column("Name", style="cyan", no_wrap=True)
52
+ table.add_column("Type", style="white")
53
+ table.add_column("Min Severity", style="yellow")
54
+ table.add_column("Digest", style="dim")
55
+
56
+ for ch in channels:
57
+ table.add_row(
58
+ ch.get("name", "—"),
59
+ ch.get("type", "—"),
60
+ ch.get("min_severity", "medium"),
61
+ "yes" if ch.get("in_digest") else "no",
62
+ )
63
+
64
+ console.print(table)
65
+ quiet = cfg.get("quiet_hours", {})
66
+ if quiet.get("enabled"):
67
+ console.print(
68
+ f"\n[dim]Quiet hours: {quiet['start']} → {quiet['end']} ({quiet.get('timezone', 'UTC')})[/dim]"
69
+ )
70
+ console.print(f"[dim]Digest frequency: {cfg.get('digest_frequency', 'daily')}[/dim]")
71
+
72
+
73
+ def _prompt_credentials(channel_type: str) -> dict[str, str]:
74
+ """Prompt for channel-specific credentials. Returns a dict of field→value."""
75
+ credentials: dict[str, str] = {}
76
+ for field_key, prompt in CHANNEL_SETUP_FIELDS.get(channel_type, []):
77
+ value = console.input(f"[cyan]{prompt}: [/cyan]").strip()
78
+ if value:
79
+ credentials[field_key] = value
80
+ return credentials
81
+
82
+
83
+ def _cmd_add(args) -> None:
84
+ root = _require_root()
85
+ channel_type = getattr(args, "channel_type", None)
86
+
87
+ valid_types = [t.value for t in ChannelType]
88
+ if not channel_type or channel_type not in valid_types:
89
+ console.print(f"[red]Specify a channel type:[/red] {', '.join(valid_types)}")
90
+ return
91
+
92
+ console.print(f"\n[bold]Adding {channel_type} channel[/bold]")
93
+ name = console.input("[cyan]Channel name (e.g. my-slack): [/cyan]").strip()
94
+ if not name:
95
+ console.print("[red]Name cannot be empty.[/red]")
96
+ return
97
+
98
+ credentials: dict[str, str] = _prompt_credentials(channel_type)
99
+
100
+ min_severity = (
101
+ console.input(
102
+ f"[cyan]Minimum severity [{'/'.join(SEVERITY_CHOICES)}] (default: medium): [/cyan]"
103
+ ).strip()
104
+ or "medium"
105
+ )
106
+ if min_severity not in SEVERITY_CHOICES:
107
+ min_severity = "medium"
108
+
109
+ in_digest_raw = (
110
+ console.input("[cyan]Batch into digest? (y/n, default: n): [/cyan]").strip().lower()
111
+ )
112
+ in_digest = in_digest_raw == "y"
113
+
114
+ raw = {
115
+ "name": name,
116
+ "type": channel_type,
117
+ "min_severity": min_severity,
118
+ "in_digest": in_digest,
119
+ **credentials,
120
+ }
121
+
122
+ # Validate before saving
123
+ try:
124
+ from_config(raw)
125
+ except ValueError as exc:
126
+ console.print(f"[red]Invalid config:[/red] {exc}")
127
+ return
128
+
129
+ cfg = config.load(root)
130
+ cfg.setdefault("notifications", [])
131
+
132
+ if any(ch["name"] == name for ch in cfg["notifications"]):
133
+ console.print(f"[red]A channel named '{name}' already exists.[/red]")
134
+ return
135
+
136
+ cfg["notifications"].append(raw)
137
+ config.save(cfg, root)
138
+ console.print(f"[green]✓ Channel '{name}' added.[/green]")
139
+ console.print("Run [bold]p notify test[/bold] to verify it works.")
140
+
141
+
142
+ def _cmd_remove(args) -> None:
143
+ root = _require_root()
144
+ name = getattr(args, "channel_name", None)
145
+ if not name:
146
+ console.print("[red]Specify a channel name to remove.[/red]")
147
+ return
148
+
149
+ cfg = config.load(root)
150
+ before = len(cfg.get("notifications", []))
151
+ cfg["notifications"] = [ch for ch in cfg.get("notifications", []) if ch.get("name") != name]
152
+
153
+ if len(cfg["notifications"]) == before:
154
+ console.print(f"[red]No channel named '{name}' found.[/red]")
155
+ return
156
+
157
+ config.save(cfg, root)
158
+ console.print(f"[green]✓ Channel '{name}' removed.[/green]")
159
+
160
+
161
+ def _cmd_test(args) -> None:
162
+ root = _require_root()
163
+ target = getattr(args, "channel_name", None)
164
+ if not config.load(root).get("notifications"):
165
+ console.print("[dim]No channels configured. Add one with p notify add.[/dim]")
166
+ return
167
+ for ch_name, ok in _notifier(root).test(target):
168
+ console.print(f" {ch_name}: {'[green]✓ sent[/green]' if ok else '[red]✗ failed[/red]'}")
169
+
170
+
171
+ def _cmd_ack(args) -> None:
172
+ aid = getattr(args, "alert_id", None)
173
+ if not aid:
174
+ console.print("[red]Provide an alert ID to acknowledge.[/red]")
175
+ return
176
+ ok = _notifier(_require_root()).acknowledge(aid)
177
+ msg = (
178
+ f"[green]✓ Alert {aid} acknowledged.[/green]"
179
+ if ok
180
+ else f"[red]Alert ID '{aid}' not found.[/red]"
181
+ )
182
+ console.print(msg)
183
+
184
+
185
+ def _cmd_flush(args) -> None:
186
+ sent = _notifier(_require_root()).flush_digest()
187
+ console.print(
188
+ f"[green]✓ Digest flushed to {sent} channel(s).[/green]"
189
+ if sent
190
+ else "[dim]Nothing to flush.[/dim]"
191
+ )
192
+
193
+
194
+ def _cmd_pending(args) -> None:
195
+ pending = _notifier(_require_root()).pending_escalations()
196
+ if not pending:
197
+ console.print("[dim]No pending escalations.[/dim]")
198
+ return
199
+ for alert in pending:
200
+ console.print(f" [red]{alert['id']}[/red] {alert['severity'].upper()} {alert['title']}")
201
+ console.print("\nAck with: [bold]p notify ack <id>[/bold]")
202
+
203
+
204
+ def _notifier(root: Path) -> Notifier:
205
+ return Notifier(root, config.load(root))
206
+
207
+
208
+ def _require_root() -> Path:
209
+ try:
210
+ return config.require_project_root()
211
+ except RuntimeError as exc:
212
+ console.print(f"[red]{exc}[/red]")
213
+ raise SystemExit(1) from exc
@@ -0,0 +1,29 @@
1
+ """
2
+ Prompt field definitions for `p notify add` interactive setup.
3
+ Kept separate so notify_cmd.py stays under 200 lines.
4
+ """
5
+
6
+ CHANNEL_SETUP_FIELDS: dict[str, list[tuple[str, str]]] = {
7
+ "email": [
8
+ ("user", "SMTP username (your email address)"),
9
+ ("password", "SMTP password or app password"),
10
+ ("host", "SMTP host (e.g. smtp.gmail.com)"),
11
+ ("port", "SMTP port [leave blank for default]"),
12
+ ("to", "Recipient address [blank = same as username]"),
13
+ ],
14
+ "slack": [
15
+ ("webhook_url", "Slack incoming webhook URL"),
16
+ ],
17
+ "discord": [
18
+ ("webhook_url", "Discord webhook URL"),
19
+ ],
20
+ "webhook": [
21
+ ("url", "Webhook URL (https://...)"),
22
+ ],
23
+ "telegram": [
24
+ ("bot_token", "Telegram bot token"),
25
+ ("chat_id", "Chat ID (your chat or channel ID)"),
26
+ ],
27
+ }
28
+
29
+ SEVERITY_CHOICES: list[str] = ["critical", "high", "medium", "low", "info"]
@@ -0,0 +1,231 @@
1
+ """
2
+ `p patch` — Manage individual patches by ID.
3
+
4
+ Subcommands:
5
+ p patch list — list all patches (all states)
6
+ p patch show <id> — show full diff + metadata for one patch
7
+ p patch apply <id> — apply a pending patch
8
+ p patch reject <id> — reject a pending patch
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from datetime import UTC
14
+ from pathlib import Path
15
+
16
+ from rich.syntax import Syntax
17
+ from rich.table import Table
18
+ from rich.text import Text
19
+
20
+ from patchi.cli.console import con
21
+ from patchi.core import memory as mem
22
+ from patchi.core.config import require_project_root
23
+ from patchi.core.fix.patch import Patch, PatchState
24
+
25
+ _STATE_COLORS = {
26
+ "proposed": "#B8A898",
27
+ "pending": "#FACC15",
28
+ "auto_applied": "#4ADE80",
29
+ "applying": "#C8621A",
30
+ "applied": "#4ADE80",
31
+ "failed": "#FF4D6D",
32
+ "rolled_back": "#FF8C42",
33
+ "rejected": "dim",
34
+ "undone": "dim",
35
+ }
36
+
37
+
38
+ def run_list(root: Path | None = None, json_output: bool = False) -> None:
39
+ """p patch list"""
40
+ try:
41
+ r = root or require_project_root()
42
+ except RuntimeError as e:
43
+ con.print(f"[red]{e}[/red]")
44
+ return
45
+
46
+ patches_raw = mem.list_patches(r)
47
+ if json_output:
48
+ import json as _json
49
+
50
+ con.print(_json.dumps({"patches": patches_raw or []}, indent=2, default=str))
51
+ return
52
+
53
+ if not patches_raw:
54
+ con.print("[dim]No patches yet. Run [bold]p fix[/bold] to generate some.[/dim]")
55
+ return
56
+
57
+ con.print()
58
+ table = Table(show_header=True, header_style="bold #C8621A", box=None, pad_edge=False)
59
+ table.add_column("ID", style="bold #F2EDD6", width=10)
60
+ table.add_column("State", width=12)
61
+ table.add_column("Agent", style="dim", width=18)
62
+ table.add_column("Risk", width=8)
63
+ table.add_column("Files", justify="right", width=6)
64
+ table.add_column("Description", width=44)
65
+
66
+ risk_colors = {"low": "#4ADE80", "medium": "#FACC15", "high": "#FF4D6D"}
67
+
68
+ for p in reversed(patches_raw): # newest first
69
+ risk_score = p.get("risk_score", 0)
70
+ risk_level = p.get("risk_level", "low")
71
+ state = p.get("state", "proposed")
72
+ table.add_row(
73
+ p.get("id", "?"),
74
+ Text(state, style=_STATE_COLORS.get(state, "white")),
75
+ p.get("agent", "?"),
76
+ Text(f"{risk_score}", style=risk_colors.get(risk_level, "white")),
77
+ str(p.get("file_count", 0)),
78
+ p.get("description", "?")[:44],
79
+ )
80
+
81
+ con.print(table)
82
+ con.print()
83
+ con.print("[dim]p patch show <id> · p patch apply <id> · p patch reject <id>[/dim]")
84
+ con.print()
85
+
86
+
87
+ def run_show(patch_id: str, root: Path | None = None, json_output: bool = False) -> None:
88
+ """p patch show <id>"""
89
+ try:
90
+ r = root or require_project_root()
91
+ except RuntimeError as e:
92
+ con.print(f"[red]{e}[/red]")
93
+ return
94
+
95
+ patch_dict = mem.get_patch(patch_id, r)
96
+ if not patch_dict:
97
+ if json_output:
98
+ import json as _json
99
+
100
+ con.print(_json.dumps({"error": f"Patch {patch_id!r} not found"}, indent=2))
101
+ else:
102
+ con.print(f"[red]Patch {patch_id!r} not found.[/red]")
103
+ return
104
+
105
+ if json_output:
106
+ import json as _json
107
+
108
+ con.print(_json.dumps(patch_dict, indent=2, default=str))
109
+ return
110
+
111
+ patch = Patch.from_dict(patch_dict)
112
+ state = patch.state.value
113
+ state_color = _STATE_COLORS.get(state, "white")
114
+ risk_colors = {"low": "#4ADE80", "medium": "#FACC15", "high": "#FF4D6D"}
115
+ risk_color = risk_colors.get(patch.risk_level, "white")
116
+
117
+ con.print()
118
+ con.print(
119
+ f"[bold #F2EDD6]Patch {patch.id}[/bold #F2EDD6] [{state_color}]{state}[/{state_color}]"
120
+ )
121
+ con.print(
122
+ f"[dim]Agent: {patch.agent} · Type: {patch.patch_type.value} · "
123
+ f"Proposed: {patch.proposed_at[:19]}[/dim]"
124
+ )
125
+ con.print()
126
+ con.print(f" {patch.description}")
127
+ if patch.ai_explanation:
128
+ con.print(f" [dim italic]{patch.ai_explanation[:120]}[/dim italic]")
129
+ con.print()
130
+ con.print(
131
+ f" [bold {risk_color}]Risk: {patch.risk_score}/100 ({patch.risk_level})[/bold {risk_color}]"
132
+ f" [dim]Confidence: {patch.confidence}%"
133
+ f" Blast radius: {patch.blast_radius}[/dim]"
134
+ )
135
+ con.print()
136
+
137
+ for change in patch.changes:
138
+ if not change.diff.strip():
139
+ continue
140
+ con.print(
141
+ f"[bold #B8A898]{change.path}[/bold #B8A898] "
142
+ f"[dim]+{change.lines_added} -{change.lines_removed}[/dim]"
143
+ )
144
+ con.print(Syntax(change.diff, "diff", theme="monokai", padding=1))
145
+
146
+ if patch.test_result:
147
+ passed = patch.test_result.get("passed")
148
+ if passed is True:
149
+ con.print("[#4ADE80]Tests passed.[/#4ADE80]")
150
+ elif passed is False:
151
+ con.print("[red]Tests failed — patch rolled back.[/red]")
152
+ out = patch.test_result.get("output", "")
153
+ if out:
154
+ con.print(f"[dim]{out[:500]}[/dim]")
155
+
156
+ con.print()
157
+
158
+
159
+ def run_apply(patch_id: str, root: Path | None = None) -> None:
160
+ """p patch apply <id>"""
161
+ try:
162
+ r = root or require_project_root()
163
+ except RuntimeError as e:
164
+ con.print(f"[red]{e}[/red]")
165
+ return
166
+
167
+ patch_dict = mem.get_patch(patch_id, r)
168
+ if not patch_dict:
169
+ con.print(f"[red]Patch {patch_id!r} not found.[/red]")
170
+ return
171
+
172
+ if patch_dict.get("state") not in ("pending", "proposed"):
173
+ con.print(f"[yellow]Patch {patch_id} is already {patch_dict['state']}.[/yellow]")
174
+ return
175
+
176
+ from patchi.core.fix.applier import PatchApplier
177
+ from patchi.core.fix.risk_gate import RiskGate
178
+
179
+ patch = Patch.from_dict(patch_dict)
180
+ gate = RiskGate(r)
181
+ gate_result = gate.evaluate(patch)
182
+
183
+ if gate_result.is_blocked:
184
+ con.print(f"[red]Blocked:[/red] {gate_result.blocks[0]}")
185
+ return
186
+
187
+ con.print(f"Applying patch [bold]{patch_id}[/bold]…")
188
+ result = PatchApplier(r).apply(patch)
189
+
190
+ if result.success:
191
+ con.print(
192
+ f"[#4ADE80]✓ Applied.[/#4ADE80] "
193
+ f"[dim]Tests: {'passed' if result.test_passed else 'skipped'} "
194
+ f"Snapshot: {result.snapshot_id}[/dim]"
195
+ )
196
+ else:
197
+ con.print(f"[red]✗ Failed.[/red] [dim]{result.error}[/dim]")
198
+ if result.rolled_back:
199
+ con.print("[dim]Rolled back to original state.[/dim]")
200
+
201
+
202
+ def run_reject(patch_id: str, root: Path | None = None) -> None:
203
+ """p patch reject <id>"""
204
+ try:
205
+ r = root or require_project_root()
206
+ except RuntimeError as e:
207
+ con.print(f"[red]{e}[/red]")
208
+ return
209
+
210
+ patch_dict = mem.get_patch(patch_id, r)
211
+ if not patch_dict:
212
+ con.print(f"[red]Patch {patch_id!r} not found.[/red]")
213
+ return
214
+
215
+ try:
216
+ import json
217
+ from datetime import datetime
218
+
219
+ from patchi.core.constants import MEMORY_FILES, MemoryCategory
220
+
221
+ patches = mem.list_patches(r)
222
+ for p in patches:
223
+ if p.get("id") == patch_id:
224
+ p["state"] = PatchState.REJECTED.value
225
+ p["rejected_at"] = datetime.now(UTC).isoformat()
226
+ break
227
+ mem_path = r / MEMORY_FILES[MemoryCategory.PATCHES]
228
+ mem_path.write_text(json.dumps(patches, indent=2), encoding="utf-8")
229
+ con.print(f"[dim]Patch {patch_id} rejected.[/dim]")
230
+ except Exception as e:
231
+ con.print(f"[red]Error: {e}[/red]")