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,457 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`p doctor` — System health check and stale command detection.
|
|
3
|
+
|
|
4
|
+
Checks:
|
|
5
|
+
1. Stale/removed commands that should be migrated
|
|
6
|
+
2. Missing dependencies
|
|
7
|
+
3. API key configuration
|
|
8
|
+
4. .patchi/ size warnings
|
|
9
|
+
5. Config validation
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib
|
|
14
|
+
import logging
|
|
15
|
+
import pathlib
|
|
16
|
+
import re
|
|
17
|
+
import shutil
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
from rich.panel import Panel
|
|
21
|
+
from rich.table import Table
|
|
22
|
+
from rich.text import Text
|
|
23
|
+
|
|
24
|
+
from patchi.cli.console import con
|
|
25
|
+
|
|
26
|
+
_log = logging.getLogger("patchi.cli.doctor")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ── Stale reference auto-fixer ───────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
# Patterns to search: "p <old>" as a shell command in any text file
|
|
32
|
+
# We match whole-word to avoid false positives (e.g. "p status" shouldn't match "p status --deep")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _build_replacement_patterns(
|
|
36
|
+
found_stale: list[tuple[str, dict]],
|
|
37
|
+
) -> list[tuple[re.Pattern, str]]:
|
|
38
|
+
"""Build regex patterns for each stale command."""
|
|
39
|
+
patterns = []
|
|
40
|
+
for name, info in found_stale:
|
|
41
|
+
replacement = info["replacement"]
|
|
42
|
+
# Match: p <old> as a standalone command (word boundary after)
|
|
43
|
+
# Handles: "p brain", "p brain ", "p brain\n", "p brain'"
|
|
44
|
+
# Also handles: "patchi brain", "python -m patchi brain"
|
|
45
|
+
pat = re.compile(
|
|
46
|
+
r"(\b)(?:p|patchi|python\s+-m\s+patchi)\s+"
|
|
47
|
+
+ re.escape(name)
|
|
48
|
+
+ r"(\s|\'|\"|$)",
|
|
49
|
+
re.MULTILINE,
|
|
50
|
+
)
|
|
51
|
+
patterns.append((pat, replacement))
|
|
52
|
+
return patterns
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _fix_stale_references(
|
|
56
|
+
root: pathlib.Path | None,
|
|
57
|
+
found_stale: list[tuple[str, dict]],
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Scan scripts/ and common locations for stale command references and fix them."""
|
|
60
|
+
if not root or not root.exists():
|
|
61
|
+
con.print("[yellow]No project root — skipping auto-fix.[/yellow]")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
patterns = _build_replacement_patterns(found_stale)
|
|
65
|
+
|
|
66
|
+
# Directories to scan
|
|
67
|
+
scan_dirs = []
|
|
68
|
+
for d in ["scripts", ".github", ".gitlab", ".circleci", "ci", "bin"]:
|
|
69
|
+
p = root / d
|
|
70
|
+
if p.is_dir():
|
|
71
|
+
scan_dirs.append(p)
|
|
72
|
+
|
|
73
|
+
# Also scan root-level shell scripts and config files
|
|
74
|
+
scan_files = list(root.glob("*.sh")) + list(root.glob("*.bash")) + list(root.glob("*.zsh"))
|
|
75
|
+
scan_files += list(root.glob("Makefile")) + list(root.glob("*.mk"))
|
|
76
|
+
scan_files += list(root.glob("*.yml")) + list(root.glob("*.yaml"))
|
|
77
|
+
scan_files += list(root.glob("*.toml")) + list(root.glob("*.cfg"))
|
|
78
|
+
scan_files += list(root.glob("*.ini"))
|
|
79
|
+
scan_files += list(root.glob("docker*"))
|
|
80
|
+
scan_files += list(root.glob("*.bat")) + list(root.glob("*.cmd")) + list(root.glob("*.ps1"))
|
|
81
|
+
|
|
82
|
+
total_fixed = 0
|
|
83
|
+
fixed_files = []
|
|
84
|
+
|
|
85
|
+
# Scan directories recursively
|
|
86
|
+
for scan_dir in scan_dirs:
|
|
87
|
+
for fpath in scan_dir.rglob("*"):
|
|
88
|
+
if fpath.is_file() and _is_text_file(fpath):
|
|
89
|
+
fixed = _fix_file(fpath, patterns)
|
|
90
|
+
if fixed > 0:
|
|
91
|
+
total_fixed += fixed
|
|
92
|
+
fixed_files.append((fpath, fixed))
|
|
93
|
+
|
|
94
|
+
# Scan root-level files
|
|
95
|
+
for fpath in scan_files:
|
|
96
|
+
if fpath.is_file() and _is_text_file(fpath):
|
|
97
|
+
fixed = _fix_file(fpath, patterns)
|
|
98
|
+
if fixed > 0:
|
|
99
|
+
total_fixed += fixed
|
|
100
|
+
fixed_files.append((fpath, fixed))
|
|
101
|
+
|
|
102
|
+
# Report results
|
|
103
|
+
if total_fixed == 0:
|
|
104
|
+
con.print("[green]✓ No stale references found in scripts/.[/green]")
|
|
105
|
+
else:
|
|
106
|
+
con.print(f"\n[bold green]✓ Fixed {total_fixed} stale reference(s) in {len(fixed_files)} file(s):[/bold green]")
|
|
107
|
+
con.print()
|
|
108
|
+
for fpath, count in fixed_files:
|
|
109
|
+
rel = fpath.relative_to(root) if fpath.is_relative_to(root) else fpath
|
|
110
|
+
con.print(f" [cyan]{rel}[/cyan] — {count} replacement(s)")
|
|
111
|
+
con.print()
|
|
112
|
+
con.print("[dim]Review the changes with `git diff` before committing.[/dim]")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _is_text_file(fpath: pathlib.Path) -> bool:
|
|
116
|
+
"""Check if a file is likely a text file (not binary)."""
|
|
117
|
+
text_exts = {
|
|
118
|
+
".sh", ".bash", ".zsh", ".bat", ".cmd", ".ps1",
|
|
119
|
+
".py", ".js", ".ts", ".jsx", ".tsx",
|
|
120
|
+
".yml", ".yaml", ".toml", ".cfg", ".ini", ".conf",
|
|
121
|
+
".md", ".rst", ".txt",
|
|
122
|
+
"Makefile", "Dockerfile", ".dockerfile",
|
|
123
|
+
".mk", ".cmake",
|
|
124
|
+
}
|
|
125
|
+
if fpath.name in text_exts or fpath.suffix in text_exts:
|
|
126
|
+
return True
|
|
127
|
+
# Check if it has no extension but is small (likely a script)
|
|
128
|
+
if not fpath.suffix and fpath.stat().st_size < 100_000:
|
|
129
|
+
try:
|
|
130
|
+
fpath.read_text(encoding="utf-8")[:100]
|
|
131
|
+
return True
|
|
132
|
+
except (UnicodeDecodeError, OSError):
|
|
133
|
+
return False
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _fix_file(fpath: pathlib.Path, patterns: list[tuple[re.Pattern, str]]) -> int:
|
|
138
|
+
"""Replace stale references in a file. Returns number of replacements."""
|
|
139
|
+
try:
|
|
140
|
+
text = fpath.read_text(encoding="utf-8")
|
|
141
|
+
except (UnicodeDecodeError, OSError):
|
|
142
|
+
return 0
|
|
143
|
+
|
|
144
|
+
new_text = text
|
|
145
|
+
total = 0
|
|
146
|
+
for pat, replacement in patterns:
|
|
147
|
+
new_text, n = pat.subn(replacement, new_text)
|
|
148
|
+
total += n
|
|
149
|
+
|
|
150
|
+
if total > 0 and new_text != text:
|
|
151
|
+
fpath.write_text(new_text, encoding="utf-8")
|
|
152
|
+
return total
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def run(
|
|
156
|
+
verbose: bool = False,
|
|
157
|
+
json_output: bool = False,
|
|
158
|
+
fix: bool = False,
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Entry point for `p doctor`."""
|
|
161
|
+
if not json_output:
|
|
162
|
+
con.print()
|
|
163
|
+
con.print("[bold #C8621A]Patchi Doctor[/bold #C8621A] [dim]system health check[/dim]")
|
|
164
|
+
con.print()
|
|
165
|
+
|
|
166
|
+
checks: list[tuple[str, str, str, str]] = [] # (label, status, note, color)
|
|
167
|
+
warnings = 0
|
|
168
|
+
errors = 0
|
|
169
|
+
|
|
170
|
+
# ── 1. Python version ─────────────────────────────────────────────────────
|
|
171
|
+
pyver = sys.version_info
|
|
172
|
+
if pyver >= (3, 11):
|
|
173
|
+
checks.append(("Python", "✓", f"{pyver.major}.{pyver.minor}.{pyver.micro}", "#4ADE80"))
|
|
174
|
+
else:
|
|
175
|
+
checks.append(("Python", "✗", f"{pyver.major}.{pyver.minor} — requires ≥ 3.11", "#FF4D6D"))
|
|
176
|
+
errors += 1
|
|
177
|
+
|
|
178
|
+
# ── 2. Required dependencies ──────────────────────────────────────────────
|
|
179
|
+
_REQUIRED = [
|
|
180
|
+
("rich", "rich", "Terminal UI rendering"),
|
|
181
|
+
("watchfiles", "watchfiles", "File watcher (p watch)"),
|
|
182
|
+
("vulture", "vulture", "Dead code detection"),
|
|
183
|
+
("httpx", "httpx", "HTTP client (AI requests)"),
|
|
184
|
+
("fastapi", "fastapi", "Web dashboard backend"),
|
|
185
|
+
("uvicorn", "uvicorn", "Web dashboard ASGI server"),
|
|
186
|
+
("psutil", "psutil", "Process + system metrics"),
|
|
187
|
+
("yaml", "pyyaml", "YAML config parsing"),
|
|
188
|
+
("loguru", "loguru", "Structured logging"),
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
for import_name, pip_name, desc in _REQUIRED:
|
|
192
|
+
try:
|
|
193
|
+
importlib.import_module(import_name)
|
|
194
|
+
checks.append((f"dep: {pip_name}", "✓", desc, "#4ADE80"))
|
|
195
|
+
except ImportError:
|
|
196
|
+
checks.append((f"dep: {pip_name}", "✗", f"Missing — pip install {pip_name}", "#FF4D6D"))
|
|
197
|
+
errors += 1
|
|
198
|
+
|
|
199
|
+
# ── 3. Project root ───────────────────────────────────────────────────────
|
|
200
|
+
try:
|
|
201
|
+
from patchi.core.config import find_project_root
|
|
202
|
+
|
|
203
|
+
root = find_project_root()
|
|
204
|
+
if root:
|
|
205
|
+
checks.append(("Project root", "✓", str(root), "#4ADE80"))
|
|
206
|
+
else:
|
|
207
|
+
checks.append(("Project root", "⚠", "Not found — run 'p init' first", "#FACC15"))
|
|
208
|
+
warnings += 1
|
|
209
|
+
except Exception as e:
|
|
210
|
+
checks.append(("Project root", "✗", str(e)[:60], "#FF4D6D"))
|
|
211
|
+
errors += 1
|
|
212
|
+
root = None
|
|
213
|
+
|
|
214
|
+
# ── 4. Stale commands check ────────────────────────────────────────────────
|
|
215
|
+
stale_commands = {
|
|
216
|
+
"brain": {
|
|
217
|
+
"replacement": "p agents list --brain",
|
|
218
|
+
"reason": "Merged into agents command",
|
|
219
|
+
},
|
|
220
|
+
"smart": {
|
|
221
|
+
"replacement": "p chat --stream",
|
|
222
|
+
"reason": "Merged into chat command",
|
|
223
|
+
},
|
|
224
|
+
"health": {
|
|
225
|
+
"replacement": "p status --deep",
|
|
226
|
+
"reason": "Merged into status command",
|
|
227
|
+
},
|
|
228
|
+
"doctor": {
|
|
229
|
+
"replacement": "p status --validate",
|
|
230
|
+
"reason": "Merged into status command",
|
|
231
|
+
},
|
|
232
|
+
"security": {
|
|
233
|
+
"replacement": "p scan",
|
|
234
|
+
"reason": "Merged into scan command",
|
|
235
|
+
},
|
|
236
|
+
"ignore": {
|
|
237
|
+
"replacement": "p memory ignore",
|
|
238
|
+
"reason": "Merged into memory command",
|
|
239
|
+
},
|
|
240
|
+
"explain": {
|
|
241
|
+
"replacement": "p chat 'explain <type>'",
|
|
242
|
+
"reason": "Merged into chat command",
|
|
243
|
+
},
|
|
244
|
+
"profile": {
|
|
245
|
+
"replacement": "p agent-stats",
|
|
246
|
+
"reason": "Merged into agent-stats command",
|
|
247
|
+
},
|
|
248
|
+
"learning": {
|
|
249
|
+
"replacement": "p agent-stats --learning",
|
|
250
|
+
"reason": "Merged into agent-stats command",
|
|
251
|
+
},
|
|
252
|
+
"ask": {
|
|
253
|
+
"replacement": "p chat",
|
|
254
|
+
"reason": "Alias — chat is the unified interface",
|
|
255
|
+
},
|
|
256
|
+
"chain": {
|
|
257
|
+
"replacement": "p chains",
|
|
258
|
+
"reason": "Merged into chains command (plural)",
|
|
259
|
+
},
|
|
260
|
+
"mode": {
|
|
261
|
+
"replacement": "p settings mode",
|
|
262
|
+
"reason": "Merged into settings command",
|
|
263
|
+
},
|
|
264
|
+
"reason": {
|
|
265
|
+
"replacement": "p chat 'reason about <topic>'",
|
|
266
|
+
"reason": "Merged into chat command",
|
|
267
|
+
},
|
|
268
|
+
"blast": {
|
|
269
|
+
"replacement": "p impact",
|
|
270
|
+
"reason": "Renamed to impact command",
|
|
271
|
+
},
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
from patchi.cli.registry import COMMANDS
|
|
276
|
+
|
|
277
|
+
cmd_names = [cmd.name for cmd in COMMANDS]
|
|
278
|
+
found_stale = []
|
|
279
|
+
for name, info in stale_commands.items():
|
|
280
|
+
if name not in cmd_names:
|
|
281
|
+
found_stale.append((name, info))
|
|
282
|
+
|
|
283
|
+
if found_stale:
|
|
284
|
+
con.print("[bold]Stale Commands Detected[/bold]")
|
|
285
|
+
con.print("[dim]These commands have been merged or removed. Update your scripts:[/dim]")
|
|
286
|
+
con.print()
|
|
287
|
+
|
|
288
|
+
table = Table(show_header=True, header_style="bold #C8621A", box=None)
|
|
289
|
+
table.add_column("Command", style="red", width=15)
|
|
290
|
+
table.add_column("Replacement", style="green", width=25)
|
|
291
|
+
table.add_column("Reason", style="dim")
|
|
292
|
+
|
|
293
|
+
for name, info in found_stale:
|
|
294
|
+
table.add_row(
|
|
295
|
+
f"p {name}",
|
|
296
|
+
info["replacement"],
|
|
297
|
+
info["reason"],
|
|
298
|
+
)
|
|
299
|
+
warnings += 1
|
|
300
|
+
|
|
301
|
+
con.print(table)
|
|
302
|
+
con.print()
|
|
303
|
+
|
|
304
|
+
# ── --fix: auto-update stale references in scripts/ ──
|
|
305
|
+
if fix:
|
|
306
|
+
_fix_stale_references(root, found_stale)
|
|
307
|
+
else:
|
|
308
|
+
checks.append(("Stale commands", "✓", "No stale commands found", "#4ADE80"))
|
|
309
|
+
except Exception as e:
|
|
310
|
+
_log.warning("Stale command check failed: %s", e)
|
|
311
|
+
|
|
312
|
+
# ── 5. API keys ───────────────────────────────────────────────────────────
|
|
313
|
+
if root:
|
|
314
|
+
try:
|
|
315
|
+
from patchi.core import config as cfg
|
|
316
|
+
|
|
317
|
+
config = cfg.load(root)
|
|
318
|
+
ai_keys = config.get("ai", {}).get("keys", [])
|
|
319
|
+
if ai_keys:
|
|
320
|
+
checks.append(("API keys", "✓", f"{len(ai_keys)} key(s) configured", "#4ADE80"))
|
|
321
|
+
else:
|
|
322
|
+
checks.append(("API keys", "⚠", "No keys — run 'p key add'", "#FACC15"))
|
|
323
|
+
warnings += 1
|
|
324
|
+
except Exception as e:
|
|
325
|
+
checks.append(("API keys", "✗", str(e)[:60], "#FF4D6D"))
|
|
326
|
+
errors += 1
|
|
327
|
+
|
|
328
|
+
# ── 6. Optional: test tooling ─────────────────────────────────────────────
|
|
329
|
+
con.print()
|
|
330
|
+
_OPTIONAL_TEST = [
|
|
331
|
+
("pytest", "pytest", "pytest", "Unit test runner"),
|
|
332
|
+
("npx", "", "node/npm", "Playwright host"),
|
|
333
|
+
("locust", "locust", "locust", "Stress test runner"),
|
|
334
|
+
]
|
|
335
|
+
for cmd, import_name, pip_name, desc in _OPTIONAL_TEST:
|
|
336
|
+
try:
|
|
337
|
+
present = (shutil.which(cmd) is not None) or (
|
|
338
|
+
import_name and importlib.import_module(import_name)
|
|
339
|
+
)
|
|
340
|
+
except ImportError:
|
|
341
|
+
present = False
|
|
342
|
+
status = "✓" if present else "—"
|
|
343
|
+
color = "#4ADE80" if present else "#6B7280"
|
|
344
|
+
note = desc if present else f"Optional — {pip_name}"
|
|
345
|
+
checks.append((f"[dim]opt:[/dim] {cmd}", status, note, color))
|
|
346
|
+
|
|
347
|
+
# ── 7. Optional: security tooling ────────────────────────────────────────
|
|
348
|
+
try:
|
|
349
|
+
from patchi.core.agents.tool_health import check_tool
|
|
350
|
+
|
|
351
|
+
_OPTIONAL_SECURITY = [
|
|
352
|
+
("bandit", "bandit", "bandit", "Python security linter"),
|
|
353
|
+
("semgrep", "", "semgrep", "Multi-language SAST scanner"),
|
|
354
|
+
]
|
|
355
|
+
for cmd, _import_name, pip_name, desc in _OPTIONAL_SECURITY:
|
|
356
|
+
try:
|
|
357
|
+
st = check_tool(cmd)
|
|
358
|
+
if st["status"] == "ok":
|
|
359
|
+
note = f"{desc} ({st['version']})"
|
|
360
|
+
checks.append((f"[dim]opt:[/dim] {cmd}", "✓", note, "#4ADE80"))
|
|
361
|
+
elif st["status"] == "broken":
|
|
362
|
+
hint = st["hint"]
|
|
363
|
+
checks.append(
|
|
364
|
+
(f"[dim]opt:[/dim] {cmd}", "✗", f"Broken: {hint}", "#FF4D6D")
|
|
365
|
+
)
|
|
366
|
+
errors += 1
|
|
367
|
+
else:
|
|
368
|
+
note = f"Optional — {pip_name}"
|
|
369
|
+
checks.append((f"[dim]opt:[/dim] {cmd}", "—", note, "#6B7280"))
|
|
370
|
+
except Exception:
|
|
371
|
+
note = f"Optional — {pip_name}"
|
|
372
|
+
checks.append((f"[dim]opt:[/dim] {cmd}", "—", note, "#6B7280"))
|
|
373
|
+
except ImportError:
|
|
374
|
+
pass
|
|
375
|
+
|
|
376
|
+
# ── 8. .patchi/ size warning ─────────────────────────────────────────────
|
|
377
|
+
if root:
|
|
378
|
+
try:
|
|
379
|
+
from patchi.cli.commands.cleanup_cmd import get_patchi_size
|
|
380
|
+
|
|
381
|
+
size_info = get_patchi_size(root)
|
|
382
|
+
total_bytes = size_info["total_bytes"]
|
|
383
|
+
total_files = size_info["total_files"]
|
|
384
|
+
|
|
385
|
+
mb = total_bytes / (1024 * 1024)
|
|
386
|
+
if total_bytes > 50 * 1024 * 1024:
|
|
387
|
+
size_str = f"{mb:.1f} MB"
|
|
388
|
+
warn_msg = f"{size_str} ({total_files} files) — consider 'p cleanup'"
|
|
389
|
+
checks.append((".patchi/ size", "⚠", warn_msg, "#FACC15"))
|
|
390
|
+
warnings += 1
|
|
391
|
+
else:
|
|
392
|
+
if total_bytes >= 1024 * 1024:
|
|
393
|
+
size_str = f"{mb:.1f} MB"
|
|
394
|
+
else:
|
|
395
|
+
size_str = f"{total_bytes / 1024:.1f} KB"
|
|
396
|
+
info_msg = f"{size_str} ({total_files} files)"
|
|
397
|
+
checks.append((".patchi/ size", "✓", info_msg, "#4ADE80"))
|
|
398
|
+
except Exception as _exc:
|
|
399
|
+
_log.warning('run failed: %s', _exc)
|
|
400
|
+
|
|
401
|
+
# ── Render results ────────────────────────────────────────────────────────
|
|
402
|
+
if json_output:
|
|
403
|
+
import json as _json
|
|
404
|
+
|
|
405
|
+
con.print(
|
|
406
|
+
_json.dumps(
|
|
407
|
+
{
|
|
408
|
+
"ok": errors == 0,
|
|
409
|
+
"errors": errors,
|
|
410
|
+
"warnings": warnings,
|
|
411
|
+
"checks": [
|
|
412
|
+
{"label": label, "status": status.strip(), "note": note}
|
|
413
|
+
for label, status, note, _color in checks
|
|
414
|
+
],
|
|
415
|
+
},
|
|
416
|
+
indent=2,
|
|
417
|
+
)
|
|
418
|
+
)
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
table = Table(show_header=False, box=None, pad_edge=False, padding=(0, 1))
|
|
422
|
+
table.add_column("Status", width=4)
|
|
423
|
+
table.add_column("Check", style="bold #F2EDD6", width=28)
|
|
424
|
+
table.add_column("Note", style="dim", width=55)
|
|
425
|
+
|
|
426
|
+
for label, status, note, color in checks:
|
|
427
|
+
table.add_row(Text(status, style=color), label, note)
|
|
428
|
+
|
|
429
|
+
con.print(table)
|
|
430
|
+
|
|
431
|
+
# ── Summary ───────────────────────────────────────────────────────────────
|
|
432
|
+
con.print()
|
|
433
|
+
if errors == 0 and warnings == 0:
|
|
434
|
+
con.print(
|
|
435
|
+
Panel(
|
|
436
|
+
"[bold #4ADE80]All checks passed. Patchi is ready.[/bold #4ADE80]",
|
|
437
|
+
border_style="#4ADE80",
|
|
438
|
+
padding=(0, 1),
|
|
439
|
+
)
|
|
440
|
+
)
|
|
441
|
+
elif errors == 0:
|
|
442
|
+
con.print(
|
|
443
|
+
Panel(
|
|
444
|
+
f"[bold #FACC15]{warnings} warning(s). Core functionality OK.[/bold #FACC15]",
|
|
445
|
+
border_style="#FACC15",
|
|
446
|
+
padding=(0, 1),
|
|
447
|
+
)
|
|
448
|
+
)
|
|
449
|
+
else:
|
|
450
|
+
con.print(
|
|
451
|
+
Panel(
|
|
452
|
+
f"[bold #FF4D6D]{errors} error(s), {warnings} warning(s). Fix errors above.[/bold #FF4D6D]",
|
|
453
|
+
border_style="#FF4D6D",
|
|
454
|
+
padding=(0, 1),
|
|
455
|
+
)
|
|
456
|
+
)
|
|
457
|
+
con.print()
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""p findings — view and manage security findings.
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
p findings — show all findings from last scan
|
|
5
|
+
p findings --summary — per-agent before/after counts (repeatable)
|
|
6
|
+
p findings --json — machine-readable output
|
|
7
|
+
p findings --save-baseline — snapshot current counts as baseline
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
|
|
19
|
+
_log = logging.getLogger("patchi.cli.commands.findings_cmd")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def run(args) -> None:
|
|
24
|
+
con = Console()
|
|
25
|
+
root = Path.cwd()
|
|
26
|
+
|
|
27
|
+
if getattr(args, "save_baseline", False):
|
|
28
|
+
_save_baseline(root, con)
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
if getattr(args, "summary", False):
|
|
32
|
+
_show_summary(root, con, json_output=getattr(args, "json_output", False))
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
_show_findings(root, con, json_output=getattr(args, "json_output", False))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _load_scan_results(root: Path) -> dict:
|
|
39
|
+
"""Load scan results from .patchi/."""
|
|
40
|
+
results_path = root / ".patchi" / "memory" / "scan_results.json"
|
|
41
|
+
if not results_path.exists():
|
|
42
|
+
return {}
|
|
43
|
+
try:
|
|
44
|
+
return json.loads(results_path.read_text(encoding="utf-8"))
|
|
45
|
+
except Exception:
|
|
46
|
+
return {}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _show_findings(root: Path, con: Console, json_output: bool = False) -> None:
|
|
50
|
+
"""Show all findings from last scan."""
|
|
51
|
+
results = _load_scan_results(root)
|
|
52
|
+
if not results:
|
|
53
|
+
con.print("[yellow]No scan results found. Run `p scan` first.[/yellow]")
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
all_findings = []
|
|
57
|
+
for agent_name, data in results.items():
|
|
58
|
+
for f in data.get("findings", []):
|
|
59
|
+
f["agent"] = agent_name
|
|
60
|
+
all_findings.append(f)
|
|
61
|
+
|
|
62
|
+
sev_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
|
|
63
|
+
all_findings.sort(key=lambda f: sev_order.get(f.get("severity", "info"), 5))
|
|
64
|
+
|
|
65
|
+
if json_output:
|
|
66
|
+
con.print(
|
|
67
|
+
json.dumps(
|
|
68
|
+
{"total": len(all_findings), "findings": all_findings}, indent=2, default=str
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
# Summary
|
|
74
|
+
by_sev = {}
|
|
75
|
+
for f in all_findings:
|
|
76
|
+
s = f.get("severity", "info")
|
|
77
|
+
by_sev[s] = by_sev.get(s, 0) + 1
|
|
78
|
+
sev_parts = ", ".join(f"{s}: {c}" for s, c in sorted(by_sev.items()))
|
|
79
|
+
con.print(f"\n[bold]Findings[/bold] ({len(all_findings)} total: {sev_parts})\n")
|
|
80
|
+
|
|
81
|
+
# Table
|
|
82
|
+
table = Table(show_header=True, header_style="bold")
|
|
83
|
+
table.add_column("#", style="dim", width=4)
|
|
84
|
+
table.add_column("Sev", width=8)
|
|
85
|
+
table.add_column("Type", width=20)
|
|
86
|
+
table.add_column("File", width=30)
|
|
87
|
+
table.add_column("Line", width=6)
|
|
88
|
+
table.add_column("Agent", width=16)
|
|
89
|
+
table.add_column("Message", max_width=40)
|
|
90
|
+
|
|
91
|
+
for i, f in enumerate(all_findings, 1):
|
|
92
|
+
sev = f.get("severity", "info")
|
|
93
|
+
sev_style = {
|
|
94
|
+
"critical": "bold red",
|
|
95
|
+
"high": "red",
|
|
96
|
+
"medium": "yellow",
|
|
97
|
+
"low": "dim",
|
|
98
|
+
"info": "dim",
|
|
99
|
+
}.get(sev, "")
|
|
100
|
+
table.add_row(
|
|
101
|
+
str(i),
|
|
102
|
+
f"[{sev_style}]{sev}[/{sev_style}]",
|
|
103
|
+
f.get("type", "?"),
|
|
104
|
+
f.get("file", "?")[-30:],
|
|
105
|
+
str(f.get("line", 0)),
|
|
106
|
+
f.get("agent", "?"),
|
|
107
|
+
(f.get("message", "")[:80]),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
con.print(table)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _show_summary(root: Path, con: Console, json_output: bool = False) -> None:
|
|
114
|
+
"""Show per-agent finding counts with before/after comparison."""
|
|
115
|
+
results = _load_scan_results(root)
|
|
116
|
+
baseline_path = root / ".patchi" / "findings_baseline.json"
|
|
117
|
+
baseline = {}
|
|
118
|
+
if baseline_path.exists():
|
|
119
|
+
try:
|
|
120
|
+
baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
|
|
121
|
+
except Exception as _exc:
|
|
122
|
+
_log.warning('_show_summary failed: %s', _exc)
|
|
123
|
+
|
|
124
|
+
# Current counts
|
|
125
|
+
agent_counts = {}
|
|
126
|
+
total = 0
|
|
127
|
+
for agent_name, data in results.items():
|
|
128
|
+
count = len(data.get("findings", []))
|
|
129
|
+
agent_counts[agent_name] = count
|
|
130
|
+
total += count
|
|
131
|
+
|
|
132
|
+
if json_output:
|
|
133
|
+
rows = []
|
|
134
|
+
for agent, count in sorted(agent_counts.items()):
|
|
135
|
+
prev = baseline.get(agent, count)
|
|
136
|
+
rows.append(
|
|
137
|
+
{
|
|
138
|
+
"agent": agent,
|
|
139
|
+
"current": count,
|
|
140
|
+
"baseline": prev,
|
|
141
|
+
"delta": count - prev,
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
con.print(json.dumps({"total": total, "agents": rows}, indent=2))
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
con.print(
|
|
148
|
+
f"\n[bold]Finding Counts Summary[/bold] — {len(agent_counts)} agents, {total} total findings\n"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
table = Table(show_header=True, header_style="bold")
|
|
152
|
+
table.add_column("Agent", width=28)
|
|
153
|
+
table.add_column("Current", justify="right", width=8)
|
|
154
|
+
table.add_column("Baseline", justify="right", width=8)
|
|
155
|
+
table.add_column("Delta", justify="right", width=8)
|
|
156
|
+
|
|
157
|
+
for agent, count in sorted(agent_counts.items()):
|
|
158
|
+
prev = baseline.get(agent, count)
|
|
159
|
+
delta = count - prev
|
|
160
|
+
delta_str = f"+{delta}" if delta > 0 else str(delta)
|
|
161
|
+
delta_style = "red" if delta > 0 else ("green" if delta < 0 else "dim")
|
|
162
|
+
table.add_row(
|
|
163
|
+
agent,
|
|
164
|
+
str(count),
|
|
165
|
+
str(prev),
|
|
166
|
+
f"[{delta_style}]{delta_str}[/{delta_style}]",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
con.print(table)
|
|
170
|
+
|
|
171
|
+
if not baseline:
|
|
172
|
+
con.print("\n[dim]No baseline set. Run `p findings --save-baseline` to create one.[/dim]")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _save_baseline(root: Path, con: Console) -> None:
|
|
176
|
+
"""Snapshot current finding counts as baseline."""
|
|
177
|
+
results = _load_scan_results(root)
|
|
178
|
+
baseline = {}
|
|
179
|
+
for agent_name, data in results.items():
|
|
180
|
+
baseline[agent_name] = len(data.get("findings", []))
|
|
181
|
+
|
|
182
|
+
baseline_path = root / ".patchi" / "findings_baseline.json"
|
|
183
|
+
baseline_path.parent.mkdir(parents=True, exist_ok=True)
|
|
184
|
+
baseline_path.write_text(json.dumps(baseline, indent=2), encoding="utf-8")
|
|
185
|
+
con.print(
|
|
186
|
+
f"[green]Baseline saved: {len(baseline)} agents, {sum(baseline.values())} total findings[/green]"
|
|
187
|
+
)
|