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,227 @@
|
|
|
1
|
+
"""Best-effort prompt-cache affinity while a parent waits on synchronous children."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import threading
|
|
7
|
+
from collections.abc import Callable, Iterator
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from time import monotonic
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ..execution_deadline import DeadlinePhase, ExecutionDeadline
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def cache_keepalive_unsupported_reason(client: Any) -> str | None:
|
|
17
|
+
"""Return why replay keepalive cannot preserve this client's cache stream."""
|
|
18
|
+
if getattr(client, "provider_auth", None) is not None:
|
|
19
|
+
return "subscription_gateway"
|
|
20
|
+
transport = str(getattr(client, "cache_keepalive_transport", "") or "").strip()
|
|
21
|
+
if transport == "response_continuation":
|
|
22
|
+
return transport
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class CacheKeepaliveRequest:
|
|
28
|
+
"""Immutable copy of the last parent request prefix sent to the provider."""
|
|
29
|
+
|
|
30
|
+
messages: list[dict[str, Any]]
|
|
31
|
+
tools: list[dict[str, Any]] | None
|
|
32
|
+
tool_choice: Any | None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ParentCacheKeepalive:
|
|
36
|
+
"""Refresh an idle parent prefix without changing its durable transcript."""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
*,
|
|
41
|
+
enabled: bool,
|
|
42
|
+
idle_threshold_s: float,
|
|
43
|
+
send_ping: Callable[[CacheKeepaliveRequest, threading.Event], bool],
|
|
44
|
+
on_disabled: Callable[[int], None] | None = None,
|
|
45
|
+
on_unsupported: Callable[[str], None] | None = None,
|
|
46
|
+
unsupported_reason: str | None = None,
|
|
47
|
+
deadline: ExecutionDeadline | None,
|
|
48
|
+
clock: Callable[[], float] = monotonic,
|
|
49
|
+
) -> None:
|
|
50
|
+
requested_enabled = bool(enabled)
|
|
51
|
+
normalized_unsupported_reason = str(unsupported_reason or "").strip()
|
|
52
|
+
self.enabled = requested_enabled and not normalized_unsupported_reason
|
|
53
|
+
self.idle_threshold_s = max(0.001, float(idle_threshold_s))
|
|
54
|
+
self._send_ping = send_ping
|
|
55
|
+
self._on_disabled = on_disabled
|
|
56
|
+
self._deadline = deadline
|
|
57
|
+
self._clock = clock
|
|
58
|
+
self._lock = threading.RLock()
|
|
59
|
+
self._snapshot: CacheKeepaliveRequest | None = None
|
|
60
|
+
self._last_parent_request_at: float | None = None
|
|
61
|
+
self._last_ping_at: float | None = None
|
|
62
|
+
self._wait_depth = 0
|
|
63
|
+
self._cancelled: Callable[[], bool] | None = None
|
|
64
|
+
self._stop_event: threading.Event | None = None
|
|
65
|
+
self._worker: threading.Thread | None = None
|
|
66
|
+
self._consecutive_failures = 0
|
|
67
|
+
self._failure_disable_reported = False
|
|
68
|
+
if requested_enabled and normalized_unsupported_reason and on_unsupported is not None:
|
|
69
|
+
try:
|
|
70
|
+
on_unsupported(normalized_unsupported_reason)
|
|
71
|
+
except Exception: # noqa: BLE001 - warning emission is best-effort
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
def note_parent_request(
|
|
75
|
+
self,
|
|
76
|
+
*,
|
|
77
|
+
messages: list[dict[str, Any]],
|
|
78
|
+
tools: list[dict[str, Any]] | None,
|
|
79
|
+
tool_choice: Any | None = None,
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Capture the exact prefix inputs before a real parent request is sent."""
|
|
82
|
+
if not self.enabled:
|
|
83
|
+
return
|
|
84
|
+
with self._lock:
|
|
85
|
+
self._snapshot = CacheKeepaliveRequest(
|
|
86
|
+
messages=copy.deepcopy(messages),
|
|
87
|
+
tools=copy.deepcopy(tools),
|
|
88
|
+
tool_choice=copy.deepcopy(tool_choice),
|
|
89
|
+
)
|
|
90
|
+
self._last_parent_request_at = self._clock()
|
|
91
|
+
self._last_ping_at = None
|
|
92
|
+
|
|
93
|
+
def forget_parent_request(self) -> None:
|
|
94
|
+
"""Drop a snapshot that must not be retained or replayed."""
|
|
95
|
+
with self._lock:
|
|
96
|
+
self._snapshot = None
|
|
97
|
+
self._last_parent_request_at = None
|
|
98
|
+
self._last_ping_at = None
|
|
99
|
+
|
|
100
|
+
@contextmanager
|
|
101
|
+
def synchronous_child_wait(
|
|
102
|
+
self,
|
|
103
|
+
*,
|
|
104
|
+
cancelled: Callable[[], bool] | None = None,
|
|
105
|
+
) -> Iterator[None]:
|
|
106
|
+
"""Mark time in which the parent is blocked on synchronous child work."""
|
|
107
|
+
self._begin_wait(cancelled=cancelled)
|
|
108
|
+
try:
|
|
109
|
+
yield
|
|
110
|
+
finally:
|
|
111
|
+
self._end_wait()
|
|
112
|
+
|
|
113
|
+
def _begin_wait(self, *, cancelled: Callable[[], bool] | None) -> None:
|
|
114
|
+
if not self.enabled:
|
|
115
|
+
return
|
|
116
|
+
with self._lock:
|
|
117
|
+
self._wait_depth += 1
|
|
118
|
+
self._cancelled = cancelled
|
|
119
|
+
if self._wait_depth != 1 or self._snapshot is None:
|
|
120
|
+
return
|
|
121
|
+
stop_event = threading.Event()
|
|
122
|
+
worker = threading.Thread(
|
|
123
|
+
target=self._run,
|
|
124
|
+
args=(stop_event,),
|
|
125
|
+
name="parent-cache-keepalive",
|
|
126
|
+
daemon=True,
|
|
127
|
+
)
|
|
128
|
+
self._stop_event = stop_event
|
|
129
|
+
self._worker = worker
|
|
130
|
+
worker.start()
|
|
131
|
+
|
|
132
|
+
def _end_wait(self) -> None:
|
|
133
|
+
with self._lock:
|
|
134
|
+
self._wait_depth = max(0, self._wait_depth - 1)
|
|
135
|
+
if self._wait_depth != 0:
|
|
136
|
+
return
|
|
137
|
+
self._cancelled = None
|
|
138
|
+
if self._stop_event is not None:
|
|
139
|
+
self._stop_event.set()
|
|
140
|
+
self._stop_event = None
|
|
141
|
+
self._worker = None
|
|
142
|
+
# Never join here: a provider that ignores cooperative cancellation must
|
|
143
|
+
# not add latency to the child result the parent is waiting for.
|
|
144
|
+
|
|
145
|
+
def close(self) -> None:
|
|
146
|
+
"""Stop refreshes during session teardown."""
|
|
147
|
+
worker: threading.Thread | None = None
|
|
148
|
+
with self._lock:
|
|
149
|
+
self._wait_depth = 0
|
|
150
|
+
self._cancelled = None
|
|
151
|
+
if self._stop_event is not None:
|
|
152
|
+
self._stop_event.set()
|
|
153
|
+
worker = self._worker
|
|
154
|
+
self._stop_event = None
|
|
155
|
+
self._worker = None
|
|
156
|
+
if worker is not None and worker is not threading.current_thread():
|
|
157
|
+
worker.join(timeout=1.0)
|
|
158
|
+
|
|
159
|
+
def _run(self, stop_event: threading.Event) -> None:
|
|
160
|
+
while not stop_event.is_set():
|
|
161
|
+
with self._lock:
|
|
162
|
+
anchor = self._last_ping_at or self._last_parent_request_at
|
|
163
|
+
active = self._wait_depth > 0
|
|
164
|
+
if not active or anchor is None:
|
|
165
|
+
return
|
|
166
|
+
wait_s = max(0.0, anchor + self.idle_threshold_s - self._clock())
|
|
167
|
+
if stop_event.wait(wait_s):
|
|
168
|
+
return
|
|
169
|
+
if not self._send_if_due(stop_event=stop_event):
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
def _send_if_due(self, *, stop_event: threading.Event) -> bool:
|
|
173
|
+
"""Send one due ping; return whether the periodic loop may continue."""
|
|
174
|
+
with self._lock:
|
|
175
|
+
snapshot = self._snapshot
|
|
176
|
+
anchor = self._last_ping_at or self._last_parent_request_at
|
|
177
|
+
cancelled = self._cancelled
|
|
178
|
+
if (
|
|
179
|
+
self._wait_depth <= 0
|
|
180
|
+
or snapshot is None
|
|
181
|
+
or anchor is None
|
|
182
|
+
or self._clock() < anchor + self.idle_threshold_s
|
|
183
|
+
):
|
|
184
|
+
return True
|
|
185
|
+
if stop_event.is_set() or (cancelled is not None and cancelled()):
|
|
186
|
+
return False
|
|
187
|
+
if not self._deadline_allows_ping():
|
|
188
|
+
return False
|
|
189
|
+
# Reserve the interval before sending so a provider error cannot create
|
|
190
|
+
# a tight retry loop. The next attempt is never earlier than one full
|
|
191
|
+
# configured threshold.
|
|
192
|
+
self._last_ping_at = self._clock()
|
|
193
|
+
request = copy.deepcopy(snapshot)
|
|
194
|
+
try:
|
|
195
|
+
succeeded = bool(self._send_ping(request, stop_event))
|
|
196
|
+
except Exception: # noqa: BLE001 - affinity can never crash child work
|
|
197
|
+
succeeded = False
|
|
198
|
+
disable_callback: Callable[[int], None] | None = None
|
|
199
|
+
failures = 0
|
|
200
|
+
with self._lock:
|
|
201
|
+
if succeeded:
|
|
202
|
+
self._consecutive_failures = 0
|
|
203
|
+
else:
|
|
204
|
+
self._consecutive_failures += 1
|
|
205
|
+
failures = self._consecutive_failures
|
|
206
|
+
if failures >= 2:
|
|
207
|
+
self.enabled = False
|
|
208
|
+
stop_event.set()
|
|
209
|
+
if not self._failure_disable_reported:
|
|
210
|
+
self._failure_disable_reported = True
|
|
211
|
+
disable_callback = self._on_disabled
|
|
212
|
+
if disable_callback is not None:
|
|
213
|
+
try:
|
|
214
|
+
disable_callback(failures)
|
|
215
|
+
except Exception: # noqa: BLE001 - warning emission is best-effort
|
|
216
|
+
pass
|
|
217
|
+
return failures < 2 and not stop_event.is_set()
|
|
218
|
+
|
|
219
|
+
def _deadline_allows_ping(self) -> bool:
|
|
220
|
+
deadline = self._deadline
|
|
221
|
+
if deadline is None:
|
|
222
|
+
return True
|
|
223
|
+
phase = deadline.phase()
|
|
224
|
+
if phase in {DeadlinePhase.FINALIZATION_WINDOW, DeadlinePhase.EXHAUSTED}:
|
|
225
|
+
return False
|
|
226
|
+
remaining = deadline.remaining_seconds()
|
|
227
|
+
return remaining is None or remaining >= self.idle_threshold_s
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .acceptance_contract import (
|
|
8
|
+
AcceptanceContract,
|
|
9
|
+
AcceptanceCriterion,
|
|
10
|
+
AcceptanceCriterionEnforcement,
|
|
11
|
+
AcceptanceCriterionKind,
|
|
12
|
+
AcceptanceCriterionStatus,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CompletionCertificateStatus(StrEnum):
|
|
17
|
+
SUFFICIENT = "SUFFICIENT"
|
|
18
|
+
INSUFFICIENT = "INSUFFICIENT"
|
|
19
|
+
CONTRADICTED = "CONTRADICTED"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_EVIDENCE_RANK = {
|
|
23
|
+
"HOST_AUTHORITATIVE": 0,
|
|
24
|
+
"USER_EXPLICIT": 1,
|
|
25
|
+
"PREEXISTING_TASK_CHECKER": 2,
|
|
26
|
+
"DIRECT_BLACK_BOX": 3,
|
|
27
|
+
"PREEXISTING_REPO_NATIVE": 4,
|
|
28
|
+
"SELF_AUTHORED": 5,
|
|
29
|
+
"AD_HOC_OBSERVATION": 6,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class CompletionCertificate:
|
|
35
|
+
status: CompletionCertificateStatus
|
|
36
|
+
problems: tuple[str, ...] = tuple()
|
|
37
|
+
hard_criterion_ids: tuple[str, ...] = tuple()
|
|
38
|
+
covered_hard_criterion_ids: tuple[str, ...] = tuple()
|
|
39
|
+
failed_hard_criterion_ids: tuple[str, ...] = tuple()
|
|
40
|
+
evidence_hierarchy: tuple[str, ...] = tuple()
|
|
41
|
+
reason: str = ""
|
|
42
|
+
# Baseline-first regression attribution (step 3): failing test ids mechanically
|
|
43
|
+
# split against the pre-edit same-command baseline. pre_existing/agent_authored
|
|
44
|
+
# do not block; regressions/unattributed do (see gate policy).
|
|
45
|
+
regressions: tuple[str, ...] = tuple()
|
|
46
|
+
unattributed_failures: tuple[str, ...] = tuple()
|
|
47
|
+
pre_existing_failures: tuple[str, ...] = tuple()
|
|
48
|
+
agent_authored_failures: tuple[str, ...] = tuple()
|
|
49
|
+
# Turn-contract v2 (step 4): expectation ids left unaddressed at the gate.
|
|
50
|
+
expectations_unaddressed: tuple[str, ...] = tuple()
|
|
51
|
+
# Reproduction-first (step 5): the protocol status for a bug-fix-shaped turn,
|
|
52
|
+
# plus any repro scaffolding still present in the working tree.
|
|
53
|
+
repro_status: str = ""
|
|
54
|
+
repro_artifacts_present: tuple[str, ...] = tuple()
|
|
55
|
+
# Blast radius (step 6): tests that passed in the clean-tree baseline of the
|
|
56
|
+
# selected scope and fail after the change, plus the protocol status.
|
|
57
|
+
blast_radius_status: str = ""
|
|
58
|
+
blast_radius_new_failures: tuple[str, ...] = tuple()
|
|
59
|
+
|
|
60
|
+
def as_payload(self) -> dict[str, Any]:
|
|
61
|
+
return {
|
|
62
|
+
"status": self.status.value,
|
|
63
|
+
"problems": list(self.problems),
|
|
64
|
+
"hard_criterion_ids": list(self.hard_criterion_ids),
|
|
65
|
+
"covered_hard_criterion_ids": list(self.covered_hard_criterion_ids),
|
|
66
|
+
"failed_hard_criterion_ids": list(self.failed_hard_criterion_ids),
|
|
67
|
+
"evidence_hierarchy": list(self.evidence_hierarchy),
|
|
68
|
+
"reason": self.reason,
|
|
69
|
+
"regressions": list(self.regressions),
|
|
70
|
+
"unattributed_failures": list(self.unattributed_failures),
|
|
71
|
+
"pre_existing_failures": list(self.pre_existing_failures),
|
|
72
|
+
"agent_authored_failures": list(self.agent_authored_failures),
|
|
73
|
+
"expectations_unaddressed": list(self.expectations_unaddressed),
|
|
74
|
+
"repro_status": self.repro_status,
|
|
75
|
+
"repro_artifacts_present": list(self.repro_artifacts_present),
|
|
76
|
+
"blast_radius_status": self.blast_radius_status,
|
|
77
|
+
"blast_radius_new_failures": list(self.blast_radius_new_failures),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class CompletionCertificateInput:
|
|
83
|
+
contract: AcceptanceContract | None
|
|
84
|
+
final_text: str
|
|
85
|
+
blocked: bool
|
|
86
|
+
blocker_valid: bool
|
|
87
|
+
material_edit_count: int
|
|
88
|
+
require_material_result: bool
|
|
89
|
+
verification_expected: bool
|
|
90
|
+
verification_attempt_count: int
|
|
91
|
+
last_verification_passed: bool | None
|
|
92
|
+
failed_verification_commands: set[str] = field(default_factory=set)
|
|
93
|
+
expected_verification_commands: set[str] = field(default_factory=set)
|
|
94
|
+
missing_verification_commands: set[str] = field(default_factory=set)
|
|
95
|
+
verification_coverage_stale: bool = False
|
|
96
|
+
accepted_verification_evidence: list[dict[str, Any]] = field(default_factory=list)
|
|
97
|
+
# Ordering rule (evidence v2): an execute-posture turn that made mutating
|
|
98
|
+
# edits to a verifiable surface requires a qualifying execution-evidence
|
|
99
|
+
# event that ran AFTER the last such edit. Independent of whether any named
|
|
100
|
+
# verification contract exists.
|
|
101
|
+
execution_evidence_required: bool = False
|
|
102
|
+
post_edit_execution_evidence_present: bool = False
|
|
103
|
+
# Baseline-first regression attribution (step 3). The four lists are the
|
|
104
|
+
# aggregated diff of post-edit test failures vs the same-command pre-edit
|
|
105
|
+
# baseline. ``regression_attribution_supersedes_last_failure`` is True when the
|
|
106
|
+
# last verification attempt was a test run and the diff attributes at least one
|
|
107
|
+
# failure as pre-existing/regression/unattributed — so the specific attribution
|
|
108
|
+
# (below) replaces the generic ``verification_failed``: pre-existing-only does
|
|
109
|
+
# not block (sympy-12489), regressions/unattributed get their own problem.
|
|
110
|
+
regression_baseline_enabled: bool = False
|
|
111
|
+
regressions: tuple[str, ...] = tuple()
|
|
112
|
+
unattributed_failures: tuple[str, ...] = tuple()
|
|
113
|
+
pre_existing_failures: tuple[str, ...] = tuple()
|
|
114
|
+
agent_authored_failures: tuple[str, ...] = tuple()
|
|
115
|
+
regression_attribution_supersedes_last_failure: bool = False
|
|
116
|
+
# Turn-contract v2 (step 4). ``expectations_unaddressed`` are the ids of task
|
|
117
|
+
# expectations (expected-output literals / named loci) that were neither
|
|
118
|
+
# mechanically confirmed nor explicitly disposed. They block as a repairable
|
|
119
|
+
# INSUFFICIENT problem (a nudge, bounded, then an honest UNCONFIRMED marker) —
|
|
120
|
+
# never CONTRADICTED, since a missing disposition is not a proven failure.
|
|
121
|
+
turn_contract_v2_enabled: bool = False
|
|
122
|
+
expectations_unaddressed: tuple[str, ...] = tuple()
|
|
123
|
+
# Reproduction-first (step 5). ``repro_unconfirmed`` is True when the turn is
|
|
124
|
+
# bug-fix-shaped, changed product code, and no reproduction of the *reported*
|
|
125
|
+
# symptom was observed failing before the fix and passing after it. It blocks
|
|
126
|
+
# as a repairable INSUFFICIENT problem — except when the reproduction actively
|
|
127
|
+
# still fails (``repro_failing_after_fix``), which is a proven CONTRADICTED
|
|
128
|
+
# result, the same rank as a detected regression. ``repro_artifacts_present``
|
|
129
|
+
# are recorded scaffolding paths still in the tree at finalization.
|
|
130
|
+
reproduction_first_enabled: bool = False
|
|
131
|
+
repro_unconfirmed: bool = False
|
|
132
|
+
repro_failing_after_fix: bool = False
|
|
133
|
+
repro_status: str = ""
|
|
134
|
+
repro_artifacts_present: tuple[str, ...] = tuple()
|
|
135
|
+
# Blast radius (step 6). ``blast_radius_new_failures`` are tests that passed in
|
|
136
|
+
# the clean-tree baseline of the selected scope and fail after the change: proven
|
|
137
|
+
# collateral damage, so they rank CONTRADICTED alongside a step-3 regression.
|
|
138
|
+
# ``blast_radius_unverified`` is the weaker deficit — the scope around the change
|
|
139
|
+
# was never run after it — and blocks as a repairable INSUFFICIENT problem.
|
|
140
|
+
blast_radius_enabled: bool = False
|
|
141
|
+
blast_radius_new_failures: tuple[str, ...] = tuple()
|
|
142
|
+
blast_radius_unverified: bool = False
|
|
143
|
+
blast_radius_status: str = ""
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def evaluate_completion_certificate(
|
|
147
|
+
certificate_input: CompletionCertificateInput,
|
|
148
|
+
) -> CompletionCertificate:
|
|
149
|
+
problems: list[str] = []
|
|
150
|
+
failed_hard: list[str] = []
|
|
151
|
+
covered_hard: list[str] = []
|
|
152
|
+
|
|
153
|
+
if not str(certificate_input.final_text or "").strip():
|
|
154
|
+
problems.append("empty_final_response")
|
|
155
|
+
if certificate_input.blocked:
|
|
156
|
+
if not certificate_input.blocker_valid:
|
|
157
|
+
problems.append("acceptance_evidence_insufficient")
|
|
158
|
+
elif certificate_input.require_material_result and certificate_input.material_edit_count <= 0:
|
|
159
|
+
problems.append("no_material_edits")
|
|
160
|
+
|
|
161
|
+
regression_enabled = certificate_input.regression_baseline_enabled
|
|
162
|
+
has_regressions = regression_enabled and bool(certificate_input.regressions)
|
|
163
|
+
has_unattributed = regression_enabled and bool(certificate_input.unattributed_failures)
|
|
164
|
+
|
|
165
|
+
# When attribution supersedes a failed non-contract run (its failures are all
|
|
166
|
+
# pre-existing / regression / unattributed), the generic verification_failed is
|
|
167
|
+
# replaced by the specific attribution below — but the missing/stale coverage
|
|
168
|
+
# checks must still run, so this only suppresses the failed-run branch itself.
|
|
169
|
+
supersede_last_failure = regression_enabled and bool(
|
|
170
|
+
certificate_input.regression_attribution_supersedes_last_failure
|
|
171
|
+
)
|
|
172
|
+
if certificate_input.verification_expected:
|
|
173
|
+
if certificate_input.verification_attempt_count <= 0:
|
|
174
|
+
problems.append("verification_not_attempted")
|
|
175
|
+
elif certificate_input.failed_verification_commands:
|
|
176
|
+
# A named contract command failed: this always blocks. Regression
|
|
177
|
+
# attribution never clears a contract failure (step 2 stays intact).
|
|
178
|
+
problems.append("verification_failed")
|
|
179
|
+
elif certificate_input.last_verification_passed is not True and not supersede_last_failure:
|
|
180
|
+
problems.append("verification_failed")
|
|
181
|
+
elif certificate_input.missing_verification_commands:
|
|
182
|
+
problems.append("verification_incomplete")
|
|
183
|
+
elif certificate_input.verification_coverage_stale:
|
|
184
|
+
problems.append("verification_incomplete")
|
|
185
|
+
|
|
186
|
+
# Regression attribution problems are independent of which attempt ran last AND
|
|
187
|
+
# of whether a named verify contract resolved: a regression proven by the
|
|
188
|
+
# agent's own before/after runs must block even when verification_expected is
|
|
189
|
+
# False (e.g. no resolvable verify command). No-edit/advisory turns are exempt
|
|
190
|
+
# structurally (the diff is empty without post-edit runs); blocked
|
|
191
|
+
# finalizations are exempt to stay consistent with step 2.
|
|
192
|
+
if regression_enabled and not certificate_input.blocked:
|
|
193
|
+
if has_regressions:
|
|
194
|
+
problems.append("regressions_detected")
|
|
195
|
+
elif has_unattributed and certificate_input.verification_expected:
|
|
196
|
+
problems.append("unattributed_failures")
|
|
197
|
+
|
|
198
|
+
# Turn-contract v2: task-named expectations left unaddressed (neither confirmed
|
|
199
|
+
# by observed evidence nor explicitly disposed) block this execute turn until
|
|
200
|
+
# resolved. Blocked finalizations are exempt (consistent with steps 2-3).
|
|
201
|
+
if (
|
|
202
|
+
certificate_input.turn_contract_v2_enabled
|
|
203
|
+
and not certificate_input.blocked
|
|
204
|
+
and certificate_input.expectations_unaddressed
|
|
205
|
+
):
|
|
206
|
+
problems.append("expectations_unaddressed")
|
|
207
|
+
|
|
208
|
+
# Reproduction-first: a bug-fix-shaped turn that changed product code without a
|
|
209
|
+
# reproduction that failed before the fix and passes after it has not validated
|
|
210
|
+
# the *reported* symptom, only its own reading of it. Blocked finalizations are
|
|
211
|
+
# exempt (consistent with steps 2-4).
|
|
212
|
+
repro_enabled = certificate_input.reproduction_first_enabled
|
|
213
|
+
repro_unconfirmed = repro_enabled and certificate_input.repro_unconfirmed
|
|
214
|
+
if repro_unconfirmed and not certificate_input.blocked:
|
|
215
|
+
problems.append("repro_unconfirmed")
|
|
216
|
+
if (
|
|
217
|
+
repro_enabled
|
|
218
|
+
and not certificate_input.blocked
|
|
219
|
+
and certificate_input.repro_artifacts_present
|
|
220
|
+
):
|
|
221
|
+
problems.append("repro_artifacts_present")
|
|
222
|
+
|
|
223
|
+
# Blast radius: a change that broke tests it was not aiming at has failed the
|
|
224
|
+
# task even when its own verification passes. New failures are proven collateral
|
|
225
|
+
# damage; a scope that was never re-run is an unmeasured blast radius. Blocked
|
|
226
|
+
# finalizations are exempt (consistent with steps 2-5).
|
|
227
|
+
blast_radius_enabled = certificate_input.blast_radius_enabled
|
|
228
|
+
has_blast_radius_regressions = blast_radius_enabled and bool(
|
|
229
|
+
certificate_input.blast_radius_new_failures
|
|
230
|
+
)
|
|
231
|
+
if not certificate_input.blocked and blast_radius_enabled:
|
|
232
|
+
if has_blast_radius_regressions:
|
|
233
|
+
problems.append("blast_radius_regressions")
|
|
234
|
+
elif certificate_input.blast_radius_unverified:
|
|
235
|
+
problems.append("blast_radius_unverified")
|
|
236
|
+
|
|
237
|
+
# Ordering rule: post-edit execution evidence. Applies even when no named
|
|
238
|
+
# verification contract exists, catching "edited source, then only ran a
|
|
239
|
+
# syntax check, then finalized". Deduped against the block above.
|
|
240
|
+
if (
|
|
241
|
+
certificate_input.execution_evidence_required
|
|
242
|
+
and not certificate_input.post_edit_execution_evidence_present
|
|
243
|
+
):
|
|
244
|
+
if certificate_input.verification_attempt_count <= 0:
|
|
245
|
+
problems.append("verification_not_attempted")
|
|
246
|
+
else:
|
|
247
|
+
problems.append("verification_incomplete")
|
|
248
|
+
|
|
249
|
+
hard_criteria = _hard_criteria(certificate_input.contract)
|
|
250
|
+
blocker_covers_missing = certificate_input.blocked and certificate_input.blocker_valid
|
|
251
|
+
for criterion in hard_criteria:
|
|
252
|
+
if criterion.status == AcceptanceCriterionStatus.PASSED:
|
|
253
|
+
covered_hard.append(criterion.criterion_id)
|
|
254
|
+
continue
|
|
255
|
+
if criterion.status == AcceptanceCriterionStatus.FAILED:
|
|
256
|
+
failed_hard.append(criterion.criterion_id)
|
|
257
|
+
problems.append("acceptance_criteria_failed")
|
|
258
|
+
if criterion.kind == AcceptanceCriterionKind.PRESERVATION_UNCHANGED_PATH:
|
|
259
|
+
problems.append("unexpected_scope_changes")
|
|
260
|
+
continue
|
|
261
|
+
if criterion.status == AcceptanceCriterionStatus.BLOCKED:
|
|
262
|
+
if blocker_covers_missing:
|
|
263
|
+
continue
|
|
264
|
+
problems.append("acceptance_evidence_insufficient")
|
|
265
|
+
continue
|
|
266
|
+
if criterion.status == AcceptanceCriterionStatus.UNVERIFIED:
|
|
267
|
+
if blocker_covers_missing:
|
|
268
|
+
continue
|
|
269
|
+
problems.append("acceptance_criteria_unverified")
|
|
270
|
+
|
|
271
|
+
status = CompletionCertificateStatus.SUFFICIENT
|
|
272
|
+
reason = "requirements_satisfied"
|
|
273
|
+
deduped_problems = tuple(dict.fromkeys(problems))
|
|
274
|
+
repro_contradicted = bool(
|
|
275
|
+
repro_enabled
|
|
276
|
+
and certificate_input.repro_failing_after_fix
|
|
277
|
+
and "repro_unconfirmed" in deduped_problems
|
|
278
|
+
)
|
|
279
|
+
if (
|
|
280
|
+
failed_hard
|
|
281
|
+
or "verification_failed" in deduped_problems
|
|
282
|
+
or "regressions_detected" in deduped_problems
|
|
283
|
+
or "blast_radius_regressions" in deduped_problems
|
|
284
|
+
or repro_contradicted
|
|
285
|
+
):
|
|
286
|
+
status = CompletionCertificateStatus.CONTRADICTED
|
|
287
|
+
reason = "hard_requirement_failed"
|
|
288
|
+
elif deduped_problems:
|
|
289
|
+
status = CompletionCertificateStatus.INSUFFICIENT
|
|
290
|
+
reason = "requirements_missing"
|
|
291
|
+
|
|
292
|
+
return CompletionCertificate(
|
|
293
|
+
status=status,
|
|
294
|
+
problems=deduped_problems,
|
|
295
|
+
hard_criterion_ids=tuple(criterion.criterion_id for criterion in hard_criteria),
|
|
296
|
+
covered_hard_criterion_ids=tuple(covered_hard),
|
|
297
|
+
failed_hard_criterion_ids=tuple(failed_hard),
|
|
298
|
+
evidence_hierarchy=_evidence_hierarchy(certificate_input.accepted_verification_evidence),
|
|
299
|
+
reason=reason,
|
|
300
|
+
regressions=tuple(certificate_input.regressions) if regression_enabled else tuple(),
|
|
301
|
+
unattributed_failures=(
|
|
302
|
+
tuple(certificate_input.unattributed_failures) if regression_enabled else tuple()
|
|
303
|
+
),
|
|
304
|
+
pre_existing_failures=(
|
|
305
|
+
tuple(certificate_input.pre_existing_failures) if regression_enabled else tuple()
|
|
306
|
+
),
|
|
307
|
+
agent_authored_failures=(
|
|
308
|
+
tuple(certificate_input.agent_authored_failures) if regression_enabled else tuple()
|
|
309
|
+
),
|
|
310
|
+
expectations_unaddressed=(
|
|
311
|
+
tuple(certificate_input.expectations_unaddressed)
|
|
312
|
+
if certificate_input.turn_contract_v2_enabled
|
|
313
|
+
else tuple()
|
|
314
|
+
),
|
|
315
|
+
repro_status=str(certificate_input.repro_status or "") if repro_enabled else "",
|
|
316
|
+
repro_artifacts_present=(
|
|
317
|
+
tuple(certificate_input.repro_artifacts_present) if repro_enabled else tuple()
|
|
318
|
+
),
|
|
319
|
+
blast_radius_status=(
|
|
320
|
+
str(certificate_input.blast_radius_status or "") if blast_radius_enabled else ""
|
|
321
|
+
),
|
|
322
|
+
blast_radius_new_failures=(
|
|
323
|
+
tuple(certificate_input.blast_radius_new_failures) if blast_radius_enabled else tuple()
|
|
324
|
+
),
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _hard_criteria(contract: AcceptanceContract | None) -> list[AcceptanceCriterion]:
|
|
329
|
+
if contract is None:
|
|
330
|
+
return []
|
|
331
|
+
return [
|
|
332
|
+
criterion
|
|
333
|
+
for criterion in contract.criteria
|
|
334
|
+
if criterion.enforcement == AcceptanceCriterionEnforcement.HARD
|
|
335
|
+
and criterion.required
|
|
336
|
+
and criterion.required_for_finalization
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _evidence_hierarchy(evidence: list[dict[str, Any]]) -> tuple[str, ...]:
|
|
341
|
+
origins = set()
|
|
342
|
+
for item in evidence:
|
|
343
|
+
if not isinstance(item, dict):
|
|
344
|
+
continue
|
|
345
|
+
raw = str(
|
|
346
|
+
item.get("origin")
|
|
347
|
+
or item.get("evidence_origin")
|
|
348
|
+
or item.get("evidence_category")
|
|
349
|
+
or item.get("category")
|
|
350
|
+
or ""
|
|
351
|
+
)
|
|
352
|
+
if not raw:
|
|
353
|
+
continue
|
|
354
|
+
origins.add(
|
|
355
|
+
{
|
|
356
|
+
"AUTHORITATIVE": "HOST_AUTHORITATIVE",
|
|
357
|
+
"TASK_ACCEPTANCE": "DIRECT_BLACK_BOX",
|
|
358
|
+
"REPO_NATIVE": "PREEXISTING_REPO_NATIVE",
|
|
359
|
+
}.get(raw, raw)
|
|
360
|
+
)
|
|
361
|
+
return tuple(
|
|
362
|
+
sorted(
|
|
363
|
+
(origin for origin in origins if origin),
|
|
364
|
+
key=lambda origin: (_EVIDENCE_RANK.get(origin, 99), origin),
|
|
365
|
+
)
|
|
366
|
+
)
|