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.
- patchi/__init__.py +1 -0
- patchi/__main__.py +13 -0
- patchi/cli/__init__.py +0 -0
- patchi/cli/command_families.py +235 -0
- patchi/cli/commands/__init__.py +0 -0
- patchi/cli/commands/access_cmd.py +65 -0
- patchi/cli/commands/agent_stats_cmd.py +273 -0
- patchi/cli/commands/agents_cmd.py +187 -0
- patchi/cli/commands/ai_cmd.py +308 -0
- patchi/cli/commands/assure_cmd.py +538 -0
- patchi/cli/commands/audit_cmd.py +286 -0
- patchi/cli/commands/auto_cmd.py +110 -0
- patchi/cli/commands/blame_cmd.py +53 -0
- patchi/cli/commands/chains_cmd.py +327 -0
- patchi/cli/commands/charter_cmd.py +173 -0
- patchi/cli/commands/chat_cmd.py +597 -0
- patchi/cli/commands/check_cmd.py +232 -0
- patchi/cli/commands/cleanup_cmd.py +328 -0
- patchi/cli/commands/cockpit_cmd.py +208 -0
- patchi/cli/commands/commands_cmd.py +160 -0
- patchi/cli/commands/cross_repo_cmd.py +113 -0
- patchi/cli/commands/deps_cmd.py +110 -0
- patchi/cli/commands/dev_check_cmd.py +342 -0
- patchi/cli/commands/dev_cmd.py +445 -0
- patchi/cli/commands/doctor_cmd.py +457 -0
- patchi/cli/commands/findings_cmd.py +187 -0
- patchi/cli/commands/fix_cmd.py +648 -0
- patchi/cli/commands/fix_review_cmd.py +273 -0
- patchi/cli/commands/heatmap_cmd.py +62 -0
- patchi/cli/commands/help_cmd.py +159 -0
- patchi/cli/commands/hosted_cmd.py +980 -0
- patchi/cli/commands/init.py +456 -0
- patchi/cli/commands/key_cmd.py +571 -0
- patchi/cli/commands/learn_cmd.py +140 -0
- patchi/cli/commands/link_cmd.py +104 -0
- patchi/cli/commands/log_cmd.py +57 -0
- patchi/cli/commands/memory_cmd.py +215 -0
- patchi/cli/commands/model_cmd.py +207 -0
- patchi/cli/commands/notify_cmd.py +213 -0
- patchi/cli/commands/notify_prompts.py +29 -0
- patchi/cli/commands/patch_cmd.py +231 -0
- patchi/cli/commands/plan_cmd.py +128 -0
- patchi/cli/commands/plugins_cmd.py +307 -0
- patchi/cli/commands/queue_cmd.py +152 -0
- patchi/cli/commands/quick_cmd.py +159 -0
- patchi/cli/commands/ready_cmd.py +292 -0
- patchi/cli/commands/report_cmd.py +416 -0
- patchi/cli/commands/restrict_cmd.py +127 -0
- patchi/cli/commands/review_cmd.py +225 -0
- patchi/cli/commands/rules_cmd.py +233 -0
- patchi/cli/commands/scan_cmd.py +1908 -0
- patchi/cli/commands/settings_cmd.py +45 -0
- patchi/cli/commands/status_cmd.py +679 -0
- patchi/cli/commands/test_cmd.py +1052 -0
- patchi/cli/commands/trend_cmd.py +296 -0
- patchi/cli/commands/undo_cmd.py +235 -0
- patchi/cli/commands/update_cmd.py +345 -0
- patchi/cli/commands/verify_cmd.py +56 -0
- patchi/cli/commands/vr_cmd.py +440 -0
- patchi/cli/commands/watch_cmd.py +304 -0
- patchi/cli/commands/web_cmd.py +114 -0
- patchi/cli/console.py +101 -0
- patchi/cli/display/__init__.py +1 -0
- patchi/cli/display/live_audit.py +269 -0
- patchi/cli/display/live_progress.py +169 -0
- patchi/cli/framework.py +241 -0
- patchi/cli/logo.py +128 -0
- patchi/cli/main.py +222 -0
- patchi/cli/registry.py +1171 -0
- patchi/cli/style.py +68 -0
- patchi/cli/themes.py +187 -0
- patchi/cli/ux.py +444 -0
- patchi/core/__init__.py +0 -0
- patchi/core/agents/__init__.py +0 -0
- patchi/core/agents/api_fuzzer_agent.py +141 -0
- patchi/core/agents/attack_agent.py +242 -0
- patchi/core/agents/base.py +1012 -0
- patchi/core/agents/bug_predictor_agent.py +58 -0
- patchi/core/agents/build_tool_validator.py +150 -0
- patchi/core/agents/cache.py +192 -0
- patchi/core/agents/chaos_agent.py +133 -0
- patchi/core/agents/cicd_generator.py +240 -0
- patchi/core/agents/comment_scanner.py +280 -0
- patchi/core/agents/console_logging_agent.py +156 -0
- patchi/core/agents/coordinator.py +858 -0
- patchi/core/agents/core_scanner.py +269 -0
- patchi/core/agents/coverage_prioritizer.py +181 -0
- patchi/core/agents/dead_code_hygiene.py +296 -0
- patchi/core/agents/dead_code_scanner.py +692 -0
- patchi/core/agents/dependency_scanner.py +725 -0
- patchi/core/agents/doc_claim_agent.py +573 -0
- patchi/core/agents/duplicate_scanner.py +568 -0
- patchi/core/agents/env_scanner.py +410 -0
- patchi/core/agents/feature_flag_agent.py +89 -0
- patchi/core/agents/frontend_framework_agent.py +133 -0
- patchi/core/agents/goroutine_agent.py +82 -0
- patchi/core/agents/governor.py +1972 -0
- patchi/core/agents/i18n_agent.py +95 -0
- patchi/core/agents/license_compliance.py +247 -0
- patchi/core/agents/linking_agent.py +251 -0
- patchi/core/agents/memory_profiler_agent.py +102 -0
- patchi/core/agents/modernization_agent.py +84 -0
- patchi/core/agents/mutation_agent.py +174 -0
- patchi/core/agents/nl_query_agent.py +74 -0
- patchi/core/agents/promise_rejection_agent.py +100 -0
- patchi/core/agents/refactoring_agent.py +286 -0
- patchi/core/agents/resource_leak_agent.py +91 -0
- patchi/core/agents/route_graph_scanner.py +951 -0
- patchi/core/agents/run_agent.py +106 -0
- patchi/core/agents/rust_unwrap_agent.py +100 -0
- patchi/core/agents/sbom_generator.py +284 -0
- patchi/core/agents/scanners.py +67 -0
- patchi/core/agents/side/__init__.py +1 -0
- patchi/core/agents/side/build_agent.py +65 -0
- patchi/core/agents/side/format_agent.py +50 -0
- patchi/core/agents/side/install_agent.py +63 -0
- patchi/core/agents/side_file_scanner.py +549 -0
- patchi/core/agents/snapshot_drift_detector.py +146 -0
- patchi/core/agents/spa_route_inventory.py +203 -0
- patchi/core/agents/test_scanner.py +442 -0
- patchi/core/agents/tool_health.py +109 -0
- patchi/core/agents/tool_runner.py +162 -0
- patchi/core/agents/type_scanner.py +168 -0
- patchi/core/agents/ui_scanner.py +558 -0
- patchi/core/ai/__init__.py +20 -0
- patchi/core/ai/agent_audit.py +153 -0
- patchi/core/ai/agent_profiler.py +277 -0
- patchi/core/ai/client.py +499 -0
- patchi/core/ai/cost_tracker.py +107 -0
- patchi/core/ai/model_router.py +318 -0
- patchi/core/ai/orchestrator.py +625 -0
- patchi/core/ai/prompts.py +681 -0
- patchi/core/ai/smart.py +302 -0
- patchi/core/ai/tool_executor.py +24 -0
- patchi/core/ai/tools/__init__.py +10 -0
- patchi/core/ai/tools/executor.py +621 -0
- patchi/core/ai/tools/realize.py +1160 -0
- patchi/core/ai/tools/registry.py +1577 -0
- patchi/core/assurance/__init__.py +33 -0
- patchi/core/assurance/builder.py +211 -0
- patchi/core/assurance/graph.py +342 -0
- patchi/core/assurance/invariants.py +168 -0
- patchi/core/atomic.py +61 -0
- patchi/core/attackers/__init__.py +17 -0
- patchi/core/attackers/api_abuse.py +89 -0
- patchi/core/attackers/auth_bypass.py +73 -0
- patchi/core/attackers/base.py +91 -0
- patchi/core/attackers/business_logic.py +88 -0
- patchi/core/attackers/planner.py +108 -0
- patchi/core/attackers/priv_escalation.py +88 -0
- patchi/core/attackers/recon.py +87 -0
- patchi/core/brain/__init__.py +0 -0
- patchi/core/brain/ast_utils/__init__.py +81 -0
- patchi/core/brain/ast_utils/assignments.py +219 -0
- patchi/core/brain/ast_utils/calls.py +139 -0
- patchi/core/brain/ast_utils/config.py +132 -0
- patchi/core/brain/ast_utils/control_flow.py +74 -0
- patchi/core/brain/ast_utils/dead_symbols.py +205 -0
- patchi/core/brain/ast_utils/decorators.py +108 -0
- patchi/core/brain/ast_utils/helpers.py +144 -0
- patchi/core/brain/ast_utils/imports.py +169 -0
- patchi/core/brain/ast_utils/scan.py +143 -0
- patchi/core/brain/ast_utils/taint.py +310 -0
- patchi/core/brain/audit.py +379 -0
- patchi/core/brain/baseline.py +195 -0
- patchi/core/brain/blast_radius.py +312 -0
- patchi/core/brain/body_tags.py +144 -0
- patchi/core/brain/brain.py +1161 -0
- patchi/core/brain/brain_context.py +393 -0
- patchi/core/brain/brain_watcher.py +340 -0
- patchi/core/brain/charter.py +473 -0
- patchi/core/brain/classifier.py +214 -0
- patchi/core/brain/cockpit.py +350 -0
- patchi/core/brain/code_query.py +710 -0
- patchi/core/brain/contract.py +889 -0
- patchi/core/brain/contract_diff.py +155 -0
- patchi/core/brain/council.py +800 -0
- patchi/core/brain/cross_repo.py +104 -0
- patchi/core/brain/dead_code_tools.py +157 -0
- patchi/core/brain/doc_validator.py +197 -0
- patchi/core/brain/domain_activator.py +2183 -0
- patchi/core/brain/enriched_context.py +220 -0
- patchi/core/brain/file_corpus.py +220 -0
- patchi/core/brain/framework.py +953 -0
- patchi/core/brain/freshness.py +273 -0
- patchi/core/brain/git_aware.py +206 -0
- patchi/core/brain/heatmap.py +104 -0
- patchi/core/brain/ignore_parser.py +153 -0
- patchi/core/brain/import_graph.py +486 -0
- patchi/core/brain/languages.py +397 -0
- patchi/core/brain/layered_brain.py +419 -0
- patchi/core/brain/learning.py +229 -0
- patchi/core/brain/orphaned_endpoints.py +313 -0
- patchi/core/brain/personas/__init__.py +648 -0
- patchi/core/brain/personas/bad_user.py +165 -0
- patchi/core/brain/personas/base.py +349 -0
- patchi/core/brain/proactive.py +807 -0
- patchi/core/brain/project_context.py +121 -0
- patchi/core/brain/project_reader.py +521 -0
- patchi/core/brain/reasoning.py +269 -0
- patchi/core/brain/route_detector/__init__.py +15 -0
- patchi/core/brain/route_detector/base.py +31 -0
- patchi/core/brain/route_detector/csharp.py +209 -0
- patchi/core/brain/route_detector/file_based.py +81 -0
- patchi/core/brain/route_detector/go.py +152 -0
- patchi/core/brain/route_detector/java.py +195 -0
- patchi/core/brain/route_detector/javascript.py +197 -0
- patchi/core/brain/route_detector/php.py +158 -0
- patchi/core/brain/route_detector/python.py +193 -0
- patchi/core/brain/route_detector/registry.py +25 -0
- patchi/core/brain/route_detector/ruby.py +134 -0
- patchi/core/brain/route_detector/rust.py +216 -0
- patchi/core/brain/route_detector/swift.py +118 -0
- patchi/core/brain/route_mapper.py +265 -0
- patchi/core/brain/scanner.py +2136 -0
- patchi/core/brain/secrets.py +280 -0
- patchi/core/brain/symbol_graph.py +1955 -0
- patchi/core/brain/team.py +28 -0
- patchi/core/brain/trace_log.py +132 -0
- patchi/core/brain/type_checker/__init__.py +65 -0
- patchi/core/brain/type_checker/base.py +60 -0
- patchi/core/brain/type_checker/csharp.py +59 -0
- patchi/core/brain/type_checker/dart.py +59 -0
- patchi/core/brain/type_checker/go.py +81 -0
- patchi/core/brain/type_checker/java.py +75 -0
- patchi/core/brain/type_checker/kotlin.py +75 -0
- patchi/core/brain/type_checker/php.py +88 -0
- patchi/core/brain/type_checker/python.py +87 -0
- patchi/core/brain/type_checker/rust.py +81 -0
- patchi/core/brain/type_checker/swift.py +59 -0
- patchi/core/brain/type_checker/typescript.py +298 -0
- patchi/core/brain/understander.py +130 -0
- patchi/core/brain/verify.py +379 -0
- patchi/core/campaigns/__init__.py +14 -0
- patchi/core/campaigns/base.py +62 -0
- patchi/core/campaigns/data_flow_abuse.py +153 -0
- patchi/core/campaigns/orchestrator.py +140 -0
- patchi/core/campaigns/state_transitions.py +178 -0
- patchi/core/ci_bundle.py +104 -0
- patchi/core/config.py +311 -0
- patchi/core/constants.py +245 -0
- patchi/core/debug/__init__.py +25 -0
- patchi/core/debug/adapters/__init__.py +11 -0
- patchi/core/debug/adapters/codelldb.py +379 -0
- patchi/core/debug/adapters/node.py +222 -0
- patchi/core/debug/adapters/powershell.py +210 -0
- patchi/core/debug/adapters/python.py +244 -0
- patchi/core/debug/capture.py +210 -0
- patchi/core/debug/dap_client.py +268 -0
- patchi/core/detector/__init__.py +0 -0
- patchi/core/detector/audit.py +194 -0
- patchi/core/detector/bus.py +226 -0
- patchi/core/detector/dispatcher.py +275 -0
- patchi/core/detector/event.py +254 -0
- patchi/core/detector/sigma_engine.py +378 -0
- patchi/core/detector/triage.py +289 -0
- patchi/core/export/sarif.py +113 -0
- patchi/core/fix/__init__.py +17 -0
- patchi/core/fix/applier.py +574 -0
- patchi/core/fix/base.py +510 -0
- patchi/core/fix/code_fixer.py +169 -0
- patchi/core/fix/dead_code_remover.py +131 -0
- patchi/core/fix/fix_agents.py +775 -0
- patchi/core/fix/patch.py +444 -0
- patchi/core/fix/risk_gate.py +383 -0
- patchi/core/fix/security_fixer.py +93 -0
- patchi/core/fix/verify_loop.py +204 -0
- patchi/core/fuzz/__init__.py +8 -0
- patchi/core/fuzz/corpus.py +137 -0
- patchi/core/fuzz/input_fuzzer.py +256 -0
- patchi/core/fuzz/sequence_fuzzer.py +162 -0
- patchi/core/fuzz/state_fuzzer.py +236 -0
- patchi/core/health.py +407 -0
- patchi/core/hosted/__init__.py +1 -0
- patchi/core/hosted/anomaly.py +298 -0
- patchi/core/hosted/audit_log.py +102 -0
- patchi/core/hosted/compliance_report.py +148 -0
- patchi/core/hosted/ip_reputation.py +169 -0
- patchi/core/hosted/log_parsers.py +251 -0
- patchi/core/hosted/tokens.py +127 -0
- patchi/core/hosted/watchlist.py +164 -0
- patchi/core/hosted/webhooks.py +156 -0
- patchi/core/memory.py +437 -0
- patchi/core/notifications/__init__.py +18 -0
- patchi/core/notifications/channels.py +192 -0
- patchi/core/notifications/digest.py +141 -0
- patchi/core/notifications/escalation.py +158 -0
- patchi/core/notifications/notifier.py +182 -0
- patchi/core/notifications/quiet_hours.py +69 -0
- patchi/core/plugins/__init__.py +48 -0
- patchi/core/plugins/analyzer.py +326 -0
- patchi/core/plugins/analyzers/__init__.py +6 -0
- patchi/core/plugins/analyzers/dead_code_analyzer.py +120 -0
- patchi/core/plugins/analyzers/secret_scanner.py +158 -0
- patchi/core/plugins/analyzers/todo_scanner.py +115 -0
- patchi/core/plugins/registry.py +335 -0
- patchi/core/queue.py +375 -0
- patchi/core/reliability/__init__.py +13 -0
- patchi/core/reliability/chaos.py +163 -0
- patchi/core/reliability/fault_injector.py +172 -0
- patchi/core/reliability/idempotency.py +128 -0
- patchi/core/reliability/recovery.py +147 -0
- patchi/core/runtime/__init__.py +5 -0
- patchi/core/runtime/tracer.py +230 -0
- patchi/core/scan_bus.py +156 -0
- patchi/core/security/__init__.py +1 -0
- patchi/core/security/ai_validator.py +462 -0
- patchi/core/security/app_mapper.py +216 -0
- patchi/core/security/app_profile.py +464 -0
- patchi/core/security/attack_feedback.py +141 -0
- patchi/core/security/attack_scenarios/__init__.py +1 -0
- patchi/core/security/attack_tree.py +176 -0
- patchi/core/security/auth_audit_agent.py +344 -0
- patchi/core/security/authz_agent.py +602 -0
- patchi/core/security/auto_fix_proactive.py +620 -0
- patchi/core/security/auto_fixer.py +660 -0
- patchi/core/security/auto_ticket.py +53 -0
- patchi/core/security/bandit_agent.py +152 -0
- patchi/core/security/blast_radius.py +226 -0
- patchi/core/security/browser_tester.py +333 -0
- patchi/core/security/business_logic_agent.py +263 -0
- patchi/core/security/catch_block_auditor.py +395 -0
- patchi/core/security/cdn_cache_agent.py +357 -0
- patchi/core/security/chain_analyzer.py +321 -0
- patchi/core/security/chain_to_assurance.py +168 -0
- patchi/core/security/charter.py +510 -0
- patchi/core/security/cloud_waf_detector.py +295 -0
- patchi/core/security/codeql_agent.py +233 -0
- patchi/core/security/compliance_agent.py +348 -0
- patchi/core/security/confidence_gate.py +423 -0
- patchi/core/security/container_scanner.py +282 -0
- patchi/core/security/crypto_agent.py +378 -0
- patchi/core/security/cve_monitor.py +262 -0
- patchi/core/security/dast_agent.py +865 -0
- patchi/core/security/defectdojo.py +238 -0
- patchi/core/security/defenders/__init__.py +75 -0
- patchi/core/security/defenders/auth_middleware.py +115 -0
- patchi/core/security/defenders/base.py +97 -0
- patchi/core/security/defenders/block_ip.py +71 -0
- patchi/core/security/defenders/block_ws_origin.py +36 -0
- patchi/core/security/defenders/crypto_fix.py +118 -0
- patchi/core/security/defenders/enforce_rate_limit.py +108 -0
- patchi/core/security/defenders/fix_code.py +93 -0
- patchi/core/security/defenders/invalidate_session.py +40 -0
- patchi/core/security/defenders/patch_config.py +70 -0
- patchi/core/security/defenders/rotate_secret.py +54 -0
- patchi/core/security/defenders/suspend_account.py +41 -0
- patchi/core/security/defenders/update_dependency.py +160 -0
- patchi/core/security/defense_layer.py +217 -0
- patchi/core/security/dependency_vulnerability_agent.py +616 -0
- patchi/core/security/detection_pipeline.py +289 -0
- patchi/core/security/dns_security_agent.py +497 -0
- patchi/core/security/domain_activator_v2.py +913 -0
- patchi/core/security/domain_loader.py +1004 -0
- patchi/core/security/domains/__init__.py +0 -0
- patchi/core/security/email_authentication_agent.py +417 -0
- patchi/core/security/env_var_validator.py +304 -0
- patchi/core/security/evidence.py +100 -0
- patchi/core/security/falco_runtime_agent.py +387 -0
- patchi/core/security/fix-playbooks/__init__.py +0 -0
- patchi/core/security/gated_finding.py +85 -0
- patchi/core/security/git_diff_activator.py +367 -0
- patchi/core/security/governance.py +208 -0
- patchi/core/security/gradual_gate.py +34 -0
- patchi/core/security/history.py +315 -0
- patchi/core/security/iac_scanner.py +250 -0
- patchi/core/security/ignore_expiry.py +111 -0
- patchi/core/security/ignore_learner.py +416 -0
- patchi/core/security/injection_agent.py +319 -0
- patchi/core/security/insecure_randomness_agent.py +288 -0
- patchi/core/security/intent_analyzer.py +326 -0
- patchi/core/security/jwt_agent.py +453 -0
- patchi/core/security/kubernetes_agent.py +638 -0
- patchi/core/security/layer2_orchestrator.py +341 -0
- patchi/core/security/llm_security_agent.py +252 -0
- patchi/core/security/misconfig_agent.py +600 -0
- patchi/core/security/mobile_security_agent.py +474 -0
- patchi/core/security/network_agent.py +376 -0
- patchi/core/security/noise_filter.py +274 -0
- patchi/core/security/orchestrator.py +313 -0
- patchi/core/security/pattern_context.py +125 -0
- patchi/core/security/pattern_loader.py +101 -0
- patchi/core/security/pentest/__init__.py +4 -0
- patchi/core/security/pentest/base.py +58 -0
- patchi/core/security/pentest/dalfox_adapter.py +40 -0
- patchi/core/security/pentest/ffuf_adapter.py +48 -0
- patchi/core/security/pentest/nuclei_adapter.py +50 -0
- patchi/core/security/pentest/registry.py +80 -0
- patchi/core/security/pentest/shannon_adapter.py +107 -0
- patchi/core/security/pentest/sqlmap_adapter.py +56 -0
- patchi/core/security/pentest/zap_adapter.py +52 -0
- patchi/core/security/plan_auditor.py +276 -0
- patchi/core/security/policy_engine.py +272 -0
- patchi/core/security/prechecks.py +172 -0
- patchi/core/security/precommit_hook.py +247 -0
- patchi/core/security/privacy_agent.py +361 -0
- patchi/core/security/push_notification_agent.py +417 -0
- patchi/core/security/pysa_agent.py +143 -0
- patchi/core/security/reasoning.py +442 -0
- patchi/core/security/red_team_agent.py +184 -0
- patchi/core/security/red_team_engine.py +1142 -0
- patchi/core/security/remediation.py +360 -0
- patchi/core/security/request_interceptor.py +294 -0
- patchi/core/security/runtime_validator.py +245 -0
- patchi/core/security/saml_sso_agent.py +401 -0
- patchi/core/security/sast_agent.py +213 -0
- patchi/core/security/sast_gate.py +95 -0
- patchi/core/security/scheduler.py +287 -0
- patchi/core/security/secrets_runtime_agent.py +476 -0
- patchi/core/security/security_agents.py +175 -0
- patchi/core/security/security_config.py +551 -0
- patchi/core/security/security_probe.py +762 -0
- patchi/core/security/security_taint.py +1005 -0
- patchi/core/security/sensitive_data_agent.py +271 -0
- patchi/core/security/service_mesh_agent.py +412 -0
- patchi/core/security/session_management_agent.py +245 -0
- patchi/core/security/ssrf_agent.py +422 -0
- patchi/core/security/supply_chain.py +449 -0
- patchi/core/security/threat_model_generator.py +378 -0
- patchi/core/security/threat_model_updater.py +196 -0
- patchi/core/security/tool_adapters.py +135 -0
- patchi/core/security/tool_verify.py +173 -0
- patchi/core/security/websocket_security_agent.py +188 -0
- patchi/core/snapshot.py +242 -0
- patchi/core/tenant.py +386 -0
- patchi/core/testing/__init__.py +17 -0
- patchi/core/testing/_browser.py +147 -0
- patchi/core/testing/ads_agent.py +141 -0
- patchi/core/testing/api_contract_agent.py +450 -0
- patchi/core/testing/app_discovery_agent.py +641 -0
- patchi/core/testing/app_launcher.py +184 -0
- patchi/core/testing/browser_test_agent.py +516 -0
- patchi/core/testing/e2e_flow_agent.py +354 -0
- patchi/core/testing/flake_detector_agent.py +292 -0
- patchi/core/testing/gate.py +31 -0
- patchi/core/testing/live_test_runner.py +304 -0
- patchi/core/testing/live_v2/__init__.py +1 -0
- patchi/core/testing/live_v2/browser_pool.py +349 -0
- patchi/core/testing/live_v2/browser_test_runner.py +313 -0
- patchi/core/testing/live_v2/runner.py +626 -0
- patchi/core/testing/live_v2/screenshot_manager.py +474 -0
- patchi/core/testing/live_v2/stress_orchestrator.py +523 -0
- patchi/core/testing/live_v2/video_recorder.py +303 -0
- patchi/core/testing/regression_agent.py +302 -0
- patchi/core/testing/security_test_agent.py +253 -0
- patchi/core/testing/stress_test_agent.py +203 -0
- patchi/core/testing/test_agents.py +50 -0
- patchi/core/testing/test_config.py +194 -0
- patchi/core/testing/ui_accessibility_agent.py +324 -0
- patchi/core/testing/ui_button_agent.py +294 -0
- patchi/core/testing/ui_layout_agent.py +330 -0
- patchi/core/testing/unit_test_agent.py +1149 -0
- patchi/core/testing/visual_regression_agent.py +260 -0
- patchi/web/__init__.py +1 -0
- patchi/web/api/__init__.py +1 -0
- patchi/web/api/brain_map.py +114 -0
- patchi/web/api/charts.py +47 -0
- patchi/web/api/chat.py +224 -0
- patchi/web/api/cicd.py +576 -0
- patchi/web/api/dast_evidence.py +49 -0
- patchi/web/api/dev_check.py +320 -0
- patchi/web/api/fix.py +134 -0
- patchi/web/api/guard.py +65 -0
- patchi/web/api/hosted.py +128 -0
- patchi/web/api/hosted_v2.py +234 -0
- patchi/web/api/live_testing.py +699 -0
- patchi/web/api/scan.py +582 -0
- patchi/web/api/smart.py +111 -0
- patchi/web/api/tenant.py +117 -0
- patchi/web/api/tools.py +85 -0
- patchi/web/api_legacy.py +2401 -0
- patchi/web/app.py +310 -0
- patchi/web/events.py +483 -0
- patchi/web/routes/__init__.py +1 -0
- patchi/web/routes/assurance.py +541 -0
- patchi/web/routes/brain.py +36 -0
- patchi/web/routes/charter.py +227 -0
- patchi/web/routes/chat.py +23 -0
- patchi/web/routes/dashboard.py +558 -0
- patchi/web/routes/dashboard_v2.py +402 -0
- patchi/web/routes/findings.py +330 -0
- patchi/web/routes/guard.py +35 -0
- patchi/web/routes/history.py +126 -0
- patchi/web/routes/landing.py +52 -0
- patchi/web/routes/live_testing.py +24 -0
- patchi/web/routes/review.py +57 -0
- patchi/web/routes/self_improvement.py +110 -0
- patchi/web/routes/settings.py +49 -0
- patchi/web/routes/tokens.py +29 -0
- patchi/web/spawn.py +139 -0
- patchi/web/ws.py +141 -0
- patchi-0.7.5.dist-info/METADATA +315 -0
- patchi-0.7.5.dist-info/RECORD +496 -0
- patchi-0.7.5.dist-info/WHEEL +5 -0
- patchi-0.7.5.dist-info/licenses/LICENSE +60 -0
- 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
|