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,800 @@
1
+ """
2
+ Council Orchestration — Multi-persona decision making for Patchi.
3
+
4
+ The Council coordinates multiple specialized personas to analyze issues,
5
+ deliberate, and reach consensus on action plans.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import logging
12
+ import time
13
+ from collections.abc import Callable
14
+ from typing import TYPE_CHECKING
15
+
16
+ if TYPE_CHECKING:
17
+ from patchi.core.ai.tools.executor import ToolExecutor
18
+ from dataclasses import dataclass, field
19
+ from datetime import UTC, datetime
20
+ from pathlib import Path
21
+
22
+ # Import the personas package (not just base) so @register_persona decorators run
23
+ import patchi.core.brain.personas as _persona_registry # noqa: F401
24
+ from patchi.core import config as cfg
25
+ from patchi.core import memory as mem
26
+ from patchi.core.brain.layered_brain import Layer, layers_from_dict
27
+ from patchi.core.brain.personas.base import (
28
+ BasePersona,
29
+ PersonaDecision,
30
+ PersonaStyle,
31
+ create_persona,
32
+ list_personas,
33
+ )
34
+
35
+ _log = logging.getLogger("patchi.brain.council")
36
+
37
+
38
+ @dataclass
39
+ class CouncilSession:
40
+ """A single council deliberation session."""
41
+
42
+ issue: str
43
+ context: dict = field(default_factory=dict)
44
+ persona_decisions: list[PersonaDecision] = field(default_factory=list)
45
+ synthesis: str = ""
46
+ action_plan: list[dict] = field(default_factory=list)
47
+ consensus_reached: bool = False
48
+ started_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
49
+ completed_at: str = ""
50
+ duration_ms: int = 0
51
+
52
+
53
+ @dataclass
54
+ class ActionStep:
55
+ """A single step in the council's action plan."""
56
+
57
+ tool: str
58
+ parameters: dict
59
+ priority: int # 1=highest
60
+ rationale: str
61
+ assigned_persona: str
62
+ depends_on: list[str] = field(default_factory=list)
63
+ estimated_duration_ms: int = 0
64
+
65
+
66
+ class Council:
67
+ """
68
+ The Council — orchestrates multi-persona deliberation.
69
+
70
+ Flow:
71
+ 1. Receive issue + context
72
+ 2. Select relevant personas
73
+ 3. Parallel analysis by each persona
74
+ 4. Synthesis & debate (if conflicts)
75
+ 5. Consensus action plan
76
+ 6. Execute & learn
77
+ """
78
+
79
+ # Persona selection rules: issue keywords -> persona names
80
+ PERSONA_SELECTION_RULES = {
81
+ "architect": [
82
+ "architecture",
83
+ "structure",
84
+ "dependency",
85
+ "module",
86
+ "layer",
87
+ "refactor",
88
+ "blast radius",
89
+ "circular",
90
+ "dead code",
91
+ "coupling",
92
+ "cohesion",
93
+ "boundary",
94
+ "interface",
95
+ "contract",
96
+ ],
97
+ "security_officer": [
98
+ "security",
99
+ "vulnerability",
100
+ "attack",
101
+ "threat",
102
+ "exploit",
103
+ "injection",
104
+ "xss",
105
+ "csrf",
106
+ "ssrf",
107
+ "auth",
108
+ "authorization",
109
+ "secrets",
110
+ "compliance",
111
+ "encryption",
112
+ "owasp",
113
+ "cve",
114
+ ],
115
+ "test_engineer": [
116
+ "test",
117
+ "testing",
118
+ "coverage",
119
+ "flaky",
120
+ "regression",
121
+ "e2e",
122
+ "integration",
123
+ "unit test",
124
+ "visual",
125
+ "accessibility",
126
+ "browser",
127
+ "stress",
128
+ "load",
129
+ "contract",
130
+ "api",
131
+ ],
132
+ "performance_analyst": [
133
+ "performance",
134
+ "slow",
135
+ "latency",
136
+ "throughput",
137
+ "bottleneck",
138
+ "memory",
139
+ "cpu",
140
+ "profiling",
141
+ "benchmark",
142
+ "optimization",
143
+ "scalability",
144
+ "cache",
145
+ "async",
146
+ "concurrent",
147
+ ],
148
+ "devops_engineer": [
149
+ "deploy",
150
+ "ci",
151
+ "cd",
152
+ "pipeline",
153
+ "infrastructure",
154
+ "kubernetes",
155
+ "docker",
156
+ "container",
157
+ "monitoring",
158
+ "observability",
159
+ "logging",
160
+ "secrets",
161
+ "supply chain",
162
+ "rollback",
163
+ "feature flag",
164
+ ],
165
+ "code_reviewer": [
166
+ "code quality",
167
+ "readability",
168
+ "maintainability",
169
+ "pattern",
170
+ "anti-pattern",
171
+ "complexity",
172
+ "duplication",
173
+ "naming",
174
+ "documentation",
175
+ "style",
176
+ "error handling",
177
+ ],
178
+ "product_owner": [
179
+ "business",
180
+ "user",
181
+ "requirement",
182
+ "acceptance",
183
+ "flow",
184
+ "journey",
185
+ "feature",
186
+ "priority",
187
+ "compliance",
188
+ "regulation",
189
+ "risk",
190
+ "value",
191
+ ],
192
+ "incident_responder": [
193
+ "crash",
194
+ "error",
195
+ "incident",
196
+ "anomaly",
197
+ "debug",
198
+ "root cause",
199
+ "runtime",
200
+ "alert",
201
+ "outage",
202
+ "recovery",
203
+ "postmortem",
204
+ ],
205
+ }
206
+
207
+ def __init__(
208
+ self,
209
+ root: Path,
210
+ on_progress: Callable[[str], None] | None = None,
211
+ ):
212
+ self.root = root
213
+ self.on_progress = on_progress or (lambda _: None)
214
+ self.config = cfg.load(root)
215
+ self.brain_layers = self._load_brain_layers()
216
+ self.project_context = self._load_project_context()
217
+ self.personas: dict[str, BasePersona] = {}
218
+ self.session_history: list[CouncilSession] = []
219
+ self._initialize_personas()
220
+
221
+ def _load_brain_layers(self) -> dict[str, Layer]:
222
+ """Load layered brain from memory."""
223
+ layers_data = mem.get_layers(self.root)
224
+ if layers_data and layers_data.get("layers"):
225
+ return layers_from_dict(layers_data)
226
+ return {}
227
+
228
+ def _load_project_context(self) -> dict:
229
+ """Load project context from brain memory."""
230
+ brain = mem.get_brain(self.root)
231
+ return {
232
+ "project_purpose": brain.get("project_purpose", ""),
233
+ "project_domain": brain.get("project_domain", ""),
234
+ "frameworks": brain.get("frameworks", []),
235
+ "active_security_domains": brain.get("active_security_domains", []),
236
+ "file_count": brain.get("file_count", 0),
237
+ "route_count": brain.get("route_count", 0),
238
+ }
239
+
240
+ def _initialize_personas(self) -> None:
241
+ """Create all persona instances, keyed by short rule names."""
242
+ # Map registered class names (e.g. "SecurityOfficerPersona") to the
243
+ # short rule keys used in PERSONA_SELECTION_RULES ("security_officer").
244
+ short_name = {
245
+ "ArchitectPersona": "architect",
246
+ "SecurityOfficerPersona": "security_officer",
247
+ "TestEngineerPersona": "test_engineer",
248
+ "PerformanceAnalystPersona": "performance_analyst",
249
+ "DevOpsEngineerPersona": "devops_engineer",
250
+ "CodeReviewerPersona": "code_reviewer",
251
+ "ProductOwnerPersona": "product_owner",
252
+ "IncidentResponderPersona": "incident_responder",
253
+ }
254
+ for cls_name in list_personas():
255
+ key = short_name.get(cls_name, cls_name.lower())
256
+ persona = create_persona(
257
+ name=cls_name,
258
+ root=self.root,
259
+ brain_layers=self.brain_layers,
260
+ project_context=self.project_context,
261
+ config=self.config,
262
+ on_progress=self.on_progress,
263
+ )
264
+ if persona:
265
+ self.personas[key] = persona
266
+ _log.info(f"Initialized persona: {key} ({cls_name})")
267
+ else:
268
+ _log.warning(f"Failed to initialize persona: {cls_name}")
269
+
270
+ def _select_personas(self, issue: str, context: dict = None) -> list[str]:
271
+ """Select relevant personas using domain relevance scoring.
272
+
273
+ Scoring factors:
274
+ 1. Keyword match (0-5 points per persona)
275
+ 2. Expertise area alignment (0-3 points)
276
+ 3. Historical success rate for similar issues (0-2 points)
277
+ 4. Context-based relevance (file types, code patterns)
278
+ """
279
+ context = context or {}
280
+ issue_lower = issue.lower()
281
+ scores: dict[str, float] = {}
282
+
283
+ for persona_name, keywords in self.PERSONA_SELECTION_RULES.items():
284
+ if persona_name not in self.personas:
285
+ continue
286
+
287
+ score = 0.0
288
+
289
+ # Factor 1: Keyword match (0-5 points)
290
+ keyword_hits = sum(1 for kw in keywords if kw in issue_lower)
291
+ score += min(keyword_hits, 5)
292
+
293
+ # Factor 2: Expertise area alignment (0-3 points)
294
+ persona = self.personas[persona_name]
295
+ expertise = persona.get_expertise_areas()
296
+ expertise_overlap = sum(1 for e in expertise if e.lower() in issue_lower)
297
+ score += min(expertise_overlap * 1.5, 3.0)
298
+
299
+ # Factor 3: Historical success rate (0-2 points)
300
+ if hasattr(persona, "memory") and persona.memory.success_rates:
301
+ # Get success rate for similar issue types
302
+ rates = persona.memory.success_rates
303
+ avg_success = sum(rates.values()) / len(rates)
304
+ score += avg_success * 2.0
305
+
306
+ # Factor 4: Context-based relevance
307
+ # Check if issue mentions files/types this persona handles
308
+ context_files = context.get("files", [])
309
+ if context_files:
310
+ # Security persona gets bonus for security-related files
311
+ if persona_name == "security_officer":
312
+ sec_kws = ["auth", "crypto", "secret", "token"]
313
+ sec_files = [f for f in context_files if any(k in f.lower() for k in sec_kws)]
314
+ score += min(len(sec_files) * 0.5, 2.0)
315
+ # DevOps for infrastructure files
316
+ elif persona_name == "devops_engineer":
317
+ infra_kws = ["docker", "ci", "deploy", "k8s"]
318
+ infra_files = [
319
+ f for f in context_files if any(k in f.lower() for k in infra_kws)
320
+ ]
321
+ score += min(len(infra_files) * 0.5, 2.0)
322
+
323
+ if score > 0:
324
+ scores[persona_name] = score
325
+
326
+ # Always include Architect for structural perspective (bonus +2)
327
+ if "architect" in self.personas:
328
+ scores["architect"] = scores.get("architect", 0) + 2.0
329
+
330
+ # Sort by score descending
331
+ selected = sorted(scores.keys(), key=lambda p: scores[p], reverse=True)
332
+
333
+ # Minimum 3, maximum 5 personas for balanced deliberation
334
+ if len(selected) < 3:
335
+ defaults = ["architect", "security_officer", "test_engineer"]
336
+ for d in defaults:
337
+ if d in self.personas and d not in selected:
338
+ selected.append(d)
339
+
340
+ return selected[:5]
341
+
342
+ def _council_cache_key(self, issue: str, context: dict | None) -> str:
343
+ import hashlib as _hl
344
+ import json as _js
345
+
346
+ ctx_str = _js.dumps(context or {}, sort_keys=True, default=str)
347
+ raw = f"{issue}\n{ctx_str}"
348
+ return _hl.sha256(raw.encode()).hexdigest()[:16]
349
+
350
+ def _council_cache_path(self) -> Path:
351
+ return self.root / ".patchi" / "memory" / "council_cache.json"
352
+
353
+ def _council_cache_get(self, key: str) -> dict | None:
354
+ p = self._council_cache_path()
355
+ if not p.exists():
356
+ return None
357
+ try:
358
+ import json as _js
359
+ import time as _tm
360
+
361
+ data = _js.loads(p.read_text(encoding="utf-8"))
362
+ entry = data.get(key)
363
+ if not entry:
364
+ return None
365
+ # 7-day TTL
366
+ age = _tm.time() - entry.get("cached_at", 0)
367
+ if age > 7 * 24 * 3600:
368
+ return None
369
+ return entry.get("value")
370
+ except Exception:
371
+ return None
372
+
373
+ def _council_cache_set(self, key: str, value: dict) -> None:
374
+ p = self._council_cache_path()
375
+ try:
376
+ import json as _js
377
+ import time as _tm
378
+
379
+ data: dict = {}
380
+ if p.exists():
381
+ try:
382
+ data = _js.loads(p.read_text(encoding="utf-8"))
383
+ except Exception:
384
+ data = {}
385
+ data[key] = {"cached_at": _tm.time(), "value": value}
386
+ # keep top-5 recurring + prune to 50 entries
387
+ if len(data) > 50:
388
+ # drop oldest
389
+ sorted_keys = sorted(data, key=lambda k: data[k].get("cached_at", 0))
390
+ for k in sorted_keys[:-50]:
391
+ data.pop(k, None)
392
+ p.parent.mkdir(parents=True, exist_ok=True)
393
+ tmp = p.with_suffix(".tmp")
394
+ tmp.write_text(_js.dumps(data, indent=2))
395
+ if tmp.exists():
396
+ import sys as _sys
397
+
398
+ if _sys.platform == "win32" and p.exists():
399
+ p.unlink()
400
+ tmp.replace(p)
401
+ except Exception as exc: # noqa: BLE001
402
+ _log.debug("council cache set failed: %s", exc)
403
+
404
+ async def deliberate(self, issue: str, context: dict = None) -> CouncilSession:
405
+ """
406
+ Run a full council deliberation on an issue.
407
+
408
+ L4: Cached by SHA256(issue+context_hash) in .patchi/memory/council_cache.json 7-day TTL.
409
+ Returns a CouncilSession with decisions, synthesis, and action plan.
410
+ """
411
+ # L4 cache check
412
+ cache_key = self._council_cache_key(issue, context)
413
+ cached = self._council_cache_get(cache_key)
414
+ if cached:
415
+ self.on_progress(f"♻️ Council cache hit ({cache_key}) — 7d TTL")
416
+ # Rehydrate minimal session from cache
417
+ try:
418
+ sess = CouncilSession(
419
+ issue=issue,
420
+ context=context or {},
421
+ synthesis=cached.get("synthesis", ""),
422
+ action_plan=cached.get("action_plan", []),
423
+ consensus_reached=bool(cached.get("consensus_reached", False)),
424
+ completed_at=cached.get("completed_at", ""),
425
+ duration_ms=int(cached.get("duration_ms", 0)),
426
+ )
427
+ # persona_decisions are not fully rehydrated (lightweight cache)
428
+ self.session_history.append(sess)
429
+ return sess
430
+ except Exception as _exc:
431
+ _log.debug('suppressed: %s', _exc)
432
+ start_time = time.monotonic()
433
+ context = context or {}
434
+
435
+ self.on_progress(f"🏛️ Council convened for: {issue[:80]}...")
436
+
437
+ # Select personas with domain relevance scoring
438
+ selected_names = self._select_personas(issue, context)
439
+ self.on_progress(f"👥 Selected personas: {', '.join(selected_names)}")
440
+
441
+ session = CouncilSession(issue=issue, context=context)
442
+
443
+ # Phase 1: Parallel Analysis
444
+ self.on_progress("📋 Phase 1: Individual Analysis")
445
+ analysis_tasks = []
446
+ for name in selected_names:
447
+ persona = self.personas[name]
448
+ analysis_tasks.append(self._run_persona_analysis(persona, issue, context))
449
+
450
+ decisions = await asyncio.gather(*analysis_tasks, return_exceptions=True)
451
+
452
+ for name, decision in zip(selected_names, decisions, strict=False):
453
+ if isinstance(decision, Exception):
454
+ _log.error(f"Persona {name} failed: {decision}")
455
+ continue
456
+ session.persona_decisions.append(decision)
457
+
458
+ # Phase 2: Synthesis & Debate
459
+ self.on_progress("🤝 Phase 2: Synthesis & Debate")
460
+ session.synthesis = await self._synthesize(session)
461
+
462
+ # Phase 3: Action Plan
463
+ self.on_progress("📋 Phase 3: Action Plan")
464
+ session.action_plan = await self._create_action_plan(session)
465
+
466
+ # Phase 4: Consensus Check
467
+ session.consensus_reached = self._check_consensus(session)
468
+
469
+ session.completed_at = datetime.now(UTC).isoformat()
470
+ session.duration_ms = int((time.monotonic() - start_time) * 1000)
471
+
472
+ self.session_history.append(session)
473
+ self.on_progress(
474
+ f"✅ Council complete in {session.duration_ms}ms. Consensus: {session.consensus_reached}"
475
+ )
476
+
477
+ # L4 cache set
478
+ try:
479
+ self._council_cache_set(
480
+ cache_key,
481
+ {
482
+ "synthesis": session.synthesis,
483
+ "action_plan": session.action_plan,
484
+ "consensus_reached": session.consensus_reached,
485
+ "completed_at": session.completed_at,
486
+ "duration_ms": session.duration_ms,
487
+ },
488
+ )
489
+ except Exception as _exc:
490
+ _log.debug('suppressed: %s', _exc)
491
+ return session
492
+
493
+ async def _run_persona_analysis(
494
+ self,
495
+ persona: BasePersona,
496
+ issue: str,
497
+ context: dict,
498
+ ) -> PersonaDecision:
499
+ """Run analysis for a single persona (in thread pool for sync AI calls)."""
500
+ loop = asyncio.get_event_loop()
501
+ return await loop.run_in_executor(None, persona.analyze, issue, context)
502
+
503
+ async def _synthesize(self, session: CouncilSession) -> str:
504
+ """Synthesize persona decisions into a unified analysis."""
505
+ if not session.persona_decisions:
506
+ return "No persona decisions to synthesize."
507
+
508
+ # Build synthesis prompt
509
+ decisions_text = "\n\n".join(
510
+ [
511
+ f"=== {d.persona_name.upper()} ===\n"
512
+ f"Analysis: {d.analysis}\n"
513
+ f"Recommendation: {d.recommendation}\n"
514
+ f"Confidence: {d.confidence:.0%}\n"
515
+ f"Tools: {', '.join(d.tools_needed) if d.tools_needed else 'none'}\n"
516
+ f"Risks: {', '.join(d.risks) if d.risks else 'none'}"
517
+ for d in session.persona_decisions
518
+ ]
519
+ )
520
+
521
+ synthesis_prompt = f"""
522
+ Synthesize the following persona analyses into a unified assessment.
523
+
524
+ ORIGINAL ISSUE: {session.issue}
525
+
526
+ PERSONA DECISIONS:
527
+ {decisions_text}
528
+
529
+ Provide synthesis in this JSON format:
530
+ {{
531
+ "unified_analysis": "Synthesized analysis combining all perspectives",
532
+ "key_agreements": ["agreement1", "agreement2"],
533
+ "key_disagreements": ["disagreement1", "disagreement2"],
534
+ "risk_assessment": "Overall risk level and key risks",
535
+ "priority": "critical|high|medium|low",
536
+ "recommended_approach": "High-level approach summary"
537
+ }}
538
+ """
539
+
540
+ # Use Architect persona for synthesis (or first available)
541
+ synthesizer = self.personas.get("architect") or list(self.personas.values())[0]
542
+ result = synthesizer._call_ai_with_persona(
543
+ synthesis_prompt,
544
+ max_tokens=2000,
545
+ temperature=0.2,
546
+ )
547
+
548
+ if result:
549
+ return result.get("unified_analysis", "Synthesis failed")
550
+ return "AI synthesis unavailable; see individual decisions above."
551
+
552
+ async def _create_action_plan(self, session: CouncilSession) -> list[dict]:
553
+ """Create prioritized action plan from decisions."""
554
+ # Collect all tools needed
555
+ all_tools = []
556
+ for d in session.persona_decisions:
557
+ for tool in d.tools_needed:
558
+ all_tools.append(
559
+ {
560
+ "tool": tool,
561
+ "persona": d.persona_name,
562
+ "confidence": d.confidence,
563
+ "rationale": d.recommendation,
564
+ }
565
+ )
566
+
567
+ # Deduplicate and prioritize
568
+ tool_map: dict[str, dict] = {}
569
+ for t in all_tools:
570
+ key = t["tool"]
571
+ if key not in tool_map or t["confidence"] > tool_map[key]["confidence"]:
572
+ tool_map[key] = t
573
+
574
+ # Build action steps
575
+ steps = []
576
+ for i, (tool_name, info) in enumerate(tool_map.items()):
577
+ steps.append(
578
+ {
579
+ "tool": tool_name,
580
+ "parameters": {}, # Will be filled by executor
581
+ "priority": i + 1,
582
+ "rationale": info["rationale"],
583
+ "assigned_persona": info["persona"],
584
+ "confidence": info["confidence"],
585
+ }
586
+ )
587
+
588
+ return steps
589
+
590
+ def _check_consensus(self, session: CouncilSession) -> bool:
591
+ """Check if personas reached weighted consensus.
592
+
593
+ Uses weighted confidence based on:
594
+ 1. Persona expertise relevance to the issue
595
+ 2. Historical accuracy of each persona
596
+ 3. Style-based weight adjustments
597
+ """
598
+ if len(session.persona_decisions) < 2:
599
+ return True
600
+
601
+ # Calculate weights for each persona
602
+ weights = []
603
+ weighted_confidences = []
604
+
605
+ for decision in session.persona_decisions:
606
+ weight = self._calculate_persona_weight(decision, session.issue)
607
+ weights.append(weight)
608
+ weighted_confidences.append(decision.confidence * weight)
609
+
610
+ total_weight = sum(weights)
611
+ if total_weight == 0:
612
+ return False
613
+
614
+ # Weighted average confidence
615
+ weighted_avg = sum(weighted_confidences) / total_weight
616
+
617
+ # Weighted standard deviation (measures agreement)
618
+ weighted_variance = (
619
+ sum(
620
+ w * (c - weighted_avg) ** 2
621
+ for w, c in zip(weights, [d.confidence for d in session.persona_decisions], strict=False)
622
+ )
623
+ / total_weight
624
+ )
625
+ weighted_std = weighted_variance**0.5
626
+
627
+ # Check for semantic agreement (recommendations align)
628
+ semantic_agreement = self._check_semantic_agreement(session)
629
+
630
+ # Consensus criteria:
631
+ # 1. Weighted confidence > 0.6
632
+ # 2. Weighted std < 0.25 (low disagreement)
633
+ # 3. At least 60% semantic agreement on recommendations
634
+ consensus = weighted_avg > 0.6 and weighted_std < 0.25 and semantic_agreement >= 0.6
635
+
636
+ return consensus
637
+
638
+ def _calculate_persona_weight(self, decision: PersonaDecision, issue: str) -> float:
639
+ """Calculate weight for a persona based on expertise and history."""
640
+ weight = 1.0 # Base weight
641
+
642
+ persona = self.personas.get(decision.persona_name)
643
+ if not persona:
644
+ return weight
645
+
646
+ # Factor 1: Expertise relevance (0-1 bonus)
647
+ expertise = persona.get_expertise_areas()
648
+ issue_lower = issue.lower()
649
+ expertise_matches = sum(1 for e in expertise if e.lower() in issue_lower)
650
+ if expertise_matches > 0:
651
+ weight += min(expertise_matches * 0.25, 1.0)
652
+
653
+ # Factor 2: Historical accuracy (0-1 bonus)
654
+ if hasattr(persona, "memory") and persona.memory.success_rates:
655
+ rates = persona.memory.success_rates
656
+ avg_success = sum(rates.values()) / len(rates)
657
+ weight += avg_success * 1.0
658
+
659
+ # Factor 3: Style-based adjustments
660
+ style = persona.get_style()
661
+ if style == PersonaStyle.CAUTIOUS:
662
+ # Cautious personas get bonus for risk-related issues
663
+ if any(kw in issue_lower for kw in ["risk", "safety", "security", "regression"]):
664
+ weight += 0.3
665
+ elif style == PersonaStyle.AGGRESSIVE:
666
+ # Aggressive personas get bonus for edge cases
667
+ if any(kw in issue_lower for kw in ["edge case", "boundary", "极限", "corner"]):
668
+ weight += 0.3
669
+ elif style == PersonaStyle.PRAGMATIC:
670
+ # Pragmatic personas get bonus for implementation issues
671
+ if any(kw in issue_lower for kw in ["implement", "fix", "deploy", "ship"]):
672
+ weight += 0.3
673
+
674
+ return weight
675
+
676
+ def _check_semantic_agreement(self, session: CouncilSession) -> float:
677
+ """Check if persona recommendations semantically agree."""
678
+ if len(session.persona_decisions) < 2:
679
+ return 1.0
680
+
681
+ recommendations = [d.recommendation.lower() for d in session.persona_decisions]
682
+
683
+ # Simple semantic check: count common action keywords
684
+ action_keywords = ["fix", "refactor", "test", "deploy", "monitor", "review", "audit"]
685
+
686
+ keyword_counts = {}
687
+ for rec in recommendations:
688
+ for kw in action_keywords:
689
+ if kw in rec:
690
+ keyword_counts[kw] = keyword_counts.get(kw, 0) + 1
691
+
692
+ if not keyword_counts:
693
+ return 0.5 # Neutral if no clear actions
694
+
695
+ # Agreement is max keyword count / total personas
696
+ max_agreement = max(keyword_counts.values()) / len(session.persona_decisions)
697
+ return max_agreement
698
+
699
+ async def execute_action_plan(
700
+ self,
701
+ session: CouncilSession,
702
+ tool_executor: ToolExecutor,
703
+ max_steps: int = 10,
704
+ ) -> list[dict]:
705
+ """Execute the council's action plan using the tool executor."""
706
+ results = []
707
+ executed_tools = set()
708
+
709
+ for step in session.action_plan[:max_steps]:
710
+ tool_name = step["tool"]
711
+
712
+ # Check dependencies
713
+ deps_met = all(dep in executed_tools for dep in step.get("depends_on", []))
714
+ if not deps_met:
715
+ self.on_progress(f"⏳ Skipping {tool_name}: dependencies not met")
716
+ continue
717
+
718
+ self.on_progress(f"⚡ Executing: {tool_name} (by {step['assigned_persona']})")
719
+
720
+ try:
721
+ result = await tool_executor.execute(tool_name, step["parameters"])
722
+ results.append(
723
+ {
724
+ "tool": tool_name,
725
+ "success": result.get("success", False),
726
+ "result": result,
727
+ "step": step,
728
+ }
729
+ )
730
+ executed_tools.add(tool_name)
731
+
732
+ # Record outcome for learning
733
+ for decision in session.persona_decisions:
734
+ if decision.persona_name == step["assigned_persona"]:
735
+ decision.record_outcome(decision, result.get("success", False))
736
+
737
+ except Exception as e:
738
+ _log.error(f"Tool {tool_name} failed: {e}")
739
+ results.append(
740
+ {
741
+ "tool": tool_name,
742
+ "success": False,
743
+ "error": str(e),
744
+ "step": step,
745
+ }
746
+ )
747
+
748
+ return results
749
+
750
+ def get_session_history(self, limit: int = 10) -> list[CouncilSession]:
751
+ """Get recent council sessions."""
752
+ return self.session_history[-limit:]
753
+
754
+ def get_persona_stats(self) -> dict:
755
+ """Get statistics for all personas."""
756
+ stats = {}
757
+ for name, persona in self.personas.items():
758
+ decisions = persona.memory.decisions
759
+ stats[name] = {
760
+ "total_decisions": len(decisions),
761
+ "avg_confidence": sum(d.confidence for d in decisions) / len(decisions)
762
+ if decisions
763
+ else 0,
764
+ "success_rates": persona.memory.success_rates,
765
+ "style": persona.get_style().value,
766
+ }
767
+ return stats
768
+
769
+
770
+ class CouncilMode:
771
+ """Council operation modes."""
772
+
773
+ AUTO = "auto" # Full autonomous: deliberate -> execute -> learn
774
+ CONFIRM = "confirm" # Deliberate -> present plan -> wait for confirmation -> execute
775
+ ADVISORY = "advisory" # Deliberate only -> present recommendations
776
+
777
+
778
+ # Convenience function for CLI integration
779
+ async def run_council(
780
+ root: Path,
781
+ issue: str,
782
+ context: dict = None,
783
+ mode: str = CouncilMode.CONFIRM,
784
+ on_progress: Callable[[str], None] = None,
785
+ ) -> CouncilSession:
786
+ """Run a council session on an issue."""
787
+ council = Council(root, on_progress)
788
+ session = await council.deliberate(issue, context)
789
+
790
+ if mode == CouncilMode.AUTO:
791
+ from patchi.core.ai.tool_executor import ToolExecutor
792
+
793
+ executor = ToolExecutor(root)
794
+ await council.execute_action_plan(session, executor)
795
+ elif mode == CouncilMode.CONFIRM:
796
+ # Would present plan and wait for user confirmation
797
+ pass
798
+ # ADVISORY: just return session
799
+
800
+ return session