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,273 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
_PYTEST_FAILED_RE = re.compile(
|
|
8
|
+
r"\bFAILED\s+(?P<nodeid>(?P<path>(?:[A-Za-z]:)?[^\s:]+\.py)(?:::[^\s]+)+)(?:\s+-\s+(?P<msg>.+))?"
|
|
9
|
+
)
|
|
10
|
+
_PYTHON_FILE_RE = re.compile(
|
|
11
|
+
r'File "(?P<path>[^"]+\.py)", line (?P<line>\d+), in (?P<symbol>[^\s]+)'
|
|
12
|
+
)
|
|
13
|
+
_PYTEST_SHORT_FILE_RE = re.compile(
|
|
14
|
+
r"(?P<path>[A-Za-z0-9_./\\-]+\.py):(?P<line>\d+):(?:\s+)?(?P<msg>.+)"
|
|
15
|
+
)
|
|
16
|
+
_JS_FAIL_RE = re.compile(r"\b(?:FAIL|Failed)\s+(?P<path>[^\s]+(?:\.[cm]?[jt]sx?))")
|
|
17
|
+
_JS_FRAME_RE = re.compile(
|
|
18
|
+
r"(?:at\s+(?P<symbol>[^\s(]+)\s+\()?(?P<path>[A-Za-z0-9_./\\-]+(?:\.[cm]?[jt]sx?)):(?P<line>\d+):(?P<column>\d+)\)?"
|
|
19
|
+
)
|
|
20
|
+
_GO_FAIL_RE = re.compile(r"---\s+FAIL:\s+(?P<name>[A-Za-z0-9_/.-]+)")
|
|
21
|
+
_GO_FILE_RE = re.compile(r"(?P<path>[A-Za-z0-9_./\\-]+\.go):(?P<line>\d+):\s*(?P<msg>.+)")
|
|
22
|
+
_RUST_FAIL_RE = re.compile(r"----\s+(?P<name>[^-\n]+?)\s+stdout\s+----")
|
|
23
|
+
_RUST_PANIC_RE = re.compile(
|
|
24
|
+
r"panicked at (?P<path>[A-Za-z0-9_./\\-]+\.rs):(?P<line>\d+):(?P<column>\d+)"
|
|
25
|
+
)
|
|
26
|
+
_JAVA_FRAME_RE = re.compile(
|
|
27
|
+
r"at\s+(?P<symbol>[A-Za-z0-9_.$<>]+)\((?P<path>[A-Za-z0-9_.$/-]+\.java):(?P<line>\d+)\)"
|
|
28
|
+
)
|
|
29
|
+
_PATH_LINE_RE = re.compile(
|
|
30
|
+
r"(?P<path>[A-Za-z0-9_./\\-]+\.(?:py|js|jsx|ts|tsx|go|rs|java)):(?P<line>\d+)"
|
|
31
|
+
)
|
|
32
|
+
_EXCEPTION_SUMMARY_RE = re.compile(
|
|
33
|
+
r"^(?:[A-Za-z_][\w.]*\.)?[A-Za-z_]\w*(?:Error|Exception|Warning|Interrupt|Exit):(?:\s+.+)?$"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
_ERROR_MARKERS = (
|
|
37
|
+
"AssertionError",
|
|
38
|
+
"ModuleNotFoundError",
|
|
39
|
+
"ImportError",
|
|
40
|
+
"TypeError",
|
|
41
|
+
"ValueError",
|
|
42
|
+
"RuntimeError",
|
|
43
|
+
"SyntaxError",
|
|
44
|
+
"Error:",
|
|
45
|
+
"Exception:",
|
|
46
|
+
"panic",
|
|
47
|
+
"panicked at",
|
|
48
|
+
"FAILED ",
|
|
49
|
+
"--- FAIL:",
|
|
50
|
+
)
|
|
51
|
+
_SKIP_PATH_PARTS = {
|
|
52
|
+
".git",
|
|
53
|
+
".venv",
|
|
54
|
+
"venv",
|
|
55
|
+
"site-packages",
|
|
56
|
+
"node_modules",
|
|
57
|
+
"__pycache__",
|
|
58
|
+
"dist-packages",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def summarize_verification_failure(
|
|
63
|
+
*,
|
|
64
|
+
root: Path,
|
|
65
|
+
command: str,
|
|
66
|
+
effective_command: str,
|
|
67
|
+
output: str,
|
|
68
|
+
output_truncated: bool = False,
|
|
69
|
+
) -> dict[str, Any] | None:
|
|
70
|
+
text = str(output or "")
|
|
71
|
+
if not text.strip():
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
framework = _infer_framework(command=effective_command or command, output=text)
|
|
75
|
+
failing_tests = _extract_failing_tests(root=root, output=text, framework=framework)
|
|
76
|
+
stack_frames = _extract_stack_frames(root=root, output=text)
|
|
77
|
+
primary_error = _primary_error_line(text)
|
|
78
|
+
likely_files = _dedupe(
|
|
79
|
+
[
|
|
80
|
+
*[item.get("path", "") for item in failing_tests],
|
|
81
|
+
*[item.get("path", "") for item in stack_frames],
|
|
82
|
+
]
|
|
83
|
+
)[:10]
|
|
84
|
+
|
|
85
|
+
if not primary_error and not failing_tests and not stack_frames:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
confidence = 0.45
|
|
89
|
+
if failing_tests:
|
|
90
|
+
confidence = 0.8
|
|
91
|
+
elif stack_frames:
|
|
92
|
+
confidence = 0.65
|
|
93
|
+
elif primary_error:
|
|
94
|
+
confidence = 0.55
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
"framework": framework,
|
|
98
|
+
"primary_error": primary_error,
|
|
99
|
+
"failing_tests": failing_tests[:8],
|
|
100
|
+
"stack_frames": stack_frames[:12],
|
|
101
|
+
"likely_next_files": likely_files,
|
|
102
|
+
"output_truncated": bool(output_truncated),
|
|
103
|
+
"heuristic": True,
|
|
104
|
+
"confidence": confidence,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _infer_framework(*, command: str, output: str) -> str:
|
|
109
|
+
combined = f"{command}\n{output}".casefold()
|
|
110
|
+
if "pytest" in combined or "::test" in combined:
|
|
111
|
+
return "pytest"
|
|
112
|
+
if "vitest" in combined:
|
|
113
|
+
return "vitest"
|
|
114
|
+
if "jest" in combined or re.search(r"\bFAIL\s+.*\.(?:js|jsx|ts|tsx)\b", output):
|
|
115
|
+
return "jest"
|
|
116
|
+
if "go test" in combined or "--- fail:" in combined:
|
|
117
|
+
return "go test"
|
|
118
|
+
if "cargo test" in combined or "panicked at" in combined:
|
|
119
|
+
return "cargo test"
|
|
120
|
+
if "mvn test" in combined or "gradle" in combined or ".java:" in combined:
|
|
121
|
+
return "junit"
|
|
122
|
+
if "traceback (most recent call last)" in combined:
|
|
123
|
+
return "python"
|
|
124
|
+
return "unknown"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _extract_failing_tests(*, root: Path, output: str, framework: str) -> list[dict[str, Any]]:
|
|
128
|
+
tests: list[dict[str, Any]] = []
|
|
129
|
+
for match in _PYTEST_FAILED_RE.finditer(output):
|
|
130
|
+
path = _normalize_path(root=root, raw_path=match.group("path"))
|
|
131
|
+
if not path:
|
|
132
|
+
continue
|
|
133
|
+
nodeid = _normalize_pytest_nodeid(normalized_path=path, raw_nodeid=match.group("nodeid"))
|
|
134
|
+
item: dict[str, Any] = {
|
|
135
|
+
"id": nodeid,
|
|
136
|
+
"path": path,
|
|
137
|
+
}
|
|
138
|
+
msg = (match.group("msg") or "").strip()
|
|
139
|
+
if msg:
|
|
140
|
+
item["message"] = _clip(msg, 240)
|
|
141
|
+
tests.append(item)
|
|
142
|
+
|
|
143
|
+
if framework in {"go test", "unknown"}:
|
|
144
|
+
current_name = ""
|
|
145
|
+
for line in output.splitlines():
|
|
146
|
+
name_match = _GO_FAIL_RE.search(line)
|
|
147
|
+
if name_match:
|
|
148
|
+
current_name = name_match.group("name")
|
|
149
|
+
continue
|
|
150
|
+
file_match = _GO_FILE_RE.search(line)
|
|
151
|
+
if file_match and current_name:
|
|
152
|
+
path = _normalize_path(root=root, raw_path=file_match.group("path"))
|
|
153
|
+
if not path:
|
|
154
|
+
continue
|
|
155
|
+
tests.append(
|
|
156
|
+
{
|
|
157
|
+
"id": current_name,
|
|
158
|
+
"path": path,
|
|
159
|
+
"line": int(file_match.group("line")),
|
|
160
|
+
"message": _clip(file_match.group("msg").strip(), 240),
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if framework in {"cargo test", "unknown"}:
|
|
165
|
+
for match in _RUST_FAIL_RE.finditer(output):
|
|
166
|
+
name = " ".join(match.group("name").split())
|
|
167
|
+
if name:
|
|
168
|
+
tests.append({"id": name})
|
|
169
|
+
|
|
170
|
+
return _dedupe_dicts(tests)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _extract_stack_frames(*, root: Path, output: str) -> list[dict[str, Any]]:
|
|
174
|
+
frames: list[dict[str, Any]] = []
|
|
175
|
+
for regex in (_PYTHON_FILE_RE, _JS_FRAME_RE, _GO_FILE_RE, _RUST_PANIC_RE, _JAVA_FRAME_RE):
|
|
176
|
+
for match in regex.finditer(output):
|
|
177
|
+
path = _normalize_path(root=root, raw_path=match.group("path"))
|
|
178
|
+
if not path:
|
|
179
|
+
continue
|
|
180
|
+
frame: dict[str, Any] = {
|
|
181
|
+
"path": path,
|
|
182
|
+
"line": int(match.group("line")),
|
|
183
|
+
}
|
|
184
|
+
symbol = match.groupdict().get("symbol")
|
|
185
|
+
if symbol:
|
|
186
|
+
frame["symbol"] = symbol
|
|
187
|
+
frames.append(frame)
|
|
188
|
+
|
|
189
|
+
for match in _PATH_LINE_RE.finditer(output):
|
|
190
|
+
path = _normalize_path(root=root, raw_path=match.group("path"))
|
|
191
|
+
if not path:
|
|
192
|
+
continue
|
|
193
|
+
frames.append({"path": path, "line": int(match.group("line"))})
|
|
194
|
+
|
|
195
|
+
return _dedupe_dicts(frames)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _primary_error_line(output: str) -> str:
|
|
199
|
+
lines = [" ".join(line.strip().split()) for line in output.splitlines() if line.strip()]
|
|
200
|
+
for line in lines:
|
|
201
|
+
if line.startswith("E "):
|
|
202
|
+
return _clip(line[2:].strip(), 320)
|
|
203
|
+
for line in reversed(lines):
|
|
204
|
+
if _EXCEPTION_SUMMARY_RE.match(line):
|
|
205
|
+
return _clip(line, 320)
|
|
206
|
+
for marker in _ERROR_MARKERS:
|
|
207
|
+
for line in lines:
|
|
208
|
+
if marker == "FAILED " and " - " in line:
|
|
209
|
+
continue
|
|
210
|
+
if line.startswith(("raise ", "assert ")):
|
|
211
|
+
continue
|
|
212
|
+
if marker in line:
|
|
213
|
+
return _clip(line, 320)
|
|
214
|
+
for line in lines:
|
|
215
|
+
lowered = line.casefold()
|
|
216
|
+
if "error" in lowered or "failed" in lowered or "exception" in lowered:
|
|
217
|
+
return _clip(line, 320)
|
|
218
|
+
return ""
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _normalize_path(*, root: Path, raw_path: str) -> str:
|
|
222
|
+
clean = str(raw_path or "").strip().strip("'\"")
|
|
223
|
+
if not clean:
|
|
224
|
+
return ""
|
|
225
|
+
clean = clean.replace("\\", "/")
|
|
226
|
+
parts = {part for part in clean.split("/") if part}
|
|
227
|
+
if parts & _SKIP_PATH_PARTS:
|
|
228
|
+
return ""
|
|
229
|
+
|
|
230
|
+
path_obj = Path(clean)
|
|
231
|
+
root_abs = root.resolve()
|
|
232
|
+
try:
|
|
233
|
+
resolved = path_obj.resolve() if path_obj.is_absolute() else (root_abs / path_obj).resolve()
|
|
234
|
+
relative = resolved.relative_to(root_abs).as_posix()
|
|
235
|
+
except (OSError, ValueError):
|
|
236
|
+
return ""
|
|
237
|
+
return "" if relative == "." else relative
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _normalize_pytest_nodeid(*, normalized_path: str, raw_nodeid: str) -> str:
|
|
241
|
+
_path_part, sep, suffix = str(raw_nodeid or "").strip().partition("::")
|
|
242
|
+
return f"{normalized_path}{sep}{suffix}" if sep else normalized_path
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _clip(text: str, limit: int) -> str:
|
|
246
|
+
clean = str(text or "").strip()
|
|
247
|
+
if len(clean) <= limit:
|
|
248
|
+
return clean
|
|
249
|
+
return clean[: limit - 3].rstrip() + "..."
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _dedupe(values: list[str]) -> list[str]:
|
|
253
|
+
seen: set[str] = set()
|
|
254
|
+
deduped: list[str] = []
|
|
255
|
+
for value in values:
|
|
256
|
+
clean = str(value or "").strip()
|
|
257
|
+
if not clean or clean in seen:
|
|
258
|
+
continue
|
|
259
|
+
seen.add(clean)
|
|
260
|
+
deduped.append(clean)
|
|
261
|
+
return deduped
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _dedupe_dicts(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
265
|
+
seen: set[tuple[tuple[str, str], ...]] = set()
|
|
266
|
+
deduped: list[dict[str, Any]] = []
|
|
267
|
+
for item in items:
|
|
268
|
+
key = tuple(sorted((str(k), str(v)) for k, v in item.items()))
|
|
269
|
+
if key in seen:
|
|
270
|
+
continue
|
|
271
|
+
seen.add(key)
|
|
272
|
+
deduped.append(item)
|
|
273
|
+
return deduped
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""Verification outcomes that never throw away work.
|
|
2
|
+
|
|
3
|
+
Two separate things used to collapse into the same "task failed" verdict:
|
|
4
|
+
|
|
5
|
+
* **Verification could not run.** No authoritative command exists for the task --
|
|
6
|
+
``verify_gate`` deliberately suppresses generic fallbacks and empties the command
|
|
7
|
+
list for docs-only, static-web, and CI-only workspaces. The files were written and
|
|
8
|
+
are on disk, but strict mode reported a plain failure, and the swarm's failure
|
|
9
|
+
cleanup then deleted the worktree. Missing tooling is not a defect in the work, so
|
|
10
|
+
it now produces :data:`TASK_STATUS_COMPLETED_UNVERIFIED` instead.
|
|
11
|
+
* **Verification ran and failed.** That *is* a real signal about the work, but a
|
|
12
|
+
single shot at it is a waste: the failing command's own output is usually enough
|
|
13
|
+
for the executing agent to fix the problem. :func:`run_verification_repair_loop`
|
|
14
|
+
gives it a bounded number of tries before the task is failed.
|
|
15
|
+
|
|
16
|
+
This module is deliberately a stdlib-only leaf: it holds the policy (how many
|
|
17
|
+
attempts, when to stop, what the repair prompt says) while the caller keeps every
|
|
18
|
+
side effect (running the agent, committing, re-running verification). That split is
|
|
19
|
+
what makes the loop unit-testable without a git repository or a provider.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
from collections.abc import Callable, Sequence
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import Any, Protocol
|
|
28
|
+
|
|
29
|
+
# Plan status for "the work landed, but nothing authoritative could check it".
|
|
30
|
+
# Not a failure: the task is terminal-successful for progress and dependency
|
|
31
|
+
# purposes, and its files/branch are kept.
|
|
32
|
+
TASK_STATUS_COMPLETED_UNVERIFIED = "completed_unverified"
|
|
33
|
+
|
|
34
|
+
DEFAULT_VERIFICATION_REPAIR_ATTEMPTS = 2
|
|
35
|
+
VERIFICATION_REPAIR_ATTEMPTS_ENV = "ALYSIS_VERIFY_REPAIR_ATTEMPTS"
|
|
36
|
+
|
|
37
|
+
# A ceiling, not a policy: a mistyped budget should not turn one task into an
|
|
38
|
+
# unbounded provider spend loop.
|
|
39
|
+
MAX_VERIFICATION_REPAIR_ATTEMPTS = 10
|
|
40
|
+
|
|
41
|
+
# Per-command excerpt budget for the repair prompt. Enough to carry a traceback and
|
|
42
|
+
# the assertion that produced it; small enough that three failing commands still fit
|
|
43
|
+
# beside the task instruction.
|
|
44
|
+
DEFAULT_COMMAND_OUTPUT_CHARS = 4000
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _VerifyCommandResultLike(Protocol):
|
|
48
|
+
command: str
|
|
49
|
+
exit_code: int
|
|
50
|
+
output: str
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def ok(self) -> bool: ...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class _VerifyRunResultLike(Protocol):
|
|
57
|
+
command_results: list[Any]
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def all_passed(self) -> bool: ...
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def summary(self) -> str: ...
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def failed_commands(self) -> list[str]: ...
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def resolve_repair_attempt_budget(
|
|
70
|
+
override: int | None = None,
|
|
71
|
+
*,
|
|
72
|
+
env: dict[str, str] | None = None,
|
|
73
|
+
) -> int:
|
|
74
|
+
"""Return how many repair attempts a failing verification gets.
|
|
75
|
+
|
|
76
|
+
Precedence: explicit override (CLI flag) > environment > default. Negative and
|
|
77
|
+
unparseable values fall back rather than raise -- a bad budget must not be the
|
|
78
|
+
reason a task cannot run at all.
|
|
79
|
+
"""
|
|
80
|
+
if override is not None:
|
|
81
|
+
return _clamp_attempts(override)
|
|
82
|
+
raw = (env if env is not None else os.environ).get(VERIFICATION_REPAIR_ATTEMPTS_ENV)
|
|
83
|
+
if raw is not None and str(raw).strip():
|
|
84
|
+
try:
|
|
85
|
+
return _clamp_attempts(int(str(raw).strip()))
|
|
86
|
+
except (TypeError, ValueError):
|
|
87
|
+
return DEFAULT_VERIFICATION_REPAIR_ATTEMPTS
|
|
88
|
+
return DEFAULT_VERIFICATION_REPAIR_ATTEMPTS
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _clamp_attempts(value: int) -> int:
|
|
92
|
+
try:
|
|
93
|
+
parsed = int(value)
|
|
94
|
+
except (TypeError, ValueError):
|
|
95
|
+
return DEFAULT_VERIFICATION_REPAIR_ATTEMPTS
|
|
96
|
+
if parsed < 0:
|
|
97
|
+
return 0
|
|
98
|
+
return min(parsed, MAX_VERIFICATION_REPAIR_ATTEMPTS)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True)
|
|
102
|
+
class RepairAttemptExecution:
|
|
103
|
+
"""What the caller actually did for one repair attempt.
|
|
104
|
+
|
|
105
|
+
``verify_result`` is the re-run verification, or ``None`` when the attempt could
|
|
106
|
+
not get far enough to re-verify (agent crash, commit failure).
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
agent_exit_code: int
|
|
110
|
+
verify_result: Any | None
|
|
111
|
+
committed: bool = False
|
|
112
|
+
changed_files: tuple[str, ...] = ()
|
|
113
|
+
error: str | None = None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class VerificationRepairAttempt:
|
|
118
|
+
"""One recorded attempt, for the report, the event stream, and the artifact."""
|
|
119
|
+
|
|
120
|
+
attempt: int
|
|
121
|
+
agent_exit_code: int
|
|
122
|
+
committed: bool
|
|
123
|
+
verification_passed: bool
|
|
124
|
+
verification_summary: str
|
|
125
|
+
failed_commands: tuple[str, ...] = ()
|
|
126
|
+
changed_files: tuple[str, ...] = ()
|
|
127
|
+
error: str | None = None
|
|
128
|
+
|
|
129
|
+
def to_payload(self) -> dict[str, Any]:
|
|
130
|
+
return {
|
|
131
|
+
"attempt": self.attempt,
|
|
132
|
+
"agent_exit_code": self.agent_exit_code,
|
|
133
|
+
"committed": bool(self.committed),
|
|
134
|
+
"verification_passed": bool(self.verification_passed),
|
|
135
|
+
"verification_summary": self.verification_summary,
|
|
136
|
+
"failed_commands": list(self.failed_commands),
|
|
137
|
+
"changed_files": list(self.changed_files),
|
|
138
|
+
"error": self.error,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True)
|
|
143
|
+
class VerificationRepairOutcome:
|
|
144
|
+
"""Result of the whole loop.
|
|
145
|
+
|
|
146
|
+
``final_result`` is the verification result the task's outcome is decided on:
|
|
147
|
+
the last one that actually ran, which is the initial result when no attempt
|
|
148
|
+
re-verified.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
final_result: Any
|
|
152
|
+
attempts: tuple[VerificationRepairAttempt, ...] = ()
|
|
153
|
+
skipped_reason: str | None = None
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def passed(self) -> bool:
|
|
157
|
+
result = self.final_result
|
|
158
|
+
return bool(getattr(result, "all_passed", False))
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def attempts_used(self) -> int:
|
|
162
|
+
return len(self.attempts)
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def repaired(self) -> bool:
|
|
166
|
+
"""True when a repair attempt is what turned verification green."""
|
|
167
|
+
return self.passed and bool(self.attempts)
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def exhausted(self) -> bool:
|
|
171
|
+
"""True when attempts were spent and verification still fails."""
|
|
172
|
+
return bool(self.attempts) and not self.passed
|
|
173
|
+
|
|
174
|
+
def to_payload(self) -> dict[str, Any]:
|
|
175
|
+
return {
|
|
176
|
+
"attempts_used": self.attempts_used,
|
|
177
|
+
"passed": self.passed,
|
|
178
|
+
"repaired": self.repaired,
|
|
179
|
+
"exhausted": self.exhausted,
|
|
180
|
+
"skipped_reason": self.skipped_reason,
|
|
181
|
+
"attempts": [item.to_payload() for item in self.attempts],
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
def report_lines(self) -> list[str]:
|
|
185
|
+
"""Human-readable lines for the task report / warning list."""
|
|
186
|
+
if self.skipped_reason and not self.attempts:
|
|
187
|
+
return [f"Verification repair skipped: {self.skipped_reason}"]
|
|
188
|
+
lines: list[str] = []
|
|
189
|
+
for item in self.attempts:
|
|
190
|
+
state = "passed" if item.verification_passed else "still failing"
|
|
191
|
+
line = f"Repair attempt {item.attempt}: {state} -- {item.verification_summary}"
|
|
192
|
+
if item.error:
|
|
193
|
+
line += f" (attempt error: {item.error})"
|
|
194
|
+
lines.append(line)
|
|
195
|
+
if self.skipped_reason:
|
|
196
|
+
lines.append(f"Verification repair stopped: {self.skipped_reason}")
|
|
197
|
+
return lines
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def run_verification_repair_loop(
|
|
201
|
+
*,
|
|
202
|
+
initial_result: Any,
|
|
203
|
+
max_attempts: int,
|
|
204
|
+
attempt_repair: Callable[[int, Any], RepairAttemptExecution],
|
|
205
|
+
repairable: Callable[[Any], bool] | None = None,
|
|
206
|
+
on_attempt: Callable[[VerificationRepairAttempt], None] | None = None,
|
|
207
|
+
) -> VerificationRepairOutcome:
|
|
208
|
+
"""Re-run the executing agent against its own failing verification output.
|
|
209
|
+
|
|
210
|
+
The caller's ``attempt_repair(attempt_number, failing_result)`` does the work --
|
|
211
|
+
build the prompt, run the agent, commit, re-verify -- and returns what happened.
|
|
212
|
+
This function owns only the decision to keep going.
|
|
213
|
+
|
|
214
|
+
An attempt that cannot re-verify (agent crashed, nothing committed, commit
|
|
215
|
+
failed) ends the loop: without a fresh verification result there is nothing new
|
|
216
|
+
to feed the next attempt, so spending the remaining budget would just repeat the
|
|
217
|
+
same prompt.
|
|
218
|
+
"""
|
|
219
|
+
budget = _clamp_attempts(max_attempts)
|
|
220
|
+
if getattr(initial_result, "all_passed", False):
|
|
221
|
+
return VerificationRepairOutcome(final_result=initial_result)
|
|
222
|
+
if budget <= 0:
|
|
223
|
+
return VerificationRepairOutcome(
|
|
224
|
+
final_result=initial_result,
|
|
225
|
+
skipped_reason="no repair attempts are configured",
|
|
226
|
+
)
|
|
227
|
+
if repairable is not None and not repairable(initial_result):
|
|
228
|
+
return VerificationRepairOutcome(
|
|
229
|
+
final_result=initial_result,
|
|
230
|
+
skipped_reason="the verification failure is not repairable by editing code",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
attempts: list[VerificationRepairAttempt] = []
|
|
234
|
+
current = initial_result
|
|
235
|
+
skipped_reason: str | None = None
|
|
236
|
+
|
|
237
|
+
for attempt_number in range(1, budget + 1):
|
|
238
|
+
execution = attempt_repair(attempt_number, current)
|
|
239
|
+
result = execution.verify_result
|
|
240
|
+
passed = bool(getattr(result, "all_passed", False)) if result is not None else False
|
|
241
|
+
record = VerificationRepairAttempt(
|
|
242
|
+
attempt=attempt_number,
|
|
243
|
+
agent_exit_code=int(execution.agent_exit_code),
|
|
244
|
+
committed=bool(execution.committed),
|
|
245
|
+
verification_passed=passed,
|
|
246
|
+
verification_summary=(
|
|
247
|
+
str(getattr(result, "summary", ""))
|
|
248
|
+
if result is not None
|
|
249
|
+
else "verification did not re-run"
|
|
250
|
+
),
|
|
251
|
+
failed_commands=tuple(
|
|
252
|
+
str(item) for item in (getattr(result, "failed_commands", ()) or ())
|
|
253
|
+
),
|
|
254
|
+
changed_files=tuple(execution.changed_files),
|
|
255
|
+
error=execution.error,
|
|
256
|
+
)
|
|
257
|
+
attempts.append(record)
|
|
258
|
+
if on_attempt is not None:
|
|
259
|
+
on_attempt(record)
|
|
260
|
+
if result is not None:
|
|
261
|
+
current = result
|
|
262
|
+
if passed:
|
|
263
|
+
break
|
|
264
|
+
if result is None:
|
|
265
|
+
skipped_reason = execution.error or "the repair attempt did not re-run verification"
|
|
266
|
+
break
|
|
267
|
+
if repairable is not None and not repairable(result):
|
|
268
|
+
skipped_reason = "the verification failure is not repairable by editing code"
|
|
269
|
+
break
|
|
270
|
+
|
|
271
|
+
return VerificationRepairOutcome(
|
|
272
|
+
final_result=current,
|
|
273
|
+
attempts=tuple(attempts),
|
|
274
|
+
skipped_reason=skipped_reason,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def verification_failure_excerpts(
|
|
279
|
+
result: Any,
|
|
280
|
+
*,
|
|
281
|
+
max_chars: int = DEFAULT_COMMAND_OUTPUT_CHARS,
|
|
282
|
+
max_commands: int = 5,
|
|
283
|
+
) -> list[tuple[str, int, str]]:
|
|
284
|
+
"""Return ``(command, exit_code, output excerpt)`` for each failing command.
|
|
285
|
+
|
|
286
|
+
The tail of the output is kept, not the head: a test runner puts the failure
|
|
287
|
+
summary last, and the first lines are usually collection noise.
|
|
288
|
+
"""
|
|
289
|
+
excerpts: list[tuple[str, int, str]] = []
|
|
290
|
+
for item in list(getattr(result, "command_results", None) or []):
|
|
291
|
+
if getattr(item, "ok", False):
|
|
292
|
+
continue
|
|
293
|
+
command = str(getattr(item, "command", "") or "")
|
|
294
|
+
exit_code = int(getattr(item, "exit_code", 1) or 0)
|
|
295
|
+
excerpts.append(
|
|
296
|
+
(command, exit_code, _tail(str(getattr(item, "output", "") or ""), max_chars))
|
|
297
|
+
)
|
|
298
|
+
if len(excerpts) >= max_commands:
|
|
299
|
+
break
|
|
300
|
+
return excerpts
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _tail(text: str, max_chars: int) -> str:
|
|
304
|
+
if max_chars <= 0 or len(text) <= max_chars:
|
|
305
|
+
return text
|
|
306
|
+
return "[... earlier output truncated ...]\n" + text[-max_chars:]
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def build_repair_instruction(
|
|
310
|
+
*,
|
|
311
|
+
base_instruction: str,
|
|
312
|
+
task_id: str,
|
|
313
|
+
attempt: int,
|
|
314
|
+
max_attempts: int,
|
|
315
|
+
verify_summary: str,
|
|
316
|
+
excerpts: Sequence[tuple[str, int, str]],
|
|
317
|
+
artifact_path: str | None = None,
|
|
318
|
+
) -> str:
|
|
319
|
+
"""Append the failing verification output to the task's own instruction.
|
|
320
|
+
|
|
321
|
+
The base instruction is repeated verbatim because a repair attempt runs in a
|
|
322
|
+
fresh session: without it the agent would be asked to fix a failure for a task
|
|
323
|
+
it has no context on.
|
|
324
|
+
"""
|
|
325
|
+
lines: list[str] = [
|
|
326
|
+
"# Verification Repair Attempt",
|
|
327
|
+
"",
|
|
328
|
+
f"Task: {task_id}",
|
|
329
|
+
f"Repair attempt {attempt} of {max_attempts}.",
|
|
330
|
+
"",
|
|
331
|
+
"Your previous changes for this task are already committed, but the "
|
|
332
|
+
"authoritative verification commands below FAILED against them.",
|
|
333
|
+
"",
|
|
334
|
+
f"Verification summary: {verify_summary}",
|
|
335
|
+
"",
|
|
336
|
+
"## Failing verification output",
|
|
337
|
+
"",
|
|
338
|
+
]
|
|
339
|
+
if not excerpts:
|
|
340
|
+
lines.append("(no per-command output was captured)")
|
|
341
|
+
lines.append("")
|
|
342
|
+
for command, exit_code, output in excerpts:
|
|
343
|
+
lines.append(f"### `{command}` (exit code {exit_code})")
|
|
344
|
+
lines.append("")
|
|
345
|
+
lines.append("```")
|
|
346
|
+
lines.append(output.rstrip("\n") if output.strip() else "(no output captured)")
|
|
347
|
+
lines.append("```")
|
|
348
|
+
lines.append("")
|
|
349
|
+
if artifact_path:
|
|
350
|
+
lines.append(f"Full verification log: {artifact_path}")
|
|
351
|
+
lines.append("")
|
|
352
|
+
lines.extend(
|
|
353
|
+
[
|
|
354
|
+
"## What to do now",
|
|
355
|
+
"",
|
|
356
|
+
"1. Read the failing output above and find the actual cause.",
|
|
357
|
+
"2. Fix it with the smallest correct change, inside this task's write scope.",
|
|
358
|
+
"3. Do not weaken, skip, or delete the failing checks to make them pass.",
|
|
359
|
+
"4. Do not re-do work that already succeeded; only repair what the output shows.",
|
|
360
|
+
"",
|
|
361
|
+
"Verification will be re-run automatically after this attempt.",
|
|
362
|
+
"",
|
|
363
|
+
"---",
|
|
364
|
+
"",
|
|
365
|
+
"# Original Task",
|
|
366
|
+
"",
|
|
367
|
+
]
|
|
368
|
+
)
|
|
369
|
+
return "\n".join(lines) + base_instruction
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
__all__ = [
|
|
373
|
+
"DEFAULT_COMMAND_OUTPUT_CHARS",
|
|
374
|
+
"DEFAULT_VERIFICATION_REPAIR_ATTEMPTS",
|
|
375
|
+
"MAX_VERIFICATION_REPAIR_ATTEMPTS",
|
|
376
|
+
"TASK_STATUS_COMPLETED_UNVERIFIED",
|
|
377
|
+
"VERIFICATION_REPAIR_ATTEMPTS_ENV",
|
|
378
|
+
"RepairAttemptExecution",
|
|
379
|
+
"VerificationRepairAttempt",
|
|
380
|
+
"VerificationRepairOutcome",
|
|
381
|
+
"build_repair_instruction",
|
|
382
|
+
"resolve_repair_attempt_budget",
|
|
383
|
+
"run_verification_repair_loop",
|
|
384
|
+
"verification_failure_excerpts",
|
|
385
|
+
]
|