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,2119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
import tarfile
|
|
8
|
+
import tempfile
|
|
9
|
+
import threading
|
|
10
|
+
import traceback
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from dataclasses import dataclass, replace
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
|
|
18
|
+
from .agent_loop import run_agent
|
|
19
|
+
from .assets import AssetError
|
|
20
|
+
from .assets.budget_allocator import (
|
|
21
|
+
TaskAssetAllocation,
|
|
22
|
+
allocate_task_assets,
|
|
23
|
+
write_task_asset_allocation,
|
|
24
|
+
)
|
|
25
|
+
from .assets.surface import build_asset_surface
|
|
26
|
+
from .assets.usage_logger import AssetUsageLogger
|
|
27
|
+
from .assets.worker_mirror import TaskAssetMirror, mirror_task_assets
|
|
28
|
+
from .assets.worker_section import render_relevant_assets_section
|
|
29
|
+
from .assets.worker_tools import build_worker_asset_mcp_manager, compose_worker_asset_mcp_manager
|
|
30
|
+
from .cancellation import CooperativeCancellationError, EventCancellationToken
|
|
31
|
+
from .config import AppConfig, clone_cfg
|
|
32
|
+
from .error_text import (
|
|
33
|
+
redact_sensitive_error_text,
|
|
34
|
+
sanitize_error_summary,
|
|
35
|
+
sanitize_optional_error_summary,
|
|
36
|
+
)
|
|
37
|
+
from .execution_shared import (
|
|
38
|
+
build_execution_reporting_diff_with_commit_range,
|
|
39
|
+
build_task_execution_instruction_bundle,
|
|
40
|
+
build_workspace_snapshot_reporting_diff,
|
|
41
|
+
mirror_plan_into_worktree,
|
|
42
|
+
mirror_selected_knowledge_into_worktree,
|
|
43
|
+
prepare_task_execution_knowledge,
|
|
44
|
+
resolve_managed_task_step_budget,
|
|
45
|
+
safe_task_file_component,
|
|
46
|
+
snapshot_runtime_tree,
|
|
47
|
+
snapshot_workspace_tree,
|
|
48
|
+
write_exec_log_artifacts,
|
|
49
|
+
write_execution_budget_artifact,
|
|
50
|
+
write_execution_context_artifact,
|
|
51
|
+
)
|
|
52
|
+
from .failure_category import (
|
|
53
|
+
FailureCategory,
|
|
54
|
+
classify_failure_category,
|
|
55
|
+
failure_category_value,
|
|
56
|
+
)
|
|
57
|
+
from .forge import RunPaths, ensure_execution_dirs, now_iso, write_task_report
|
|
58
|
+
from .git_ops import (
|
|
59
|
+
GitOpsError,
|
|
60
|
+
added_files_since,
|
|
61
|
+
changed_files_between,
|
|
62
|
+
changed_files_since,
|
|
63
|
+
commit_all,
|
|
64
|
+
ensure_not_staged_paths,
|
|
65
|
+
ensure_not_staged_prefixes,
|
|
66
|
+
ensure_not_staged_runtime_artifacts,
|
|
67
|
+
ensure_runtime_artifact_excludes,
|
|
68
|
+
format_patch_stdout,
|
|
69
|
+
head_commit,
|
|
70
|
+
reset_mixed,
|
|
71
|
+
stage_all,
|
|
72
|
+
staged_files,
|
|
73
|
+
unstage_staged_paths,
|
|
74
|
+
unstage_staged_prefixes,
|
|
75
|
+
unstage_staged_runtime_artifacts,
|
|
76
|
+
)
|
|
77
|
+
from .knowledge_base import rebuild_knowledge_index, write_issue_entry, write_task_attempt_entry
|
|
78
|
+
from .knowledge_capture import (
|
|
79
|
+
RecordingSurface,
|
|
80
|
+
mark_knowledge_capture_promotion_skipped,
|
|
81
|
+
persist_execution_knowledge_capture,
|
|
82
|
+
)
|
|
83
|
+
from .mcp.manager import create_mcp_manager
|
|
84
|
+
from .model_registry import ModelRegistry
|
|
85
|
+
from .model_router import ROLE_CODING, resolve_model_for_role
|
|
86
|
+
from .repo_scan import RepoScanResult, scan_workspace
|
|
87
|
+
from .runtime_artifacts import has_grounded_rust_target_runtime_artifacts
|
|
88
|
+
from .runtime_kind import RuntimeKind
|
|
89
|
+
from .surface.console import make_console
|
|
90
|
+
from .swarm_trace import (
|
|
91
|
+
NoopSwarmTraceSink,
|
|
92
|
+
SwarmTraceSink,
|
|
93
|
+
SwarmWorkerTraceSurface,
|
|
94
|
+
build_swarm_trace_event,
|
|
95
|
+
)
|
|
96
|
+
from .swarm_write_guard import SwarmWriteScopeGuard
|
|
97
|
+
from .task_readiness import TASK_KIND_ANALYSIS_ONLY, classify_task_lifecycle
|
|
98
|
+
from .task_scope import (
|
|
99
|
+
SCRATCH_ARTIFACT_ENV,
|
|
100
|
+
ScopeViolationDiagnostic,
|
|
101
|
+
assess_scope_changes,
|
|
102
|
+
is_non_material_untracked_path,
|
|
103
|
+
list_changed_files_including_untracked,
|
|
104
|
+
list_untracked_packaging_metadata_paths,
|
|
105
|
+
normalize_scope_patterns,
|
|
106
|
+
relocate_known_scratch_artifacts,
|
|
107
|
+
)
|
|
108
|
+
from .verify_gate import (
|
|
109
|
+
ResolvedVerifyCommands,
|
|
110
|
+
VerifyRunResult,
|
|
111
|
+
resolve_authoritative_task_verify_command_selection,
|
|
112
|
+
run_task_verification,
|
|
113
|
+
verify_run_result_to_payload,
|
|
114
|
+
)
|
|
115
|
+
from .workspace_context import WorkspaceContextError, resolve_workspace_context
|
|
116
|
+
|
|
117
|
+
_DIAGNOSTIC_TASK_TITLE_RE = re.compile(
|
|
118
|
+
r"^\s*(?:explore|investigate|examine|inspect|locate|find|survey|map|analy[sz]e|diagnose|audit|understand|identify)\b",
|
|
119
|
+
re.IGNORECASE,
|
|
120
|
+
)
|
|
121
|
+
_MUTATING_TASK_TEXT_RE = re.compile(
|
|
122
|
+
r"\b(?:fix|implement|update|add|change|modify|create|delete|remove|refactor|write)\b",
|
|
123
|
+
re.IGNORECASE,
|
|
124
|
+
)
|
|
125
|
+
_CONDITIONAL_NOOP_TASK_RE = re.compile(
|
|
126
|
+
r"\b(?:if|when)\b.{0,100}\b(?:exists?|present|available|needed|applicable)\b"
|
|
127
|
+
r"|\bif\s+needed\b|\bwhere\s+applicable\b",
|
|
128
|
+
re.IGNORECASE | re.DOTALL,
|
|
129
|
+
)
|
|
130
|
+
_VERIFY_FAILED_TEST_RE = re.compile(r"^\s*(?:FAILED|ERROR)\s+([^\s]+)", re.MULTILINE)
|
|
131
|
+
_VERIFY_FAILURE_SECTION_RE = re.compile(r"^_{3,}\s+(.+?)\s+_{3,}\s*$", re.MULTILINE)
|
|
132
|
+
_VERIFY_ACTIONABLE_FAILURE_RE = re.compile(
|
|
133
|
+
r"^\s*(?:E\s+|AssertionError|ImportError|ModuleNotFoundError|ValueError|TypeError|"
|
|
134
|
+
r"SyntaxError|NameError|AttributeError)(.+)?$",
|
|
135
|
+
re.MULTILINE,
|
|
136
|
+
)
|
|
137
|
+
_KNOWLEDGE_INDEX_REBUILD_LOCK = threading.Lock()
|
|
138
|
+
_BASELINE_FAILURE_TOKEN_RE = re.compile(r"[A-Za-z][A-Za-z0-9_]{2,}")
|
|
139
|
+
_BASELINE_FAILURE_STOPWORDS = {
|
|
140
|
+
"assert",
|
|
141
|
+
"assertion",
|
|
142
|
+
"command",
|
|
143
|
+
"error",
|
|
144
|
+
"expected",
|
|
145
|
+
"failed",
|
|
146
|
+
"failure",
|
|
147
|
+
"format",
|
|
148
|
+
"formatting",
|
|
149
|
+
"function",
|
|
150
|
+
"got",
|
|
151
|
+
"line",
|
|
152
|
+
"module",
|
|
153
|
+
"pytest",
|
|
154
|
+
"render",
|
|
155
|
+
"render_value",
|
|
156
|
+
"return",
|
|
157
|
+
"returns",
|
|
158
|
+
"style",
|
|
159
|
+
"test",
|
|
160
|
+
"tests",
|
|
161
|
+
"unknown",
|
|
162
|
+
"value",
|
|
163
|
+
"values",
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class _HostManagedApprovalSurface:
|
|
168
|
+
"""Route worker approval requests to the owning IDE session."""
|
|
169
|
+
|
|
170
|
+
host_managed_approvals = True
|
|
171
|
+
|
|
172
|
+
def __init__(self, delegate: Any, *, task_id: str, handler: Callable[..., Any]) -> None:
|
|
173
|
+
self._delegate = delegate
|
|
174
|
+
self._task_id = task_id
|
|
175
|
+
self._handler = handler
|
|
176
|
+
|
|
177
|
+
def request_approval(self, request: Any) -> Any:
|
|
178
|
+
return self._handler(task_id=self._task_id, request=request)
|
|
179
|
+
|
|
180
|
+
def __getattr__(self, name: str) -> Any:
|
|
181
|
+
return getattr(self._delegate, name)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass(frozen=True)
|
|
185
|
+
class TaskWorkerResult:
|
|
186
|
+
task_id: str
|
|
187
|
+
title: str
|
|
188
|
+
branch: str
|
|
189
|
+
worktree_path: str
|
|
190
|
+
started_at: str
|
|
191
|
+
finished_at: str
|
|
192
|
+
success: bool
|
|
193
|
+
summary: str
|
|
194
|
+
commit_hash: str | None
|
|
195
|
+
error: str | None
|
|
196
|
+
report_path: str
|
|
197
|
+
patch_path: str
|
|
198
|
+
log_path: str
|
|
199
|
+
log_pointer_path: str
|
|
200
|
+
warnings: list[str]
|
|
201
|
+
changed_files: list[str]
|
|
202
|
+
verify_failed: bool
|
|
203
|
+
verify_summary: str | None
|
|
204
|
+
verify_artifact_path: str | None
|
|
205
|
+
# True when the task succeeded but nothing authoritative could check it. A
|
|
206
|
+
# completion, not a failure -- the orchestrator keeps the work and merges nothing.
|
|
207
|
+
verification_unavailable: bool = False
|
|
208
|
+
knowledge_capture_artifact_dir: str | None = None
|
|
209
|
+
task_attempt_entry_id: str | None = None
|
|
210
|
+
task_attempt_knowledge_file_path: str | None = None
|
|
211
|
+
verify_payload: dict[str, Any] | None = None
|
|
212
|
+
verify_command_source: str | None = None
|
|
213
|
+
failure_reason: str | None = None
|
|
214
|
+
failure_category: FailureCategory | str | None = None
|
|
215
|
+
scope_violation_files: list[str] | None = None
|
|
216
|
+
scope_diagnostics: list[dict[str, Any]] | None = None
|
|
217
|
+
allowed_scope: list[str] | None = None
|
|
218
|
+
agent_exit_code: int = 0
|
|
219
|
+
salvaged_nonzero_exit: bool = False
|
|
220
|
+
salvaged_agent_exception: bool = False
|
|
221
|
+
agent_exception_summary: str | None = None
|
|
222
|
+
result_kind: str | None = None
|
|
223
|
+
noop_reason: str | None = None
|
|
224
|
+
task_kind: str | None = None
|
|
225
|
+
task_lifecycle_reason: str | None = None
|
|
226
|
+
interrupted: bool = False
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def effective_result_kind(self) -> str:
|
|
230
|
+
if self.result_kind:
|
|
231
|
+
return self.result_kind
|
|
232
|
+
if self.success:
|
|
233
|
+
return "success_commit"
|
|
234
|
+
return "failure"
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def noop_success(self) -> bool:
|
|
238
|
+
return self.effective_result_kind == "success_noop"
|
|
239
|
+
|
|
240
|
+
def to_json(self) -> dict[str, Any]:
|
|
241
|
+
clean_agent_exception_summary = sanitize_optional_error_summary(
|
|
242
|
+
self.agent_exception_summary
|
|
243
|
+
)
|
|
244
|
+
return {
|
|
245
|
+
"task_id": self.task_id,
|
|
246
|
+
"title": self.title,
|
|
247
|
+
"branch": self.branch,
|
|
248
|
+
"worktree_path": self.worktree_path,
|
|
249
|
+
"started_at": self.started_at,
|
|
250
|
+
"finished_at": self.finished_at,
|
|
251
|
+
"success": self.success,
|
|
252
|
+
"summary": self.summary,
|
|
253
|
+
"commit_hash": self.commit_hash,
|
|
254
|
+
"error": self.error,
|
|
255
|
+
"report_path": self.report_path,
|
|
256
|
+
"patch_path": self.patch_path,
|
|
257
|
+
"log_path": self.log_path,
|
|
258
|
+
"log_pointer_path": self.log_pointer_path,
|
|
259
|
+
"warnings": self.warnings,
|
|
260
|
+
"changed_files": self.changed_files,
|
|
261
|
+
"verify_failed": self.verify_failed,
|
|
262
|
+
"verification_unavailable": self.verification_unavailable,
|
|
263
|
+
"verify_summary": self.verify_summary,
|
|
264
|
+
"verify_artifact_path": self.verify_artifact_path,
|
|
265
|
+
"knowledge_capture_artifact_dir": self.knowledge_capture_artifact_dir,
|
|
266
|
+
"task_attempt_entry_id": self.task_attempt_entry_id,
|
|
267
|
+
"task_attempt_knowledge_file_path": self.task_attempt_knowledge_file_path,
|
|
268
|
+
"verify_payload": self.verify_payload,
|
|
269
|
+
"verify_command_source": self.verify_command_source,
|
|
270
|
+
"failure_reason": self.failure_reason,
|
|
271
|
+
"failure_category": None
|
|
272
|
+
if self.success
|
|
273
|
+
else failure_category_value(self.failure_category),
|
|
274
|
+
"scope_violation_files": self.scope_violation_files,
|
|
275
|
+
"scope_diagnostics": self.scope_diagnostics,
|
|
276
|
+
"allowed_scope": self.allowed_scope,
|
|
277
|
+
"agent_exit_code": self.agent_exit_code,
|
|
278
|
+
"salvaged_nonzero_exit": self.salvaged_nonzero_exit,
|
|
279
|
+
"salvaged_agent_exception": self.salvaged_agent_exception,
|
|
280
|
+
"agent_exception_summary": clean_agent_exception_summary,
|
|
281
|
+
"result_kind": self.effective_result_kind,
|
|
282
|
+
"noop_success": self.noop_success,
|
|
283
|
+
"noop_reason": self.noop_reason,
|
|
284
|
+
"task_kind": self.task_kind,
|
|
285
|
+
"task_lifecycle_reason": self.task_lifecycle_reason,
|
|
286
|
+
"interrupted": self.interrupted,
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@dataclass(frozen=True)
|
|
291
|
+
class ResolvedWorkerVerifyContract:
|
|
292
|
+
cfg: AppConfig
|
|
293
|
+
commands: tuple[str, ...]
|
|
294
|
+
selection: ResolvedVerifyCommands | None
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def resolve_worker_verify_contract(
|
|
298
|
+
*,
|
|
299
|
+
cfg: AppConfig,
|
|
300
|
+
verify_mode: str,
|
|
301
|
+
verify_commands: list[str] | None,
|
|
302
|
+
verify_command_selection: ResolvedVerifyCommands | None,
|
|
303
|
+
task: dict[str, Any] | None,
|
|
304
|
+
root: Path,
|
|
305
|
+
repo_scan: RepoScanResult | None = None,
|
|
306
|
+
plan_requirements: list[str] | None = None,
|
|
307
|
+
) -> ResolvedWorkerVerifyContract:
|
|
308
|
+
run_cfg = clone_cfg(cfg)
|
|
309
|
+
if verify_mode == "off":
|
|
310
|
+
return ResolvedWorkerVerifyContract(
|
|
311
|
+
cfg=run_cfg,
|
|
312
|
+
commands=(),
|
|
313
|
+
selection=None,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
selection = resolve_authoritative_task_verify_command_selection(
|
|
317
|
+
cfg=cfg,
|
|
318
|
+
verify_cmd=verify_commands,
|
|
319
|
+
task=task,
|
|
320
|
+
root=root,
|
|
321
|
+
repo_scan=repo_scan,
|
|
322
|
+
plan_requirements=plan_requirements,
|
|
323
|
+
selection=verify_command_selection,
|
|
324
|
+
allow_empty_config=True,
|
|
325
|
+
)
|
|
326
|
+
run_cfg.verify_commands = list(selection.commands)
|
|
327
|
+
return ResolvedWorkerVerifyContract(
|
|
328
|
+
cfg=run_cfg,
|
|
329
|
+
commands=selection.commands,
|
|
330
|
+
selection=selection,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def reject_abnormal_success_result(result: TaskWorkerResult) -> TaskWorkerResult:
|
|
335
|
+
if not result.success:
|
|
336
|
+
return result
|
|
337
|
+
abnormal_exit = result.agent_exit_code not in (None, 0)
|
|
338
|
+
salvage_label = result.salvaged_nonzero_exit or result.salvaged_agent_exception
|
|
339
|
+
if not abnormal_exit and not salvage_label:
|
|
340
|
+
return result
|
|
341
|
+
reasons: list[str] = []
|
|
342
|
+
if abnormal_exit:
|
|
343
|
+
reasons.append(f"agent_exit_code={result.agent_exit_code}")
|
|
344
|
+
if result.salvaged_agent_exception:
|
|
345
|
+
reasons.append("salvaged_agent_exception=true")
|
|
346
|
+
if result.salvaged_nonzero_exit:
|
|
347
|
+
reasons.append("salvaged_nonzero_exit=true")
|
|
348
|
+
reason = ", ".join(reasons)
|
|
349
|
+
error = (
|
|
350
|
+
"worker result reported success after abnormal agent termination "
|
|
351
|
+
f"({reason}); refusing to accept partial worker result"
|
|
352
|
+
)
|
|
353
|
+
summary = f"Worker rejected abnormal success result. Error: {error}"
|
|
354
|
+
return replace(
|
|
355
|
+
result,
|
|
356
|
+
success=False,
|
|
357
|
+
summary=summary,
|
|
358
|
+
error=error,
|
|
359
|
+
failure_reason=result.failure_reason or "agent_abnormal_success",
|
|
360
|
+
failure_category=result.failure_category or FailureCategory.IMPLEMENTATION_FAILED,
|
|
361
|
+
salvaged_nonzero_exit=False,
|
|
362
|
+
salvaged_agent_exception=False,
|
|
363
|
+
result_kind=None,
|
|
364
|
+
noop_reason=None,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _repo_rel(root: Path, path: Path) -> str:
|
|
369
|
+
return os.fspath(path.resolve().relative_to(root.resolve()))
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _normalize_rel_prefix(value: str) -> str:
|
|
373
|
+
cleaned = value.strip().replace("\\", "/")
|
|
374
|
+
while cleaned.startswith("./"):
|
|
375
|
+
cleaned = cleaned[2:]
|
|
376
|
+
return cleaned.rstrip("/")
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _matches_prefix(rel_path: str, prefix: str) -> bool:
|
|
380
|
+
return rel_path == prefix or rel_path.startswith(prefix + "/")
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _protected_path_violations(paths: list[str], prefixes: list[str]) -> list[str]:
|
|
384
|
+
normalized = [_normalize_rel_prefix(prefix) for prefix in prefixes if prefix.strip()]
|
|
385
|
+
violations: list[str] = []
|
|
386
|
+
for path in paths:
|
|
387
|
+
rel = _normalize_rel_prefix(path)
|
|
388
|
+
if any(_matches_prefix(rel, prefix) for prefix in normalized):
|
|
389
|
+
violations.append(path)
|
|
390
|
+
return violations
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _scope_violation_message(
|
|
394
|
+
*,
|
|
395
|
+
violations: list[str],
|
|
396
|
+
allowed_scope: list[str],
|
|
397
|
+
blocked: bool,
|
|
398
|
+
diagnostics: list[dict[str, Any]] | None = None,
|
|
399
|
+
) -> str:
|
|
400
|
+
violations_preview = ", ".join(violations[:20])
|
|
401
|
+
if len(violations) > 20:
|
|
402
|
+
violations_preview += ", ..."
|
|
403
|
+
message = (
|
|
404
|
+
f"Out-of-scope file changes detected ({len(violations)}): {violations_preview}. "
|
|
405
|
+
f"Allowed scope: {allowed_scope or ['(none)']}."
|
|
406
|
+
)
|
|
407
|
+
if diagnostics:
|
|
408
|
+
classes = sorted(
|
|
409
|
+
{
|
|
410
|
+
str(item.get("classification") or "unknown")
|
|
411
|
+
for item in diagnostics
|
|
412
|
+
if not bool(item.get("allowed"))
|
|
413
|
+
}
|
|
414
|
+
)
|
|
415
|
+
if classes:
|
|
416
|
+
message += f" Scope classifications: {', '.join(classes)}."
|
|
417
|
+
if blocked:
|
|
418
|
+
message += " Task was blocked due to strict scope isolation."
|
|
419
|
+
return message
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _scope_diagnostics_payload(
|
|
423
|
+
diagnostics: list[ScopeViolationDiagnostic],
|
|
424
|
+
) -> list[dict[str, Any]]:
|
|
425
|
+
return [item.to_payload() for item in diagnostics]
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _scope_recovery_warning(diagnostic: ScopeViolationDiagnostic) -> str:
|
|
429
|
+
return (
|
|
430
|
+
"Scope recovery: "
|
|
431
|
+
f"{diagnostic.classification} for {diagnostic.path} "
|
|
432
|
+
f"({diagnostic.reason_code}; action={diagnostic.recommended_action})."
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _scratch_artifact_section(scratch_dir: Path) -> str:
|
|
437
|
+
return "\n".join(
|
|
438
|
+
[
|
|
439
|
+
"## Scratch Artifacts",
|
|
440
|
+
"",
|
|
441
|
+
"Temporary diagnostic dumps, command-output captures, and ad hoc test logs must not be",
|
|
442
|
+
"written in the repository root. Use the managed scratch artifact directory below,",
|
|
443
|
+
f"or `{SCRATCH_ARTIFACT_ENV}` when the shell environment exposes it. Use `/tmp` only",
|
|
444
|
+
"for throwaway files that do not need to be retained.",
|
|
445
|
+
f"Managed scratch artifact directory: `{scratch_dir}`.",
|
|
446
|
+
"",
|
|
447
|
+
]
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _merge_changed_files(*path_groups: list[str]) -> list[str]:
|
|
452
|
+
seen: set[str] = set()
|
|
453
|
+
merged: list[str] = []
|
|
454
|
+
for group in path_groups:
|
|
455
|
+
for raw in group:
|
|
456
|
+
value = str(raw).strip().replace("\\", "/")
|
|
457
|
+
while value.startswith("./"):
|
|
458
|
+
value = value[2:]
|
|
459
|
+
if not value or value in seen:
|
|
460
|
+
continue
|
|
461
|
+
seen.add(value)
|
|
462
|
+
merged.append(value)
|
|
463
|
+
return merged
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _task_is_diagnostic_analysis_only(task: dict[str, Any]) -> bool:
|
|
467
|
+
raw_flag = task.get("analysis_only")
|
|
468
|
+
if isinstance(raw_flag, bool):
|
|
469
|
+
return raw_flag
|
|
470
|
+
lifecycle = classify_task_lifecycle(
|
|
471
|
+
title=str(task.get("title") or "").strip(),
|
|
472
|
+
description=str(task.get("description") or "").strip(),
|
|
473
|
+
acceptance_criteria=[
|
|
474
|
+
str(item or "")
|
|
475
|
+
for item in (task.get("acceptance_criteria") or [])
|
|
476
|
+
if str(item or "").strip()
|
|
477
|
+
],
|
|
478
|
+
estimated_files=[
|
|
479
|
+
str(item or "")
|
|
480
|
+
for item in (task.get("estimated_files") or [])
|
|
481
|
+
if str(item or "").strip()
|
|
482
|
+
],
|
|
483
|
+
write_scope=[
|
|
484
|
+
str(item or "") for item in (task.get("write_scope") or []) if str(item or "").strip()
|
|
485
|
+
],
|
|
486
|
+
)
|
|
487
|
+
if lifecycle.kind == TASK_KIND_ANALYSIS_ONLY:
|
|
488
|
+
return True
|
|
489
|
+
title = str(task.get("title") or "").strip()
|
|
490
|
+
if not title or not _DIAGNOSTIC_TASK_TITLE_RE.search(title):
|
|
491
|
+
return False
|
|
492
|
+
return _MUTATING_TASK_TEXT_RE.search(title) is None
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _path_has_cargo_manifest(root: Path, rel_dir: str) -> bool:
|
|
496
|
+
normalized = str(rel_dir or "").strip().replace("\\", "/").strip("/")
|
|
497
|
+
candidate = root if not normalized else root / normalized
|
|
498
|
+
try:
|
|
499
|
+
candidate.relative_to(root)
|
|
500
|
+
except ValueError:
|
|
501
|
+
return False
|
|
502
|
+
return (candidate / "Cargo.toml").is_file()
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _is_rust_build_metadata_path_for_repo(root: Path, path: str) -> bool:
|
|
506
|
+
normalized = str(path or "").strip().replace("\\", "/").strip("/")
|
|
507
|
+
if not normalized or normalized.startswith("../") or "/../" in normalized:
|
|
508
|
+
return False
|
|
509
|
+
parts = [part for part in normalized.split("/") if part]
|
|
510
|
+
if not parts:
|
|
511
|
+
return False
|
|
512
|
+
if parts[-1] == "Cargo.lock":
|
|
513
|
+
return _path_has_cargo_manifest(root, "/".join(parts[:-1]))
|
|
514
|
+
if "target" in parts:
|
|
515
|
+
target_index = parts.index("target")
|
|
516
|
+
return _path_has_cargo_manifest(root, "/".join(parts[:target_index]))
|
|
517
|
+
return False
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _diagnostic_side_effects_are_rust_build_metadata(root: Path, paths: list[str]) -> bool:
|
|
521
|
+
return bool(paths) and all(_is_rust_build_metadata_path_for_repo(root, path) for path in paths)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _task_allows_conditional_noop(task: dict[str, Any]) -> bool:
|
|
525
|
+
acceptance = task.get("acceptance_criteria") or []
|
|
526
|
+
if not isinstance(acceptance, list):
|
|
527
|
+
acceptance = []
|
|
528
|
+
text = "\n".join(
|
|
529
|
+
[
|
|
530
|
+
str(task.get("title") or ""),
|
|
531
|
+
str(task.get("description") or ""),
|
|
532
|
+
*(str(item or "") for item in acceptance),
|
|
533
|
+
]
|
|
534
|
+
)
|
|
535
|
+
return bool(_CONDITIONAL_NOOP_TASK_RE.search(text))
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _verification_failure_fingerprints(result: VerifyRunResult) -> set[str]:
|
|
539
|
+
fingerprints: set[str] = set()
|
|
540
|
+
for command_result in result.command_results:
|
|
541
|
+
if command_result.exit_code == 0:
|
|
542
|
+
continue
|
|
543
|
+
command = command_result.command or command_result.effective_command or "<unknown>"
|
|
544
|
+
output = command_result.output or "\n".join(
|
|
545
|
+
part for part in (command_result.stdout, command_result.stderr) if part
|
|
546
|
+
)
|
|
547
|
+
matched = False
|
|
548
|
+
for match in _VERIFY_FAILED_TEST_RE.finditer(output):
|
|
549
|
+
fingerprints.add(f"{command}::{match.group(1).strip()}")
|
|
550
|
+
matched = True
|
|
551
|
+
for match in _VERIFY_FAILURE_SECTION_RE.finditer(output):
|
|
552
|
+
label = " ".join(match.group(1).strip().split())
|
|
553
|
+
if label and label.lower() not in {"failures", "errors"}:
|
|
554
|
+
fingerprints.add(f"{command}::{label}")
|
|
555
|
+
matched = True
|
|
556
|
+
if matched:
|
|
557
|
+
continue
|
|
558
|
+
fallback_match = _VERIFY_ACTIONABLE_FAILURE_RE.search(output)
|
|
559
|
+
if fallback_match is not None:
|
|
560
|
+
line = " ".join(fallback_match.group(0).strip().split())
|
|
561
|
+
if line:
|
|
562
|
+
fingerprints.add(f"{command}::{line[:200]}")
|
|
563
|
+
continue
|
|
564
|
+
fingerprints.add(f"{command}::exit:{command_result.exit_code}")
|
|
565
|
+
return fingerprints
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _failure_fingerprints_by_command(fingerprints: set[str]) -> dict[str, set[str]]:
|
|
569
|
+
by_command: dict[str, set[str]] = {}
|
|
570
|
+
for fingerprint in fingerprints:
|
|
571
|
+
command, _, _detail = fingerprint.partition("::")
|
|
572
|
+
if not command:
|
|
573
|
+
continue
|
|
574
|
+
by_command.setdefault(command, set()).add(fingerprint)
|
|
575
|
+
return by_command
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _baseline_failure_tokens(text: str) -> set[str]:
|
|
579
|
+
tokens: set[str] = set()
|
|
580
|
+
for match in _BASELINE_FAILURE_TOKEN_RE.finditer(text):
|
|
581
|
+
token = match.group(0).lower().strip("_")
|
|
582
|
+
if not token or token in _BASELINE_FAILURE_STOPWORDS:
|
|
583
|
+
continue
|
|
584
|
+
tokens.add(token)
|
|
585
|
+
return tokens
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _task_focus_tokens(task: dict[str, Any] | None) -> set[str]:
|
|
589
|
+
if not isinstance(task, dict):
|
|
590
|
+
return set()
|
|
591
|
+
acceptance = task.get("acceptance_criteria") or []
|
|
592
|
+
if not isinstance(acceptance, list):
|
|
593
|
+
acceptance = []
|
|
594
|
+
text = "\n".join(
|
|
595
|
+
[
|
|
596
|
+
str(task.get("title") or ""),
|
|
597
|
+
str(task.get("description") or ""),
|
|
598
|
+
*(str(item or "") for item in acceptance),
|
|
599
|
+
]
|
|
600
|
+
)
|
|
601
|
+
return _baseline_failure_tokens(text)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _residual_failure_is_unrelated_to_task(fingerprint: str, task_tokens: set[str]) -> bool:
|
|
605
|
+
if not task_tokens:
|
|
606
|
+
return False
|
|
607
|
+
_command, _separator, detail = fingerprint.partition("::")
|
|
608
|
+
failure_tokens = _baseline_failure_tokens(detail or fingerprint)
|
|
609
|
+
if not failure_tokens:
|
|
610
|
+
return False
|
|
611
|
+
return failure_tokens.isdisjoint(task_tokens)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _baseline_improved_failure_comparison(
|
|
615
|
+
*,
|
|
616
|
+
baseline: VerifyRunResult | None,
|
|
617
|
+
current: VerifyRunResult,
|
|
618
|
+
task: dict[str, Any] | None = None,
|
|
619
|
+
) -> dict[str, Any] | None:
|
|
620
|
+
if baseline is None or baseline.all_passed or current.all_passed:
|
|
621
|
+
return None
|
|
622
|
+
if (
|
|
623
|
+
failure_category_value(current.failure_category_value)
|
|
624
|
+
== FailureCategory.INFRA_UNAVAILABLE.value
|
|
625
|
+
):
|
|
626
|
+
return None
|
|
627
|
+
|
|
628
|
+
baseline_failures = _verification_failure_fingerprints(baseline)
|
|
629
|
+
current_failures = _verification_failure_fingerprints(current)
|
|
630
|
+
if not baseline_failures or not current_failures:
|
|
631
|
+
return None
|
|
632
|
+
if not current_failures <= baseline_failures:
|
|
633
|
+
return None
|
|
634
|
+
baseline_by_command = _failure_fingerprints_by_command(baseline_failures)
|
|
635
|
+
current_by_command = _failure_fingerprints_by_command(current_failures)
|
|
636
|
+
task_tokens = _task_focus_tokens(task)
|
|
637
|
+
unchanged_residual_failures: set[str] = set()
|
|
638
|
+
for command, command_failures in current_by_command.items():
|
|
639
|
+
baseline_command_failures = baseline_by_command.get(command, set())
|
|
640
|
+
if command_failures < baseline_command_failures:
|
|
641
|
+
continue
|
|
642
|
+
unchanged_residual_failures.update(command_failures)
|
|
643
|
+
task_related_unchanged = [
|
|
644
|
+
failure
|
|
645
|
+
for failure in unchanged_residual_failures
|
|
646
|
+
if not _residual_failure_is_unrelated_to_task(failure, task_tokens)
|
|
647
|
+
]
|
|
648
|
+
if task_related_unchanged:
|
|
649
|
+
return None
|
|
650
|
+
resolved_failures = sorted(baseline_failures - current_failures)
|
|
651
|
+
if not resolved_failures and not unchanged_residual_failures:
|
|
652
|
+
return None
|
|
653
|
+
reason = (
|
|
654
|
+
"post-task verification failures are unchanged from baseline but unrelated to "
|
|
655
|
+
"the task focus"
|
|
656
|
+
if not resolved_failures
|
|
657
|
+
else "post-task verification failures are a subset of baseline failures"
|
|
658
|
+
)
|
|
659
|
+
return {
|
|
660
|
+
"accepted": True,
|
|
661
|
+
"reason": reason,
|
|
662
|
+
"baseline_failure_count": len(baseline_failures),
|
|
663
|
+
"current_failure_count": len(current_failures),
|
|
664
|
+
"resolved_failure_count": len(resolved_failures),
|
|
665
|
+
"current_failures": sorted(current_failures),
|
|
666
|
+
"resolved_failures": resolved_failures,
|
|
667
|
+
"unchanged_unrelated_failures": sorted(unchanged_residual_failures),
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def _extract_git_archive_to_directory(
|
|
672
|
+
*,
|
|
673
|
+
repo: Path,
|
|
674
|
+
commit: str,
|
|
675
|
+
target_dir: Path,
|
|
676
|
+
) -> bool:
|
|
677
|
+
try:
|
|
678
|
+
cp = subprocess.run(
|
|
679
|
+
["git", "-C", os.fspath(repo), "archive", "--format=tar", commit],
|
|
680
|
+
check=False,
|
|
681
|
+
capture_output=True,
|
|
682
|
+
)
|
|
683
|
+
except OSError:
|
|
684
|
+
return False
|
|
685
|
+
if cp.returncode != 0:
|
|
686
|
+
return False
|
|
687
|
+
|
|
688
|
+
target_root = target_dir.resolve()
|
|
689
|
+
with tarfile.open(fileobj=io.BytesIO(cp.stdout), mode="r:") as archive:
|
|
690
|
+
members = archive.getmembers()
|
|
691
|
+
for member in members:
|
|
692
|
+
destination = (target_root / member.name).resolve()
|
|
693
|
+
if not destination.is_relative_to(target_root):
|
|
694
|
+
return False
|
|
695
|
+
archive.extractall(target_root, members=members)
|
|
696
|
+
return True
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def _run_baseline_verification_snapshot(
|
|
700
|
+
*,
|
|
701
|
+
repo: Path,
|
|
702
|
+
commit: str | None,
|
|
703
|
+
commands: list[str],
|
|
704
|
+
artifact_path: Path,
|
|
705
|
+
cfg: AppConfig,
|
|
706
|
+
) -> VerifyRunResult | None:
|
|
707
|
+
if not commit or not commands:
|
|
708
|
+
return None
|
|
709
|
+
try:
|
|
710
|
+
with tempfile.TemporaryDirectory(prefix="alysis-verify-baseline-") as temp_root:
|
|
711
|
+
baseline_root = Path(temp_root) / "repo"
|
|
712
|
+
baseline_root.mkdir(parents=True, exist_ok=True)
|
|
713
|
+
if not _extract_git_archive_to_directory(
|
|
714
|
+
repo=repo,
|
|
715
|
+
commit=commit,
|
|
716
|
+
target_dir=baseline_root,
|
|
717
|
+
):
|
|
718
|
+
return None
|
|
719
|
+
return run_task_verification(
|
|
720
|
+
root=baseline_root,
|
|
721
|
+
commands=commands,
|
|
722
|
+
artifact_path=artifact_path,
|
|
723
|
+
cfg=cfg,
|
|
724
|
+
)
|
|
725
|
+
except (OSError, tarfile.TarError):
|
|
726
|
+
return None
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _append_patch_debug_section(patch_path: Path, *, title: str, patch_text: str) -> None:
|
|
730
|
+
existing = patch_path.read_text(encoding="utf-8") if patch_path.exists() else ""
|
|
731
|
+
parts: list[str] = []
|
|
732
|
+
if existing:
|
|
733
|
+
parts.append(existing if existing.endswith("\n") else existing + "\n")
|
|
734
|
+
parts.append(f"# {title}\n")
|
|
735
|
+
if patch_text:
|
|
736
|
+
parts.append(patch_text if patch_text.endswith("\n") else patch_text + "\n")
|
|
737
|
+
patch_path.write_text("\n".join(part.rstrip("\n") for part in parts if part), encoding="utf-8")
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def _format_agent_exception_summary(exc: Exception) -> str:
|
|
741
|
+
name = exc.__class__.__name__
|
|
742
|
+
message = str(exc).strip()
|
|
743
|
+
return sanitize_error_summary(f"{name}: {message}" if message else name)
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _persist_agent_exception_traceback(artifact_dir: Path, exc: BaseException) -> None:
|
|
747
|
+
"""Persist the full, secret-redacted agent traceback for post-mortem.
|
|
748
|
+
|
|
749
|
+
The worker result keeps only a short summary; the full traceback is what an
|
|
750
|
+
autonomous fix-loop actually needs to root-cause a Forge worker failure. The
|
|
751
|
+
text is run through the shared secret redactor before it touches disk. Best
|
|
752
|
+
effort: a failure to persist must never mask the agent error itself.
|
|
753
|
+
"""
|
|
754
|
+
try:
|
|
755
|
+
text = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
|
|
756
|
+
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
757
|
+
(artifact_dir / "agent_exception_traceback.txt").write_text(
|
|
758
|
+
redact_sensitive_error_text(text),
|
|
759
|
+
encoding="utf-8",
|
|
760
|
+
)
|
|
761
|
+
except Exception: # noqa: BLE001 - diagnostics must not mask the real failure
|
|
762
|
+
return
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def _empty_task_asset_mirror(workspace_path: Path, *, task_id: str = "") -> TaskAssetMirror:
|
|
766
|
+
workspace = workspace_path.resolve()
|
|
767
|
+
return TaskAssetMirror(
|
|
768
|
+
workspace_path=workspace,
|
|
769
|
+
manifest_path=workspace / ".alysis" / "task_assets" / "manifest.json",
|
|
770
|
+
primary=[],
|
|
771
|
+
may_need=[],
|
|
772
|
+
pinned=[],
|
|
773
|
+
task_id=task_id,
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def _combined_task_image_paths(
|
|
778
|
+
*,
|
|
779
|
+
legacy_paths: list[str],
|
|
780
|
+
mirror: TaskAssetMirror,
|
|
781
|
+
allocation: TaskAssetAllocation | None,
|
|
782
|
+
cfg: AppConfig,
|
|
783
|
+
role_model: str,
|
|
784
|
+
model_registry: ModelRegistry,
|
|
785
|
+
usage_logger: AssetUsageLogger,
|
|
786
|
+
) -> list[str] | None:
|
|
787
|
+
combined: list[str] = []
|
|
788
|
+
seen: set[str] = set()
|
|
789
|
+
|
|
790
|
+
def _append(path: str) -> None:
|
|
791
|
+
try:
|
|
792
|
+
normalized = os.fspath(Path(path).resolve())
|
|
793
|
+
except OSError:
|
|
794
|
+
return
|
|
795
|
+
if normalized in seen:
|
|
796
|
+
return
|
|
797
|
+
seen.add(normalized)
|
|
798
|
+
combined.append(normalized)
|
|
799
|
+
|
|
800
|
+
for path in legacy_paths:
|
|
801
|
+
_append(path)
|
|
802
|
+
if cfg.assets.worker.inline_images and model_registry.get(role_model).supports_vision:
|
|
803
|
+
decision_by_id = {
|
|
804
|
+
decision.asset_id: decision.mode
|
|
805
|
+
for decision in (allocation.decisions if allocation else [])
|
|
806
|
+
}
|
|
807
|
+
max_new = max(0, int(cfg.assets.worker.max_inline_images))
|
|
808
|
+
added_new = 0
|
|
809
|
+
for entry in mirror.primary:
|
|
810
|
+
if entry.kind != "image" or entry.status != "mirrored":
|
|
811
|
+
continue
|
|
812
|
+
if decision_by_id.get(entry.asset_id) not in {"full_inline", "focused_extract"}:
|
|
813
|
+
continue
|
|
814
|
+
if entry.raw_workspace_path is None or not entry.raw_workspace_path.exists():
|
|
815
|
+
continue
|
|
816
|
+
before = len(combined)
|
|
817
|
+
_append(os.fspath(entry.raw_workspace_path))
|
|
818
|
+
if len(combined) > before:
|
|
819
|
+
usage_logger.inline_injection(asset_id=entry.asset_id, kind=entry.kind)
|
|
820
|
+
added_new += 1
|
|
821
|
+
if added_new >= max_new:
|
|
822
|
+
break
|
|
823
|
+
max_total = max(0, int(cfg.assets.worker.max_inline_images))
|
|
824
|
+
if max_total > 0:
|
|
825
|
+
combined = combined[:max_total]
|
|
826
|
+
return combined or None
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def run_task_worker(
|
|
830
|
+
*,
|
|
831
|
+
task: dict[str, Any],
|
|
832
|
+
plan: dict[str, Any],
|
|
833
|
+
worktree_repo_path: Path,
|
|
834
|
+
base_branch: str,
|
|
835
|
+
run_paths: RunPaths,
|
|
836
|
+
cfg: AppConfig,
|
|
837
|
+
mode: str,
|
|
838
|
+
yes: bool,
|
|
839
|
+
max_steps: int | None,
|
|
840
|
+
api_key_override: str | None,
|
|
841
|
+
no_log: bool = False,
|
|
842
|
+
console: Console | None = None,
|
|
843
|
+
scope_mode: str = "strict",
|
|
844
|
+
verify_mode: str = "warn",
|
|
845
|
+
verify_commands: list[str] | None = None,
|
|
846
|
+
verify_command_selection: ResolvedVerifyCommands | None = None,
|
|
847
|
+
trace_sink: SwarmTraceSink | None = None,
|
|
848
|
+
trace_level: str = "off",
|
|
849
|
+
cancellation_event: threading.Event | None = None,
|
|
850
|
+
approval_handler: Callable[..., Any] | None = None,
|
|
851
|
+
) -> TaskWorkerResult:
|
|
852
|
+
ensure_execution_dirs(run_paths)
|
|
853
|
+
(run_paths.execution_dir / "worker_results").mkdir(parents=True, exist_ok=True)
|
|
854
|
+
|
|
855
|
+
task_id = str(task.get("id") or "").strip()
|
|
856
|
+
task_title = str(task.get("title") or "").strip()
|
|
857
|
+
task_branch = str(task.get("branch") or "").strip()
|
|
858
|
+
safe_task = safe_task_file_component(task_id)
|
|
859
|
+
plan_requirements = (
|
|
860
|
+
[str(item).strip() for item in (plan.get("requirements") or []) if str(item).strip()]
|
|
861
|
+
if isinstance(plan, dict)
|
|
862
|
+
else None
|
|
863
|
+
)
|
|
864
|
+
verify_contract = resolve_worker_verify_contract(
|
|
865
|
+
cfg=cfg,
|
|
866
|
+
verify_mode=verify_mode,
|
|
867
|
+
verify_commands=verify_commands,
|
|
868
|
+
verify_command_selection=verify_command_selection,
|
|
869
|
+
task=task,
|
|
870
|
+
root=worktree_repo_path,
|
|
871
|
+
plan_requirements=plan_requirements,
|
|
872
|
+
)
|
|
873
|
+
verify_resolution = verify_contract.selection
|
|
874
|
+
verify_cmds = list(verify_contract.commands)
|
|
875
|
+
verify_command_source = verify_resolution.source if verify_resolution is not None else None
|
|
876
|
+
run_cfg = verify_contract.cfg
|
|
877
|
+
run_cfg.model = resolve_model_for_role(
|
|
878
|
+
cfg=cfg,
|
|
879
|
+
role=ROLE_CODING,
|
|
880
|
+
plan=plan,
|
|
881
|
+
prefer_context="forge",
|
|
882
|
+
)
|
|
883
|
+
worker_trace_sink = trace_sink or NoopSwarmTraceSink()
|
|
884
|
+
worker_surface = SwarmWorkerTraceSurface(
|
|
885
|
+
run_id=run_paths.run_id,
|
|
886
|
+
task_id=task_id,
|
|
887
|
+
trace_sink=worker_trace_sink,
|
|
888
|
+
trace_level=trace_level,
|
|
889
|
+
)
|
|
890
|
+
if verify_mode != "off":
|
|
891
|
+
resolved_verify_source = verify_command_source or "unknown"
|
|
892
|
+
resolved_verify_commands = ", ".join(verify_cmds) if verify_cmds else "(none)"
|
|
893
|
+
worker_trace_sink.emit(
|
|
894
|
+
build_swarm_trace_event(
|
|
895
|
+
run_id=run_paths.run_id,
|
|
896
|
+
task_id=task_id,
|
|
897
|
+
phase="verify.lifecycle",
|
|
898
|
+
message=(
|
|
899
|
+
"Resolved authoritative verification commands "
|
|
900
|
+
f"from {resolved_verify_source}: {resolved_verify_commands}."
|
|
901
|
+
),
|
|
902
|
+
verbosity="full",
|
|
903
|
+
)
|
|
904
|
+
)
|
|
905
|
+
recording_surface = RecordingSurface(worker_surface)
|
|
906
|
+
agent_surface: Any = recording_surface
|
|
907
|
+
if approval_handler is not None:
|
|
908
|
+
agent_surface = _HostManagedApprovalSurface(
|
|
909
|
+
recording_surface,
|
|
910
|
+
task_id=task_id,
|
|
911
|
+
handler=approval_handler,
|
|
912
|
+
)
|
|
913
|
+
worker_console = console or make_console(
|
|
914
|
+
file=io.StringIO(), force_terminal=False, no_color=True
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
started_at = now_iso()
|
|
918
|
+
patch_path = run_paths.execution_patches_dir / f"{safe_task}.diff"
|
|
919
|
+
verify_path = run_paths.execution_verify_dir / f"{safe_task}.txt"
|
|
920
|
+
baseline_verify_path = run_paths.execution_verify_dir / f"{safe_task}.baseline.txt"
|
|
921
|
+
allowed_scope = normalize_scope_patterns(task, root=worktree_repo_path)
|
|
922
|
+
task_lifecycle = classify_task_lifecycle(
|
|
923
|
+
title=task_title,
|
|
924
|
+
description=str(task.get("description") or "").strip(),
|
|
925
|
+
acceptance_criteria=[
|
|
926
|
+
str(item or "")
|
|
927
|
+
for item in (task.get("acceptance_criteria") or [])
|
|
928
|
+
if str(item or "").strip()
|
|
929
|
+
],
|
|
930
|
+
estimated_files=[
|
|
931
|
+
str(item or "")
|
|
932
|
+
for item in (task.get("estimated_files") or [])
|
|
933
|
+
if str(item or "").strip()
|
|
934
|
+
],
|
|
935
|
+
write_scope=[
|
|
936
|
+
str(item or "") for item in (task.get("write_scope") or []) if str(item or "").strip()
|
|
937
|
+
],
|
|
938
|
+
explicit_analysis_only=True if task.get("analysis_only") is True else None,
|
|
939
|
+
)
|
|
940
|
+
diagnostic_analysis_only = _task_is_diagnostic_analysis_only(task)
|
|
941
|
+
conditional_noop_allowed = _task_allows_conditional_noop(task)
|
|
942
|
+
prompt_verification_enabled = (
|
|
943
|
+
verify_mode != "off" and bool(verify_cmds) and not diagnostic_analysis_only
|
|
944
|
+
)
|
|
945
|
+
mirror_plan_into_worktree(run_paths=run_paths, worktree_repo_path=worktree_repo_path)
|
|
946
|
+
prepared_knowledge = prepare_task_execution_knowledge(
|
|
947
|
+
run_paths=run_paths,
|
|
948
|
+
task=task,
|
|
949
|
+
selection_label="execution",
|
|
950
|
+
)
|
|
951
|
+
mirror_selected_knowledge_into_worktree(
|
|
952
|
+
materialized=prepared_knowledge,
|
|
953
|
+
run_paths=run_paths,
|
|
954
|
+
worktree_repo_path=worktree_repo_path,
|
|
955
|
+
)
|
|
956
|
+
scratch_artifact_dir = run_paths.execution_dir / "scratch" / safe_task
|
|
957
|
+
scratch_artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
958
|
+
scratch_artifact_section = _scratch_artifact_section(scratch_artifact_dir)
|
|
959
|
+
|
|
960
|
+
def _cancel_requested() -> bool:
|
|
961
|
+
return cancellation_event is not None and cancellation_event.is_set()
|
|
962
|
+
|
|
963
|
+
cancellation_token = (
|
|
964
|
+
EventCancellationToken(cancellation_event) if cancellation_event is not None else None
|
|
965
|
+
)
|
|
966
|
+
write_guard: SwarmWriteScopeGuard | None = None
|
|
967
|
+
if scope_mode == "strict":
|
|
968
|
+
write_guard = SwarmWriteScopeGuard(
|
|
969
|
+
worktree_root=worktree_repo_path,
|
|
970
|
+
allowed_patterns=allowed_scope,
|
|
971
|
+
extra_allowed_roots=(scratch_artifact_dir,),
|
|
972
|
+
)
|
|
973
|
+
asset_setup_warnings: list[str] = []
|
|
974
|
+
asset_setup_error: str | None = None
|
|
975
|
+
asset_usage_logger = AssetUsageLogger(run_paths=run_paths, task_id=task_id)
|
|
976
|
+
asset_model_registry = ModelRegistry(cfg=run_cfg)
|
|
977
|
+
asset_surface = (
|
|
978
|
+
build_asset_surface(
|
|
979
|
+
cfg=run_cfg,
|
|
980
|
+
run_paths=run_paths,
|
|
981
|
+
model_registry=asset_model_registry,
|
|
982
|
+
)
|
|
983
|
+
if run_cfg.assets.enabled
|
|
984
|
+
else None
|
|
985
|
+
)
|
|
986
|
+
task_asset_mirror = _empty_task_asset_mirror(worktree_repo_path, task_id=task_id)
|
|
987
|
+
if asset_surface is not None:
|
|
988
|
+
try:
|
|
989
|
+
task_asset_mirror = mirror_task_assets(
|
|
990
|
+
task=task,
|
|
991
|
+
plan=plan,
|
|
992
|
+
surface=asset_surface,
|
|
993
|
+
workspace_path=worktree_repo_path,
|
|
994
|
+
)
|
|
995
|
+
except AssetError as exc:
|
|
996
|
+
if run_cfg.assets.worker.fail_on_mirror_error:
|
|
997
|
+
asset_setup_error = (
|
|
998
|
+
f"worker asset mirror failed: {sanitize_error_summary(str(exc))}"
|
|
999
|
+
)
|
|
1000
|
+
else:
|
|
1001
|
+
warning = (
|
|
1002
|
+
f"worker asset mirror skipped: {sanitize_optional_error_summary(str(exc))}"
|
|
1003
|
+
)
|
|
1004
|
+
asset_setup_warnings.append(warning)
|
|
1005
|
+
for entry in [
|
|
1006
|
+
*task_asset_mirror.primary,
|
|
1007
|
+
*task_asset_mirror.may_need,
|
|
1008
|
+
*task_asset_mirror.pinned,
|
|
1009
|
+
]:
|
|
1010
|
+
asset_usage_logger.mirror(
|
|
1011
|
+
asset_id=entry.asset_id,
|
|
1012
|
+
kind=entry.kind,
|
|
1013
|
+
status=entry.status,
|
|
1014
|
+
)
|
|
1015
|
+
instruction_bundle = build_task_execution_instruction_bundle(
|
|
1016
|
+
plan=plan,
|
|
1017
|
+
task=task,
|
|
1018
|
+
root=worktree_repo_path,
|
|
1019
|
+
cfg=run_cfg,
|
|
1020
|
+
role_model=run_cfg.model,
|
|
1021
|
+
mode=mode,
|
|
1022
|
+
yes=yes,
|
|
1023
|
+
deny_write_prefixes=[".alysis"],
|
|
1024
|
+
allow_write_globs=allowed_scope if scope_mode == "strict" else None,
|
|
1025
|
+
non_interactive=True,
|
|
1026
|
+
verification_enabled=prompt_verification_enabled,
|
|
1027
|
+
authoritative_verification_commands=(verify_cmds if prompt_verification_enabled else None),
|
|
1028
|
+
api_key=api_key_override,
|
|
1029
|
+
subagents_enabled=False,
|
|
1030
|
+
leading_sections=[scratch_artifact_section, prepared_knowledge.prompt_section],
|
|
1031
|
+
)
|
|
1032
|
+
asset_allocation: TaskAssetAllocation | None = None
|
|
1033
|
+
relevant_assets_section = ""
|
|
1034
|
+
if asset_surface is not None and task_asset_mirror.primary and not _cancel_requested():
|
|
1035
|
+
asset_allocation = allocate_task_assets(
|
|
1036
|
+
task=task,
|
|
1037
|
+
plan=plan,
|
|
1038
|
+
mirror=task_asset_mirror,
|
|
1039
|
+
cfg=run_cfg,
|
|
1040
|
+
model_registry=asset_model_registry,
|
|
1041
|
+
instruction_token_budget=instruction_bundle.budget.final_instruction_budget,
|
|
1042
|
+
api_key=api_key_override,
|
|
1043
|
+
)
|
|
1044
|
+
for decision in asset_allocation.decisions:
|
|
1045
|
+
asset_usage_logger.allocation_decision(
|
|
1046
|
+
asset_id=decision.asset_id,
|
|
1047
|
+
mode=decision.mode,
|
|
1048
|
+
)
|
|
1049
|
+
relevant_assets_section = render_relevant_assets_section(
|
|
1050
|
+
mirror=task_asset_mirror,
|
|
1051
|
+
allocation=asset_allocation,
|
|
1052
|
+
cfg=run_cfg,
|
|
1053
|
+
surface=asset_surface,
|
|
1054
|
+
model_registry=asset_model_registry,
|
|
1055
|
+
api_key=api_key_override,
|
|
1056
|
+
)
|
|
1057
|
+
elif asset_surface is not None and (task_asset_mirror.may_need or task_asset_mirror.pinned):
|
|
1058
|
+
asset_allocation = TaskAssetAllocation(
|
|
1059
|
+
task_id=task_id,
|
|
1060
|
+
decisions=[],
|
|
1061
|
+
elapsed_ms=0,
|
|
1062
|
+
model=None,
|
|
1063
|
+
tokens_used={},
|
|
1064
|
+
fallback_used=False,
|
|
1065
|
+
fallback_reason=None,
|
|
1066
|
+
)
|
|
1067
|
+
relevant_assets_section = render_relevant_assets_section(
|
|
1068
|
+
mirror=task_asset_mirror,
|
|
1069
|
+
allocation=asset_allocation,
|
|
1070
|
+
cfg=run_cfg,
|
|
1071
|
+
surface=asset_surface,
|
|
1072
|
+
model_registry=asset_model_registry,
|
|
1073
|
+
api_key=api_key_override,
|
|
1074
|
+
)
|
|
1075
|
+
if relevant_assets_section:
|
|
1076
|
+
instruction_bundle = build_task_execution_instruction_bundle(
|
|
1077
|
+
plan=plan,
|
|
1078
|
+
task=task,
|
|
1079
|
+
root=worktree_repo_path,
|
|
1080
|
+
cfg=run_cfg,
|
|
1081
|
+
role_model=run_cfg.model,
|
|
1082
|
+
mode=mode,
|
|
1083
|
+
yes=yes,
|
|
1084
|
+
deny_write_prefixes=[".alysis"],
|
|
1085
|
+
allow_write_globs=allowed_scope if scope_mode == "strict" else None,
|
|
1086
|
+
non_interactive=True,
|
|
1087
|
+
verification_enabled=prompt_verification_enabled,
|
|
1088
|
+
authoritative_verification_commands=(
|
|
1089
|
+
verify_cmds if prompt_verification_enabled else None
|
|
1090
|
+
),
|
|
1091
|
+
api_key=api_key_override,
|
|
1092
|
+
subagents_enabled=False,
|
|
1093
|
+
leading_sections=[scratch_artifact_section, prepared_knowledge.prompt_section],
|
|
1094
|
+
relevant_assets_section=relevant_assets_section,
|
|
1095
|
+
)
|
|
1096
|
+
instruction = instruction_bundle.instruction
|
|
1097
|
+
write_execution_context_artifact(
|
|
1098
|
+
run_paths=run_paths,
|
|
1099
|
+
task_id=task_id,
|
|
1100
|
+
context_text=instruction_bundle.artifact_text,
|
|
1101
|
+
)
|
|
1102
|
+
task_image_paths = _combined_task_image_paths(
|
|
1103
|
+
legacy_paths=list(instruction_bundle.image_paths),
|
|
1104
|
+
mirror=task_asset_mirror,
|
|
1105
|
+
allocation=asset_allocation,
|
|
1106
|
+
cfg=run_cfg,
|
|
1107
|
+
role_model=run_cfg.model,
|
|
1108
|
+
model_registry=asset_model_registry,
|
|
1109
|
+
usage_logger=asset_usage_logger,
|
|
1110
|
+
)
|
|
1111
|
+
task_asset_mcp_manager: Any | None = None
|
|
1112
|
+
task_attempts_raw = task.get("attempts")
|
|
1113
|
+
try:
|
|
1114
|
+
task_attempt_count = max(1, int(task_attempts_raw if task_attempts_raw is not None else 1))
|
|
1115
|
+
except (TypeError, ValueError):
|
|
1116
|
+
task_attempt_count = 1
|
|
1117
|
+
task_step_budget = resolve_managed_task_step_budget(
|
|
1118
|
+
cfg=run_cfg,
|
|
1119
|
+
plan=plan,
|
|
1120
|
+
task=task,
|
|
1121
|
+
kind="managed_task",
|
|
1122
|
+
mode=mode,
|
|
1123
|
+
verification_enabled=verify_mode != "off",
|
|
1124
|
+
max_steps_override=max_steps,
|
|
1125
|
+
attempt_count=task_attempt_count,
|
|
1126
|
+
image_count=len(task_image_paths or []),
|
|
1127
|
+
)
|
|
1128
|
+
budget_artifact_payload = instruction_bundle.to_budget_artifact_payload()
|
|
1129
|
+
budget_artifact_payload["step_budget"] = task_step_budget.to_payload()
|
|
1130
|
+
budget_artifact_path = write_execution_budget_artifact(
|
|
1131
|
+
run_paths=run_paths,
|
|
1132
|
+
task_id=task_id,
|
|
1133
|
+
payload=budget_artifact_payload,
|
|
1134
|
+
)
|
|
1135
|
+
protected_prefixes = [".alysis", ".alysis_images", "alysis-feedback"]
|
|
1136
|
+
head_before_run = head_commit(worktree_repo_path)
|
|
1137
|
+
before_runtime_snapshot = snapshot_runtime_tree(worktree_repo_path)
|
|
1138
|
+
|
|
1139
|
+
run_code = 1
|
|
1140
|
+
run_err: str | None = asset_setup_error
|
|
1141
|
+
agent_run_exception = False
|
|
1142
|
+
agent_exception_summary: str | None = None
|
|
1143
|
+
interrupted = False
|
|
1144
|
+
failure_category: FailureCategory | str | None = None
|
|
1145
|
+
worktree_git_marker = worktree_repo_path / ".git"
|
|
1146
|
+
if worktree_git_marker.exists():
|
|
1147
|
+
try:
|
|
1148
|
+
ensure_runtime_artifact_excludes(worktree_repo_path)
|
|
1149
|
+
except GitOpsError as e:
|
|
1150
|
+
run_err = f"worker setup failed: {e}"
|
|
1151
|
+
try:
|
|
1152
|
+
if run_err is None and _cancel_requested():
|
|
1153
|
+
interrupted = True
|
|
1154
|
+
run_code = 130
|
|
1155
|
+
run_err = "interrupted before the agent turn started"
|
|
1156
|
+
elif run_err is None:
|
|
1157
|
+
if asset_surface is not None:
|
|
1158
|
+
task_asset_mcp_manager = compose_worker_asset_mcp_manager(
|
|
1159
|
+
base_manager=create_mcp_manager(
|
|
1160
|
+
workspace_root=worktree_repo_path,
|
|
1161
|
+
runtime_kind=RuntimeKind.SWARM_WORKER,
|
|
1162
|
+
session_id=safe_task,
|
|
1163
|
+
),
|
|
1164
|
+
asset_manager=build_worker_asset_mcp_manager(
|
|
1165
|
+
cfg=run_cfg,
|
|
1166
|
+
surface=asset_surface,
|
|
1167
|
+
model_registry=asset_model_registry,
|
|
1168
|
+
mirror=task_asset_mirror,
|
|
1169
|
+
usage_logger=asset_usage_logger,
|
|
1170
|
+
api_key=api_key_override,
|
|
1171
|
+
),
|
|
1172
|
+
)
|
|
1173
|
+
run_code = run_agent(
|
|
1174
|
+
cfg=run_cfg,
|
|
1175
|
+
root=worktree_repo_path,
|
|
1176
|
+
instruction=instruction,
|
|
1177
|
+
mode=mode,
|
|
1178
|
+
runtime_kind=RuntimeKind.SWARM_WORKER,
|
|
1179
|
+
yes=yes,
|
|
1180
|
+
max_steps=task_step_budget.resolved_max_steps,
|
|
1181
|
+
no_log=no_log,
|
|
1182
|
+
api_key_override=api_key_override,
|
|
1183
|
+
console=worker_console,
|
|
1184
|
+
surface=agent_surface,
|
|
1185
|
+
image_paths=task_image_paths,
|
|
1186
|
+
deny_write_prefixes=[".alysis"],
|
|
1187
|
+
session_log_dir_override=run_paths.execution_sessions_dir,
|
|
1188
|
+
session_id_override=safe_task,
|
|
1189
|
+
allow_write_globs=allowed_scope if scope_mode == "strict" else None,
|
|
1190
|
+
non_interactive=True,
|
|
1191
|
+
usage_role=f"swarm_worker:{task_id}",
|
|
1192
|
+
enable_compaction=False,
|
|
1193
|
+
enable_tool_output_offload=True,
|
|
1194
|
+
enable_conversation_summarization=True,
|
|
1195
|
+
compaction_profile="execution",
|
|
1196
|
+
enable_chat_turn_step_budget=False,
|
|
1197
|
+
one_shot_execution=True,
|
|
1198
|
+
verification_enabled=prompt_verification_enabled,
|
|
1199
|
+
authoritative_verification_commands=(
|
|
1200
|
+
verify_cmds if prompt_verification_enabled else None
|
|
1201
|
+
),
|
|
1202
|
+
subagents_enabled=False,
|
|
1203
|
+
enforce_explicit_subagent_requests=False,
|
|
1204
|
+
mcp_manager=task_asset_mcp_manager,
|
|
1205
|
+
cancellation_token=cancellation_token,
|
|
1206
|
+
tool_dispatch_guard=write_guard,
|
|
1207
|
+
)
|
|
1208
|
+
except CooperativeCancellationError as e:
|
|
1209
|
+
interrupted = True
|
|
1210
|
+
run_code = 130
|
|
1211
|
+
run_err = f"interrupted at cooperative checkpoint: {e.reason}"
|
|
1212
|
+
except Exception as e: # noqa: BLE001
|
|
1213
|
+
run_code = 1
|
|
1214
|
+
agent_run_exception = True
|
|
1215
|
+
agent_exception_summary = _format_agent_exception_summary(e)
|
|
1216
|
+
# Classify with the shared mapper so a worker that hits a permanent provider
|
|
1217
|
+
# rejection (auth / bad-request / unknown-model) is recorded as PROVIDER_ERROR
|
|
1218
|
+
# and joins the chat/run vocabulary, instead of silently defaulting to
|
|
1219
|
+
# IMPLEMENTATION_FAILED downstream.
|
|
1220
|
+
failure_category = classify_failure_category(e)
|
|
1221
|
+
_persist_agent_exception_traceback(scratch_artifact_dir, e)
|
|
1222
|
+
finally:
|
|
1223
|
+
if task_asset_mcp_manager is not None:
|
|
1224
|
+
task_asset_mcp_manager.close()
|
|
1225
|
+
|
|
1226
|
+
after_runtime_snapshot = snapshot_runtime_tree(worktree_repo_path)
|
|
1227
|
+
runtime_artifacts_changed = before_runtime_snapshot != after_runtime_snapshot
|
|
1228
|
+
if asset_allocation is not None:
|
|
1229
|
+
write_task_asset_allocation(
|
|
1230
|
+
run_paths=run_paths,
|
|
1231
|
+
allocation=asset_allocation,
|
|
1232
|
+
started_at=started_at,
|
|
1233
|
+
)
|
|
1234
|
+
asset_usage_logger.summary(
|
|
1235
|
+
primary_count=len(task_asset_mirror.primary),
|
|
1236
|
+
may_need_count=len(task_asset_mirror.may_need),
|
|
1237
|
+
pinned_count=len(task_asset_mirror.pinned),
|
|
1238
|
+
)
|
|
1239
|
+
head_after_run = head_commit(worktree_repo_path)
|
|
1240
|
+
scope_inspection_error: str | None = None
|
|
1241
|
+
try:
|
|
1242
|
+
reporting_diff = build_execution_reporting_diff_with_commit_range(
|
|
1243
|
+
worktree_repo_path,
|
|
1244
|
+
before_commit=head_before_run,
|
|
1245
|
+
after_commit=head_after_run,
|
|
1246
|
+
)
|
|
1247
|
+
except GitOpsError as e:
|
|
1248
|
+
reporting_diff = build_execution_reporting_diff_with_commit_range(
|
|
1249
|
+
worktree_repo_path,
|
|
1250
|
+
before_commit=None,
|
|
1251
|
+
after_commit=None,
|
|
1252
|
+
)
|
|
1253
|
+
scope_inspection_error = f"scope inspection failed: {e}"
|
|
1254
|
+
patch_path.write_text(reporting_diff.patch_text, encoding="utf-8")
|
|
1255
|
+
scratch_scope_diagnostics = relocate_known_scratch_artifacts(
|
|
1256
|
+
root=worktree_repo_path,
|
|
1257
|
+
artifact_dir=scratch_artifact_dir,
|
|
1258
|
+
)
|
|
1259
|
+
relocated_scratch_paths = {item.path for item in scratch_scope_diagnostics}
|
|
1260
|
+
changed_files = _merge_changed_files(
|
|
1261
|
+
list(reporting_diff.changed_files),
|
|
1262
|
+
list_changed_files_including_untracked(worktree_repo_path),
|
|
1263
|
+
)
|
|
1264
|
+
if relocated_scratch_paths:
|
|
1265
|
+
changed_files = [path for path in changed_files if path not in relocated_scratch_paths]
|
|
1266
|
+
agent_added_non_material_paths: list[str] = []
|
|
1267
|
+
if head_before_run and head_after_run and head_after_run != head_before_run:
|
|
1268
|
+
agent_added_non_material_paths = [
|
|
1269
|
+
path
|
|
1270
|
+
for path in added_files_since(
|
|
1271
|
+
worktree_repo_path,
|
|
1272
|
+
before_commit=head_before_run,
|
|
1273
|
+
after_commit=head_after_run,
|
|
1274
|
+
)
|
|
1275
|
+
if is_non_material_untracked_path(path)
|
|
1276
|
+
]
|
|
1277
|
+
if agent_added_non_material_paths:
|
|
1278
|
+
changed_files = [
|
|
1279
|
+
path for path in changed_files if path not in agent_added_non_material_paths
|
|
1280
|
+
]
|
|
1281
|
+
warnings: list[str] = list(asset_setup_warnings)
|
|
1282
|
+
warnings.extend(_scope_recovery_warning(item) for item in scratch_scope_diagnostics)
|
|
1283
|
+
|
|
1284
|
+
commit_hash: str | None = None
|
|
1285
|
+
verify_failed = False
|
|
1286
|
+
verification_unavailable = False
|
|
1287
|
+
verify_summary: str | None = None
|
|
1288
|
+
verify_payload: dict[str, Any] | None = None
|
|
1289
|
+
failure_reason: str | None = None
|
|
1290
|
+
scope_violation_files: list[str] = []
|
|
1291
|
+
scope_diagnostics: list[dict[str, Any]] = _scope_diagnostics_payload(scratch_scope_diagnostics)
|
|
1292
|
+
result_kind: str | None = None
|
|
1293
|
+
noop_reason: str | None = None
|
|
1294
|
+
material_changes_detected = bool(changed_files)
|
|
1295
|
+
diagnostic_tolerated_side_effects: list[str] = []
|
|
1296
|
+
if (
|
|
1297
|
+
diagnostic_analysis_only
|
|
1298
|
+
and material_changes_detected
|
|
1299
|
+
and _diagnostic_side_effects_are_rust_build_metadata(worktree_repo_path, changed_files)
|
|
1300
|
+
):
|
|
1301
|
+
diagnostic_tolerated_side_effects = list(changed_files)
|
|
1302
|
+
changed_files = []
|
|
1303
|
+
material_changes_detected = False
|
|
1304
|
+
warnings.append(
|
|
1305
|
+
"Diagnostic analysis-only task produced generated Rust build metadata "
|
|
1306
|
+
f"({', '.join(diagnostic_tolerated_side_effects[:20])}); ignored for merge."
|
|
1307
|
+
)
|
|
1308
|
+
nonzero_agent_exit = run_code != 0 and run_err is None and not agent_run_exception
|
|
1309
|
+
strict_scope_blocked = False
|
|
1310
|
+
success = False
|
|
1311
|
+
|
|
1312
|
+
def _append_run_error(message: str) -> None:
|
|
1313
|
+
nonlocal run_err
|
|
1314
|
+
run_err = (run_err + "; " if run_err else "") + message
|
|
1315
|
+
|
|
1316
|
+
if interrupted:
|
|
1317
|
+
failure_reason = failure_reason or "interrupted"
|
|
1318
|
+
failure_category = failure_category or "interrupted"
|
|
1319
|
+
elif nonzero_agent_exit:
|
|
1320
|
+
failure_reason = failure_reason or "agent_nonzero_exit"
|
|
1321
|
+
failure_category = failure_category or FailureCategory.IMPLEMENTATION_FAILED
|
|
1322
|
+
_append_run_error(
|
|
1323
|
+
f"agent exited non-zero ({run_code}); refusing to accept partial worker result"
|
|
1324
|
+
)
|
|
1325
|
+
elif agent_run_exception:
|
|
1326
|
+
failure_reason = failure_reason or "agent_exception"
|
|
1327
|
+
failure_category = failure_category or FailureCategory.IMPLEMENTATION_FAILED
|
|
1328
|
+
|
|
1329
|
+
def _refresh_authoritative_verification_after_material_changes() -> None:
|
|
1330
|
+
nonlocal run_cfg
|
|
1331
|
+
nonlocal verify_cmds
|
|
1332
|
+
nonlocal verify_command_source
|
|
1333
|
+
nonlocal verify_resolution
|
|
1334
|
+
|
|
1335
|
+
if verify_mode == "off" or verify_cmds or diagnostic_analysis_only:
|
|
1336
|
+
return
|
|
1337
|
+
try:
|
|
1338
|
+
post_change_scan = scan_workspace(context=resolve_workspace_context(worktree_repo_path))
|
|
1339
|
+
except (WorkspaceContextError, OSError):
|
|
1340
|
+
return
|
|
1341
|
+
refreshed_contract = resolve_worker_verify_contract(
|
|
1342
|
+
cfg=cfg,
|
|
1343
|
+
verify_mode=verify_mode,
|
|
1344
|
+
verify_commands=None,
|
|
1345
|
+
verify_command_selection=None,
|
|
1346
|
+
task=task,
|
|
1347
|
+
root=worktree_repo_path,
|
|
1348
|
+
repo_scan=post_change_scan,
|
|
1349
|
+
plan_requirements=plan_requirements,
|
|
1350
|
+
)
|
|
1351
|
+
refreshed_cmds = list(refreshed_contract.commands)
|
|
1352
|
+
if not refreshed_cmds:
|
|
1353
|
+
return
|
|
1354
|
+
verify_resolution = refreshed_contract.selection
|
|
1355
|
+
verify_cmds = refreshed_cmds
|
|
1356
|
+
verify_command_source = verify_resolution.source if verify_resolution is not None else None
|
|
1357
|
+
run_cfg.verify_commands = list(refreshed_cmds)
|
|
1358
|
+
resolved_verify_source = verify_command_source or "unknown"
|
|
1359
|
+
worker_trace_sink.emit(
|
|
1360
|
+
build_swarm_trace_event(
|
|
1361
|
+
run_id=run_paths.run_id,
|
|
1362
|
+
task_id=task_id,
|
|
1363
|
+
phase="verify.lifecycle",
|
|
1364
|
+
message=(
|
|
1365
|
+
"Refreshed authoritative verification commands after material changes "
|
|
1366
|
+
f"from {resolved_verify_source}: {', '.join(refreshed_cmds)}."
|
|
1367
|
+
),
|
|
1368
|
+
verbosity="full",
|
|
1369
|
+
)
|
|
1370
|
+
)
|
|
1371
|
+
|
|
1372
|
+
_refresh_authoritative_verification_after_material_changes()
|
|
1373
|
+
|
|
1374
|
+
def _run_authoritative_verification(
|
|
1375
|
+
*,
|
|
1376
|
+
fail_on_failed_verify: bool,
|
|
1377
|
+
fail_on_repo_mutation: bool,
|
|
1378
|
+
failed_verify_reason: str = "verification_failed",
|
|
1379
|
+
failed_verify_error_prefix: str = "verification failed",
|
|
1380
|
+
infra_unavailable_error_prefix: str = "verification infrastructure unavailable",
|
|
1381
|
+
mark_verify_failed: bool = True,
|
|
1382
|
+
allow_baseline_improved_failure: bool = False,
|
|
1383
|
+
) -> bool:
|
|
1384
|
+
nonlocal success
|
|
1385
|
+
nonlocal commit_hash
|
|
1386
|
+
nonlocal verify_failed
|
|
1387
|
+
nonlocal verify_summary
|
|
1388
|
+
nonlocal verify_payload
|
|
1389
|
+
nonlocal failure_reason
|
|
1390
|
+
nonlocal failure_category
|
|
1391
|
+
nonlocal changed_files
|
|
1392
|
+
nonlocal scope_violation_files
|
|
1393
|
+
nonlocal scope_diagnostics
|
|
1394
|
+
|
|
1395
|
+
before_verify_snapshot = snapshot_workspace_tree(worktree_repo_path)
|
|
1396
|
+
verify_result = run_task_verification(
|
|
1397
|
+
root=worktree_repo_path,
|
|
1398
|
+
commands=verify_cmds,
|
|
1399
|
+
artifact_path=verify_path,
|
|
1400
|
+
cfg=run_cfg,
|
|
1401
|
+
)
|
|
1402
|
+
verify_summary = verify_result.summary
|
|
1403
|
+
verify_payload = verify_run_result_to_payload(
|
|
1404
|
+
root=run_paths.root,
|
|
1405
|
+
result=verify_result,
|
|
1406
|
+
)
|
|
1407
|
+
after_verify_snapshot = snapshot_workspace_tree(worktree_repo_path)
|
|
1408
|
+
verify_mutation_diff = build_workspace_snapshot_reporting_diff(
|
|
1409
|
+
worktree_repo_path,
|
|
1410
|
+
before_snapshot=before_verify_snapshot,
|
|
1411
|
+
after_snapshot=after_verify_snapshot,
|
|
1412
|
+
)
|
|
1413
|
+
verify_mutation_paths = list(verify_mutation_diff.changed_files)
|
|
1414
|
+
verify_extra_diagnostics = relocate_known_scratch_artifacts(
|
|
1415
|
+
root=worktree_repo_path,
|
|
1416
|
+
artifact_dir=scratch_artifact_dir,
|
|
1417
|
+
)
|
|
1418
|
+
if verify_extra_diagnostics:
|
|
1419
|
+
relocated_verify_scratch = {item.path for item in verify_extra_diagnostics}
|
|
1420
|
+
verify_mutation_paths = [
|
|
1421
|
+
path for path in verify_mutation_paths if path not in relocated_verify_scratch
|
|
1422
|
+
]
|
|
1423
|
+
warnings.extend(_scope_recovery_warning(item) for item in verify_extra_diagnostics)
|
|
1424
|
+
scope_diagnostics.extend(_scope_diagnostics_payload(verify_extra_diagnostics))
|
|
1425
|
+
verification_ok = True
|
|
1426
|
+
|
|
1427
|
+
if verify_mutation_paths:
|
|
1428
|
+
verify_mutation_msg = "Verification commands modified repository state"
|
|
1429
|
+
if commit_hash is not None:
|
|
1430
|
+
verify_mutation_msg += (
|
|
1431
|
+
f" after the task commit: {', '.join(verify_mutation_paths[:20])}"
|
|
1432
|
+
)
|
|
1433
|
+
else:
|
|
1434
|
+
verify_mutation_msg += f": {', '.join(verify_mutation_paths[:20])}"
|
|
1435
|
+
if len(verify_mutation_paths) > 20:
|
|
1436
|
+
verify_mutation_msg += ", ..."
|
|
1437
|
+
verify_mutation_msg += "."
|
|
1438
|
+
if scope_mode in {"warn", "strict"}:
|
|
1439
|
+
scope_assessment = assess_scope_changes(
|
|
1440
|
+
verify_mutation_paths,
|
|
1441
|
+
allowed_scope,
|
|
1442
|
+
task=task,
|
|
1443
|
+
root=worktree_repo_path,
|
|
1444
|
+
)
|
|
1445
|
+
scope_diagnostics.extend(_scope_diagnostics_payload(scope_assessment.diagnostics))
|
|
1446
|
+
if not scope_assessment.ok:
|
|
1447
|
+
verify_scope_violations = scope_assessment.blocking_paths
|
|
1448
|
+
scope_violation_files = _merge_changed_files(
|
|
1449
|
+
scope_violation_files,
|
|
1450
|
+
verify_scope_violations,
|
|
1451
|
+
)
|
|
1452
|
+
scope_msg = _scope_violation_message(
|
|
1453
|
+
violations=verify_scope_violations,
|
|
1454
|
+
allowed_scope=allowed_scope,
|
|
1455
|
+
blocked=scope_mode == "strict" or fail_on_repo_mutation,
|
|
1456
|
+
diagnostics=_scope_diagnostics_payload(scope_assessment.diagnostics),
|
|
1457
|
+
)
|
|
1458
|
+
if commit_hash is not None:
|
|
1459
|
+
scope_msg += " Verification commands modified repository state after the task commit."
|
|
1460
|
+
else:
|
|
1461
|
+
scope_msg += " Verification commands modified repository state during already-satisfied verification."
|
|
1462
|
+
if scope_mode == "strict" or fail_on_repo_mutation:
|
|
1463
|
+
success = False
|
|
1464
|
+
verification_ok = False
|
|
1465
|
+
failure_reason = "scope_violation"
|
|
1466
|
+
failure_category = failure_category or FailureCategory.IMPLEMENTATION_FAILED
|
|
1467
|
+
commit_hash = None
|
|
1468
|
+
_append_run_error(scope_msg)
|
|
1469
|
+
changed_files = _merge_changed_files(changed_files, verify_mutation_paths)
|
|
1470
|
+
_append_patch_debug_section(
|
|
1471
|
+
patch_path,
|
|
1472
|
+
title="Post-verification workspace diff",
|
|
1473
|
+
patch_text=verify_mutation_diff.patch_text,
|
|
1474
|
+
)
|
|
1475
|
+
else:
|
|
1476
|
+
warnings.append(scope_msg)
|
|
1477
|
+
elif scope_mode == "strict" or fail_on_repo_mutation:
|
|
1478
|
+
success = False
|
|
1479
|
+
verification_ok = False
|
|
1480
|
+
failure_category = failure_category or FailureCategory.IMPLEMENTATION_FAILED
|
|
1481
|
+
commit_hash = None
|
|
1482
|
+
_append_run_error(verify_mutation_msg)
|
|
1483
|
+
changed_files = _merge_changed_files(changed_files, verify_mutation_paths)
|
|
1484
|
+
_append_patch_debug_section(
|
|
1485
|
+
patch_path,
|
|
1486
|
+
title="Post-verification workspace diff",
|
|
1487
|
+
patch_text=verify_mutation_diff.patch_text,
|
|
1488
|
+
)
|
|
1489
|
+
else:
|
|
1490
|
+
warnings.append(verify_mutation_msg)
|
|
1491
|
+
elif fail_on_repo_mutation:
|
|
1492
|
+
success = False
|
|
1493
|
+
verification_ok = False
|
|
1494
|
+
failure_category = failure_category or FailureCategory.IMPLEMENTATION_FAILED
|
|
1495
|
+
commit_hash = None
|
|
1496
|
+
_append_run_error(verify_mutation_msg)
|
|
1497
|
+
changed_files = _merge_changed_files(changed_files, verify_mutation_paths)
|
|
1498
|
+
_append_patch_debug_section(
|
|
1499
|
+
patch_path,
|
|
1500
|
+
title="Post-verification workspace diff",
|
|
1501
|
+
patch_text=verify_mutation_diff.patch_text,
|
|
1502
|
+
)
|
|
1503
|
+
else:
|
|
1504
|
+
warnings.append(verify_mutation_msg)
|
|
1505
|
+
|
|
1506
|
+
if not verify_result.all_passed:
|
|
1507
|
+
verify_failure_category = (
|
|
1508
|
+
verify_result.failure_category_value or FailureCategory.VERIFICATION_FAILED
|
|
1509
|
+
)
|
|
1510
|
+
verify_infra_unavailable = (
|
|
1511
|
+
failure_category_value(verify_failure_category)
|
|
1512
|
+
== FailureCategory.INFRA_UNAVAILABLE.value
|
|
1513
|
+
)
|
|
1514
|
+
worker_had_material_change = (
|
|
1515
|
+
material_changes_detected
|
|
1516
|
+
or commit_hash is not None
|
|
1517
|
+
or bool(head_before_run and head_after_run and head_after_run != head_before_run)
|
|
1518
|
+
)
|
|
1519
|
+
tolerate_warn_infra = (
|
|
1520
|
+
verify_mode == "warn" and verify_infra_unavailable and worker_had_material_change
|
|
1521
|
+
)
|
|
1522
|
+
baseline_comparison: dict[str, Any] | None = None
|
|
1523
|
+
if (
|
|
1524
|
+
allow_baseline_improved_failure
|
|
1525
|
+
and verify_mode == "warn"
|
|
1526
|
+
and fail_on_failed_verify
|
|
1527
|
+
and not verify_infra_unavailable
|
|
1528
|
+
):
|
|
1529
|
+
baseline_result = _run_baseline_verification_snapshot(
|
|
1530
|
+
repo=worktree_repo_path,
|
|
1531
|
+
commit=head_before_run,
|
|
1532
|
+
commands=verify_cmds,
|
|
1533
|
+
artifact_path=baseline_verify_path,
|
|
1534
|
+
cfg=run_cfg,
|
|
1535
|
+
)
|
|
1536
|
+
baseline_comparison = _baseline_improved_failure_comparison(
|
|
1537
|
+
baseline=baseline_result,
|
|
1538
|
+
current=verify_result,
|
|
1539
|
+
task=task,
|
|
1540
|
+
)
|
|
1541
|
+
if baseline_comparison is not None:
|
|
1542
|
+
if verify_payload is not None:
|
|
1543
|
+
verify_payload["baseline_comparison"] = baseline_comparison
|
|
1544
|
+
verify_payload["baseline_artifact_path"] = str(
|
|
1545
|
+
baseline_verify_path.relative_to(run_paths.root)
|
|
1546
|
+
if baseline_verify_path.is_relative_to(run_paths.root)
|
|
1547
|
+
else baseline_verify_path
|
|
1548
|
+
)
|
|
1549
|
+
if baseline_comparison["resolved_failure_count"]:
|
|
1550
|
+
warnings.append(
|
|
1551
|
+
"Verification warning: post-task verification still fails, but it "
|
|
1552
|
+
"reduced pre-existing baseline failures "
|
|
1553
|
+
f"({baseline_comparison['baseline_failure_count']} -> "
|
|
1554
|
+
f"{baseline_comparison['current_failure_count']})."
|
|
1555
|
+
)
|
|
1556
|
+
else:
|
|
1557
|
+
warnings.append(
|
|
1558
|
+
"Verification warning: post-task verification still fails, but the "
|
|
1559
|
+
"remaining failures match the pre-task baseline and are unrelated to "
|
|
1560
|
+
"this task's focus."
|
|
1561
|
+
)
|
|
1562
|
+
return verification_ok
|
|
1563
|
+
if verify_mode == "strict":
|
|
1564
|
+
verify_failed = True
|
|
1565
|
+
failure_reason = failure_reason or (
|
|
1566
|
+
"verification_infra_unavailable"
|
|
1567
|
+
if verify_infra_unavailable
|
|
1568
|
+
else "verification_failed"
|
|
1569
|
+
)
|
|
1570
|
+
failure_category = failure_category or verify_failure_category
|
|
1571
|
+
success = False
|
|
1572
|
+
verification_ok = False
|
|
1573
|
+
if verify_infra_unavailable:
|
|
1574
|
+
_append_run_error(
|
|
1575
|
+
f"strict verification infrastructure unavailable: {verify_result.summary}"
|
|
1576
|
+
)
|
|
1577
|
+
else:
|
|
1578
|
+
_append_run_error(f"strict verification failed: {verify_result.summary}")
|
|
1579
|
+
elif fail_on_failed_verify and not tolerate_warn_infra:
|
|
1580
|
+
if mark_verify_failed:
|
|
1581
|
+
verify_failed = True
|
|
1582
|
+
failure_reason = failure_reason or (
|
|
1583
|
+
"verification_infra_unavailable"
|
|
1584
|
+
if verify_infra_unavailable
|
|
1585
|
+
else failed_verify_reason
|
|
1586
|
+
)
|
|
1587
|
+
failure_category = failure_category or verify_failure_category
|
|
1588
|
+
success = False
|
|
1589
|
+
verification_ok = False
|
|
1590
|
+
if verify_infra_unavailable:
|
|
1591
|
+
_append_run_error(f"{infra_unavailable_error_prefix}: {verify_result.summary}")
|
|
1592
|
+
else:
|
|
1593
|
+
_append_run_error(f"{failed_verify_error_prefix}: {verify_result.summary}")
|
|
1594
|
+
else:
|
|
1595
|
+
warning_prefix = (
|
|
1596
|
+
"Verification infrastructure warning"
|
|
1597
|
+
if verify_infra_unavailable
|
|
1598
|
+
else "Verification warning"
|
|
1599
|
+
)
|
|
1600
|
+
warnings.append(f"{warning_prefix}: {verify_result.summary}")
|
|
1601
|
+
|
|
1602
|
+
return verification_ok and verify_result.all_passed
|
|
1603
|
+
|
|
1604
|
+
if scope_inspection_error:
|
|
1605
|
+
if scope_mode == "strict":
|
|
1606
|
+
strict_scope_blocked = True
|
|
1607
|
+
failure_reason = "scope_violation"
|
|
1608
|
+
failure_category = failure_category or FailureCategory.IMPLEMENTATION_FAILED
|
|
1609
|
+
_append_run_error(scope_inspection_error)
|
|
1610
|
+
else:
|
|
1611
|
+
warnings.append(scope_inspection_error)
|
|
1612
|
+
if scope_mode in {"warn", "strict"}:
|
|
1613
|
+
scope_assessment = assess_scope_changes(
|
|
1614
|
+
changed_files,
|
|
1615
|
+
allowed_scope,
|
|
1616
|
+
task=task,
|
|
1617
|
+
root=worktree_repo_path,
|
|
1618
|
+
extra_diagnostics=scratch_scope_diagnostics,
|
|
1619
|
+
)
|
|
1620
|
+
changed_files = list(scope_assessment.effective_changed_files)
|
|
1621
|
+
scope_diagnostics = _scope_diagnostics_payload(scope_assessment.diagnostics)
|
|
1622
|
+
for diagnostic in scope_assessment.diagnostics:
|
|
1623
|
+
if diagnostic.allowed and diagnostic.classification != "in_scope":
|
|
1624
|
+
warning = _scope_recovery_warning(diagnostic)
|
|
1625
|
+
if warning not in warnings:
|
|
1626
|
+
warnings.append(warning)
|
|
1627
|
+
if not scope_assessment.ok:
|
|
1628
|
+
violations = scope_assessment.blocking_paths
|
|
1629
|
+
scope_violation_files = list(violations)
|
|
1630
|
+
scope_msg = _scope_violation_message(
|
|
1631
|
+
violations=violations,
|
|
1632
|
+
allowed_scope=allowed_scope,
|
|
1633
|
+
blocked=scope_mode == "strict",
|
|
1634
|
+
diagnostics=scope_diagnostics,
|
|
1635
|
+
)
|
|
1636
|
+
if scope_mode == "strict":
|
|
1637
|
+
strict_scope_blocked = True
|
|
1638
|
+
failure_reason = "scope_violation"
|
|
1639
|
+
failure_category = failure_category or FailureCategory.IMPLEMENTATION_FAILED
|
|
1640
|
+
_append_run_error(scope_msg)
|
|
1641
|
+
else:
|
|
1642
|
+
warnings.append(scope_msg)
|
|
1643
|
+
|
|
1644
|
+
guard_violations = write_guard.violations if write_guard is not None else []
|
|
1645
|
+
if guard_violations:
|
|
1646
|
+
strict_scope_blocked = True
|
|
1647
|
+
if not interrupted:
|
|
1648
|
+
failure_reason = "write_scope_guard_violation"
|
|
1649
|
+
failure_category = failure_category or FailureCategory.IMPLEMENTATION_FAILED
|
|
1650
|
+
scope_violation_files = _merge_changed_files(
|
|
1651
|
+
scope_violation_files,
|
|
1652
|
+
[item.resolved_relpath or item.raw_path for item in guard_violations],
|
|
1653
|
+
)
|
|
1654
|
+
scope_diagnostics.extend(item.to_payload() for item in guard_violations)
|
|
1655
|
+
violation_preview = "; ".join(
|
|
1656
|
+
f"{item.tool_name}: {item.raw_path} ({item.reason})" for item in guard_violations[:5]
|
|
1657
|
+
)
|
|
1658
|
+
if len(guard_violations) > 5:
|
|
1659
|
+
violation_preview += "; ..."
|
|
1660
|
+
_append_run_error(
|
|
1661
|
+
f"swarm write-scope guard blocked {len(guard_violations)} write(s) "
|
|
1662
|
+
f"outside the task write scope: {violation_preview}"
|
|
1663
|
+
)
|
|
1664
|
+
for item in guard_violations:
|
|
1665
|
+
worker_trace_sink.emit(
|
|
1666
|
+
build_swarm_trace_event(
|
|
1667
|
+
run_id=run_paths.run_id,
|
|
1668
|
+
task_id=task_id,
|
|
1669
|
+
phase="scope.violation",
|
|
1670
|
+
message=(
|
|
1671
|
+
f"Write blocked by swarm write-scope guard ({item.reason}): "
|
|
1672
|
+
f"{item.tool_name} -> {item.raw_path}"
|
|
1673
|
+
),
|
|
1674
|
+
)
|
|
1675
|
+
)
|
|
1676
|
+
|
|
1677
|
+
can_attempt_commit_verify = (
|
|
1678
|
+
not runtime_artifacts_changed
|
|
1679
|
+
and material_changes_detected
|
|
1680
|
+
and not strict_scope_blocked
|
|
1681
|
+
and run_code == 0
|
|
1682
|
+
)
|
|
1683
|
+
|
|
1684
|
+
if can_attempt_commit_verify:
|
|
1685
|
+
try:
|
|
1686
|
+
agent_commit_hash: str | None = None
|
|
1687
|
+
if head_before_run and head_after_run and head_after_run != head_before_run:
|
|
1688
|
+
agent_commit_hash = head_after_run
|
|
1689
|
+
agent_commit_paths = changed_files_since(
|
|
1690
|
+
worktree_repo_path,
|
|
1691
|
+
before_commit=head_before_run,
|
|
1692
|
+
after_commit=head_after_run,
|
|
1693
|
+
)
|
|
1694
|
+
protected_violations = _protected_path_violations(
|
|
1695
|
+
agent_commit_paths,
|
|
1696
|
+
protected_prefixes,
|
|
1697
|
+
)
|
|
1698
|
+
if protected_violations:
|
|
1699
|
+
preview = ", ".join(protected_violations[:20])
|
|
1700
|
+
if len(protected_violations) > 20:
|
|
1701
|
+
preview += ", ..."
|
|
1702
|
+
raise GitOpsError(f"worker created commit touching protected paths: {preview}")
|
|
1703
|
+
agent_added_non_material_paths = [
|
|
1704
|
+
path
|
|
1705
|
+
for path in added_files_since(
|
|
1706
|
+
worktree_repo_path,
|
|
1707
|
+
before_commit=head_before_run,
|
|
1708
|
+
after_commit=head_after_run,
|
|
1709
|
+
)
|
|
1710
|
+
if is_non_material_untracked_path(path)
|
|
1711
|
+
]
|
|
1712
|
+
if agent_added_non_material_paths:
|
|
1713
|
+
reset_mixed(worktree_repo_path, target=head_before_run)
|
|
1714
|
+
agent_commit_hash = None
|
|
1715
|
+
non_material_untracked_paths = list_untracked_packaging_metadata_paths(
|
|
1716
|
+
worktree_repo_path
|
|
1717
|
+
)
|
|
1718
|
+
stage_all(worktree_repo_path)
|
|
1719
|
+
unstage_staged_prefixes(worktree_repo_path, protected_prefixes)
|
|
1720
|
+
ensure_not_staged_prefixes(worktree_repo_path, protected_prefixes)
|
|
1721
|
+
if non_material_untracked_paths:
|
|
1722
|
+
unstage_staged_paths(worktree_repo_path, non_material_untracked_paths)
|
|
1723
|
+
ensure_not_staged_paths(worktree_repo_path, non_material_untracked_paths)
|
|
1724
|
+
staged_now = staged_files(worktree_repo_path)
|
|
1725
|
+
if staged_now and has_grounded_rust_target_runtime_artifacts(worktree_repo_path):
|
|
1726
|
+
unstage_staged_runtime_artifacts(
|
|
1727
|
+
worktree_repo_path,
|
|
1728
|
+
current_paths=staged_now,
|
|
1729
|
+
)
|
|
1730
|
+
staged_now = staged_files(worktree_repo_path)
|
|
1731
|
+
ensure_not_staged_runtime_artifacts(
|
|
1732
|
+
worktree_repo_path,
|
|
1733
|
+
current_paths=staged_now,
|
|
1734
|
+
)
|
|
1735
|
+
if staged_now:
|
|
1736
|
+
commit_hash = commit_all(
|
|
1737
|
+
worktree_repo_path,
|
|
1738
|
+
message=f"{task_id}: {task_title or 'task update'}",
|
|
1739
|
+
)
|
|
1740
|
+
elif agent_commit_hash is not None:
|
|
1741
|
+
commit_hash = agent_commit_hash
|
|
1742
|
+
else:
|
|
1743
|
+
raise GitOpsError("no repository changes were staged for commit")
|
|
1744
|
+
success = True
|
|
1745
|
+
patch_text = format_patch_stdout(worktree_repo_path, base_branch=base_branch)
|
|
1746
|
+
patch_path.write_text(
|
|
1747
|
+
patch_text if patch_text else "(empty format-patch output)\n",
|
|
1748
|
+
encoding="utf-8",
|
|
1749
|
+
)
|
|
1750
|
+
changed_files = changed_files_between(
|
|
1751
|
+
worktree_repo_path,
|
|
1752
|
+
revspec=f"{base_branch}..HEAD",
|
|
1753
|
+
)
|
|
1754
|
+
if verify_mode != "off" and verify_cmds:
|
|
1755
|
+
_run_authoritative_verification(
|
|
1756
|
+
fail_on_failed_verify=True,
|
|
1757
|
+
fail_on_repo_mutation=False,
|
|
1758
|
+
failed_verify_reason="verification_failed",
|
|
1759
|
+
failed_verify_error_prefix="verification failed",
|
|
1760
|
+
infra_unavailable_error_prefix="verification infrastructure unavailable",
|
|
1761
|
+
mark_verify_failed=True,
|
|
1762
|
+
allow_baseline_improved_failure=True,
|
|
1763
|
+
)
|
|
1764
|
+
elif verify_mode == "strict":
|
|
1765
|
+
# The commit exists and no authoritative command exists to check it --
|
|
1766
|
+
# a property of the workspace, not a defect in the work. Failing here
|
|
1767
|
+
# discarded finished tasks over missing tooling, so the worker now
|
|
1768
|
+
# reports an unverified completion: the commit and its worktree are
|
|
1769
|
+
# kept, and the orchestrator merges nothing.
|
|
1770
|
+
verification_unavailable = True
|
|
1771
|
+
verify_summary = (
|
|
1772
|
+
"verification skipped: no authoritative commands available; "
|
|
1773
|
+
"task kept as completed_unverified"
|
|
1774
|
+
)
|
|
1775
|
+
warnings.append(
|
|
1776
|
+
"Strict verification found no authoritative command for this task. "
|
|
1777
|
+
"The work was committed but nothing checked it -- review the task "
|
|
1778
|
+
"branch before merging, or provide an explicit verify command."
|
|
1779
|
+
)
|
|
1780
|
+
elif verify_mode != "off":
|
|
1781
|
+
verify_summary = "verification skipped: no authoritative commands available"
|
|
1782
|
+
else:
|
|
1783
|
+
verify_summary = "verification disabled (--verify off)"
|
|
1784
|
+
if success:
|
|
1785
|
+
result_kind = "success_commit"
|
|
1786
|
+
except GitOpsError as e:
|
|
1787
|
+
success = False
|
|
1788
|
+
failure_category = failure_category or FailureCategory.IMPLEMENTATION_FAILED
|
|
1789
|
+
_append_run_error(f"worker commit/patch failed: {e}")
|
|
1790
|
+
elif (
|
|
1791
|
+
not runtime_artifacts_changed
|
|
1792
|
+
and not material_changes_detected
|
|
1793
|
+
and not strict_scope_blocked
|
|
1794
|
+
and run_err is None
|
|
1795
|
+
and scope_inspection_error is None
|
|
1796
|
+
and run_code == 0
|
|
1797
|
+
):
|
|
1798
|
+
if (
|
|
1799
|
+
diagnostic_analysis_only
|
|
1800
|
+
and not agent_run_exception
|
|
1801
|
+
and failure_category != FailureCategory.PROVIDER_THROTTLED
|
|
1802
|
+
):
|
|
1803
|
+
success = True
|
|
1804
|
+
result_kind = "success_noop"
|
|
1805
|
+
noop_reason = "diagnostic_analysis_only"
|
|
1806
|
+
verify_summary = "verification skipped: diagnostic analysis-only task made no changes"
|
|
1807
|
+
if run_code != 0:
|
|
1808
|
+
warnings.append(
|
|
1809
|
+
"Diagnostic analysis-only task exited non-zero after making no repository "
|
|
1810
|
+
"changes; accepted as a non-blocking diagnostic no-op."
|
|
1811
|
+
)
|
|
1812
|
+
elif (
|
|
1813
|
+
conditional_noop_allowed
|
|
1814
|
+
and not agent_run_exception
|
|
1815
|
+
and failure_category != FailureCategory.PROVIDER_THROTTLED
|
|
1816
|
+
):
|
|
1817
|
+
success = True
|
|
1818
|
+
result_kind = "success_noop"
|
|
1819
|
+
noop_reason = "conditional_noop"
|
|
1820
|
+
verify_summary = "verification skipped: conditional task made no changes"
|
|
1821
|
+
if run_code != 0:
|
|
1822
|
+
warnings.append(
|
|
1823
|
+
"Conditional task exited non-zero after making no repository changes; "
|
|
1824
|
+
"accepted because the task text allowed no work when not applicable."
|
|
1825
|
+
)
|
|
1826
|
+
elif verify_mode == "off":
|
|
1827
|
+
success = True
|
|
1828
|
+
result_kind = "success_noop"
|
|
1829
|
+
noop_reason = "already_satisfied"
|
|
1830
|
+
verify_summary = "verification skipped: no changes needed"
|
|
1831
|
+
elif not verify_cmds:
|
|
1832
|
+
success = True
|
|
1833
|
+
result_kind = "success_noop"
|
|
1834
|
+
noop_reason = "already_satisfied"
|
|
1835
|
+
verify_summary = "verification skipped: no changes needed"
|
|
1836
|
+
else:
|
|
1837
|
+
success = True
|
|
1838
|
+
if _run_authoritative_verification(
|
|
1839
|
+
fail_on_failed_verify=True,
|
|
1840
|
+
fail_on_repo_mutation=True,
|
|
1841
|
+
failed_verify_reason="noop_verification_failed",
|
|
1842
|
+
failed_verify_error_prefix="already-satisfied verification failed",
|
|
1843
|
+
infra_unavailable_error_prefix=(
|
|
1844
|
+
"already-satisfied verification infrastructure unavailable"
|
|
1845
|
+
),
|
|
1846
|
+
mark_verify_failed=False,
|
|
1847
|
+
):
|
|
1848
|
+
result_kind = "success_noop"
|
|
1849
|
+
noop_reason = "already_satisfied"
|
|
1850
|
+
else:
|
|
1851
|
+
success = False
|
|
1852
|
+
|
|
1853
|
+
salvaged_nonzero_exit = False
|
|
1854
|
+
salvaged_agent_exception = False
|
|
1855
|
+
if not success and agent_exception_summary:
|
|
1856
|
+
agent_exception_error = f"agent raised: {agent_exception_summary}"
|
|
1857
|
+
run_err = f"{agent_exception_error}; {run_err}" if run_err else agent_exception_error
|
|
1858
|
+
if not success and failure_category is None:
|
|
1859
|
+
# TODO(failure-category-audit): default=implementation_failed,
|
|
1860
|
+
# site=swarm_worker generic failure result, see failure_category.py
|
|
1861
|
+
failure_category = FailureCategory.IMPLEMENTATION_FAILED
|
|
1862
|
+
|
|
1863
|
+
if interrupted:
|
|
1864
|
+
summary = (
|
|
1865
|
+
"Worker interrupted by cooperative cancellation at a checkpoint; "
|
|
1866
|
+
"the task worktree is preserved for harvest."
|
|
1867
|
+
)
|
|
1868
|
+
elif runtime_artifacts_changed:
|
|
1869
|
+
summary = "Worker failed: attempted protected .alysis modifications."
|
|
1870
|
+
elif failure_reason == "write_scope_guard_violation":
|
|
1871
|
+
summary = "Worker blocked by the swarm write-scope guard."
|
|
1872
|
+
elif failure_reason == "scope_violation":
|
|
1873
|
+
summary = "Worker blocked due to strict scope isolation."
|
|
1874
|
+
elif success and result_kind == "success_noop" and noop_reason == "diagnostic_analysis_only":
|
|
1875
|
+
summary = "Worker completed diagnostic analysis with no repository changes."
|
|
1876
|
+
elif success and result_kind == "success_noop" and noop_reason == "conditional_noop":
|
|
1877
|
+
summary = "Worker completed conditional task with no repository changes."
|
|
1878
|
+
elif success and result_kind == "success_noop":
|
|
1879
|
+
summary = (
|
|
1880
|
+
"Worker completed successfully with no repository changes; task was already satisfied."
|
|
1881
|
+
)
|
|
1882
|
+
elif success and verification_unavailable:
|
|
1883
|
+
summary = (
|
|
1884
|
+
"Worker completed and produced a task commit, but nothing verified it: "
|
|
1885
|
+
"no authoritative verification command exists for this workspace."
|
|
1886
|
+
)
|
|
1887
|
+
elif success:
|
|
1888
|
+
summary = "Worker completed successfully and produced a task commit."
|
|
1889
|
+
else:
|
|
1890
|
+
summary = "Worker failed."
|
|
1891
|
+
if run_err:
|
|
1892
|
+
summary += f" Error: {run_err}"
|
|
1893
|
+
if warnings:
|
|
1894
|
+
summary += " Warnings: " + " | ".join(warnings)
|
|
1895
|
+
|
|
1896
|
+
finished_at = now_iso()
|
|
1897
|
+
exec_artifacts = write_exec_log_artifacts(
|
|
1898
|
+
paths=run_paths,
|
|
1899
|
+
task_id=task_id,
|
|
1900
|
+
cfg=run_cfg,
|
|
1901
|
+
no_log=no_log,
|
|
1902
|
+
before_logs=set(),
|
|
1903
|
+
sessions_dir=run_paths.execution_sessions_dir,
|
|
1904
|
+
expected_session_id=safe_task,
|
|
1905
|
+
)
|
|
1906
|
+
report_path = write_task_report(
|
|
1907
|
+
paths=run_paths,
|
|
1908
|
+
task=task,
|
|
1909
|
+
result="success" if success else "failure",
|
|
1910
|
+
result_kind=result_kind or ("success_commit" if success else "failure"),
|
|
1911
|
+
summary=summary,
|
|
1912
|
+
started_at=started_at,
|
|
1913
|
+
finished_at=finished_at,
|
|
1914
|
+
changed_files=changed_files,
|
|
1915
|
+
verify_commands=verify_cmds,
|
|
1916
|
+
patch_path=patch_path,
|
|
1917
|
+
budget_artifact_path=budget_artifact_path,
|
|
1918
|
+
execution_log_artifacts=exec_artifacts,
|
|
1919
|
+
verify_artifact_path=verify_path if verify_path.exists() else None,
|
|
1920
|
+
verify_summary=verify_summary,
|
|
1921
|
+
verify_payload=verify_payload,
|
|
1922
|
+
verify_command_source=verify_command_source,
|
|
1923
|
+
task_kind=task_lifecycle.kind,
|
|
1924
|
+
task_lifecycle_reason=task_lifecycle.reason_code,
|
|
1925
|
+
base_branch=base_branch,
|
|
1926
|
+
task_branch=task_branch,
|
|
1927
|
+
commit_hash=commit_hash,
|
|
1928
|
+
merge_commit_hash=None,
|
|
1929
|
+
merge_result=(
|
|
1930
|
+
"no merge required (analysis-only no-op)"
|
|
1931
|
+
if result_kind == "success_noop" and noop_reason == "diagnostic_analysis_only"
|
|
1932
|
+
else (
|
|
1933
|
+
"no merge required (conditional no-op)"
|
|
1934
|
+
if result_kind == "success_noop" and noop_reason == "conditional_noop"
|
|
1935
|
+
else (
|
|
1936
|
+
"no merge required (already-satisfied no-op)"
|
|
1937
|
+
if result_kind == "success_noop"
|
|
1938
|
+
else "not merged"
|
|
1939
|
+
)
|
|
1940
|
+
)
|
|
1941
|
+
),
|
|
1942
|
+
salvaged_nonzero_exit=salvaged_nonzero_exit,
|
|
1943
|
+
salvaged_agent_exception=salvaged_agent_exception,
|
|
1944
|
+
agent_exception_summary=agent_exception_summary,
|
|
1945
|
+
noop_reason=noop_reason,
|
|
1946
|
+
)
|
|
1947
|
+
persisted_capture = persist_execution_knowledge_capture(
|
|
1948
|
+
paths=run_paths,
|
|
1949
|
+
task=task,
|
|
1950
|
+
source="swarm_worker",
|
|
1951
|
+
assistant_message=recording_surface.final_assistant_message,
|
|
1952
|
+
artifact_dir=(
|
|
1953
|
+
run_paths.execution_knowledge_capture_dir
|
|
1954
|
+
/ safe_task
|
|
1955
|
+
/ safe_task_file_component(started_at)
|
|
1956
|
+
),
|
|
1957
|
+
report_path=report_path,
|
|
1958
|
+
patch_path=patch_path,
|
|
1959
|
+
verify_artifact_path=verify_path if verify_path.exists() else None,
|
|
1960
|
+
budget_artifact_path=budget_artifact_path,
|
|
1961
|
+
session_artifact_dir=exec_artifacts.session_artifact_dir,
|
|
1962
|
+
)
|
|
1963
|
+
if not success:
|
|
1964
|
+
mark_knowledge_capture_promotion_skipped(
|
|
1965
|
+
artifact_dir=persisted_capture.artifact_dir,
|
|
1966
|
+
reason="worker execution outcome was not accepted",
|
|
1967
|
+
)
|
|
1968
|
+
|
|
1969
|
+
task_attempt_entry = write_task_attempt_entry(
|
|
1970
|
+
paths=run_paths,
|
|
1971
|
+
task=task,
|
|
1972
|
+
source="swarm_worker",
|
|
1973
|
+
result="success" if success else "failure",
|
|
1974
|
+
summary=summary,
|
|
1975
|
+
changed_files=changed_files,
|
|
1976
|
+
verify_summary=verify_summary,
|
|
1977
|
+
report_path=report_path,
|
|
1978
|
+
patch_path=patch_path,
|
|
1979
|
+
verify_artifact_path=verify_path if verify_path.exists() else None,
|
|
1980
|
+
budget_artifact_path=budget_artifact_path,
|
|
1981
|
+
session_artifact_dir=exec_artifacts.session_artifact_dir,
|
|
1982
|
+
acceptance_state="pending" if success else "rejected",
|
|
1983
|
+
extra_tags=(
|
|
1984
|
+
["execution", "worker", result_kind] if result_kind else ["execution", "worker"]
|
|
1985
|
+
),
|
|
1986
|
+
)
|
|
1987
|
+
|
|
1988
|
+
result = TaskWorkerResult(
|
|
1989
|
+
task_id=task_id,
|
|
1990
|
+
title=task_title,
|
|
1991
|
+
branch=task_branch,
|
|
1992
|
+
worktree_path=os.fspath(worktree_repo_path),
|
|
1993
|
+
started_at=started_at,
|
|
1994
|
+
finished_at=finished_at,
|
|
1995
|
+
success=success,
|
|
1996
|
+
summary=summary,
|
|
1997
|
+
commit_hash=commit_hash,
|
|
1998
|
+
error=run_err,
|
|
1999
|
+
report_path=_repo_rel(run_paths.root, report_path),
|
|
2000
|
+
patch_path=_repo_rel(run_paths.root, patch_path),
|
|
2001
|
+
log_path=_repo_rel(run_paths.root, exec_artifacts.log_copy_path),
|
|
2002
|
+
log_pointer_path=_repo_rel(run_paths.root, exec_artifacts.pointer_path),
|
|
2003
|
+
warnings=warnings,
|
|
2004
|
+
changed_files=changed_files,
|
|
2005
|
+
verify_failed=verify_failed,
|
|
2006
|
+
verification_unavailable=verification_unavailable,
|
|
2007
|
+
verify_summary=verify_summary,
|
|
2008
|
+
verify_artifact_path=(
|
|
2009
|
+
_repo_rel(run_paths.root, verify_path) if verify_path.exists() else None
|
|
2010
|
+
),
|
|
2011
|
+
knowledge_capture_artifact_dir=_repo_rel(run_paths.root, persisted_capture.artifact_dir),
|
|
2012
|
+
task_attempt_entry_id=task_attempt_entry.id,
|
|
2013
|
+
task_attempt_knowledge_file_path=(
|
|
2014
|
+
_repo_rel(run_paths.root, task_attempt_entry.file_path)
|
|
2015
|
+
if task_attempt_entry.file_path is not None
|
|
2016
|
+
else None
|
|
2017
|
+
),
|
|
2018
|
+
verify_payload=verify_payload,
|
|
2019
|
+
verify_command_source=verify_command_source,
|
|
2020
|
+
failure_reason=failure_reason,
|
|
2021
|
+
failure_category=failure_category,
|
|
2022
|
+
scope_violation_files=(list(scope_violation_files) if scope_violation_files else None),
|
|
2023
|
+
scope_diagnostics=(list(scope_diagnostics) if scope_diagnostics else None),
|
|
2024
|
+
allowed_scope=list(allowed_scope),
|
|
2025
|
+
agent_exit_code=run_code,
|
|
2026
|
+
salvaged_nonzero_exit=salvaged_nonzero_exit,
|
|
2027
|
+
salvaged_agent_exception=salvaged_agent_exception,
|
|
2028
|
+
agent_exception_summary=agent_exception_summary,
|
|
2029
|
+
result_kind=result_kind,
|
|
2030
|
+
noop_reason=noop_reason,
|
|
2031
|
+
task_kind=task_lifecycle.kind,
|
|
2032
|
+
task_lifecycle_reason=task_lifecycle.reason_code,
|
|
2033
|
+
interrupted=interrupted,
|
|
2034
|
+
)
|
|
2035
|
+
issue_paths = changed_files or list(allowed_scope)
|
|
2036
|
+
if runtime_artifacts_changed:
|
|
2037
|
+
write_issue_entry(
|
|
2038
|
+
paths=run_paths,
|
|
2039
|
+
task=task,
|
|
2040
|
+
source="swarm_worker",
|
|
2041
|
+
title=f"{task_id}: protected .alysis mutation attempt",
|
|
2042
|
+
summary="Worker execution attempted to modify protected .alysis runtime state.",
|
|
2043
|
+
paths_in_scope=issue_paths,
|
|
2044
|
+
report_path=report_path,
|
|
2045
|
+
patch_path=patch_path,
|
|
2046
|
+
verify_artifact_path=verify_path if verify_path.exists() else None,
|
|
2047
|
+
budget_artifact_path=budget_artifact_path,
|
|
2048
|
+
session_artifact_dir=exec_artifacts.session_artifact_dir,
|
|
2049
|
+
tags=["protected_runtime_mutation"],
|
|
2050
|
+
)
|
|
2051
|
+
elif failure_reason == "noop_verification_failed":
|
|
2052
|
+
write_issue_entry(
|
|
2053
|
+
paths=run_paths,
|
|
2054
|
+
task=task,
|
|
2055
|
+
source="swarm_worker",
|
|
2056
|
+
title=f"{task_id}: already-satisfied verification failed",
|
|
2057
|
+
summary=verify_summary or "Authoritative verification failed for zero-diff success.",
|
|
2058
|
+
paths_in_scope=issue_paths,
|
|
2059
|
+
report_path=report_path,
|
|
2060
|
+
patch_path=patch_path,
|
|
2061
|
+
verify_artifact_path=verify_path if verify_path.exists() else None,
|
|
2062
|
+
budget_artifact_path=budget_artifact_path,
|
|
2063
|
+
session_artifact_dir=exec_artifacts.session_artifact_dir,
|
|
2064
|
+
tags=["verification_failure", "noop_verification_failure"],
|
|
2065
|
+
)
|
|
2066
|
+
elif failure_reason == "verification_infra_unavailable":
|
|
2067
|
+
write_issue_entry(
|
|
2068
|
+
paths=run_paths,
|
|
2069
|
+
task=task,
|
|
2070
|
+
source="swarm_worker",
|
|
2071
|
+
title=f"{task_id}: verification infrastructure unavailable",
|
|
2072
|
+
summary=verify_summary or "Worker verification could not run due to infrastructure.",
|
|
2073
|
+
paths_in_scope=issue_paths,
|
|
2074
|
+
report_path=report_path,
|
|
2075
|
+
patch_path=patch_path,
|
|
2076
|
+
verify_artifact_path=verify_path if verify_path.exists() else None,
|
|
2077
|
+
budget_artifact_path=budget_artifact_path,
|
|
2078
|
+
session_artifact_dir=exec_artifacts.session_artifact_dir,
|
|
2079
|
+
tags=["verification_failure", "infra_unavailable"],
|
|
2080
|
+
)
|
|
2081
|
+
elif verify_failed:
|
|
2082
|
+
write_issue_entry(
|
|
2083
|
+
paths=run_paths,
|
|
2084
|
+
task=task,
|
|
2085
|
+
source="swarm_worker",
|
|
2086
|
+
title=f"{task_id}: strict verification failed",
|
|
2087
|
+
summary=verify_summary or "Worker verification failed.",
|
|
2088
|
+
paths_in_scope=issue_paths,
|
|
2089
|
+
report_path=report_path,
|
|
2090
|
+
patch_path=patch_path,
|
|
2091
|
+
verify_artifact_path=verify_path if verify_path.exists() else None,
|
|
2092
|
+
budget_artifact_path=budget_artifact_path,
|
|
2093
|
+
session_artifact_dir=exec_artifacts.session_artifact_dir,
|
|
2094
|
+
tags=["verification_failure"],
|
|
2095
|
+
)
|
|
2096
|
+
elif not success:
|
|
2097
|
+
write_issue_entry(
|
|
2098
|
+
paths=run_paths,
|
|
2099
|
+
task=task,
|
|
2100
|
+
source="swarm_worker",
|
|
2101
|
+
title=f"{task_id}: worker execution failed",
|
|
2102
|
+
summary=summary,
|
|
2103
|
+
paths_in_scope=issue_paths,
|
|
2104
|
+
report_path=report_path,
|
|
2105
|
+
patch_path=patch_path,
|
|
2106
|
+
verify_artifact_path=verify_path if verify_path.exists() else None,
|
|
2107
|
+
budget_artifact_path=budget_artifact_path,
|
|
2108
|
+
session_artifact_dir=exec_artifacts.session_artifact_dir,
|
|
2109
|
+
tags=(
|
|
2110
|
+
["execution_failure", "scope_violation"]
|
|
2111
|
+
if failure_reason == "scope_violation"
|
|
2112
|
+
else ["execution_failure"]
|
|
2113
|
+
),
|
|
2114
|
+
)
|
|
2115
|
+
# Workers finish concurrently. Serializing the shared workspace index
|
|
2116
|
+
# avoids Windows replace/share violations while preserving atomic writes.
|
|
2117
|
+
with _KNOWLEDGE_INDEX_REBUILD_LOCK:
|
|
2118
|
+
rebuild_knowledge_index(run_paths)
|
|
2119
|
+
return result
|