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,2885 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass, field, replace
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from ..diff_paths import iter_patch_paths
|
|
10
|
+
from ..failure_category import FailureCategory, is_infra_unavailable_error
|
|
11
|
+
from ..language_policy import normalize_language_name
|
|
12
|
+
from ..runtime_kind import RuntimeKind
|
|
13
|
+
from ..tools.availability import is_tool_unavailable_result
|
|
14
|
+
from ..verification_command_analysis import (
|
|
15
|
+
analyze_verification_command,
|
|
16
|
+
is_benign_non_execution_reason,
|
|
17
|
+
)
|
|
18
|
+
from ..verify_gate import (
|
|
19
|
+
ResolvedVerifyCommands,
|
|
20
|
+
assess_verification_command_execution,
|
|
21
|
+
extract_actionable_failure_snippet,
|
|
22
|
+
extract_verification_failure_snippet,
|
|
23
|
+
is_authoritative_verify_command_selection,
|
|
24
|
+
is_toolchain_unavailable_verification_output,
|
|
25
|
+
resolve_task_aware_verify_command_selection,
|
|
26
|
+
verification_selection_payload,
|
|
27
|
+
)
|
|
28
|
+
from ..verify_gate import run_task_verification as run_task_verification
|
|
29
|
+
from .acceptance_contract import (
|
|
30
|
+
AcceptanceContract,
|
|
31
|
+
acceptance_contract_problem_payload,
|
|
32
|
+
extract_explicit_acceptance_commands,
|
|
33
|
+
record_acceptance_tool_effect,
|
|
34
|
+
)
|
|
35
|
+
from .blast_radius import (
|
|
36
|
+
MAX_SCOPE_RUNS,
|
|
37
|
+
BlastRadiusAssessment,
|
|
38
|
+
BlastRadiusPolicy,
|
|
39
|
+
BlastRadiusScope,
|
|
40
|
+
BlastRadiusStatus,
|
|
41
|
+
ScopePhase,
|
|
42
|
+
ScopeRun,
|
|
43
|
+
assess_blast_radius,
|
|
44
|
+
blast_radius_blocks_finalization,
|
|
45
|
+
build_blast_radius_nudge_line,
|
|
46
|
+
classify_scope_phase,
|
|
47
|
+
command_path_selectors,
|
|
48
|
+
)
|
|
49
|
+
from .completion_certificate import (
|
|
50
|
+
CompletionCertificateInput,
|
|
51
|
+
evaluate_completion_certificate,
|
|
52
|
+
)
|
|
53
|
+
from .completion_gate import CompletionGateControllerState
|
|
54
|
+
from .mutation_classification import classify_mutation_paths, material_mutation_paths
|
|
55
|
+
from .prompt_context import (
|
|
56
|
+
_extract_workspace_relation_paths_from_text,
|
|
57
|
+
_normalize_repo_relative_hint_path,
|
|
58
|
+
_paths_require_verification,
|
|
59
|
+
_session_repo_scan,
|
|
60
|
+
_session_task_brief_content,
|
|
61
|
+
_session_verify_command_selection,
|
|
62
|
+
_task_brief_lines_from_text,
|
|
63
|
+
_verification_commands_apply_to_paths,
|
|
64
|
+
refresh_session_environment_context_message,
|
|
65
|
+
)
|
|
66
|
+
from .regression_baseline import (
|
|
67
|
+
EMPTY_REGRESSION_DIFF,
|
|
68
|
+
BaselineRecord,
|
|
69
|
+
PostEditTestRun,
|
|
70
|
+
RegressionDiffResult,
|
|
71
|
+
TestReport,
|
|
72
|
+
aggregate_regression_results,
|
|
73
|
+
baseline_command_key,
|
|
74
|
+
classify_regression_diff,
|
|
75
|
+
command_is_test_runner,
|
|
76
|
+
parse_test_report,
|
|
77
|
+
)
|
|
78
|
+
from .reproduction_first import (
|
|
79
|
+
MAX_REPRO_ARTIFACTS,
|
|
80
|
+
MAX_REPRO_RUNS,
|
|
81
|
+
ReproAssessment,
|
|
82
|
+
ReproPhase,
|
|
83
|
+
ReproRun,
|
|
84
|
+
TaskShape,
|
|
85
|
+
assess_reproduction,
|
|
86
|
+
build_repro_artifacts_nudge_line,
|
|
87
|
+
build_repro_nudge_line,
|
|
88
|
+
classify_repro_phase,
|
|
89
|
+
match_repro_artifacts,
|
|
90
|
+
repro_blocks_finalization,
|
|
91
|
+
)
|
|
92
|
+
from .turn_contract import (
|
|
93
|
+
AdvisoryCompletion,
|
|
94
|
+
DispositionRecord,
|
|
95
|
+
Expectation,
|
|
96
|
+
ExpectationAssessment,
|
|
97
|
+
ExpectationEvidence,
|
|
98
|
+
assess_expectations,
|
|
99
|
+
match_expectation_evidence,
|
|
100
|
+
)
|
|
101
|
+
from .verification_commands import (
|
|
102
|
+
_matching_effective_verification_commands,
|
|
103
|
+
_normalize_shell_command_for_match,
|
|
104
|
+
)
|
|
105
|
+
from .verification_evidence import (
|
|
106
|
+
VerificationEvidence,
|
|
107
|
+
VerificationEvidenceCategory,
|
|
108
|
+
classify_verification_evidence,
|
|
109
|
+
command_is_qualifying_execution_evidence,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if TYPE_CHECKING:
|
|
113
|
+
from .turn_path import _OneShotRepoTurnIntent
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
_COMMAND_LIKE_MUTATION_TOOL_NAMES = {"verify_run", "shell_run"}
|
|
117
|
+
_MATERIAL_EDIT_TOOL_NAMES = {
|
|
118
|
+
"fs_write",
|
|
119
|
+
"fs_edit",
|
|
120
|
+
"git_apply_patch",
|
|
121
|
+
"fs_move",
|
|
122
|
+
"fs_copy",
|
|
123
|
+
"fs_delete",
|
|
124
|
+
"fs_mkdir",
|
|
125
|
+
"shell_service_start",
|
|
126
|
+
"workspace_preview_start",
|
|
127
|
+
}
|
|
128
|
+
_VERIFICATION_SHELL_MARKERS = (
|
|
129
|
+
"pytest",
|
|
130
|
+
"py.test",
|
|
131
|
+
"unittest",
|
|
132
|
+
"tox",
|
|
133
|
+
"nox",
|
|
134
|
+
"go test",
|
|
135
|
+
"cargo test",
|
|
136
|
+
"npm test",
|
|
137
|
+
"pnpm test",
|
|
138
|
+
"yarn test",
|
|
139
|
+
"vitest",
|
|
140
|
+
"jest",
|
|
141
|
+
"ruff check",
|
|
142
|
+
"mypy",
|
|
143
|
+
"flake8",
|
|
144
|
+
"pylint",
|
|
145
|
+
"make test",
|
|
146
|
+
"make check",
|
|
147
|
+
)
|
|
148
|
+
_TEST_EXECUTION_COMMAND_RE = re.compile(
|
|
149
|
+
r"(?:^|\s)(?:pytest|py\.test|tox|nox)(?:\s|$)|"
|
|
150
|
+
r"\b(?:python(?:3)?|py)\s+-m\s+(?:pytest|unittest)\b|"
|
|
151
|
+
r"\b(?:python(?:3)?|py)\b[^\n]*\bmanage\.py\s+test\b|"
|
|
152
|
+
r"\b(?:python(?:3)?|py)\b[^\n]*\b(?:runtests?|test_[^\s/]+|[^\s/]+_test)\.py\b|"
|
|
153
|
+
r"(?:^|\s)(?:\./)?(?:bin/)?(?:runtests?|test)(?:\s|$)|"
|
|
154
|
+
r"\b(?:go|cargo)\s+test\b|"
|
|
155
|
+
r"\b(?:npm|pnpm|yarn)\s+test\b|"
|
|
156
|
+
r"\b(?:vitest|jest|rspec|phpunit|ctest)\b|"
|
|
157
|
+
r"\b(?:mvn|mvnw|maven|gradle|gradlew|dotnet|bazel|mix)\b[^\n]*\btest\b|"
|
|
158
|
+
r"\b(?:make|just)\s+(?:test|check)\b",
|
|
159
|
+
re.IGNORECASE,
|
|
160
|
+
)
|
|
161
|
+
_TEST_SUCCESS_CLAIM_RE = re.compile(
|
|
162
|
+
r"\b(?:all\s+)?(?:\d+\s+)?tests?(?:\s+suite)?\s+"
|
|
163
|
+
r"(?:(?:is|are|was|were)\s+)?"
|
|
164
|
+
r"(?:pass(?:ed|es|ing)?|succeed(?:ed|s)?|green)\b|"
|
|
165
|
+
r"\btests?\s*:\s*[^\n]{0,120}\b(?:pass(?:ed|es|ing)?|succeed(?:ed|s)?)\b|"
|
|
166
|
+
r"\b(?:pass(?:ed|es|ing)?|green)\s+(?:all\s+)?tests?\b",
|
|
167
|
+
re.IGNORECASE,
|
|
168
|
+
)
|
|
169
|
+
_GENERIC_VERIFICATION_SUCCESS_CLAIM_RE = re.compile(
|
|
170
|
+
r"\bverified\b|"
|
|
171
|
+
r"\bverification\s+(?:passed|succeeded|completed|was\s+successful)\b|"
|
|
172
|
+
r"\b(?:validation|checks?)\s+(?:passed|succeeded)\b",
|
|
173
|
+
re.IGNORECASE,
|
|
174
|
+
)
|
|
175
|
+
_NEGATED_CLAIM_PREFIX_RE = re.compile(
|
|
176
|
+
r"(?:\b(?:not|never|no|without)\b[^.!?\n]{0,32}|"
|
|
177
|
+
r"\b(?:cannot|can't|could\s+not|couldn't|did\s+not|didn't|wasn't|isn't|"
|
|
178
|
+
r"unable\s+to|failed\s+to)(?:\s+be)?)\s*$",
|
|
179
|
+
re.IGNORECASE,
|
|
180
|
+
)
|
|
181
|
+
_SAFE_LEADING_CD_RE = re.compile(
|
|
182
|
+
r"^\s*cd(?:\s+/d)?\s+(?:\"[^\"]*\"|'[^']*'|[^\s]+)\s*&&\s*",
|
|
183
|
+
re.IGNORECASE,
|
|
184
|
+
)
|
|
185
|
+
_UNSAFE_CLAIM_EVIDENCE_SHELL_RE = re.compile(
|
|
186
|
+
r"\|\||(?<![&])\|(?![&])|;|[\r\n]|&&|(?:^|\s)&(?:\s|$)",
|
|
187
|
+
)
|
|
188
|
+
_SHELL_REDIRECTION_RE = re.compile(
|
|
189
|
+
r"\s+(?:\d*>&\d+|\d*(?:>>?|<)\s*[^\s]+)(?=\s|$)",
|
|
190
|
+
)
|
|
191
|
+
SUPPLEMENTAL_VERIFICATION_ADVISORY = (
|
|
192
|
+
"Note: every passing check so far was authored during this session. "
|
|
193
|
+
"Self-written tests verify your interpretation, not the task's. Re-read the "
|
|
194
|
+
"task's exact requirements (output path, format, names, values) and confirm "
|
|
195
|
+
"your deliverable against the spec itself before finalizing."
|
|
196
|
+
)
|
|
197
|
+
# One-shot advisory emitted at the first verification-relevant edit when no
|
|
198
|
+
# baseline exists for any known verification-contract command. Advisory only -
|
|
199
|
+
# it never blocks the edit; it teaches the baseline-first protocol so failures
|
|
200
|
+
# can later be attributed to the change vs pre-existing breakage.
|
|
201
|
+
REGRESSION_BASELINE_PRE_EDIT_ADVISORY = (
|
|
202
|
+
"Baseline advisory: this is your first change to a verifiable surface and no "
|
|
203
|
+
"pre-edit test run is recorded. To let me tell failures your change causes "
|
|
204
|
+
"apart from ones already present in the repo, run the relevant test command "
|
|
205
|
+
"(your verification command) once before further edits. Advisory only - this "
|
|
206
|
+
"does not block your edit."
|
|
207
|
+
)
|
|
208
|
+
# One-shot advisory emitted the first time a material edit lands inside a
|
|
209
|
+
# generated or vendored tree (node_modules, vendor, externals, third_party, ...).
|
|
210
|
+
# Advisory only - it never blocks the edit; legitimate vendored fixes exist, but
|
|
211
|
+
# in practice edits there are usually a mistargeted change that breaks
|
|
212
|
+
# neighboring tests wholesale.
|
|
213
|
+
VENDORED_PATH_EDIT_ADVISORY = (
|
|
214
|
+
"Scope advisory: this edit changes files under a generated or vendored tree "
|
|
215
|
+
"({paths}). Vendored/generated code is almost never where the fix belongs - "
|
|
216
|
+
"it is overwritten by upstream syncs and edits there tend to break many "
|
|
217
|
+
"unrelated tests. Prefer the first-party source module; if the vendored copy "
|
|
218
|
+
"truly is the target, keep the change minimal and run the neighboring tests "
|
|
219
|
+
"before finalizing. Advisory only - this does not block your edit."
|
|
220
|
+
)
|
|
221
|
+
# One-shot finalize-time advisory for execute turns whose completion gate is
|
|
222
|
+
# otherwise clear. Small diffs that satisfy the agent's own reproduction are the
|
|
223
|
+
# dominant shape of "almost right" outcomes: the issue usually implies more
|
|
224
|
+
# behavior (exact message wording, boundary inputs, interactions) than the first
|
|
225
|
+
# repro covers. One adversarial pass converts a measurable share of these.
|
|
226
|
+
ADVERSARIAL_FINALIZE_REVIEW_ADVISORY = (
|
|
227
|
+
"Adversarial review - one pass before you finish: re-read the original "
|
|
228
|
+
"request end to end and enumerate every behavior it implies, not just the "
|
|
229
|
+
"headline symptom: exact error/message wording, boundary and degenerate "
|
|
230
|
+
"inputs (zero, empty, None, negative), types and units, and every "
|
|
231
|
+
"interaction or API named anywhere in the report. For each implied "
|
|
232
|
+
"behavior, either point at evidence you already ran that covers it, or "
|
|
233
|
+
"extend your reproduction to cover it and run it now. Acceptance checks "
|
|
234
|
+
"usually probe edge semantics beyond the reported case. If everything is "
|
|
235
|
+
"already covered, finalize."
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _adversarial_finalize_enabled(cfg: Any | None) -> bool:
|
|
240
|
+
"""Kill-switch for the adversarial finalize review (near-miss pass).
|
|
241
|
+
|
|
242
|
+
``ALYSIS_ADVERSARIAL_FINALIZE`` (off/0/false/no/disabled) wins over the
|
|
243
|
+
config value; default is on.
|
|
244
|
+
"""
|
|
245
|
+
from ..branding import env_get
|
|
246
|
+
|
|
247
|
+
env_value = env_get("ALYSIS_ADVERSARIAL_FINALIZE")
|
|
248
|
+
if env_value is not None:
|
|
249
|
+
normalized = str(env_value).strip().lower()
|
|
250
|
+
if normalized in {"off", "0", "false", "no", "disabled"}:
|
|
251
|
+
return False
|
|
252
|
+
if normalized in {"on", "1", "true", "yes", "enabled"}:
|
|
253
|
+
return True
|
|
254
|
+
return bool(getattr(cfg, "adversarial_finalize_review", True))
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# Maximum bounded repair rounds for a post-edit execution-evidence deficit before
|
|
258
|
+
# the gate finalizes honestly-unverified rather than accepting prose in its place.
|
|
259
|
+
EVIDENCE_REPAIR_ROUND_BOUND = 2
|
|
260
|
+
# Visible marker appended to the final summary when the run finalizes without the
|
|
261
|
+
# execution evidence the ordering rule requires (fail honest, never silent).
|
|
262
|
+
HONEST_UNVERIFIED_FINALIZATION_MARKER = (
|
|
263
|
+
"\n\n---\n"
|
|
264
|
+
"⚠️ Unverified: I could not obtain a passing test execution after my last "
|
|
265
|
+
"change to the code within this run. This result is finalized as UNVERIFIED — "
|
|
266
|
+
"the change has not been confirmed by running the relevant tests."
|
|
267
|
+
)
|
|
268
|
+
# Visible marker appended when the run finalizes with regressions the change
|
|
269
|
+
# introduced that a bounded action-only repair could not resolve (fail honest).
|
|
270
|
+
# Distinct wording from the unverified marker; leads with "REGRESSIONS UNRESOLVED".
|
|
271
|
+
_REGRESSIONS_UNRESOLVED_FINALIZATION_MARKER_PREFIX = (
|
|
272
|
+
"\n\n---\n"
|
|
273
|
+
"⛔ REGRESSIONS UNRESOLVED: {ids}. These tests passed in the pre-edit baseline "
|
|
274
|
+
"of `{baseline}` and fail after my change; I could not make them pass within "
|
|
275
|
+
"this run. This result is finalized with KNOWN REGRESSIONS my change introduced."
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def build_regressions_unresolved_marker(
|
|
280
|
+
regressed_ids: list[str] | tuple[str, ...],
|
|
281
|
+
*,
|
|
282
|
+
baseline_command: str = "",
|
|
283
|
+
) -> str:
|
|
284
|
+
ids = ", ".join(str(item) for item in regressed_ids if str(item).strip())
|
|
285
|
+
baseline = str(baseline_command or "").strip() or "the baseline command"
|
|
286
|
+
return _REGRESSIONS_UNRESOLVED_FINALIZATION_MARKER_PREFIX.format(ids=ids, baseline=baseline)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# Visible marker appended when the run finalizes with test failures whose
|
|
290
|
+
# relationship to the change could not be established (no comparable baseline).
|
|
291
|
+
# Distinct wording again; leads with "UNATTRIBUTED FAILURES".
|
|
292
|
+
_UNATTRIBUTED_FAILURES_FINALIZATION_MARKER_PREFIX = (
|
|
293
|
+
"\n\n---\n"
|
|
294
|
+
"⚠️ UNATTRIBUTED FAILURES: {ids}. These tests fail after my change, but I have "
|
|
295
|
+
"no comparable pre-edit baseline for the same command to determine whether my "
|
|
296
|
+
"change caused them. This result is finalized with their cause UNATTRIBUTED — "
|
|
297
|
+
"neither confirmed pre-existing nor confirmed a regression."
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def build_unattributed_failures_marker(unattributed_ids: list[str] | tuple[str, ...]) -> str:
|
|
302
|
+
ids = ", ".join(str(item) for item in unattributed_ids if str(item).strip())
|
|
303
|
+
return _UNATTRIBUTED_FAILURES_FINALIZATION_MARKER_PREFIX.format(ids=ids)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
_COMPLETION_GATE_PROBLEM_LABELS = {
|
|
307
|
+
"empty_final_response": "empty final response",
|
|
308
|
+
"no_material_edits": "no material edits",
|
|
309
|
+
"verification_not_attempted": "verification not attempted",
|
|
310
|
+
"verification_incomplete": "verification coverage incomplete",
|
|
311
|
+
"verification_failed": "verification failing",
|
|
312
|
+
"regressions_detected": "regressions introduced",
|
|
313
|
+
"unattributed_failures": "failures not yet attributed",
|
|
314
|
+
"expectations_unaddressed": "task expectations unaddressed",
|
|
315
|
+
"repro_unconfirmed": "reported symptom not reproduced",
|
|
316
|
+
"repro_artifacts_present": "reproduction scaffolding left in the tree",
|
|
317
|
+
"blast_radius_regressions": "neighbouring tests broken by the change",
|
|
318
|
+
"blast_radius_unverified": "blast radius not measured",
|
|
319
|
+
"acceptance_criteria_unverified": "acceptance criteria unverified",
|
|
320
|
+
"acceptance_criteria_failed": "acceptance criteria failed",
|
|
321
|
+
"acceptance_evidence_insufficient": "acceptance evidence insufficient",
|
|
322
|
+
"unexpected_scope_changes": "unexpected scope changes",
|
|
323
|
+
}
|
|
324
|
+
_ONE_SHOT_COMPLETION_GATE_NUDGE_PREFIX = (
|
|
325
|
+
"Completion gate: this one-shot execution run cannot finalize yet."
|
|
326
|
+
)
|
|
327
|
+
_RUNTIME_DEFAULT_LANGUAGE = "english"
|
|
328
|
+
_RUNTIME_MESSAGE_CATALOG: dict[str, dict[str, str]] = {
|
|
329
|
+
"english": {
|
|
330
|
+
"phase_understanding_request": "Understanding your request.",
|
|
331
|
+
"phase_drafting_response": "Contacting model provider.",
|
|
332
|
+
"phase_compacted_history": "Compacted conversation history.",
|
|
333
|
+
"phase_retrying_step": "Retrying with higher temperature for this step.",
|
|
334
|
+
"phase_running_tool_steps": "Running {count} tool step(s): {names}.",
|
|
335
|
+
"phase_post_explore_bootstrap": (
|
|
336
|
+
"Detected post-explore stagnation; nudging implementation bootstrap."
|
|
337
|
+
),
|
|
338
|
+
"phase_exploration_stagnation": (
|
|
339
|
+
"Detected exploration stagnation; nudging toward implementation."
|
|
340
|
+
),
|
|
341
|
+
"phase_failed_edit_loop": "Detected failed edit loop; nudging strategy switch.",
|
|
342
|
+
"phase_continuing_one_shot": (
|
|
343
|
+
"Continuing one-shot execution after non-final progress update."
|
|
344
|
+
),
|
|
345
|
+
"phase_continuing_execution": "Continuing execution after non-final progress update.",
|
|
346
|
+
"phase_completion_gate_repair": (
|
|
347
|
+
"Completion gate detected missing execution evidence; requesting action-oriented repair."
|
|
348
|
+
),
|
|
349
|
+
"phase_optional_finalization_review": (
|
|
350
|
+
"Requirements satisfied; running an optional final review."
|
|
351
|
+
),
|
|
352
|
+
"phase_step_budget_handoff": (
|
|
353
|
+
"Step budget exhausted; preparing a concise handoff so the chat can continue."
|
|
354
|
+
),
|
|
355
|
+
"phase_writing_final_response": "Writing the final response.",
|
|
356
|
+
"one_shot_continuation_nudge": (
|
|
357
|
+
"Continue execution now. A text-only plan or progress update is incomplete "
|
|
358
|
+
"for this one-shot run. Use the next required tool action to implement or "
|
|
359
|
+
"create the requested deliverable, run an implementation-producing command, "
|
|
360
|
+
"verify only after material work exists or when the implementation already "
|
|
361
|
+
"exists, or explain a concrete evidence-backed blocker."
|
|
362
|
+
),
|
|
363
|
+
"interactive_continuation_nudge": (
|
|
364
|
+
"Continue execution now. Do not stop at a planning/progress update. "
|
|
365
|
+
"Use tools to make progress, run relevant verification, or explain a concrete blocker."
|
|
366
|
+
),
|
|
367
|
+
"one_shot_exploration_nudge": (
|
|
368
|
+
"Avoid repeated read-only exploration. Start implementing or creating the "
|
|
369
|
+
"requested deliverable now, delegate once to a suitable available subagent "
|
|
370
|
+
"if more investigation is genuinely needed, or explain a concrete "
|
|
371
|
+
"evidence-backed blocker."
|
|
372
|
+
),
|
|
373
|
+
"one_shot_post_explore_bootstrap_nudge": (
|
|
374
|
+
"A subagent already returned useful context in this one-shot turn. You now have enough "
|
|
375
|
+
"context to start implementation. Do not call the same research subagent again "
|
|
376
|
+
"in this turn. Do not use more read-only tools unless there is a concrete blocker. "
|
|
377
|
+
"Your next step must be an implementation or deliverable-creation action "
|
|
378
|
+
"(for example fs_edit, fs_write, git_apply_patch, fs_move, fs_copy, or "
|
|
379
|
+
"shell_run only when it actually performs implementation or creates the "
|
|
380
|
+
"requested deliverable) or a concrete evidence-backed blocker report. "
|
|
381
|
+
"Verification comes after material work exists."
|
|
382
|
+
),
|
|
383
|
+
"one_shot_post_explore_bootstrap_targets": ("Likely repo-root-relative targets: {joined}."),
|
|
384
|
+
"one_shot_edit_strategy_nudge": (
|
|
385
|
+
"Edit strategy is stuck. Switch approach now: re-read the target lines, then use "
|
|
386
|
+
"fs_edit replace_lines/insert_before_line/insert_after_line with expected_old when "
|
|
387
|
+
"possible, or exact ops replace_exact/insert_before_exact/insert_after_exact when "
|
|
388
|
+
"matching known text. If localized fs_edit is a poor fit, use git_apply_patch or "
|
|
389
|
+
"fs_write. Do not repeat the same failing edit call."
|
|
390
|
+
),
|
|
391
|
+
"one_shot_non_final_progress_stopped": (
|
|
392
|
+
"One-shot run stopped: model returned repeated/non-final progress text "
|
|
393
|
+
"without continuing implementation."
|
|
394
|
+
),
|
|
395
|
+
"interactive_non_final_progress_stopped": (
|
|
396
|
+
"Execution turn stopped: model returned repeated/non-final progress text "
|
|
397
|
+
"without continuing implementation."
|
|
398
|
+
),
|
|
399
|
+
"one_shot_post_explore_retry_exhausted": (
|
|
400
|
+
"One-shot run stopped: post-explore stagnation persisted after bounded "
|
|
401
|
+
"implementation-bootstrap nudges. Start implementing or creating the requested "
|
|
402
|
+
"deliverable now or report a concrete blocker."
|
|
403
|
+
),
|
|
404
|
+
"one_shot_exploration_retry_exhausted": (
|
|
405
|
+
"One-shot run stopped: exploration stagnation persisted after bounded nudges. "
|
|
406
|
+
"Start implementing or creating the requested deliverable, delegate once to a "
|
|
407
|
+
"suitable available subagent if more "
|
|
408
|
+
"investigation is genuinely needed, or report a concrete blocker."
|
|
409
|
+
),
|
|
410
|
+
"one_shot_edit_retry_exhausted": (
|
|
411
|
+
"One-shot run stopped: failed edit/write loop persisted after bounded strategy "
|
|
412
|
+
"nudges. Switch to exact-match fs_edit ops, or use git_apply_patch/fs_write, "
|
|
413
|
+
"or report a concrete blocker."
|
|
414
|
+
),
|
|
415
|
+
"one_shot_post_explore_step_budget_exhausted": (
|
|
416
|
+
"One-shot run stopped: post-explore stagnation consumed the step budget. "
|
|
417
|
+
"Start implementing or creating the requested deliverable now or report a concrete blocker."
|
|
418
|
+
),
|
|
419
|
+
"one_shot_exploration_step_budget_exhausted": (
|
|
420
|
+
"One-shot run stopped: exploration stagnation consumed the step budget. "
|
|
421
|
+
"Start implementing or creating the requested deliverable, delegate once to a "
|
|
422
|
+
"suitable available subagent if more "
|
|
423
|
+
"investigation is genuinely needed, or report a concrete blocker."
|
|
424
|
+
),
|
|
425
|
+
"one_shot_edit_step_budget_exhausted": (
|
|
426
|
+
"One-shot run stopped: failed edit/write loop consumed the step budget. "
|
|
427
|
+
"Switch to exact-match fs_edit ops, or use git_apply_patch/fs_write, "
|
|
428
|
+
"or report a concrete blocker."
|
|
429
|
+
),
|
|
430
|
+
"completion_gate_nudge_prefix": _ONE_SHOT_COMPLETION_GATE_NUDGE_PREFIX,
|
|
431
|
+
"interactive_completion_gate_nudge_prefix": (
|
|
432
|
+
"Completion gate: this interactive execution turn cannot finalize yet."
|
|
433
|
+
),
|
|
434
|
+
"max_steps_exceeded": "max_steps exceeded",
|
|
435
|
+
},
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
@dataclass
|
|
440
|
+
class TurnExecutionState:
|
|
441
|
+
execution_requested: bool
|
|
442
|
+
expected_verification_commands: set[str] = field(default_factory=set)
|
|
443
|
+
covered_verification_commands: set[str] = field(default_factory=set)
|
|
444
|
+
covered_verification_command_generations: dict[str, int] = field(default_factory=dict)
|
|
445
|
+
material_edit_count: int = 0
|
|
446
|
+
material_edit_generation: int = 0
|
|
447
|
+
material_edit_tools: set[str] = field(default_factory=set)
|
|
448
|
+
touched_repo_paths: set[str] = field(default_factory=set)
|
|
449
|
+
last_diff_review_generation: int | None = None
|
|
450
|
+
verification_attempt_count: int = 0
|
|
451
|
+
verification_tools: set[str] = field(default_factory=set)
|
|
452
|
+
last_verification_passed: bool | None = None
|
|
453
|
+
last_verification_failure_snippet: str = ""
|
|
454
|
+
last_verification_failure_category: str = ""
|
|
455
|
+
failed_verification_command_snippets: dict[str, str] = field(default_factory=dict)
|
|
456
|
+
verification_relevant_edit_generation: int = 0
|
|
457
|
+
last_successful_verification_generation: int | None = None
|
|
458
|
+
verification_evidence_counts: dict[str, int] = field(default_factory=dict)
|
|
459
|
+
latest_verification_evidence_category: str = ""
|
|
460
|
+
latest_verification_evidence_reason: str = ""
|
|
461
|
+
accepted_verification_evidence: list[dict[str, Any]] = field(default_factory=list)
|
|
462
|
+
supplemental_verification_evidence: list[dict[str, Any]] = field(default_factory=list)
|
|
463
|
+
rejected_verification_evidence: list[dict[str, Any]] = field(default_factory=list)
|
|
464
|
+
executed_verification_evidence: list[dict[str, Any]] = field(default_factory=list)
|
|
465
|
+
verification_evidence_generation: int = 0
|
|
466
|
+
# Ordering rule: the verification-relevant edit generation at the time of the
|
|
467
|
+
# most recent qualifying execution-evidence event (a real test/execution run,
|
|
468
|
+
# not a syntax-only or static check). Post-edit execution evidence exists when
|
|
469
|
+
# this equals the current verification_relevant_edit_generation. Keying on the
|
|
470
|
+
# verification-relevant generation (not every material edit) means a docs-only
|
|
471
|
+
# edit after a passing run does not re-open the requirement.
|
|
472
|
+
last_post_edit_execution_generation: int | None = None
|
|
473
|
+
completion_gate_repair_attempts: int = 0
|
|
474
|
+
completion_gate_no_material_edits_repair_attempts: int = 0
|
|
475
|
+
completion_gate_missing_verify_repair_attempts: int = 0
|
|
476
|
+
completion_gate_failed_verify_repair_attempts: int = 0
|
|
477
|
+
completion_gate_regression_repair_attempts: int = 0
|
|
478
|
+
completion_gate_unattributed_repair_attempts: int = 0
|
|
479
|
+
completion_gate_expectations_repair_attempts: int = 0
|
|
480
|
+
completion_gate_repro_repair_attempts: int = 0
|
|
481
|
+
# Baseline-first regression protocol (step 3). Baselines are parsed per-test
|
|
482
|
+
# outcomes of runs recorded before the first verification-relevant edit
|
|
483
|
+
# (generation 0), keyed by the normalized executed command. Post-edit runs
|
|
484
|
+
# are compared against the same-command baseline at the completion gate.
|
|
485
|
+
test_baselines: dict[str, BaselineRecord] = field(default_factory=dict)
|
|
486
|
+
post_edit_test_runs: list[PostEditTestRun] = field(default_factory=list)
|
|
487
|
+
agent_created_paths: set[str] = field(default_factory=set)
|
|
488
|
+
regression_baseline_pre_edit_nudge_sent: bool = False
|
|
489
|
+
# True when the most recent verification attempt executed a test-runner
|
|
490
|
+
# command (pytest/unittest). Combined with an all-pre-existing/agent-authored
|
|
491
|
+
# diff, this lets the gate clear a non-contract test failure (the sympy-12489
|
|
492
|
+
# model) without masking a failing non-test command.
|
|
493
|
+
last_verification_attempt_was_test_run: bool = False
|
|
494
|
+
latest_regression_diff: dict[str, Any] = field(default_factory=dict)
|
|
495
|
+
pending_regression_capture_events: list[dict[str, Any]] = field(default_factory=list)
|
|
496
|
+
# Turn-contract v2 (step 4). ``post_edit_run_outputs`` are bounded observed
|
|
497
|
+
# outputs of post-edit runs, the fact surface the expected-output evidence
|
|
498
|
+
# linker substring-matches contract literals against. ``recorded_*`` hold any
|
|
499
|
+
# agent-declared dispositions / advisory-completion reason (unpopulated in this
|
|
500
|
+
# release; the gate synthesizes mechanically — see turn_contract.py).
|
|
501
|
+
post_edit_run_outputs: list[dict[str, Any]] = field(default_factory=list)
|
|
502
|
+
recorded_expectation_dispositions: dict[str, DispositionRecord] = field(default_factory=dict)
|
|
503
|
+
recorded_advisory_completion: AdvisoryCompletion | None = None
|
|
504
|
+
latest_expectation_assessment: dict[str, Any] = field(default_factory=dict)
|
|
505
|
+
latest_expectation_evidence: list[dict[str, Any]] = field(default_factory=list)
|
|
506
|
+
# Reproduction-first (step 5). ``repro_task_shape`` is set once at turn start;
|
|
507
|
+
# ``repro_runs`` are the observed executions of agent-created artifacts, each
|
|
508
|
+
# already phase-classified against the artifact set known at the time. The
|
|
509
|
+
# remaining fields carry the guardrail signals the summary must surface.
|
|
510
|
+
repro_task_shape: TaskShape = TaskShape.OTHER
|
|
511
|
+
repro_runs: list[ReproRun] = field(default_factory=list)
|
|
512
|
+
repro_artifact_paths: set[str] = field(default_factory=set)
|
|
513
|
+
repro_revision_rounds: int = 0
|
|
514
|
+
repro_artifacts_edited_after_fix: set[str] = field(default_factory=set)
|
|
515
|
+
repro_surviving_artifacts: tuple[str, ...] = ()
|
|
516
|
+
repro_pre_edit_nudge_sent: bool = False
|
|
517
|
+
repro_not_reproducing_nudge_sent: bool = False
|
|
518
|
+
repro_edited_after_fix_nudge_sent: bool = False
|
|
519
|
+
latest_repro_assessment: dict[str, Any] = field(default_factory=dict)
|
|
520
|
+
pending_repro_run_events: list[dict[str, Any]] = field(default_factory=list)
|
|
521
|
+
# Blast radius (step 6). ``blast_radius_scope`` is recomputed by the turn loop
|
|
522
|
+
# as the touched-path set grows; ``blast_radius_runs`` are every parsed test run
|
|
523
|
+
# observed this turn, each already tagged with what it selected and whether it
|
|
524
|
+
# ran on the clean tree. Capture is command-agnostic on purpose: the scope is
|
|
525
|
+
# matched by coverage at assessment time, so the agent may run it any way.
|
|
526
|
+
blast_radius_scope: BlastRadiusScope = field(default_factory=BlastRadiusScope)
|
|
527
|
+
blast_radius_runs: list[ScopeRun] = field(default_factory=list)
|
|
528
|
+
blast_radius_policy: BlastRadiusPolicy = field(default_factory=BlastRadiusPolicy)
|
|
529
|
+
blast_radius_scope_advisory_sent: bool = False
|
|
530
|
+
blast_radius_shrink_rounds: int = 0
|
|
531
|
+
completion_gate_blast_radius_repair_attempts: int = 0
|
|
532
|
+
latest_blast_radius_assessment: dict[str, Any] = field(default_factory=dict)
|
|
533
|
+
pending_blast_radius_events: list[dict[str, Any]] = field(default_factory=list)
|
|
534
|
+
completion_gate_controller_state: CompletionGateControllerState = field(
|
|
535
|
+
default_factory=CompletionGateControllerState
|
|
536
|
+
)
|
|
537
|
+
acceptance_contract: AcceptanceContract | None = None
|
|
538
|
+
latest_completion_certificate: dict[str, Any] = field(default_factory=dict)
|
|
539
|
+
|
|
540
|
+
def refresh_verification_coverage(self) -> None:
|
|
541
|
+
self.covered_verification_commands = {
|
|
542
|
+
command
|
|
543
|
+
for command, generation in self.covered_verification_command_generations.items()
|
|
544
|
+
if generation == self.verification_relevant_edit_generation
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
def note_verification_relevant_edit(self) -> None:
|
|
548
|
+
self.verification_relevant_edit_generation += 1
|
|
549
|
+
self.refresh_verification_coverage()
|
|
550
|
+
|
|
551
|
+
def note_material_edit(self) -> None:
|
|
552
|
+
self.material_edit_count += 1
|
|
553
|
+
self.material_edit_generation += 1
|
|
554
|
+
|
|
555
|
+
def record_diff_review(self) -> None:
|
|
556
|
+
self.last_diff_review_generation = self.material_edit_generation
|
|
557
|
+
|
|
558
|
+
def diff_review_is_stale(self) -> bool:
|
|
559
|
+
return self.material_edit_count > 0 and (
|
|
560
|
+
self.last_diff_review_generation is None
|
|
561
|
+
or self.last_diff_review_generation < self.material_edit_generation
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
def record_verification_coverage(self, commands: set[str]) -> None:
|
|
565
|
+
if not commands:
|
|
566
|
+
return
|
|
567
|
+
for command in commands:
|
|
568
|
+
self.covered_verification_command_generations[command] = (
|
|
569
|
+
self.verification_relevant_edit_generation
|
|
570
|
+
)
|
|
571
|
+
self.failed_verification_command_snippets.pop(command, None)
|
|
572
|
+
self.last_successful_verification_generation = self.verification_relevant_edit_generation
|
|
573
|
+
self.refresh_verification_coverage()
|
|
574
|
+
|
|
575
|
+
def record_verification_failures(self, failures: dict[str, str]) -> None:
|
|
576
|
+
for command, snippet in failures.items():
|
|
577
|
+
clean_command = str(command or "").strip()
|
|
578
|
+
if not clean_command:
|
|
579
|
+
continue
|
|
580
|
+
clean_snippet = str(snippet or "").strip()
|
|
581
|
+
self.failed_verification_command_snippets[clean_command] = clean_snippet
|
|
582
|
+
|
|
583
|
+
def record_verification_evidence(
|
|
584
|
+
self,
|
|
585
|
+
evidence: VerificationEvidence,
|
|
586
|
+
*,
|
|
587
|
+
accepted: bool,
|
|
588
|
+
observed_exit_code: int | None = None,
|
|
589
|
+
observed_output: bool = False,
|
|
590
|
+
) -> None:
|
|
591
|
+
category = evidence.category.value
|
|
592
|
+
self.verification_evidence_counts[category] = (
|
|
593
|
+
self.verification_evidence_counts.get(category, 0) + 1
|
|
594
|
+
)
|
|
595
|
+
self.latest_verification_evidence_category = category
|
|
596
|
+
self.latest_verification_evidence_reason = evidence.reason
|
|
597
|
+
payload = evidence.as_payload()
|
|
598
|
+
payload["accepted"] = bool(accepted)
|
|
599
|
+
payload["generation"] = self.verification_relevant_edit_generation
|
|
600
|
+
payload["observed_exit_code"] = observed_exit_code
|
|
601
|
+
payload["observed_output"] = bool(observed_output)
|
|
602
|
+
if evidence.real_execution is True:
|
|
603
|
+
self.executed_verification_evidence.append(payload)
|
|
604
|
+
self.executed_verification_evidence[:] = self.executed_verification_evidence[-20:]
|
|
605
|
+
if accepted:
|
|
606
|
+
self.verification_evidence_generation += 1
|
|
607
|
+
self.accepted_verification_evidence.append(payload)
|
|
608
|
+
self.accepted_verification_evidence[:] = self.accepted_verification_evidence[-10:]
|
|
609
|
+
elif evidence.supplemental_only:
|
|
610
|
+
self.supplemental_verification_evidence.append(payload)
|
|
611
|
+
self.supplemental_verification_evidence[:] = self.supplemental_verification_evidence[
|
|
612
|
+
-10:
|
|
613
|
+
]
|
|
614
|
+
else:
|
|
615
|
+
self.rejected_verification_evidence.append(payload)
|
|
616
|
+
self.rejected_verification_evidence[:] = self.rejected_verification_evidence[-10:]
|
|
617
|
+
|
|
618
|
+
def record_executed_command_evidence(
|
|
619
|
+
self,
|
|
620
|
+
*,
|
|
621
|
+
normalized_command: str,
|
|
622
|
+
observed_exit_code: int,
|
|
623
|
+
observed_output: bool,
|
|
624
|
+
) -> None:
|
|
625
|
+
payload: dict[str, Any] = {
|
|
626
|
+
"evidence_category": "COMMAND_EXECUTION",
|
|
627
|
+
"normalized_command": normalized_command,
|
|
628
|
+
"matched_command": None,
|
|
629
|
+
"real_execution": True,
|
|
630
|
+
"allowed_to_satisfy_contract": False,
|
|
631
|
+
"reason": "observed_shell_verification_execution",
|
|
632
|
+
"covered_verification_commands": [],
|
|
633
|
+
"supplemental_only": False,
|
|
634
|
+
"accepted": False,
|
|
635
|
+
"generation": self.verification_relevant_edit_generation,
|
|
636
|
+
"observed_exit_code": observed_exit_code,
|
|
637
|
+
"observed_output": bool(observed_output),
|
|
638
|
+
}
|
|
639
|
+
self.executed_verification_evidence.append(payload)
|
|
640
|
+
self.executed_verification_evidence[:] = self.executed_verification_evidence[-20:]
|
|
641
|
+
|
|
642
|
+
def note_qualifying_execution_evidence(self) -> None:
|
|
643
|
+
self.last_post_edit_execution_generation = self.verification_relevant_edit_generation
|
|
644
|
+
|
|
645
|
+
def has_post_edit_execution_evidence(self) -> bool:
|
|
646
|
+
return (
|
|
647
|
+
self.material_edit_count > 0
|
|
648
|
+
and self.last_post_edit_execution_generation is not None
|
|
649
|
+
and self.last_post_edit_execution_generation
|
|
650
|
+
== self.verification_relevant_edit_generation
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
def note_agent_created_path(self, path: str) -> None:
|
|
654
|
+
cleaned = str(path or "").strip()
|
|
655
|
+
if cleaned:
|
|
656
|
+
self.agent_created_paths.add(cleaned)
|
|
657
|
+
|
|
658
|
+
def has_baseline_for_any(self, commands: list[str] | tuple[str, ...] | set[str] | None) -> bool:
|
|
659
|
+
"""True when a usable baseline exists for any of ``commands``."""
|
|
660
|
+
for command in commands or []:
|
|
661
|
+
record = self.test_baselines.get(baseline_command_key(str(command)))
|
|
662
|
+
if record is not None and record.usable:
|
|
663
|
+
return True
|
|
664
|
+
return False
|
|
665
|
+
|
|
666
|
+
def note_test_execution(
|
|
667
|
+
self,
|
|
668
|
+
*,
|
|
669
|
+
command: str,
|
|
670
|
+
report: Any,
|
|
671
|
+
timestamp: str = "",
|
|
672
|
+
) -> None:
|
|
673
|
+
"""Record a parsed test run as a baseline (gen 0) or a post-edit run.
|
|
674
|
+
|
|
675
|
+
A run recorded before any verification-relevant edit (generation 0) with
|
|
676
|
+
a usable parse is a baseline for its normalized command; a later run is a
|
|
677
|
+
post-edit run. Unparseable pre-edit output is noted for telemetry but can
|
|
678
|
+
never serve as a baseline.
|
|
679
|
+
"""
|
|
680
|
+
command_key = baseline_command_key(command)
|
|
681
|
+
if not command_key:
|
|
682
|
+
return
|
|
683
|
+
generation = self.verification_relevant_edit_generation
|
|
684
|
+
if generation == 0:
|
|
685
|
+
if report.usable_as_baseline:
|
|
686
|
+
record = BaselineRecord(
|
|
687
|
+
command=str(command),
|
|
688
|
+
command_key=command_key,
|
|
689
|
+
report=report,
|
|
690
|
+
edit_generation=0,
|
|
691
|
+
timestamp=timestamp,
|
|
692
|
+
)
|
|
693
|
+
self.test_baselines[command_key] = record
|
|
694
|
+
self.pending_regression_capture_events.append(
|
|
695
|
+
{
|
|
696
|
+
"kind": "baseline",
|
|
697
|
+
"command": str(command),
|
|
698
|
+
"command_key": command_key,
|
|
699
|
+
"edit_generation": 0,
|
|
700
|
+
"report": report.as_payload(),
|
|
701
|
+
}
|
|
702
|
+
)
|
|
703
|
+
else:
|
|
704
|
+
self.pending_regression_capture_events.append(
|
|
705
|
+
{
|
|
706
|
+
"kind": "baseline_unusable",
|
|
707
|
+
"command": str(command),
|
|
708
|
+
"command_key": command_key,
|
|
709
|
+
"edit_generation": 0,
|
|
710
|
+
"report": report.as_payload(),
|
|
711
|
+
}
|
|
712
|
+
)
|
|
713
|
+
return
|
|
714
|
+
run = PostEditTestRun(
|
|
715
|
+
command=str(command),
|
|
716
|
+
command_key=command_key,
|
|
717
|
+
report=report,
|
|
718
|
+
generation=generation,
|
|
719
|
+
)
|
|
720
|
+
self.post_edit_test_runs.append(run)
|
|
721
|
+
self.post_edit_test_runs[:] = self.post_edit_test_runs[-40:]
|
|
722
|
+
self.pending_regression_capture_events.append(
|
|
723
|
+
{
|
|
724
|
+
"kind": "post_edit",
|
|
725
|
+
"command": str(command),
|
|
726
|
+
"command_key": command_key,
|
|
727
|
+
"generation": generation,
|
|
728
|
+
"report": report.as_payload(),
|
|
729
|
+
}
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
def current_post_edit_test_runs(self) -> list[PostEditTestRun]:
|
|
733
|
+
"""Post-edit runs recorded after the last verification-relevant edit."""
|
|
734
|
+
return [
|
|
735
|
+
run
|
|
736
|
+
for run in self.post_edit_test_runs
|
|
737
|
+
if run.generation == self.verification_relevant_edit_generation
|
|
738
|
+
]
|
|
739
|
+
|
|
740
|
+
def compute_regression_diff(self, *, enabled: bool) -> RegressionDiffResult:
|
|
741
|
+
"""Aggregate the same-command diff over current post-edit runs.
|
|
742
|
+
|
|
743
|
+
Pure attribution: each current-generation post-edit run is compared only
|
|
744
|
+
against a baseline of the same normalized command. With ``enabled`` off,
|
|
745
|
+
returns the empty diff (legacy gate policy).
|
|
746
|
+
"""
|
|
747
|
+
if not enabled:
|
|
748
|
+
self.latest_regression_diff = {}
|
|
749
|
+
return EMPTY_REGRESSION_DIFF
|
|
750
|
+
results = [
|
|
751
|
+
classify_regression_diff(
|
|
752
|
+
post_report=run.report,
|
|
753
|
+
baseline=self.test_baselines.get(run.command_key),
|
|
754
|
+
agent_created_paths=self.agent_created_paths,
|
|
755
|
+
)
|
|
756
|
+
for run in self.current_post_edit_test_runs()
|
|
757
|
+
]
|
|
758
|
+
aggregate = aggregate_regression_results(results)
|
|
759
|
+
self.latest_regression_diff = aggregate.as_payload()
|
|
760
|
+
return aggregate
|
|
761
|
+
|
|
762
|
+
def note_post_edit_run_output(self, *, command: str, output: str, generation: int) -> None:
|
|
763
|
+
"""Record a bounded post-edit run output for expectation evidence linking.
|
|
764
|
+
|
|
765
|
+
Only runs after a verification-relevant edit (generation > 0) are captured;
|
|
766
|
+
each output is bounded and the buffer is capped, so evidence matching stays
|
|
767
|
+
cheap and never balloons a long turn's state.
|
|
768
|
+
"""
|
|
769
|
+
text = str(output or "")
|
|
770
|
+
if not text:
|
|
771
|
+
return
|
|
772
|
+
self.post_edit_run_outputs.append(
|
|
773
|
+
{
|
|
774
|
+
"normalized_command": _normalize_shell_command_for_match(str(command or "")),
|
|
775
|
+
"output": text[:8000],
|
|
776
|
+
"generation": int(generation),
|
|
777
|
+
}
|
|
778
|
+
)
|
|
779
|
+
self.post_edit_run_outputs[:] = self.post_edit_run_outputs[-30:]
|
|
780
|
+
|
|
781
|
+
def current_expectation_evidence(
|
|
782
|
+
self, expectations: list[Expectation]
|
|
783
|
+
) -> list[ExpectationEvidence]:
|
|
784
|
+
"""Link expected-output literals to post-edit runs at the current generation."""
|
|
785
|
+
generation = self.verification_relevant_edit_generation
|
|
786
|
+
runs = [
|
|
787
|
+
run
|
|
788
|
+
for run in self.post_edit_run_outputs
|
|
789
|
+
if int(run.get("generation") or 0) >= generation
|
|
790
|
+
]
|
|
791
|
+
return match_expectation_evidence(expectations, runs)
|
|
792
|
+
|
|
793
|
+
def note_repro_run(
|
|
794
|
+
self,
|
|
795
|
+
*,
|
|
796
|
+
command: str,
|
|
797
|
+
artifact_paths: tuple[str, ...],
|
|
798
|
+
exit_code: int | None,
|
|
799
|
+
passed: bool,
|
|
800
|
+
) -> None:
|
|
801
|
+
"""Record one observed execution of an agent-created artifact.
|
|
802
|
+
|
|
803
|
+
The phase is resolved *at record time* against the paths the agent created
|
|
804
|
+
this turn, so a run is pre-fix exactly when no pre-existing repo path has
|
|
805
|
+
been modified yet. Writing the repro is itself a material edit, which is
|
|
806
|
+
why the edit generation cannot decide this.
|
|
807
|
+
"""
|
|
808
|
+
if not artifact_paths:
|
|
809
|
+
return
|
|
810
|
+
phase, product_paths = classify_repro_phase(
|
|
811
|
+
touched_repo_paths=self.touched_repo_paths,
|
|
812
|
+
created_paths=self.agent_created_paths,
|
|
813
|
+
)
|
|
814
|
+
self.repro_artifact_paths.update(artifact_paths)
|
|
815
|
+
if len(self.repro_artifact_paths) > MAX_REPRO_ARTIFACTS:
|
|
816
|
+
self.repro_artifact_paths = set(sorted(self.repro_artifact_paths)[:MAX_REPRO_ARTIFACTS])
|
|
817
|
+
run = ReproRun(
|
|
818
|
+
command=str(command or ""),
|
|
819
|
+
artifact_paths=tuple(artifact_paths),
|
|
820
|
+
phase=phase,
|
|
821
|
+
passed=bool(passed),
|
|
822
|
+
exit_code=exit_code,
|
|
823
|
+
product_paths=product_paths,
|
|
824
|
+
)
|
|
825
|
+
self.repro_runs.append(run)
|
|
826
|
+
self.repro_runs[:] = self.repro_runs[-MAX_REPRO_RUNS:]
|
|
827
|
+
self.pending_repro_run_events.append(run.as_payload())
|
|
828
|
+
|
|
829
|
+
def note_blast_radius_run(
|
|
830
|
+
self,
|
|
831
|
+
*,
|
|
832
|
+
command: str,
|
|
833
|
+
report: Any,
|
|
834
|
+
duration_seconds: float | None = None,
|
|
835
|
+
) -> None:
|
|
836
|
+
"""Record one observed test run for the blast-radius diff (step 6).
|
|
837
|
+
|
|
838
|
+
The phase is resolved *at record time* from the edits recorded so far, so a
|
|
839
|
+
run counts as a clean-tree baseline exactly when no pre-existing repo path
|
|
840
|
+
had been modified when it ran. A run made afterwards is never graced into a
|
|
841
|
+
baseline: by then the agent may already have finished its fix, and crediting
|
|
842
|
+
it would mask the very breakage this step exists to catch.
|
|
843
|
+
"""
|
|
844
|
+
cleaned = str(command or "").strip()
|
|
845
|
+
if not cleaned:
|
|
846
|
+
return
|
|
847
|
+
phase = classify_scope_phase(
|
|
848
|
+
touched_repo_paths=self.touched_repo_paths,
|
|
849
|
+
created_paths=self.agent_created_paths,
|
|
850
|
+
)
|
|
851
|
+
run = ScopeRun(
|
|
852
|
+
command=cleaned,
|
|
853
|
+
selectors=command_path_selectors(cleaned),
|
|
854
|
+
phase=phase,
|
|
855
|
+
report=report,
|
|
856
|
+
duration_seconds=duration_seconds,
|
|
857
|
+
)
|
|
858
|
+
self.blast_radius_runs.append(run)
|
|
859
|
+
self.blast_radius_runs[:] = self.blast_radius_runs[-MAX_SCOPE_RUNS:]
|
|
860
|
+
self.pending_blast_radius_events.append(run.as_payload())
|
|
861
|
+
|
|
862
|
+
def has_blast_radius_baseline(self) -> bool:
|
|
863
|
+
"""True when a usable clean-tree run already covers the selected scope."""
|
|
864
|
+
paths = self.blast_radius_scope.paths
|
|
865
|
+
if not paths:
|
|
866
|
+
return False
|
|
867
|
+
return any(
|
|
868
|
+
run.phase == ScopePhase.BASELINE and run.usable and run.covers(paths)
|
|
869
|
+
for run in self.blast_radius_runs
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
def compute_blast_radius_assessment(
|
|
873
|
+
self, *, enabled: bool, turn_intent: str
|
|
874
|
+
) -> BlastRadiusAssessment:
|
|
875
|
+
"""Assess the blast-radius gate mechanically (step 6).
|
|
876
|
+
|
|
877
|
+
With the feature disabled, on a non-execute turn, or with no scope selected
|
|
878
|
+
(nothing edited yet, or no test surface near the change), returns the empty
|
|
879
|
+
(non-applicable) assessment — the gate then behaves exactly as it did before
|
|
880
|
+
this step.
|
|
881
|
+
"""
|
|
882
|
+
applicable = bool(enabled and str(turn_intent or "") == "execute")
|
|
883
|
+
assessment = assess_blast_radius(
|
|
884
|
+
scope=self.blast_radius_scope,
|
|
885
|
+
runs=self.blast_radius_runs,
|
|
886
|
+
applicable=applicable,
|
|
887
|
+
policy=self.blast_radius_policy,
|
|
888
|
+
agent_created_paths=self.agent_created_paths,
|
|
889
|
+
)
|
|
890
|
+
self.latest_blast_radius_assessment = (
|
|
891
|
+
assessment.as_payload() if assessment.applicable else {}
|
|
892
|
+
)
|
|
893
|
+
return assessment
|
|
894
|
+
|
|
895
|
+
def note_repro_revision_round(self) -> None:
|
|
896
|
+
self.repro_revision_rounds += 1
|
|
897
|
+
|
|
898
|
+
def note_repro_artifact_edited_after_fix(self, paths: set[str] | tuple[str, ...]) -> None:
|
|
899
|
+
for path in paths:
|
|
900
|
+
cleaned = str(path or "").strip()
|
|
901
|
+
if cleaned:
|
|
902
|
+
self.repro_artifacts_edited_after_fix.add(cleaned)
|
|
903
|
+
|
|
904
|
+
def repro_protocol_applicable(
|
|
905
|
+
self, *, enabled: bool, turn_intent: str, engagement_based: bool = False
|
|
906
|
+
) -> bool:
|
|
907
|
+
if not enabled or str(turn_intent or "") != "execute":
|
|
908
|
+
return False
|
|
909
|
+
if self.repro_task_shape == TaskShape.BUG_FIX:
|
|
910
|
+
return True
|
|
911
|
+
if not engagement_based:
|
|
912
|
+
return False
|
|
913
|
+
# Router-free path: no pre-turn task-shape prediction exists. The
|
|
914
|
+
# protocol binds exactly when the agent demonstrably reproduced a
|
|
915
|
+
# failure on the unpatched tree — from then on "the same repro must
|
|
916
|
+
# pass after the fix" is enforceable without interpreting language.
|
|
917
|
+
# Helper scripts that only ever passed never engage the gate.
|
|
918
|
+
return any(run.phase is ReproPhase.PRE_FIX and not run.passed for run in self.repro_runs)
|
|
919
|
+
|
|
920
|
+
def compute_repro_assessment(
|
|
921
|
+
self, *, enabled: bool, turn_intent: str, engagement_based: bool = False
|
|
922
|
+
) -> ReproAssessment:
|
|
923
|
+
"""Assess the reproduction protocol mechanically (step 5).
|
|
924
|
+
|
|
925
|
+
With the feature disabled, on a non-execute turn, or on a task that reports
|
|
926
|
+
no symptom, returns the empty (non-applicable) assessment — the gate then
|
|
927
|
+
behaves exactly as it did before this step.
|
|
928
|
+
"""
|
|
929
|
+
applicable = self.repro_protocol_applicable(
|
|
930
|
+
enabled=enabled,
|
|
931
|
+
turn_intent=turn_intent,
|
|
932
|
+
engagement_based=engagement_based,
|
|
933
|
+
)
|
|
934
|
+
if not applicable:
|
|
935
|
+
self.latest_repro_assessment = {}
|
|
936
|
+
return ReproAssessment()
|
|
937
|
+
assessment = assess_reproduction(
|
|
938
|
+
runs=self.repro_runs,
|
|
939
|
+
applicable=True,
|
|
940
|
+
artifact_paths=self.repro_artifact_paths,
|
|
941
|
+
revision_rounds=self.repro_revision_rounds,
|
|
942
|
+
edited_after_fix=sorted(self.repro_artifacts_edited_after_fix),
|
|
943
|
+
surviving_artifacts=self.repro_surviving_artifacts,
|
|
944
|
+
)
|
|
945
|
+
self.latest_repro_assessment = assessment.as_payload()
|
|
946
|
+
return assessment
|
|
947
|
+
|
|
948
|
+
def compute_expectation_assessment(
|
|
949
|
+
self, *, enabled: bool, turn_intent: str
|
|
950
|
+
) -> ExpectationAssessment:
|
|
951
|
+
"""Assess task expectations mechanically at the gate (turn-contract v2).
|
|
952
|
+
|
|
953
|
+
Confirmed = an expected-output literal observed in a post-edit run, or a
|
|
954
|
+
named locus that was edited, or an explicit recorded disposition; the rest
|
|
955
|
+
are unaddressed. With the feature disabled, on a non-execute turn, or when
|
|
956
|
+
the contract names no expectations, returns the empty assessment.
|
|
957
|
+
"""
|
|
958
|
+
contract = self.acceptance_contract
|
|
959
|
+
expectations = list(contract.expectations) if contract is not None else []
|
|
960
|
+
if not enabled or str(turn_intent or "") != "execute" or not expectations:
|
|
961
|
+
self.latest_expectation_assessment = {}
|
|
962
|
+
self.latest_expectation_evidence = []
|
|
963
|
+
return ExpectationAssessment()
|
|
964
|
+
evidence = self.current_expectation_evidence(expectations)
|
|
965
|
+
assessment = assess_expectations(
|
|
966
|
+
expectations=expectations,
|
|
967
|
+
evidence=evidence,
|
|
968
|
+
edited_loci=self.touched_repo_paths,
|
|
969
|
+
dispositions=self.recorded_expectation_dispositions,
|
|
970
|
+
)
|
|
971
|
+
self.latest_expectation_assessment = assessment.as_payload()
|
|
972
|
+
self.latest_expectation_evidence = [item.as_payload() for item in evidence]
|
|
973
|
+
return assessment
|
|
974
|
+
|
|
975
|
+
def missing_verification_commands(self) -> set[str]:
|
|
976
|
+
return self.expected_verification_commands - self.covered_verification_commands
|
|
977
|
+
|
|
978
|
+
def failed_verification_commands(self) -> set[str]:
|
|
979
|
+
return set(self.failed_verification_command_snippets) & self.expected_verification_commands
|
|
980
|
+
|
|
981
|
+
def first_failed_verification_snippet(self) -> str:
|
|
982
|
+
for command in sorted(self.failed_verification_commands()):
|
|
983
|
+
snippet = self.failed_verification_command_snippets.get(command, "")
|
|
984
|
+
if snippet:
|
|
985
|
+
return snippet
|
|
986
|
+
return ""
|
|
987
|
+
|
|
988
|
+
def verification_coverage_is_stale(self) -> bool:
|
|
989
|
+
return (
|
|
990
|
+
bool(self.expected_verification_commands)
|
|
991
|
+
and self.last_successful_verification_generation is not None
|
|
992
|
+
and self.last_successful_verification_generation
|
|
993
|
+
< self.verification_relevant_edit_generation
|
|
994
|
+
)
|
|
995
|
+
|
|
996
|
+
def repair_attempts_for_stage(self, stage: str) -> int:
|
|
997
|
+
if stage == "no_material_edits":
|
|
998
|
+
return self.completion_gate_no_material_edits_repair_attempts
|
|
999
|
+
if stage == "verification_not_attempted":
|
|
1000
|
+
return self.completion_gate_missing_verify_repair_attempts
|
|
1001
|
+
if stage == "verification_incomplete":
|
|
1002
|
+
return self.completion_gate_missing_verify_repair_attempts
|
|
1003
|
+
if stage == "verification_failed":
|
|
1004
|
+
return self.completion_gate_failed_verify_repair_attempts
|
|
1005
|
+
if stage == "regressions_detected":
|
|
1006
|
+
return self.completion_gate_regression_repair_attempts
|
|
1007
|
+
if stage == "unattributed_failures":
|
|
1008
|
+
return self.completion_gate_unattributed_repair_attempts
|
|
1009
|
+
if stage == "expectations_unaddressed":
|
|
1010
|
+
return self.completion_gate_expectations_repair_attempts
|
|
1011
|
+
if stage == "repro_unconfirmed":
|
|
1012
|
+
return self.completion_gate_repro_repair_attempts
|
|
1013
|
+
# Both blast-radius stages share one repair budget: they are two faces of the
|
|
1014
|
+
# same protocol, and a run that alternates between them must not get double
|
|
1015
|
+
# the rounds.
|
|
1016
|
+
if stage in {"blast_radius_regressions", "blast_radius_unverified"}:
|
|
1017
|
+
return self.completion_gate_blast_radius_repair_attempts
|
|
1018
|
+
return self.completion_gate_repair_attempts
|
|
1019
|
+
|
|
1020
|
+
def increment_repair_attempts_for_stage(self, stage: str) -> None:
|
|
1021
|
+
self.completion_gate_repair_attempts += 1
|
|
1022
|
+
if stage == "no_material_edits":
|
|
1023
|
+
self.completion_gate_no_material_edits_repair_attempts += 1
|
|
1024
|
+
elif stage == "verification_not_attempted":
|
|
1025
|
+
self.completion_gate_missing_verify_repair_attempts += 1
|
|
1026
|
+
elif stage == "verification_incomplete":
|
|
1027
|
+
self.completion_gate_missing_verify_repair_attempts += 1
|
|
1028
|
+
elif stage == "verification_failed":
|
|
1029
|
+
self.completion_gate_failed_verify_repair_attempts += 1
|
|
1030
|
+
elif stage == "regressions_detected":
|
|
1031
|
+
self.completion_gate_regression_repair_attempts += 1
|
|
1032
|
+
elif stage == "unattributed_failures":
|
|
1033
|
+
self.completion_gate_unattributed_repair_attempts += 1
|
|
1034
|
+
elif stage == "expectations_unaddressed":
|
|
1035
|
+
self.completion_gate_expectations_repair_attempts += 1
|
|
1036
|
+
elif stage == "repro_unconfirmed":
|
|
1037
|
+
self.completion_gate_repro_repair_attempts += 1
|
|
1038
|
+
elif stage in {"blast_radius_regressions", "blast_radius_unverified"}:
|
|
1039
|
+
self.completion_gate_blast_radius_repair_attempts += 1
|
|
1040
|
+
|
|
1041
|
+
def as_payload(self) -> dict[str, Any]:
|
|
1042
|
+
return {
|
|
1043
|
+
"execution_requested": self.execution_requested,
|
|
1044
|
+
"expected_verification_commands": sorted(self.expected_verification_commands),
|
|
1045
|
+
"covered_verification_commands": sorted(self.covered_verification_commands),
|
|
1046
|
+
"missing_verification_commands": sorted(self.missing_verification_commands()),
|
|
1047
|
+
"material_edit_count": self.material_edit_count,
|
|
1048
|
+
"material_edit_generation": self.material_edit_generation,
|
|
1049
|
+
"material_edit_tools": sorted(self.material_edit_tools),
|
|
1050
|
+
"touched_repo_paths": sorted(self.touched_repo_paths),
|
|
1051
|
+
"last_diff_review_generation": self.last_diff_review_generation,
|
|
1052
|
+
"diff_review_stale": self.diff_review_is_stale(),
|
|
1053
|
+
"verification_attempt_count": self.verification_attempt_count,
|
|
1054
|
+
"verification_tools": sorted(self.verification_tools),
|
|
1055
|
+
"last_verification_passed": self.last_verification_passed,
|
|
1056
|
+
"last_verification_failure_snippet": self.last_verification_failure_snippet,
|
|
1057
|
+
"last_verification_failure_category": self.last_verification_failure_category,
|
|
1058
|
+
"failed_verification_commands": sorted(self.failed_verification_commands()),
|
|
1059
|
+
"verification_relevant_edit_generation": self.verification_relevant_edit_generation,
|
|
1060
|
+
"last_successful_verification_generation": self.last_successful_verification_generation,
|
|
1061
|
+
"verification_coverage_stale": self.verification_coverage_is_stale(),
|
|
1062
|
+
"verification_evidence_counts": dict(sorted(self.verification_evidence_counts.items())),
|
|
1063
|
+
"latest_verification_evidence_category": (self.latest_verification_evidence_category),
|
|
1064
|
+
"latest_verification_evidence_reason": self.latest_verification_evidence_reason,
|
|
1065
|
+
"accepted_verification_evidence": list(self.accepted_verification_evidence),
|
|
1066
|
+
"supplemental_verification_evidence": list(self.supplemental_verification_evidence),
|
|
1067
|
+
"rejected_verification_evidence": list(self.rejected_verification_evidence),
|
|
1068
|
+
"executed_verification_evidence": list(self.executed_verification_evidence),
|
|
1069
|
+
"verification_evidence_generation": self.verification_evidence_generation,
|
|
1070
|
+
"last_post_edit_execution_generation": self.last_post_edit_execution_generation,
|
|
1071
|
+
"post_edit_execution_evidence_present": self.has_post_edit_execution_evidence(),
|
|
1072
|
+
"completion_gate_repair_attempts": self.completion_gate_repair_attempts,
|
|
1073
|
+
"completion_gate_no_material_edits_repair_attempts": self.completion_gate_no_material_edits_repair_attempts,
|
|
1074
|
+
"completion_gate_missing_verify_repair_attempts": self.completion_gate_missing_verify_repair_attempts,
|
|
1075
|
+
"completion_gate_failed_verify_repair_attempts": self.completion_gate_failed_verify_repair_attempts,
|
|
1076
|
+
"completion_gate_regression_repair_attempts": self.completion_gate_regression_repair_attempts,
|
|
1077
|
+
"completion_gate_unattributed_repair_attempts": self.completion_gate_unattributed_repair_attempts,
|
|
1078
|
+
"completion_gate_expectations_repair_attempts": self.completion_gate_expectations_repair_attempts,
|
|
1079
|
+
"completion_gate_repro_repair_attempts": self.completion_gate_repro_repair_attempts,
|
|
1080
|
+
"test_baselines": {
|
|
1081
|
+
key: record.as_payload() for key, record in sorted(self.test_baselines.items())
|
|
1082
|
+
},
|
|
1083
|
+
"post_edit_test_runs": [run.as_payload() for run in self.post_edit_test_runs],
|
|
1084
|
+
"agent_created_paths": sorted(self.agent_created_paths),
|
|
1085
|
+
"last_verification_attempt_was_test_run": (self.last_verification_attempt_was_test_run),
|
|
1086
|
+
"regression_diff": dict(self.latest_regression_diff),
|
|
1087
|
+
"expectation_assessment": dict(self.latest_expectation_assessment),
|
|
1088
|
+
"expectation_evidence": list(self.latest_expectation_evidence),
|
|
1089
|
+
"repro_task_shape": self.repro_task_shape.value,
|
|
1090
|
+
"repro_runs": [run.as_payload() for run in self.repro_runs],
|
|
1091
|
+
"repro_artifact_paths": sorted(self.repro_artifact_paths),
|
|
1092
|
+
"repro_revision_rounds": self.repro_revision_rounds,
|
|
1093
|
+
"repro_artifacts_edited_after_fix": sorted(self.repro_artifacts_edited_after_fix),
|
|
1094
|
+
"repro_surviving_artifacts": list(self.repro_surviving_artifacts),
|
|
1095
|
+
"repro_assessment": dict(self.latest_repro_assessment),
|
|
1096
|
+
"completion_gate_blast_radius_repair_attempts": (
|
|
1097
|
+
self.completion_gate_blast_radius_repair_attempts
|
|
1098
|
+
),
|
|
1099
|
+
"blast_radius_scope": self.blast_radius_scope.as_payload(),
|
|
1100
|
+
"blast_radius_runs": [run.as_payload() for run in self.blast_radius_runs],
|
|
1101
|
+
"blast_radius_assessment": dict(self.latest_blast_radius_assessment),
|
|
1102
|
+
"advisory_completion": (
|
|
1103
|
+
self.recorded_advisory_completion.as_payload()
|
|
1104
|
+
if self.recorded_advisory_completion is not None
|
|
1105
|
+
else None
|
|
1106
|
+
),
|
|
1107
|
+
"completion_gate_controller": self.completion_gate_controller_state.as_payload(),
|
|
1108
|
+
"completion_gate_last_decision_kind": self.completion_gate_controller_state.last_decision_kind,
|
|
1109
|
+
"completion_certificate": dict(self.latest_completion_certificate),
|
|
1110
|
+
**acceptance_contract_problem_payload(self.acceptance_contract),
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
def acceptance_problem_names(self) -> list[str]:
|
|
1114
|
+
if self.acceptance_contract is None:
|
|
1115
|
+
return []
|
|
1116
|
+
return self.acceptance_contract.problem_names()
|
|
1117
|
+
|
|
1118
|
+
def acceptance_requires_execution(self) -> bool:
|
|
1119
|
+
if self.acceptance_contract is None:
|
|
1120
|
+
return False
|
|
1121
|
+
return any(
|
|
1122
|
+
criterion.required_for_finalization
|
|
1123
|
+
and criterion.required
|
|
1124
|
+
and (
|
|
1125
|
+
criterion.commands
|
|
1126
|
+
or criterion.thresholds
|
|
1127
|
+
or criterion.ports
|
|
1128
|
+
or criterion.kind.value
|
|
1129
|
+
in {
|
|
1130
|
+
"explicit_command_io",
|
|
1131
|
+
"functional_api_protocol",
|
|
1132
|
+
"persistent_service",
|
|
1133
|
+
"explicit_host_user_verification_command",
|
|
1134
|
+
}
|
|
1135
|
+
)
|
|
1136
|
+
for criterion in self.acceptance_contract.criteria
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
def _successful_verification_claim_kind(final_text: str) -> str | None:
|
|
1141
|
+
text = str(final_text or "")
|
|
1142
|
+
for kind, pattern in (
|
|
1143
|
+
("tests", _TEST_SUCCESS_CLAIM_RE),
|
|
1144
|
+
("verification", _GENERIC_VERIFICATION_SUCCESS_CLAIM_RE),
|
|
1145
|
+
):
|
|
1146
|
+
for match in pattern.finditer(text):
|
|
1147
|
+
prefix = text[max(0, match.start() - 48) : match.start()]
|
|
1148
|
+
if _NEGATED_CLAIM_PREFIX_RE.search(prefix):
|
|
1149
|
+
continue
|
|
1150
|
+
return kind
|
|
1151
|
+
return None
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
def _fresh_executed_evidence_for_claim(
|
|
1155
|
+
state: TurnExecutionState,
|
|
1156
|
+
*,
|
|
1157
|
+
claim_kind: str,
|
|
1158
|
+
) -> list[dict[str, Any]]:
|
|
1159
|
+
required_generation = state.verification_relevant_edit_generation
|
|
1160
|
+
evidence: list[dict[str, Any]] = []
|
|
1161
|
+
for raw_item in state.executed_verification_evidence:
|
|
1162
|
+
if not isinstance(raw_item, dict):
|
|
1163
|
+
continue
|
|
1164
|
+
item = dict(raw_item)
|
|
1165
|
+
try:
|
|
1166
|
+
generation = int(item.get("generation"))
|
|
1167
|
+
except (TypeError, ValueError):
|
|
1168
|
+
continue
|
|
1169
|
+
if generation < required_generation:
|
|
1170
|
+
continue
|
|
1171
|
+
if item.get("real_execution") is not True:
|
|
1172
|
+
continue
|
|
1173
|
+
if item.get("reason") == "mutated_material_paths":
|
|
1174
|
+
continue
|
|
1175
|
+
if item.get("observed_exit_code") != 0 or item.get("observed_output") is not True:
|
|
1176
|
+
continue
|
|
1177
|
+
command = str(item.get("normalized_command") or "").strip()
|
|
1178
|
+
if not command:
|
|
1179
|
+
continue
|
|
1180
|
+
if claim_kind == "tests":
|
|
1181
|
+
analysis = analyze_verification_command(command, trusted=True)
|
|
1182
|
+
family = str(analysis.command_family or "").casefold()
|
|
1183
|
+
if "test" not in family and _TEST_EXECUTION_COMMAND_RE.search(command) is None:
|
|
1184
|
+
continue
|
|
1185
|
+
evidence.append(item)
|
|
1186
|
+
return evidence
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
def _runtime_message_locale(
|
|
1190
|
+
*,
|
|
1191
|
+
language: str = "",
|
|
1192
|
+
explicit_language_override: bool = False,
|
|
1193
|
+
) -> str:
|
|
1194
|
+
if not explicit_language_override:
|
|
1195
|
+
return _RUNTIME_DEFAULT_LANGUAGE
|
|
1196
|
+
normalized = normalize_language_name(language).casefold()
|
|
1197
|
+
if normalized in _RUNTIME_MESSAGE_CATALOG:
|
|
1198
|
+
return normalized
|
|
1199
|
+
return _RUNTIME_DEFAULT_LANGUAGE
|
|
1200
|
+
|
|
1201
|
+
|
|
1202
|
+
def _runtime_message(
|
|
1203
|
+
key: str,
|
|
1204
|
+
*,
|
|
1205
|
+
language: str = "",
|
|
1206
|
+
explicit_language_override: bool = False,
|
|
1207
|
+
**kwargs: Any,
|
|
1208
|
+
) -> str:
|
|
1209
|
+
locale = _runtime_message_locale(
|
|
1210
|
+
language=language,
|
|
1211
|
+
explicit_language_override=explicit_language_override,
|
|
1212
|
+
)
|
|
1213
|
+
template = _RUNTIME_MESSAGE_CATALOG.get(locale, {}).get(key)
|
|
1214
|
+
if template is None:
|
|
1215
|
+
template = _RUNTIME_MESSAGE_CATALOG[_RUNTIME_DEFAULT_LANGUAGE].get(key, key)
|
|
1216
|
+
try:
|
|
1217
|
+
return template.format(**kwargs)
|
|
1218
|
+
except Exception: # noqa: BLE001
|
|
1219
|
+
return template
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
def _extract_touched_repo_paths(
|
|
1223
|
+
*,
|
|
1224
|
+
root: Path,
|
|
1225
|
+
tool_name: str,
|
|
1226
|
+
arguments: dict[str, Any],
|
|
1227
|
+
result: dict[str, Any],
|
|
1228
|
+
) -> set[str]:
|
|
1229
|
+
normalized_tool = tool_name.strip().lower()
|
|
1230
|
+
raw_paths: list[str] = []
|
|
1231
|
+
|
|
1232
|
+
if normalized_tool in {"fs_write", "fs_edit", "fs_delete", "fs_mkdir"}:
|
|
1233
|
+
raw_path = result.get("path", arguments.get("path"))
|
|
1234
|
+
if isinstance(raw_path, str):
|
|
1235
|
+
raw_paths.append(raw_path)
|
|
1236
|
+
elif normalized_tool in {"fs_move", "fs_copy"}:
|
|
1237
|
+
for key in ("source_path", "destination_path"):
|
|
1238
|
+
raw_path = result.get(key, arguments.get(key))
|
|
1239
|
+
if isinstance(raw_path, str):
|
|
1240
|
+
raw_paths.append(raw_path)
|
|
1241
|
+
elif normalized_tool == "git_apply_patch":
|
|
1242
|
+
patch = str(arguments.get("patch") or "")
|
|
1243
|
+
raw_paths.extend(iter_patch_paths(patch))
|
|
1244
|
+
elif normalized_tool == "subagent_run":
|
|
1245
|
+
touched_paths = result.get(
|
|
1246
|
+
"material_touched_repo_paths",
|
|
1247
|
+
result.get("touched_repo_paths"),
|
|
1248
|
+
)
|
|
1249
|
+
if isinstance(touched_paths, list):
|
|
1250
|
+
raw_paths.extend(str(item) for item in touched_paths if isinstance(item, str))
|
|
1251
|
+
elif normalized_tool in _COMMAND_LIKE_MUTATION_TOOL_NAMES:
|
|
1252
|
+
touched_paths = result.get("touched_repo_paths")
|
|
1253
|
+
if isinstance(touched_paths, list):
|
|
1254
|
+
raw_paths.extend(str(item) for item in touched_paths if isinstance(item, str))
|
|
1255
|
+
|
|
1256
|
+
touched: set[str] = set()
|
|
1257
|
+
for raw_path in raw_paths:
|
|
1258
|
+
normalized = _normalize_repo_relative_hint_path(root=root, raw=raw_path)
|
|
1259
|
+
if normalized:
|
|
1260
|
+
touched.add(normalized)
|
|
1261
|
+
if normalized_tool in _COMMAND_LIKE_MUTATION_TOOL_NAMES:
|
|
1262
|
+
return set(material_mutation_paths(touched, root=root))
|
|
1263
|
+
return touched
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
def _verification_attempt_passed(
|
|
1267
|
+
*,
|
|
1268
|
+
tool_name: str,
|
|
1269
|
+
status: str,
|
|
1270
|
+
result: dict[str, Any],
|
|
1271
|
+
evidence: VerificationEvidence | None = None,
|
|
1272
|
+
) -> bool:
|
|
1273
|
+
if status == "failed":
|
|
1274
|
+
return False
|
|
1275
|
+
if evidence is not None and not evidence.allowed_to_satisfy_contract:
|
|
1276
|
+
return False
|
|
1277
|
+
normalized_tool = tool_name.strip().lower()
|
|
1278
|
+
touched_repo_paths = result.get("material_touched_repo_paths", result.get("touched_repo_paths"))
|
|
1279
|
+
normalized_touched = (
|
|
1280
|
+
{str(item) for item in touched_repo_paths if isinstance(item, str) and str(item).strip()}
|
|
1281
|
+
if isinstance(touched_repo_paths, list)
|
|
1282
|
+
else set()
|
|
1283
|
+
)
|
|
1284
|
+
if normalized_tool == "verify_run":
|
|
1285
|
+
if normalized_touched and _paths_require_verification(normalized_touched):
|
|
1286
|
+
return False
|
|
1287
|
+
all_passed = result.get("all_passed")
|
|
1288
|
+
if isinstance(all_passed, bool):
|
|
1289
|
+
return all_passed
|
|
1290
|
+
command_results = result.get("command_results")
|
|
1291
|
+
if isinstance(command_results, list):
|
|
1292
|
+
checks: list[bool] = []
|
|
1293
|
+
for item in command_results:
|
|
1294
|
+
if not isinstance(item, dict):
|
|
1295
|
+
checks.append(False)
|
|
1296
|
+
continue
|
|
1297
|
+
real_execution = item.get("real_execution")
|
|
1298
|
+
if real_execution is not True:
|
|
1299
|
+
checks.append(False)
|
|
1300
|
+
continue
|
|
1301
|
+
ok = item.get("ok")
|
|
1302
|
+
if isinstance(ok, bool):
|
|
1303
|
+
checks.append(ok)
|
|
1304
|
+
continue
|
|
1305
|
+
exit_code = item.get("exit_code")
|
|
1306
|
+
checks.append(isinstance(exit_code, int) and exit_code == 0)
|
|
1307
|
+
return bool(checks) and all(checks)
|
|
1308
|
+
return False
|
|
1309
|
+
if normalized_tool == "shell_run":
|
|
1310
|
+
exit_code = result.get("exit_code")
|
|
1311
|
+
if not (isinstance(exit_code, int) and exit_code == 0):
|
|
1312
|
+
return False
|
|
1313
|
+
if normalized_touched and _paths_require_verification(normalized_touched):
|
|
1314
|
+
return False
|
|
1315
|
+
output = "\n".join(
|
|
1316
|
+
[
|
|
1317
|
+
str(result.get("stdout") or "").strip(),
|
|
1318
|
+
str(result.get("stderr") or "").strip(),
|
|
1319
|
+
]
|
|
1320
|
+
).strip()
|
|
1321
|
+
assessment = assess_verification_command_execution(
|
|
1322
|
+
command=str(result.get("effective_cmd") or result.get("cmd") or ""),
|
|
1323
|
+
exit_code=exit_code,
|
|
1324
|
+
output=output,
|
|
1325
|
+
)
|
|
1326
|
+
return assessment.real_execution is True
|
|
1327
|
+
return False
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
def _verification_relevant_material_paths(paths: set[str]) -> set[str]:
|
|
1331
|
+
if not paths or not _paths_require_verification(paths):
|
|
1332
|
+
return set()
|
|
1333
|
+
return set(paths)
|
|
1334
|
+
|
|
1335
|
+
|
|
1336
|
+
def _verification_command_result_is_benign_skip(item: dict[str, Any]) -> bool:
|
|
1337
|
+
return (
|
|
1338
|
+
item.get("status") == "skipped"
|
|
1339
|
+
and item.get("ok") is True
|
|
1340
|
+
and is_benign_non_execution_reason(str(item.get("non_execution_reason") or ""))
|
|
1341
|
+
)
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
def _verification_command_result_passed(item: dict[str, Any]) -> bool:
|
|
1345
|
+
if _verification_command_result_is_benign_skip(item):
|
|
1346
|
+
return True
|
|
1347
|
+
real_execution = item.get("real_execution")
|
|
1348
|
+
if real_execution is not True:
|
|
1349
|
+
return False
|
|
1350
|
+
ok = item.get("ok")
|
|
1351
|
+
if isinstance(ok, bool):
|
|
1352
|
+
return ok
|
|
1353
|
+
exit_code = item.get("exit_code")
|
|
1354
|
+
return isinstance(exit_code, int) and exit_code == 0
|
|
1355
|
+
|
|
1356
|
+
|
|
1357
|
+
def _verification_command_result_snippet(item: dict[str, Any]) -> str:
|
|
1358
|
+
parts = [
|
|
1359
|
+
str(item.get("output_preview") or "").strip(),
|
|
1360
|
+
str(item.get("output") or "").strip(),
|
|
1361
|
+
str(item.get("stderr") or "").strip(),
|
|
1362
|
+
str(item.get("stdout") or "").strip(),
|
|
1363
|
+
]
|
|
1364
|
+
text = "\n".join(part for part in parts if part)
|
|
1365
|
+
snippet = extract_actionable_failure_snippet(text)
|
|
1366
|
+
return snippet or (text[:240].rstrip() if text else "")
|
|
1367
|
+
|
|
1368
|
+
|
|
1369
|
+
def _verification_failure_category_for_tool_result(
|
|
1370
|
+
*,
|
|
1371
|
+
tool_name: str,
|
|
1372
|
+
arguments: dict[str, Any],
|
|
1373
|
+
result: dict[str, Any],
|
|
1374
|
+
) -> str:
|
|
1375
|
+
normalized_tool = tool_name.strip().lower()
|
|
1376
|
+
if normalized_tool == "verify_run":
|
|
1377
|
+
category = str(result.get("failure_category") or "").strip()
|
|
1378
|
+
return category or FailureCategory.VERIFICATION_FAILED.value
|
|
1379
|
+
|
|
1380
|
+
if normalized_tool == "shell_run":
|
|
1381
|
+
output = "\n".join(
|
|
1382
|
+
[
|
|
1383
|
+
str(result.get("stdout") or "").strip(),
|
|
1384
|
+
str(result.get("stderr") or "").strip(),
|
|
1385
|
+
]
|
|
1386
|
+
).strip()
|
|
1387
|
+
command = str(
|
|
1388
|
+
result.get("effective_cmd") or result.get("cmd") or arguments.get("cmd") or ""
|
|
1389
|
+
)
|
|
1390
|
+
exit_code_raw = result.get("exit_code")
|
|
1391
|
+
exit_code = exit_code_raw if isinstance(exit_code_raw, int) else 1
|
|
1392
|
+
assessment = assess_verification_command_execution(
|
|
1393
|
+
command=command,
|
|
1394
|
+
exit_code=exit_code,
|
|
1395
|
+
output=output,
|
|
1396
|
+
)
|
|
1397
|
+
if (
|
|
1398
|
+
assessment.non_execution_reason == "execution_layer_failure"
|
|
1399
|
+
or is_infra_unavailable_error(output)
|
|
1400
|
+
or is_toolchain_unavailable_verification_output(output)
|
|
1401
|
+
):
|
|
1402
|
+
return FailureCategory.INFRA_UNAVAILABLE.value
|
|
1403
|
+
|
|
1404
|
+
return FailureCategory.VERIFICATION_FAILED.value
|
|
1405
|
+
|
|
1406
|
+
|
|
1407
|
+
def _record_verify_run_command_outcomes(
|
|
1408
|
+
*,
|
|
1409
|
+
state: TurnExecutionState,
|
|
1410
|
+
result: dict[str, Any],
|
|
1411
|
+
known_verification_commands: list[str] | None,
|
|
1412
|
+
) -> None:
|
|
1413
|
+
command_results = result.get("command_results")
|
|
1414
|
+
if not isinstance(command_results, list):
|
|
1415
|
+
if result.get("all_passed") is True:
|
|
1416
|
+
commands = result.get("commands")
|
|
1417
|
+
if isinstance(commands, list):
|
|
1418
|
+
covered: set[str] = set()
|
|
1419
|
+
for command in commands:
|
|
1420
|
+
covered.update(
|
|
1421
|
+
_matching_effective_verification_commands(
|
|
1422
|
+
observed_command=str(command),
|
|
1423
|
+
effective_verification_commands=known_verification_commands,
|
|
1424
|
+
)
|
|
1425
|
+
)
|
|
1426
|
+
state.record_verification_coverage(covered)
|
|
1427
|
+
return
|
|
1428
|
+
|
|
1429
|
+
covered: set[str] = set()
|
|
1430
|
+
failures: dict[str, str] = {}
|
|
1431
|
+
for item in command_results:
|
|
1432
|
+
if not isinstance(item, dict):
|
|
1433
|
+
continue
|
|
1434
|
+
matches: set[str] = set()
|
|
1435
|
+
observed_candidates = [
|
|
1436
|
+
str(item.get("command") or ""),
|
|
1437
|
+
str(item.get("effective_command") or ""),
|
|
1438
|
+
]
|
|
1439
|
+
for observed in observed_candidates:
|
|
1440
|
+
if not observed:
|
|
1441
|
+
continue
|
|
1442
|
+
matches.update(
|
|
1443
|
+
_matching_effective_verification_commands(
|
|
1444
|
+
observed_command=observed,
|
|
1445
|
+
effective_verification_commands=known_verification_commands,
|
|
1446
|
+
)
|
|
1447
|
+
)
|
|
1448
|
+
if not matches:
|
|
1449
|
+
continue
|
|
1450
|
+
if _verification_command_result_passed(item):
|
|
1451
|
+
covered.update(matches)
|
|
1452
|
+
continue
|
|
1453
|
+
snippet = _verification_command_result_snippet(item)
|
|
1454
|
+
for command in matches:
|
|
1455
|
+
failures[command] = f"{command}: {snippet}" if snippet else command
|
|
1456
|
+
|
|
1457
|
+
state.record_verification_coverage(covered)
|
|
1458
|
+
state.record_verification_failures(failures)
|
|
1459
|
+
|
|
1460
|
+
|
|
1461
|
+
def _record_shell_verification_command_outcome(
|
|
1462
|
+
*,
|
|
1463
|
+
state: TurnExecutionState,
|
|
1464
|
+
arguments: dict[str, Any],
|
|
1465
|
+
result: dict[str, Any],
|
|
1466
|
+
known_verification_commands: list[str] | None,
|
|
1467
|
+
passed: bool,
|
|
1468
|
+
evidence: VerificationEvidence | None = None,
|
|
1469
|
+
) -> None:
|
|
1470
|
+
matches = (
|
|
1471
|
+
set(evidence.covered_verification_commands)
|
|
1472
|
+
if evidence is not None and evidence.allowed_to_satisfy_contract
|
|
1473
|
+
else set()
|
|
1474
|
+
)
|
|
1475
|
+
if not matches:
|
|
1476
|
+
matches = _matching_effective_verification_commands(
|
|
1477
|
+
observed_command=str(result.get("effective_cmd") or arguments.get("cmd") or ""),
|
|
1478
|
+
effective_verification_commands=known_verification_commands,
|
|
1479
|
+
)
|
|
1480
|
+
if not matches:
|
|
1481
|
+
return
|
|
1482
|
+
if passed:
|
|
1483
|
+
state.record_verification_coverage(matches)
|
|
1484
|
+
return
|
|
1485
|
+
output = "\n".join(
|
|
1486
|
+
[
|
|
1487
|
+
str(result.get("stdout") or "").strip(),
|
|
1488
|
+
str(result.get("stderr") or "").strip(),
|
|
1489
|
+
]
|
|
1490
|
+
).strip()
|
|
1491
|
+
snippet = extract_actionable_failure_snippet(output) or output[:240].rstrip()
|
|
1492
|
+
state.record_verification_failures(
|
|
1493
|
+
{command: f"{command}: {snippet}" if snippet else command for command in matches}
|
|
1494
|
+
)
|
|
1495
|
+
|
|
1496
|
+
|
|
1497
|
+
def _verification_output_text(result: dict[str, Any]) -> str:
|
|
1498
|
+
return "\n".join(
|
|
1499
|
+
[
|
|
1500
|
+
str(result.get("stdout") or "").strip(),
|
|
1501
|
+
str(result.get("stderr") or "").strip(),
|
|
1502
|
+
str(result.get("output") or "").strip(),
|
|
1503
|
+
str(result.get("output_preview") or "").strip(),
|
|
1504
|
+
]
|
|
1505
|
+
).strip()
|
|
1506
|
+
|
|
1507
|
+
|
|
1508
|
+
def _aggregate_verification_evidence(
|
|
1509
|
+
records: list[VerificationEvidence],
|
|
1510
|
+
*,
|
|
1511
|
+
fallback_command: str = "",
|
|
1512
|
+
) -> VerificationEvidence:
|
|
1513
|
+
if not records:
|
|
1514
|
+
return VerificationEvidence(
|
|
1515
|
+
category=VerificationEvidenceCategory.NOT_VERIFICATION,
|
|
1516
|
+
normalized_command=fallback_command,
|
|
1517
|
+
reason="no_verification_evidence",
|
|
1518
|
+
)
|
|
1519
|
+
priority = {
|
|
1520
|
+
VerificationEvidenceCategory.AUTHORITATIVE: 0,
|
|
1521
|
+
VerificationEvidenceCategory.REPO_NATIVE: 1,
|
|
1522
|
+
VerificationEvidenceCategory.TASK_ACCEPTANCE: 2,
|
|
1523
|
+
VerificationEvidenceCategory.NOT_VERIFICATION: 3,
|
|
1524
|
+
}
|
|
1525
|
+
primary = sorted(records, key=lambda item: priority[item.category])[0]
|
|
1526
|
+
covered = sorted(
|
|
1527
|
+
{command for item in records for command in item.covered_verification_commands if command}
|
|
1528
|
+
)
|
|
1529
|
+
allowed = bool(records) and all(
|
|
1530
|
+
item.allowed_to_satisfy_contract
|
|
1531
|
+
for item in records
|
|
1532
|
+
if item.category != VerificationEvidenceCategory.NOT_VERIFICATION
|
|
1533
|
+
)
|
|
1534
|
+
if any(item.category == VerificationEvidenceCategory.NOT_VERIFICATION for item in records):
|
|
1535
|
+
allowed = False
|
|
1536
|
+
return VerificationEvidence(
|
|
1537
|
+
category=primary.category,
|
|
1538
|
+
normalized_command=primary.normalized_command,
|
|
1539
|
+
matched_command=primary.matched_command,
|
|
1540
|
+
real_execution=primary.real_execution,
|
|
1541
|
+
allowed_to_satisfy_contract=allowed,
|
|
1542
|
+
reason=primary.reason
|
|
1543
|
+
if allowed
|
|
1544
|
+
else next(
|
|
1545
|
+
(item.reason for item in records if not item.allowed_to_satisfy_contract),
|
|
1546
|
+
primary.reason,
|
|
1547
|
+
),
|
|
1548
|
+
covered_verification_commands=tuple(covered),
|
|
1549
|
+
supplemental_only=all(item.supplemental_only for item in records),
|
|
1550
|
+
)
|
|
1551
|
+
|
|
1552
|
+
|
|
1553
|
+
def _verification_evidence_note(
|
|
1554
|
+
evidence: VerificationEvidence,
|
|
1555
|
+
*,
|
|
1556
|
+
result: dict[str, Any] | None = None,
|
|
1557
|
+
) -> str:
|
|
1558
|
+
if evidence.category == VerificationEvidenceCategory.NOT_VERIFICATION:
|
|
1559
|
+
return ""
|
|
1560
|
+
if evidence.supplemental_only:
|
|
1561
|
+
return (
|
|
1562
|
+
"evidence origin: SELF_AUTHORED "
|
|
1563
|
+
"(supplemental - cannot independently confirm spec compliance)"
|
|
1564
|
+
)
|
|
1565
|
+
result_payload = result if isinstance(result, dict) else {}
|
|
1566
|
+
command_specs = result_payload.get("verification_command_specs")
|
|
1567
|
+
if isinstance(command_specs, list) and any(
|
|
1568
|
+
isinstance(item, dict) and item.get("provenance") == "PREEXISTING_REPO_NATIVE"
|
|
1569
|
+
for item in command_specs
|
|
1570
|
+
):
|
|
1571
|
+
return "evidence origin: PREEXISTING_REPO_NATIVE (independent)"
|
|
1572
|
+
if result_payload.get("verification_contract_type") == "repo_native":
|
|
1573
|
+
return "evidence origin: PREEXISTING_REPO_NATIVE (independent)"
|
|
1574
|
+
if evidence.category == VerificationEvidenceCategory.AUTHORITATIVE:
|
|
1575
|
+
return "evidence origin: USER_EXPLICIT (independent)"
|
|
1576
|
+
if evidence.category == VerificationEvidenceCategory.REPO_NATIVE:
|
|
1577
|
+
return "evidence origin: PREEXISTING_REPO_NATIVE (independent)"
|
|
1578
|
+
if evidence.category == VerificationEvidenceCategory.TASK_ACCEPTANCE:
|
|
1579
|
+
return "evidence origin: DIRECT_BLACK_BOX (independent)"
|
|
1580
|
+
return ""
|
|
1581
|
+
|
|
1582
|
+
|
|
1583
|
+
def _verify_run_evidence_records(
|
|
1584
|
+
*,
|
|
1585
|
+
result: dict[str, Any],
|
|
1586
|
+
known_verification_commands: list[str] | None,
|
|
1587
|
+
verification_authoritative: bool,
|
|
1588
|
+
material_touched_paths: set[str],
|
|
1589
|
+
root: Path,
|
|
1590
|
+
evidence_v2: bool = True,
|
|
1591
|
+
) -> list[VerificationEvidence]:
|
|
1592
|
+
command_results = result.get("command_results")
|
|
1593
|
+
verification_relevant_touched_paths = _verification_relevant_material_paths(
|
|
1594
|
+
material_touched_paths
|
|
1595
|
+
)
|
|
1596
|
+
records: list[VerificationEvidence] = []
|
|
1597
|
+
if isinstance(command_results, list):
|
|
1598
|
+
for item in command_results:
|
|
1599
|
+
if not isinstance(item, dict):
|
|
1600
|
+
continue
|
|
1601
|
+
command = str(item.get("command") or item.get("effective_command") or "")
|
|
1602
|
+
if not command:
|
|
1603
|
+
continue
|
|
1604
|
+
exit_code_raw = item.get("exit_code")
|
|
1605
|
+
exit_code = exit_code_raw if isinstance(exit_code_raw, int) else None
|
|
1606
|
+
record = classify_verification_evidence(
|
|
1607
|
+
command,
|
|
1608
|
+
known_verification_commands=known_verification_commands,
|
|
1609
|
+
authoritative=verification_authoritative,
|
|
1610
|
+
material_touched_paths=verification_relevant_touched_paths,
|
|
1611
|
+
exit_code=exit_code,
|
|
1612
|
+
output=_verification_output_text(item),
|
|
1613
|
+
real_execution=(
|
|
1614
|
+
item.get("real_execution")
|
|
1615
|
+
if isinstance(item.get("real_execution"), bool)
|
|
1616
|
+
or item.get("real_execution") is None
|
|
1617
|
+
else None
|
|
1618
|
+
),
|
|
1619
|
+
root=root,
|
|
1620
|
+
evidence_v2=evidence_v2,
|
|
1621
|
+
)
|
|
1622
|
+
if (
|
|
1623
|
+
_verification_command_result_is_benign_skip(item)
|
|
1624
|
+
and record.category != VerificationEvidenceCategory.NOT_VERIFICATION
|
|
1625
|
+
and record.covered_verification_commands
|
|
1626
|
+
):
|
|
1627
|
+
record = replace(
|
|
1628
|
+
record,
|
|
1629
|
+
allowed_to_satisfy_contract=True,
|
|
1630
|
+
reason=str(item.get("non_execution_reason") or "verification_skipped"),
|
|
1631
|
+
)
|
|
1632
|
+
records.append(record)
|
|
1633
|
+
return records
|
|
1634
|
+
|
|
1635
|
+
commands = result.get("commands")
|
|
1636
|
+
if isinstance(commands, list):
|
|
1637
|
+
all_passed = result.get("all_passed")
|
|
1638
|
+
exit_code = 0 if all_passed is True else 1 if all_passed is False else None
|
|
1639
|
+
for command in commands:
|
|
1640
|
+
records.append(
|
|
1641
|
+
classify_verification_evidence(
|
|
1642
|
+
str(command),
|
|
1643
|
+
known_verification_commands=known_verification_commands,
|
|
1644
|
+
authoritative=verification_authoritative,
|
|
1645
|
+
material_touched_paths=verification_relevant_touched_paths,
|
|
1646
|
+
exit_code=exit_code,
|
|
1647
|
+
output=_verification_output_text(result),
|
|
1648
|
+
root=root,
|
|
1649
|
+
evidence_v2=evidence_v2,
|
|
1650
|
+
)
|
|
1651
|
+
)
|
|
1652
|
+
return records
|
|
1653
|
+
|
|
1654
|
+
|
|
1655
|
+
def _shell_verification_evidence(
|
|
1656
|
+
*,
|
|
1657
|
+
root: Path,
|
|
1658
|
+
state: TurnExecutionState,
|
|
1659
|
+
arguments: dict[str, Any],
|
|
1660
|
+
result: dict[str, Any],
|
|
1661
|
+
known_verification_commands: list[str] | None,
|
|
1662
|
+
verification_authoritative: bool,
|
|
1663
|
+
material_touched_paths: set[str],
|
|
1664
|
+
evidence_v2: bool = True,
|
|
1665
|
+
) -> VerificationEvidence:
|
|
1666
|
+
exit_code_raw = result.get("exit_code")
|
|
1667
|
+
exit_code = exit_code_raw if isinstance(exit_code_raw, int) else None
|
|
1668
|
+
command = str(result.get("effective_cmd") or result.get("cmd") or arguments.get("cmd") or "")
|
|
1669
|
+
stage_status_raw = result.get("pipeline_stage_status")
|
|
1670
|
+
stage_status = (
|
|
1671
|
+
[int(item) for item in stage_status_raw]
|
|
1672
|
+
if isinstance(stage_status_raw, list)
|
|
1673
|
+
and all(isinstance(item, int) for item in stage_status_raw)
|
|
1674
|
+
else None
|
|
1675
|
+
)
|
|
1676
|
+
return classify_verification_evidence(
|
|
1677
|
+
command,
|
|
1678
|
+
known_verification_commands=known_verification_commands,
|
|
1679
|
+
authoritative=verification_authoritative,
|
|
1680
|
+
changed_paths=state.touched_repo_paths,
|
|
1681
|
+
material_touched_paths=_verification_relevant_material_paths(material_touched_paths),
|
|
1682
|
+
exit_code=exit_code,
|
|
1683
|
+
output=_verification_output_text(result),
|
|
1684
|
+
root=root,
|
|
1685
|
+
stage_status=stage_status,
|
|
1686
|
+
evidence_v2=evidence_v2,
|
|
1687
|
+
)
|
|
1688
|
+
|
|
1689
|
+
|
|
1690
|
+
def _verification_evidence_observation(
|
|
1691
|
+
*,
|
|
1692
|
+
tool_name: str,
|
|
1693
|
+
evidence: VerificationEvidence,
|
|
1694
|
+
result: dict[str, Any],
|
|
1695
|
+
) -> tuple[int | None, bool]:
|
|
1696
|
+
def _has_output(payload: dict[str, Any]) -> bool:
|
|
1697
|
+
if any(
|
|
1698
|
+
str(payload.get(key) or "").strip()
|
|
1699
|
+
for key in ("output", "output_preview", "stdout", "stderr")
|
|
1700
|
+
):
|
|
1701
|
+
return True
|
|
1702
|
+
output_chars = payload.get("output_chars")
|
|
1703
|
+
return isinstance(output_chars, int) and output_chars > 0
|
|
1704
|
+
|
|
1705
|
+
normalized_tool = tool_name.strip().casefold()
|
|
1706
|
+
if normalized_tool == "shell_run":
|
|
1707
|
+
exit_code = result.get("exit_code")
|
|
1708
|
+
return (
|
|
1709
|
+
exit_code if isinstance(exit_code, int) else None,
|
|
1710
|
+
_has_output(result),
|
|
1711
|
+
)
|
|
1712
|
+
|
|
1713
|
+
if normalized_tool == "verify_run":
|
|
1714
|
+
command_results = result.get("command_results")
|
|
1715
|
+
if isinstance(command_results, list):
|
|
1716
|
+
evidence_command = _normalize_shell_command_for_match(evidence.normalized_command)
|
|
1717
|
+
for raw_item in command_results:
|
|
1718
|
+
if not isinstance(raw_item, dict):
|
|
1719
|
+
continue
|
|
1720
|
+
command = str(raw_item.get("command") or raw_item.get("effective_command") or "")
|
|
1721
|
+
effective_command = str(
|
|
1722
|
+
raw_item.get("effective_command") or raw_item.get("command") or ""
|
|
1723
|
+
)
|
|
1724
|
+
normalized_candidates = {
|
|
1725
|
+
_normalize_shell_command_for_match(command),
|
|
1726
|
+
_normalize_shell_command_for_match(effective_command),
|
|
1727
|
+
}
|
|
1728
|
+
if evidence_command not in normalized_candidates:
|
|
1729
|
+
continue
|
|
1730
|
+
exit_code = raw_item.get("exit_code")
|
|
1731
|
+
return (
|
|
1732
|
+
exit_code if isinstance(exit_code, int) else None,
|
|
1733
|
+
_has_output(raw_item),
|
|
1734
|
+
)
|
|
1735
|
+
all_passed = result.get("all_passed")
|
|
1736
|
+
exit_code = 0 if all_passed is True else 1 if all_passed is False else None
|
|
1737
|
+
return (
|
|
1738
|
+
exit_code,
|
|
1739
|
+
_has_output(result),
|
|
1740
|
+
)
|
|
1741
|
+
|
|
1742
|
+
return None, False
|
|
1743
|
+
|
|
1744
|
+
|
|
1745
|
+
def _unmasked_shell_verification_command(command: str) -> str:
|
|
1746
|
+
candidate = str(command or "").strip()
|
|
1747
|
+
while match := _SAFE_LEADING_CD_RE.match(candidate):
|
|
1748
|
+
candidate = candidate[match.end() :].strip()
|
|
1749
|
+
if not candidate or _UNSAFE_CLAIM_EVIDENCE_SHELL_RE.search(candidate):
|
|
1750
|
+
return ""
|
|
1751
|
+
analysis_candidate = _SHELL_REDIRECTION_RE.sub("", candidate).strip()
|
|
1752
|
+
analysis = analyze_verification_command(analysis_candidate, trusted=True)
|
|
1753
|
+
if analysis.command_family is None:
|
|
1754
|
+
return ""
|
|
1755
|
+
return _normalize_shell_command_for_match(analysis_candidate)
|
|
1756
|
+
|
|
1757
|
+
|
|
1758
|
+
def _tool_effect_has_qualifying_execution(
|
|
1759
|
+
*,
|
|
1760
|
+
tool_name: str,
|
|
1761
|
+
evidence_records: list[VerificationEvidence],
|
|
1762
|
+
result: dict[str, Any],
|
|
1763
|
+
) -> bool:
|
|
1764
|
+
"""True when a real test/execution run (pass or fail) is observed.
|
|
1765
|
+
|
|
1766
|
+
Qualifying = a recognized test/execution program that actually executed the
|
|
1767
|
+
code — a passing run (``real_execution is True``) or a genuine failing run
|
|
1768
|
+
(a non-zero exit of a recognized test command). Excludes syntax-only and
|
|
1769
|
+
static checks (ast.parse, py_compile, mypy, ruff check) and non-executions
|
|
1770
|
+
(no-tests collected, vacuous commands). Used only for the ordering rule.
|
|
1771
|
+
"""
|
|
1772
|
+
for record in evidence_records:
|
|
1773
|
+
if record.category == VerificationEvidenceCategory.NOT_VERIFICATION:
|
|
1774
|
+
continue
|
|
1775
|
+
if record.real_execution is False:
|
|
1776
|
+
continue
|
|
1777
|
+
if not command_is_qualifying_execution_evidence(record.normalized_command or ""):
|
|
1778
|
+
continue
|
|
1779
|
+
if record.real_execution is True:
|
|
1780
|
+
return True
|
|
1781
|
+
observed_exit_code, _observed_output = _verification_evidence_observation(
|
|
1782
|
+
tool_name=tool_name,
|
|
1783
|
+
evidence=record,
|
|
1784
|
+
result=result,
|
|
1785
|
+
)
|
|
1786
|
+
if observed_exit_code is not None and observed_exit_code != 0:
|
|
1787
|
+
return True
|
|
1788
|
+
return False
|
|
1789
|
+
|
|
1790
|
+
|
|
1791
|
+
def _regression_capture_timestamp() -> str:
|
|
1792
|
+
try:
|
|
1793
|
+
return datetime.now(timezone.utc).isoformat()
|
|
1794
|
+
except Exception: # noqa: BLE001 - a telemetry timestamp must never crash a turn
|
|
1795
|
+
return ""
|
|
1796
|
+
|
|
1797
|
+
|
|
1798
|
+
def _iter_executed_test_commands(
|
|
1799
|
+
*,
|
|
1800
|
+
tool_name: str,
|
|
1801
|
+
arguments: dict[str, Any],
|
|
1802
|
+
result: dict[str, Any],
|
|
1803
|
+
) -> list[tuple[str, str]]:
|
|
1804
|
+
"""Yield ``(command, output)`` pairs for executed test-runner commands.
|
|
1805
|
+
|
|
1806
|
+
Only commands whose meaningful first stage is pytest or unittest/Django are
|
|
1807
|
+
returned — the runners the parsers understand. Other qualifying executions
|
|
1808
|
+
(validation scripts, linters) emit no per-test ids and are out of scope.
|
|
1809
|
+
"""
|
|
1810
|
+
pairs: list[tuple[str, str]] = []
|
|
1811
|
+
if tool_name == "shell_run":
|
|
1812
|
+
command = str(
|
|
1813
|
+
result.get("effective_cmd") or result.get("cmd") or arguments.get("cmd") or ""
|
|
1814
|
+
)
|
|
1815
|
+
if command and command_is_test_runner(command):
|
|
1816
|
+
pairs.append((command, _verification_output_text(result)))
|
|
1817
|
+
return pairs
|
|
1818
|
+
if tool_name == "verify_run":
|
|
1819
|
+
command_results = result.get("command_results")
|
|
1820
|
+
if isinstance(command_results, list):
|
|
1821
|
+
for item in command_results:
|
|
1822
|
+
if not isinstance(item, dict):
|
|
1823
|
+
continue
|
|
1824
|
+
command = str(item.get("command") or item.get("effective_command") or "")
|
|
1825
|
+
if command and command_is_test_runner(command):
|
|
1826
|
+
pairs.append((command, _verification_output_text(item)))
|
|
1827
|
+
return pairs
|
|
1828
|
+
|
|
1829
|
+
|
|
1830
|
+
def _iter_executed_commands_with_outcome(
|
|
1831
|
+
*,
|
|
1832
|
+
tool_name: str,
|
|
1833
|
+
arguments: dict[str, Any],
|
|
1834
|
+
status: str,
|
|
1835
|
+
result: dict[str, Any],
|
|
1836
|
+
) -> list[tuple[str, int | None]]:
|
|
1837
|
+
"""Yield ``(command, exit_code)`` for every command this tool actually ran.
|
|
1838
|
+
|
|
1839
|
+
``exit_code`` is ``None`` when the runner reported none; a failed tool status
|
|
1840
|
+
with no exit code is reported as a non-zero sentinel so a crashed run is never
|
|
1841
|
+
mistaken for a passing one.
|
|
1842
|
+
"""
|
|
1843
|
+
|
|
1844
|
+
def _exit_code(payload: dict[str, Any]) -> int | None:
|
|
1845
|
+
raw = payload.get("exit_code")
|
|
1846
|
+
if raw is None:
|
|
1847
|
+
raw = payload.get("returncode")
|
|
1848
|
+
if raw is None:
|
|
1849
|
+
return 1 if status == "failed" else None
|
|
1850
|
+
try:
|
|
1851
|
+
return int(raw)
|
|
1852
|
+
except (TypeError, ValueError):
|
|
1853
|
+
return 1 if status == "failed" else None
|
|
1854
|
+
|
|
1855
|
+
pairs: list[tuple[str, int | None]] = []
|
|
1856
|
+
if tool_name == "shell_run":
|
|
1857
|
+
command = str(
|
|
1858
|
+
result.get("effective_cmd") or result.get("cmd") or arguments.get("cmd") or ""
|
|
1859
|
+
)
|
|
1860
|
+
if command:
|
|
1861
|
+
pairs.append((command, _exit_code(result)))
|
|
1862
|
+
return pairs
|
|
1863
|
+
if tool_name == "verify_run":
|
|
1864
|
+
command_results = result.get("command_results")
|
|
1865
|
+
if isinstance(command_results, list):
|
|
1866
|
+
for item in command_results:
|
|
1867
|
+
if not isinstance(item, dict):
|
|
1868
|
+
continue
|
|
1869
|
+
command = str(item.get("command") or item.get("effective_command") or "")
|
|
1870
|
+
if command:
|
|
1871
|
+
pairs.append((command, _exit_code(item)))
|
|
1872
|
+
return pairs
|
|
1873
|
+
|
|
1874
|
+
|
|
1875
|
+
def _capture_repro_runs(
|
|
1876
|
+
*,
|
|
1877
|
+
state: TurnExecutionState,
|
|
1878
|
+
tool_name: str,
|
|
1879
|
+
arguments: dict[str, Any],
|
|
1880
|
+
status: str,
|
|
1881
|
+
result: dict[str, Any],
|
|
1882
|
+
) -> None:
|
|
1883
|
+
"""Record executions of files the agent created this turn (step 5).
|
|
1884
|
+
|
|
1885
|
+
A command "runs a reproduction" when one of its path tokens is a path the
|
|
1886
|
+
agent created this turn — a fact, not an inference about the file's purpose.
|
|
1887
|
+
A run with no resolvable exit code is not recorded at all: an unobservable
|
|
1888
|
+
outcome can neither confirm nor refute the reproduction.
|
|
1889
|
+
"""
|
|
1890
|
+
if tool_name not in _COMMAND_LIKE_MUTATION_TOOL_NAMES:
|
|
1891
|
+
return
|
|
1892
|
+
if not state.agent_created_paths:
|
|
1893
|
+
return
|
|
1894
|
+
for command, exit_code in _iter_executed_commands_with_outcome(
|
|
1895
|
+
tool_name=tool_name,
|
|
1896
|
+
arguments=arguments,
|
|
1897
|
+
status=status,
|
|
1898
|
+
result=result,
|
|
1899
|
+
):
|
|
1900
|
+
artifacts = match_repro_artifacts(command, state.agent_created_paths)
|
|
1901
|
+
if not artifacts or exit_code is None:
|
|
1902
|
+
continue
|
|
1903
|
+
state.note_repro_run(
|
|
1904
|
+
command=command,
|
|
1905
|
+
artifact_paths=artifacts,
|
|
1906
|
+
exit_code=exit_code,
|
|
1907
|
+
passed=exit_code == 0,
|
|
1908
|
+
)
|
|
1909
|
+
|
|
1910
|
+
|
|
1911
|
+
def _capture_repro_artifact_edits(
|
|
1912
|
+
*,
|
|
1913
|
+
state: TurnExecutionState,
|
|
1914
|
+
touched_paths: set[str],
|
|
1915
|
+
) -> None:
|
|
1916
|
+
"""Record edits to a recorded reproduction artifact made after a product edit.
|
|
1917
|
+
|
|
1918
|
+
Called before ``touched_repo_paths`` absorbs this edit, so the "product code
|
|
1919
|
+
already changed" test reads only prior edits.
|
|
1920
|
+
"""
|
|
1921
|
+
if not state.repro_artifact_paths or not touched_paths:
|
|
1922
|
+
return
|
|
1923
|
+
edited_artifacts = touched_paths & state.repro_artifact_paths
|
|
1924
|
+
if not edited_artifacts:
|
|
1925
|
+
return
|
|
1926
|
+
if not (state.touched_repo_paths - state.agent_created_paths):
|
|
1927
|
+
return
|
|
1928
|
+
state.note_repro_artifact_edited_after_fix(edited_artifacts)
|
|
1929
|
+
|
|
1930
|
+
|
|
1931
|
+
def _capture_regression_test_runs(
|
|
1932
|
+
*,
|
|
1933
|
+
state: TurnExecutionState,
|
|
1934
|
+
tool_name: str,
|
|
1935
|
+
arguments: dict[str, Any],
|
|
1936
|
+
result: dict[str, Any],
|
|
1937
|
+
elapsed_ms: int | None = None,
|
|
1938
|
+
) -> None:
|
|
1939
|
+
timestamp = _regression_capture_timestamp()
|
|
1940
|
+
pairs = _iter_executed_test_commands(
|
|
1941
|
+
tool_name=tool_name,
|
|
1942
|
+
arguments=arguments,
|
|
1943
|
+
result=result,
|
|
1944
|
+
)
|
|
1945
|
+
# A tool call's elapsed time covers everything it ran, so attributing it to a
|
|
1946
|
+
# single command is only honest when that call ran exactly one test command.
|
|
1947
|
+
duration_seconds: float | None = None
|
|
1948
|
+
if elapsed_ms is not None and len(pairs) == 1:
|
|
1949
|
+
try:
|
|
1950
|
+
duration_seconds = max(0.0, float(elapsed_ms) / 1000.0)
|
|
1951
|
+
except (TypeError, ValueError):
|
|
1952
|
+
duration_seconds = None
|
|
1953
|
+
for command, output in pairs:
|
|
1954
|
+
report = parse_test_report(output)
|
|
1955
|
+
if tool_name == "verify_run":
|
|
1956
|
+
report = _structured_verify_test_report(
|
|
1957
|
+
result=result,
|
|
1958
|
+
command=command,
|
|
1959
|
+
parsed_report=report,
|
|
1960
|
+
)
|
|
1961
|
+
state.note_test_execution(command=command, report=report, timestamp=timestamp)
|
|
1962
|
+
# Blast radius (step 6) reads the same parsed reports but keys them by what
|
|
1963
|
+
# each run selected rather than by command identity, so a clean whole-suite
|
|
1964
|
+
# run can baseline a scope the agent never named.
|
|
1965
|
+
state.note_blast_radius_run(
|
|
1966
|
+
command=command,
|
|
1967
|
+
report=report,
|
|
1968
|
+
duration_seconds=duration_seconds,
|
|
1969
|
+
)
|
|
1970
|
+
|
|
1971
|
+
|
|
1972
|
+
def _structured_verify_test_report(
|
|
1973
|
+
*,
|
|
1974
|
+
result: dict[str, Any],
|
|
1975
|
+
command: str,
|
|
1976
|
+
parsed_report: TestReport,
|
|
1977
|
+
) -> TestReport:
|
|
1978
|
+
"""Prefer host-recorded verify success; raw output only adds parsed detail."""
|
|
1979
|
+
command_key = baseline_command_key(command)
|
|
1980
|
+
command_results = result.get("command_results")
|
|
1981
|
+
if not isinstance(command_results, list):
|
|
1982
|
+
return parsed_report
|
|
1983
|
+
for raw_item in command_results:
|
|
1984
|
+
if not isinstance(raw_item, dict):
|
|
1985
|
+
continue
|
|
1986
|
+
observed_command = str(raw_item.get("effective_command") or raw_item.get("command") or "")
|
|
1987
|
+
if baseline_command_key(observed_command) != command_key:
|
|
1988
|
+
continue
|
|
1989
|
+
exit_code = raw_item.get("exit_code")
|
|
1990
|
+
structured_passed = raw_item.get("ok") is True or (
|
|
1991
|
+
result.get("all_passed") is True and exit_code == 0
|
|
1992
|
+
)
|
|
1993
|
+
if not structured_passed or raw_item.get("real_execution") is False:
|
|
1994
|
+
return parsed_report
|
|
1995
|
+
runner = parsed_report.runner
|
|
1996
|
+
if runner == "unknown":
|
|
1997
|
+
lowered = observed_command.casefold()
|
|
1998
|
+
runner = "pytest" if "pytest" in lowered else "unittest"
|
|
1999
|
+
return TestReport(
|
|
2000
|
+
runner=runner,
|
|
2001
|
+
passed=parsed_report.passed,
|
|
2002
|
+
failed=0,
|
|
2003
|
+
skipped=parsed_report.skipped,
|
|
2004
|
+
errors=0,
|
|
2005
|
+
counts_known=True,
|
|
2006
|
+
)
|
|
2007
|
+
return parsed_report
|
|
2008
|
+
|
|
2009
|
+
|
|
2010
|
+
def _capture_expectation_run_outputs(
|
|
2011
|
+
*,
|
|
2012
|
+
state: TurnExecutionState,
|
|
2013
|
+
tool_name: str,
|
|
2014
|
+
arguments: dict[str, Any],
|
|
2015
|
+
result: dict[str, Any],
|
|
2016
|
+
) -> None:
|
|
2017
|
+
"""Capture bounded observed outputs of post-edit runs (turn-contract v2).
|
|
2018
|
+
|
|
2019
|
+
The expected-output evidence linker substring-matches contract literals against
|
|
2020
|
+
these outputs. Capture is unconditional telemetry (never kill-switched) and
|
|
2021
|
+
only records runs after a verification-relevant edit — pre-edit output can never
|
|
2022
|
+
confirm a post-edit expectation.
|
|
2023
|
+
"""
|
|
2024
|
+
generation = state.verification_relevant_edit_generation
|
|
2025
|
+
if generation <= 0:
|
|
2026
|
+
return
|
|
2027
|
+
if tool_name == "shell_run":
|
|
2028
|
+
command = str(
|
|
2029
|
+
result.get("effective_cmd") or result.get("cmd") or arguments.get("cmd") or ""
|
|
2030
|
+
)
|
|
2031
|
+
output = _verification_output_text(result)
|
|
2032
|
+
if command and output:
|
|
2033
|
+
state.note_post_edit_run_output(command=command, output=output, generation=generation)
|
|
2034
|
+
elif tool_name == "verify_run":
|
|
2035
|
+
command_results = result.get("command_results")
|
|
2036
|
+
if isinstance(command_results, list):
|
|
2037
|
+
for item in command_results:
|
|
2038
|
+
if not isinstance(item, dict):
|
|
2039
|
+
continue
|
|
2040
|
+
command = str(item.get("command") or item.get("effective_command") or "")
|
|
2041
|
+
output = _verification_output_text(item)
|
|
2042
|
+
if command and output:
|
|
2043
|
+
state.note_post_edit_run_output(
|
|
2044
|
+
command=command, output=output, generation=generation
|
|
2045
|
+
)
|
|
2046
|
+
else:
|
|
2047
|
+
output = _verification_output_text(result)
|
|
2048
|
+
commands = result.get("commands")
|
|
2049
|
+
command = (
|
|
2050
|
+
", ".join(str(item) for item in commands if item)
|
|
2051
|
+
if isinstance(commands, list)
|
|
2052
|
+
else ""
|
|
2053
|
+
)
|
|
2054
|
+
if output:
|
|
2055
|
+
state.note_post_edit_run_output(
|
|
2056
|
+
command=command or "verify_run", output=output, generation=generation
|
|
2057
|
+
)
|
|
2058
|
+
|
|
2059
|
+
|
|
2060
|
+
def _verification_attempt_executed_test_runner(
|
|
2061
|
+
*,
|
|
2062
|
+
tool_name: str,
|
|
2063
|
+
arguments: dict[str, Any],
|
|
2064
|
+
result: dict[str, Any],
|
|
2065
|
+
) -> bool:
|
|
2066
|
+
"""True when the recorded verification attempt ran a test-runner command."""
|
|
2067
|
+
return bool(
|
|
2068
|
+
_iter_executed_test_commands(
|
|
2069
|
+
tool_name=tool_name,
|
|
2070
|
+
arguments=arguments,
|
|
2071
|
+
result=result,
|
|
2072
|
+
)
|
|
2073
|
+
)
|
|
2074
|
+
|
|
2075
|
+
|
|
2076
|
+
def _record_tool_effect(
|
|
2077
|
+
*,
|
|
2078
|
+
root: Path,
|
|
2079
|
+
state: TurnExecutionState,
|
|
2080
|
+
tool_name: str,
|
|
2081
|
+
arguments: dict[str, Any],
|
|
2082
|
+
status: str,
|
|
2083
|
+
result: dict[str, Any],
|
|
2084
|
+
known_verification_commands: list[str] | None,
|
|
2085
|
+
verification_authoritative: bool = False,
|
|
2086
|
+
evidence_v2: bool = True,
|
|
2087
|
+
elapsed_ms: int | None = None,
|
|
2088
|
+
) -> None:
|
|
2089
|
+
if is_tool_unavailable_result(result):
|
|
2090
|
+
return
|
|
2091
|
+
normalized_tool = tool_name.strip().lower()
|
|
2092
|
+
touched_paths: set[str] = set()
|
|
2093
|
+
benign_runtime_paths: set[str] = set()
|
|
2094
|
+
if normalized_tool in _COMMAND_LIKE_MUTATION_TOOL_NAMES:
|
|
2095
|
+
raw_touched_paths = result.get("touched_repo_paths")
|
|
2096
|
+
if isinstance(raw_touched_paths, list):
|
|
2097
|
+
classifications = classify_mutation_paths(
|
|
2098
|
+
[str(item) for item in raw_touched_paths if isinstance(item, str)],
|
|
2099
|
+
root=root,
|
|
2100
|
+
command_was_verification=normalized_tool == "verify_run",
|
|
2101
|
+
)
|
|
2102
|
+
touched_paths = {item.path for item in classifications if item.is_material}
|
|
2103
|
+
benign_runtime_paths = {item.path for item in classifications if not item.is_material}
|
|
2104
|
+
if touched_paths:
|
|
2105
|
+
result["material_touched_repo_paths"] = sorted(touched_paths)
|
|
2106
|
+
if benign_runtime_paths:
|
|
2107
|
+
result["benign_runtime_paths"] = sorted(benign_runtime_paths)
|
|
2108
|
+
else:
|
|
2109
|
+
touched_paths = _extract_touched_repo_paths(
|
|
2110
|
+
root=root,
|
|
2111
|
+
tool_name=normalized_tool,
|
|
2112
|
+
arguments=arguments,
|
|
2113
|
+
result=result,
|
|
2114
|
+
)
|
|
2115
|
+
elif status != "failed" and normalized_tool in _MATERIAL_EDIT_TOOL_NAMES:
|
|
2116
|
+
touched_paths = _extract_touched_repo_paths(
|
|
2117
|
+
root=root,
|
|
2118
|
+
tool_name=normalized_tool,
|
|
2119
|
+
arguments=arguments,
|
|
2120
|
+
result=result,
|
|
2121
|
+
)
|
|
2122
|
+
elif normalized_tool == "subagent_run":
|
|
2123
|
+
touched_paths = _extract_touched_repo_paths(
|
|
2124
|
+
root=root,
|
|
2125
|
+
tool_name=normalized_tool,
|
|
2126
|
+
arguments=arguments,
|
|
2127
|
+
result=result,
|
|
2128
|
+
)
|
|
2129
|
+
|
|
2130
|
+
if status != "failed" and normalized_tool == "fs_write" and result.get("created") is True:
|
|
2131
|
+
# A brand-new file the agent authored this turn: a failing test in it is
|
|
2132
|
+
# signal (agent_authored), not a regression. touched_paths already holds
|
|
2133
|
+
# the normalized repo-relative path for fs_write.
|
|
2134
|
+
for created_path in touched_paths:
|
|
2135
|
+
state.note_agent_created_path(created_path)
|
|
2136
|
+
|
|
2137
|
+
# Reproduction-first guardrail (step 5): the reproduction is only evidence
|
|
2138
|
+
# while it stays the one that failed before the fix. Editing a recorded
|
|
2139
|
+
# artifact once product code has already changed is recorded and surfaced.
|
|
2140
|
+
if status != "failed" and normalized_tool in _MATERIAL_EDIT_TOOL_NAMES:
|
|
2141
|
+
_capture_repro_artifact_edits(state=state, touched_paths=touched_paths)
|
|
2142
|
+
|
|
2143
|
+
if (status != "failed" and normalized_tool in _MATERIAL_EDIT_TOOL_NAMES) or (
|
|
2144
|
+
normalized_tool == "subagent_run" and touched_paths
|
|
2145
|
+
):
|
|
2146
|
+
state.note_material_edit()
|
|
2147
|
+
state.material_edit_tools.add(normalized_tool)
|
|
2148
|
+
state.touched_repo_paths.update(touched_paths)
|
|
2149
|
+
if _paths_require_verification(touched_paths):
|
|
2150
|
+
state.note_verification_relevant_edit()
|
|
2151
|
+
elif normalized_tool in _COMMAND_LIKE_MUTATION_TOOL_NAMES and touched_paths:
|
|
2152
|
+
state.note_material_edit()
|
|
2153
|
+
state.material_edit_tools.add(normalized_tool)
|
|
2154
|
+
state.touched_repo_paths.update(touched_paths)
|
|
2155
|
+
if _paths_require_verification(touched_paths):
|
|
2156
|
+
state.note_verification_relevant_edit()
|
|
2157
|
+
elif status != "failed" and normalized_tool == "git_diff":
|
|
2158
|
+
state.record_diff_review()
|
|
2159
|
+
|
|
2160
|
+
verification_attempt = False
|
|
2161
|
+
evidence_records: list[VerificationEvidence] = []
|
|
2162
|
+
evidence = VerificationEvidence(
|
|
2163
|
+
category=VerificationEvidenceCategory.NOT_VERIFICATION,
|
|
2164
|
+
normalized_command=str(arguments.get("cmd") or ""),
|
|
2165
|
+
reason="not_checked",
|
|
2166
|
+
)
|
|
2167
|
+
if normalized_tool == "verify_run":
|
|
2168
|
+
verification_attempt = True
|
|
2169
|
+
evidence_records = _verify_run_evidence_records(
|
|
2170
|
+
result=result,
|
|
2171
|
+
known_verification_commands=known_verification_commands,
|
|
2172
|
+
verification_authoritative=verification_authoritative,
|
|
2173
|
+
material_touched_paths=touched_paths,
|
|
2174
|
+
root=root,
|
|
2175
|
+
evidence_v2=evidence_v2,
|
|
2176
|
+
)
|
|
2177
|
+
evidence = _aggregate_verification_evidence(evidence_records)
|
|
2178
|
+
elif normalized_tool == "shell_run":
|
|
2179
|
+
evidence = _shell_verification_evidence(
|
|
2180
|
+
root=root,
|
|
2181
|
+
state=state,
|
|
2182
|
+
arguments=arguments,
|
|
2183
|
+
result=result,
|
|
2184
|
+
known_verification_commands=known_verification_commands,
|
|
2185
|
+
verification_authoritative=verification_authoritative,
|
|
2186
|
+
material_touched_paths=touched_paths,
|
|
2187
|
+
evidence_v2=evidence_v2,
|
|
2188
|
+
)
|
|
2189
|
+
evidence_records = [evidence]
|
|
2190
|
+
verification_attempt = evidence.category != VerificationEvidenceCategory.NOT_VERIFICATION
|
|
2191
|
+
if normalized_tool in _COMMAND_LIKE_MUTATION_TOOL_NAMES:
|
|
2192
|
+
result["verification_evidence_category"] = evidence.category.value
|
|
2193
|
+
result["verification_evidence_reason"] = evidence.reason
|
|
2194
|
+
result["verification_evidence_allowed"] = evidence.allowed_to_satisfy_contract
|
|
2195
|
+
result["verification_evidence_supplemental_only"] = evidence.supplemental_only
|
|
2196
|
+
result["evidence_verdict"] = evidence.evidence_verdict
|
|
2197
|
+
verification_note = _verification_evidence_note(evidence, result=result)
|
|
2198
|
+
if verification_note:
|
|
2199
|
+
result["verification_note"] = verification_note
|
|
2200
|
+
if evidence.supplemental_only:
|
|
2201
|
+
result["verification_supplemental_only_note"] = SUPPLEMENTAL_VERIFICATION_ADVISORY
|
|
2202
|
+
record_acceptance_tool_effect(
|
|
2203
|
+
contract=state.acceptance_contract,
|
|
2204
|
+
root=root,
|
|
2205
|
+
tool_name=normalized_tool,
|
|
2206
|
+
arguments=arguments,
|
|
2207
|
+
status=status,
|
|
2208
|
+
result=result,
|
|
2209
|
+
touched_paths=touched_paths,
|
|
2210
|
+
known_verification_commands=known_verification_commands,
|
|
2211
|
+
verification_authoritative=verification_authoritative,
|
|
2212
|
+
evidence_category=evidence.category.value,
|
|
2213
|
+
evidence_allowed=evidence.allowed_to_satisfy_contract,
|
|
2214
|
+
)
|
|
2215
|
+
# Baseline-first regression protocol (step 3): capture parsed per-test
|
|
2216
|
+
# outcomes for baseline/attribution. Runs for every executed test-runner
|
|
2217
|
+
# command regardless of the evidence classifier's verdict (so an
|
|
2218
|
+
# unobservable-pipeline run still contributes what its output shows), and
|
|
2219
|
+
# regardless of the kill-switch (capture is telemetry; only the gate policy
|
|
2220
|
+
# is gated).
|
|
2221
|
+
_capture_regression_test_runs(
|
|
2222
|
+
state=state,
|
|
2223
|
+
tool_name=normalized_tool,
|
|
2224
|
+
arguments=arguments,
|
|
2225
|
+
result=result,
|
|
2226
|
+
elapsed_ms=elapsed_ms,
|
|
2227
|
+
)
|
|
2228
|
+
# Turn-contract v2 (step 4): capture post-edit run output for the expected-output
|
|
2229
|
+
# evidence linker. Like the regression capture above, this is unconditional
|
|
2230
|
+
# telemetry (only the gate policy is kill-switched).
|
|
2231
|
+
_capture_expectation_run_outputs(
|
|
2232
|
+
state=state,
|
|
2233
|
+
tool_name=normalized_tool,
|
|
2234
|
+
arguments=arguments,
|
|
2235
|
+
result=result,
|
|
2236
|
+
)
|
|
2237
|
+
# Reproduction-first (step 5): capture every executed command that runs a file
|
|
2238
|
+
# the agent created this turn, phase-classified against the product edits
|
|
2239
|
+
# recorded so far. Unconditional telemetry, like the captures above — only the
|
|
2240
|
+
# gate policy and the turn directives are kill-switched.
|
|
2241
|
+
_capture_repro_runs(
|
|
2242
|
+
state=state,
|
|
2243
|
+
tool_name=normalized_tool,
|
|
2244
|
+
arguments=arguments,
|
|
2245
|
+
status=status,
|
|
2246
|
+
result=result,
|
|
2247
|
+
)
|
|
2248
|
+
if normalized_tool == "shell_run" and not verification_attempt:
|
|
2249
|
+
raw_command = str(
|
|
2250
|
+
result.get("effective_cmd") or result.get("cmd") or arguments.get("cmd") or ""
|
|
2251
|
+
)
|
|
2252
|
+
normalized_command = _unmasked_shell_verification_command(raw_command)
|
|
2253
|
+
observed_exit_code, observed_output = _verification_evidence_observation(
|
|
2254
|
+
tool_name=normalized_tool,
|
|
2255
|
+
evidence=evidence,
|
|
2256
|
+
result=result,
|
|
2257
|
+
)
|
|
2258
|
+
if normalized_command and observed_exit_code == 0 and observed_output:
|
|
2259
|
+
state.record_executed_command_evidence(
|
|
2260
|
+
normalized_command=normalized_command,
|
|
2261
|
+
observed_exit_code=observed_exit_code,
|
|
2262
|
+
observed_output=observed_output,
|
|
2263
|
+
)
|
|
2264
|
+
if not verification_attempt:
|
|
2265
|
+
return
|
|
2266
|
+
|
|
2267
|
+
state.verification_attempt_count += 1
|
|
2268
|
+
state.verification_tools.add(normalized_tool)
|
|
2269
|
+
state.last_verification_passed = _verification_attempt_passed(
|
|
2270
|
+
tool_name=normalized_tool,
|
|
2271
|
+
status=status,
|
|
2272
|
+
result=result,
|
|
2273
|
+
evidence=evidence,
|
|
2274
|
+
)
|
|
2275
|
+
for record in evidence_records:
|
|
2276
|
+
observed_exit_code, observed_output = _verification_evidence_observation(
|
|
2277
|
+
tool_name=normalized_tool,
|
|
2278
|
+
evidence=record,
|
|
2279
|
+
result=result,
|
|
2280
|
+
)
|
|
2281
|
+
state.record_verification_evidence(
|
|
2282
|
+
record,
|
|
2283
|
+
accepted=(
|
|
2284
|
+
state.last_verification_passed is True and record.allowed_to_satisfy_contract
|
|
2285
|
+
),
|
|
2286
|
+
observed_exit_code=observed_exit_code,
|
|
2287
|
+
observed_output=observed_output,
|
|
2288
|
+
)
|
|
2289
|
+
if evidence_v2 and _tool_effect_has_qualifying_execution(
|
|
2290
|
+
tool_name=normalized_tool,
|
|
2291
|
+
evidence_records=evidence_records,
|
|
2292
|
+
result=result,
|
|
2293
|
+
):
|
|
2294
|
+
# Ordering rule: stamp that a real execution run happened after the most
|
|
2295
|
+
# recent material edit, so finalization can require post-edit evidence.
|
|
2296
|
+
state.note_qualifying_execution_evidence()
|
|
2297
|
+
# Baseline-first regression protocol (step 3): remember whether this
|
|
2298
|
+
# verification attempt ran a test-runner command, so the gate can clear a
|
|
2299
|
+
# non-contract all-pre-existing test failure without masking a non-test one.
|
|
2300
|
+
state.last_verification_attempt_was_test_run = _verification_attempt_executed_test_runner(
|
|
2301
|
+
tool_name=normalized_tool,
|
|
2302
|
+
arguments=arguments,
|
|
2303
|
+
result=result,
|
|
2304
|
+
)
|
|
2305
|
+
if normalized_tool == "verify_run":
|
|
2306
|
+
_record_verify_run_command_outcomes(
|
|
2307
|
+
state=state,
|
|
2308
|
+
result=result,
|
|
2309
|
+
known_verification_commands=known_verification_commands,
|
|
2310
|
+
)
|
|
2311
|
+
elif normalized_tool == "shell_run":
|
|
2312
|
+
_record_shell_verification_command_outcome(
|
|
2313
|
+
state=state,
|
|
2314
|
+
arguments=arguments,
|
|
2315
|
+
result=result,
|
|
2316
|
+
known_verification_commands=known_verification_commands,
|
|
2317
|
+
passed=state.last_verification_passed is True,
|
|
2318
|
+
evidence=evidence,
|
|
2319
|
+
)
|
|
2320
|
+
|
|
2321
|
+
if state.last_verification_passed is True:
|
|
2322
|
+
state.last_verification_failure_category = ""
|
|
2323
|
+
if not state.failed_verification_commands():
|
|
2324
|
+
state.last_verification_failure_snippet = ""
|
|
2325
|
+
else:
|
|
2326
|
+
state.last_verification_failure_category = _verification_failure_category_for_tool_result(
|
|
2327
|
+
tool_name=normalized_tool,
|
|
2328
|
+
arguments=arguments,
|
|
2329
|
+
result=result,
|
|
2330
|
+
)
|
|
2331
|
+
state.last_verification_failure_snippet = (
|
|
2332
|
+
extract_verification_failure_snippet(
|
|
2333
|
+
tool_name=normalized_tool,
|
|
2334
|
+
result=result,
|
|
2335
|
+
)
|
|
2336
|
+
or state.first_failed_verification_snippet()
|
|
2337
|
+
)
|
|
2338
|
+
|
|
2339
|
+
|
|
2340
|
+
def _verification_expected_for_turn(
|
|
2341
|
+
*,
|
|
2342
|
+
turn_intent: _OneShotRepoTurnIntent,
|
|
2343
|
+
blocked: bool,
|
|
2344
|
+
touched_repo_paths: set[str],
|
|
2345
|
+
verification_contract_requires_execution: bool = False,
|
|
2346
|
+
verification_contract_available: bool = True,
|
|
2347
|
+
effective_verification_commands: list[str] | tuple[str, ...] | set[str] | None = None,
|
|
2348
|
+
) -> bool:
|
|
2349
|
+
if turn_intent != "execute":
|
|
2350
|
+
return False
|
|
2351
|
+
if verification_contract_requires_execution:
|
|
2352
|
+
return True
|
|
2353
|
+
if blocked:
|
|
2354
|
+
return False
|
|
2355
|
+
if not verification_contract_available:
|
|
2356
|
+
return False
|
|
2357
|
+
return _verification_commands_apply_to_paths(
|
|
2358
|
+
touched_repo_paths,
|
|
2359
|
+
effective_verification_commands,
|
|
2360
|
+
)
|
|
2361
|
+
|
|
2362
|
+
|
|
2363
|
+
def _completion_gate_blocker_allows_final(
|
|
2364
|
+
*,
|
|
2365
|
+
state: TurnExecutionState,
|
|
2366
|
+
blocked_response: bool,
|
|
2367
|
+
) -> bool:
|
|
2368
|
+
if not blocked_response:
|
|
2369
|
+
return False
|
|
2370
|
+
if not state.touched_repo_paths or not _paths_require_verification(state.touched_repo_paths):
|
|
2371
|
+
return True
|
|
2372
|
+
if state.verification_attempt_count <= 0:
|
|
2373
|
+
return False
|
|
2374
|
+
if state.last_verification_passed is True:
|
|
2375
|
+
return True
|
|
2376
|
+
return state.last_verification_failure_category == FailureCategory.INFRA_UNAVAILABLE.value
|
|
2377
|
+
|
|
2378
|
+
|
|
2379
|
+
def _execution_evidence_required_for_turn(
|
|
2380
|
+
*,
|
|
2381
|
+
state: TurnExecutionState,
|
|
2382
|
+
turn_intent: str,
|
|
2383
|
+
blocked: bool,
|
|
2384
|
+
evidence_v2: bool,
|
|
2385
|
+
verification_expected: bool,
|
|
2386
|
+
) -> bool:
|
|
2387
|
+
"""Ordering rule trigger: an execute turn that mutated a verifiable surface.
|
|
2388
|
+
|
|
2389
|
+
The point of the rule is to catch "edited source, then only ran a syntax
|
|
2390
|
+
check (or nothing), then finalized". It requires that verification is
|
|
2391
|
+
actually applicable for this turn (``verification_expected``) so greenfield
|
|
2392
|
+
workspaces with no test surface are not harassed. Turns with no mutating
|
|
2393
|
+
edits (pure Q&A/analysis/advisory) are exempt, as are non-execute turns and
|
|
2394
|
+
blocker finalizations.
|
|
2395
|
+
"""
|
|
2396
|
+
return bool(
|
|
2397
|
+
evidence_v2
|
|
2398
|
+
and verification_expected
|
|
2399
|
+
and str(turn_intent or "") == "execute"
|
|
2400
|
+
and not blocked
|
|
2401
|
+
and state.material_edit_count > 0
|
|
2402
|
+
and _paths_require_verification(state.touched_repo_paths)
|
|
2403
|
+
)
|
|
2404
|
+
|
|
2405
|
+
|
|
2406
|
+
def _completion_gate_problems(
|
|
2407
|
+
*,
|
|
2408
|
+
state: TurnExecutionState,
|
|
2409
|
+
final_text: str,
|
|
2410
|
+
blocked: bool,
|
|
2411
|
+
verification_expected: bool,
|
|
2412
|
+
require_material_edit_evidence: bool = True,
|
|
2413
|
+
evidence_v2: bool = False,
|
|
2414
|
+
turn_intent: str = "",
|
|
2415
|
+
regression_baseline_enabled: bool = False,
|
|
2416
|
+
turn_contract_v2_enabled: bool = False,
|
|
2417
|
+
reproduction_first_enabled: bool = False,
|
|
2418
|
+
repro_engagement_based: bool = False,
|
|
2419
|
+
blast_radius_enabled: bool = False,
|
|
2420
|
+
) -> list[str]:
|
|
2421
|
+
expectation_assessment = state.compute_expectation_assessment(
|
|
2422
|
+
enabled=turn_contract_v2_enabled,
|
|
2423
|
+
turn_intent=turn_intent,
|
|
2424
|
+
)
|
|
2425
|
+
repro_assessment = state.compute_repro_assessment(
|
|
2426
|
+
enabled=reproduction_first_enabled,
|
|
2427
|
+
turn_intent=turn_intent,
|
|
2428
|
+
engagement_based=repro_engagement_based,
|
|
2429
|
+
)
|
|
2430
|
+
blast_radius_assessment = state.compute_blast_radius_assessment(
|
|
2431
|
+
enabled=blast_radius_enabled,
|
|
2432
|
+
turn_intent=turn_intent,
|
|
2433
|
+
)
|
|
2434
|
+
execution_evidence_required = _execution_evidence_required_for_turn(
|
|
2435
|
+
state=state,
|
|
2436
|
+
turn_intent=turn_intent,
|
|
2437
|
+
blocked=blocked,
|
|
2438
|
+
evidence_v2=evidence_v2,
|
|
2439
|
+
verification_expected=verification_expected,
|
|
2440
|
+
)
|
|
2441
|
+
regression_diff = state.compute_regression_diff(enabled=regression_baseline_enabled)
|
|
2442
|
+
# Let attribution supersede a non-contract "last attempt failed" block only
|
|
2443
|
+
# when that last attempt was itself a test run AND the diff attributes at
|
|
2444
|
+
# least one failure as pre-existing/regression/unattributed. The test-run
|
|
2445
|
+
# guard stops an all-benign earlier run from masking a failing non-test
|
|
2446
|
+
# command; the "not agent-authored-only" guard keeps a failing repro the agent
|
|
2447
|
+
# just wrote (agent-authored only) blocking as a generic verification failure.
|
|
2448
|
+
regression_attribution_supersedes_last_failure = bool(
|
|
2449
|
+
regression_baseline_enabled
|
|
2450
|
+
and state.last_verification_attempt_was_test_run
|
|
2451
|
+
and (
|
|
2452
|
+
regression_diff.regressions
|
|
2453
|
+
or regression_diff.unattributed
|
|
2454
|
+
or regression_diff.pre_existing
|
|
2455
|
+
)
|
|
2456
|
+
)
|
|
2457
|
+
certificate = evaluate_completion_certificate(
|
|
2458
|
+
CompletionCertificateInput(
|
|
2459
|
+
contract=state.acceptance_contract,
|
|
2460
|
+
final_text=final_text,
|
|
2461
|
+
blocked=blocked,
|
|
2462
|
+
blocker_valid=blocked,
|
|
2463
|
+
material_edit_count=state.material_edit_count,
|
|
2464
|
+
require_material_result=require_material_edit_evidence,
|
|
2465
|
+
verification_expected=verification_expected,
|
|
2466
|
+
verification_attempt_count=state.verification_attempt_count,
|
|
2467
|
+
last_verification_passed=state.last_verification_passed,
|
|
2468
|
+
failed_verification_commands=state.failed_verification_commands(),
|
|
2469
|
+
expected_verification_commands=set(state.expected_verification_commands),
|
|
2470
|
+
missing_verification_commands=state.missing_verification_commands(),
|
|
2471
|
+
verification_coverage_stale=state.verification_coverage_is_stale(),
|
|
2472
|
+
accepted_verification_evidence=list(state.accepted_verification_evidence),
|
|
2473
|
+
execution_evidence_required=execution_evidence_required,
|
|
2474
|
+
post_edit_execution_evidence_present=state.has_post_edit_execution_evidence(),
|
|
2475
|
+
regression_baseline_enabled=regression_baseline_enabled,
|
|
2476
|
+
regressions=regression_diff.regressions,
|
|
2477
|
+
unattributed_failures=regression_diff.unattributed,
|
|
2478
|
+
pre_existing_failures=regression_diff.pre_existing,
|
|
2479
|
+
agent_authored_failures=regression_diff.agent_authored,
|
|
2480
|
+
regression_attribution_supersedes_last_failure=(
|
|
2481
|
+
regression_attribution_supersedes_last_failure
|
|
2482
|
+
),
|
|
2483
|
+
turn_contract_v2_enabled=turn_contract_v2_enabled,
|
|
2484
|
+
expectations_unaddressed=expectation_assessment.unaddressed,
|
|
2485
|
+
reproduction_first_enabled=reproduction_first_enabled,
|
|
2486
|
+
repro_unconfirmed=repro_blocks_finalization(
|
|
2487
|
+
repro_assessment,
|
|
2488
|
+
material_edit_count=state.material_edit_count,
|
|
2489
|
+
),
|
|
2490
|
+
repro_failing_after_fix=repro_assessment.contradicted,
|
|
2491
|
+
repro_status=repro_assessment.status.value if repro_assessment.applicable else "",
|
|
2492
|
+
repro_artifacts_present=repro_assessment.surviving_artifacts,
|
|
2493
|
+
blast_radius_enabled=blast_radius_enabled,
|
|
2494
|
+
# Failures step 3 already reports as regressions of the same command are
|
|
2495
|
+
# dropped here: one fact, one blocker. What remains is the breakage only
|
|
2496
|
+
# the selected scope saw — the tests the agent never chose to run.
|
|
2497
|
+
blast_radius_new_failures=tuple(
|
|
2498
|
+
test_id
|
|
2499
|
+
for test_id in blast_radius_assessment.new_failures
|
|
2500
|
+
if test_id not in set(regression_diff.regressions)
|
|
2501
|
+
),
|
|
2502
|
+
# Only the "never measured" state feeds the weaker problem; a REGRESSED
|
|
2503
|
+
# assessment whose ids step 3 already owns must not resurface here as a
|
|
2504
|
+
# coverage complaint about a scope the agent demonstrably ran.
|
|
2505
|
+
blast_radius_unverified=(
|
|
2506
|
+
blast_radius_assessment.status == BlastRadiusStatus.GATE_MISSING
|
|
2507
|
+
and blast_radius_blocks_finalization(
|
|
2508
|
+
blast_radius_assessment,
|
|
2509
|
+
material_edit_count=state.material_edit_count,
|
|
2510
|
+
)
|
|
2511
|
+
),
|
|
2512
|
+
blast_radius_status=(
|
|
2513
|
+
blast_radius_assessment.status.value if blast_radius_assessment.applicable else ""
|
|
2514
|
+
),
|
|
2515
|
+
)
|
|
2516
|
+
)
|
|
2517
|
+
state.latest_completion_certificate = certificate.as_payload()
|
|
2518
|
+
return list(certificate.problems)
|
|
2519
|
+
|
|
2520
|
+
|
|
2521
|
+
def _sorted_missing_verification_commands(state: TurnExecutionState) -> list[str]:
|
|
2522
|
+
return sorted(state.missing_verification_commands())
|
|
2523
|
+
|
|
2524
|
+
|
|
2525
|
+
def _completion_gate_problem_summary(problems: list[str]) -> str:
|
|
2526
|
+
labels = [_COMPLETION_GATE_PROBLEM_LABELS.get(item, item) for item in problems]
|
|
2527
|
+
return ", ".join(labels) if labels else "unknown completion gate failure"
|
|
2528
|
+
|
|
2529
|
+
|
|
2530
|
+
def _completion_gate_repair_stage(problems: list[str]) -> str:
|
|
2531
|
+
if "no_material_edits" in problems:
|
|
2532
|
+
return "no_material_edits"
|
|
2533
|
+
# Regressions are the most specific, most actionable verification failure:
|
|
2534
|
+
# named tests that passed pre-edit and now fail. Rank them ahead of the
|
|
2535
|
+
# generic verification_failed so the repair nudge names them concretely.
|
|
2536
|
+
if "regressions_detected" in problems:
|
|
2537
|
+
return "regressions_detected"
|
|
2538
|
+
# Proven collateral damage ranks with the other regression stages and above the
|
|
2539
|
+
# generic verification failure: it names concrete tests, and its repair is a
|
|
2540
|
+
# different action (narrow the change) than "make your own check pass".
|
|
2541
|
+
if "blast_radius_regressions" in problems:
|
|
2542
|
+
return "blast_radius_regressions"
|
|
2543
|
+
if "verification_failed" in problems:
|
|
2544
|
+
return "verification_failed"
|
|
2545
|
+
if "verification_incomplete" in problems:
|
|
2546
|
+
return "verification_incomplete"
|
|
2547
|
+
if "verification_not_attempted" in problems:
|
|
2548
|
+
return "verification_not_attempted"
|
|
2549
|
+
if "unattributed_failures" in problems:
|
|
2550
|
+
return "unattributed_failures"
|
|
2551
|
+
# An unmeasured blast radius ranks *below* the verification stages: when nothing
|
|
2552
|
+
# has been run at all, "you ran no tests" is the more fundamental complaint and
|
|
2553
|
+
# owns the repair loop. This stage takes over once that is satisfied and only the
|
|
2554
|
+
# neighbouring tests are still unrun.
|
|
2555
|
+
if "blast_radius_unverified" in problems:
|
|
2556
|
+
return "blast_radius_unverified"
|
|
2557
|
+
# Reproduction-first: an unvalidated reported symptom ranks above the
|
|
2558
|
+
# task-expectation stage — a reproduction is the most direct evidence that the
|
|
2559
|
+
# delivered change addresses what was reported, not the agent's reading of it.
|
|
2560
|
+
# Scaffolding cleanup shares the stage; the nudge names whichever applies.
|
|
2561
|
+
if "repro_unconfirmed" in problems or "repro_artifacts_present" in problems:
|
|
2562
|
+
return "repro_unconfirmed"
|
|
2563
|
+
# Turn-contract v2: task-named expectations rank below verification/regression
|
|
2564
|
+
# deficits (broken behavior is more urgent) but above acceptance-criteria stages.
|
|
2565
|
+
if "expectations_unaddressed" in problems:
|
|
2566
|
+
return "expectations_unaddressed"
|
|
2567
|
+
if "acceptance_criteria_failed" in problems or "unexpected_scope_changes" in problems:
|
|
2568
|
+
return "acceptance_failed"
|
|
2569
|
+
if (
|
|
2570
|
+
"acceptance_criteria_unverified" in problems
|
|
2571
|
+
or "acceptance_evidence_insufficient" in problems
|
|
2572
|
+
):
|
|
2573
|
+
return "acceptance_unverified"
|
|
2574
|
+
if "empty_final_response" in problems:
|
|
2575
|
+
return "empty_final_response"
|
|
2576
|
+
return "generic"
|
|
2577
|
+
|
|
2578
|
+
|
|
2579
|
+
_LIVE_BACKGROUND_PROCESS_FINALIZATION_LINE = (
|
|
2580
|
+
"- You have {n} background process(es) started with shell_background; they are "
|
|
2581
|
+
"terminated when this run ends. If the task requires a server/daemon to still "
|
|
2582
|
+
"be running after you finish, start it with shell_service_start (durable) instead, "
|
|
2583
|
+
"and re-verify."
|
|
2584
|
+
)
|
|
2585
|
+
|
|
2586
|
+
|
|
2587
|
+
def _live_background_process_finalization_advisory_line(
|
|
2588
|
+
*,
|
|
2589
|
+
one_shot_execution: bool,
|
|
2590
|
+
live_background_processes: int = 0,
|
|
2591
|
+
) -> str:
|
|
2592
|
+
try:
|
|
2593
|
+
count = int(live_background_processes)
|
|
2594
|
+
except (TypeError, ValueError):
|
|
2595
|
+
count = 0
|
|
2596
|
+
if not one_shot_execution or count <= 0:
|
|
2597
|
+
return ""
|
|
2598
|
+
return _LIVE_BACKGROUND_PROCESS_FINALIZATION_LINE.format(n=count)
|
|
2599
|
+
|
|
2600
|
+
|
|
2601
|
+
def _completion_gate_nudge_message(
|
|
2602
|
+
problems: list[str],
|
|
2603
|
+
*,
|
|
2604
|
+
prefix_key: str = "completion_gate_nudge_prefix",
|
|
2605
|
+
verification_failure_snippet: str = "",
|
|
2606
|
+
missing_verification_commands: list[str] | None = None,
|
|
2607
|
+
verification_coverage_stale: bool = False,
|
|
2608
|
+
anchor_paths: list[str] | None = None,
|
|
2609
|
+
has_material_edits: bool = False,
|
|
2610
|
+
all_verification_evidence_self_authored: bool = False,
|
|
2611
|
+
diff_review_stale: bool = False,
|
|
2612
|
+
language: str = "",
|
|
2613
|
+
explicit_language_override: bool = False,
|
|
2614
|
+
one_shot_execution: bool = False,
|
|
2615
|
+
live_background_processes: int = 0,
|
|
2616
|
+
execution_evidence_missing_detail: str = "",
|
|
2617
|
+
regression_ids: list[str] | None = None,
|
|
2618
|
+
regression_baseline_command: str = "",
|
|
2619
|
+
unattributed_ids: list[str] | None = None,
|
|
2620
|
+
expectation_details: list[str] | None = None,
|
|
2621
|
+
repro_assessment: ReproAssessment | None = None,
|
|
2622
|
+
blast_radius_assessment: BlastRadiusAssessment | None = None,
|
|
2623
|
+
) -> str:
|
|
2624
|
+
_ = (
|
|
2625
|
+
prefix_key,
|
|
2626
|
+
verification_coverage_stale,
|
|
2627
|
+
anchor_paths,
|
|
2628
|
+
language,
|
|
2629
|
+
explicit_language_override,
|
|
2630
|
+
)
|
|
2631
|
+
problem_set = set(problems)
|
|
2632
|
+
# A post-edit execution-evidence deficit is action-only: the model must run
|
|
2633
|
+
# the tests; a written explanation cannot clear it. Naming the concrete
|
|
2634
|
+
# missing fact keeps successive nudges specific rather than repetitive.
|
|
2635
|
+
evidence_deficit = bool(execution_evidence_missing_detail) and bool(
|
|
2636
|
+
problem_set & {"verification_not_attempted", "verification_incomplete"}
|
|
2637
|
+
)
|
|
2638
|
+
# Regressions are action-only in the same way: only making the named tests
|
|
2639
|
+
# pass again clears the deficit; prose cannot. Unattributed failures need a
|
|
2640
|
+
# fact (a rerun of the baseline-known command) to be attributed.
|
|
2641
|
+
regression_deficit = "regressions_detected" in problem_set
|
|
2642
|
+
unattributed_deficit = "unattributed_failures" in problem_set
|
|
2643
|
+
# Turn-contract v2: task-named expectations neither confirmed nor disposed. Each
|
|
2644
|
+
# is addressed by editing the named locus or producing (and running) the expected
|
|
2645
|
+
# output — prose alone cannot clear it.
|
|
2646
|
+
expectation_deficit = "expectations_unaddressed" in problem_set
|
|
2647
|
+
# Reproduction-first: an unvalidated reported symptom is action-only too — only
|
|
2648
|
+
# a reproduction that failed before the fix and passes after it clears it.
|
|
2649
|
+
repro_deficit = "repro_unconfirmed" in problem_set
|
|
2650
|
+
repro_artifacts_deficit = "repro_artifacts_present" in problem_set
|
|
2651
|
+
# Blast radius: both states are action-only. Only running the scope measures it,
|
|
2652
|
+
# and only making the broken tests pass again (by narrowing the change) clears a
|
|
2653
|
+
# regression; neither can be talked away.
|
|
2654
|
+
blast_radius_deficit = bool(
|
|
2655
|
+
problem_set & {"blast_radius_regressions", "blast_radius_unverified"}
|
|
2656
|
+
)
|
|
2657
|
+
lines = ["Finalization check - one pass before you finish:"]
|
|
2658
|
+
if "no_material_edits" in problem_set:
|
|
2659
|
+
lines.append(
|
|
2660
|
+
"- No file changes are recorded yet. If the task required creating/modifying "
|
|
2661
|
+
"something, do it now; if you concluded no change is needed, say so explicitly "
|
|
2662
|
+
"with your reasoning."
|
|
2663
|
+
)
|
|
2664
|
+
snippet = extract_actionable_failure_snippet(verification_failure_snippet)
|
|
2665
|
+
if "verification_failed" in problem_set:
|
|
2666
|
+
failure_detail = snippet or "the latest verification attempt did not pass"
|
|
2667
|
+
lines.append(
|
|
2668
|
+
f"- Your last verification failed: {failure_detail}. Fix and re-run, or explain "
|
|
2669
|
+
"why the failure is expected/out of scope."
|
|
2670
|
+
)
|
|
2671
|
+
if missing_verification_commands and (
|
|
2672
|
+
"verification_not_attempted" in problem_set or "verification_incomplete" in problem_set
|
|
2673
|
+
):
|
|
2674
|
+
lines.append(
|
|
2675
|
+
"- Expected verification not yet run: "
|
|
2676
|
+
+ ", ".join(missing_verification_commands)
|
|
2677
|
+
+ ". Run them, or state why they don't apply."
|
|
2678
|
+
)
|
|
2679
|
+
elif "verification_not_attempted" in problem_set or "verification_incomplete" in problem_set:
|
|
2680
|
+
lines.append(
|
|
2681
|
+
"- Expected verification has not been completed. Run it, or state why it does not apply."
|
|
2682
|
+
)
|
|
2683
|
+
if evidence_deficit:
|
|
2684
|
+
lines.append(
|
|
2685
|
+
f"- No test execution recorded {execution_evidence_missing_detail}. Run the relevant "
|
|
2686
|
+
"tests now and observe their output and exit code. A written explanation cannot "
|
|
2687
|
+
"clear this - only a new test run can."
|
|
2688
|
+
)
|
|
2689
|
+
if regression_deficit and regression_ids:
|
|
2690
|
+
baseline = str(regression_baseline_command or "").strip() or "the baseline command"
|
|
2691
|
+
lines.append(
|
|
2692
|
+
"- Regressions your change introduced: "
|
|
2693
|
+
+ ", ".join(str(item) for item in regression_ids)
|
|
2694
|
+
+ f". These tests passed in the pre-edit baseline of `{baseline}` and now fail. "
|
|
2695
|
+
"Fix them and re-run so they pass again. A written explanation cannot clear this - "
|
|
2696
|
+
"only making the tests pass can."
|
|
2697
|
+
)
|
|
2698
|
+
if unattributed_deficit and unattributed_ids:
|
|
2699
|
+
lines.append(
|
|
2700
|
+
"- Failures with no comparable pre-edit baseline (cannot tell if your change caused "
|
|
2701
|
+
"them): "
|
|
2702
|
+
+ ", ".join(str(item) for item in unattributed_ids)
|
|
2703
|
+
+ ". Re-run the exact command you have a baseline for (or run it now to establish "
|
|
2704
|
+
"one) so these can be attributed, or state their relationship to your change with "
|
|
2705
|
+
"evidence."
|
|
2706
|
+
)
|
|
2707
|
+
if expectation_deficit and expectation_details:
|
|
2708
|
+
lines.append(
|
|
2709
|
+
"- Task expectations not yet addressed: "
|
|
2710
|
+
+ "; ".join(str(item) for item in expectation_details)
|
|
2711
|
+
+ ". The task named these concretely. Either make your change satisfy each one "
|
|
2712
|
+
"(edit the named locus, or produce the expected output and run the command so it "
|
|
2713
|
+
"is observed), or explicitly state why the expectation no longer applies."
|
|
2714
|
+
)
|
|
2715
|
+
if repro_deficit and repro_assessment is not None:
|
|
2716
|
+
repro_line = build_repro_nudge_line(repro_assessment)
|
|
2717
|
+
if repro_line:
|
|
2718
|
+
lines.append(repro_line)
|
|
2719
|
+
if repro_artifacts_deficit and repro_assessment is not None:
|
|
2720
|
+
artifacts_line = build_repro_artifacts_nudge_line(repro_assessment.surviving_artifacts)
|
|
2721
|
+
if artifacts_line:
|
|
2722
|
+
lines.append(artifacts_line)
|
|
2723
|
+
if blast_radius_deficit and blast_radius_assessment is not None:
|
|
2724
|
+
blast_radius_line = build_blast_radius_nudge_line(blast_radius_assessment)
|
|
2725
|
+
if blast_radius_line:
|
|
2726
|
+
lines.append(blast_radius_line)
|
|
2727
|
+
if all_verification_evidence_self_authored:
|
|
2728
|
+
lines.append(f"- {SUPPLEMENTAL_VERIFICATION_ADVISORY}")
|
|
2729
|
+
if has_material_edits and diff_review_stale:
|
|
2730
|
+
lines.append(
|
|
2731
|
+
"- Consider reviewing the current diff for accidental scope or quality issues before "
|
|
2732
|
+
"finalizing."
|
|
2733
|
+
)
|
|
2734
|
+
live_background_process_line = _live_background_process_finalization_advisory_line(
|
|
2735
|
+
one_shot_execution=one_shot_execution,
|
|
2736
|
+
live_background_processes=live_background_processes,
|
|
2737
|
+
)
|
|
2738
|
+
if live_background_process_line:
|
|
2739
|
+
lines.append(live_background_process_line)
|
|
2740
|
+
lines.append(
|
|
2741
|
+
"- Re-read the task statement once and confirm every explicitly named output "
|
|
2742
|
+
"(paths, formats, values) exists exactly as requested."
|
|
2743
|
+
)
|
|
2744
|
+
if (
|
|
2745
|
+
evidence_deficit
|
|
2746
|
+
or regression_deficit
|
|
2747
|
+
or unattributed_deficit
|
|
2748
|
+
or expectation_deficit
|
|
2749
|
+
or repro_deficit
|
|
2750
|
+
or blast_radius_deficit
|
|
2751
|
+
):
|
|
2752
|
+
lines.append(
|
|
2753
|
+
"Run the relevant tests now, then give your final answer once you have observed "
|
|
2754
|
+
"the result."
|
|
2755
|
+
)
|
|
2756
|
+
else:
|
|
2757
|
+
lines.append(
|
|
2758
|
+
"Then give your final answer. If you are confident the work is complete as-is, "
|
|
2759
|
+
"finalize - this checklist is advisory."
|
|
2760
|
+
)
|
|
2761
|
+
return "\n".join(lines)
|
|
2762
|
+
|
|
2763
|
+
|
|
2764
|
+
def _build_interactive_turn_verify_task(
|
|
2765
|
+
*,
|
|
2766
|
+
session: Any,
|
|
2767
|
+
instruction: str,
|
|
2768
|
+
) -> tuple[dict[str, Any] | None, list[str]]:
|
|
2769
|
+
task_paths = _extract_workspace_relation_paths_from_text(root=session.root, text=instruction)
|
|
2770
|
+
task_brief = _session_task_brief_content(session)
|
|
2771
|
+
if task_brief:
|
|
2772
|
+
for path in _extract_workspace_relation_paths_from_text(root=session.root, text=task_brief):
|
|
2773
|
+
if path not in task_paths:
|
|
2774
|
+
task_paths.append(path)
|
|
2775
|
+
task_texts = [str(instruction or "").strip()]
|
|
2776
|
+
if task_brief:
|
|
2777
|
+
task_texts.extend(_task_brief_lines_from_text(task_brief, max_lines=6))
|
|
2778
|
+
task_texts = [text for text in task_texts if text]
|
|
2779
|
+
if not task_paths and not task_texts:
|
|
2780
|
+
return None, []
|
|
2781
|
+
task: dict[str, Any] = {}
|
|
2782
|
+
if task_paths:
|
|
2783
|
+
task["estimated_files"] = list(task_paths)
|
|
2784
|
+
task["write_scope"] = list(task_paths)
|
|
2785
|
+
if task_texts:
|
|
2786
|
+
task["acceptance_criteria"] = list(task_texts)
|
|
2787
|
+
return task, task_texts
|
|
2788
|
+
|
|
2789
|
+
|
|
2790
|
+
def _refresh_execute_turn_verification_selection(
|
|
2791
|
+
session: Any,
|
|
2792
|
+
*,
|
|
2793
|
+
instruction: str,
|
|
2794
|
+
route_execution_posture: str,
|
|
2795
|
+
) -> None:
|
|
2796
|
+
if not bool(getattr(session, "verification_enabled", True)):
|
|
2797
|
+
return
|
|
2798
|
+
runtime_kind = getattr(session, "runtime_kind", RuntimeKind.INTERACTIVE_CHAT)
|
|
2799
|
+
one_shot_execution = bool(getattr(session, "one_shot_execution", False))
|
|
2800
|
+
if runtime_kind != RuntimeKind.INTERACTIVE_CHAT and not one_shot_execution:
|
|
2801
|
+
return
|
|
2802
|
+
if getattr(session, "authoritative_verification_commands", None) is not None:
|
|
2803
|
+
return
|
|
2804
|
+
if str(route_execution_posture or "").strip().lower() != "execute":
|
|
2805
|
+
return
|
|
2806
|
+
|
|
2807
|
+
repo_scan = _session_repo_scan(session)
|
|
2808
|
+
task, plan_requirements = _build_interactive_turn_verify_task(
|
|
2809
|
+
session=session,
|
|
2810
|
+
instruction=instruction,
|
|
2811
|
+
)
|
|
2812
|
+
current = _session_verify_command_selection(session)
|
|
2813
|
+
resolved = resolve_task_aware_verify_command_selection(
|
|
2814
|
+
cfg=session.cfg,
|
|
2815
|
+
verify_cmd=None,
|
|
2816
|
+
task=task,
|
|
2817
|
+
root=session.root,
|
|
2818
|
+
repo_scan=repo_scan,
|
|
2819
|
+
plan_requirements=plan_requirements,
|
|
2820
|
+
selection=current,
|
|
2821
|
+
)
|
|
2822
|
+
explicit_commands = extract_explicit_acceptance_commands(
|
|
2823
|
+
instruction,
|
|
2824
|
+
*[str(item) for item in plan_requirements],
|
|
2825
|
+
)
|
|
2826
|
+
if (
|
|
2827
|
+
explicit_commands
|
|
2828
|
+
and not is_authoritative_verify_command_selection(current)
|
|
2829
|
+
and resolved.contract_type in {"generic_fallback", "unavailable", ""}
|
|
2830
|
+
):
|
|
2831
|
+
resolved = ResolvedVerifyCommands(
|
|
2832
|
+
commands=tuple(explicit_commands),
|
|
2833
|
+
source="task_refinement.explicit_user_command",
|
|
2834
|
+
reason="explicit user command is the task-native verification contract",
|
|
2835
|
+
contract_type="task_acceptance",
|
|
2836
|
+
)
|
|
2837
|
+
if (
|
|
2838
|
+
current is not None
|
|
2839
|
+
and current.commands == resolved.commands
|
|
2840
|
+
and current.source == resolved.source
|
|
2841
|
+
and current.reason == resolved.reason
|
|
2842
|
+
and current.contract_type == resolved.contract_type
|
|
2843
|
+
):
|
|
2844
|
+
return
|
|
2845
|
+
|
|
2846
|
+
previous_payload = (
|
|
2847
|
+
verification_selection_payload(
|
|
2848
|
+
current,
|
|
2849
|
+
authoritative=is_authoritative_verify_command_selection(current),
|
|
2850
|
+
)
|
|
2851
|
+
if current is not None
|
|
2852
|
+
else None
|
|
2853
|
+
)
|
|
2854
|
+
session.effective_verification_commands = list(resolved.commands)
|
|
2855
|
+
session.verification_selection_source = resolved.source
|
|
2856
|
+
session.verification_selection_reason = resolved.reason
|
|
2857
|
+
session.verification_contract_type = resolved.contract_type
|
|
2858
|
+
session.verification_authoritative = is_authoritative_verify_command_selection(resolved)
|
|
2859
|
+
refresh_session_environment_context_message(session)
|
|
2860
|
+
payload: dict[str, Any] = {
|
|
2861
|
+
"instruction_paths": list(task.get("estimated_files", []))
|
|
2862
|
+
if isinstance(task, dict)
|
|
2863
|
+
else [],
|
|
2864
|
+
"route_execution_posture": route_execution_posture,
|
|
2865
|
+
**verification_selection_payload(
|
|
2866
|
+
resolved,
|
|
2867
|
+
authoritative=is_authoritative_verify_command_selection(resolved),
|
|
2868
|
+
),
|
|
2869
|
+
}
|
|
2870
|
+
if previous_payload is not None:
|
|
2871
|
+
payload["previous"] = previous_payload
|
|
2872
|
+
session.store.append("verification_contract_updated", payload)
|
|
2873
|
+
|
|
2874
|
+
|
|
2875
|
+
def _refresh_interactive_turn_verification_selection(
|
|
2876
|
+
session: Any,
|
|
2877
|
+
*,
|
|
2878
|
+
instruction: str,
|
|
2879
|
+
route_execution_posture: str,
|
|
2880
|
+
) -> None:
|
|
2881
|
+
_refresh_execute_turn_verification_selection(
|
|
2882
|
+
session,
|
|
2883
|
+
instruction=instruction,
|
|
2884
|
+
route_execution_posture=route_execution_posture,
|
|
2885
|
+
)
|