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,296 @@
1
+ """
2
+ `p trend` — Health and quality trend tracking over time.
3
+
4
+ Shows how your project's health changes across scans:
5
+ - Health score history with sparkline
6
+ - Security finding trends
7
+ - Test coverage progression
8
+ - Dead code ratio changes
9
+ - Dependency health over time
10
+
11
+ This catches regressions before they become problems.
12
+ No other tool tracks quality trends across time.
13
+
14
+ Usage:
15
+ p trend — show health trend over last 20 scans
16
+ p trend --last 50 — show last 50 scans
17
+ p trend security — security findings trend only
18
+ p trend tests — test coverage trend only
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import logging
24
+ import time
25
+ from pathlib import Path
26
+
27
+ from rich.panel import Panel
28
+ from rich.table import Table
29
+ from rich.text import Text
30
+
31
+ from patchi.cli.console import con
32
+ from patchi.core.config import require_project_root
33
+
34
+ _log = logging.getLogger("patchi.cli.trend_cmd")
35
+
36
+
37
+ def run(
38
+ metric: str | None = None, last_n: int = 20, root: Path | None = None, json_output: bool = False
39
+ ) -> None:
40
+ """Entry point for `p trend`."""
41
+ try:
42
+ r = root or require_project_root()
43
+ except RuntimeError as e:
44
+ con.print(f"[red]{e}[/red]")
45
+ return
46
+
47
+ from patchi.core import memory as mem
48
+
49
+ brain = mem.get_brain(r)
50
+ scans = mem.get_scan_results(r)
51
+
52
+ # Build trend data from scan history
53
+ trend_data = _build_trend_data(scans, brain)
54
+
55
+ if not trend_data:
56
+ if json_output:
57
+ import json as _json
58
+
59
+ con.print(_json.dumps({"trend": [], "note": "no scan history yet"}))
60
+ else:
61
+ con.print(
62
+ "[dim]No scan history yet. Run `p scan` a few times to build trend data.[/dim]"
63
+ )
64
+ return
65
+
66
+ if json_output:
67
+ import json as _json
68
+
69
+ con.print(_json.dumps({"trend": trend_data[-last_n:]}, indent=2, default=str))
70
+ return
71
+
72
+ if metric == "security":
73
+ _show_security_trend(trend_data, last_n)
74
+ elif metric == "tests":
75
+ _show_test_trend(trend_data, last_n)
76
+ else:
77
+ _show_health_trend(trend_data, last_n)
78
+
79
+
80
+ def _build_trend_data(scans: dict, brain: dict) -> list[dict]:
81
+ """Extract trend data from scan results."""
82
+ entries = []
83
+
84
+ for name, data in scans.items():
85
+ if not isinstance(data, dict):
86
+ continue
87
+ ts = data.get("timestamp", "")
88
+ finding_count = data.get("finding_count", 0)
89
+ status = data.get("status", "done")
90
+ duration = data.get("duration_ms", 0)
91
+
92
+ # Extract severity breakdown if available
93
+ findings = data.get("findings", [])
94
+ by_sev = {"critical": 0, "high": 0, "medium": 0, "low": 0}
95
+ for f in findings:
96
+ if isinstance(f, dict):
97
+ sev = f.get("severity", "low").lower()
98
+ if sev in by_sev:
99
+ by_sev[sev] += 1
100
+
101
+ entries.append(
102
+ {
103
+ "timestamp": ts,
104
+ "agent": name,
105
+ "finding_count": finding_count,
106
+ "status": status,
107
+ "duration_ms": duration,
108
+ "by_severity": by_sev,
109
+ }
110
+ )
111
+
112
+ # Sort by timestamp
113
+ entries.sort(key=lambda x: x.get("timestamp", ""))
114
+ return entries
115
+
116
+
117
+ def _show_health_trend(trend_data: list[dict], last_n: int) -> None:
118
+ """Show overall health trend."""
119
+ entries = trend_data[-last_n:]
120
+
121
+ table = Table(show_header=True, header_style="bold #C8621A", box=None, pad_edge=False)
122
+ table.add_column("Time", style="dim", width=19)
123
+ table.add_column("Agent", style="bold #F2EDD6", width=22)
124
+ table.add_column("Status", width=8)
125
+ table.add_column("Findings", justify="right", width=9)
126
+ table.add_column("Critical", justify="right", width=9)
127
+ table.add_column("High", justify="right", width=7)
128
+ table.add_column("Duration", justify="right", width=10)
129
+ table.add_column("Trend", width=12)
130
+
131
+ prev_findings = None
132
+ for entry in entries:
133
+ ts = entry.get("timestamp", "")
134
+ try:
135
+ ts_fmt = time.strftime("%Y-%m-%d %H:%M", time.localtime(float(ts))) if ts else "—"
136
+ except Exception as e:
137
+ _log.warning("_show_health_trend failed: %s", e)
138
+ ts_fmt = str(ts)[:16]
139
+
140
+ agent = entry.get("agent", "—")
141
+ status = entry.get("status", "done")
142
+ findings = entry.get("finding_count", 0)
143
+ by_sev = entry.get("by_severity", {})
144
+ crit = by_sev.get("critical", 0)
145
+ high = by_sev.get("high", 0)
146
+ dur = entry.get("duration_ms", 0)
147
+
148
+ # Trend indicator
149
+ if prev_findings is not None:
150
+ if findings < prev_findings:
151
+ trend = Text("↓ improving", style="#4ADE80")
152
+ elif findings > prev_findings:
153
+ trend = Text("↑ regressing", style="#FF4D6D")
154
+ else:
155
+ trend = Text("→ stable", style="dim")
156
+ else:
157
+ trend = Text("—", style="dim")
158
+ prev_findings = findings
159
+
160
+ status_color = "#4ADE80" if status == "done" else "#FF4D6D"
161
+
162
+ table.add_row(
163
+ ts_fmt,
164
+ agent[:22],
165
+ Text(status, style=status_color),
166
+ str(findings) if findings else "—",
167
+ Text(str(crit), style="#FF4D6D bold") if crit else "—",
168
+ Text(str(high), style="#FF8C42") if high else "—",
169
+ f"{dur}ms" if dur else "—",
170
+ trend,
171
+ )
172
+
173
+ con.print()
174
+ con.print(Panel(table, title="📈 Health Trend", border_style="#C8621A"))
175
+
176
+ # Sparkline summary
177
+ findings_series = [e.get("finding_count", 0) for e in entries]
178
+ if findings_series:
179
+ sparkline = _sparkline(findings_series)
180
+ con.print(f"\n Findings: {sparkline}")
181
+ con.print(
182
+ f" [dim]Latest: {findings_series[-1]} findings "
183
+ f"(avg: {sum(findings_series) // len(findings_series)})[/dim]"
184
+ )
185
+ con.print()
186
+
187
+
188
+ def _show_security_trend(trend_data: list[dict], last_n: int) -> None:
189
+ """Show security-specific trend."""
190
+ from patchi.core.agents.base import AgentGroup, list_agents
191
+
192
+ security_agents = {a.name for a in list_agents(AgentGroup.SECURITY)}
193
+
194
+ entries = [e for e in trend_data if e.get("agent") in security_agents][-last_n:]
195
+
196
+ if not entries:
197
+ con.print("[dim]No security scan data yet. Run `p security` first.[/dim]")
198
+ return
199
+
200
+ table = Table(show_header=True, header_style="bold #C8621A", box=None, pad_edge=False)
201
+ table.add_column("Time", style="dim", width=19)
202
+ table.add_column("Agent", style="bold #F2EDD6", width=22)
203
+ table.add_column("Critical", justify="right", width=9)
204
+ table.add_column("High", justify="right", width=7)
205
+ table.add_column("Medium", justify="right", width=8)
206
+ table.add_column("Low", justify="right", width=6)
207
+ table.add_column("Total", justify="right", width=7)
208
+
209
+ for entry in entries:
210
+ ts = entry.get("timestamp", "")
211
+ try:
212
+ ts_fmt = time.strftime("%Y-%m-%d %H:%M", time.localtime(float(ts))) if ts else "—"
213
+ except Exception as e:
214
+ _log.warning("_show_security_trend failed: %s", e)
215
+ ts_fmt = str(ts)[:16]
216
+
217
+ by_sev = entry.get("by_severity", {})
218
+ crit = by_sev.get("critical", 0)
219
+ high = by_sev.get("high", 0)
220
+ med = by_sev.get("medium", 0)
221
+ low = by_sev.get("low", 0)
222
+ total = crit + high + med + low
223
+
224
+ table.add_row(
225
+ ts_fmt,
226
+ entry.get("agent", "—")[:22],
227
+ Text(str(crit), style="#FF4D6D bold") if crit else "—",
228
+ Text(str(high), style="#FF8C42") if high else "—",
229
+ str(med) if med else "—",
230
+ str(low) if low else "—",
231
+ str(total) if total else "—",
232
+ )
233
+
234
+ con.print()
235
+ con.print(Panel(table, title="🛡 Security Trend", border_style="#C8621A"))
236
+ con.print()
237
+
238
+
239
+ def _show_test_trend(trend_data: list[dict], last_n: int) -> None:
240
+ """Show test-related trend."""
241
+ from patchi.core.agents.base import AgentGroup, list_agents
242
+
243
+ test_agents = {a.name for a in list_agents(AgentGroup.TEST)}
244
+
245
+ entries = [e for e in trend_data if e.get("agent") in test_agents][-last_n:]
246
+
247
+ if not entries:
248
+ con.print("[dim]No test data yet. Run `p test` first.[/dim]")
249
+ return
250
+
251
+ table = Table(show_header=True, header_style="bold #C8621A", box=None, pad_edge=False)
252
+ table.add_column("Time", style="dim", width=19)
253
+ table.add_column("Agent", style="bold #F2EDD6", width=22)
254
+ table.add_column("Status", width=8)
255
+ table.add_column("Findings", justify="right", width=9)
256
+ table.add_column("Duration", justify="right", width=10)
257
+
258
+ for entry in entries:
259
+ ts = entry.get("timestamp", "")
260
+ try:
261
+ ts_fmt = time.strftime("%Y-%m-%d %H:%M", time.localtime(float(ts))) if ts else "—"
262
+ except Exception as e:
263
+ _log.warning("_show_test_trend failed: %s", e)
264
+ ts_fmt = str(ts)[:16]
265
+
266
+ status = entry.get("status", "done")
267
+ findings = entry.get("finding_count", 0)
268
+ dur = entry.get("duration_ms", 0)
269
+
270
+ status_color = "#4ADE80" if findings == 0 else "#FF4D6D"
271
+
272
+ table.add_row(
273
+ ts_fmt,
274
+ entry.get("agent", "—")[:22],
275
+ Text(status, style=status_color),
276
+ str(findings) if findings else "—",
277
+ f"{dur}ms" if dur else "—",
278
+ )
279
+
280
+ con.print()
281
+ con.print(Panel(table, title="🧪 Test Trend", border_style="#C8621A"))
282
+ con.print()
283
+
284
+
285
+ def _sparkline(values: list[int], width: int = 40) -> str:
286
+ """Create a text sparkline from numeric values."""
287
+ if not values:
288
+ return ""
289
+ blocks = [" ", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]
290
+ mn = min(values)
291
+ mx = max(values)
292
+ rng = mx - mn if mx != mn else 1
293
+ # Downsample to width
294
+ step = max(1, len(values) // width)
295
+ sampled = values[::step][:width]
296
+ return "".join(blocks[min(int((v - mn) / rng * 7), 7)] for v in sampled)
@@ -0,0 +1,235 @@
1
+ """
2
+ `p undo`, `p redo`, `p rollback` — Patch history navigation.
3
+
4
+ p undo — undo last applied fix
5
+ p undo <id> — undo specific patch by ID
6
+ p redo — redo last undone fix
7
+ p redo <id> — redo specific patch by ID
8
+ p rollback <id> — rollback all patches back to before a specific patch
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from datetime import UTC
15
+ from pathlib import Path
16
+
17
+ from rich.prompt import Confirm
18
+
19
+ from patchi.cli.console import con
20
+ from patchi.core import memory as mem
21
+ from patchi.core.config import require_project_root
22
+ from patchi.core.fix.applier import PatchApplier
23
+ from patchi.core.fix.patch import Patch, PatchState
24
+
25
+ _log = logging.getLogger("patchi.cli.undo_cmd")
26
+
27
+
28
+ def run_undo(patch_id: str | None = None, root: Path | None = None) -> None:
29
+ """p undo [id]"""
30
+ try:
31
+ r = root or require_project_root()
32
+ except RuntimeError as e:
33
+ con.print(f"[red]{e}[/red]")
34
+ return
35
+
36
+ target = _find_for_undo(patch_id, r)
37
+ if not target:
38
+ con.print("[dim]Nothing to undo.[/dim]")
39
+ return
40
+
41
+ patch = Patch.from_dict(target)
42
+ snapshot_id = patch.snapshot_id
43
+
44
+ if not snapshot_id:
45
+ con.print(f"[red]Patch {patch.id} has no snapshot — cannot undo.[/red]")
46
+ return
47
+
48
+ con.print(f"Undoing [bold]{patch.id}[/bold]: {patch.description[:60]}")
49
+
50
+ applier = PatchApplier(r)
51
+ result = applier.rollback(patch.id, snapshot_id)
52
+
53
+ if result.success:
54
+ _mark_undone(patch.id, r)
55
+ con.print(
56
+ f"[#4ADE80]✓ Undone.[/#4ADE80] [dim]Files restored from snapshot {snapshot_id}[/dim]"
57
+ )
58
+ else:
59
+ con.print(f"[red]✗ Undo failed.[/red] [dim]{result.error}[/dim]")
60
+
61
+
62
+ def run_redo(patch_id: str | None = None, root: Path | None = None) -> None:
63
+ """p redo [id]"""
64
+ try:
65
+ r = root or require_project_root()
66
+ except RuntimeError as e:
67
+ con.print(f"[red]{e}[/red]")
68
+ return
69
+
70
+ target = _find_for_redo(patch_id, r)
71
+ if not target:
72
+ con.print("[dim]Nothing to redo.[/dim]")
73
+ return
74
+
75
+ patch = Patch.from_dict(target)
76
+
77
+ con.print(f"Re-applying [bold]{patch.id}[/bold]: {patch.description[:60]}")
78
+
79
+ from patchi.core.fix.risk_gate import RiskGate
80
+
81
+ gate_result = RiskGate(r).evaluate(patch)
82
+ if gate_result.is_blocked:
83
+ con.print(f"[red]Blocked:[/red] {gate_result.blocks[0]}")
84
+ return
85
+
86
+ # Re-apply the patch from its stored changes
87
+ patch.state = PatchState.PENDING
88
+ applier = PatchApplier(r)
89
+ result = applier.apply(patch)
90
+
91
+ if result.success:
92
+ _mark_applied(patch.id, r)
93
+ con.print(
94
+ f"[#4ADE80]✓ Re-applied.[/#4ADE80] "
95
+ f"[dim]Tests: {'passed' if result.test_passed else 'skipped'}[/dim]"
96
+ )
97
+ else:
98
+ con.print(f"[red]✗ Failed.[/red] [dim]{result.error}[/dim]")
99
+
100
+
101
+ def run_rollback(patch_id: str, root: Path | None = None) -> None:
102
+ """
103
+ p rollback <id> — rollback all changes back to the state before this patch.
104
+
105
+ Restores the snapshot taken before patch_id was applied.
106
+ All patches applied AFTER patch_id are also undone (oldest first).
107
+ """
108
+ try:
109
+ r = root or require_project_root()
110
+ except RuntimeError as e:
111
+ con.print(f"[red]{e}[/red]")
112
+ return
113
+
114
+ patches_raw = mem.list_patches(r)
115
+ target = next((p for p in patches_raw if p.get("id") == patch_id), None)
116
+
117
+ if not target:
118
+ con.print(f"[red]Patch {patch_id!r} not found.[/red]")
119
+ return
120
+
121
+ # Find all patches applied at or after target (in order)
122
+ applied_patches = [p for p in patches_raw if p.get("state") in ("applied", "auto_applied")]
123
+ applied_patches.sort(key=lambda p: p.get("applied_at", ""))
124
+
125
+ try:
126
+ target_idx = next(i for i, p in enumerate(applied_patches) if p.get("id") == patch_id)
127
+ except StopIteration:
128
+ con.print(
129
+ f"[yellow]Patch {patch_id} was not applied (state: {target.get('state')})[/yellow]"
130
+ )
131
+ return
132
+
133
+ patches_to_undo = applied_patches[target_idx:]
134
+ count = len(patches_to_undo)
135
+
136
+ con.print()
137
+ con.print(f"[yellow]This will undo {count} patch(es):[/yellow]")
138
+ for p in patches_to_undo:
139
+ con.print(f" [dim]● {p['id']}[/dim] {p.get('description', '')[:60]}")
140
+ con.print()
141
+
142
+ if not Confirm.ask("Proceed with rollback?", default=False):
143
+ con.print("[dim]Cancelled.[/dim]")
144
+ return
145
+
146
+ applier = PatchApplier(r)
147
+ undone = 0
148
+
149
+ # Undo in reverse order (newest first)
150
+ for p_dict in reversed(patches_to_undo):
151
+ snap_id = p_dict.get("snapshot_id", "")
152
+ if not snap_id:
153
+ con.print(f" [yellow]⚠[/yellow] {p_dict['id']} — no snapshot, skipping")
154
+ continue
155
+ result = applier.rollback(p_dict["id"], snap_id)
156
+ if result.success:
157
+ _mark_undone(p_dict["id"], r)
158
+ con.print(f" [#4ADE80]✓[/#4ADE80] {p_dict['id']} undone")
159
+ undone += 1
160
+ else:
161
+ con.print(f" [red]✗[/red] {p_dict['id']} — {result.error[:60]}")
162
+ break # Stop on first failure — don't continue partial rollback
163
+
164
+ con.print()
165
+ con.print(f"[dim]Rollback complete: {undone}/{count} patches undone.[/dim]")
166
+ con.print()
167
+
168
+
169
+ # ── Finders ────────────────────────────────────────────────────────────────────
170
+
171
+
172
+ def _find_for_undo(patch_id: str | None, root: Path) -> dict | None:
173
+ patches = mem.list_patches(root)
174
+ applied = [p for p in patches if p.get("state") in ("applied", "auto_applied")]
175
+ if not applied:
176
+ return None
177
+ if patch_id:
178
+ return next((p for p in applied if p.get("id") == patch_id), None)
179
+ # Last applied
180
+ applied.sort(key=lambda p: p.get("applied_at", ""))
181
+ return applied[-1]
182
+
183
+
184
+ def _find_for_redo(patch_id: str | None, root: Path) -> dict | None:
185
+ patches = mem.list_patches(root)
186
+ undone = [p for p in patches if p.get("state") == "undone"]
187
+ if not undone:
188
+ return None
189
+ if patch_id:
190
+ return next((p for p in undone if p.get("id") == patch_id), None)
191
+ # Most recently undone
192
+ undone.sort(key=lambda p: p.get("rolled_back_at", ""), reverse=True)
193
+ return undone[0]
194
+
195
+
196
+ # ── State update ───────────────────────────────────────────────────────────────
197
+
198
+
199
+ def _mark_undone(patch_id: str, root: Path) -> None:
200
+ try:
201
+ import json
202
+ from datetime import datetime
203
+
204
+ from patchi.core.constants import MEMORY_FILES, MemoryCategory
205
+
206
+ patches = mem.list_patches(root)
207
+ for p in patches:
208
+ if p.get("id") == patch_id:
209
+ p["state"] = PatchState.UNDONE.value
210
+ p["rolled_back_at"] = datetime.now(UTC).isoformat()
211
+ break
212
+ mem_path = root / MEMORY_FILES[MemoryCategory.PATCHES]
213
+ mem_path.write_text(json.dumps(patches, indent=2), encoding="utf-8")
214
+ except Exception as e:
215
+ _log.warning("_mark_undone failed: %s", e)
216
+
217
+
218
+ def _mark_applied(patch_id: str, root: Path) -> None:
219
+ """Mark a patch as applied (used after redo succeeds)."""
220
+ try:
221
+ import json
222
+ from datetime import datetime
223
+
224
+ from patchi.core.constants import MEMORY_FILES, MemoryCategory
225
+
226
+ patches = mem.list_patches(root)
227
+ for p in patches:
228
+ if p.get("id") == patch_id:
229
+ p["state"] = PatchState.APPLIED.value
230
+ p["applied_at"] = datetime.now(UTC).isoformat()
231
+ break
232
+ mem_path = root / MEMORY_FILES[MemoryCategory.PATCHES]
233
+ mem_path.write_text(json.dumps(patches, indent=2), encoding="utf-8")
234
+ except Exception as e:
235
+ _log.warning("_mark_applied failed: %s", e)