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,679 @@
1
+ """
2
+ `p status` — unified status, health, and doctor command.
3
+
4
+ Shows: Brain health · Current mode · Queue depth · AI key status
5
+
6
+ Flags:
7
+ --deep Show detailed health breakdown (was p health)
8
+ --validate Run system validation checks (was p doctor)
9
+ --verbose Show detailed output
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from pathlib import Path
16
+ from typing import TYPE_CHECKING
17
+
18
+ from rich.panel import Panel
19
+ from rich.table import Table
20
+ from rich.text import Text
21
+
22
+ from patchi.cli.console import con
23
+
24
+ if TYPE_CHECKING:
25
+ pass
26
+
27
+ _log = logging.getLogger("patchi.cli.status_cmd")
28
+
29
+
30
+ def _health_bar(score: int, width: int = 20) -> str:
31
+ """Build a visual health bar: [████████░░░░░░░░░░░░] 80/100."""
32
+ filled = int(score / 100 * width)
33
+ empty = width - filled
34
+ if score >= 80:
35
+ color = "#4ADE80"
36
+ elif score >= 50:
37
+ color = "#FACC15"
38
+ elif score >= 25:
39
+ color = "#FF8C42"
40
+ else:
41
+ color = "#FF4D6D"
42
+ bar = f"[{color}]{'█' * filled}[/{color}][dim]{'░' * empty}[/dim]"
43
+ return f"{bar} {score}/100"
44
+
45
+
46
+ def run(
47
+ root: Path | None = None,
48
+ json_output: bool = False,
49
+ deep: bool = False,
50
+ validate: bool = False,
51
+ verbose: bool = False,
52
+ ) -> None:
53
+ """p status — unified command for status, health, and doctor."""
54
+ from patchi.core import config as cfg
55
+
56
+ try:
57
+ r = root or cfg.find_project_root()
58
+ if not r:
59
+ if json_output:
60
+ import json as _json
61
+ con.print(_json.dumps({"error": "no project root found"}))
62
+ else:
63
+ con.print("[yellow]No project root found. Run 'p init' first.[/yellow]")
64
+ return
65
+ except RuntimeError as e:
66
+ con.print(f"[red]{e}[/red]")
67
+ return
68
+
69
+ if validate:
70
+ _run_doctor(r, json_output=json_output, verbose=verbose)
71
+ return
72
+
73
+ if deep:
74
+ _run_health(r, json_output=json_output)
75
+ return
76
+
77
+ # Default: status view
78
+ _run_status(r, json_output=json_output)
79
+
80
+
81
+ def _run_status(root: Path, json_output: bool) -> None:
82
+ """Original status view: Brain health, mode, queue state, AI keys."""
83
+ from patchi.core import config as cfg
84
+ from patchi.core import memory as mem
85
+ from patchi.core import queue as q
86
+
87
+ try:
88
+ config = cfg.load(root)
89
+ brain = mem.get_brain(root)
90
+ qstats = q.stats(root)
91
+ mode = cfg.get_mode(root)
92
+ qmode = cfg.get_queue_mode(root)
93
+ except RuntimeError as e:
94
+ con.print(f"[red]{e}[/red]")
95
+ return
96
+
97
+ if json_output:
98
+ import json as _json
99
+
100
+ health_score = brain.get("health_score", {}) if brain else {}
101
+ stack = (brain or {}).get("stack", {}) or {}
102
+ con.print(
103
+ _json.dumps(
104
+ {
105
+ "mode": mode,
106
+ "queue_mode": qmode,
107
+ "queue": qstats,
108
+ "health_score": health_score,
109
+ "brain": {
110
+ "ready": bool(brain and not brain.get("stale") and not brain.get("error")),
111
+ "stale": bool((brain or {}).get("stale")),
112
+ "file_count": (brain or {}).get("file_count", 0),
113
+ "route_count": (brain or {}).get("route_count", 0),
114
+ "frameworks": [f.get("name", "") for f in stack.get("frameworks", [])[:3]],
115
+ },
116
+ },
117
+ indent=2,
118
+ default=str,
119
+ )
120
+ )
121
+ return
122
+
123
+ con.print()
124
+
125
+ # ── Build info sections ────────────────────────────────────────────────────
126
+
127
+ # Health score
128
+ health_score = brain.get("health_score", {})
129
+ health_total = health_score.get("total")
130
+ health_grade = health_score.get("grade", "")
131
+
132
+ if health_total is not None:
133
+ health_bar = _health_bar(health_total)
134
+ comps = health_score.get("components", {})
135
+ health_detail = (
136
+ f"security {comps.get('security', 0):.0f} "
137
+ f"tests {comps.get('test_coverage', 0):.0f} "
138
+ f"dead {comps.get('dead_code', 0):.0f} "
139
+ f"deps {comps.get('dependency', 0):.0f} "
140
+ f"contract {comps.get('contract', 0):.0f}"
141
+ )
142
+ else:
143
+ health_bar = "[dim]No scan yet[/dim]"
144
+ health_detail = "Run [bold]p scan[/bold] to compute"
145
+
146
+ # Brain
147
+ if not brain:
148
+ brain_line = "[dim]No scan yet — run [bold]p scan[/bold][/dim]"
149
+ elif brain.get("stale"):
150
+ reason = brain.get("stale_reason", "Files changed since last scan")
151
+ brain_line = f"[yellow]Stale[/yellow] — {reason}"
152
+ elif brain.get("error"):
153
+ brain_line = f"[red]Error:[/red] {brain.get('error', '')}"
154
+ else:
155
+ fc = brain.get("file_count", 0)
156
+ rc = brain.get("route_count", 0)
157
+ fw = ""
158
+ stack = brain.get("stack", {})
159
+ if stack and stack.get("frameworks"):
160
+ fw_names = [f.get("name", "") for f in stack["frameworks"][:3]]
161
+ fw = f" · {', '.join(fw_names)}"
162
+ brain_line = f"[#4ADE80]Ready[/#4ADE80] — {fc} files · {rc} routes{fw}"
163
+
164
+ # Mode
165
+ mode_colors = {"confirm": "#60A5FA", "auto": "#FACC15", "autopilot": "#4ADE80"}
166
+ mode_icons = {"confirm": "🔒", "auto": "⚡", "autopilot": "🤖"}
167
+ mc = mode_colors.get(mode.value, "dim")
168
+ mi = mode_icons.get(mode.value, "")
169
+ mode_line = f"[bold {mc}]{mi} {mode.label()}[/bold {mc}]"
170
+ mode_desc = {
171
+ "confirm": "Every fix requires your approval",
172
+ "auto": "Low-risk auto-applied, risky ones ask",
173
+ "autopilot": "Full trust — Patchi decides",
174
+ }.get(mode.value, "")
175
+
176
+ # Queue
177
+ queue_paused = qstats.get("paused", False)
178
+ queue_waiting = qstats.get("waiting", 0)
179
+ queue_active = qstats.get("active", 0)
180
+
181
+ if queue_paused:
182
+ queue_line = "[yellow]Paused[/yellow]"
183
+ elif queue_active:
184
+ queue_line = f"[#C8621A]{queue_active} active[/#C8621A]"
185
+ elif queue_waiting:
186
+ queue_line = f"{queue_waiting} waiting"
187
+ else:
188
+ queue_line = "[dim]Empty[/dim]"
189
+ queue_detail = f"mode: {qmode.value}"
190
+
191
+ # AI
192
+ ai_config = config.get("ai", {})
193
+ local_model = ai_config.get("local_model_name")
194
+ keys = ai_config.get("keys", [])
195
+
196
+ if local_model:
197
+ ai_line = f"[#4ADE80]Local:[/#4ADE80] {local_model}"
198
+ ai_detail = "Offline · zero cost"
199
+ elif keys:
200
+ working = [k for k in keys if k.get("status") != "error"]
201
+ if working:
202
+ ai_line = f"[#4ADE80]{len(working)}/{len(keys)} keys active[/#4ADE80]"
203
+ else:
204
+ ai_line = f"[red]0/{len(keys)} keys active[/red]"
205
+ ai_detail = " ".join(k.get("nickname", "?") for k in keys[:4])
206
+ if len(keys) > 4:
207
+ ai_detail += f" +{len(keys) - 4} more"
208
+ else:
209
+ ai_line = "[yellow]Not configured[/yellow]"
210
+ ai_detail = "Run [bold]p key add[/bold] or [bold]p init[/bold]"
211
+
212
+ # ── Render ─────────────────────────────────────────────────────────────────
213
+
214
+ table = Table(show_header=False, box=None, pad_edge=False, padding=(0, 2))
215
+ table.add_column("Key", style="bold #F2EDD6", width=14)
216
+ table.add_column("Value", min_width=30)
217
+ table.add_column("Detail", style="dim")
218
+
219
+ table.add_row("Health", Text.from_markup(health_bar), health_detail)
220
+ table.add_row("Brain", Text.from_markup(brain_line), "")
221
+ table.add_row("Mode", Text.from_markup(mode_line), mode_desc)
222
+ table.add_row("Queue", Text.from_markup(queue_line), queue_detail)
223
+ table.add_row("AI", Text.from_markup(ai_line), ai_detail)
224
+
225
+ panel_title = "[bold #C8621A]Patchi Status[/bold #C8621A]"
226
+ if health_grade:
227
+ panel_title += f" [dim]{health_grade}[/dim]"
228
+
229
+ con.print(
230
+ Panel(
231
+ table,
232
+ title=panel_title,
233
+ border_style="#2A3D28",
234
+ padding=(0, 1),
235
+ )
236
+ )
237
+ con.print()
238
+
239
+
240
+ def _run_health(root: Path, json_output: bool) -> None:
241
+ """Detailed health breakdown (was p health)."""
242
+ from patchi.core import health as hm
243
+ from patchi.core import memory as mem
244
+
245
+ brain = mem.get_brain(root)
246
+ if not brain or not brain.get("file_count"):
247
+ if json_output:
248
+ import json as _json
249
+ con.print(_json.dumps({"error": "no scan data yet"}))
250
+ else:
251
+ con.print("[yellow]No scan data yet. Run 'p scan' first.[/yellow]")
252
+ return
253
+
254
+ score = hm.compute(root)
255
+ if json_output:
256
+ import json as _json
257
+ from dataclasses import asdict, is_dataclass
258
+
259
+ payload = (
260
+ asdict(score)
261
+ if is_dataclass(score)
262
+ else (score.__dict__ if hasattr(score, "__dict__") else {"score": str(score)})
263
+ )
264
+ con.print(_json.dumps(payload, indent=2, default=str))
265
+ return
266
+
267
+ _show_health(score, brain, root)
268
+
269
+
270
+ def _show_health(score, brain: dict, root: Path) -> None:
271
+ """Render the detailed health breakdown."""
272
+ breakdown = score.breakdown or {}
273
+ components = score.to_dict().get("components", {})
274
+
275
+ con.print()
276
+ con.print(
277
+ f"[bold #C8621A]Project Health[/bold #C8621A] "
278
+ f"[dim]grade {score.grade} · {score.total}/100[/dim]"
279
+ )
280
+
281
+ # ── Score bar ─────────────────────────────────────────────────────────────
282
+ bar_width = 40
283
+ filled = int(score.total / 100 * bar_width)
284
+ empty = bar_width - filled
285
+ bar_fill = "█" * filled
286
+ bar_empty = "░" * empty
287
+ con.print(f" [{score.color}]{bar_fill}[/{score.color}][dim]{bar_empty}[/dim]")
288
+ con.print()
289
+
290
+ # ── Component scores ──────────────────────────────────────────────────────
291
+ table = Table(show_header=True, header_style="bold #C8621A", box=None, pad_edge=False)
292
+ table.add_column("Component", style="bold #F2EDD6", width=20)
293
+ table.add_column("Score", justify="right", width=8)
294
+ table.add_column("Weight", justify="right", width=8)
295
+ table.add_column("Weighted", justify="right", width=10)
296
+ table.add_column("Detail", style="dim", width=50)
297
+
298
+ weight_map = {
299
+ "security": 0.35,
300
+ "test_coverage": 0.25,
301
+ "dead_code": 0.20,
302
+ "dependency": 0.10,
303
+ "contract": 0.10,
304
+ }
305
+
306
+ for comp_key, raw_score in components.items():
307
+ comp_label = comp_key.replace("_", " ").title()
308
+ w = weight_map.get(comp_key, 0)
309
+ weighted = round(raw_score * w, 1)
310
+
311
+ color = "#4ADE80" if raw_score >= 80 else ("#FACC15" if raw_score >= 50 else "#FF4D6D")
312
+ detail = _detail_for(comp_key, breakdown)
313
+
314
+ table.add_row(
315
+ comp_label,
316
+ Text(f"{raw_score:.0f}", style=color),
317
+ f"{w * 100:.0f}%",
318
+ Text(f"{weighted:.1f}", style="dim"),
319
+ detail,
320
+ )
321
+
322
+ con.print(table)
323
+ con.print()
324
+
325
+ # ── Project snapshot ───────────────────────────────────────────────────────
326
+ scans = breakdown.get("agents_run", [])
327
+ if scans:
328
+ file_count = breakdown.get("file_count", 0)
329
+ route_count = breakdown.get("route_count", 0)
330
+ fw = breakdown.get("framework", "Unknown")
331
+ circular = breakdown.get("circular_deps", 0)
332
+ patches = breakdown.get("patches_applied", 0)
333
+ test_pct = breakdown.get("test_coverage_pct", 0)
334
+
335
+ con.print("[bold]Project snapshot[/bold]")
336
+ con.print(
337
+ f" [dim]Files:[/dim] {file_count} [dim]Routes:[/dim] {route_count} "
338
+ f"[dim]Framework:[/dim] {fw}"
339
+ )
340
+ con.print(
341
+ f" [dim]Circular deps:[/dim] {circular} [dim]Tests:[/dim] {test_pct:.0f}% coverage "
342
+ f"[dim]Patches:[/dim] {patches}"
343
+ )
344
+ con.print()
345
+
346
+ # ── Active security domains ───────────────────────────────────────────────
347
+ active_domains = brain.get("active_security_domains", [])
348
+ if active_domains:
349
+ con.print("[bold]Active Security Domains[/bold]")
350
+ con.print(f" [dim]{', '.join(active_domains)}[/dim]")
351
+ con.print()
352
+
353
+ # ── Findings summary from scan results ────────────────────────────────────
354
+ scans = _get_scan_results_summary(root)
355
+ if scans["total"] > 0:
356
+ con.print("[bold #FF4D6D]Recent Scan Findings[/bold #FF4D6D]")
357
+ con.print(
358
+ f" [dim]{scans['total']} total, "
359
+ f"{scans['critical']} critical, {scans['high']} high, "
360
+ f"{scans['medium']} medium, {scans['low']} low[/dim]"
361
+ )
362
+ con.print()
363
+
364
+ # ── Grade scale ────────────────────────────────────────────────────────
365
+ con.print(
366
+ "[dim]Grade scale: [bold #4ADE80]A[/bold #4ADE80] 90–100 "
367
+ "[bold #86EFAC]B[/bold #86EFAC] 70–89 [bold #FACC15]C[/bold #FACC15] 50–69 "
368
+ "[bold #FB923C]D[/bold #FB923C] 30–49 [bold #FF4D6D]F[/bold #FF4D6D] 0–29[/dim]"
369
+ )
370
+ con.print()
371
+
372
+ # ── Actions ────────────────────────────────────────────────────────────────
373
+ actions = []
374
+ if components.get("security", 100) < 70:
375
+ actions.append("[dim]→ Run [bold]p scan[/bold] to scan for vulnerabilities[/dim]")
376
+ if components.get("test_coverage", 100) < 50:
377
+ actions.append("[dim]→ Run [bold]p test[/bold] to check test coverage[/dim]")
378
+ if components.get("dead_code", 100) < 70:
379
+ actions.append("[dim]→ Run [bold]p fix[/bold] to remove dead code[/dim]")
380
+ if components.get("contract", 100) < 60:
381
+ actions.append(
382
+ "[dim]→ Run [bold]p scan[/bold] then [bold]p contract confirm[/bold]"
383
+ " to confirm flows[/dim]"
384
+ )
385
+
386
+ if actions:
387
+ con.print("[bold]Suggested Actions[/bold]")
388
+ for a in actions:
389
+ con.print(f" {a}")
390
+ con.print()
391
+
392
+
393
+ def _detail_for(component: str, breakdown: dict) -> str:
394
+ """Return a detail string for a health component."""
395
+
396
+ def _count(v):
397
+ return v if isinstance(v, int) else len(v or [])
398
+
399
+ details = {
400
+ "security": f"{breakdown.get('file_count', 0)} files, route scan active",
401
+ "test_coverage": f"{breakdown.get('test_coverage_pct', 0):.0f}% of source files have tests",
402
+ "dead_code": f"{_count(breakdown.get('circular_deps', 0))} circular chains",
403
+ "dependency": f"{_count(breakdown.get('patches_applied', 0))} applied patches"
404
+ if _count(breakdown.get("patches_applied", 0))
405
+ else "dependency scan active",
406
+ "contract": f"{_count(breakdown.get('agents_run', []))} agents available",
407
+ }
408
+ return details.get(component, "")
409
+
410
+
411
+ def _get_scan_results_summary(root: Path) -> dict:
412
+ """Aggregate finding counts from scan results memory."""
413
+ from patchi.core import memory as mem
414
+
415
+ try:
416
+ scans = mem.get_scan_results(root)
417
+ except Exception as e:
418
+ _log.warning("_get_scan_results_summary failed: %s", e)
419
+ return {"total": 0, "critical": 0, "high": 0, "medium": 0, "low": 0}
420
+
421
+ total = critical = high = medium = low = 0
422
+ for _agent_name, data in scans.items():
423
+ findings = data.get("findings", [])
424
+ total += len(findings)
425
+ for f in findings:
426
+ sev = f.get("severity", "info")
427
+ if sev == "critical":
428
+ critical += 1
429
+ elif sev == "high":
430
+ high += 1
431
+ elif sev == "medium":
432
+ medium += 1
433
+ elif sev == "low":
434
+ low += 1
435
+
436
+ return {"total": total, "critical": critical, "high": high, "medium": medium, "low": low}
437
+
438
+
439
+ def _run_doctor(root: Path, json_output: bool = False, verbose: bool = False) -> None:
440
+ """System validation checks (was p doctor)."""
441
+ import importlib
442
+ import shutil
443
+ import sys
444
+
445
+ if not json_output:
446
+ con.print()
447
+ con.print("[bold #C8621A]Patchi Doctor[/bold #C8621A] [dim]system health check[/dim]")
448
+ con.print()
449
+
450
+ checks: list[tuple[str, str, str, str]] = [] # (label, status, note, color)
451
+ warnings = 0
452
+ errors = 0
453
+
454
+ # ── 1. Python version ─────────────────────────────────────────────────────
455
+ pyver = sys.version_info
456
+ if pyver >= (3, 11):
457
+ checks.append(("Python", "✓", f"{pyver.major}.{pyver.minor}.{pyver.micro}", "#4ADE80"))
458
+ else:
459
+ checks.append(("Python", "✗", f"{pyver.major}.{pyver.minor} — requires ≥ 3.11", "#FF4D6D"))
460
+ errors += 1
461
+
462
+ # ── 2. Required dependencies ──────────────────────────────────────────────
463
+ _REQUIRED = [
464
+ ("rich", "rich", "Terminal UI rendering"),
465
+ ("watchfiles", "watchfiles", "File watcher (p watch)"),
466
+ ("vulture", "vulture", "Dead code detection"),
467
+ ("httpx", "httpx", "HTTP client (AI requests)"),
468
+ ("fastapi", "fastapi", "Web dashboard backend"),
469
+ ("uvicorn", "uvicorn", "Web dashboard ASGI server"),
470
+ ("psutil", "psutil", "Process + system metrics"),
471
+ ("yaml", "pyyaml", "YAML config parsing"),
472
+ ("loguru", "loguru", "Structured logging"),
473
+ ]
474
+
475
+ for import_name, pip_name, desc in _REQUIRED:
476
+ try:
477
+ importlib.import_module(import_name)
478
+ checks.append((f"dep: {pip_name}", "✓", desc, "#4ADE80"))
479
+ except ImportError:
480
+ checks.append((f"dep: {pip_name}", "✗", f"Missing — pip install {pip_name}", "#FF4D6D"))
481
+ errors += 1
482
+
483
+ # ── 3. Project root ───────────────────────────────────────────────────────
484
+ from patchi.core.config import find_project_root
485
+
486
+ root_found = find_project_root()
487
+ if root_found:
488
+ checks.append(("Project root", "✓", str(root_found), "#4ADE80"))
489
+ else:
490
+ checks.append(("Project root", "⚠", "Not found — run 'p init' first", "#FACC15"))
491
+ warnings += 1
492
+
493
+ # ── 4. API keys ───────────────────────────────────────────────────────────
494
+ try:
495
+ from patchi.core import config as cfg
496
+
497
+ config = cfg.load(root)
498
+ ai_keys = config.get("ai", {}).get("keys", [])
499
+ if ai_keys:
500
+ checks.append(("API keys", "✓", f"{len(ai_keys)} key(s) configured", "#4ADE80"))
501
+ else:
502
+ checks.append(("API keys", "⚠", "No keys — run 'p key add'", "#FACC15"))
503
+ warnings += 1
504
+ except Exception as e:
505
+ checks.append(("API keys", "✗", str(e)[:60], "#FF4D6D"))
506
+ errors += 1
507
+
508
+ # ── 5. Local model (Ollama) ───────────────────────────────────────────────
509
+ try:
510
+ from patchi.core import config as cfg
511
+
512
+ local_model = cfg.load(root).get("ai", {}).get("local_model_name")
513
+ if local_model:
514
+ checks.append((f"Ollama: {local_model}", "✓", "Configured", "#4ADE80"))
515
+ else:
516
+ checks.append(("Ollama", "—", "No local model configured (optional)", "#6B7280"))
517
+ except Exception as _exc:
518
+ _log.warning('_run_doctor failed: %s', _exc)
519
+
520
+ # ── 6. Optional: test tooling ─────────────────────────────────────────────
521
+ con.print()
522
+ _OPTIONAL_TEST = [
523
+ ("pytest", "pytest", "pytest", "Unit test runner"),
524
+ ("npx", "", "node/npm", "Playwright host"),
525
+ ("locust", "locust", "locust", "Stress test runner"),
526
+ ]
527
+ for cmd, import_name, pip_name, desc in _OPTIONAL_TEST:
528
+ try:
529
+ on_path = shutil.which(cmd) is not None
530
+ importable = import_name and importlib.import_module(import_name)
531
+ present = on_path or bool(importable)
532
+ except ImportError:
533
+ present = False
534
+ status = "✓" if present else "—"
535
+ color = "#4ADE80" if present else "#6B7280"
536
+ note = desc if present else f"Optional — {pip_name}"
537
+ checks.append((f"[dim]opt:[/dim] {cmd}", status, note, color))
538
+
539
+ # ── 7. Optional: security tooling ────────────────────────────────────────
540
+ try:
541
+ from patchi.core.agents.tool_health import check_tool
542
+
543
+ _OPTIONAL_SECURITY = [
544
+ ("bandit", "bandit", "bandit", "Python security linter"),
545
+ ("semgrep", "", "semgrep", "Multi-language SAST scanner"),
546
+ ]
547
+ for cmd, _import_name, pip_name, desc in _OPTIONAL_SECURITY:
548
+ try:
549
+ st = check_tool(cmd)
550
+ if st["status"] == "ok":
551
+ note = f"{desc} ({st['version']})"
552
+ checks.append((
553
+ f"[dim]opt:[/dim] {cmd}", "✓", note, "#4ADE80"
554
+ ))
555
+ elif st["status"] == "broken":
556
+ hint = st["hint"]
557
+ checks.append((
558
+ f"[dim]opt:[/dim] {cmd}", "✗", f"Broken: {hint}", "#FF4D6D"
559
+ ))
560
+ errors += 1
561
+ else:
562
+ note = f"Optional — {pip_name}"
563
+ checks.append((
564
+ f"[dim]opt:[/dim] {cmd}", "—", note, "#6B7280"
565
+ ))
566
+ except Exception:
567
+ note = f"Optional — {pip_name}"
568
+ checks.append((
569
+ f"[dim]opt:[/dim] {cmd}", "—", note, "#6B7280"
570
+ ))
571
+ except ImportError:
572
+ pass
573
+
574
+ # ── 8. .patchi/ size warning ─────────────────────────────────────────────
575
+ try:
576
+ from patchi.cli.commands.cleanup_cmd import get_patchi_size
577
+
578
+ size_info = get_patchi_size(root)
579
+ total_bytes = size_info["total_bytes"]
580
+ total_files = size_info["total_files"]
581
+
582
+ mb = total_bytes / (1024 * 1024)
583
+ if total_bytes > 50 * 1024 * 1024:
584
+ size_str = f"{mb:.1f} MB"
585
+ warn_msg = f"{size_str} ({total_files} files) — consider 'p cleanup'"
586
+ checks.append((".patchi/ size", "⚠", warn_msg, "#FACC15"))
587
+ warnings += 1
588
+ else:
589
+ if total_bytes >= 1024 * 1024:
590
+ size_str = f"{mb:.1f} MB"
591
+ else:
592
+ size_str = f"{total_bytes / 1024:.1f} KB"
593
+ info_msg = f"{size_str} ({total_files} files)"
594
+ checks.append((".patchi/ size", "✓", info_msg, "#4ADE80"))
595
+ except Exception as _exc:
596
+ _log.warning('_run_doctor failed: %s', _exc)
597
+
598
+ # ── 9. Stale commands check ────────────────────────────────────────────────
599
+ try:
600
+ from patchi.cli.registry import COMMANDS
601
+
602
+ stale_commands = {
603
+ "brain": "Merged into 'p agents list --brain'",
604
+ "smart": "Merged into 'p chat --stream'",
605
+ "health": "Merged into 'p status --deep'",
606
+ "doctor": "Merged into 'p status --validate'",
607
+ "security": "Merged into 'p scan'",
608
+ "ignore": "Merged into 'p memory ignore'",
609
+ }
610
+
611
+ cmd_names = [cmd.name for cmd in COMMANDS]
612
+ for name, suggestion in stale_commands.items():
613
+ if name not in cmd_names:
614
+ checks.append((f"stale: {name}", "⚠", f"Removed — {suggestion}", "#FACC15"))
615
+ warnings += 1
616
+
617
+ if not any(name not in cmd_names for name in stale_commands):
618
+ checks.append(("command hygiene", "✓", "No stale commands found", "#4ADE80"))
619
+ except Exception as _exc:
620
+ _log.warning('_run_doctor failed: %s', _exc)
621
+
622
+ # ── Render results ────────────────────────────────────────────────────────
623
+ if json_output:
624
+ import json as _json
625
+
626
+ con.print(
627
+ _json.dumps(
628
+ {
629
+ "ok": errors == 0,
630
+ "errors": errors,
631
+ "warnings": warnings,
632
+ "checks": [
633
+ {"label": label, "status": status.strip(), "note": note}
634
+ for label, status, note, _color in checks
635
+ ],
636
+ },
637
+ indent=2,
638
+ )
639
+ )
640
+ return
641
+
642
+ table = Table(show_header=False, box=None, pad_edge=False, padding=(0, 1))
643
+ table.add_column("Status", width=4)
644
+ table.add_column("Check", style="bold #F2EDD6", width=28)
645
+ table.add_column("Note", style="dim", width=55)
646
+
647
+ for label, status, note, color in checks:
648
+ table.add_row(Text(status, style=color), label, note)
649
+
650
+ con.print(table)
651
+
652
+ # ── Summary ───────────────────────────────────────────────────────────────
653
+ con.print()
654
+ if errors == 0 and warnings == 0:
655
+ con.print(
656
+ Panel(
657
+ "[bold #4ADE80]All checks passed. Patchi is ready.[/bold #4ADE80]",
658
+ border_style="#4ADE80",
659
+ padding=(0, 1),
660
+ )
661
+ )
662
+ elif errors == 0:
663
+ con.print(
664
+ Panel(
665
+ f"[bold #FACC15]{warnings} warning(s). Core functionality OK.[/bold #FACC15]",
666
+ border_style="#FACC15",
667
+ padding=(0, 1),
668
+ )
669
+ )
670
+ else:
671
+ con.print(
672
+ Panel(
673
+ f"[bold #FF4D6D]{errors} error(s), {warnings} warning(s)"
674
+ f". Fix errors above.[/bold #FF4D6D]",
675
+ border_style="#FF4D6D",
676
+ padding=(0, 1),
677
+ )
678
+ )
679
+ con.print()