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,207 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`p model` — Manage Patchi's local Ollama model integration.
|
|
3
|
+
|
|
4
|
+
Subcommands:
|
|
5
|
+
p model set <model_name> — Set the local model Patchi uses
|
|
6
|
+
p model list — List available models from Ollama
|
|
7
|
+
p model status — Show current model + Ollama connection health
|
|
8
|
+
|
|
9
|
+
Ollama API base: http://localhost:11434
|
|
10
|
+
All network calls time out in 5s — they are informational checks, not blockers.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
# ── Subcommand runners ─────────────────────────────────────────────────────────
|
|
16
|
+
import logging
|
|
17
|
+
import time
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from rich.table import Table
|
|
21
|
+
|
|
22
|
+
from patchi.cli.console import con
|
|
23
|
+
from patchi.core.config import require_project_root
|
|
24
|
+
from patchi.core.constants import OLLAMA_BASE_URL
|
|
25
|
+
|
|
26
|
+
_log = logging.getLogger("patchi.cli.model_cmd")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def run_set(model_name: str, root: Path | None = None) -> None:
|
|
30
|
+
"""Set the local model Patchi will use."""
|
|
31
|
+
try:
|
|
32
|
+
r = root or require_project_root()
|
|
33
|
+
except RuntimeError as e:
|
|
34
|
+
con.print(f"[red]{e}[/red]")
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
from patchi.core import config as cfg
|
|
38
|
+
|
|
39
|
+
cfg.set_value("ai.local_model_name", model_name, r)
|
|
40
|
+
con.print()
|
|
41
|
+
con.print(f"[#4ADE80]✓[/#4ADE80] Local model set to [bold]{model_name}[/bold]")
|
|
42
|
+
con.print()
|
|
43
|
+
|
|
44
|
+
# Quick availability check
|
|
45
|
+
ok, note = _model_available(model_name)
|
|
46
|
+
if ok:
|
|
47
|
+
con.print(f" [dim]✓ {note}[/dim]")
|
|
48
|
+
else:
|
|
49
|
+
con.print(f" [#FACC15]⚠ {note}[/#FACC15]")
|
|
50
|
+
con.print()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def run_list(root: Path | None = None) -> None:
|
|
54
|
+
"""List models available from local Ollama instance."""
|
|
55
|
+
con.print()
|
|
56
|
+
con.print("[bold #C8621A]Local Models[/bold #C8621A] [dim]from Ollama (localhost:11434)[/dim]")
|
|
57
|
+
con.print()
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
r = root or require_project_root()
|
|
61
|
+
from patchi.core import config as cfg
|
|
62
|
+
|
|
63
|
+
current_model = cfg.load(r).get("ai", {}).get("local_model_name", "")
|
|
64
|
+
except Exception as e:
|
|
65
|
+
_log.warning("run_list failed: %s", e)
|
|
66
|
+
current_model = ""
|
|
67
|
+
|
|
68
|
+
models = _fetch_models()
|
|
69
|
+
|
|
70
|
+
if models is None:
|
|
71
|
+
con.print("[#FACC15]⚠ Ollama is not running.[/#FACC15]")
|
|
72
|
+
con.print("[dim]Start it with: ollama serve[/dim]")
|
|
73
|
+
con.print()
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
if not models:
|
|
77
|
+
con.print("[dim]No models pulled yet.[/dim]")
|
|
78
|
+
con.print("[dim]Try: ollama pull llama3.2[/dim]")
|
|
79
|
+
con.print()
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
table = Table(show_header=True, header_style="bold #C8621A", box=None, pad_edge=False)
|
|
83
|
+
table.add_column("Model", style="bold #F2EDD6", width=32)
|
|
84
|
+
table.add_column("Size", justify="right", width=10)
|
|
85
|
+
table.add_column("Modified", width=22)
|
|
86
|
+
table.add_column("Active", width=8)
|
|
87
|
+
|
|
88
|
+
for m in sorted(models, key=lambda x: x.get("name", "")):
|
|
89
|
+
name = m.get("name", "?")
|
|
90
|
+
size = _fmt_size(m.get("size", 0))
|
|
91
|
+
mod = m.get("modified_at", "")[:19].replace("T", " ")
|
|
92
|
+
active = (
|
|
93
|
+
"[#4ADE80]●[/#4ADE80]"
|
|
94
|
+
if name.split(":")[0] == (current_model or "").split(":")[0]
|
|
95
|
+
else ""
|
|
96
|
+
)
|
|
97
|
+
table.add_row(name, size, mod, active)
|
|
98
|
+
|
|
99
|
+
con.print(table)
|
|
100
|
+
con.print()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def run_status(root: Path | None = None) -> None:
|
|
104
|
+
"""Show Ollama connection health and current model."""
|
|
105
|
+
con.print()
|
|
106
|
+
con.print("[bold #C8621A]Model Status[/bold #C8621A]")
|
|
107
|
+
con.print()
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
r = root or require_project_root()
|
|
111
|
+
from patchi.core import config as cfg
|
|
112
|
+
|
|
113
|
+
config = cfg.load(r)
|
|
114
|
+
local_model = config.get("ai", {}).get("local_model_name")
|
|
115
|
+
except RuntimeError:
|
|
116
|
+
local_model = None
|
|
117
|
+
|
|
118
|
+
# ── Ollama health ─────────────────────────────────────────────────────────
|
|
119
|
+
ollama_ok, ollama_note = _check_ollama()
|
|
120
|
+
marker = "[#4ADE80]●[/#4ADE80]" if ollama_ok else "[#FF4D6D]●[/#FF4D6D]"
|
|
121
|
+
con.print(f" {marker} Ollama [dim]{ollama_note}[/dim]")
|
|
122
|
+
|
|
123
|
+
# ── Configured model ──────────────────────────────────────────────────────
|
|
124
|
+
if local_model:
|
|
125
|
+
avail_ok, avail_note = _model_available(local_model)
|
|
126
|
+
marker2 = "[#4ADE80]●[/#4ADE80]" if avail_ok else "[#FACC15]●[/#FACC15]"
|
|
127
|
+
con.print(f" {marker2} Model [bold]{local_model}[/bold] [dim]{avail_note}[/dim]")
|
|
128
|
+
|
|
129
|
+
# Benchmark ping if available
|
|
130
|
+
if ollama_ok:
|
|
131
|
+
ms = _ping_model(local_model)
|
|
132
|
+
if ms is not None:
|
|
133
|
+
con.print(f" [dim] Response time: {ms}ms[/dim]")
|
|
134
|
+
else:
|
|
135
|
+
con.print(" [dim]— No local model configured. Run: p model set <name>[/dim]")
|
|
136
|
+
|
|
137
|
+
con.print()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ── Ollama API helpers ─────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _fetch_models() -> list[dict] | None:
|
|
144
|
+
"""Return list of pulled model objects, or None if Ollama is unreachable."""
|
|
145
|
+
try:
|
|
146
|
+
import httpx
|
|
147
|
+
|
|
148
|
+
resp = httpx.get(f"{OLLAMA_BASE_URL}/api/tags", timeout=4.0)
|
|
149
|
+
if resp.status_code == 200:
|
|
150
|
+
return resp.json().get("models", [])
|
|
151
|
+
return []
|
|
152
|
+
except Exception as e:
|
|
153
|
+
_log.warning("_fetch_models failed: %s", e)
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _check_ollama() -> tuple[bool, str]:
|
|
158
|
+
try:
|
|
159
|
+
import httpx
|
|
160
|
+
|
|
161
|
+
t0 = time.monotonic()
|
|
162
|
+
resp = httpx.get(f"{OLLAMA_BASE_URL}/", timeout=4.0)
|
|
163
|
+
ms = int((time.monotonic() - t0) * 1000)
|
|
164
|
+
if resp.status_code in (200, 404):
|
|
165
|
+
return True, f"Running — localhost:11434 ({ms}ms)"
|
|
166
|
+
return False, f"HTTP {resp.status_code}"
|
|
167
|
+
except Exception as e:
|
|
168
|
+
_log.debug("_check_ollama failed: %s", e)
|
|
169
|
+
return False, "Not running — start with: ollama serve"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _model_available(model_name: str) -> tuple[bool, str]:
|
|
173
|
+
models = _fetch_models()
|
|
174
|
+
if models is None:
|
|
175
|
+
return False, "Ollama not running"
|
|
176
|
+
stem = model_name.split(":")[0]
|
|
177
|
+
if any(m.get("name", "").split(":")[0] == stem for m in models):
|
|
178
|
+
return True, "Model is available locally"
|
|
179
|
+
return False, f"Not pulled — run: ollama pull {model_name}"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _ping_model(model_name: str) -> int | None:
|
|
183
|
+
"""Send a minimal generation request to measure first-token latency."""
|
|
184
|
+
try:
|
|
185
|
+
import httpx
|
|
186
|
+
|
|
187
|
+
t0 = time.monotonic()
|
|
188
|
+
resp = httpx.post(
|
|
189
|
+
f"{OLLAMA_BASE_URL}/api/generate",
|
|
190
|
+
json={"model": model_name, "prompt": "hi", "stream": False, "max_tokens": 1},
|
|
191
|
+
timeout=15.0,
|
|
192
|
+
)
|
|
193
|
+
ms = int((time.monotonic() - t0) * 1000)
|
|
194
|
+
if resp.status_code == 200:
|
|
195
|
+
return ms
|
|
196
|
+
return None
|
|
197
|
+
except Exception as e:
|
|
198
|
+
_log.debug("_ping_model failed: %s", e)
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _fmt_size(size_bytes: int) -> str:
|
|
203
|
+
if size_bytes >= 1_073_741_824:
|
|
204
|
+
return f"{size_bytes / 1_073_741_824:.1f} GB"
|
|
205
|
+
if size_bytes >= 1_048_576:
|
|
206
|
+
return f"{size_bytes / 1_048_576:.0f} MB"
|
|
207
|
+
return f"{size_bytes} B"
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`p notify` — manage notification channels.
|
|
3
|
+
list | add <type> | remove <name> | test [name] | ack <id> | flush | pending
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
import patchi.core.config as config
|
|
14
|
+
from patchi.cli.commands.notify_prompts import CHANNEL_SETUP_FIELDS, SEVERITY_CHOICES
|
|
15
|
+
from patchi.core.notifications.channels import ChannelType, from_config
|
|
16
|
+
from patchi.core.notifications.notifier import Notifier
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def run_notify(args) -> None:
|
|
22
|
+
sub = getattr(args, "notify_cmd", None)
|
|
23
|
+
|
|
24
|
+
if sub == "list" or sub is None:
|
|
25
|
+
_cmd_list(args)
|
|
26
|
+
elif sub == "add":
|
|
27
|
+
_cmd_add(args)
|
|
28
|
+
elif sub == "remove":
|
|
29
|
+
_cmd_remove(args)
|
|
30
|
+
elif sub == "test":
|
|
31
|
+
_cmd_test(args)
|
|
32
|
+
elif sub == "ack":
|
|
33
|
+
_cmd_ack(args)
|
|
34
|
+
elif sub == "flush":
|
|
35
|
+
_cmd_flush(args)
|
|
36
|
+
elif sub == "pending":
|
|
37
|
+
_cmd_pending(args)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _cmd_list(args) -> None:
|
|
41
|
+
root = _require_root()
|
|
42
|
+
cfg = config.load(root)
|
|
43
|
+
channels = cfg.get("notifications", [])
|
|
44
|
+
|
|
45
|
+
if not channels:
|
|
46
|
+
console.print("[dim]No notification channels configured.[/dim]")
|
|
47
|
+
console.print("Add one with: [bold]p notify add slack[/bold]")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
table = Table(show_header=True, header_style="bold", box=None, pad_edge=False)
|
|
51
|
+
table.add_column("Name", style="cyan", no_wrap=True)
|
|
52
|
+
table.add_column("Type", style="white")
|
|
53
|
+
table.add_column("Min Severity", style="yellow")
|
|
54
|
+
table.add_column("Digest", style="dim")
|
|
55
|
+
|
|
56
|
+
for ch in channels:
|
|
57
|
+
table.add_row(
|
|
58
|
+
ch.get("name", "—"),
|
|
59
|
+
ch.get("type", "—"),
|
|
60
|
+
ch.get("min_severity", "medium"),
|
|
61
|
+
"yes" if ch.get("in_digest") else "no",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
console.print(table)
|
|
65
|
+
quiet = cfg.get("quiet_hours", {})
|
|
66
|
+
if quiet.get("enabled"):
|
|
67
|
+
console.print(
|
|
68
|
+
f"\n[dim]Quiet hours: {quiet['start']} → {quiet['end']} ({quiet.get('timezone', 'UTC')})[/dim]"
|
|
69
|
+
)
|
|
70
|
+
console.print(f"[dim]Digest frequency: {cfg.get('digest_frequency', 'daily')}[/dim]")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _prompt_credentials(channel_type: str) -> dict[str, str]:
|
|
74
|
+
"""Prompt for channel-specific credentials. Returns a dict of field→value."""
|
|
75
|
+
credentials: dict[str, str] = {}
|
|
76
|
+
for field_key, prompt in CHANNEL_SETUP_FIELDS.get(channel_type, []):
|
|
77
|
+
value = console.input(f"[cyan]{prompt}: [/cyan]").strip()
|
|
78
|
+
if value:
|
|
79
|
+
credentials[field_key] = value
|
|
80
|
+
return credentials
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _cmd_add(args) -> None:
|
|
84
|
+
root = _require_root()
|
|
85
|
+
channel_type = getattr(args, "channel_type", None)
|
|
86
|
+
|
|
87
|
+
valid_types = [t.value for t in ChannelType]
|
|
88
|
+
if not channel_type or channel_type not in valid_types:
|
|
89
|
+
console.print(f"[red]Specify a channel type:[/red] {', '.join(valid_types)}")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
console.print(f"\n[bold]Adding {channel_type} channel[/bold]")
|
|
93
|
+
name = console.input("[cyan]Channel name (e.g. my-slack): [/cyan]").strip()
|
|
94
|
+
if not name:
|
|
95
|
+
console.print("[red]Name cannot be empty.[/red]")
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
credentials: dict[str, str] = _prompt_credentials(channel_type)
|
|
99
|
+
|
|
100
|
+
min_severity = (
|
|
101
|
+
console.input(
|
|
102
|
+
f"[cyan]Minimum severity [{'/'.join(SEVERITY_CHOICES)}] (default: medium): [/cyan]"
|
|
103
|
+
).strip()
|
|
104
|
+
or "medium"
|
|
105
|
+
)
|
|
106
|
+
if min_severity not in SEVERITY_CHOICES:
|
|
107
|
+
min_severity = "medium"
|
|
108
|
+
|
|
109
|
+
in_digest_raw = (
|
|
110
|
+
console.input("[cyan]Batch into digest? (y/n, default: n): [/cyan]").strip().lower()
|
|
111
|
+
)
|
|
112
|
+
in_digest = in_digest_raw == "y"
|
|
113
|
+
|
|
114
|
+
raw = {
|
|
115
|
+
"name": name,
|
|
116
|
+
"type": channel_type,
|
|
117
|
+
"min_severity": min_severity,
|
|
118
|
+
"in_digest": in_digest,
|
|
119
|
+
**credentials,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Validate before saving
|
|
123
|
+
try:
|
|
124
|
+
from_config(raw)
|
|
125
|
+
except ValueError as exc:
|
|
126
|
+
console.print(f"[red]Invalid config:[/red] {exc}")
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
cfg = config.load(root)
|
|
130
|
+
cfg.setdefault("notifications", [])
|
|
131
|
+
|
|
132
|
+
if any(ch["name"] == name for ch in cfg["notifications"]):
|
|
133
|
+
console.print(f"[red]A channel named '{name}' already exists.[/red]")
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
cfg["notifications"].append(raw)
|
|
137
|
+
config.save(cfg, root)
|
|
138
|
+
console.print(f"[green]✓ Channel '{name}' added.[/green]")
|
|
139
|
+
console.print("Run [bold]p notify test[/bold] to verify it works.")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _cmd_remove(args) -> None:
|
|
143
|
+
root = _require_root()
|
|
144
|
+
name = getattr(args, "channel_name", None)
|
|
145
|
+
if not name:
|
|
146
|
+
console.print("[red]Specify a channel name to remove.[/red]")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
cfg = config.load(root)
|
|
150
|
+
before = len(cfg.get("notifications", []))
|
|
151
|
+
cfg["notifications"] = [ch for ch in cfg.get("notifications", []) if ch.get("name") != name]
|
|
152
|
+
|
|
153
|
+
if len(cfg["notifications"]) == before:
|
|
154
|
+
console.print(f"[red]No channel named '{name}' found.[/red]")
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
config.save(cfg, root)
|
|
158
|
+
console.print(f"[green]✓ Channel '{name}' removed.[/green]")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _cmd_test(args) -> None:
|
|
162
|
+
root = _require_root()
|
|
163
|
+
target = getattr(args, "channel_name", None)
|
|
164
|
+
if not config.load(root).get("notifications"):
|
|
165
|
+
console.print("[dim]No channels configured. Add one with p notify add.[/dim]")
|
|
166
|
+
return
|
|
167
|
+
for ch_name, ok in _notifier(root).test(target):
|
|
168
|
+
console.print(f" {ch_name}: {'[green]✓ sent[/green]' if ok else '[red]✗ failed[/red]'}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _cmd_ack(args) -> None:
|
|
172
|
+
aid = getattr(args, "alert_id", None)
|
|
173
|
+
if not aid:
|
|
174
|
+
console.print("[red]Provide an alert ID to acknowledge.[/red]")
|
|
175
|
+
return
|
|
176
|
+
ok = _notifier(_require_root()).acknowledge(aid)
|
|
177
|
+
msg = (
|
|
178
|
+
f"[green]✓ Alert {aid} acknowledged.[/green]"
|
|
179
|
+
if ok
|
|
180
|
+
else f"[red]Alert ID '{aid}' not found.[/red]"
|
|
181
|
+
)
|
|
182
|
+
console.print(msg)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _cmd_flush(args) -> None:
|
|
186
|
+
sent = _notifier(_require_root()).flush_digest()
|
|
187
|
+
console.print(
|
|
188
|
+
f"[green]✓ Digest flushed to {sent} channel(s).[/green]"
|
|
189
|
+
if sent
|
|
190
|
+
else "[dim]Nothing to flush.[/dim]"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _cmd_pending(args) -> None:
|
|
195
|
+
pending = _notifier(_require_root()).pending_escalations()
|
|
196
|
+
if not pending:
|
|
197
|
+
console.print("[dim]No pending escalations.[/dim]")
|
|
198
|
+
return
|
|
199
|
+
for alert in pending:
|
|
200
|
+
console.print(f" [red]{alert['id']}[/red] {alert['severity'].upper()} {alert['title']}")
|
|
201
|
+
console.print("\nAck with: [bold]p notify ack <id>[/bold]")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _notifier(root: Path) -> Notifier:
|
|
205
|
+
return Notifier(root, config.load(root))
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _require_root() -> Path:
|
|
209
|
+
try:
|
|
210
|
+
return config.require_project_root()
|
|
211
|
+
except RuntimeError as exc:
|
|
212
|
+
console.print(f"[red]{exc}[/red]")
|
|
213
|
+
raise SystemExit(1) from exc
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prompt field definitions for `p notify add` interactive setup.
|
|
3
|
+
Kept separate so notify_cmd.py stays under 200 lines.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
CHANNEL_SETUP_FIELDS: dict[str, list[tuple[str, str]]] = {
|
|
7
|
+
"email": [
|
|
8
|
+
("user", "SMTP username (your email address)"),
|
|
9
|
+
("password", "SMTP password or app password"),
|
|
10
|
+
("host", "SMTP host (e.g. smtp.gmail.com)"),
|
|
11
|
+
("port", "SMTP port [leave blank for default]"),
|
|
12
|
+
("to", "Recipient address [blank = same as username]"),
|
|
13
|
+
],
|
|
14
|
+
"slack": [
|
|
15
|
+
("webhook_url", "Slack incoming webhook URL"),
|
|
16
|
+
],
|
|
17
|
+
"discord": [
|
|
18
|
+
("webhook_url", "Discord webhook URL"),
|
|
19
|
+
],
|
|
20
|
+
"webhook": [
|
|
21
|
+
("url", "Webhook URL (https://...)"),
|
|
22
|
+
],
|
|
23
|
+
"telegram": [
|
|
24
|
+
("bot_token", "Telegram bot token"),
|
|
25
|
+
("chat_id", "Chat ID (your chat or channel ID)"),
|
|
26
|
+
],
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
SEVERITY_CHOICES: list[str] = ["critical", "high", "medium", "low", "info"]
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`p patch` — Manage individual patches by ID.
|
|
3
|
+
|
|
4
|
+
Subcommands:
|
|
5
|
+
p patch list — list all patches (all states)
|
|
6
|
+
p patch show <id> — show full diff + metadata for one patch
|
|
7
|
+
p patch apply <id> — apply a pending patch
|
|
8
|
+
p patch reject <id> — reject a pending patch
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from datetime import UTC
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from rich.syntax import Syntax
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
from rich.text import Text
|
|
19
|
+
|
|
20
|
+
from patchi.cli.console import con
|
|
21
|
+
from patchi.core import memory as mem
|
|
22
|
+
from patchi.core.config import require_project_root
|
|
23
|
+
from patchi.core.fix.patch import Patch, PatchState
|
|
24
|
+
|
|
25
|
+
_STATE_COLORS = {
|
|
26
|
+
"proposed": "#B8A898",
|
|
27
|
+
"pending": "#FACC15",
|
|
28
|
+
"auto_applied": "#4ADE80",
|
|
29
|
+
"applying": "#C8621A",
|
|
30
|
+
"applied": "#4ADE80",
|
|
31
|
+
"failed": "#FF4D6D",
|
|
32
|
+
"rolled_back": "#FF8C42",
|
|
33
|
+
"rejected": "dim",
|
|
34
|
+
"undone": "dim",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def run_list(root: Path | None = None, json_output: bool = False) -> None:
|
|
39
|
+
"""p patch list"""
|
|
40
|
+
try:
|
|
41
|
+
r = root or require_project_root()
|
|
42
|
+
except RuntimeError as e:
|
|
43
|
+
con.print(f"[red]{e}[/red]")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
patches_raw = mem.list_patches(r)
|
|
47
|
+
if json_output:
|
|
48
|
+
import json as _json
|
|
49
|
+
|
|
50
|
+
con.print(_json.dumps({"patches": patches_raw or []}, indent=2, default=str))
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
if not patches_raw:
|
|
54
|
+
con.print("[dim]No patches yet. Run [bold]p fix[/bold] to generate some.[/dim]")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
con.print()
|
|
58
|
+
table = Table(show_header=True, header_style="bold #C8621A", box=None, pad_edge=False)
|
|
59
|
+
table.add_column("ID", style="bold #F2EDD6", width=10)
|
|
60
|
+
table.add_column("State", width=12)
|
|
61
|
+
table.add_column("Agent", style="dim", width=18)
|
|
62
|
+
table.add_column("Risk", width=8)
|
|
63
|
+
table.add_column("Files", justify="right", width=6)
|
|
64
|
+
table.add_column("Description", width=44)
|
|
65
|
+
|
|
66
|
+
risk_colors = {"low": "#4ADE80", "medium": "#FACC15", "high": "#FF4D6D"}
|
|
67
|
+
|
|
68
|
+
for p in reversed(patches_raw): # newest first
|
|
69
|
+
risk_score = p.get("risk_score", 0)
|
|
70
|
+
risk_level = p.get("risk_level", "low")
|
|
71
|
+
state = p.get("state", "proposed")
|
|
72
|
+
table.add_row(
|
|
73
|
+
p.get("id", "?"),
|
|
74
|
+
Text(state, style=_STATE_COLORS.get(state, "white")),
|
|
75
|
+
p.get("agent", "?"),
|
|
76
|
+
Text(f"{risk_score}", style=risk_colors.get(risk_level, "white")),
|
|
77
|
+
str(p.get("file_count", 0)),
|
|
78
|
+
p.get("description", "?")[:44],
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
con.print(table)
|
|
82
|
+
con.print()
|
|
83
|
+
con.print("[dim]p patch show <id> · p patch apply <id> · p patch reject <id>[/dim]")
|
|
84
|
+
con.print()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def run_show(patch_id: str, root: Path | None = None, json_output: bool = False) -> None:
|
|
88
|
+
"""p patch show <id>"""
|
|
89
|
+
try:
|
|
90
|
+
r = root or require_project_root()
|
|
91
|
+
except RuntimeError as e:
|
|
92
|
+
con.print(f"[red]{e}[/red]")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
patch_dict = mem.get_patch(patch_id, r)
|
|
96
|
+
if not patch_dict:
|
|
97
|
+
if json_output:
|
|
98
|
+
import json as _json
|
|
99
|
+
|
|
100
|
+
con.print(_json.dumps({"error": f"Patch {patch_id!r} not found"}, indent=2))
|
|
101
|
+
else:
|
|
102
|
+
con.print(f"[red]Patch {patch_id!r} not found.[/red]")
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
if json_output:
|
|
106
|
+
import json as _json
|
|
107
|
+
|
|
108
|
+
con.print(_json.dumps(patch_dict, indent=2, default=str))
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
patch = Patch.from_dict(patch_dict)
|
|
112
|
+
state = patch.state.value
|
|
113
|
+
state_color = _STATE_COLORS.get(state, "white")
|
|
114
|
+
risk_colors = {"low": "#4ADE80", "medium": "#FACC15", "high": "#FF4D6D"}
|
|
115
|
+
risk_color = risk_colors.get(patch.risk_level, "white")
|
|
116
|
+
|
|
117
|
+
con.print()
|
|
118
|
+
con.print(
|
|
119
|
+
f"[bold #F2EDD6]Patch {patch.id}[/bold #F2EDD6] [{state_color}]{state}[/{state_color}]"
|
|
120
|
+
)
|
|
121
|
+
con.print(
|
|
122
|
+
f"[dim]Agent: {patch.agent} · Type: {patch.patch_type.value} · "
|
|
123
|
+
f"Proposed: {patch.proposed_at[:19]}[/dim]"
|
|
124
|
+
)
|
|
125
|
+
con.print()
|
|
126
|
+
con.print(f" {patch.description}")
|
|
127
|
+
if patch.ai_explanation:
|
|
128
|
+
con.print(f" [dim italic]{patch.ai_explanation[:120]}[/dim italic]")
|
|
129
|
+
con.print()
|
|
130
|
+
con.print(
|
|
131
|
+
f" [bold {risk_color}]Risk: {patch.risk_score}/100 ({patch.risk_level})[/bold {risk_color}]"
|
|
132
|
+
f" [dim]Confidence: {patch.confidence}%"
|
|
133
|
+
f" Blast radius: {patch.blast_radius}[/dim]"
|
|
134
|
+
)
|
|
135
|
+
con.print()
|
|
136
|
+
|
|
137
|
+
for change in patch.changes:
|
|
138
|
+
if not change.diff.strip():
|
|
139
|
+
continue
|
|
140
|
+
con.print(
|
|
141
|
+
f"[bold #B8A898]{change.path}[/bold #B8A898] "
|
|
142
|
+
f"[dim]+{change.lines_added} -{change.lines_removed}[/dim]"
|
|
143
|
+
)
|
|
144
|
+
con.print(Syntax(change.diff, "diff", theme="monokai", padding=1))
|
|
145
|
+
|
|
146
|
+
if patch.test_result:
|
|
147
|
+
passed = patch.test_result.get("passed")
|
|
148
|
+
if passed is True:
|
|
149
|
+
con.print("[#4ADE80]Tests passed.[/#4ADE80]")
|
|
150
|
+
elif passed is False:
|
|
151
|
+
con.print("[red]Tests failed — patch rolled back.[/red]")
|
|
152
|
+
out = patch.test_result.get("output", "")
|
|
153
|
+
if out:
|
|
154
|
+
con.print(f"[dim]{out[:500]}[/dim]")
|
|
155
|
+
|
|
156
|
+
con.print()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def run_apply(patch_id: str, root: Path | None = None) -> None:
|
|
160
|
+
"""p patch apply <id>"""
|
|
161
|
+
try:
|
|
162
|
+
r = root or require_project_root()
|
|
163
|
+
except RuntimeError as e:
|
|
164
|
+
con.print(f"[red]{e}[/red]")
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
patch_dict = mem.get_patch(patch_id, r)
|
|
168
|
+
if not patch_dict:
|
|
169
|
+
con.print(f"[red]Patch {patch_id!r} not found.[/red]")
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
if patch_dict.get("state") not in ("pending", "proposed"):
|
|
173
|
+
con.print(f"[yellow]Patch {patch_id} is already {patch_dict['state']}.[/yellow]")
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
from patchi.core.fix.applier import PatchApplier
|
|
177
|
+
from patchi.core.fix.risk_gate import RiskGate
|
|
178
|
+
|
|
179
|
+
patch = Patch.from_dict(patch_dict)
|
|
180
|
+
gate = RiskGate(r)
|
|
181
|
+
gate_result = gate.evaluate(patch)
|
|
182
|
+
|
|
183
|
+
if gate_result.is_blocked:
|
|
184
|
+
con.print(f"[red]Blocked:[/red] {gate_result.blocks[0]}")
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
con.print(f"Applying patch [bold]{patch_id}[/bold]…")
|
|
188
|
+
result = PatchApplier(r).apply(patch)
|
|
189
|
+
|
|
190
|
+
if result.success:
|
|
191
|
+
con.print(
|
|
192
|
+
f"[#4ADE80]✓ Applied.[/#4ADE80] "
|
|
193
|
+
f"[dim]Tests: {'passed' if result.test_passed else 'skipped'} "
|
|
194
|
+
f"Snapshot: {result.snapshot_id}[/dim]"
|
|
195
|
+
)
|
|
196
|
+
else:
|
|
197
|
+
con.print(f"[red]✗ Failed.[/red] [dim]{result.error}[/dim]")
|
|
198
|
+
if result.rolled_back:
|
|
199
|
+
con.print("[dim]Rolled back to original state.[/dim]")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def run_reject(patch_id: str, root: Path | None = None) -> None:
|
|
203
|
+
"""p patch reject <id>"""
|
|
204
|
+
try:
|
|
205
|
+
r = root or require_project_root()
|
|
206
|
+
except RuntimeError as e:
|
|
207
|
+
con.print(f"[red]{e}[/red]")
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
patch_dict = mem.get_patch(patch_id, r)
|
|
211
|
+
if not patch_dict:
|
|
212
|
+
con.print(f"[red]Patch {patch_id!r} not found.[/red]")
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
import json
|
|
217
|
+
from datetime import datetime
|
|
218
|
+
|
|
219
|
+
from patchi.core.constants import MEMORY_FILES, MemoryCategory
|
|
220
|
+
|
|
221
|
+
patches = mem.list_patches(r)
|
|
222
|
+
for p in patches:
|
|
223
|
+
if p.get("id") == patch_id:
|
|
224
|
+
p["state"] = PatchState.REJECTED.value
|
|
225
|
+
p["rejected_at"] = datetime.now(UTC).isoformat()
|
|
226
|
+
break
|
|
227
|
+
mem_path = r / MEMORY_FILES[MemoryCategory.PATCHES]
|
|
228
|
+
mem_path.write_text(json.dumps(patches, indent=2), encoding="utf-8")
|
|
229
|
+
con.print(f"[dim]Patch {patch_id} rejected.[/dim]")
|
|
230
|
+
except Exception as e:
|
|
231
|
+
con.print(f"[red]Error: {e}[/red]")
|