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,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bad-user personas for adversarial testing (§p test personas=bad).
|
|
3
|
+
|
|
4
|
+
Three hostile users that think like attackers, not test scripts:
|
|
5
|
+
- breaker: floods inputs with injection payloads (InputFuzzer)
|
|
6
|
+
- impatient: rapid navigation, double submits, back-button races
|
|
7
|
+
- malicious: auth bypass probes (unsigned JWT, missing headers, role paths)
|
|
8
|
+
|
|
9
|
+
Each persona exposes get_system_prompt_additions() for call_ai Skill.TEST_GENERATE
|
|
10
|
+
plus craft_inputs() producing concrete hostile cases for the tester agents.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from patchi.core.brain.personas.base import (
|
|
16
|
+
BasePersona,
|
|
17
|
+
PersonaStyle,
|
|
18
|
+
register_persona,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@register_persona
|
|
23
|
+
class BreakerPersona(BasePersona):
|
|
24
|
+
"""The Breaker — throws malformed input at everything until it cracks."""
|
|
25
|
+
|
|
26
|
+
def get_expertise_areas(self) -> list[str]:
|
|
27
|
+
return ["input_fuzzing", "injection", "boundary_values", "error_handling"]
|
|
28
|
+
|
|
29
|
+
def get_style(self) -> PersonaStyle:
|
|
30
|
+
return PersonaStyle.AGGRESSIVE
|
|
31
|
+
|
|
32
|
+
def get_system_prompt_additions(self) -> str:
|
|
33
|
+
return """
|
|
34
|
+
You are the BREAKER — a hostile user who types the worst possible input.
|
|
35
|
+
|
|
36
|
+
Your perspective:
|
|
37
|
+
- Every text field is a SQLi/XSS/command-injection candidate until proven otherwise
|
|
38
|
+
- Boundaries are made to be crossed: empty, 10MB, unicode, null bytes
|
|
39
|
+
- An error message that leaks a stack trace is a finding, not a shrug
|
|
40
|
+
- If the app 500s on weird input, that IS the bug report
|
|
41
|
+
|
|
42
|
+
When generating hostile inputs:
|
|
43
|
+
1. Take each input/endpoint and produce injection variants (SQL, XSS, path, command)
|
|
44
|
+
2. Add boundary cases: "", " ", very long, unicode RTL, %00, ${}, {{}}
|
|
45
|
+
3. Prefer payloads likely to reach a sink (quotes, semicolons, $(), backticks)
|
|
46
|
+
|
|
47
|
+
Your voice: Relentless, payload-focused, no mercy for unvalidated input.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def get_tool_permissions(self) -> list[str]:
|
|
51
|
+
return ["run_tests", "browser_test", "screenshot", "get_scan_results", "get_brain"]
|
|
52
|
+
|
|
53
|
+
def craft_inputs(self, seeds: list[str] | None = None, count: int = 8) -> list[str]:
|
|
54
|
+
"""Injection-heavy hostile inputs for the given seed strings."""
|
|
55
|
+
try:
|
|
56
|
+
from patchi.core.fuzz import InputFuzzer
|
|
57
|
+
|
|
58
|
+
fuzz = InputFuzzer(seed=42)
|
|
59
|
+
out: list[str] = []
|
|
60
|
+
for seed in seeds or ["test", "1", "admin"]:
|
|
61
|
+
try:
|
|
62
|
+
out.extend(str(fi.value) for fi in fuzz.fuzz_string(seed, count=count))
|
|
63
|
+
except Exception:
|
|
64
|
+
out.append(seed)
|
|
65
|
+
return out[: count * max(1, len(seeds or ["test"]))]
|
|
66
|
+
except Exception:
|
|
67
|
+
return list(seeds or ["test"])
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@register_persona
|
|
71
|
+
class ImpatientPersona(BasePersona):
|
|
72
|
+
"""The Impatient — clicks everything twice, never waits, breaks flows."""
|
|
73
|
+
|
|
74
|
+
def get_expertise_areas(self) -> list[str]:
|
|
75
|
+
return ["race_conditions", "double_submit", "navigation_flows", "loading_states"]
|
|
76
|
+
|
|
77
|
+
def get_style(self) -> PersonaStyle:
|
|
78
|
+
return PersonaStyle.AGGRESSIVE
|
|
79
|
+
|
|
80
|
+
def get_system_prompt_additions(self) -> str:
|
|
81
|
+
return """
|
|
82
|
+
You are the IMPATIENT — a user on a 3G phone with somewhere to be.
|
|
83
|
+
|
|
84
|
+
Your perspective:
|
|
85
|
+
- Double-click every submit button; if it charges twice, that's the bug
|
|
86
|
+
- Navigate away mid-request; hit back during loads; spam refresh
|
|
87
|
+
- If there is no loading state, the app is lying about its state
|
|
88
|
+
- Slow endpoints + impatient users = race conditions in production
|
|
89
|
+
|
|
90
|
+
When generating hostile flows:
|
|
91
|
+
1. List multi-step flows (checkout, wizards, deletes) and where interruption hurts
|
|
92
|
+
2. Demand idempotency keys on every mutating action
|
|
93
|
+
3. Flag any destructive action without confirm + undo
|
|
94
|
+
|
|
95
|
+
Your voice: Hurried, flow-breaking, allergic to spinners that lie.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def get_tool_permissions(self) -> list[str]:
|
|
99
|
+
return ["run_tests", "browser_test", "screenshot", "get_scan_results", "get_brain"]
|
|
100
|
+
|
|
101
|
+
def craft_inputs(self, routes: list[str] | None = None) -> list[dict]:
|
|
102
|
+
"""Rapid-navigation + double-submit scenarios for the given routes."""
|
|
103
|
+
routes = routes or ["/"]
|
|
104
|
+
flows: list[dict] = []
|
|
105
|
+
for route in routes[:10]:
|
|
106
|
+
flows.append(
|
|
107
|
+
{"route": route, "action": "double_submit", "note": "click submit twice fast"}
|
|
108
|
+
)
|
|
109
|
+
flows.append(
|
|
110
|
+
{"route": route, "action": "navigate_away", "note": "leave mid-load, hit back"}
|
|
111
|
+
)
|
|
112
|
+
return flows
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@register_persona
|
|
116
|
+
class MaliciousPersona(BasePersona):
|
|
117
|
+
"""The Malicious — probes auth like an attacker with a free afternoon."""
|
|
118
|
+
|
|
119
|
+
def get_expertise_areas(self) -> list[str]:
|
|
120
|
+
return ["auth_bypass", "jwt_attacks", "idor", "privilege_escalation"]
|
|
121
|
+
|
|
122
|
+
def get_style(self) -> PersonaStyle:
|
|
123
|
+
return PersonaStyle.AGGRESSIVE
|
|
124
|
+
|
|
125
|
+
def get_system_prompt_additions(self) -> str:
|
|
126
|
+
return """
|
|
127
|
+
You are the MALICIOUS — an attacker probing auth with legitimate-looking requests.
|
|
128
|
+
|
|
129
|
+
Your perspective:
|
|
130
|
+
- Try every protected route with NO token, a garbage token, and alg=none JWT
|
|
131
|
+
- Swap IDs in URLs (/users/123 → /users/124); if it works, that's IDOR
|
|
132
|
+
- Replay old tokens; tamper the payload; drop the signature
|
|
133
|
+
- Admin paths (/admin, /debug, /metrics) should not exist for you
|
|
134
|
+
|
|
135
|
+
When generating hostile probes:
|
|
136
|
+
1. Enumerate auth-guarded routes and the exact bypass variant per route
|
|
137
|
+
2. Prefer low-noise probes first (missing header), then tampered tokens
|
|
138
|
+
3. Every 200 on a guarded route without valid auth is a critical finding
|
|
139
|
+
|
|
140
|
+
Your voice: Quiet, methodical, thinks in attack graphs, never skips auth.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def get_tool_permissions(self) -> list[str]:
|
|
144
|
+
return ["run_tests", "browser_test", "screenshot", "get_scan_results", "get_brain"]
|
|
145
|
+
|
|
146
|
+
def craft_inputs(self, routes: list[str] | None = None) -> list[dict]:
|
|
147
|
+
"""Auth-bypass probe cases for the given guarded routes."""
|
|
148
|
+
probes: list[dict] = []
|
|
149
|
+
for route in routes or ["/admin", "/api/users/1"]:
|
|
150
|
+
probes.append({"route": route, "headers": {}, "note": "no auth header"})
|
|
151
|
+
probes.append(
|
|
152
|
+
{
|
|
153
|
+
"route": route,
|
|
154
|
+
"headers": {"Authorization": "Bearer eyJhbGciOiJub25lIn0.e30."},
|
|
155
|
+
"note": "alg=none JWT",
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
probes.append(
|
|
159
|
+
{
|
|
160
|
+
"route": route,
|
|
161
|
+
"headers": {"Authorization": "Bearer invalid.token.here"},
|
|
162
|
+
"note": "garbage token",
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
return probes
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base Persona Class — Foundation for all Council personas.
|
|
3
|
+
|
|
4
|
+
Each persona is a specialized AI agent with:
|
|
5
|
+
- Unique system prompt defining expertise and perspective
|
|
6
|
+
- Tool access permissions (which tools it can call)
|
|
7
|
+
- Decision-making style (cautious, aggressive, balanced, etc.)
|
|
8
|
+
- Memory of past decisions for learning
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
from abc import ABC, abstractmethod
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from datetime import UTC, datetime
|
|
19
|
+
from enum import Enum
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from patchi.core.ai.client import call_ai_structured
|
|
24
|
+
from patchi.core.ai.prompts import Skill, get_system_prompt
|
|
25
|
+
from patchi.core.brain.layered_brain import Layer
|
|
26
|
+
|
|
27
|
+
_log = logging.getLogger("patchi.brain.personas")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PersonaStyle(Enum):
|
|
31
|
+
"""Decision-making style of the persona."""
|
|
32
|
+
|
|
33
|
+
CAUTIOUS = "cautious" # Prefers safe, proven approaches
|
|
34
|
+
AGGRESSIVE = "aggressive" # Pushes boundaries, finds edge cases
|
|
35
|
+
BALANCED = "balanced" # Weighs tradeoffs carefully
|
|
36
|
+
INNOVATIVE = "innovative" # Seeks novel solutions
|
|
37
|
+
PRAGMATIC = "pragmatic" # Focuses on practical outcomes
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class PersonaDecision:
|
|
42
|
+
"""A single decision from a persona."""
|
|
43
|
+
|
|
44
|
+
persona_name: str
|
|
45
|
+
issue: str
|
|
46
|
+
analysis: str
|
|
47
|
+
recommendation: str
|
|
48
|
+
confidence: float # 0.0 - 1.0
|
|
49
|
+
tools_needed: list[str] = field(default_factory=list)
|
|
50
|
+
risks: list[str] = field(default_factory=list)
|
|
51
|
+
timestamp: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
|
52
|
+
reasoning_trace: list[str] = field(default_factory=list)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class PersonaMemory:
|
|
57
|
+
"""Long-term memory for a persona."""
|
|
58
|
+
|
|
59
|
+
decisions: list[PersonaDecision] = field(default_factory=list)
|
|
60
|
+
patterns_learned: dict[str, Any] = field(default_factory=dict)
|
|
61
|
+
success_rates: dict[str, float] = field(default_factory=dict)
|
|
62
|
+
last_updated: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class BasePersona(ABC):
|
|
66
|
+
"""
|
|
67
|
+
Abstract base class for all Council personas.
|
|
68
|
+
|
|
69
|
+
Each persona must implement:
|
|
70
|
+
- analyze(): Core analysis logic
|
|
71
|
+
- get_tool_permissions(): Which tools this persona can call
|
|
72
|
+
- get_style(): Decision-making style
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
name: str,
|
|
78
|
+
root: Path,
|
|
79
|
+
brain_layers: dict[str, Layer],
|
|
80
|
+
project_context: dict,
|
|
81
|
+
config: dict,
|
|
82
|
+
on_progress: Callable[[str], None] | None = None,
|
|
83
|
+
):
|
|
84
|
+
self.name = name
|
|
85
|
+
self.root = root
|
|
86
|
+
self.layers = brain_layers
|
|
87
|
+
self.project_context = project_context
|
|
88
|
+
self.config = config
|
|
89
|
+
self.on_progress = on_progress or (lambda _: None)
|
|
90
|
+
self.memory = self._load_memory()
|
|
91
|
+
self._decision_count = 0
|
|
92
|
+
|
|
93
|
+
@abstractmethod
|
|
94
|
+
def get_expertise_areas(self) -> list[str]:
|
|
95
|
+
"""Return list of domains this persona specializes in."""
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
def get_style(self) -> PersonaStyle:
|
|
100
|
+
"""Return the persona's decision-making style."""
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
@abstractmethod
|
|
104
|
+
def get_system_prompt_additions(self) -> str:
|
|
105
|
+
"""Additional system prompt content specific to this persona."""
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
def get_tool_permissions(self) -> list[str]:
|
|
109
|
+
"""Return list of tool names this persona is allowed to call."""
|
|
110
|
+
# Default: all tools. Override to restrict.
|
|
111
|
+
return ["*"]
|
|
112
|
+
|
|
113
|
+
def _load_memory(self) -> PersonaMemory:
|
|
114
|
+
"""Load persona memory from disk."""
|
|
115
|
+
mem_path = self.root / ".patchi" / "memory" / f"persona_{self.name.lower()}.json"
|
|
116
|
+
if mem_path.exists():
|
|
117
|
+
try:
|
|
118
|
+
data = json.loads(mem_path.read_text(encoding="utf-8"))
|
|
119
|
+
return PersonaMemory(
|
|
120
|
+
decisions=[PersonaDecision(**d) for d in data.get("decisions", [])],
|
|
121
|
+
patterns_learned=data.get("patterns_learned", {}),
|
|
122
|
+
success_rates=data.get("success_rates", {}),
|
|
123
|
+
last_updated=data.get("last_updated", ""),
|
|
124
|
+
)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
_log.warning(f"Failed to load memory for {self.name}: {e}")
|
|
127
|
+
return PersonaMemory()
|
|
128
|
+
|
|
129
|
+
def _save_memory(self) -> None:
|
|
130
|
+
"""Save persona memory to disk."""
|
|
131
|
+
mem_path = self.root / ".patchi" / "memory" / f"persona_{self.name.lower()}.json"
|
|
132
|
+
mem_path.parent.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
data = {
|
|
134
|
+
"decisions": [d.__dict__ for d in self.memory.decisions[-100:]], # Keep last 100
|
|
135
|
+
"patterns_learned": self.memory.patterns_learned,
|
|
136
|
+
"success_rates": self.memory.success_rates,
|
|
137
|
+
"last_updated": datetime.now(UTC).isoformat(),
|
|
138
|
+
}
|
|
139
|
+
try:
|
|
140
|
+
mem_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
141
|
+
except Exception as e:
|
|
142
|
+
_log.warning(f"Failed to save memory for {self.name}: {e}")
|
|
143
|
+
|
|
144
|
+
def _build_context(self, issue: str, additional_context: dict = None) -> str:
|
|
145
|
+
"""Build context string for AI analysis."""
|
|
146
|
+
ctx_parts = [
|
|
147
|
+
"=== PROJECT CONTEXT ===",
|
|
148
|
+
f"Project: {self.project_context.get('project_purpose', 'Unknown')}",
|
|
149
|
+
f"Domain: {self.project_context.get('project_domain', 'Unknown')}",
|
|
150
|
+
f"Frameworks: {', '.join(f.get('name', '') for f in self.project_context.get('frameworks', []))}",
|
|
151
|
+
f"Active Security Domains: {', '.join(self.project_context.get('active_security_domains', []))}",
|
|
152
|
+
"",
|
|
153
|
+
"=== LAYERED BRAIN SUMMARY ===",
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
# Add relevant layer summaries
|
|
157
|
+
for _layer_name, layer in self.layers.items():
|
|
158
|
+
if layer.level <= 2: # Project and subsystem levels
|
|
159
|
+
ctx_parts.append(f"[{layer.name}] {layer.summary}")
|
|
160
|
+
|
|
161
|
+
if additional_context:
|
|
162
|
+
ctx_parts.append("\n=== ADDITIONAL CONTEXT ===")
|
|
163
|
+
ctx_parts.append(json.dumps(additional_context, indent=2))
|
|
164
|
+
|
|
165
|
+
ctx_parts.append("\n=== CURRENT ISSUE ===")
|
|
166
|
+
ctx_parts.append(issue)
|
|
167
|
+
|
|
168
|
+
return "\n".join(ctx_parts)
|
|
169
|
+
|
|
170
|
+
def _call_ai_with_persona(
|
|
171
|
+
self,
|
|
172
|
+
prompt: str,
|
|
173
|
+
system_prompt: str = None,
|
|
174
|
+
max_tokens: int = 2000,
|
|
175
|
+
temperature: float = 0.3,
|
|
176
|
+
) -> dict | None:
|
|
177
|
+
"""Call AI with persona-specific system prompt."""
|
|
178
|
+
if system_prompt is None:
|
|
179
|
+
base_prompt = get_system_prompt(Skill.DEEP_ANALYSIS)
|
|
180
|
+
system_prompt = f"{base_prompt}\n\n{self.get_system_prompt_additions()}"
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
result = call_ai_structured(
|
|
184
|
+
config=self.config,
|
|
185
|
+
system_prompt=system_prompt,
|
|
186
|
+
user_prompt=prompt,
|
|
187
|
+
max_tokens=max_tokens,
|
|
188
|
+
temperature=temperature,
|
|
189
|
+
)
|
|
190
|
+
return result
|
|
191
|
+
except Exception as e:
|
|
192
|
+
_log.warning(f"{self.name} AI call failed: {e}")
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
def analyze(self, issue: str, context: dict = None) -> PersonaDecision:
|
|
196
|
+
"""
|
|
197
|
+
Analyze an issue and produce a decision.
|
|
198
|
+
|
|
199
|
+
This is the main entry point called by the Council.
|
|
200
|
+
"""
|
|
201
|
+
self._decision_count += 1
|
|
202
|
+
self.on_progress(f"[{self.name}] Analyzing: {issue[:80]}...")
|
|
203
|
+
|
|
204
|
+
# Build context
|
|
205
|
+
self._build_context(issue, context)
|
|
206
|
+
|
|
207
|
+
# Create analysis prompt
|
|
208
|
+
analysis_prompt = f"""
|
|
209
|
+
Analyze the following issue from your perspective as {self.name}.
|
|
210
|
+
|
|
211
|
+
Your expertise: {", ".join(self.get_expertise_areas())}
|
|
212
|
+
Your style: {self.get_style().value}
|
|
213
|
+
|
|
214
|
+
Issue: {issue}
|
|
215
|
+
|
|
216
|
+
Provide your analysis in this JSON format:
|
|
217
|
+
{{
|
|
218
|
+
"analysis": "Your detailed analysis of the issue",
|
|
219
|
+
"recommendation": "Your specific recommendation for action",
|
|
220
|
+
"confidence": 0.85,
|
|
221
|
+
"tools_needed": ["tool1", "tool2"],
|
|
222
|
+
"risks": ["risk1", "risk2"],
|
|
223
|
+
"reasoning_trace": ["step 1", "step 2", "step 3"]
|
|
224
|
+
}}
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
result = self._call_ai_with_persona(analysis_prompt, max_tokens=3000)
|
|
228
|
+
|
|
229
|
+
if result:
|
|
230
|
+
decision = PersonaDecision(
|
|
231
|
+
persona_name=self.name,
|
|
232
|
+
issue=issue,
|
|
233
|
+
analysis=result.get("analysis", ""),
|
|
234
|
+
recommendation=result.get("recommendation", ""),
|
|
235
|
+
confidence=float(result.get("confidence", 0.5)),
|
|
236
|
+
tools_needed=result.get("tools_needed", []),
|
|
237
|
+
risks=result.get("risks", []),
|
|
238
|
+
reasoning_trace=result.get("reasoning_trace", []),
|
|
239
|
+
)
|
|
240
|
+
else:
|
|
241
|
+
# Fallback heuristic decision
|
|
242
|
+
decision = self._heuristic_decision(issue, context)
|
|
243
|
+
|
|
244
|
+
# Store in memory
|
|
245
|
+
self.memory.decisions.append(decision)
|
|
246
|
+
self._save_memory()
|
|
247
|
+
|
|
248
|
+
self.on_progress(
|
|
249
|
+
f"[{self.name}] Decision: {decision.recommendation[:80]}... (confidence: {decision.confidence:.0%})"
|
|
250
|
+
)
|
|
251
|
+
return decision
|
|
252
|
+
|
|
253
|
+
def _heuristic_decision(self, issue: str, context: dict = None) -> PersonaDecision:
|
|
254
|
+
"""Fallback heuristic decision when AI is unavailable."""
|
|
255
|
+
return PersonaDecision(
|
|
256
|
+
persona_name=self.name,
|
|
257
|
+
issue=issue,
|
|
258
|
+
analysis=f"Heuristic analysis for: {issue}",
|
|
259
|
+
recommendation="Run standard scan and review findings manually",
|
|
260
|
+
confidence=0.4,
|
|
261
|
+
tools_needed=["scan_project"],
|
|
262
|
+
risks=["AI unavailable - limited analysis"],
|
|
263
|
+
reasoning_trace=["AI call failed, using fallback"],
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def record_outcome(self, decision: PersonaDecision, success: bool, details: str = "") -> None:
|
|
267
|
+
"""Record the outcome of a decision for learning."""
|
|
268
|
+
key = decision.issue[:50]
|
|
269
|
+
current = self.memory.success_rates.get(key, 0.5)
|
|
270
|
+
# Exponential moving average
|
|
271
|
+
self.memory.success_rates[key] = current * 0.7 + (1.0 if success else 0.0) * 0.3
|
|
272
|
+
self.memory.patterns_learned[key] = {
|
|
273
|
+
"last_outcome": "success" if success else "failure",
|
|
274
|
+
"details": details,
|
|
275
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
276
|
+
}
|
|
277
|
+
self._save_memory()
|
|
278
|
+
|
|
279
|
+
def get_relevant_context(self, issue: str) -> dict:
|
|
280
|
+
"""Extract relevant context from brain layers for this issue."""
|
|
281
|
+
# Use reasoning engine to find relevant layers
|
|
282
|
+
from patchi.core.brain.reasoning import ReasoningEngine
|
|
283
|
+
|
|
284
|
+
ReasoningEngine(self.root)
|
|
285
|
+
relevant = {}
|
|
286
|
+
|
|
287
|
+
# Find layers matching issue keywords
|
|
288
|
+
issue_lower = issue.lower()
|
|
289
|
+
for name, layer in self.layers.items():
|
|
290
|
+
layer_text = f"{name} {layer.summary} {layer.purpose}".lower()
|
|
291
|
+
# Simple keyword matching
|
|
292
|
+
if any(word in layer_text for word in issue_lower.split() if len(word) > 3):
|
|
293
|
+
relevant[name] = {
|
|
294
|
+
"level": layer.level,
|
|
295
|
+
"summary": layer.summary,
|
|
296
|
+
"purpose": layer.purpose,
|
|
297
|
+
"depends_on": layer.depends_on,
|
|
298
|
+
"dependents": layer.dependents,
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return relevant
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# ── Persona Registry ────────────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
PERSONA_REGISTRY: dict[str, type[BasePersona]] = {}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def register_persona(cls: type[BasePersona]) -> type[BasePersona]:
|
|
310
|
+
"""Decorator to register a persona class."""
|
|
311
|
+
PERSONA_REGISTRY[cls.__name__] = cls
|
|
312
|
+
return cls
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def get_persona_class(name: str) -> type[BasePersona] | None:
|
|
316
|
+
"""Get a persona class by name."""
|
|
317
|
+
return PERSONA_REGISTRY.get(name)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def list_personas() -> list[str]:
|
|
321
|
+
"""List all registered persona names."""
|
|
322
|
+
return list(PERSONA_REGISTRY.keys())
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def create_persona(
|
|
326
|
+
name: str,
|
|
327
|
+
root: Path,
|
|
328
|
+
brain_layers: dict[str, Layer],
|
|
329
|
+
project_context: dict,
|
|
330
|
+
config: dict,
|
|
331
|
+
on_progress: Callable[[str], None] = None,
|
|
332
|
+
) -> BasePersona | None:
|
|
333
|
+
"""Factory function to create a persona instance."""
|
|
334
|
+
cls = get_persona_class(name)
|
|
335
|
+
if cls is None:
|
|
336
|
+
_log.warning(f"Unknown persona: {name}")
|
|
337
|
+
return None
|
|
338
|
+
try:
|
|
339
|
+
return cls(
|
|
340
|
+
name=name,
|
|
341
|
+
root=root,
|
|
342
|
+
brain_layers=brain_layers,
|
|
343
|
+
project_context=project_context,
|
|
344
|
+
config=config,
|
|
345
|
+
on_progress=on_progress,
|
|
346
|
+
)
|
|
347
|
+
except Exception as e:
|
|
348
|
+
_log.error(f"Failed to create persona {name}: {e}")
|
|
349
|
+
return None
|