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,232 @@
1
+ """
2
+ P-Check Agent — sole job to get app into runnable, testable state.
3
+
4
+ Spec:
5
+ 1. Install all dependencies
6
+ 2. Run formatting/lint checks
7
+ 3. Run other required setup (env files, codegen, config validation)
8
+ 4. Attempt to build and start the app as local dev server (localhost)
9
+
10
+ When everything succeeds: output READY_TO_SERVE with exact local dev server URL and port. Stop.
11
+ When something fails: flag what failed + exact error + what is needed, escalate to Brain, wait for fix, re-run from step 1. Never READY_TO_SERVE while blocking error remains.
12
+ Hard rule: No other agent may treat app as usable until READY_TO_SERVE.
13
+
14
+ Gate file: .patchi/p_check_status.json {status, url, port, timestamp, error}
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import logging
21
+ import time
22
+ from pathlib import Path
23
+
24
+ from rich.panel import Panel
25
+
26
+ from patchi.cli.console import con
27
+ from patchi.core.config import require_project_root
28
+
29
+ _STATUS_FILE = ".patchi/p_check_status.json"
30
+
31
+
32
+ def _write_status(root: Path, status: str, url: str | None = None, port: int | None = None, error: str | None = None, domain: str | None = None) -> None:
33
+ p = root / _STATUS_FILE
34
+ p.parent.mkdir(parents=True, exist_ok=True)
35
+ data = {"status": status, "url": url, "port": port, "error": error, "domain": domain, "timestamp": time.time(), "iso": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}
36
+ tmp = p.with_suffix(".tmp")
37
+ tmp.write_text(json.dumps(data, indent=2), encoding="utf-8")
38
+ tmp.replace(p)
39
+
40
+
41
+ def _read_status(root: Path) -> dict | None:
42
+ p = root / _STATUS_FILE
43
+ if not p.exists():
44
+ return None
45
+ try:
46
+ return json.loads(p.read_text(encoding="utf-8"))
47
+ except Exception:
48
+ return None
49
+
50
+
51
+ def is_ready(root: Path) -> tuple[bool, dict | None]:
52
+ """Gate Rule helper: returns (ready, status_dict)."""
53
+ st = _read_status(root)
54
+ if not st:
55
+ return False, None
56
+ if st.get("status") != "READY_TO_SERVE":
57
+ return False, st
58
+ # optional TTL: 1 hour? For now no expiry, but check url still reachable?
59
+ if not st.get("url"):
60
+ return False, st
61
+ # check timestamp within 1 hour or still running?
62
+ return True, st
63
+
64
+
65
+ # Finding types p check --fix may auto-repair (its lane: type/deps/build/format/install).
66
+ # Everything else (incl. all security findings) is reported for the Brain, never touched.
67
+ _FIXABLE_TYPES = {"missing_env", "format_error"}
68
+
69
+
70
+ def _apply_domain_fixes(r: Path, blocking: list) -> list[str]:
71
+ """Attempt safe auto-fixes for in-lane findings. Returns human-readable actions taken."""
72
+ import shutil
73
+ import subprocess
74
+
75
+ actions: list[str] = []
76
+ for f in blocking:
77
+ ftype = getattr(f, "type", "") or ""
78
+ if ftype == "missing_env":
79
+ src, dst = r / ".env.example", r / ".env"
80
+ try:
81
+ if src.exists() and not dst.exists():
82
+ dst.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
83
+ actions.append("copied .env.example → .env (fill values before run)")
84
+ except OSError:
85
+ pass
86
+ elif ftype == "format_error":
87
+ if shutil.which("ruff") and (r / "pyproject.toml").exists():
88
+ try:
89
+ proc = subprocess.run(
90
+ ["ruff", "format", "."],
91
+ capture_output=True, text=True, timeout=60, cwd=str(r),
92
+ )
93
+ if proc.returncode == 0:
94
+ actions.append("ran ruff format .")
95
+ except Exception as _exc:
96
+ logging.getLogger("patchi").debug('suppressed: %s', _exc)
97
+ return actions
98
+
99
+
100
+ def _run_side_agents(r: Path, brain: dict, config: dict):
101
+ from patchi.core.agents.base import AgentInput
102
+ from patchi.core.agents.base import list_agents as la
103
+
104
+ agents = [a for a in la() if a.name in ("InstallAgent", "BuildAgent", "FormatAgent")]
105
+ findings: list = []
106
+ blocking: list = []
107
+ for cls in agents:
108
+ res = cls().run(AgentInput(root=r, scope=[], brain=brain, config=config, extra={}))
109
+ findings.extend(res.findings)
110
+ for f in res.findings:
111
+ sev = f.severity.value if hasattr(f.severity, "value") else str(f.severity)
112
+ if sev in ("high", "critical"):
113
+ blocking.append(f)
114
+ return findings, blocking
115
+
116
+
117
+ def run(fix: bool = False, json_output: bool = False, root: Path | None = None) -> None:
118
+ try:
119
+ r = root or require_project_root()
120
+ except RuntimeError as e:
121
+ con.print(f"[red]{e}[/red]")
122
+ return
123
+
124
+ from patchi.core.agents.base import discover_agent_modules
125
+
126
+ discover_agent_modules()
127
+ import patchi.core.agents.side.build_agent # noqa: F401
128
+ import patchi.core.agents.side.format_agent # noqa: F401
129
+ import patchi.core.agents.side.install_agent # noqa: F401
130
+
131
+ try:
132
+ from patchi.core import config as cfg
133
+ from patchi.core import memory as mem
134
+
135
+ brain = mem.get_brain(r)
136
+ config = cfg.load(r)
137
+ except Exception:
138
+ brain, config = {}, {}
139
+
140
+ # Step 1-3: run side agents
141
+ findings, blocking = _run_side_agents(r, brain, config)
142
+
143
+ # --fix: repair in-lane findings once, then re-run from step 1
144
+ if fix and blocking:
145
+ lane = [f for f in blocking if (getattr(f, "type", "") or "") in _FIXABLE_TYPES]
146
+ if lane:
147
+ actions = _apply_domain_fixes(r, lane)
148
+ if actions:
149
+ for a in actions:
150
+ con.print(f"[dim]--fix: {a}[/dim]")
151
+ findings, blocking = _run_side_agents(r, brain, config)
152
+
153
+ # Step 3 extra: env file check (missing .env but .env.example exists)
154
+ try:
155
+ if (r / ".env.example").exists() and not (r / ".env").exists():
156
+ from patchi.core.agents.base import Severity, make_finding
157
+
158
+ findings.append(
159
+ make_finding(
160
+ severity=Severity.HIGH,
161
+ file=".env",
162
+ line_start=0,
163
+ title="Missing .env (have .env.example)",
164
+ description="Copy .env.example to .env and fill values before app can run",
165
+ finding_type="missing_env",
166
+ )
167
+ )
168
+ blocking.append(findings[-1])
169
+ except Exception as _exc:
170
+ logging.getLogger("patchi").debug('suppressed: %s', _exc)
171
+
172
+ if blocking:
173
+ # Escalate to Brain
174
+ msg = f"P-Check BLOCKED: {len(blocking)} blocking error(s) in {', '.join({f.file or 'unknown' for f in blocking})}"
175
+ con.print()
176
+ con.print(Panel(f"[red]{msg}[/red]\n" + "\n".join(f" • {f.file}:{f.line} {f.message[:100]}" for f in blocking[:5]), title="P-Check — BLOCKED", border_style="#FF4D6D"))
177
+ con.print(f"[dim]Escalating to Brain (orchestrator) — domain: {[f.type for f in blocking[:3]]}[/dim]")
178
+ _write_status(r, "BLOCKED", error=msg, domain=",".join({f.type for f in blocking}))
179
+ try:
180
+ from patchi.core import memory as mem
181
+
182
+ mem.save_scan_result("PCheck", {"status": "BLOCKED", "findings": [f.to_dict() for f in findings]}, r)
183
+ # also save as issue for brain
184
+ for f in blocking[:5]:
185
+ mem.save_issue({"type": f.type, "file": f.file, "line": f.line, "message": f.message, "source": "PCheck"}, r)
186
+ except Exception as _exc:
187
+ logging.getLogger("patchi").debug('suppressed: %s', _exc)
188
+ con.print("[yellow]P-Check: fix blocking errors (or run p check --fix if in domain) then re-run `p check` from step 1. Never READY_TO_SERVE while blocked.[/yellow]")
189
+ con.print()
190
+ if json_output:
191
+ con.print(json.dumps({"status": "BLOCKED", "blocking": [f.to_dict() for f in blocking]}, indent=2))
192
+ return
193
+
194
+ # Step 4: attempt to build and start app as local dev server
195
+ url: str | None = None
196
+ port: int | None = None
197
+ try:
198
+ from patchi.core.testing.app_launcher import ensure_running
199
+
200
+ url = ensure_running(r, config, {})
201
+ if url:
202
+ # parse port
203
+ try:
204
+ port = int(url.rsplit(":", 1)[-1].split("/")[0])
205
+ except Exception:
206
+ port = None
207
+ except Exception as exc: # noqa: BLE001
208
+ con.print(f"[red]P-Check start failed: {exc}[/red]")
209
+ _write_status(r, "BLOCKED", error=str(exc), domain="app_start")
210
+ return
211
+
212
+ if not url:
213
+ msg = "P-Check could not start app — no start command detected or port health check failed"
214
+ con.print()
215
+ con.print(Panel(f"[yellow]{msg}[/yellow]\n[dim]Check app_launcher logs at .patchi/launcher/app.log[/dim]", title="P-Check — BLOCKED (no URL)", border_style="#FF8C42"))
216
+ con.print()
217
+ _write_status(r, "BLOCKED", error=msg, domain="app_start")
218
+ return
219
+
220
+ # Success
221
+ con.print()
222
+ con.print(Panel(f"[bold #4ADE80]READY_TO_SERVE[/bold #4ADE80]\nLocal dev server URL: [bold]{url}[/bold] port {port}\n[dim]P-Check complete — app is runnable, testable. No other agent may proceed until this was emitted.[/dim]", border_style="#4ADE80"))
223
+ con.print()
224
+ _write_status(r, "READY_TO_SERVE", url=url, port=port)
225
+ try:
226
+ from patchi.core import memory as mem
227
+
228
+ mem.save_scan_result("PCheck", {"status": "READY_TO_SERVE", "url": url, "port": port, "findings": [f.to_dict() for f in findings]}, r)
229
+ except Exception as _exc:
230
+ logging.getLogger("patchi").debug('suppressed: %s', _exc)
231
+ if json_output:
232
+ con.print(json.dumps({"status": "READY_TO_SERVE", "url": url, "port": port}, indent=2))
@@ -0,0 +1,328 @@
1
+ """
2
+ `p cleanup` — Remove stale .patchi/ artifacts.
3
+
4
+ Cleans up generated/temporary files while preserving essential state:
5
+ - JUnit XML reports from dev checks
6
+ - Flake databases (patchi_history.db, *.db-journal, etc.)
7
+ - Stale caches (AST, file_info, brain freshness, CVE, etc.)
8
+ - Old log files
9
+ - Ephemeral snapshots
10
+ - Browser test screenshots/evidence (optional)
11
+ - Profiling/stats files (.stats, .prof, .lprof)
12
+
13
+ Preserves:
14
+ - config.json (project configuration)
15
+ - memory/ (charter, layers, known issues, brain state)
16
+ - api_key
17
+ - queue.json
18
+
19
+ Usage:
20
+ p cleanup # Preview what would be removed (dry-run)
21
+ p cleanup --apply # Actually delete stale files
22
+ p cleanup --all # Also remove evidence/screenshots
23
+ p cleanup --older-than 7d # Only remove files older than 7 days
24
+ p cleanup --dry-run --json # JSON output for CI integration
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import logging
30
+ import time
31
+ from pathlib import Path
32
+
33
+ from patchi.cli.console import con
34
+ from patchi.core.config import find_project_root
35
+
36
+ _log = logging.getLogger("patchi.cli.commands.cleanup_cmd")
37
+
38
+
39
+ # ── Artifact Patterns ────────────────────────────────────────────────────────
40
+ # (glob_pattern, description, keep_in_safe_mode)
41
+
42
+ CLEANUP_TARGETS = [
43
+ # JUnit XML reports
44
+ ("**/*junit*.xml", "JUnit test reports", False),
45
+ ("**/*test-result*.xml", "pytest result files", False),
46
+ ("**/dev-check-*.xml", "dev-check XML reports", False),
47
+
48
+ # Flake/test databases
49
+ ("**/*.db", "database files", False),
50
+ ("**/*.db-journal", "database journals", False),
51
+ ("**/*.db-wal", "database WAL files", False),
52
+ ("**/*.db-shm", "database SHM files", False),
53
+
54
+ # Stale caches
55
+ ("ast_cache.json", "AST parse cache", False),
56
+ ("file_info_cache.json", "file info cache", False),
57
+ ("brain_freshness.json", "brain freshness cache", False),
58
+ ("diff_activation_cache.json", "diff activation cache", False),
59
+ ("threat_model.json", "threat model cache", False),
60
+ ("queue.json", "queue state", False),
61
+ ("cache/cve_cache.json", "CVE lookup cache", False),
62
+
63
+ # Logs
64
+ ("logs/*.log", "log files", False),
65
+ ("logs/*.jsonl", "structured logs", False),
66
+
67
+ # Snapshots
68
+ ("snapshots/**/*", "ephemeral snapshots", False),
69
+
70
+ # Profiling
71
+ ("*.stats", "profiling stats", False),
72
+ ("*.prof", "profiling data", False),
73
+ ("*.lprof", "line profiling data", False),
74
+
75
+ # Scratch files
76
+ ("b.json", "scratch data", False),
77
+ ("command_list.md", "generated command list", False),
78
+
79
+ # Evidence (only with --all)
80
+ ("evidence/dast/**/*", "DAST screenshots/evidence", True),
81
+ ("evidence/browser_tests/**/*", "browser test evidence", True),
82
+ ("evidence/test/**/*", "test evidence", True),
83
+ ]
84
+
85
+
86
+ def _parse_age(age_str: str) -> float:
87
+ """Parse age string like '7d', '24h', '30m' into seconds."""
88
+ age_str = age_str.strip().lower()
89
+ if age_str.endswith("d"):
90
+ return float(age_str[:-1]) * 86400
91
+ elif age_str.endswith("h"):
92
+ return float(age_str[:-1]) * 3600
93
+ elif age_str.endswith("m"):
94
+ return float(age_str[:-1]) * 60
95
+ else:
96
+ # Assume days
97
+ return float(age_str) * 86400
98
+
99
+
100
+ def _is_file_older_than(file_path: Path, max_age_seconds: float) -> bool:
101
+ """Check if a file is older than the specified age."""
102
+ try:
103
+ mtime = file_path.stat().st_mtime
104
+ age = time.time() - mtime
105
+ return age > max_age_seconds
106
+ except Exception:
107
+ return False
108
+
109
+
110
+ def cleanup(
111
+ apply: bool = False,
112
+ include_evidence: bool = False,
113
+ older_than: str | None = None,
114
+ json_output: bool = False,
115
+ root: Path | None = None,
116
+ ):
117
+ """Remove stale .patchi/ artifacts.
118
+
119
+ Args:
120
+ apply: If True, actually delete files. If False, just show what would be removed.
121
+ include_evidence: If True, also remove evidence/screenshots.
122
+ older_than: Only remove files older than this (e.g. '7d', '24h', '30m').
123
+ json_output: If True, output as JSON for CI integration.
124
+ root: Override project root detection.
125
+ """
126
+ try:
127
+ r = root or find_project_root()
128
+ except Exception:
129
+ try:
130
+ from patchi.core.config import require_project_root
131
+ r = require_project_root()
132
+ except Exception as e:
133
+ if json_output:
134
+ pass
135
+ else:
136
+ con.print(f"[red]{e}[/red]")
137
+ return
138
+
139
+ patchi_dir = r / ".patchi"
140
+ if not patchi_dir.is_dir():
141
+ if json_output:
142
+ pass
143
+ else:
144
+ con.print(f"[yellow]No .patchi directory found at {r}[/yellow]")
145
+ return
146
+
147
+ # Parse age filter
148
+ max_age_seconds = _parse_age(older_than) if older_than else None
149
+
150
+ if not json_output:
151
+ con.print()
152
+ con.print("[bold #C8621A]Patchi Cleanup[/bold #C8621A]")
153
+ con.print(f"[dim]Directory: {patchi_dir}[/dim]")
154
+ mode_label = "DRY RUN (use --apply to delete)" if not apply else "[red]DELETING FILES[/red]"
155
+ age_label = f" | Age filter: > {older_than}" if older_than else ""
156
+ con.print(f"[dim]{mode_label}{age_label}[/dim]")
157
+ con.print()
158
+
159
+ total_files = 0
160
+ total_size = 0
161
+ deleted_files = 0
162
+ deleted_size = 0
163
+ skipped_files = 0
164
+ filtered_out = 0
165
+ cleanup_data = []
166
+
167
+ for pattern, description, evidence_only in CLEANUP_TARGETS:
168
+ if evidence_only and not include_evidence:
169
+ continue
170
+
171
+ matches = list(patchi_dir.glob(pattern))
172
+
173
+ if not matches:
174
+ continue
175
+
176
+ # Filter to actual files (not directories)
177
+ files = [f for f in matches if f.is_file()]
178
+
179
+ if not files:
180
+ continue
181
+
182
+ # Apply age filter
183
+ if max_age_seconds is not None:
184
+ old_files = [f for f in files if _is_file_older_than(f, max_age_seconds)]
185
+ filtered_out += len(files) - len(old_files)
186
+ files = old_files
187
+
188
+ if not files:
189
+ continue
190
+
191
+ # Calculate size
192
+ file_size = sum(f.stat().st_size for f in files if f.exists())
193
+ total_files += len(files)
194
+ total_size += file_size
195
+
196
+ size_str = _format_size(file_size)
197
+ count_str = f"{len(files)} file{'s' if len(files) != 1 else ''}"
198
+
199
+ if apply:
200
+ deleted_count = 0
201
+ for f in files:
202
+ try:
203
+ file_size_actual = f.stat().st_size if f.exists() else 0
204
+ f.unlink()
205
+ deleted_count += 1
206
+ deleted_files += 1
207
+ deleted_size += file_size_actual
208
+ except Exception as e:
209
+ skipped_files += 1
210
+ if not json_output:
211
+ con.print(f" [yellow]⚠ Could not delete {f.name}: {e}[/yellow]")
212
+
213
+ if deleted_count > 0 and not json_output:
214
+ con.print(f" [red]✗[/red] {description}: {count_str} ({size_str})")
215
+ else:
216
+ if not json_output:
217
+ con.print(f" [yellow]?[/yellow] {description}: {count_str} ({size_str})")
218
+
219
+ # Collect data for JSON output
220
+ cleanup_data.append({
221
+ "description": description,
222
+ "pattern": pattern,
223
+ "count": len(files),
224
+ "size_bytes": file_size,
225
+ "files": [str(f.relative_to(patchi_dir)) for f in files[:20]], # Limit to 20
226
+ })
227
+
228
+ # Also clean empty directories
229
+ empty_dirs = []
230
+ for d in sorted(patchi_dir.rglob("*"), reverse=True):
231
+ if d.is_dir() and not any(d.iterdir()):
232
+ empty_dirs.append(d)
233
+
234
+ if apply and empty_dirs:
235
+ for d in empty_dirs:
236
+ try:
237
+ d.rmdir()
238
+ if not json_output:
239
+ con.print(f" [red]✗[/red] Empty dir: {d.relative_to(patchi_dir)}")
240
+ except Exception as _exc:
241
+ _log.warning('cleanup failed: %s', _exc)
242
+
243
+ # JSON output for CI
244
+ if json_output:
245
+ {
246
+ "dry_run": not apply,
247
+ "older_than": older_than,
248
+ "total_files": total_files,
249
+ "total_size_bytes": total_size,
250
+ "deleted_files": deleted_files,
251
+ "deleted_size_bytes": deleted_size,
252
+ "skipped_files": skipped_files,
253
+ "filtered_out": filtered_out,
254
+ "categories": cleanup_data,
255
+ "preserved": _get_preserved(patchi_dir),
256
+ }
257
+ return
258
+
259
+ # Summary
260
+ con.print()
261
+ if apply:
262
+ if deleted_files > 0:
263
+ con.print(f"[green]✓ Cleaned {deleted_files} files ({_format_size(deleted_size)} freed)[/green]")
264
+ else:
265
+ con.print("[dim]No stale files to clean.[/dim]")
266
+ if skipped_files:
267
+ con.print(f"[yellow]⚠ {skipped_files} files could not be deleted[/yellow]")
268
+ else:
269
+ con.print(f"[yellow]Would remove {total_files} files ({_format_size(total_size)})[/yellow]")
270
+ if filtered_out > 0:
271
+ con.print(f"[dim]Filtered out {filtered_out} files (newer than {older_than})[/dim]")
272
+ con.print("[dim]Run with --apply to actually delete files.[/dim]")
273
+
274
+ # Show what's preserved
275
+ con.print()
276
+ con.print("[dim]Preserved:[/dim]")
277
+ preserved = _get_preserved(patchi_dir)
278
+ if preserved:
279
+ con.print(f"[dim] {', '.join(preserved)}[/dim]")
280
+ else:
281
+ con.print("[dim] (nothing to preserve)[/dim]")
282
+ con.print()
283
+
284
+
285
+ def _get_preserved(patchi_dir: Path) -> list[str]:
286
+ """Get list of preserved items."""
287
+ preserved = []
288
+ if (patchi_dir / "config.json").exists():
289
+ preserved.append("config.json")
290
+ if (patchi_dir / "memory").is_dir():
291
+ preserved.append("memory/")
292
+ if (patchi_dir / "api_key").exists():
293
+ preserved.append("api_key")
294
+ return preserved
295
+
296
+
297
+ def get_patchi_size(root: Path | None = None) -> dict:
298
+ """Get the total size of .patchi/ directory for doctor warnings."""
299
+ try:
300
+ r = root or find_project_root()
301
+ except Exception:
302
+ return {"total_bytes": 0, "total_files": 0}
303
+
304
+ patchi_dir = r / ".patchi"
305
+ if not patchi_dir.is_dir():
306
+ return {"total_bytes": 0, "total_files": 0}
307
+
308
+ total_bytes = 0
309
+ total_files = 0
310
+ for f in patchi_dir.rglob("*"):
311
+ if f.is_file():
312
+ try:
313
+ total_bytes += f.stat().st_size
314
+ total_files += 1
315
+ except Exception as _exc:
316
+ _log.warning('get_patchi_size failed: %s', _exc)
317
+
318
+ return {"total_bytes": total_bytes, "total_files": total_files}
319
+
320
+
321
+ def _format_size(size: int) -> str:
322
+ """Format bytes as human-readable string."""
323
+ if size < 1024:
324
+ return f"{size} B"
325
+ elif size < 1024 * 1024:
326
+ return f"{size / 1024:.1f} KB"
327
+ else:
328
+ return f"{size / (1024 * 1024):.1f} MB"