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,574 @@
1
+ """
2
+ Patch applier for Patchi.
3
+
4
+ Applies a patch to disk:
5
+ 1. Take snapshot of all files to be changed (via core.snapshot)
6
+ 2. Write each FileChange.proposed to disk
7
+ 3. Run relevant tests (scoped to changed files)
8
+ 4. If tests fail → automatic rollback via snapshot
9
+ 5. Update patch state and save to memory
10
+
11
+ Fix agents always run sequentially (one at a time — spec says non-negotiable).
12
+ The applier is synchronous and blocking. The coordinator calls it in a queue loop.
13
+
14
+ Test runner strategy (uses subprocess; dedicated test agents run full coverage):
15
+ Python → pytest [changed file path] --tb=short -q
16
+ Node/TS → npm test / jest [changed file pattern] --passWithNoTests
17
+ No tests → skip test phase, warn user
18
+
19
+ Auto-rollback fires when:
20
+ - Any test command exits with a non-zero code
21
+ - Disk write fails midway (partial write safety)
22
+ - Timeout exceeded (60s default)
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ import logging
29
+ import subprocess
30
+ import time
31
+ from dataclasses import dataclass
32
+ from datetime import UTC
33
+ from pathlib import Path
34
+
35
+ from patchi.core import memory as mem
36
+ from patchi.core import snapshot as snap
37
+ from patchi.core.atomic import atomic_write_text
38
+ from patchi.core.fix.patch import Patch, PatchState
39
+
40
+ _log = logging.getLogger("patchi.fix.applier")
41
+
42
+ # ── Apply result ───────────────────────────────────────────────────────────────
43
+
44
+
45
+ @dataclass
46
+ class ApplyResult:
47
+ patch_id: str
48
+ success: bool
49
+ rolled_back: bool = False
50
+ snapshot_id: str = ""
51
+ test_passed: bool | None = None # None = no tests found
52
+ test_output: str = ""
53
+ lint_passed: bool | None = None # None = no linter found
54
+ lint_output: str = ""
55
+ error: str = ""
56
+ duration_ms: int = 0
57
+ verify_status: str = "unverified" # M-16: passed/failed/unverified
58
+ verify_remaining: int = 0
59
+
60
+ def to_dict(self) -> dict:
61
+ return {
62
+ "patch_id": self.patch_id,
63
+ "success": self.success,
64
+ "rolled_back": self.rolled_back,
65
+ "snapshot_id": self.snapshot_id,
66
+ "test_passed": self.test_passed,
67
+ "test_output": self.test_output[:2000], # cap output stored
68
+ "error": self.error,
69
+ "duration_ms": self.duration_ms,
70
+ }
71
+
72
+
73
+ # ── Applier ────────────────────────────────────────────────────────────────────
74
+
75
+
76
+ class PatchApplier:
77
+ """
78
+ Applies a patch to disk with snapshot + auto-rollback.
79
+
80
+ Usage:
81
+ applier = PatchApplier(project_root)
82
+ result = applier.apply(patch)
83
+ """
84
+
85
+ TEST_TIMEOUT = 60 # seconds
86
+
87
+ def __init__(self, root: Path):
88
+ self.root = root
89
+
90
+ def apply(self, patch: Patch) -> ApplyResult:
91
+ """Apply a patch. Returns ApplyResult. Never raises."""
92
+ t0 = time.monotonic()
93
+
94
+ # ── Policy gate check (WIRE-03) ────────────────────────────────────────
95
+ try:
96
+ from patchi.core.constants import RiskLevel
97
+ from patchi.core.security.governance import patchi_action_log, patchi_policy_gate
98
+
99
+ risk_sev = RiskLevel.from_score(patch.risk_score).value
100
+ for change in patch.changes:
101
+ allowed, reason = patchi_policy_gate(
102
+ self.root, "patch_apply", change.path, risk_sev
103
+ )
104
+ if not allowed:
105
+ patchi_action_log(
106
+ self.root, "patch_blocked", change.path, detail=reason, status="blocked"
107
+ )
108
+ return ApplyResult(
109
+ patch_id=patch.id,
110
+ success=False,
111
+ error=f"Policy gate blocked: {reason}",
112
+ duration_ms=_ms(t0),
113
+ )
114
+ except ImportError:
115
+ pass # governance module unavailable — proceed
116
+
117
+ # ── 1. Snapshot ────────────────────────────────────────────────────────
118
+ try:
119
+ snapshot_id = snap.create(patch.affected_paths, patch.id, self.root)
120
+ except Exception as e:
121
+ return ApplyResult(
122
+ patch_id=patch.id,
123
+ success=False,
124
+ error=f"Snapshot failed: {e}",
125
+ duration_ms=_ms(t0),
126
+ )
127
+
128
+ # ── 2. Write files ─────────────────────────────────────────────────────
129
+ written: list[str] = []
130
+ try:
131
+ for change in patch.changes:
132
+ # Policy gate check before writing (WIRE-03)
133
+ try:
134
+ from patchi.core.security.governance import (
135
+ patchi_action_log,
136
+ patchi_policy_gate,
137
+ )
138
+
139
+ severity_str = (
140
+ "high"
141
+ if patch.risk_score >= 61
142
+ else "medium"
143
+ if patch.risk_score >= 31
144
+ else "low"
145
+ )
146
+ allowed, reason = patchi_policy_gate(
147
+ self.root, "patch_apply", change.path, severity_str
148
+ )
149
+ if not allowed:
150
+ snap.restore(snapshot_id, self.root)
151
+ return ApplyResult(
152
+ patch_id=patch.id,
153
+ success=False,
154
+ rolled_back=True,
155
+ snapshot_id=snapshot_id,
156
+ error=f"Policy gate blocked: {reason}",
157
+ duration_ms=_ms(t0),
158
+ )
159
+ patchi_action_log(
160
+ self.root,
161
+ "patch_apply",
162
+ change.path,
163
+ agent="PatchApplier",
164
+ detail=f"patch_id={patch.id}",
165
+ )
166
+ except ImportError:
167
+ pass
168
+
169
+ # Secrets gate check (NEW-02) — block patches introducing secrets
170
+ try:
171
+ from patchi.core.security.secrets_guard import gate_check_proposed_code
172
+
173
+ if change.proposed:
174
+ safe, secret_findings = gate_check_proposed_code(
175
+ change.proposed, change.path
176
+ )
177
+ if not safe:
178
+ snap.restore(snapshot_id, self.root)
179
+ return ApplyResult(
180
+ patch_id=patch.id,
181
+ success=False,
182
+ rolled_back=True,
183
+ snapshot_id=snapshot_id,
184
+ error=f"Secrets gate blocked: proposed code in {change.path} introduces secrets",
185
+ duration_ms=_ms(t0),
186
+ )
187
+ except ImportError:
188
+ pass
189
+
190
+ abs_path = self.root / change.path
191
+ if change.proposed == "":
192
+ # Empty proposed = deletion (DeadCodeRemover uses this)
193
+ if abs_path.exists():
194
+ abs_path.unlink()
195
+ else:
196
+ atomic_write_text(abs_path, change.proposed)
197
+ written.append(change.path)
198
+ except Exception as e:
199
+ # Partial write — rollback immediately
200
+ rollback_ok = True
201
+ try:
202
+ snap.restore(snapshot_id, self.root)
203
+ except Exception as rb_err:
204
+ rollback_ok = False
205
+ _log.warning(
206
+ "Rollback after failed write also failed (snapshot=%s): %s",
207
+ snapshot_id,
208
+ rb_err,
209
+ )
210
+ return ApplyResult(
211
+ patch_id=patch.id,
212
+ success=False,
213
+ rolled_back=rollback_ok,
214
+ snapshot_id=snapshot_id,
215
+ error=(
216
+ f"Write failed after {len(written)} file(s): {e}"
217
+ + (
218
+ ""
219
+ if rollback_ok
220
+ else " (rollback ALSO failed — files may be in a partial state!)"
221
+ )
222
+ ),
223
+ duration_ms=_ms(t0),
224
+ )
225
+
226
+ # ── 3. Run tests ───────────────────────────────────────────────────────
227
+ test_result = self._run_tests(patch.affected_paths)
228
+
229
+ # ── 3b. Run linter ────────────────────────────────────────────────────
230
+ lint_result = self._run_linter(patch.affected_paths)
231
+
232
+ # ── 4. Rollback if tests or linter failed ─────────────────────────────
233
+ failed = False
234
+ failure_reason = ""
235
+ if test_result["passed"] is False:
236
+ failed = True
237
+ failure_reason = "Tests failed"
238
+ elif lint_result["passed"] is False:
239
+ failed = True
240
+ failure_reason = "Linter errors introduced"
241
+
242
+ if failed:
243
+ try:
244
+ snap.restore(snapshot_id, self.root)
245
+ rolled_back = True
246
+ except Exception as rb_err:
247
+ rolled_back = False
248
+ test_result["rollback_error"] = str(rb_err)
249
+
250
+ # Update patch state
251
+ self._update_patch_state(patch.id, PatchState.FAILED, test_result, snapshot_id)
252
+
253
+ return ApplyResult(
254
+ patch_id=patch.id,
255
+ success=False,
256
+ rolled_back=rolled_back,
257
+ snapshot_id=snapshot_id,
258
+ test_passed=test_result.get("passed"),
259
+ lint_passed=lint_result.get("passed"),
260
+ test_output=test_result.get("output", ""),
261
+ lint_output=lint_result.get("output", ""),
262
+ error=f"{failure_reason}. {'Rolled back.' if rolled_back else 'Rollback also failed!'}",
263
+ duration_ms=_ms(t0),
264
+ )
265
+
266
+ # ── 5. Verify fix (M-16: detect-fix-verify loop) ──────────────────────
267
+ verify_result = self._verify_fix(patch)
268
+
269
+ # ── 6. Success ─────────────────────────────────────────────────────────
270
+ self._update_patch_state(patch.id, PatchState.APPLIED, test_result, snapshot_id)
271
+
272
+ # Audit log for successful apply (WIRE-04)
273
+ try:
274
+ from patchi.core.security.governance import patchi_action_log
275
+
276
+ targets = ", ".join(c.path for c in patch.changes[:5])
277
+ patchi_action_log(self.root, "patch_applied", targets, detail=patch.id, status="ok")
278
+ except Exception as e:
279
+ _log.warning("Failed to write audit log entry for patch %s: %s", patch.id, e)
280
+
281
+ # Mark brain stale (files changed)
282
+ try:
283
+ mem.mark_brain_stale(patch.affected_paths, self.root)
284
+ except Exception as e:
285
+ _log.warning("Failed to mark brain stale for patch %s: %s", patch.id, e)
286
+
287
+ result = ApplyResult(
288
+ patch_id=patch.id,
289
+ success=True,
290
+ rolled_back=False,
291
+ snapshot_id=snapshot_id,
292
+ test_passed=test_result.get("passed"),
293
+ test_output=test_result.get("output", ""),
294
+ duration_ms=_ms(t0),
295
+ )
296
+ result.verify_status = verify_result.get("status", "unverified")
297
+ result.verify_remaining = verify_result.get("remaining", 0)
298
+ return result
299
+
300
+ def rollback(self, patch_id: str, snapshot_id: str) -> ApplyResult:
301
+ """
302
+ Manually rollback a previously applied patch.
303
+ Used by `p undo [id]` and `p rollback [id]`.
304
+ """
305
+ t0 = time.monotonic()
306
+ try:
307
+ restored = snap.restore(snapshot_id, self.root)
308
+ self._update_patch_state(patch_id, PatchState.ROLLED_BACK, {}, snapshot_id)
309
+ mem.mark_brain_stale(restored, self.root)
310
+ return ApplyResult(
311
+ patch_id=patch_id,
312
+ success=True,
313
+ rolled_back=True,
314
+ snapshot_id=snapshot_id,
315
+ duration_ms=_ms(t0),
316
+ )
317
+ except Exception as e:
318
+ return ApplyResult(
319
+ patch_id=patch_id,
320
+ success=False,
321
+ error=f"Rollback failed: {e}",
322
+ duration_ms=_ms(t0),
323
+ )
324
+
325
+ # ── Verify fix (MISS-15: detect-fix-verify loop) ───────────────────────────
326
+
327
+ def _verify_fix(self, patch: Patch) -> dict:
328
+ """Re-scan changed files to confirm the fix resolved the finding.
329
+
330
+ Uses the original scanner agent (if known) for precise verification.
331
+ Falls back to FileScanner complexity check for generic patches.
332
+ """
333
+ try:
334
+ from patchi.core.agents.base import get_agent
335
+ from patchi.core.brain.scanner import FileScanner
336
+
337
+ # If we know which scanner agent found the issue, re-run it
338
+ scanner_agent = patch.scanner_agent or patch.source_finding.get("agent", "")
339
+ finding_type = patch.finding_id or patch.source_finding.get("type", "")
340
+ finding_file = patch.source_finding.get("file", "")
341
+ finding_line = patch.source_finding.get("line", 0)
342
+
343
+ if scanner_agent and finding_type:
344
+ agent_cls = get_agent(scanner_agent)
345
+ if agent_cls:
346
+ from patchi.core.agents.base import AgentInput
347
+
348
+ inp = AgentInput(
349
+ root=self.root, scope=patch.affected_paths, brain={}, config={}
350
+ )
351
+ result = agent_cls().run(inp)
352
+ # Check if the same finding type still exists on the same file
353
+ still_present = any(
354
+ f.type == finding_type
355
+ and f.file == finding_file
356
+ and (finding_line == 0 or abs(f.line - finding_line) <= 3)
357
+ for f in result.findings
358
+ )
359
+ if not still_present:
360
+ return {"status": "passed", "remaining": 0}
361
+ patch.verify_retries = max(patch.verify_retries - 1, 0)
362
+ return {
363
+ "status": "failed",
364
+ "remaining": 1,
365
+ "can_retry": patch.verify_retries > 0,
366
+ }
367
+
368
+ # Fallback: generic complexity check
369
+ scanner = FileScanner(self.root)
370
+ remaining = 0
371
+ for change in patch.changes:
372
+ abs_path = self.root / change.path
373
+ if not abs_path.exists():
374
+ continue
375
+ info = scanner.scan_file(abs_path)
376
+ if info and info.complexity and info.complexity > 10:
377
+ remaining += 1
378
+ if remaining == 0:
379
+ return {"status": "passed", "remaining": 0}
380
+ return {"status": "failed", "remaining": remaining}
381
+ except Exception as e:
382
+ _log.warning("Fix verification failed for patch %s: %s", patch.id, e)
383
+ return {"status": "unverified", "remaining": -1}
384
+
385
+ # ── Test runner ────────────────────────────────────────────────────────────
386
+
387
+ def _run_tests(self, changed_paths: list[str]) -> dict:
388
+ """
389
+ Run relevant tests for the changed files.
390
+ Returns {passed: bool|None, output: str, runner: str}
391
+ """
392
+ runner, cmd = self._detect_test_runner(changed_paths)
393
+ if runner is None:
394
+ return {"passed": None, "output": "No test runner detected.", "runner": None}
395
+
396
+ try:
397
+ proc = subprocess.run(
398
+ cmd,
399
+ capture_output=True,
400
+ text=True,
401
+ cwd=str(self.root),
402
+ timeout=self.TEST_TIMEOUT,
403
+ )
404
+ passed = proc.returncode == 0
405
+ output = (proc.stdout + proc.stderr)[:3000]
406
+ return {"passed": passed, "output": output, "runner": runner}
407
+ except subprocess.TimeoutExpired:
408
+ return {"passed": False, "output": "Tests timed out (60s).", "runner": runner}
409
+ except Exception as e:
410
+ return {"passed": None, "output": str(e), "runner": runner}
411
+
412
+ def _detect_test_runner(self, changed_paths: list[str]) -> tuple[str | None, list[str]]:
413
+ """
414
+ Detect the test runner for the changed files.
415
+ Returns (runner_name, command_list) or (None, []) if none found.
416
+ """
417
+ import shutil
418
+
419
+ # Python: pytest — only if matching test files exist
420
+ py_files = [p for p in changed_paths if p.endswith(".py")]
421
+ if py_files and shutil.which("pytest"):
422
+ test_files = self._find_python_tests(py_files)
423
+ if test_files:
424
+ return "pytest", ["pytest", "--tb=short", "-q"] + test_files
425
+
426
+ # Node/TypeScript: jest or npm test
427
+ js_files = [p for p in changed_paths if p.endswith((".js", ".ts", ".jsx", ".tsx"))]
428
+ if js_files:
429
+ if shutil.which("jest") and (self.root / "package.json").exists():
430
+ try:
431
+ pkg = json.loads((self.root / "package.json").read_text())
432
+ if "jest" in str(pkg.get("devDependencies", {})):
433
+ pattern = "|".join(Path(p).stem for p in js_files[:3])
434
+ return "jest", [
435
+ "jest",
436
+ "--testPathPattern",
437
+ pattern or ".",
438
+ "--passWithNoTests",
439
+ ]
440
+ except Exception as e:
441
+ _log.debug("Could not parse package.json for jest detection: %s", e)
442
+ if (self.root / "package.json").exists() and shutil.which("npm"):
443
+ try:
444
+ pkg = json.loads((self.root / "package.json").read_text())
445
+ if "test" in pkg.get("scripts", {}):
446
+ return "npm", ["npm", "test", "--", "--passWithNoTests"]
447
+ except Exception as e:
448
+ _log.debug("Could not parse package.json for npm test detection: %s", e)
449
+
450
+ return None, []
451
+
452
+ def _find_python_tests(self, source_paths: list[str]) -> list[str]:
453
+ """Find pytest test files corresponding to source files."""
454
+ test_files: list[str] = []
455
+ for src in source_paths:
456
+ from pathlib import PurePosixPath
457
+
458
+ stem = PurePosixPath(src).stem
459
+ candidates = [
460
+ f"tests/test_{stem}.py",
461
+ f"tests/{stem}_test.py",
462
+ f"test_{stem}.py",
463
+ f"{PurePosixPath(src).parent}/test_{stem}.py",
464
+ f"{PurePosixPath(src).parent}/tests/test_{stem}.py",
465
+ ]
466
+ for c in candidates:
467
+ if (self.root / c).exists():
468
+ test_files.append(c)
469
+ return test_files
470
+
471
+ LINT_TIMEOUT = 30
472
+
473
+ def _run_linter(self, changed_paths: list[str]) -> dict:
474
+ """
475
+ Run linter on changed files to catch introduced errors.
476
+ Returns {passed: bool|None, output: str, linter: str}
477
+ """
478
+ import shutil
479
+
480
+ py_files = [p for p in changed_paths if p.endswith(".py")]
481
+ if py_files and shutil.which("ruff"):
482
+ try:
483
+ proc = subprocess.run(
484
+ ["ruff", "check", "--select=E,F,W"] + py_files,
485
+ capture_output=True,
486
+ text=True,
487
+ cwd=str(self.root),
488
+ timeout=self.LINT_TIMEOUT,
489
+ )
490
+ passed = proc.returncode == 0
491
+ output = (proc.stdout + proc.stderr)[:2000]
492
+ return {"passed": passed, "output": output, "linter": "ruff"}
493
+ except subprocess.TimeoutExpired:
494
+ return {"passed": None, "output": "Linter timed out.", "linter": "ruff"}
495
+ except Exception as e:
496
+ return {"passed": None, "output": str(e), "linter": "ruff"}
497
+
498
+ js_files = [p for p in changed_paths if p.endswith((".js", ".ts", ".jsx", ".tsx"))]
499
+ if js_files and shutil.which("npx"):
500
+ try:
501
+ proc = subprocess.run(
502
+ ["npx", "--yes", "eslint"] + js_files[:5],
503
+ capture_output=True,
504
+ text=True,
505
+ cwd=str(self.root),
506
+ timeout=self.LINT_TIMEOUT,
507
+ )
508
+ passed = proc.returncode == 0
509
+ output = (proc.stdout + proc.stderr)[:2000]
510
+ return {"passed": passed, "output": output, "linter": "eslint"}
511
+ except subprocess.TimeoutExpired:
512
+ return {"passed": None, "output": "Linter timed out.", "linter": "eslint"}
513
+ except Exception as e:
514
+ return {"passed": None, "output": str(e), "linter": "eslint"}
515
+
516
+ return {"passed": None, "output": "No linter available.", "linter": None}
517
+
518
+ # ── State updates ──────────────────────────────────────────────────────────
519
+
520
+ def _update_patch_state(
521
+ self,
522
+ patch_id: str,
523
+ state: PatchState,
524
+ test_result: dict,
525
+ snapshot_id: str,
526
+ ) -> None:
527
+ """Update patch state in memory."""
528
+ try:
529
+ from datetime import datetime
530
+
531
+ from patchi.core import memory as mem
532
+
533
+ patches = mem.list_patches(self.root)
534
+ for patch in patches:
535
+ if patch.get("id") == patch_id:
536
+ patch["state"] = state.value
537
+ patch["snapshot_id"] = snapshot_id
538
+ patch["test_result"] = test_result
539
+ now = datetime.now(UTC).isoformat()
540
+ if state == PatchState.APPLIED:
541
+ patch["applied_at"] = now
542
+ elif state == PatchState.FAILED:
543
+ patch["rolled_back_at"] = now
544
+ elif state == PatchState.ROLLED_BACK:
545
+ patch["rolled_back_at"] = now
546
+ break
547
+
548
+ # Save back — need to write to memory file
549
+ import json
550
+
551
+ from patchi.core.constants import MEMORY_FILES, MemoryCategory
552
+
553
+ mem_path = self.root / MEMORY_FILES[MemoryCategory.PATCHES]
554
+ mem_path.parent.mkdir(parents=True, exist_ok=True)
555
+ tmp_path = mem_path.with_suffix(".tmp")
556
+ with tmp_path.open("w", encoding="utf-8") as f:
557
+ json.dump(patches, f, indent=2)
558
+ # Atomic replace
559
+ import sys
560
+
561
+ if sys.platform == "win32":
562
+ # Windows doesn't support atomic rename over existing file
563
+ if mem_path.exists():
564
+ mem_path.unlink()
565
+ tmp_path.rename(mem_path)
566
+ except Exception as e:
567
+ _log.warning("Failed to persist patch state for %s (state=%s): %s", patch_id, state, e)
568
+
569
+
570
+ # ── Helpers ────────────────────────────────────────────────────────────────────
571
+
572
+
573
+ def _ms(t0: float) -> int:
574
+ return int((time.monotonic() - t0) * 1000)