alysis-code 0.13.0__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.
- alysis_code/__init__.py +3 -0
- alysis_code/__main__.py +4 -0
- alysis_code/_build_info.py +14 -0
- alysis_code/account_login.py +468 -0
- alysis_code/agent/README.md +35 -0
- alysis_code/agent/__init__.py +11 -0
- alysis_code/agent/acceptance_contract.py +2217 -0
- alysis_code/agent/blast_radius.py +1403 -0
- alysis_code/agent/cache_keepalive.py +227 -0
- alysis_code/agent/completion_certificate.py +366 -0
- alysis_code/agent/completion_gate.py +306 -0
- alysis_code/agent/empty_response_stall.py +403 -0
- alysis_code/agent/errors.py +28 -0
- alysis_code/agent/llm_calls.py +475 -0
- alysis_code/agent/mutation_classification.py +227 -0
- alysis_code/agent/prompt_context.py +2508 -0
- alysis_code/agent/read_ledger.py +253 -0
- alysis_code/agent/regression_baseline.py +642 -0
- alysis_code/agent/reproduction_first.py +610 -0
- alysis_code/agent/sensitive_output.py +629 -0
- alysis_code/agent/session.py +3218 -0
- alysis_code/agent/steering.py +191 -0
- alysis_code/agent/subagent_execution.py +5177 -0
- alysis_code/agent/subagent_workspace.py +666 -0
- alysis_code/agent/tools_assembly.py +4728 -0
- alysis_code/agent/turn/__init__.py +101 -0
- alysis_code/agent/turn/core.py +8483 -0
- alysis_code/agent/turn/events.py +113 -0
- alysis_code/agent/turn/exploration.py +590 -0
- alysis_code/agent/turn/interventions.py +65 -0
- alysis_code/agent/turn/read_cache.py +420 -0
- alysis_code/agent/turn/snapshot.py +179 -0
- alysis_code/agent/turn_contract.py +661 -0
- alysis_code/agent/turn_path.py +129 -0
- alysis_code/agent/verification.py +2885 -0
- alysis_code/agent/verification_commands.py +512 -0
- alysis_code/agent/verification_evidence.py +738 -0
- alysis_code/agent_loop.py +693 -0
- alysis_code/agent_runtimes/__init__.py +51 -0
- alysis_code/agent_runtimes/base.py +114 -0
- alysis_code/agent_runtimes/builtins.py +129 -0
- alysis_code/agent_runtimes/codex_cli.py +664 -0
- alysis_code/agent_runtimes/host.py +263 -0
- alysis_code/agent_runtimes/registry.py +64 -0
- alysis_code/agent_runtimes/service.py +150 -0
- alysis_code/agentbox_client.py +416 -0
- alysis_code/agentbox_integration.py +310 -0
- alysis_code/alysis_cloud.py +152 -0
- alysis_code/approval_scope.py +276 -0
- alysis_code/assets/README.md +33 -0
- alysis_code/assets/__init__.py +126 -0
- alysis_code/assets/asset_read_core.py +281 -0
- alysis_code/assets/budget_allocator.py +456 -0
- alysis_code/assets/comprehender.py +759 -0
- alysis_code/assets/index.py +654 -0
- alysis_code/assets/ingestion.py +275 -0
- alysis_code/assets/legacy_migration.py +413 -0
- alysis_code/assets/models.py +263 -0
- alysis_code/assets/ocr.py +239 -0
- alysis_code/assets/owl/ascii/f-000.txt +13 -0
- alysis_code/assets/owl/ascii/f-001.txt +13 -0
- alysis_code/assets/owl/ascii/f-002.txt +13 -0
- alysis_code/assets/owl/ascii/f-003.txt +13 -0
- alysis_code/assets/owl/ascii/f-004.txt +13 -0
- alysis_code/assets/owl/ascii/f-005.txt +13 -0
- alysis_code/assets/owl/ascii/f-006.txt +13 -0
- alysis_code/assets/owl/ascii/f-007.txt +13 -0
- alysis_code/assets/owl/ascii/f-008.txt +13 -0
- alysis_code/assets/owl/ascii/f-009.txt +13 -0
- alysis_code/assets/owl/ascii/f-010.txt +13 -0
- alysis_code/assets/owl/ascii/f-011.txt +13 -0
- alysis_code/assets/owl/ascii/f-012.txt +13 -0
- alysis_code/assets/owl/ascii/f-013.txt +13 -0
- alysis_code/assets/owl/ascii/f-014.txt +13 -0
- alysis_code/assets/owl/ascii/f-015.txt +13 -0
- alysis_code/assets/owl/ascii/f-016.txt +13 -0
- alysis_code/assets/owl/ascii/f-017.txt +13 -0
- alysis_code/assets/owl/ascii/f-018.txt +13 -0
- alysis_code/assets/owl/ascii/f-019.txt +13 -0
- alysis_code/assets/owl/ascii/f-020.txt +13 -0
- alysis_code/assets/owl/index.html +98 -0
- alysis_code/assets/owl/show-owl.sh +761 -0
- alysis_code/assets/paths.py +49 -0
- alysis_code/assets/plan_binding.py +326 -0
- alysis_code/assets/planner_context.py +466 -0
- alysis_code/assets/planner_tools.py +184 -0
- alysis_code/assets/prompts.py +101 -0
- alysis_code/assets/replanner_context.py +239 -0
- alysis_code/assets/surface.py +521 -0
- alysis_code/assets/untrusted_content.py +48 -0
- alysis_code/assets/usage_logger.py +94 -0
- alysis_code/assets/worker_mirror.py +428 -0
- alysis_code/assets/worker_section.py +303 -0
- alysis_code/assets/worker_tools.py +468 -0
- alysis_code/atomic_io.py +83 -0
- alysis_code/auth_diagnostics.py +272 -0
- alysis_code/background_runner.py +366 -0
- alysis_code/branding.py +270 -0
- alysis_code/budget_policy.py +390 -0
- alysis_code/build_identity.py +465 -0
- alysis_code/builtin_hooks/__init__.py +7 -0
- alysis_code/builtin_hooks/notify_done_windows.py +65 -0
- alysis_code/bwrap_etc.py +76 -0
- alysis_code/cancellation.py +41 -0
- alysis_code/capabilities.py +137 -0
- alysis_code/chatgpt_codex_static_provider.py +133 -0
- alysis_code/cli.py +51 -0
- alysis_code/cli_impl/__init__.py +1 -0
- alysis_code/cli_impl/assets_cli.py +537 -0
- alysis_code/cli_impl/assets_modal.py +412 -0
- alysis_code/cli_impl/chat/__init__.py +156 -0
- alysis_code/cli_impl/chat/commands.py +2616 -0
- alysis_code/cli_impl/chat/loop.py +4508 -0
- alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
- alysis_code/cli_impl/chat/rendering.py +444 -0
- alysis_code/cli_impl/chat/state.py +124 -0
- alysis_code/cli_impl/chat_resume.py +830 -0
- alysis_code/cli_impl/chat_slash_completer.py +258 -0
- alysis_code/cli_impl/commands/__init__.py +11 -0
- alysis_code/cli_impl/commands/_shared.py +89 -0
- alysis_code/cli_impl/commands/auth.py +623 -0
- alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
- alysis_code/cli_impl/commands/chat_state.py +158 -0
- alysis_code/cli_impl/commands/chat_status.py +1248 -0
- alysis_code/cli_impl/commands/chat_terminal.py +942 -0
- alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
- alysis_code/cli_impl/commands/cli_common.py +1223 -0
- alysis_code/cli_impl/commands/cli_surface.py +77 -0
- alysis_code/cli_impl/commands/config.py +131 -0
- alysis_code/cli_impl/commands/conventions.py +85 -0
- alysis_code/cli_impl/commands/execution_helpers.py +350 -0
- alysis_code/cli_impl/commands/extensions.py +401 -0
- alysis_code/cli_impl/commands/forge.py +1282 -0
- alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
- alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
- alysis_code/cli_impl/commands/hooks.py +737 -0
- alysis_code/cli_impl/commands/ide_bridge.py +31 -0
- alysis_code/cli_impl/commands/mcp.py +700 -0
- alysis_code/cli_impl/commands/profile.py +453 -0
- alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
- alysis_code/cli_impl/commands/report.py +88 -0
- alysis_code/cli_impl/commands/root.py +1118 -0
- alysis_code/cli_impl/commands/sandbox.py +184 -0
- alysis_code/cli_impl/commands/server.py +54 -0
- alysis_code/cli_impl/commands/sessions.py +252 -0
- alysis_code/cli_impl/commands/skills.py +404 -0
- alysis_code/cli_impl/commands/startup.py +946 -0
- alysis_code/cli_impl/commands/tools.py +335 -0
- alysis_code/cli_impl/commands/update.py +364 -0
- alysis_code/cli_impl/commands/welcome.py +972 -0
- alysis_code/cli_impl/config_menu.py +3882 -0
- alysis_code/cli_impl/forge.py +4509 -0
- alysis_code/cli_impl/forge_recovery.py +485 -0
- alysis_code/cli_impl/setup_wizard.py +2409 -0
- alysis_code/cli_impl/tui/__init__.py +58 -0
- alysis_code/cli_impl/tui/app.py +4551 -0
- alysis_code/cli_impl/tui/config.py +32 -0
- alysis_code/cli_impl/tui/config_flow.py +2754 -0
- alysis_code/cli_impl/tui/config_overlay.py +566 -0
- alysis_code/cli_impl/tui/content.py +78 -0
- alysis_code/cli_impl/tui/footer.py +218 -0
- alysis_code/cli_impl/tui/forge_status.py +136 -0
- alysis_code/cli_impl/tui/markdown.py +244 -0
- alysis_code/cli_impl/tui/owl.py +109 -0
- alysis_code/cli_impl/tui/plan_meta.py +477 -0
- alysis_code/cli_impl/tui/setup_app.py +519 -0
- alysis_code/cli_impl/tui/setup_flow.py +1622 -0
- alysis_code/cli_impl/tui/state.py +101 -0
- alysis_code/cli_impl/tui/subagent_identity.py +66 -0
- alysis_code/cli_impl/tui/subagent_panel.py +186 -0
- alysis_code/cli_impl/tui/surface.py +796 -0
- alysis_code/cli_impl/tui/transcript.py +514 -0
- alysis_code/cli_impl/tui/update_prompt.py +79 -0
- alysis_code/cli_impl/tui/workspace_guard.py +384 -0
- alysis_code/clipboard.py +172 -0
- alysis_code/code_review.py +1211 -0
- alysis_code/compaction/__init__.py +28 -0
- alysis_code/compaction/conversation_compactor.py +2932 -0
- alysis_code/compaction/importance.py +177 -0
- alysis_code/compaction/settings.py +297 -0
- alysis_code/compaction/tool_output_offload.py +447 -0
- alysis_code/config.py +3509 -0
- alysis_code/conflict_auto_resolver.py +895 -0
- alysis_code/context/__init__.py +1 -0
- alysis_code/context/tool_schema_budgeter.py +220 -0
- alysis_code/crash_diagnostics.py +282 -0
- alysis_code/custom_tools/README.md +34 -0
- alysis_code/custom_tools/__init__.py +43 -0
- alysis_code/custom_tools/discovery.py +903 -0
- alysis_code/custom_tools/runtime.py +1516 -0
- alysis_code/custom_tools/session.py +227 -0
- alysis_code/custom_tools/trust.py +232 -0
- alysis_code/diff_paths.py +113 -0
- alysis_code/direction_change.py +293 -0
- alysis_code/dispatch_timing.py +306 -0
- alysis_code/durable_service_manager.py +1236 -0
- alysis_code/edit_discipline.py +659 -0
- alysis_code/error_text.py +73 -0
- alysis_code/execution_budget.py +411 -0
- alysis_code/execution_context.py +915 -0
- alysis_code/execution_deadline.py +1065 -0
- alysis_code/execution_shared.py +1904 -0
- alysis_code/extensions/README.md +30 -0
- alysis_code/extensions/__init__.py +93 -0
- alysis_code/extensions/activation.py +138 -0
- alysis_code/extensions/install.py +1436 -0
- alysis_code/extensions/manifest.py +487 -0
- alysis_code/extensions/models.py +74 -0
- alysis_code/extensions/paths.py +56 -0
- alysis_code/extensions/registry.json +4 -0
- alysis_code/extensions/registry.py +52 -0
- alysis_code/extensions/state.py +83 -0
- alysis_code/extensions/workspace_trust.py +101 -0
- alysis_code/failed_task_evidence.py +369 -0
- alysis_code/failure_category.py +315 -0
- alysis_code/feedback_report.py +1647 -0
- alysis_code/file_classification.py +485 -0
- alysis_code/forge.py +2064 -0
- alysis_code/forge_completion.py +362 -0
- alysis_code/forge_events.py +475 -0
- alysis_code/frontmatter_utils.py +95 -0
- alysis_code/git_evidence.py +1181 -0
- alysis_code/git_ops.py +560 -0
- alysis_code/git_safe.py +62 -0
- alysis_code/git_worktrees.py +190 -0
- alysis_code/hooks/README.md +33 -0
- alysis_code/hooks/__init__.py +67 -0
- alysis_code/hooks/audit.py +171 -0
- alysis_code/hooks/config.py +225 -0
- alysis_code/hooks/dispatcher.py +1110 -0
- alysis_code/hooks/models.py +447 -0
- alysis_code/hooks/trust.py +202 -0
- alysis_code/host_actions.py +543 -0
- alysis_code/host_browser.py +103 -0
- alysis_code/ide/__init__.py +5 -0
- alysis_code/ide/activity_events.py +399 -0
- alysis_code/ide/approvals.py +337 -0
- alysis_code/ide/artifacts.py +153 -0
- alysis_code/ide/browser_egress_proxy.py +1076 -0
- alysis_code/ide/cdp_websocket_transport.py +1192 -0
- alysis_code/ide/change_ledger.py +1721 -0
- alysis_code/ide/context_blocks.py +979 -0
- alysis_code/ide/event_stream.py +531 -0
- alysis_code/ide/forge_protocol.py +3112 -0
- alysis_code/ide/forge_request_ledger.py +737 -0
- alysis_code/ide/health.py +965 -0
- alysis_code/ide/managed_browser.py +2251 -0
- alysis_code/ide/management_protocol.py +3414 -0
- alysis_code/ide/mcp_oauth_coordinator.py +744 -0
- alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
- alysis_code/ide/prompt_queue.py +1070 -0
- alysis_code/ide/protocol.py +191 -0
- alysis_code/ide/resumable_swarm.py +1543 -0
- alysis_code/ide/session_search.py +295 -0
- alysis_code/ide/stdio_bridge.py +9935 -0
- alysis_code/ide/structured_state.py +1579 -0
- alysis_code/ide/swarm_protocol.py +816 -0
- alysis_code/integration_gate.py +506 -0
- alysis_code/interactive_input_guard.py +39 -0
- alysis_code/interactive_plan_mode.py +26 -0
- alysis_code/internal_artifacts.py +179 -0
- alysis_code/knowledge_base.py +1409 -0
- alysis_code/knowledge_capture.py +1190 -0
- alysis_code/knowledge_librarian.py +605 -0
- alysis_code/language_policy.py +34 -0
- alysis_code/litellm_static_provider.py +535 -0
- alysis_code/llm/__init__.py +1 -0
- alysis_code/llm/anthropic_messages.py +2288 -0
- alysis_code/llm/base.py +71 -0
- alysis_code/llm/cache_capabilities.py +985 -0
- alysis_code/llm/cache_control_blocks.py +244 -0
- alysis_code/llm/cache_policy.py +388 -0
- alysis_code/llm/factory.py +373 -0
- alysis_code/llm/gemini_generate_content.py +2652 -0
- alysis_code/llm/gemini_interactions.py +739 -0
- alysis_code/llm/metadata.py +450 -0
- alysis_code/llm/openai_compat.py +2947 -0
- alysis_code/llm/openai_responses.py +2604 -0
- alysis_code/llm/protocols.py +609 -0
- alysis_code/llm/provider_limits.py +525 -0
- alysis_code/llm/request_plan.py +389 -0
- alysis_code/llm/request_shape.py +238 -0
- alysis_code/llm/streaming.py +108 -0
- alysis_code/llm/temperature_compat.py +78 -0
- alysis_code/llm/types.py +195 -0
- alysis_code/llm/usage_normalization.py +222 -0
- alysis_code/llm_error_display.py +315 -0
- alysis_code/logging_redaction.py +326 -0
- alysis_code/managed_host_deadline.py +196 -0
- alysis_code/mcp/README.md +33 -0
- alysis_code/mcp/__init__.py +24 -0
- alysis_code/mcp/client.py +1137 -0
- alysis_code/mcp/config.py +597 -0
- alysis_code/mcp/errors.py +113 -0
- alysis_code/mcp/forge_scope.py +154 -0
- alysis_code/mcp/jsonrpc.py +214 -0
- alysis_code/mcp/manager.py +2308 -0
- alysis_code/mcp/models.py +666 -0
- alysis_code/mcp/oauth.py +972 -0
- alysis_code/mcp/oauth_runtime.py +310 -0
- alysis_code/mcp/oauth_store.py +276 -0
- alysis_code/mcp/prompts.py +329 -0
- alysis_code/mcp/resources.py +295 -0
- alysis_code/mcp/roots.py +106 -0
- alysis_code/mcp/server_requests.py +75 -0
- alysis_code/mcp/token_store.py +859 -0
- alysis_code/mcp/transport_http.py +1338 -0
- alysis_code/mcp/transport_stdio.py +1267 -0
- alysis_code/mcp/untrusted_content.py +119 -0
- alysis_code/merge_conflict_reviewer.py +729 -0
- alysis_code/model_catalog/__init__.py +1 -0
- alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
- alysis_code/model_metadata_policy.py +223 -0
- alysis_code/model_metadata_utils.py +103 -0
- alysis_code/model_registry.py +1420 -0
- alysis_code/model_router.py +147 -0
- alysis_code/permission_policy.py +1016 -0
- alysis_code/personas.py +451 -0
- alysis_code/pipeline_facts.py +233 -0
- alysis_code/plan_assistant.py +4763 -0
- alysis_code/plan_mode.py +393 -0
- alysis_code/plan_reconciliation.py +1228 -0
- alysis_code/plan_repair.py +652 -0
- alysis_code/plan_validation.py +1099 -0
- alysis_code/planning_constraints.py +904 -0
- alysis_code/policy.py +95 -0
- alysis_code/preview_server.py +457 -0
- alysis_code/process_reaping.py +566 -0
- alysis_code/profile_presets.py +1834 -0
- alysis_code/profiles.py +666 -0
- alysis_code/provider_auth/__init__.py +29 -0
- alysis_code/provider_auth/base.py +99 -0
- alysis_code/provider_auth/openai_codex.py +951 -0
- alysis_code/provider_auth/registry.py +76 -0
- alysis_code/provider_auth/store.py +125 -0
- alysis_code/provider_diagnostics.py +1209 -0
- alysis_code/provider_model_catalog.py +685 -0
- alysis_code/provider_telemetry.py +1699 -0
- alysis_code/provider_url.py +75 -0
- alysis_code/reasoning_contracts.py +911 -0
- alysis_code/remote_sync.py +350 -0
- alysis_code/replanning.py +1195 -0
- alysis_code/repo_scan.py +1152 -0
- alysis_code/request_estimation.py +296 -0
- alysis_code/review_gate.py +617 -0
- alysis_code/run_lock.py +1141 -0
- alysis_code/run_outcome.py +58 -0
- alysis_code/run_provenance.py +774 -0
- alysis_code/run_state.py +445 -0
- alysis_code/runtime_artifacts.py +116 -0
- alysis_code/runtime_context_features.py +78 -0
- alysis_code/runtime_kind.py +52 -0
- alysis_code/safety/__init__.py +11 -0
- alysis_code/safety/mcp_sanitize.py +29 -0
- alysis_code/safety/safe_http.py +297 -0
- alysis_code/safety/subagent_report.py +184 -0
- alysis_code/sandbox_doctor.py +682 -0
- alysis_code/sandbox_runner.py +1025 -0
- alysis_code/sandbox_settings.py +423 -0
- alysis_code/serialized_paths.py +355 -0
- alysis_code/server/__init__.py +3 -0
- alysis_code/server/app.py +367 -0
- alysis_code/server/auth.py +34 -0
- alysis_code/server/job_config.py +30 -0
- alysis_code/server/settings.py +215 -0
- alysis_code/server/store.py +193 -0
- alysis_code/server/worker_runner.py +657 -0
- alysis_code/service_persistence.py +355 -0
- alysis_code/session_artifacts.py +108 -0
- alysis_code/session_metrics.py +331 -0
- alysis_code/session_store.py +624 -0
- alysis_code/skills/README.md +34 -0
- alysis_code/skills/__init__.py +104 -0
- alysis_code/skills/conventions.py +84 -0
- alysis_code/skills/discovery.py +176 -0
- alysis_code/skills/eval_models.py +232 -0
- alysis_code/skills/eval_runner.py +372 -0
- alysis_code/skills/evals.py +1344 -0
- alysis_code/skills/install.py +293 -0
- alysis_code/skills/loader.py +118 -0
- alysis_code/skills/matching.py +103 -0
- alysis_code/skills/models.py +71 -0
- alysis_code/skills/paths.py +56 -0
- alysis_code/skills/prompting.py +500 -0
- alysis_code/skills/scaffold.py +142 -0
- alysis_code/skills/state.py +441 -0
- alysis_code/skills/transactions.py +125 -0
- alysis_code/skills/validation.py +304 -0
- alysis_code/step_budget.py +238 -0
- alysis_code/subagent_labels.py +49 -0
- alysis_code/subagents.py +1072 -0
- alysis_code/surface/__init__.py +80 -0
- alysis_code/surface/base.py +305 -0
- alysis_code/surface/console.py +387 -0
- alysis_code/surface/events.py +372 -0
- alysis_code/surface/hidden_surface.py +529 -0
- alysis_code/surface/noop_surface.py +219 -0
- alysis_code/surface/rich_surface.py +1555 -0
- alysis_code/surface/styles.py +67 -0
- alysis_code/surface/theme.py +455 -0
- alysis_code/surface/types.py +100 -0
- alysis_code/swarm_backend.py +926 -0
- alysis_code/swarm_orchestrator.py +4020 -0
- alysis_code/swarm_scheduler.py +441 -0
- alysis_code/swarm_trace.py +429 -0
- alysis_code/swarm_worker.py +2119 -0
- alysis_code/swarm_write_guard.py +348 -0
- alysis_code/task_dependencies.py +170 -0
- alysis_code/task_readiness.py +992 -0
- alysis_code/task_scope.py +2148 -0
- alysis_code/terminal_manager.py +762 -0
- alysis_code/terminal_ownership.py +460 -0
- alysis_code/text_normalization.py +30 -0
- alysis_code/token_budget.py +97 -0
- alysis_code/tools/README.md +34 -0
- alysis_code/tools/__init__.py +1 -0
- alysis_code/tools/artifacts.py +127 -0
- alysis_code/tools/availability.py +188 -0
- alysis_code/tools/fs.py +1456 -0
- alysis_code/tools/git.py +461 -0
- alysis_code/tools/history.py +229 -0
- alysis_code/tools/http_timeout.py +78 -0
- alysis_code/tools/image_generation.py +552 -0
- alysis_code/tools/registry.py +2936 -0
- alysis_code/tools/repo_map.py +476 -0
- alysis_code/tools/search.py +563 -0
- alysis_code/tools/shell.py +135 -0
- alysis_code/tools/symbols.py +1350 -0
- alysis_code/tools/test_discovery.py +643 -0
- alysis_code/tools/web.py +482 -0
- alysis_code/tools/web_search.py +2012 -0
- alysis_code/tools/web_search_dashscope.py +557 -0
- alysis_code/tools/web_search_ddgs.py +221 -0
- alysis_code/tools/web_search_provider_adapters.py +1429 -0
- alysis_code/tools/web_search_tavily.py +194 -0
- alysis_code/updates.py +933 -0
- alysis_code/usage_tracker.py +1990 -0
- alysis_code/verification_command_analysis.py +1004 -0
- alysis_code/verification_contract.py +574 -0
- alysis_code/verification_failure_summary.py +273 -0
- alysis_code/verification_repair.py +385 -0
- alysis_code/verify_gate.py +3129 -0
- alysis_code/web_research.py +1872 -0
- alysis_code/web_search_adapters.py +66 -0
- alysis_code/web_search_policy.py +27 -0
- alysis_code/workspace_binding.py +389 -0
- alysis_code/workspace_binding_ui.py +408 -0
- alysis_code/workspace_context.py +273 -0
- alysis_code/workspace_isolation.py +138 -0
- alysis_code/workspace_provisioning.py +455 -0
- alysis_code-0.13.0.dist-info/METADATA +507 -0
- alysis_code-0.13.0.dist-info/RECORD +458 -0
- alysis_code-0.13.0.dist-info/WHEEL +4 -0
- alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
- alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
- alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
import warnings
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .config import (
|
|
12
|
+
AppConfig,
|
|
13
|
+
ConfigError,
|
|
14
|
+
get_api_key,
|
|
15
|
+
resolve_llm_timeout_s,
|
|
16
|
+
resolve_model_access_api_key,
|
|
17
|
+
resolve_prompt_cache_key,
|
|
18
|
+
resolve_prompt_cache_retention,
|
|
19
|
+
resolve_role_temperature,
|
|
20
|
+
)
|
|
21
|
+
from .execution_shared import safe_task_file_component
|
|
22
|
+
from .forge import RunPaths, ensure_execution_dirs, now_iso
|
|
23
|
+
from .git_safe import build_git_cmd
|
|
24
|
+
from .llm.base import ChatClient
|
|
25
|
+
from .llm.factory import _resolve_base_url, make_llm_client
|
|
26
|
+
from .llm.openai_compat import OpenAICompatClient as _OpenAICompatClient
|
|
27
|
+
from .llm.types import LLMError
|
|
28
|
+
from .model_metadata_policy import (
|
|
29
|
+
ActiveModelRef,
|
|
30
|
+
ModelMetadataPolicyError,
|
|
31
|
+
evaluate_active_model_metadata_policy,
|
|
32
|
+
)
|
|
33
|
+
from .model_registry import ModelRegistry
|
|
34
|
+
from .model_router import ROLE_CONFLICT_REVIEW, resolve_model_for_role
|
|
35
|
+
from .profiles import get_active_profile
|
|
36
|
+
|
|
37
|
+
_ALLOWED_RESOLUTION = {"take_ours", "take_theirs", "manual_merge"}
|
|
38
|
+
_ALLOWED_CONFIDENCE = {"high", "medium", "low"}
|
|
39
|
+
_ALLOWED_RISK = {"low", "medium", "high"}
|
|
40
|
+
_CONFLICT_REVIEW_TRANSIENT_REQUEST_MAX_ATTEMPTS = 2
|
|
41
|
+
_RETRYABLE_LLM_HTTP_STATUSES = {408, 429, 500, 502, 503, 504}
|
|
42
|
+
_RETRYABLE_LLM_REQUEST_ERROR_MARKERS = (
|
|
43
|
+
"connection aborted",
|
|
44
|
+
"connection reset",
|
|
45
|
+
"connect timeout",
|
|
46
|
+
"connecttimeout",
|
|
47
|
+
"eof",
|
|
48
|
+
"pool timeout",
|
|
49
|
+
"read error",
|
|
50
|
+
"read timeout",
|
|
51
|
+
"readtimeout",
|
|
52
|
+
"remote protocol error",
|
|
53
|
+
"temporarily unavailable",
|
|
54
|
+
"timed out",
|
|
55
|
+
)
|
|
56
|
+
_REQUEST_RETRY_STATE_NONE = "none"
|
|
57
|
+
_REQUEST_RETRY_STATE_RECOVERED = "recovered"
|
|
58
|
+
_REQUEST_RETRY_STATE_EXHAUSTED = "exhausted"
|
|
59
|
+
_REQUEST_RETRY_STATE_FINAL_FAILURE_AFTER_RETRY = "final_failure_after_retry"
|
|
60
|
+
|
|
61
|
+
# Compatibility surface for tests and downstream monkeypatches that predate the LLM factory.
|
|
62
|
+
OpenAICompatClient = _OpenAICompatClient
|
|
63
|
+
|
|
64
|
+
MERGE_CONFLICT_REVIEWER_SYSTEM_PROMPT = """You are a strict merge-conflict reviewer.
|
|
65
|
+
|
|
66
|
+
Goal
|
|
67
|
+
- Analyze the merge conflict context (base/ours/theirs/working excerpts) and recommend the safest resolution strategy.
|
|
68
|
+
- Be conservative: when uncertain, recommend manual_merge and explain why.
|
|
69
|
+
|
|
70
|
+
Guidelines
|
|
71
|
+
- Prefer minimal, low-risk merges that preserve intended behavior.
|
|
72
|
+
- Consider task acceptance criteria and write scope constraints if provided.
|
|
73
|
+
- Call out risks (build breaks, semantic changes, missing tests, formatting traps).
|
|
74
|
+
|
|
75
|
+
Output
|
|
76
|
+
- Return valid JSON only, strictly matching the schema requested by the user prompt.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class ConflictReviewOutcome:
|
|
82
|
+
review_json: dict[str, Any] | None
|
|
83
|
+
review_markdown: str
|
|
84
|
+
skipped_reason: str | None
|
|
85
|
+
request_retry_count: int = 0
|
|
86
|
+
request_retry_state: str = _REQUEST_RETRY_STATE_NONE
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class ConflictArtifactPaths:
|
|
91
|
+
conflict_dir: Path
|
|
92
|
+
context_json_path: Path
|
|
93
|
+
review_json_path: Path | None
|
|
94
|
+
review_md_path: Path
|
|
95
|
+
cleanup_log_path: Path
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _run_git(root: Path, args: list[str]) -> subprocess.CompletedProcess[str]:
|
|
99
|
+
return subprocess.run(
|
|
100
|
+
build_git_cmd(root, args),
|
|
101
|
+
check=False,
|
|
102
|
+
capture_output=True,
|
|
103
|
+
text=True,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _truncate_head_tail(text: str, *, max_chars: int = 8000) -> str:
|
|
108
|
+
if len(text) <= max_chars:
|
|
109
|
+
return text
|
|
110
|
+
half = max_chars // 2
|
|
111
|
+
omitted = len(text) - max_chars
|
|
112
|
+
return text[:half] + f"\n\n... [truncated {omitted} chars] ...\n\n" + text[-half:]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _read_text_excerpt(path: Path, *, max_chars: int = 8000) -> str:
|
|
116
|
+
if not path.exists() or not path.is_file():
|
|
117
|
+
return ""
|
|
118
|
+
try:
|
|
119
|
+
content = path.read_text(encoding="utf-8", errors="replace")
|
|
120
|
+
except OSError:
|
|
121
|
+
return ""
|
|
122
|
+
return _truncate_head_tail(content, max_chars=max_chars)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _extract_json_object(raw: str) -> dict[str, Any]:
|
|
126
|
+
text = raw.strip()
|
|
127
|
+
try:
|
|
128
|
+
parsed = json.loads(text)
|
|
129
|
+
except json.JSONDecodeError:
|
|
130
|
+
parsed = None
|
|
131
|
+
if isinstance(parsed, dict):
|
|
132
|
+
return parsed
|
|
133
|
+
|
|
134
|
+
start = text.find("{")
|
|
135
|
+
end = text.rfind("}")
|
|
136
|
+
if start != -1 and end != -1 and end > start:
|
|
137
|
+
sliced = text[start : end + 1]
|
|
138
|
+
parsed = json.loads(sliced)
|
|
139
|
+
if isinstance(parsed, dict):
|
|
140
|
+
return parsed
|
|
141
|
+
raise ValueError("model response is not a valid JSON object")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _normalize_per_file(value: Any) -> list[dict[str, str]]:
|
|
145
|
+
if not isinstance(value, list):
|
|
146
|
+
raise ValueError("per_file must be a list")
|
|
147
|
+
out: list[dict[str, str]] = []
|
|
148
|
+
for idx, item in enumerate(value, start=1):
|
|
149
|
+
if not isinstance(item, dict):
|
|
150
|
+
raise ValueError(f"per_file[{idx}] must be an object")
|
|
151
|
+
path = str(item.get("path") or "").strip()
|
|
152
|
+
recommended_resolution = str(item.get("recommended_resolution") or "").strip().lower()
|
|
153
|
+
notes = str(item.get("notes") or "").strip()
|
|
154
|
+
risk = str(item.get("risk") or "").strip().lower()
|
|
155
|
+
if recommended_resolution not in _ALLOWED_RESOLUTION:
|
|
156
|
+
raise ValueError(
|
|
157
|
+
f"per_file[{idx}].recommended_resolution must be one of "
|
|
158
|
+
"take_ours|take_theirs|manual_merge"
|
|
159
|
+
)
|
|
160
|
+
if risk not in _ALLOWED_RISK:
|
|
161
|
+
raise ValueError(f"per_file[{idx}].risk must be one of low|medium|high")
|
|
162
|
+
out.append(
|
|
163
|
+
{
|
|
164
|
+
"path": path or "(unknown)",
|
|
165
|
+
"recommended_resolution": recommended_resolution,
|
|
166
|
+
"notes": notes or "(no notes)",
|
|
167
|
+
"risk": risk,
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
return out
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _normalize_review_json(raw: dict[str, Any], *, task_id: str) -> dict[str, Any]:
|
|
174
|
+
confidence = str(raw.get("confidence") or "").strip().lower()
|
|
175
|
+
summary = str(raw.get("summary") or "").strip()
|
|
176
|
+
root_cause = str(raw.get("root_cause") or "").strip()
|
|
177
|
+
strategy = str(raw.get("recommended_strategy") or "").strip()
|
|
178
|
+
|
|
179
|
+
if confidence not in _ALLOWED_CONFIDENCE:
|
|
180
|
+
raise ValueError("confidence must be high|medium|low")
|
|
181
|
+
if not summary:
|
|
182
|
+
raise ValueError("summary is required")
|
|
183
|
+
if not root_cause:
|
|
184
|
+
raise ValueError("root_cause is required")
|
|
185
|
+
if not strategy:
|
|
186
|
+
raise ValueError("recommended_strategy is required")
|
|
187
|
+
|
|
188
|
+
next_steps_raw = raw.get("next_steps")
|
|
189
|
+
if not isinstance(next_steps_raw, list):
|
|
190
|
+
raise ValueError("next_steps must be a list")
|
|
191
|
+
next_steps = [str(item).strip() for item in next_steps_raw if str(item).strip()]
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
"task_id": task_id,
|
|
195
|
+
"confidence": confidence,
|
|
196
|
+
"summary": summary,
|
|
197
|
+
"root_cause": root_cause,
|
|
198
|
+
"recommended_strategy": strategy,
|
|
199
|
+
"per_file": _normalize_per_file(raw.get("per_file")),
|
|
200
|
+
"next_steps": next_steps,
|
|
201
|
+
"generated_at": now_iso(),
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _llm_error_status_code(err: LLMError) -> int | None:
|
|
206
|
+
match = re.match(r"LLM error (\d{3}):", str(err or "").strip())
|
|
207
|
+
if match is None:
|
|
208
|
+
return None
|
|
209
|
+
try:
|
|
210
|
+
return int(match.group(1))
|
|
211
|
+
except ValueError:
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _is_retryable_conflict_review_request_error(err: LLMError) -> bool:
|
|
216
|
+
status_code = _llm_error_status_code(err)
|
|
217
|
+
if status_code is not None:
|
|
218
|
+
return status_code in _RETRYABLE_LLM_HTTP_STATUSES
|
|
219
|
+
|
|
220
|
+
message = str(err or "").casefold().strip()
|
|
221
|
+
if not message.startswith("llm request failed:"):
|
|
222
|
+
return False
|
|
223
|
+
return any(marker in message for marker in _RETRYABLE_LLM_REQUEST_ERROR_MARKERS)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _attach_conflict_review_retry_details(
|
|
227
|
+
err: LLMError,
|
|
228
|
+
*,
|
|
229
|
+
retry_count: int,
|
|
230
|
+
exhausted: bool,
|
|
231
|
+
) -> LLMError:
|
|
232
|
+
err.conflict_review_request_retry_count = int(retry_count) # type: ignore[attr-defined]
|
|
233
|
+
err.conflict_review_request_retry_exhausted = bool(exhausted) # type: ignore[attr-defined]
|
|
234
|
+
return err
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _request_retry_state_for_terminal_outcome(
|
|
238
|
+
*,
|
|
239
|
+
request_retry_count: int,
|
|
240
|
+
request_retry_exhausted: bool = False,
|
|
241
|
+
skipped_reason: str | None,
|
|
242
|
+
) -> str:
|
|
243
|
+
if request_retry_count <= 0:
|
|
244
|
+
return _REQUEST_RETRY_STATE_NONE
|
|
245
|
+
if not skipped_reason:
|
|
246
|
+
return _REQUEST_RETRY_STATE_RECOVERED
|
|
247
|
+
if request_retry_exhausted:
|
|
248
|
+
return _REQUEST_RETRY_STATE_EXHAUSTED
|
|
249
|
+
return _REQUEST_RETRY_STATE_FINAL_FAILURE_AFTER_RETRY
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _request_retry_line(*, retry_count: int, retry_state: str) -> str | None:
|
|
253
|
+
if retry_count <= 0:
|
|
254
|
+
return None
|
|
255
|
+
retry_word = "retry" if retry_count == 1 else "retries"
|
|
256
|
+
if retry_state == _REQUEST_RETRY_STATE_RECOVERED:
|
|
257
|
+
return f"- Request Retries: {retry_count} transient {retry_word} before successful review."
|
|
258
|
+
if retry_state == _REQUEST_RETRY_STATE_EXHAUSTED:
|
|
259
|
+
return (
|
|
260
|
+
f"- Request Retries: {retry_count} transient {retry_word} exhausted before review "
|
|
261
|
+
"was skipped."
|
|
262
|
+
)
|
|
263
|
+
if retry_state == _REQUEST_RETRY_STATE_FINAL_FAILURE_AFTER_RETRY:
|
|
264
|
+
return (
|
|
265
|
+
f"- Request Retries: {retry_count} transient {retry_word} before final review failure."
|
|
266
|
+
)
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _request_conflict_review_response(
|
|
271
|
+
*,
|
|
272
|
+
client: ChatClient,
|
|
273
|
+
messages: list[dict[str, str]],
|
|
274
|
+
) -> tuple[Any, int]:
|
|
275
|
+
retry_count = 0
|
|
276
|
+
for attempt in range(1, _CONFLICT_REVIEW_TRANSIENT_REQUEST_MAX_ATTEMPTS + 1):
|
|
277
|
+
try:
|
|
278
|
+
return (
|
|
279
|
+
client.chat(
|
|
280
|
+
messages=messages,
|
|
281
|
+
stream=False,
|
|
282
|
+
),
|
|
283
|
+
retry_count,
|
|
284
|
+
)
|
|
285
|
+
except LLMError as e:
|
|
286
|
+
retryable = _is_retryable_conflict_review_request_error(e)
|
|
287
|
+
if not retryable or attempt >= _CONFLICT_REVIEW_TRANSIENT_REQUEST_MAX_ATTEMPTS:
|
|
288
|
+
# TODO(failure-category-audit): default=implementation_failed,
|
|
289
|
+
# site=merge conflict reviewer retry exhaustion outside Forge result trend, see failure_category.py
|
|
290
|
+
raise _attach_conflict_review_retry_details(
|
|
291
|
+
e,
|
|
292
|
+
retry_count=retry_count,
|
|
293
|
+
exhausted=retryable
|
|
294
|
+
and attempt >= _CONFLICT_REVIEW_TRANSIENT_REQUEST_MAX_ATTEMPTS,
|
|
295
|
+
) from e
|
|
296
|
+
retry_count += 1
|
|
297
|
+
raise AssertionError("unreachable")
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _make_conflict_review_llm_client(
|
|
301
|
+
*,
|
|
302
|
+
cfg: AppConfig,
|
|
303
|
+
api_key: str,
|
|
304
|
+
model_name: str,
|
|
305
|
+
) -> ChatClient:
|
|
306
|
+
temperature = resolve_role_temperature(cfg, role="conflict_review")
|
|
307
|
+
if OpenAICompatClient is _OpenAICompatClient:
|
|
308
|
+
return make_llm_client(
|
|
309
|
+
cfg=cfg,
|
|
310
|
+
api_key=api_key,
|
|
311
|
+
model=model_name,
|
|
312
|
+
timeout_s=resolve_llm_timeout_s(cfg),
|
|
313
|
+
temperature=temperature,
|
|
314
|
+
prompt_cache_key=resolve_prompt_cache_key(cfg),
|
|
315
|
+
prompt_cache_retention=resolve_prompt_cache_retention(cfg),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
profile = get_active_profile(cfg)
|
|
319
|
+
return OpenAICompatClient(
|
|
320
|
+
base_url=_resolve_base_url(cfg=cfg, profile=profile),
|
|
321
|
+
api_key=api_key,
|
|
322
|
+
model=model_name,
|
|
323
|
+
timeout_s=resolve_llm_timeout_s(cfg),
|
|
324
|
+
temperature=temperature,
|
|
325
|
+
prompt_cache_key=resolve_prompt_cache_key(cfg),
|
|
326
|
+
prompt_cache_retention=resolve_prompt_cache_retention(cfg),
|
|
327
|
+
extra_headers=profile.extra_headers,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _review_markdown(
|
|
332
|
+
*,
|
|
333
|
+
task: dict[str, Any],
|
|
334
|
+
context: dict[str, Any],
|
|
335
|
+
review: dict[str, Any] | None,
|
|
336
|
+
skipped_reason: str | None,
|
|
337
|
+
request_retry_count: int = 0,
|
|
338
|
+
request_retry_state: str = _REQUEST_RETRY_STATE_NONE,
|
|
339
|
+
) -> str:
|
|
340
|
+
task_id = str(task.get("id") or "")
|
|
341
|
+
title = str(task.get("title") or "")
|
|
342
|
+
lines: list[str] = [
|
|
343
|
+
f"# Merge Conflict Review: {task_id}",
|
|
344
|
+
"",
|
|
345
|
+
f"- Task Title: {title}",
|
|
346
|
+
f"- Generated At: {now_iso()}",
|
|
347
|
+
]
|
|
348
|
+
retry_line = _request_retry_line(
|
|
349
|
+
retry_count=request_retry_count,
|
|
350
|
+
retry_state=request_retry_state,
|
|
351
|
+
)
|
|
352
|
+
if retry_line is not None:
|
|
353
|
+
lines.append(retry_line)
|
|
354
|
+
lines.append("")
|
|
355
|
+
|
|
356
|
+
unmerged_files = context.get("unmerged_files")
|
|
357
|
+
if isinstance(unmerged_files, list) and unmerged_files:
|
|
358
|
+
lines.append("## Unmerged Files")
|
|
359
|
+
lines.append("")
|
|
360
|
+
for item in unmerged_files:
|
|
361
|
+
lines.append(f"- `{item}`")
|
|
362
|
+
lines.append("")
|
|
363
|
+
|
|
364
|
+
if skipped_reason:
|
|
365
|
+
lines.extend(
|
|
366
|
+
[
|
|
367
|
+
"## Review Status",
|
|
368
|
+
"",
|
|
369
|
+
f"LLM conflict review skipped: {skipped_reason}",
|
|
370
|
+
"",
|
|
371
|
+
"## Suggested Next Steps",
|
|
372
|
+
"",
|
|
373
|
+
"- Resolve conflicts manually in listed files.",
|
|
374
|
+
"- Commit the resolution on the task branch.",
|
|
375
|
+
"- Retry merge after resolution.",
|
|
376
|
+
]
|
|
377
|
+
)
|
|
378
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
379
|
+
|
|
380
|
+
assert review is not None
|
|
381
|
+
lines.extend(
|
|
382
|
+
[
|
|
383
|
+
"## Summary",
|
|
384
|
+
"",
|
|
385
|
+
review["summary"],
|
|
386
|
+
"",
|
|
387
|
+
"## Root Cause",
|
|
388
|
+
"",
|
|
389
|
+
review["root_cause"],
|
|
390
|
+
"",
|
|
391
|
+
"## Recommended Strategy",
|
|
392
|
+
"",
|
|
393
|
+
review["recommended_strategy"],
|
|
394
|
+
"",
|
|
395
|
+
f"- Confidence: {review['confidence']}",
|
|
396
|
+
"",
|
|
397
|
+
"## Per-file Resolution",
|
|
398
|
+
"",
|
|
399
|
+
]
|
|
400
|
+
)
|
|
401
|
+
if review["per_file"]:
|
|
402
|
+
for entry in review["per_file"]:
|
|
403
|
+
lines.append(
|
|
404
|
+
f"- `{entry['path']}` -> {entry['recommended_resolution']} "
|
|
405
|
+
f"(risk={entry['risk']}): {entry['notes']}"
|
|
406
|
+
)
|
|
407
|
+
else:
|
|
408
|
+
lines.append("- (none)")
|
|
409
|
+
|
|
410
|
+
lines.extend(["", "## Next Steps", ""])
|
|
411
|
+
if review["next_steps"]:
|
|
412
|
+
for step in review["next_steps"]:
|
|
413
|
+
lines.append(f"- {step}")
|
|
414
|
+
else:
|
|
415
|
+
lines.append("- Resolve conflicts manually and retry merge.")
|
|
416
|
+
|
|
417
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def list_unmerged_files(root: Path) -> list[str]:
|
|
421
|
+
cp = _run_git(root, ["diff", "--name-only", "--diff-filter=U"])
|
|
422
|
+
if cp.returncode != 0:
|
|
423
|
+
return []
|
|
424
|
+
return [line.strip() for line in cp.stdout.splitlines() if line.strip()]
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def read_stage_blob(root: Path, stage: int, path: str) -> str:
|
|
428
|
+
if stage not in {1, 2, 3}:
|
|
429
|
+
raise ValueError("stage must be one of: 1, 2, 3")
|
|
430
|
+
cp = _run_git(root, ["show", f":{stage}:{path}"])
|
|
431
|
+
if cp.returncode != 0:
|
|
432
|
+
return ""
|
|
433
|
+
return cp.stdout
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def capture_merge_conflict_context(
|
|
437
|
+
root: Path,
|
|
438
|
+
*,
|
|
439
|
+
base_branch: str,
|
|
440
|
+
task_branch: str,
|
|
441
|
+
merge_error: str,
|
|
442
|
+
) -> dict[str, Any]:
|
|
443
|
+
status_cp = _run_git(root, ["status", "--porcelain=v1"])
|
|
444
|
+
git_status = status_cp.stdout if status_cp.returncode == 0 else (status_cp.stderr or "")
|
|
445
|
+
|
|
446
|
+
files = list_unmerged_files(root)
|
|
447
|
+
per_file: list[dict[str, str]] = []
|
|
448
|
+
for path in files:
|
|
449
|
+
working_excerpt = _read_text_excerpt(root / path)
|
|
450
|
+
per_file.append(
|
|
451
|
+
{
|
|
452
|
+
"path": path,
|
|
453
|
+
"base_version_excerpt": _truncate_head_tail(read_stage_blob(root, 1, path)),
|
|
454
|
+
"ours_version_excerpt": _truncate_head_tail(read_stage_blob(root, 2, path)),
|
|
455
|
+
"theirs_version_excerpt": _truncate_head_tail(read_stage_blob(root, 3, path)),
|
|
456
|
+
"working_excerpt": working_excerpt,
|
|
457
|
+
}
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
return {
|
|
461
|
+
"base_branch": base_branch,
|
|
462
|
+
"task_branch": task_branch,
|
|
463
|
+
"merge_error": merge_error,
|
|
464
|
+
"git_status_porcelain": _truncate_head_tail(git_status, max_chars=12000),
|
|
465
|
+
"unmerged_files": files,
|
|
466
|
+
"files": per_file,
|
|
467
|
+
"captured_at": now_iso(),
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def try_abort_merge(root: Path, *, base_branch: str) -> tuple[bool, str]:
|
|
472
|
+
logs: list[str] = []
|
|
473
|
+
|
|
474
|
+
def _record(cp: subprocess.CompletedProcess[str], cmd: str) -> None:
|
|
475
|
+
out = (cp.stdout or "").strip()
|
|
476
|
+
err = (cp.stderr or "").strip()
|
|
477
|
+
logs.append(f"$ {cmd}")
|
|
478
|
+
logs.append(f"returncode={cp.returncode}")
|
|
479
|
+
if out:
|
|
480
|
+
logs.append("stdout:")
|
|
481
|
+
logs.append(out)
|
|
482
|
+
if err:
|
|
483
|
+
logs.append("stderr:")
|
|
484
|
+
logs.append(err)
|
|
485
|
+
logs.append("")
|
|
486
|
+
|
|
487
|
+
abort_cp = _run_git(root, ["merge", "--abort"])
|
|
488
|
+
_record(abort_cp, f"git -C {root} merge --abort")
|
|
489
|
+
abort_ok = abort_cp.returncode == 0
|
|
490
|
+
|
|
491
|
+
reset_ok = False
|
|
492
|
+
if not abort_ok:
|
|
493
|
+
reset_cp = _run_git(root, ["reset", "--hard"])
|
|
494
|
+
_record(reset_cp, f"git -C {root} reset --hard")
|
|
495
|
+
reset_ok = reset_cp.returncode == 0
|
|
496
|
+
|
|
497
|
+
checkout_cp = _run_git(root, ["checkout", base_branch])
|
|
498
|
+
_record(checkout_cp, f"git -C {root} checkout {base_branch}")
|
|
499
|
+
checkout_ok = checkout_cp.returncode == 0
|
|
500
|
+
|
|
501
|
+
ok = (abort_ok or reset_ok) and checkout_ok
|
|
502
|
+
return ok, "\n".join(logs).rstrip() + "\n"
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def review_merge_conflict(
|
|
506
|
+
*,
|
|
507
|
+
paths: RunPaths,
|
|
508
|
+
task: dict[str, Any],
|
|
509
|
+
cfg: AppConfig,
|
|
510
|
+
api_key_override: str | None,
|
|
511
|
+
context: dict[str, Any],
|
|
512
|
+
plan: dict[str, Any] | None = None,
|
|
513
|
+
) -> ConflictReviewOutcome:
|
|
514
|
+
task_id = str(task.get("id") or "").strip() or "(unknown)"
|
|
515
|
+
|
|
516
|
+
try:
|
|
517
|
+
api_key = resolve_model_access_api_key(
|
|
518
|
+
cfg,
|
|
519
|
+
override=api_key_override,
|
|
520
|
+
legacy_resolver=get_api_key,
|
|
521
|
+
)
|
|
522
|
+
except ConfigError as exc:
|
|
523
|
+
reason = str(exc)
|
|
524
|
+
review_md = _review_markdown(
|
|
525
|
+
task=task,
|
|
526
|
+
context=context,
|
|
527
|
+
review=None,
|
|
528
|
+
skipped_reason=reason,
|
|
529
|
+
request_retry_count=0,
|
|
530
|
+
request_retry_state=_REQUEST_RETRY_STATE_NONE,
|
|
531
|
+
)
|
|
532
|
+
return ConflictReviewOutcome(
|
|
533
|
+
review_json=None,
|
|
534
|
+
review_markdown=review_md,
|
|
535
|
+
skipped_reason=reason,
|
|
536
|
+
request_retry_count=0,
|
|
537
|
+
request_retry_state=_REQUEST_RETRY_STATE_NONE,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
prompt_payload = {
|
|
541
|
+
"task": {
|
|
542
|
+
"id": task.get("id", ""),
|
|
543
|
+
"title": task.get("title", ""),
|
|
544
|
+
"description": task.get("description", ""),
|
|
545
|
+
"acceptance_criteria": task.get("acceptance_criteria", []),
|
|
546
|
+
"write_scope": task.get("write_scope", []),
|
|
547
|
+
"estimated_files": task.get("estimated_files", []),
|
|
548
|
+
"branch": task.get("branch", ""),
|
|
549
|
+
},
|
|
550
|
+
"conflict_context": context,
|
|
551
|
+
}
|
|
552
|
+
schema_hint = {
|
|
553
|
+
"task_id": task_id,
|
|
554
|
+
"confidence": "high|medium|low",
|
|
555
|
+
"summary": "string",
|
|
556
|
+
"root_cause": "string",
|
|
557
|
+
"recommended_strategy": "string",
|
|
558
|
+
"per_file": [
|
|
559
|
+
{
|
|
560
|
+
"path": "src/x.py",
|
|
561
|
+
"recommended_resolution": "take_ours|take_theirs|manual_merge",
|
|
562
|
+
"notes": "string",
|
|
563
|
+
"risk": "low|medium|high",
|
|
564
|
+
}
|
|
565
|
+
],
|
|
566
|
+
"next_steps": ["string"],
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
prompt = (
|
|
570
|
+
"Analyze this git merge conflict and return STRICT JSON only.\n\n"
|
|
571
|
+
"Rubric:\n"
|
|
572
|
+
"- root cause of conflict\n"
|
|
573
|
+
"- safest resolution strategy per file\n"
|
|
574
|
+
"- risks of taking ours/theirs\n"
|
|
575
|
+
"- concrete next steps\n\n"
|
|
576
|
+
f"Input:\n{json.dumps(prompt_payload, ensure_ascii=True, indent=2)}\n\n"
|
|
577
|
+
f"Required schema:\n{json.dumps(schema_hint, ensure_ascii=True, indent=2)}\n"
|
|
578
|
+
)
|
|
579
|
+
model_name = resolve_model_for_role(
|
|
580
|
+
cfg=cfg,
|
|
581
|
+
role=ROLE_CONFLICT_REVIEW,
|
|
582
|
+
plan=plan,
|
|
583
|
+
prefer_context="forge",
|
|
584
|
+
)
|
|
585
|
+
registry = ModelRegistry(cfg=cfg, api_key=api_key)
|
|
586
|
+
try:
|
|
587
|
+
metadata_policy_result = evaluate_active_model_metadata_policy(
|
|
588
|
+
cfg=cfg,
|
|
589
|
+
registry=registry,
|
|
590
|
+
active_models=[ActiveModelRef(role=ROLE_CONFLICT_REVIEW, model_name=model_name)],
|
|
591
|
+
)
|
|
592
|
+
except ModelMetadataPolicyError as e:
|
|
593
|
+
raise ConfigError(str(e)) from e
|
|
594
|
+
for warning_message in metadata_policy_result.warning_messages:
|
|
595
|
+
warnings.warn(warning_message, stacklevel=2)
|
|
596
|
+
|
|
597
|
+
client = _make_conflict_review_llm_client(
|
|
598
|
+
cfg=cfg,
|
|
599
|
+
api_key=api_key,
|
|
600
|
+
model_name=model_name,
|
|
601
|
+
)
|
|
602
|
+
request_retry_count = 0
|
|
603
|
+
try:
|
|
604
|
+
response, request_retry_count = _request_conflict_review_response(
|
|
605
|
+
client=client,
|
|
606
|
+
messages=[
|
|
607
|
+
{
|
|
608
|
+
"role": "system",
|
|
609
|
+
"content": MERGE_CONFLICT_REVIEWER_SYSTEM_PROMPT,
|
|
610
|
+
},
|
|
611
|
+
{"role": "user", "content": prompt},
|
|
612
|
+
],
|
|
613
|
+
)
|
|
614
|
+
raw = _extract_json_object(response.content)
|
|
615
|
+
review_json = _normalize_review_json(raw, task_id=task_id)
|
|
616
|
+
review_md = _review_markdown(
|
|
617
|
+
task=task,
|
|
618
|
+
context=context,
|
|
619
|
+
review=review_json,
|
|
620
|
+
skipped_reason=None,
|
|
621
|
+
request_retry_count=request_retry_count,
|
|
622
|
+
request_retry_state=_request_retry_state_for_terminal_outcome(
|
|
623
|
+
request_retry_count=request_retry_count,
|
|
624
|
+
skipped_reason=None,
|
|
625
|
+
),
|
|
626
|
+
)
|
|
627
|
+
return ConflictReviewOutcome(
|
|
628
|
+
review_json=review_json,
|
|
629
|
+
review_markdown=review_md,
|
|
630
|
+
skipped_reason=None,
|
|
631
|
+
request_retry_count=request_retry_count,
|
|
632
|
+
request_retry_state=_request_retry_state_for_terminal_outcome(
|
|
633
|
+
request_retry_count=request_retry_count,
|
|
634
|
+
skipped_reason=None,
|
|
635
|
+
),
|
|
636
|
+
)
|
|
637
|
+
except LLMError as e:
|
|
638
|
+
request_retry_count = int(getattr(e, "conflict_review_request_retry_count", 0) or 0)
|
|
639
|
+
request_retry_exhausted = bool(
|
|
640
|
+
getattr(e, "conflict_review_request_retry_exhausted", False) or False
|
|
641
|
+
)
|
|
642
|
+
retry_word = "retry" if request_retry_count == 1 else "retries"
|
|
643
|
+
reason = f"review failed: {e}"
|
|
644
|
+
if request_retry_count > 0:
|
|
645
|
+
reason = f"review failed after {request_retry_count} transient {retry_word}: {e}"
|
|
646
|
+
retry_state = _request_retry_state_for_terminal_outcome(
|
|
647
|
+
request_retry_count=request_retry_count,
|
|
648
|
+
request_retry_exhausted=request_retry_exhausted,
|
|
649
|
+
skipped_reason=reason,
|
|
650
|
+
)
|
|
651
|
+
review_md = _review_markdown(
|
|
652
|
+
task=task,
|
|
653
|
+
context=context,
|
|
654
|
+
review=None,
|
|
655
|
+
skipped_reason=reason,
|
|
656
|
+
request_retry_count=request_retry_count,
|
|
657
|
+
request_retry_state=retry_state,
|
|
658
|
+
)
|
|
659
|
+
return ConflictReviewOutcome(
|
|
660
|
+
review_json=None,
|
|
661
|
+
review_markdown=review_md,
|
|
662
|
+
skipped_reason=reason,
|
|
663
|
+
request_retry_count=request_retry_count,
|
|
664
|
+
request_retry_state=retry_state,
|
|
665
|
+
)
|
|
666
|
+
except (ValueError, json.JSONDecodeError) as e:
|
|
667
|
+
reason = f"review failed: {e}"
|
|
668
|
+
retry_state = _request_retry_state_for_terminal_outcome(
|
|
669
|
+
request_retry_count=request_retry_count,
|
|
670
|
+
skipped_reason=reason,
|
|
671
|
+
)
|
|
672
|
+
review_md = _review_markdown(
|
|
673
|
+
task=task,
|
|
674
|
+
context=context,
|
|
675
|
+
review=None,
|
|
676
|
+
skipped_reason=reason,
|
|
677
|
+
request_retry_count=request_retry_count,
|
|
678
|
+
request_retry_state=retry_state,
|
|
679
|
+
)
|
|
680
|
+
return ConflictReviewOutcome(
|
|
681
|
+
review_json=None,
|
|
682
|
+
review_markdown=review_md,
|
|
683
|
+
skipped_reason=reason,
|
|
684
|
+
request_retry_count=request_retry_count,
|
|
685
|
+
request_retry_state=retry_state,
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def write_conflict_artifacts(
|
|
690
|
+
*,
|
|
691
|
+
paths: RunPaths,
|
|
692
|
+
task_id: str,
|
|
693
|
+
context: dict[str, Any],
|
|
694
|
+
review_json: dict[str, Any] | None,
|
|
695
|
+
review_md: str,
|
|
696
|
+
cleanup_log: str,
|
|
697
|
+
) -> ConflictArtifactPaths:
|
|
698
|
+
ensure_execution_dirs(paths)
|
|
699
|
+
conflict_root = paths.execution_dir / "conflicts"
|
|
700
|
+
conflict_root.mkdir(parents=True, exist_ok=True)
|
|
701
|
+
|
|
702
|
+
safe = safe_task_file_component(task_id)
|
|
703
|
+
out_dir = conflict_root / safe
|
|
704
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
705
|
+
|
|
706
|
+
context_json_path = out_dir / "conflict_context.json"
|
|
707
|
+
review_json_path = out_dir / "conflict_review.json" if review_json is not None else None
|
|
708
|
+
review_md_path = out_dir / "conflict_review.md"
|
|
709
|
+
cleanup_log_path = out_dir / "merge_cleanup.log"
|
|
710
|
+
|
|
711
|
+
context_json_path.write_text(
|
|
712
|
+
json.dumps(context, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
|
|
713
|
+
encoding="utf-8",
|
|
714
|
+
)
|
|
715
|
+
if review_json_path is not None:
|
|
716
|
+
review_json_path.write_text(
|
|
717
|
+
json.dumps(review_json, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
|
|
718
|
+
encoding="utf-8",
|
|
719
|
+
)
|
|
720
|
+
review_md_path.write_text(review_md.rstrip() + "\n", encoding="utf-8")
|
|
721
|
+
cleanup_log_path.write_text(cleanup_log.rstrip() + "\n", encoding="utf-8")
|
|
722
|
+
|
|
723
|
+
return ConflictArtifactPaths(
|
|
724
|
+
conflict_dir=out_dir,
|
|
725
|
+
context_json_path=context_json_path,
|
|
726
|
+
review_json_path=review_json_path,
|
|
727
|
+
review_md_path=review_md_path,
|
|
728
|
+
cleanup_log_path=cleanup_log_path,
|
|
729
|
+
)
|