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,558 @@
|
|
|
1
|
+
"""Unified Dashboard — Single web entry point for Patchi.
|
|
2
|
+
|
|
3
|
+
Mission: v2 as base + v1 enhancements + full CLI wrapper.
|
|
4
|
+
- All v2 pages: brain-map, council, attack-timeline, live-tests, hosted
|
|
5
|
+
- v1 enhancements: brain map viz, health components, recent findings, cost alerts, project context
|
|
6
|
+
- Full CLI wrapper: every CLI command exposed via web UI/API
|
|
7
|
+
- Single design: one nav, one WebSocket (/ws), one template env (templates_v2)
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from fastapi import APIRouter, Request, WebSocket, WebSocketDisconnect
|
|
17
|
+
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
|
|
18
|
+
from fastapi.templating import Jinja2Templates
|
|
19
|
+
|
|
20
|
+
from patchi.core import config as cfg
|
|
21
|
+
from patchi.core.ai.tools.executor import ToolExecutor, WebConfirmationProvider
|
|
22
|
+
|
|
23
|
+
router = APIRouter()
|
|
24
|
+
|
|
25
|
+
_log = logging.getLogger("patchi.web.dashboard")
|
|
26
|
+
|
|
27
|
+
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
|
28
|
+
templates.env.filters["tojson"] = lambda v: json.dumps(v)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
32
|
+
# Helpers
|
|
33
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
def _safe_mode(root: Path) -> str:
|
|
36
|
+
try:
|
|
37
|
+
return cfg.load(root).get("mode", "confirm")
|
|
38
|
+
except Exception:
|
|
39
|
+
return "confirm"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _collect_recent_findings(root: Path, limit: int = 10) -> list[dict]:
|
|
43
|
+
"""Collect recent findings from all scanners."""
|
|
44
|
+
from patchi.core import memory as mem
|
|
45
|
+
scan_results = mem.get_scan_results(root)
|
|
46
|
+
findings = []
|
|
47
|
+
for scanner, data in scan_results.items():
|
|
48
|
+
for finding in data.get("findings", [])[-limit:]:
|
|
49
|
+
if isinstance(finding, dict):
|
|
50
|
+
finding["source_scanner"] = scanner
|
|
51
|
+
findings.append(finding)
|
|
52
|
+
findings.sort(key=lambda f: f.get("timestamp", ""), reverse=True)
|
|
53
|
+
return findings
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _get_brain_data(root: Path) -> dict:
|
|
57
|
+
"""Get consolidated brain data for dashboard."""
|
|
58
|
+
from patchi.core import memory as mem
|
|
59
|
+
from patchi.core.health import compute as compute_health
|
|
60
|
+
|
|
61
|
+
brain = mem.get_brain(root)
|
|
62
|
+
hs = compute_health(root)
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
"health_score": hs.total,
|
|
66
|
+
"health_grade": hs.grade,
|
|
67
|
+
"health_components": hs.to_dict().get("components", {}),
|
|
68
|
+
"file_count": brain.get("file_count", 0),
|
|
69
|
+
"route_count": brain.get("route_count", 0),
|
|
70
|
+
"framework": brain.get("framework", "Unknown"),
|
|
71
|
+
"languages": brain.get("languages", {}),
|
|
72
|
+
"project_purpose": brain.get("project_purpose", ""),
|
|
73
|
+
"project_domain": brain.get("project_domain", ""),
|
|
74
|
+
"active_security_domains": brain.get("active_security_domains", []),
|
|
75
|
+
"recent_findings": _collect_recent_findings(root, 10),
|
|
76
|
+
"stale": brain.get("stale", False),
|
|
77
|
+
"last_scan": brain.get("last_scan", ""),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
82
|
+
# Main Dashboard (/)
|
|
83
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
@router.get("/", response_class=HTMLResponse)
|
|
86
|
+
async def dashboard(request: Request):
|
|
87
|
+
"""Unified Mission Control — single entry point at /."""
|
|
88
|
+
root = request.app.state.root
|
|
89
|
+
|
|
90
|
+
cost_alert = None
|
|
91
|
+
try:
|
|
92
|
+
from patchi.core.tenant import check_tenant_cost_alert
|
|
93
|
+
cost_alert = check_tenant_cost_alert(root, cfg.load(root))
|
|
94
|
+
except Exception as _exc:
|
|
95
|
+
_log.debug('suppressed: %s', _exc)
|
|
96
|
+
|
|
97
|
+
import asyncio as _asyncio
|
|
98
|
+
brain_data = await _asyncio.to_thread(_get_brain_data, request.app.state.root)
|
|
99
|
+
|
|
100
|
+
return templates.TemplateResponse(
|
|
101
|
+
request,
|
|
102
|
+
"dashboard.html",
|
|
103
|
+
{
|
|
104
|
+
"request": request,
|
|
105
|
+
**brain_data,
|
|
106
|
+
"cost_alert": cost_alert,
|
|
107
|
+
"mode": _safe_mode(request.app.state.root),
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@router.get("/landing", response_class=HTMLResponse)
|
|
113
|
+
async def landing_page(request: Request):
|
|
114
|
+
"""Public landing / marketing page (templates/landing.html)."""
|
|
115
|
+
root = request.app.state.root
|
|
116
|
+
brain = _get_brain_data(root)
|
|
117
|
+
brain["active_domains"] = brain.get("active_security_domains", [])
|
|
118
|
+
return templates.TemplateResponse(
|
|
119
|
+
request,
|
|
120
|
+
"landing.html",
|
|
121
|
+
{"request": request, **brain},
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@router.get("/doctor", response_class=HTMLResponse)
|
|
126
|
+
async def doctor_page(request: Request):
|
|
127
|
+
"""Web front-end for `p doctor` — shows environment diagnostics."""
|
|
128
|
+
from patchi.web.api_legacy import web_doctor
|
|
129
|
+
|
|
130
|
+
resp = await web_doctor(request)
|
|
131
|
+
data = json.loads(resp.body)
|
|
132
|
+
checks = data.get("checks", [])
|
|
133
|
+
errors = data.get("errors", 0)
|
|
134
|
+
|
|
135
|
+
rows = []
|
|
136
|
+
for c in checks:
|
|
137
|
+
badge = "ok" if c.get("ok") else "fail"
|
|
138
|
+
rows.append(
|
|
139
|
+
f"<tr><td>{c.get('label','')}</td>"
|
|
140
|
+
f"<td class='badge {badge}'>{'OK' if c.get('ok') else 'FAIL'}</td>"
|
|
141
|
+
f"<td>{c.get('note','')}</td></tr>"
|
|
142
|
+
)
|
|
143
|
+
rows_html = "\n".join(rows)
|
|
144
|
+
|
|
145
|
+
html = f"""<!doctype html>
|
|
146
|
+
<html lang="en"><head><meta charset="utf-8">
|
|
147
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
148
|
+
<title>Patchi · Doctor</title>
|
|
149
|
+
<style>
|
|
150
|
+
body {{ font-family: ui-sans-serif, system-ui, sans-serif; margin: 0; background: #0d1117; color: #e6edf3; padding: 2rem; }}
|
|
151
|
+
h1 {{ font-size: 1.6rem; }} .wrap {{ max-width: 720px; margin: 0 auto; }}
|
|
152
|
+
table {{ width: 100%; border-collapse: collapse; margin-top: 1rem; }}
|
|
153
|
+
td {{ padding: .6rem .8rem; border-bottom: 1px solid #21262d; }}
|
|
154
|
+
.badge {{ font-weight: 600; }}
|
|
155
|
+
.badge.ok {{ color: #3fb950; }} .badge.fail {{ color: #f85149; }}
|
|
156
|
+
.summary {{ margin-top: 1rem; font-weight: 600; }}
|
|
157
|
+
a {{ color: #58a6ff; }}
|
|
158
|
+
</style></head>
|
|
159
|
+
<body><div class="wrap">
|
|
160
|
+
<h1>Patchi Doctor</h1>
|
|
161
|
+
<p>Environment diagnostics for this Patchi installation.</p>
|
|
162
|
+
<table>{rows_html}</table>
|
|
163
|
+
<div class="summary">{'All checks passed.' if errors == 0 else str(errors) + ' check(s) failed.'}</div>
|
|
164
|
+
<p><a href="/">← Back to Mission Control</a></p>
|
|
165
|
+
</div></body></html>"""
|
|
166
|
+
return HTMLResponse(html)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
170
|
+
# HTML Page Routes
|
|
171
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
@router.get("/brain-map", response_class=HTMLResponse)
|
|
174
|
+
async def brain_map_page(request: Request):
|
|
175
|
+
"""Brain map visualization with full import graph."""
|
|
176
|
+
root = request.app.state.root
|
|
177
|
+
from patchi.core import memory as mem
|
|
178
|
+
layers_data = mem.get_layers(root)
|
|
179
|
+
return templates.TemplateResponse(
|
|
180
|
+
request, "brain.html", {"request": request, "layers_data": layers_data}
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@router.get("/council", response_class=HTMLResponse)
|
|
185
|
+
async def council_page(request: Request):
|
|
186
|
+
"""Council deliberation view with session history."""
|
|
187
|
+
root = request.app.state.root
|
|
188
|
+
from patchi.core import memory as mem
|
|
189
|
+
council_sessions = mem.read(mem.MemoryCategory.ISSUES, root)
|
|
190
|
+
council_sessions = [s for s in council_sessions if s.get("type") == "council_session"]
|
|
191
|
+
return templates.TemplateResponse(
|
|
192
|
+
request, "council.html", {"request": request, "council_sessions": council_sessions[-20:]}
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@router.get("/attack-timeline", response_class=HTMLResponse)
|
|
197
|
+
async def attack_timeline_page(request: Request):
|
|
198
|
+
"""Attack simulation timeline (Red Team results)."""
|
|
199
|
+
root = request.app.state.root
|
|
200
|
+
from patchi.core import memory as mem
|
|
201
|
+
scan_results = mem.get_scan_results(root)
|
|
202
|
+
attack_data = scan_results.get("RedTeamEngineAgent", {})
|
|
203
|
+
return templates.TemplateResponse(
|
|
204
|
+
request, "attacks.html", {"request": request, "attack_data": attack_data}
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@router.get("/live-tests", response_class=HTMLResponse)
|
|
209
|
+
async def live_tests_page(request: Request):
|
|
210
|
+
"""Live test session monitor."""
|
|
211
|
+
root = request.app.state.root
|
|
212
|
+
from patchi.core import memory as mem
|
|
213
|
+
scan_results = mem.get_scan_results(root)
|
|
214
|
+
test_data = scan_results.get("TestRunner", {})
|
|
215
|
+
return templates.TemplateResponse(
|
|
216
|
+
request, "live_testing.html", {"request": request, "test_data": test_data}
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@router.get("/hosted", response_class=HTMLResponse)
|
|
221
|
+
async def hosted_page(request: Request):
|
|
222
|
+
"""Hosted mode dashboard (experimental)."""
|
|
223
|
+
root = request.app.state.root
|
|
224
|
+
from patchi.core import memory as mem
|
|
225
|
+
scan_results = mem.get_scan_results(root)
|
|
226
|
+
hosted_data = scan_results.get("HostedModeAgent", {})
|
|
227
|
+
return templates.TemplateResponse(
|
|
228
|
+
request, "hosted.html", {"request": request, "hosted_data": hosted_data}
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
233
|
+
# CLI Wrapper API Routes — Every CLI command exposed
|
|
234
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
@router.get("/api/cli/commands")
|
|
237
|
+
async def list_cli_commands(request: Request):
|
|
238
|
+
"""List all available CLI commands for web exposure."""
|
|
239
|
+
from patchi.cli.registry import COMMANDS as REGISTRY_COMMANDS
|
|
240
|
+
commands = []
|
|
241
|
+
for cmd in REGISTRY_COMMANDS:
|
|
242
|
+
commands.append({
|
|
243
|
+
"name": cmd.name,
|
|
244
|
+
"help": cmd.help,
|
|
245
|
+
"args": [{"name": a.name, "help": a.help, "required": not a.nargs or a.nargs != "?"} for a in cmd.args],
|
|
246
|
+
"subcommands": [{"name": sc.name, "help": sc.help} for sc in cmd.subcommands] if cmd.subcommands else [],
|
|
247
|
+
})
|
|
248
|
+
return {"commands": commands}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@router.get("/api/v2/agents/stream")
|
|
252
|
+
async def agents_stream(request: Request):
|
|
253
|
+
"""Server-sent events stream for live agent progress."""
|
|
254
|
+
root = request.app.state.root
|
|
255
|
+
from patchi.core import memory as mem
|
|
256
|
+
|
|
257
|
+
async def event_generator():
|
|
258
|
+
scan_results = mem.get_scan_results(root)
|
|
259
|
+
for scanner, data in scan_results.items():
|
|
260
|
+
yield f"data: {json.dumps({'agent': scanner, 'status': 'completed', 'findings': len(data.get('findings', []))})}\n\n"
|
|
261
|
+
await asyncio.sleep(0.1)
|
|
262
|
+
|
|
263
|
+
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@router.get("/api/v2/brain/layers")
|
|
267
|
+
async def get_brain_layers(request: Request):
|
|
268
|
+
"""Get brain layers for visualization."""
|
|
269
|
+
root = request.app.state.root
|
|
270
|
+
from patchi.core import memory as mem
|
|
271
|
+
layers_data = mem.get_layers(root)
|
|
272
|
+
return layers_data
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@router.get("/api/v2/council/sessions")
|
|
276
|
+
async def get_council_sessions(request: Request):
|
|
277
|
+
"""Get council session history."""
|
|
278
|
+
root = request.app.state.root
|
|
279
|
+
from patchi.core import memory as mem
|
|
280
|
+
history = mem.read(mem.MemoryCategory.ISSUES, root)
|
|
281
|
+
sessions = [s for s in history if s.get("type") == "council_session"]
|
|
282
|
+
return {"sessions": sessions[-50:]}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@router.get("/api/v2/attack/timeline")
|
|
286
|
+
async def get_attack_timeline(request: Request):
|
|
287
|
+
"""Get attack simulation timeline."""
|
|
288
|
+
root = request.app.state.root
|
|
289
|
+
from patchi.core import memory as mem
|
|
290
|
+
scan_results = mem.get_scan_results(root)
|
|
291
|
+
attack_data = scan_results.get("RedTeamEngineAgent", {})
|
|
292
|
+
return attack_data
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@router.get("/api/v2/live-tests/status")
|
|
296
|
+
async def get_live_test_status(request: Request):
|
|
297
|
+
"""Get live test status."""
|
|
298
|
+
root = request.app.state.root
|
|
299
|
+
from patchi.core import memory as mem
|
|
300
|
+
scan_results = mem.get_scan_results(root)
|
|
301
|
+
test_data = scan_results.get("TestRunner", {})
|
|
302
|
+
return test_data
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@router.post("/api/v2/tools/execute")
|
|
306
|
+
async def execute_tool(request: Request):
|
|
307
|
+
"""Execute an AI tool call from dashboard."""
|
|
308
|
+
root = request.app.state.root
|
|
309
|
+
data = await request.json()
|
|
310
|
+
tool_name = data.get("tool")
|
|
311
|
+
parameters = data.get("parameters", {})
|
|
312
|
+
|
|
313
|
+
executor = ToolExecutor(root, confirmation_provider=WebConfirmationProvider())
|
|
314
|
+
result = await executor.execute(tool_name, parameters, invoked_by="dashboard")
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
"success": result.success,
|
|
318
|
+
"result": result.result,
|
|
319
|
+
"error": result.error,
|
|
320
|
+
"invocation_id": result.invocation_id,
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@router.get("/api/v2/tools/list")
|
|
325
|
+
async def list_tools(request: Request):
|
|
326
|
+
"""List all available AI tools."""
|
|
327
|
+
from patchi.core.ai.tools.registry import get_tool_registry
|
|
328
|
+
registry = get_tool_registry()
|
|
329
|
+
category = request.query_params.get("category")
|
|
330
|
+
tools = registry.list_tools(category)
|
|
331
|
+
return {"tools": tools}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@router.post("/api/v2/council/deliberate")
|
|
335
|
+
async def deliberate(request: Request):
|
|
336
|
+
"""Run a full Council deliberation on an issue (REST form of the WS action)."""
|
|
337
|
+
root = request.app.state.root
|
|
338
|
+
try:
|
|
339
|
+
body = await request.json()
|
|
340
|
+
except Exception:
|
|
341
|
+
return JSONResponse({"error": "JSON body required"}, status_code=400)
|
|
342
|
+
|
|
343
|
+
issue = (body.get("issue") or "").strip()
|
|
344
|
+
if not issue:
|
|
345
|
+
return JSONResponse({"error": "issue is required"}, status_code=400)
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
from patchi.core.brain.council import run_council
|
|
349
|
+
session = await run_council(root, issue)
|
|
350
|
+
except Exception as e:
|
|
351
|
+
_log.error("Council deliberation failed: %s", e)
|
|
352
|
+
return JSONResponse({"error": f"Deliberation failed: {e}"}, status_code=500)
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
from patchi.core import memory as mem
|
|
356
|
+
mem.save_issue(
|
|
357
|
+
{
|
|
358
|
+
"type": "council_session",
|
|
359
|
+
"issue": issue,
|
|
360
|
+
"synthesis": session.synthesis,
|
|
361
|
+
"consensus": session.consensus_reached,
|
|
362
|
+
"timestamp": session.started_at,
|
|
363
|
+
"decisions": [
|
|
364
|
+
{
|
|
365
|
+
"persona_name": d.persona_name,
|
|
366
|
+
"analysis": d.analysis,
|
|
367
|
+
"recommendation": d.recommendation,
|
|
368
|
+
"confidence": d.confidence,
|
|
369
|
+
}
|
|
370
|
+
for d in session.persona_decisions
|
|
371
|
+
],
|
|
372
|
+
"action_plan": session.action_plan,
|
|
373
|
+
},
|
|
374
|
+
root,
|
|
375
|
+
)
|
|
376
|
+
except Exception as e:
|
|
377
|
+
_log.warning("Failed to persist council session: %s", e)
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
"issue": issue,
|
|
381
|
+
"synthesis": session.synthesis,
|
|
382
|
+
"consensus": session.consensus_reached,
|
|
383
|
+
"action_plan": session.action_plan,
|
|
384
|
+
"decisions": [
|
|
385
|
+
{
|
|
386
|
+
"persona_name": d.persona_name,
|
|
387
|
+
"analysis": d.analysis,
|
|
388
|
+
"recommendation": d.recommendation,
|
|
389
|
+
"confidence": d.confidence,
|
|
390
|
+
}
|
|
391
|
+
for d in session.persona_decisions
|
|
392
|
+
],
|
|
393
|
+
"duration_ms": session.duration_ms,
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
398
|
+
# WebSocket
|
|
399
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
@router.websocket("/ws")
|
|
402
|
+
async def websocket_endpoint(ws: WebSocket):
|
|
403
|
+
"""Unified WebSocket for real-time dashboard updates."""
|
|
404
|
+
from patchi.web.ws import manager as ws_manager
|
|
405
|
+
|
|
406
|
+
await ws.accept()
|
|
407
|
+
await ws_manager.connect(ws)
|
|
408
|
+
root = ws.app.state.root
|
|
409
|
+
|
|
410
|
+
try:
|
|
411
|
+
await _send_initial_state(ws, root)
|
|
412
|
+
while True:
|
|
413
|
+
raw = await ws.receive_text()
|
|
414
|
+
await _handle_ws_message(ws, root, raw)
|
|
415
|
+
except WebSocketDisconnect:
|
|
416
|
+
pass
|
|
417
|
+
except Exception as e:
|
|
418
|
+
_log.error(f"WebSocket error: {e}")
|
|
419
|
+
finally:
|
|
420
|
+
await ws_manager.disconnect(ws)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
async def _send_initial_state(ws: WebSocket, root: Path):
|
|
424
|
+
"""Send initial dashboard state."""
|
|
425
|
+
from patchi.core import memory as mem
|
|
426
|
+
from patchi.core.health import compute as compute_health
|
|
427
|
+
|
|
428
|
+
brain = mem.get_brain(root)
|
|
429
|
+
hs = compute_health(root)
|
|
430
|
+
|
|
431
|
+
await ws.send_json(
|
|
432
|
+
{
|
|
433
|
+
"event": "initial_state",
|
|
434
|
+
"data": {
|
|
435
|
+
"health_score": hs.total,
|
|
436
|
+
"health_grade": hs.grade,
|
|
437
|
+
"health_components": hs.to_dict().get("components", {}),
|
|
438
|
+
"file_count": brain.get("file_count", 0),
|
|
439
|
+
"route_count": brain.get("route_count", 0),
|
|
440
|
+
"framework": brain.get("framework", "Unknown"),
|
|
441
|
+
"languages": brain.get("languages", {}),
|
|
442
|
+
"mode": _safe_mode(root),
|
|
443
|
+
},
|
|
444
|
+
}
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
async def _handle_ws_message(ws: WebSocket, root: Path, raw: str):
|
|
449
|
+
"""Handle incoming WebSocket message."""
|
|
450
|
+
try:
|
|
451
|
+
msg = json.loads(raw)
|
|
452
|
+
except json.JSONDecodeError:
|
|
453
|
+
return
|
|
454
|
+
|
|
455
|
+
action = msg.get("action")
|
|
456
|
+
from patchi.core import memory as mem
|
|
457
|
+
if action == "subscribe":
|
|
458
|
+
await ws.send_json({"event": "subscribed", "data": {"channels": msg.get("data", {}).get("channels", [])}})
|
|
459
|
+
elif action == "execute_tool":
|
|
460
|
+
tool_name = msg.get("tool")
|
|
461
|
+
parameters = msg.get("parameters", {})
|
|
462
|
+
executor = ToolExecutor(root, confirmation_provider=WebConfirmationProvider())
|
|
463
|
+
exec_result = await executor.execute(tool_name, parameters, invoked_by="dashboard")
|
|
464
|
+
await ws.send_json(
|
|
465
|
+
{
|
|
466
|
+
"event": "tool_result",
|
|
467
|
+
"data": {
|
|
468
|
+
"tool": tool_name,
|
|
469
|
+
"success": exec_result.success,
|
|
470
|
+
"result": exec_result.result,
|
|
471
|
+
"error": exec_result.error,
|
|
472
|
+
},
|
|
473
|
+
}
|
|
474
|
+
)
|
|
475
|
+
elif action == "start_scan":
|
|
476
|
+
def _cli_scan():
|
|
477
|
+
from patchi.cli.commands.scan_cmd import run as cli_scan
|
|
478
|
+
cli_scan(root=root, quiet=True, no_logo=True)
|
|
479
|
+
|
|
480
|
+
try:
|
|
481
|
+
await asyncio.to_thread(_cli_scan)
|
|
482
|
+
brain_mem = mem.get_brain(root)
|
|
483
|
+
scans = mem.get_scan_results(root)
|
|
484
|
+
brain_meta = scans.get("Brain", {})
|
|
485
|
+
await ws.send_json(
|
|
486
|
+
{
|
|
487
|
+
"event": "scan_completed",
|
|
488
|
+
"data": {
|
|
489
|
+
"file_count": brain_mem.get("file_count", 0),
|
|
490
|
+
"route_count": brain_mem.get("route_count", 0),
|
|
491
|
+
"duration": brain_meta.get("duration", 0),
|
|
492
|
+
"findings": sum(len(d.get("findings", [])) for d in scans.values()),
|
|
493
|
+
},
|
|
494
|
+
}
|
|
495
|
+
)
|
|
496
|
+
except Exception as e:
|
|
497
|
+
_log.error("Scan failed: %s", e)
|
|
498
|
+
await ws.send_json({"event": "error", "data": {"message": str(e)}})
|
|
499
|
+
elif action == "run_fix":
|
|
500
|
+
# Execute fix command via CLI
|
|
501
|
+
def _cli_fix():
|
|
502
|
+
from patchi.cli.commands.fix_cmd import run as cli_fix
|
|
503
|
+
cli_fix(root=root, dry_run=False)
|
|
504
|
+
|
|
505
|
+
try:
|
|
506
|
+
await asyncio.to_thread(_cli_fix)
|
|
507
|
+
await ws.send_json({"event": "fix_completed", "data": {"success": True}})
|
|
508
|
+
except Exception as e:
|
|
509
|
+
_log.error("Fix failed: %s", e)
|
|
510
|
+
await ws.send_json({"event": "error", "data": {"message": str(e)}})
|
|
511
|
+
elif action == "run_council":
|
|
512
|
+
issue = msg.get("issue", "")
|
|
513
|
+
if issue:
|
|
514
|
+
try:
|
|
515
|
+
from patchi.core.brain.council import run_council
|
|
516
|
+
session = await run_council(root, issue)
|
|
517
|
+
await ws.send_json({
|
|
518
|
+
"event": "council_completed",
|
|
519
|
+
"data": {
|
|
520
|
+
"issue": issue,
|
|
521
|
+
"synthesis": session.synthesis,
|
|
522
|
+
"consensus": session.consensus_reached,
|
|
523
|
+
"action_plan": session.action_plan,
|
|
524
|
+
}
|
|
525
|
+
})
|
|
526
|
+
except Exception as e:
|
|
527
|
+
_log.error("Council failed: %s", e)
|
|
528
|
+
await ws.send_json({"event": "error", "data": {"message": str(e)}})
|
|
529
|
+
elif action == "status.request":
|
|
530
|
+
# Legacy base.html polls for current status on connect.
|
|
531
|
+
import time
|
|
532
|
+
|
|
533
|
+
try:
|
|
534
|
+
conf = cfg.load(root)
|
|
535
|
+
except Exception:
|
|
536
|
+
conf = {}
|
|
537
|
+
try:
|
|
538
|
+
brain = mem.get_brain(root)
|
|
539
|
+
except Exception:
|
|
540
|
+
brain = {}
|
|
541
|
+
await ws.send_json(
|
|
542
|
+
{
|
|
543
|
+
"event": "status",
|
|
544
|
+
"data": {
|
|
545
|
+
"connected": True,
|
|
546
|
+
"mode": conf.get("mode", "confirm"),
|
|
547
|
+
"project_purpose": brain.get("project_purpose", ""),
|
|
548
|
+
"timestamp": time.time(),
|
|
549
|
+
},
|
|
550
|
+
}
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _safe_mode(root: Path) -> str:
|
|
555
|
+
try:
|
|
556
|
+
return cfg.load(root).get("mode", "confirm")
|
|
557
|
+
except Exception:
|
|
558
|
+
return "confirm"
|