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,354 @@
1
+ """
2
+ E2EFlowAgent — AI-generated end-to-end test scripts via Playwright.
3
+
4
+ Uses the AI layer to:
5
+ - Analyze the app's HTML/routes and generate Playwright test scripts
6
+ - Test full user flows (login → dashboard → action → logout)
7
+ - Test error states and edge cases
8
+ - Generate tests from app contract critical flows
9
+ - Run generated tests and report results
10
+
11
+ Each test script is generated as a standalone Python file using playwright.sync_api.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ import subprocess
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ from patchi.core.brain.languages import DEFAULT_IGNORE_DIRS
22
+
23
+ from ..agents.base import (
24
+ AgentDomain,
25
+ AgentGroup,
26
+ AgentInput,
27
+ AgentResult,
28
+ BaseAgent,
29
+ Severity,
30
+ make_finding,
31
+ register,
32
+ )
33
+
34
+
35
+ @register
36
+ class E2EFlowAgent(BaseAgent):
37
+ """AI-generated E2E tests for full user flows via Playwright."""
38
+
39
+ group = AgentGroup.TEST
40
+ domain = AgentDomain.TESTING
41
+ name = "E2EFlowAgent"
42
+ description = "AI-generated Playwright E2E tests: login, checkout, forms, navigation"
43
+ timeout = 300
44
+
45
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
46
+ try:
47
+ from playwright.sync_api import (
48
+ sync_playwright, # noqa: F401 — used to check availability
49
+ )
50
+ except ImportError:
51
+ self.skip(result, "playwright not installed")
52
+ return
53
+
54
+ base_url = self._find_base_url(inp)
55
+ if not base_url:
56
+ self.skip(result, "no running server found")
57
+ return
58
+
59
+ # Step 1: Discover pages and routes
60
+ pages = self._discover_pages(inp.root)
61
+ routes = self._extract_routes(inp)
62
+ flows = self._get_contract_flows(inp)
63
+
64
+ # Step 2: Generate test scripts using AI
65
+ test_scripts = self._generate_test_scripts(inp, pages, routes, flows, base_url)
66
+
67
+ if not test_scripts:
68
+ # Fallback: generate basic smoke tests without AI
69
+ test_scripts = self._generate_smoke_tests(pages, routes, base_url)
70
+
71
+ # Step 3: Run generated tests
72
+ test_results = self._run_generated_tests(test_scripts, inp.root)
73
+
74
+ # Step 4: Report results
75
+ total = test_results.get("total", 0)
76
+ passed = test_results.get("passed", 0)
77
+ failed = test_results.get("failed", 0)
78
+
79
+ for detail in test_results.get("details", []):
80
+ severity = Severity.INFO if detail["passed"] else Severity.HIGH
81
+ result.add_finding(
82
+ make_finding(
83
+ agent=self.name,
84
+ finding_type="e2e_pass" if detail["passed"] else "e2e_fail",
85
+ severity=severity,
86
+ file=detail.get("file", "generated_test"),
87
+ message=f"{'PASS' if detail['passed'] else 'FAIL'}: {detail['name']}",
88
+ detail=detail.get("error", "")[:500] if not detail["passed"] else "",
89
+ )
90
+ )
91
+
92
+ result.data["suite"] = {
93
+ "runner": "playwright_e2e",
94
+ "total": total,
95
+ "passed": passed,
96
+ "failed": failed,
97
+ "scripts_generated": len(test_scripts),
98
+ "details": test_results.get("details", [])[:50],
99
+ }
100
+ result.files_scanned = len(pages)
101
+
102
+ def _generate_test_scripts(
103
+ self, inp: AgentInput, pages: list, routes: list, flows: list, base_url: str
104
+ ) -> list[dict]:
105
+ """Use AI to generate Playwright test scripts."""
106
+ from ..ai.client import call_ai
107
+
108
+ config = inp.config
109
+ if not config.get("ai", {}).get("keys") and not config.get("ai", {}).get(
110
+ "local_model_name"
111
+ ):
112
+ return []
113
+
114
+ pages_text = "\n".join(f"- {p}" for p in pages[:20])
115
+ routes_text = "\n".join(f"- {r}" for r in routes[:20]) if routes else "No routes detected"
116
+ flows_text = ""
117
+ if flows:
118
+ flows_text = "\nConfirmed critical flows:\n" + "\n".join(
119
+ f"- {f.get('name', '?')}: {f.get('description', '')}" for f in flows
120
+ )
121
+
122
+ system = """You are a Playwright test generator for the Patchi testing framework.
123
+
124
+ Generate Python test scripts using playwright.sync_api that test the web application.
125
+
126
+ RULES:
127
+ - Use `from playwright.sync_api import sync_playwright` (sync API, not async)
128
+ - Each test function must accept `base_url` as a parameter
129
+ - Tests must be self-contained (no external fixtures)
130
+ - Use try/except to handle errors gracefully
131
+ - Print structured results: print("PASS: test_name") or print("FAIL: test_name: error")
132
+ - Test real user interactions: click buttons, fill forms, verify content
133
+ - Test navigation between pages
134
+ - Test error states (invalid inputs, missing required fields)
135
+ - Test responsive behavior at mobile viewport
136
+
137
+ Generate 5-10 test functions that cover the most important user flows.
138
+ Each function should be named `test_<something>` and take `base_url` as argument.
139
+ At the end, include a `def main():` that runs all tests and prints a summary."""
140
+
141
+ user_prompt = f"""Generate Playwright E2E tests for this application.
142
+
143
+ Base URL: {base_url}
144
+
145
+ Available pages:
146
+ {pages_text}
147
+
148
+ Routes:
149
+ {routes_text}
150
+ {flows_text}
151
+
152
+ Generate comprehensive E2E tests covering:
153
+ 1. Page load and rendering tests
154
+ 2. Button click tests (verify actions happen)
155
+ 3. Form submission tests (valid + invalid inputs)
156
+ 4. Navigation tests (clicking links moves between pages)
157
+ 5. Error state tests (what happens with bad input)
158
+ 6. At least one responsive test (mobile viewport)
159
+
160
+ Return ONLY Python code in a single code block."""
161
+
162
+ response = call_ai(config, system, user_prompt, max_tokens=6000)
163
+ if not response:
164
+ return []
165
+
166
+ # Extract Python code blocks
167
+ blocks = re.findall(r"```(?:python)?\n(.*?)```", response, re.DOTALL)
168
+ if not blocks:
169
+ # Try to find function definitions directly
170
+ if "def test_" in response:
171
+ blocks = [response]
172
+
173
+ scripts = []
174
+ for i, block in enumerate(blocks):
175
+ scripts.append(
176
+ {
177
+ "name": f"ai_generated_{i}",
178
+ "code": block,
179
+ "source": "ai",
180
+ }
181
+ )
182
+
183
+ return scripts
184
+
185
+ def _generate_smoke_tests(self, pages: list, routes: list, base_url: str) -> list[dict]:
186
+ """Generate basic smoke tests without AI as a fallback."""
187
+ test_code = f'''
188
+ from playwright.sync_api import sync_playwright
189
+ import sys
190
+
191
+ def test_page_loads(base_url, page_path):
192
+ """Test that a page loads without errors."""
193
+ with sync_playwright() as p:
194
+ browser = p.chromium.launch(headless=True)
195
+ page = browser.new_page()
196
+ url = f"{{base_url}}/{{page_path}}" if not page_path.startswith("http") else page_path
197
+ try:
198
+ response = page.goto(url, wait_until="domcontentloaded", timeout=15000)
199
+ if response and response.status < 400:
200
+ print(f"PASS: page_loads_{{page_path.replace('/', '_').replace('.html', '')}}")
201
+ else:
202
+ print(f"FAIL: page_loads_{{page_path.replace('/', '_').replace('.html', '')}}: status {{response.status if response else 'None'}}")
203
+ except Exception as e:
204
+ print(f"FAIL: page_loads_{{page_path.replace('/', '_').replace('.html', '')}}: {{e}}")
205
+ finally:
206
+ browser.close()
207
+
208
+ def test_no_console_errors(base_url, page_path):
209
+ """Test that no JavaScript errors occur on page load."""
210
+ with sync_playwright() as p:
211
+ browser = p.chromium.launch(headless=True)
212
+ page = browser.new_page()
213
+ errors = []
214
+ page.on("console", lambda msg: errors.append(msg.text) if msg.type == "error" else None)
215
+ url = f"{{base_url}}/{{page_path}}" if not page_path.startswith("http") else page_path
216
+ try:
217
+ page.goto(url, wait_until="domcontentloaded", timeout=15000)
218
+ page.wait_for_timeout(2000)
219
+ if errors:
220
+ print(f"FAIL: console_errors_{{page_path.replace('/', '_').replace('.html', '')}}: {{errors[0][:100]}}")
221
+ else:
222
+ print(f"PASS: console_errors_{{page_path.replace('/', '_').replace('.html', '')}}")
223
+ except Exception as e:
224
+ print(f"PASS: console_errors_{{page_path.replace('/', '_').replace('.html', '')}}: page failed to load")
225
+ finally:
226
+ browser.close()
227
+
228
+ if __name__ == "__main__":
229
+ base = "{base_url}"
230
+ pages = {pages[:10]}
231
+ passed = failed = 0
232
+ for page_path in pages:
233
+ test_page_loads(base, page_path)
234
+ test_no_console_errors(base, page_path)
235
+ '''
236
+
237
+ return [{"name": "smoke_tests", "code": test_code, "source": "generated"}]
238
+
239
+ def _run_generated_tests(self, scripts: list[dict], root: Path) -> dict:
240
+ """Run generated test scripts as subprocesses."""
241
+ total = 0
242
+ passed = 0
243
+ failed = 0
244
+ details = []
245
+
246
+ for script in scripts:
247
+ # Write script to temp file
248
+ script_path = root / ".patchi" / "tests" / "e2e" / f"{script['name']}.py"
249
+ script_path.parent.mkdir(parents=True, exist_ok=True)
250
+ script_path.write_text(script["code"], encoding="utf-8")
251
+
252
+ try:
253
+ proc = subprocess.run(
254
+ [sys.executable, str(script_path)],
255
+ capture_output=True,
256
+ text=True,
257
+ timeout=120,
258
+ cwd=str(root),
259
+ )
260
+ output = proc.stdout + proc.stderr
261
+
262
+ for line in output.splitlines():
263
+ if line.startswith("PASS:"):
264
+ total += 1
265
+ passed += 1
266
+ details.append(
267
+ {"name": line[6:].strip(), "passed": True, "file": script["name"]}
268
+ )
269
+ elif line.startswith("FAIL:"):
270
+ total += 1
271
+ failed += 1
272
+ parts = line[6:].split(":", 1)
273
+ details.append(
274
+ {
275
+ "name": parts[0].strip(),
276
+ "passed": False,
277
+ "error": parts[1].strip() if len(parts) > 1 else "unknown",
278
+ "file": script["name"],
279
+ }
280
+ )
281
+ except subprocess.TimeoutExpired:
282
+ total += 1
283
+ failed += 1
284
+ details.append(
285
+ {
286
+ "name": script["name"],
287
+ "passed": False,
288
+ "error": "timeout",
289
+ "file": script["name"],
290
+ }
291
+ )
292
+ except Exception as e:
293
+ total += 1
294
+ failed += 1
295
+ details.append(
296
+ {
297
+ "name": script["name"],
298
+ "passed": False,
299
+ "error": str(e)[:200],
300
+ "file": script["name"],
301
+ }
302
+ )
303
+
304
+ return {"total": total, "passed": passed, "failed": failed, "details": details}
305
+
306
+ def _find_base_url(self, inp: AgentInput) -> str | None:
307
+ extra_base = (inp.extra or {}).get("base_url")
308
+ if extra_base:
309
+ return extra_base
310
+ test_config = inp.config.get("test_config", {})
311
+ if test_config.get("base_url"):
312
+ return test_config["base_url"]
313
+ import socket
314
+
315
+ # Check config/brain for configured dev server port first (LIMIT-12)
316
+ config = inp.config or {}
317
+ brain = inp.brain or {}
318
+ config_port = config.get("web_port") or brain.get("web_port")
319
+ if config_port:
320
+ try:
321
+ with socket.create_connection(("127.0.0.1", int(config_port)), timeout=1):
322
+ return f"http://127.0.0.1:{int(config_port)}"
323
+ except (ConnectionRefusedError, OSError, ValueError):
324
+ pass
325
+ for port in [3000, 5173, 8080, 4200, 8000, 4321, 5189]:
326
+ try:
327
+ with socket.create_connection(("127.0.0.1", port), timeout=1):
328
+ return f"http://127.0.0.1:{port}"
329
+ except (ConnectionRefusedError, OSError):
330
+ continue
331
+ return None
332
+
333
+ def _discover_pages(self, root: Path) -> list[str]:
334
+ pages = []
335
+ for p in root.rglob("*.html"):
336
+ if any(part in DEFAULT_IGNORE_DIRS for part in p.parts):
337
+ continue
338
+ pages.append(p.relative_to(root).as_posix())
339
+ return sorted(set(pages))[:20]
340
+
341
+ def _extract_routes(self, inp: AgentInput) -> list[str]:
342
+ brain = inp.brain or {}
343
+ route_map = brain.get("route_map", {})
344
+ routes = []
345
+ for _key, val in route_map.items():
346
+ if isinstance(val, dict) and "path" in val:
347
+ routes.append(val["path"])
348
+ elif isinstance(val, str):
349
+ routes.append(val)
350
+ return routes[:20]
351
+
352
+ def _get_contract_flows(self, inp: AgentInput) -> list[dict]:
353
+ brain = inp.brain or {}
354
+ return brain.get("confirmed_flows", [])
@@ -0,0 +1,292 @@
1
+ """FlakeDetectorAgent — tracks test results across runs to detect flaky tests.
2
+
3
+ Persists per-run test results (name, status, duration) to SQLite.
4
+ After N runs, queries history to find tests that pass/fail inconsistently
5
+ and tests with abnormal duration variance.
6
+
7
+ Language-agnostic: any test framework output that produces test names,
8
+ pass/fail status, and durations works here.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import sqlite3
14
+ import statistics
15
+ import time
16
+ import uuid
17
+ from pathlib import Path
18
+
19
+ from ..agents.base import (
20
+ AgentDomain,
21
+ AgentGroup,
22
+ AgentInput,
23
+ AgentResult,
24
+ AgentStatus,
25
+ BaseAgent,
26
+ Finding,
27
+ Severity,
28
+ register,
29
+ )
30
+
31
+ _FLAKE_DB_NAME = "flake_history.db"
32
+ _MIN_RUNS_FOR_FLAKE = 2
33
+
34
+
35
+ def _get_flake_db(root: Path) -> sqlite3.Connection:
36
+ db_path = root / ".patchi" / _FLAKE_DB_NAME
37
+ db_path.parent.mkdir(parents=True, exist_ok=True)
38
+ conn = sqlite3.connect(str(db_path), check_same_thread=False)
39
+ conn.executescript("""
40
+ CREATE TABLE IF NOT EXISTS test_runs (
41
+ run_id TEXT PRIMARY KEY,
42
+ timestamp TEXT NOT NULL,
43
+ runner TEXT,
44
+ total INT DEFAULT 0,
45
+ passed INT DEFAULT 0,
46
+ failed INT DEFAULT 0,
47
+ skipped INT DEFAULT 0,
48
+ duration_ms INT DEFAULT 0
49
+ );
50
+ CREATE TABLE IF NOT EXISTS test_results (
51
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
52
+ run_id TEXT NOT NULL,
53
+ test_name TEXT NOT NULL,
54
+ passed INT NOT NULL,
55
+ duration_ms INT DEFAULT 0,
56
+ file TEXT DEFAULT '',
57
+ line INT DEFAULT 0,
58
+ FOREIGN KEY (run_id) REFERENCES test_runs(run_id)
59
+ );
60
+ CREATE INDEX IF NOT EXISTS idx_test_results_name ON test_results(test_name);
61
+ """)
62
+ return conn
63
+
64
+
65
+ def _record_test_run(
66
+ root: Path,
67
+ runner: str,
68
+ cases: list[dict],
69
+ total: int,
70
+ passed: int,
71
+ failed: int,
72
+ skipped: int,
73
+ duration_ms: int,
74
+ ) -> str:
75
+ run_id = uuid.uuid4().hex[:12]
76
+ now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
77
+ conn = _get_flake_db(root)
78
+ try:
79
+ conn.execute(
80
+ "INSERT INTO test_runs (run_id, timestamp, runner, total, passed, failed, skipped, duration_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
81
+ (run_id, now, runner, total, passed, failed, skipped, duration_ms),
82
+ )
83
+ for c in cases:
84
+ conn.execute(
85
+ "INSERT INTO test_results (run_id, test_name, passed, duration_ms, file, line) VALUES (?, ?, ?, ?, ?, ?)",
86
+ (
87
+ run_id,
88
+ c.get("name", "?"),
89
+ 1 if c.get("passed", True) else 0,
90
+ c.get("duration_ms", 0),
91
+ c.get("file", ""),
92
+ c.get("line", 0),
93
+ ),
94
+ )
95
+ conn.commit()
96
+ finally:
97
+ conn.close()
98
+ return run_id
99
+
100
+
101
+ def _get_all_runs(root: Path) -> list[dict]:
102
+ conn = _get_flake_db(root)
103
+ try:
104
+ cur = conn.execute(
105
+ "SELECT run_id, timestamp, runner, total, passed, failed, skipped, duration_ms FROM test_runs ORDER BY rowid ASC"
106
+ )
107
+ return [
108
+ {
109
+ "run_id": r[0],
110
+ "timestamp": r[1],
111
+ "runner": r[2],
112
+ "total": r[3],
113
+ "passed": r[4],
114
+ "failed": r[5],
115
+ "skipped": r[6],
116
+ "duration_ms": r[7],
117
+ }
118
+ for r in cur.fetchall()
119
+ ]
120
+ finally:
121
+ conn.close()
122
+
123
+
124
+ def _get_test_history(root: Path, test_name: str) -> list[dict]:
125
+ conn = _get_flake_db(root)
126
+ try:
127
+ cur = conn.execute(
128
+ "SELECT tr.run_id, tr.timestamp, res.passed, res.duration_ms, res.file, res.line "
129
+ "FROM test_results res JOIN test_runs tr ON res.run_id = tr.run_id "
130
+ "WHERE res.test_name = ? ORDER BY tr.rowid ASC",
131
+ (test_name,),
132
+ )
133
+ return [
134
+ {
135
+ "run_id": r[0],
136
+ "timestamp": r[1],
137
+ "passed": bool(r[2]),
138
+ "duration_ms": r[3],
139
+ "file": r[4],
140
+ "line": r[5],
141
+ }
142
+ for r in cur.fetchall()
143
+ ]
144
+ finally:
145
+ conn.close()
146
+
147
+
148
+ def _get_all_test_names(root: Path) -> list[str]:
149
+ conn = _get_flake_db(root)
150
+ try:
151
+ cur = conn.execute("SELECT DISTINCT test_name FROM test_results ORDER BY test_name")
152
+ return [r[0] for r in cur.fetchall()]
153
+ finally:
154
+ conn.close()
155
+
156
+
157
+ def _detect_flaky_tests(root: Path, min_runs: int = _MIN_RUNS_FOR_FLAKE) -> list[dict]:
158
+ flakes: list[dict] = []
159
+ for test_name in _get_all_test_names(root):
160
+ history = _get_test_history(root, test_name)
161
+ if len(history) < min_runs:
162
+ continue
163
+ outcomes = [h["passed"] for h in history]
164
+ if len(set(outcomes)) > 1:
165
+ flakes.append(
166
+ {
167
+ "test_name": test_name,
168
+ "history": history,
169
+ "run_count": len(history),
170
+ "pass_count": sum(1 for h in history if h["passed"]),
171
+ "fail_count": sum(1 for h in history if not h["passed"]),
172
+ "latest_file": history[-1]["file"],
173
+ "latest_line": history[-1]["line"],
174
+ }
175
+ )
176
+ return flakes
177
+
178
+
179
+ def _detect_duration_outliers(root: Path, z_threshold: float = 3.0) -> list[dict]:
180
+ outliers: list[dict] = []
181
+ for test_name in _get_all_test_names(root):
182
+ history = _get_test_history(root, test_name)
183
+ durations = [h["duration_ms"] for h in history if h["duration_ms"] > 0]
184
+ if len(durations) < 3:
185
+ continue
186
+ mean = statistics.mean(durations)
187
+ stdev = statistics.stdev(durations) if len(durations) > 1 else 0.0
188
+ if stdev == 0:
189
+ continue
190
+ latest_dur = history[-1]["duration_ms"]
191
+ z_score = (latest_dur - mean) / stdev
192
+ if z_score > z_threshold:
193
+ outliers.append(
194
+ {
195
+ "test_name": test_name,
196
+ "mean_duration_ms": round(mean, 1),
197
+ "stdev_duration_ms": round(stdev, 1),
198
+ "latest_duration_ms": latest_dur,
199
+ "z_score": round(z_score, 2),
200
+ "history": history,
201
+ "latest_file": history[-1]["file"],
202
+ "latest_line": history[-1]["line"],
203
+ }
204
+ )
205
+ return outliers
206
+
207
+
208
+ @register
209
+ class FlakeDetectorAgent(BaseAgent):
210
+ """Detects flaky tests and duration regressions across multiple test runs."""
211
+
212
+ group = AgentGroup.TEST
213
+ domain = AgentDomain.TESTING
214
+ name = "FlakeDetectorAgent"
215
+ description = (
216
+ "Track test results across runs to detect flaky tests "
217
+ "(inconsistent pass/fail) and duration regressions. "
218
+ "Language-agnostic: works with pytest, jest, mocha, unittest, go test, etc."
219
+ )
220
+
221
+ min_runs: int = 2
222
+
223
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
224
+
225
+ if not inp.root:
226
+ result.status = AgentStatus.FAILED
227
+ result.error = "No project root provided"
228
+ return
229
+
230
+ run_count = _get_all_runs(inp.root)
231
+ result.data["total_runs"] = len(run_count)
232
+
233
+ flakes = _detect_flaky_tests(inp.root, min_runs=self.min_runs)
234
+ result.data["flaky_tests"] = len(flakes)
235
+ result.data["flaky_details"] = flakes
236
+
237
+ outliers = _detect_duration_outliers(inp.root, z_threshold=3.0)
238
+ result.data["duration_outliers"] = len(outliers)
239
+ result.data["duration_outlier_details"] = outliers
240
+
241
+ for f in flakes:
242
+ result.add_finding(
243
+ Finding(
244
+ agent=self.name,
245
+ type="flaky_test",
246
+ severity=Severity.MEDIUM,
247
+ file=f.get("latest_file", ""),
248
+ line=f.get("latest_line", 0),
249
+ message=f"Flaky test: {f['test_name']}",
250
+ detail=(
251
+ f"Passed {f['pass_count']}/{f['run_count']} runs, "
252
+ f"failed {f['fail_count']}/{f['run_count']} runs."
253
+ ),
254
+ )
255
+ )
256
+
257
+ for o in outliers:
258
+ result.add_finding(
259
+ Finding(
260
+ agent=self.name,
261
+ type="duration_regression",
262
+ severity=Severity.LOW,
263
+ file=o.get("latest_file", ""),
264
+ line=o.get("latest_line", 0),
265
+ message=f"Duration regression: {o['test_name']}",
266
+ detail=(
267
+ f"Latest run: {o['latest_duration_ms']}ms "
268
+ f"(mean {o['mean_duration_ms']}ms, "
269
+ f"z-score {o['z_score']})"
270
+ ),
271
+ )
272
+ )
273
+
274
+ result.files_scanned = result.data["total_runs"]
275
+ return
276
+
277
+
278
+ def record_test_run(
279
+ root: Path,
280
+ runner: str,
281
+ cases: list[dict],
282
+ total: int,
283
+ passed: int,
284
+ failed: int,
285
+ skipped: int,
286
+ duration_ms: int,
287
+ ) -> str:
288
+ """Public entry point for UnitTestAgent (or any caller) to record results.
289
+
290
+ Returns the run_id.
291
+ """
292
+ return _record_test_run(root, runner, cases, total, passed, failed, skipped, duration_ms)
@@ -0,0 +1,31 @@
1
+ """
2
+ Gate Rule — injected into every Testing/Live/Attack agent.
3
+
4
+ Before doing any work, MUST confirm:
5
+ 1. P-Check has run
6
+ 2. It returned READY_TO_SERVE with zero unresolved errors
7
+ If false: do not run, request P-Check, stay idle.
8
+ Once READY_TO_SERVE confirmed: retrieve URL and begin work.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path
14
+
15
+
16
+ def require_ready(root: Path) -> tuple[bool, str | None, dict | None]:
17
+ """Check gate. Returns (ready, url, status_dict)."""
18
+ from patchi.cli.commands.check_cmd import is_ready
19
+
20
+ ready, st = is_ready(root)
21
+ if ready and st:
22
+ return True, st.get("url"), st
23
+ return False, None, st
24
+
25
+
26
+ def gate_message(status: dict | None) -> str:
27
+ if not status:
28
+ return "P-Check has not run — request `p check` and wait for READY_TO_SERVE. Do not run/test/attack anything."
29
+ if status.get("status") != "READY_TO_SERVE":
30
+ return f"P-Check is {status.get('status')} ({status.get('error','')}) — request `p check` re-run and let it escalate to Brain. Stay idle."
31
+ return "READY_TO_SERVE confirmed"