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,992 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .direction_change import filter_obsolete_direction_paths
|
|
8
|
+
from .file_classification import is_docs_path, is_test_path
|
|
9
|
+
from .task_scope import (
|
|
10
|
+
extract_forbidden_repo_path_hints,
|
|
11
|
+
extract_repo_path_hints,
|
|
12
|
+
is_agent_internal_scope_path,
|
|
13
|
+
split_normalized_repo_path_list,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
MUTATING_TASK_SCOPE_WARNING = (
|
|
17
|
+
"runnable or ambiguous task requires runnable estimated_files/write_scope; provide "
|
|
18
|
+
"repo-relative file scope, explicit repo-relative path hints, or mark the task as "
|
|
19
|
+
"analysis-only/report-only."
|
|
20
|
+
)
|
|
21
|
+
EXECUTION_UNREADY_SCOPE_WARNING = (
|
|
22
|
+
"runnable or ambiguous task lacks runnable estimated_files/write_scope; include "
|
|
23
|
+
"repo-relative file paths in the task, use /plan edit / planner flow to produce "
|
|
24
|
+
"execution-ready scope, or mark the task as analysis-only/report-only."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_TASK_ACTION_WORD_RE = re.compile(r"[a-z][a-z0-9_-]*")
|
|
28
|
+
_MUTATING_TASK_ACTIONS = frozenset(
|
|
29
|
+
{
|
|
30
|
+
"add",
|
|
31
|
+
"build",
|
|
32
|
+
"change",
|
|
33
|
+
"configure",
|
|
34
|
+
"create",
|
|
35
|
+
"document",
|
|
36
|
+
"edit",
|
|
37
|
+
"enable",
|
|
38
|
+
"fix",
|
|
39
|
+
"improve",
|
|
40
|
+
"implement",
|
|
41
|
+
"make",
|
|
42
|
+
"migrate",
|
|
43
|
+
"modify",
|
|
44
|
+
"patch",
|
|
45
|
+
"refactor",
|
|
46
|
+
"remove",
|
|
47
|
+
"rename",
|
|
48
|
+
"repair",
|
|
49
|
+
"support",
|
|
50
|
+
"test",
|
|
51
|
+
"touch",
|
|
52
|
+
"update",
|
|
53
|
+
"upgrade",
|
|
54
|
+
"wire",
|
|
55
|
+
"write",
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
_WEAK_MUTATING_TASK_ACTIONS = frozenset({"change", "test"})
|
|
59
|
+
_NON_MUTATING_TASK_ACTIONS = frozenset(
|
|
60
|
+
{
|
|
61
|
+
"analyze",
|
|
62
|
+
"analyse",
|
|
63
|
+
"assess",
|
|
64
|
+
"audit",
|
|
65
|
+
"compare",
|
|
66
|
+
"diagnose",
|
|
67
|
+
"examine",
|
|
68
|
+
"explain",
|
|
69
|
+
"explore",
|
|
70
|
+
"find",
|
|
71
|
+
"inspect",
|
|
72
|
+
"investigate",
|
|
73
|
+
"identify",
|
|
74
|
+
"locate",
|
|
75
|
+
"map",
|
|
76
|
+
"report",
|
|
77
|
+
"review",
|
|
78
|
+
"summarize",
|
|
79
|
+
"summarise",
|
|
80
|
+
"survey",
|
|
81
|
+
"triage",
|
|
82
|
+
"understand",
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
_NON_MUTATING_TASK_PHRASES = (
|
|
86
|
+
"analysis only",
|
|
87
|
+
"explain the issue",
|
|
88
|
+
"investigate and report",
|
|
89
|
+
"no code changes",
|
|
90
|
+
"no file changes",
|
|
91
|
+
"read current",
|
|
92
|
+
"read-only",
|
|
93
|
+
"readonly",
|
|
94
|
+
"report findings",
|
|
95
|
+
"report only",
|
|
96
|
+
"review and report",
|
|
97
|
+
"summarize findings",
|
|
98
|
+
"summarise findings",
|
|
99
|
+
)
|
|
100
|
+
_REPORT_ONLY_DOCUMENTATION_PHRASES = (
|
|
101
|
+
"document findings",
|
|
102
|
+
"document the findings",
|
|
103
|
+
"findings documented",
|
|
104
|
+
"findings are documented",
|
|
105
|
+
"root cause documented",
|
|
106
|
+
"root cause is documented",
|
|
107
|
+
"summary documented",
|
|
108
|
+
"summary is documented",
|
|
109
|
+
)
|
|
110
|
+
_REPORT_ONLY_DOCUMENTATION_MUTATING_TOKENS = frozenset({"document"})
|
|
111
|
+
_NOUN_PRONE_MUTATING_TASK_ACTIONS = frozenset(
|
|
112
|
+
{
|
|
113
|
+
"build",
|
|
114
|
+
"change",
|
|
115
|
+
"make",
|
|
116
|
+
"patch",
|
|
117
|
+
"repair",
|
|
118
|
+
"support",
|
|
119
|
+
"test",
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
_TASK_ACTION_CLAUSE_CONNECTORS = frozenset(
|
|
123
|
+
{
|
|
124
|
+
"and",
|
|
125
|
+
"must",
|
|
126
|
+
"need",
|
|
127
|
+
"needs",
|
|
128
|
+
"require",
|
|
129
|
+
"requires",
|
|
130
|
+
"should",
|
|
131
|
+
"then",
|
|
132
|
+
"to",
|
|
133
|
+
"will",
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
_TASK_ACTION_TOKEN_CANONICAL_MAP = {
|
|
137
|
+
"added": "add",
|
|
138
|
+
"adding": "add",
|
|
139
|
+
"adds": "add",
|
|
140
|
+
"analysed": "analyse",
|
|
141
|
+
"analyses": "analyse",
|
|
142
|
+
"analysing": "analyse",
|
|
143
|
+
"analyzed": "analyze",
|
|
144
|
+
"analyzes": "analyze",
|
|
145
|
+
"analyzing": "analyze",
|
|
146
|
+
"assessed": "assess",
|
|
147
|
+
"assesses": "assess",
|
|
148
|
+
"assessing": "assess",
|
|
149
|
+
"audited": "audit",
|
|
150
|
+
"auditing": "audit",
|
|
151
|
+
"audits": "audit",
|
|
152
|
+
"changed": "change",
|
|
153
|
+
"changes": "change",
|
|
154
|
+
"changing": "change",
|
|
155
|
+
"built": "build",
|
|
156
|
+
"building": "build",
|
|
157
|
+
"builds": "build",
|
|
158
|
+
"compared": "compare",
|
|
159
|
+
"compares": "compare",
|
|
160
|
+
"comparing": "compare",
|
|
161
|
+
"configured": "configure",
|
|
162
|
+
"configures": "configure",
|
|
163
|
+
"configuring": "configure",
|
|
164
|
+
"created": "create",
|
|
165
|
+
"creates": "create",
|
|
166
|
+
"creating": "create",
|
|
167
|
+
"documented": "document",
|
|
168
|
+
"documenting": "document",
|
|
169
|
+
"documents": "document",
|
|
170
|
+
"diagnosed": "diagnose",
|
|
171
|
+
"diagnoses": "diagnose",
|
|
172
|
+
"diagnosing": "diagnose",
|
|
173
|
+
"edited": "edit",
|
|
174
|
+
"editing": "edit",
|
|
175
|
+
"edits": "edit",
|
|
176
|
+
"enabled": "enable",
|
|
177
|
+
"enables": "enable",
|
|
178
|
+
"enabling": "enable",
|
|
179
|
+
"explained": "explain",
|
|
180
|
+
"explaining": "explain",
|
|
181
|
+
"explains": "explain",
|
|
182
|
+
"examined": "examine",
|
|
183
|
+
"examines": "examine",
|
|
184
|
+
"examining": "examine",
|
|
185
|
+
"explored": "explore",
|
|
186
|
+
"explores": "explore",
|
|
187
|
+
"exploring": "explore",
|
|
188
|
+
"found": "find",
|
|
189
|
+
"finding": "find",
|
|
190
|
+
"finds": "find",
|
|
191
|
+
"fixed": "fix",
|
|
192
|
+
"fixes": "fix",
|
|
193
|
+
"fixing": "fix",
|
|
194
|
+
"identified": "identify",
|
|
195
|
+
"identifies": "identify",
|
|
196
|
+
"identifying": "identify",
|
|
197
|
+
"improved": "improve",
|
|
198
|
+
"improves": "improve",
|
|
199
|
+
"improving": "improve",
|
|
200
|
+
"implemented": "implement",
|
|
201
|
+
"implementing": "implement",
|
|
202
|
+
"implements": "implement",
|
|
203
|
+
"inspected": "inspect",
|
|
204
|
+
"inspecting": "inspect",
|
|
205
|
+
"inspects": "inspect",
|
|
206
|
+
"investigated": "investigate",
|
|
207
|
+
"investigates": "investigate",
|
|
208
|
+
"investigating": "investigate",
|
|
209
|
+
"located": "locate",
|
|
210
|
+
"locates": "locate",
|
|
211
|
+
"locating": "locate",
|
|
212
|
+
"made": "make",
|
|
213
|
+
"makes": "make",
|
|
214
|
+
"making": "make",
|
|
215
|
+
"mapped": "map",
|
|
216
|
+
"mapping": "map",
|
|
217
|
+
"maps": "map",
|
|
218
|
+
"migrated": "migrate",
|
|
219
|
+
"migrates": "migrate",
|
|
220
|
+
"migrating": "migrate",
|
|
221
|
+
"modified": "modify",
|
|
222
|
+
"modifies": "modify",
|
|
223
|
+
"modifying": "modify",
|
|
224
|
+
"patched": "patch",
|
|
225
|
+
"patches": "patch",
|
|
226
|
+
"patching": "patch",
|
|
227
|
+
"refactored": "refactor",
|
|
228
|
+
"refactoring": "refactor",
|
|
229
|
+
"refactors": "refactor",
|
|
230
|
+
"removed": "remove",
|
|
231
|
+
"removes": "remove",
|
|
232
|
+
"removing": "remove",
|
|
233
|
+
"renamed": "rename",
|
|
234
|
+
"renames": "rename",
|
|
235
|
+
"renaming": "rename",
|
|
236
|
+
"repaired": "repair",
|
|
237
|
+
"repairing": "repair",
|
|
238
|
+
"repairs": "repair",
|
|
239
|
+
"reported": "report",
|
|
240
|
+
"reporting": "report",
|
|
241
|
+
"reports": "report",
|
|
242
|
+
"read": "read",
|
|
243
|
+
"reading": "read",
|
|
244
|
+
"reads": "read",
|
|
245
|
+
"reviewed": "review",
|
|
246
|
+
"reviewing": "review",
|
|
247
|
+
"reviews": "review",
|
|
248
|
+
"surveyed": "survey",
|
|
249
|
+
"surveying": "survey",
|
|
250
|
+
"surveys": "survey",
|
|
251
|
+
"summarised": "summarise",
|
|
252
|
+
"summarises": "summarise",
|
|
253
|
+
"summarising": "summarise",
|
|
254
|
+
"summarized": "summarize",
|
|
255
|
+
"summarizes": "summarize",
|
|
256
|
+
"summarizing": "summarize",
|
|
257
|
+
"supported": "support",
|
|
258
|
+
"supporting": "support",
|
|
259
|
+
"supports": "support",
|
|
260
|
+
"tested": "test",
|
|
261
|
+
"testing": "test",
|
|
262
|
+
"tests": "test",
|
|
263
|
+
"touched": "touch",
|
|
264
|
+
"touches": "touch",
|
|
265
|
+
"touching": "touch",
|
|
266
|
+
"triaged": "triage",
|
|
267
|
+
"triages": "triage",
|
|
268
|
+
"triaging": "triage",
|
|
269
|
+
"understood": "understand",
|
|
270
|
+
"understanding": "understand",
|
|
271
|
+
"understands": "understand",
|
|
272
|
+
"updated": "update",
|
|
273
|
+
"updates": "update",
|
|
274
|
+
"updating": "update",
|
|
275
|
+
"upgraded": "upgrade",
|
|
276
|
+
"upgrades": "upgrade",
|
|
277
|
+
"upgrading": "upgrade",
|
|
278
|
+
"wired": "wire",
|
|
279
|
+
"wires": "wire",
|
|
280
|
+
"wiring": "wire",
|
|
281
|
+
"writes": "write",
|
|
282
|
+
"writing": "write",
|
|
283
|
+
"written": "write",
|
|
284
|
+
"wrote": "write",
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
TASK_KIND_ANALYSIS_ONLY = "analysis_only"
|
|
288
|
+
TASK_KIND_IMPLEMENTATION = "implementation"
|
|
289
|
+
TASK_KIND_TEST_ONLY = "test_only"
|
|
290
|
+
TASK_KIND_VERIFICATION_ONLY = "verification_only"
|
|
291
|
+
TASK_KIND_DOCUMENTATION_ONLY = "documentation_only"
|
|
292
|
+
TASK_KIND_UNKNOWN = "unknown"
|
|
293
|
+
|
|
294
|
+
ZERO_DIFF_ANALYSIS_OK = "analysis_artifact"
|
|
295
|
+
ZERO_DIFF_REQUIRES_VERIFICATION = "requires_verification"
|
|
296
|
+
ZERO_DIFF_SUSPICIOUS = "needs_attention"
|
|
297
|
+
|
|
298
|
+
_VERIFICATION_ONLY_RE = re.compile(
|
|
299
|
+
r"^\s*(?:run\s+)?(?:verify|verification|validate|validation|check|tests?)\b",
|
|
300
|
+
re.IGNORECASE,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@dataclass(frozen=True)
|
|
305
|
+
class TaskFileScopeNormalization:
|
|
306
|
+
estimated_files: list[str]
|
|
307
|
+
write_scope: list[str]
|
|
308
|
+
warnings: list[str]
|
|
309
|
+
requires_runnable_scope: bool
|
|
310
|
+
task_kind: str = TASK_KIND_UNKNOWN
|
|
311
|
+
task_kind_reason: str = "unclassified"
|
|
312
|
+
zero_diff_policy: str = ZERO_DIFF_SUSPICIOUS
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@dataclass(frozen=True)
|
|
316
|
+
class ExecutionUnreadyTask:
|
|
317
|
+
task_id: str
|
|
318
|
+
title: str
|
|
319
|
+
status: str
|
|
320
|
+
warning: str
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@dataclass(frozen=True)
|
|
324
|
+
class TaskLifecycleClassification:
|
|
325
|
+
kind: str
|
|
326
|
+
reason_code: str
|
|
327
|
+
mutating: bool
|
|
328
|
+
requires_runnable_scope: bool
|
|
329
|
+
zero_diff_policy: str
|
|
330
|
+
|
|
331
|
+
@property
|
|
332
|
+
def read_only(self) -> bool:
|
|
333
|
+
return not self.mutating
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def normalized_text_list(value: Any) -> list[str]:
|
|
337
|
+
if not isinstance(value, list):
|
|
338
|
+
return []
|
|
339
|
+
out: list[str] = []
|
|
340
|
+
for item in value:
|
|
341
|
+
text = str(item).strip()
|
|
342
|
+
if text:
|
|
343
|
+
out.append(text)
|
|
344
|
+
return out
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _dedupe_keep_order(values: list[str]) -> list[str]:
|
|
348
|
+
seen: set[str] = set()
|
|
349
|
+
out: list[str] = []
|
|
350
|
+
for value in values:
|
|
351
|
+
key = value.casefold()
|
|
352
|
+
if key in seen:
|
|
353
|
+
continue
|
|
354
|
+
seen.add(key)
|
|
355
|
+
out.append(value)
|
|
356
|
+
return out
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _scope_identity_key(value: str) -> str:
|
|
360
|
+
normalized = str(value or "").strip().replace("\\", "/").rstrip("/")
|
|
361
|
+
if normalized.casefold() in {"readme", "readme.md"}:
|
|
362
|
+
return "__readme_alias__"
|
|
363
|
+
return normalized.casefold()
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def task_text_blob(*, title: str, description: str, acceptance_criteria: list[str]) -> str:
|
|
367
|
+
return "\n".join(
|
|
368
|
+
[
|
|
369
|
+
str(title or ""),
|
|
370
|
+
str(description or ""),
|
|
371
|
+
*(str(item or "") for item in acceptance_criteria),
|
|
372
|
+
]
|
|
373
|
+
).strip()
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _first_task_action_word(text: str) -> str:
|
|
377
|
+
for token in _TASK_ACTION_WORD_RE.findall((text or "").casefold()):
|
|
378
|
+
if token in {"a", "an", "the", "to"}:
|
|
379
|
+
continue
|
|
380
|
+
return _normalize_task_action_token(token)
|
|
381
|
+
return ""
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _normalize_task_action_token(token: str) -> str:
|
|
385
|
+
return _TASK_ACTION_TOKEN_CANONICAL_MAP.get(token.casefold(), token.casefold())
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _normalized_task_action_tokens(text: str) -> list[str]:
|
|
389
|
+
return [
|
|
390
|
+
_normalize_task_action_token(token)
|
|
391
|
+
for token in _TASK_ACTION_WORD_RE.findall((text or "").casefold())
|
|
392
|
+
]
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def contains_mutating_task_signal(text: str) -> bool:
|
|
396
|
+
return any(token in _MUTATING_TASK_ACTIONS for token in _normalized_task_action_tokens(text))
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def has_mutating_task_action_clause(text: str) -> bool:
|
|
400
|
+
return _has_mutating_task_action_clause(text, allowed_tokens=frozenset())
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _has_mutating_task_action_clause(text: str, *, allowed_tokens: frozenset[str]) -> bool:
|
|
404
|
+
folded = (text or "").casefold()
|
|
405
|
+
previous_token = ""
|
|
406
|
+
previous_end = 0
|
|
407
|
+
for index, match in enumerate(_TASK_ACTION_WORD_RE.finditer(folded)):
|
|
408
|
+
token = _normalize_task_action_token(match.group(0))
|
|
409
|
+
gap = folded[previous_end : match.start()]
|
|
410
|
+
starts_clause = (
|
|
411
|
+
index == 0
|
|
412
|
+
or any(separator in gap for separator in "\n.;:")
|
|
413
|
+
or ("," in gap and token not in _NOUN_PRONE_MUTATING_TASK_ACTIONS)
|
|
414
|
+
or previous_token in _TASK_ACTION_CLAUSE_CONNECTORS
|
|
415
|
+
)
|
|
416
|
+
if starts_clause and token in (
|
|
417
|
+
_MUTATING_TASK_ACTIONS - _WEAK_MUTATING_TASK_ACTIONS - allowed_tokens
|
|
418
|
+
):
|
|
419
|
+
return True
|
|
420
|
+
previous_token = token
|
|
421
|
+
previous_end = match.end()
|
|
422
|
+
return False
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def is_clearly_non_mutating_task(
|
|
426
|
+
*,
|
|
427
|
+
title: str,
|
|
428
|
+
description: str,
|
|
429
|
+
acceptance_criteria: list[str],
|
|
430
|
+
) -> bool:
|
|
431
|
+
raw_text = task_text_blob(
|
|
432
|
+
title=title,
|
|
433
|
+
description=description,
|
|
434
|
+
acceptance_criteria=acceptance_criteria,
|
|
435
|
+
)
|
|
436
|
+
text = raw_text.casefold()
|
|
437
|
+
if not text:
|
|
438
|
+
return False
|
|
439
|
+
normalized_action_tokens = _normalized_task_action_tokens(text)
|
|
440
|
+
first_title_word = _first_task_action_word(title)
|
|
441
|
+
has_explicit_non_mutating_phrase = any(phrase in text for phrase in _NON_MUTATING_TASK_PHRASES)
|
|
442
|
+
has_report_only_documentation_phrase = any(
|
|
443
|
+
phrase in text for phrase in _REPORT_ONLY_DOCUMENTATION_PHRASES
|
|
444
|
+
)
|
|
445
|
+
allowed_report_only_tokens = (
|
|
446
|
+
_REPORT_ONLY_DOCUMENTATION_MUTATING_TOKENS
|
|
447
|
+
if has_report_only_documentation_phrase
|
|
448
|
+
else frozenset()
|
|
449
|
+
)
|
|
450
|
+
has_mutating_action_clause = _has_mutating_task_action_clause(
|
|
451
|
+
raw_text,
|
|
452
|
+
allowed_tokens=allowed_report_only_tokens,
|
|
453
|
+
)
|
|
454
|
+
if (
|
|
455
|
+
first_title_word in _NON_MUTATING_TASK_ACTIONS
|
|
456
|
+
and has_report_only_documentation_phrase
|
|
457
|
+
and not has_mutating_action_clause
|
|
458
|
+
):
|
|
459
|
+
return True
|
|
460
|
+
if has_explicit_non_mutating_phrase and not has_mutating_action_clause:
|
|
461
|
+
return True
|
|
462
|
+
if first_title_word == "read" and not has_mutating_action_clause:
|
|
463
|
+
return True
|
|
464
|
+
if first_title_word in _NON_MUTATING_TASK_ACTIONS:
|
|
465
|
+
if not has_mutating_action_clause:
|
|
466
|
+
return True
|
|
467
|
+
if any(token in _MUTATING_TASK_ACTIONS for token in normalized_action_tokens):
|
|
468
|
+
return False
|
|
469
|
+
if any(token in _NON_MUTATING_TASK_ACTIONS for token in normalized_action_tokens):
|
|
470
|
+
return not has_mutating_action_clause
|
|
471
|
+
if first_title_word in _NON_MUTATING_TASK_ACTIONS:
|
|
472
|
+
return True
|
|
473
|
+
return has_explicit_non_mutating_phrase
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _normalized_scoped_paths(*, estimated_files: list[str], write_scope: list[str]) -> list[str]:
|
|
477
|
+
return _dedupe_keep_order([*estimated_files, *write_scope])
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _coerce_explicit_task_kind(value: Any) -> str | None:
|
|
481
|
+
raw = str(value or "").strip().casefold().replace("-", "_")
|
|
482
|
+
if not raw:
|
|
483
|
+
return None
|
|
484
|
+
aliases = {
|
|
485
|
+
"analysis": TASK_KIND_ANALYSIS_ONLY,
|
|
486
|
+
"analysis_only": TASK_KIND_ANALYSIS_ONLY,
|
|
487
|
+
"discovery": TASK_KIND_ANALYSIS_ONLY,
|
|
488
|
+
"read_only": TASK_KIND_ANALYSIS_ONLY,
|
|
489
|
+
"readonly": TASK_KIND_ANALYSIS_ONLY,
|
|
490
|
+
"report_only": TASK_KIND_ANALYSIS_ONLY,
|
|
491
|
+
"implementation": TASK_KIND_IMPLEMENTATION,
|
|
492
|
+
"mutation": TASK_KIND_IMPLEMENTATION,
|
|
493
|
+
"mutating": TASK_KIND_IMPLEMENTATION,
|
|
494
|
+
"test": TASK_KIND_TEST_ONLY,
|
|
495
|
+
"test_only": TASK_KIND_TEST_ONLY,
|
|
496
|
+
"verification": TASK_KIND_VERIFICATION_ONLY,
|
|
497
|
+
"verification_only": TASK_KIND_VERIFICATION_ONLY,
|
|
498
|
+
"docs": TASK_KIND_DOCUMENTATION_ONLY,
|
|
499
|
+
"documentation": TASK_KIND_DOCUMENTATION_ONLY,
|
|
500
|
+
"documentation_only": TASK_KIND_DOCUMENTATION_ONLY,
|
|
501
|
+
"unknown": TASK_KIND_UNKNOWN,
|
|
502
|
+
}
|
|
503
|
+
return aliases.get(raw)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _classification_for_kind(kind: str, *, reason_code: str) -> TaskLifecycleClassification:
|
|
507
|
+
if kind == TASK_KIND_ANALYSIS_ONLY:
|
|
508
|
+
return TaskLifecycleClassification(
|
|
509
|
+
kind=kind,
|
|
510
|
+
reason_code=reason_code,
|
|
511
|
+
mutating=False,
|
|
512
|
+
requires_runnable_scope=False,
|
|
513
|
+
zero_diff_policy=ZERO_DIFF_ANALYSIS_OK,
|
|
514
|
+
)
|
|
515
|
+
if kind == TASK_KIND_VERIFICATION_ONLY:
|
|
516
|
+
return TaskLifecycleClassification(
|
|
517
|
+
kind=kind,
|
|
518
|
+
reason_code=reason_code,
|
|
519
|
+
mutating=False,
|
|
520
|
+
requires_runnable_scope=False,
|
|
521
|
+
zero_diff_policy=ZERO_DIFF_REQUIRES_VERIFICATION,
|
|
522
|
+
)
|
|
523
|
+
if kind == TASK_KIND_TEST_ONLY:
|
|
524
|
+
return TaskLifecycleClassification(
|
|
525
|
+
kind=kind,
|
|
526
|
+
reason_code=reason_code,
|
|
527
|
+
mutating=True,
|
|
528
|
+
requires_runnable_scope=True,
|
|
529
|
+
zero_diff_policy=ZERO_DIFF_SUSPICIOUS,
|
|
530
|
+
)
|
|
531
|
+
if kind == TASK_KIND_DOCUMENTATION_ONLY:
|
|
532
|
+
return TaskLifecycleClassification(
|
|
533
|
+
kind=kind,
|
|
534
|
+
reason_code=reason_code,
|
|
535
|
+
mutating=True,
|
|
536
|
+
requires_runnable_scope=True,
|
|
537
|
+
zero_diff_policy=ZERO_DIFF_SUSPICIOUS,
|
|
538
|
+
)
|
|
539
|
+
if kind == TASK_KIND_IMPLEMENTATION:
|
|
540
|
+
return TaskLifecycleClassification(
|
|
541
|
+
kind=kind,
|
|
542
|
+
reason_code=reason_code,
|
|
543
|
+
mutating=True,
|
|
544
|
+
requires_runnable_scope=True,
|
|
545
|
+
zero_diff_policy=ZERO_DIFF_REQUIRES_VERIFICATION,
|
|
546
|
+
)
|
|
547
|
+
return TaskLifecycleClassification(
|
|
548
|
+
kind=TASK_KIND_UNKNOWN,
|
|
549
|
+
reason_code=reason_code,
|
|
550
|
+
mutating=True,
|
|
551
|
+
requires_runnable_scope=True,
|
|
552
|
+
zero_diff_policy=ZERO_DIFF_SUSPICIOUS,
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def classify_task_lifecycle(
|
|
557
|
+
*,
|
|
558
|
+
title: str,
|
|
559
|
+
description: str,
|
|
560
|
+
acceptance_criteria: list[str],
|
|
561
|
+
estimated_files: list[str] | None = None,
|
|
562
|
+
write_scope: list[str] | None = None,
|
|
563
|
+
explicit_analysis_only: bool | None = None,
|
|
564
|
+
explicit_task_kind: Any = None,
|
|
565
|
+
) -> TaskLifecycleClassification:
|
|
566
|
+
explicit_kind = _coerce_explicit_task_kind(explicit_task_kind)
|
|
567
|
+
if explicit_analysis_only is True:
|
|
568
|
+
return _classification_for_kind(
|
|
569
|
+
TASK_KIND_ANALYSIS_ONLY,
|
|
570
|
+
reason_code="explicit_analysis_only",
|
|
571
|
+
)
|
|
572
|
+
if explicit_kind is not None:
|
|
573
|
+
return _classification_for_kind(
|
|
574
|
+
explicit_kind,
|
|
575
|
+
reason_code=f"explicit_task_kind:{explicit_kind}",
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
acceptance = normalized_text_list(acceptance_criteria)
|
|
579
|
+
if is_clearly_non_mutating_task(
|
|
580
|
+
title=title,
|
|
581
|
+
description=description,
|
|
582
|
+
acceptance_criteria=acceptance,
|
|
583
|
+
):
|
|
584
|
+
return _classification_for_kind(
|
|
585
|
+
TASK_KIND_ANALYSIS_ONLY,
|
|
586
|
+
reason_code="non_mutating_intent",
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
text = task_text_blob(
|
|
590
|
+
title=title,
|
|
591
|
+
description=description,
|
|
592
|
+
acceptance_criteria=acceptance,
|
|
593
|
+
)
|
|
594
|
+
has_mutating_clause = _has_mutating_task_action_clause(text, allowed_tokens=frozenset())
|
|
595
|
+
if _VERIFICATION_ONLY_RE.search(text) and not has_mutating_clause:
|
|
596
|
+
return _classification_for_kind(
|
|
597
|
+
TASK_KIND_VERIFICATION_ONLY,
|
|
598
|
+
reason_code="verification_intent",
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
scoped_paths = _normalized_scoped_paths(
|
|
602
|
+
estimated_files=list(estimated_files or []),
|
|
603
|
+
write_scope=list(write_scope or []),
|
|
604
|
+
)
|
|
605
|
+
if scoped_paths and all(is_test_path(path) for path in scoped_paths):
|
|
606
|
+
return _classification_for_kind(TASK_KIND_TEST_ONLY, reason_code="test_scope")
|
|
607
|
+
if scoped_paths and all(is_docs_path(path) for path in scoped_paths):
|
|
608
|
+
return _classification_for_kind(
|
|
609
|
+
TASK_KIND_DOCUMENTATION_ONLY,
|
|
610
|
+
reason_code="documentation_scope",
|
|
611
|
+
)
|
|
612
|
+
if scoped_paths:
|
|
613
|
+
return _classification_for_kind(
|
|
614
|
+
TASK_KIND_IMPLEMENTATION,
|
|
615
|
+
reason_code="runnable_file_scope",
|
|
616
|
+
)
|
|
617
|
+
if contains_mutating_task_signal(text):
|
|
618
|
+
return _classification_for_kind(
|
|
619
|
+
TASK_KIND_IMPLEMENTATION,
|
|
620
|
+
reason_code="mutating_intent",
|
|
621
|
+
)
|
|
622
|
+
return _classification_for_kind(TASK_KIND_UNKNOWN, reason_code="ambiguous_intent")
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def has_runnable_local_file_scope(
|
|
626
|
+
*,
|
|
627
|
+
estimated_files: list[str],
|
|
628
|
+
write_scope: list[str],
|
|
629
|
+
) -> bool:
|
|
630
|
+
return bool(estimated_files or write_scope)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def normalize_existing_task_scope_fields(
|
|
634
|
+
*,
|
|
635
|
+
estimated_files: Any,
|
|
636
|
+
write_scope: Any,
|
|
637
|
+
) -> tuple[list[str], list[str]]:
|
|
638
|
+
normalized_estimated, _dropped_estimated = split_normalized_repo_path_list(estimated_files)
|
|
639
|
+
normalized_write_scope, _dropped_write_scope = split_normalized_repo_path_list(write_scope)
|
|
640
|
+
|
|
641
|
+
def _filter_runnable_paths(paths: list[str]) -> list[str]:
|
|
642
|
+
filtered: list[str] = []
|
|
643
|
+
for path in paths:
|
|
644
|
+
if is_agent_internal_scope_path(path):
|
|
645
|
+
continue
|
|
646
|
+
filtered.append(path)
|
|
647
|
+
return _dedupe_keep_order(filtered)
|
|
648
|
+
|
|
649
|
+
return _filter_runnable_paths(normalized_estimated), _filter_runnable_paths(
|
|
650
|
+
normalized_write_scope
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
_normalize_existing_task_scope_fields = normalize_existing_task_scope_fields
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def task_requires_runnable_file_scope(
|
|
658
|
+
*,
|
|
659
|
+
title: str,
|
|
660
|
+
description: str,
|
|
661
|
+
acceptance_criteria: list[str],
|
|
662
|
+
estimated_files: list[str],
|
|
663
|
+
write_scope: list[str],
|
|
664
|
+
) -> bool:
|
|
665
|
+
lifecycle = classify_task_lifecycle(
|
|
666
|
+
title=title,
|
|
667
|
+
description=description,
|
|
668
|
+
acceptance_criteria=acceptance_criteria,
|
|
669
|
+
estimated_files=estimated_files,
|
|
670
|
+
write_scope=write_scope,
|
|
671
|
+
)
|
|
672
|
+
if lifecycle.requires_runnable_scope:
|
|
673
|
+
return True
|
|
674
|
+
text = task_text_blob(
|
|
675
|
+
title=title,
|
|
676
|
+
description=description,
|
|
677
|
+
acceptance_criteria=acceptance_criteria,
|
|
678
|
+
)
|
|
679
|
+
if extract_repo_path_hints(text):
|
|
680
|
+
return lifecycle.mutating
|
|
681
|
+
return lifecycle.requires_runnable_scope
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def normalize_task_file_fields(
|
|
685
|
+
*,
|
|
686
|
+
title: str,
|
|
687
|
+
description: str,
|
|
688
|
+
acceptance_criteria: list[str],
|
|
689
|
+
estimated_files: Any,
|
|
690
|
+
write_scope: Any,
|
|
691
|
+
warning_prefix: str,
|
|
692
|
+
latest_user_text: str = "",
|
|
693
|
+
) -> TaskFileScopeNormalization:
|
|
694
|
+
warnings: list[str] = []
|
|
695
|
+
normalized_estimated, dropped_estimated = split_normalized_repo_path_list(estimated_files)
|
|
696
|
+
normalized_write_scope, dropped_write_scope = split_normalized_repo_path_list(write_scope)
|
|
697
|
+
if dropped_estimated:
|
|
698
|
+
warnings.append(
|
|
699
|
+
f"{warning_prefix}: dropped invalid estimated_files entries: "
|
|
700
|
+
+ ", ".join(dropped_estimated)
|
|
701
|
+
)
|
|
702
|
+
if dropped_write_scope:
|
|
703
|
+
warnings.append(
|
|
704
|
+
f"{warning_prefix}: dropped invalid write_scope entries: "
|
|
705
|
+
+ ", ".join(dropped_write_scope)
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
task_text = task_text_blob(
|
|
709
|
+
title=title,
|
|
710
|
+
description=description,
|
|
711
|
+
acceptance_criteria=acceptance_criteria,
|
|
712
|
+
)
|
|
713
|
+
initial_lifecycle = classify_task_lifecycle(
|
|
714
|
+
title=title,
|
|
715
|
+
description=description,
|
|
716
|
+
acceptance_criteria=acceptance_criteria,
|
|
717
|
+
estimated_files=normalized_estimated,
|
|
718
|
+
write_scope=normalized_write_scope,
|
|
719
|
+
)
|
|
720
|
+
clearly_non_mutating = initial_lifecycle.kind == TASK_KIND_ANALYSIS_ONLY
|
|
721
|
+
forbidden_paths = extract_forbidden_repo_path_hints(
|
|
722
|
+
"\n".join([task_text, str(latest_user_text or "")])
|
|
723
|
+
)
|
|
724
|
+
forbidden_identities = {_scope_identity_key(path) for path in forbidden_paths}
|
|
725
|
+
|
|
726
|
+
def _drop_forbidden(paths: list[str]) -> tuple[list[str], list[str]]:
|
|
727
|
+
kept: list[str] = []
|
|
728
|
+
dropped: list[str] = []
|
|
729
|
+
for path in paths:
|
|
730
|
+
if _scope_identity_key(path) in forbidden_identities:
|
|
731
|
+
dropped.append(path)
|
|
732
|
+
continue
|
|
733
|
+
kept.append(path)
|
|
734
|
+
return kept, dropped
|
|
735
|
+
|
|
736
|
+
inferred_paths = [] if clearly_non_mutating else extract_repo_path_hints(task_text)
|
|
737
|
+
inferred_paths, forbidden_inferred_paths = _drop_forbidden(inferred_paths)
|
|
738
|
+
if forbidden_inferred_paths:
|
|
739
|
+
warnings.append(
|
|
740
|
+
f"{warning_prefix}: ignored forbidden path hints from task text: "
|
|
741
|
+
+ ", ".join(forbidden_inferred_paths)
|
|
742
|
+
)
|
|
743
|
+
inferred_paths, obsolete_inferred_paths = filter_obsolete_direction_paths(
|
|
744
|
+
inferred_paths,
|
|
745
|
+
latest_user_text=latest_user_text,
|
|
746
|
+
task_text=task_text,
|
|
747
|
+
)
|
|
748
|
+
if obsolete_inferred_paths:
|
|
749
|
+
warnings.append(
|
|
750
|
+
f"{warning_prefix}: ignored obsolete inferred path hints from direction-change text: "
|
|
751
|
+
+ ", ".join(obsolete_inferred_paths)
|
|
752
|
+
)
|
|
753
|
+
added_inferred: list[str] = []
|
|
754
|
+
for path in inferred_paths:
|
|
755
|
+
if path not in normalized_estimated:
|
|
756
|
+
normalized_estimated.append(path)
|
|
757
|
+
added_inferred.append(path)
|
|
758
|
+
if added_inferred:
|
|
759
|
+
warnings.append(
|
|
760
|
+
f"{warning_prefix}: inferred estimated_files from task text: "
|
|
761
|
+
+ ", ".join(added_inferred)
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
filtered_estimated: list[str] = []
|
|
765
|
+
dropped_internal_estimated: list[str] = []
|
|
766
|
+
for path in normalized_estimated:
|
|
767
|
+
if is_agent_internal_scope_path(path):
|
|
768
|
+
dropped_internal_estimated.append(path)
|
|
769
|
+
continue
|
|
770
|
+
filtered_estimated.append(path)
|
|
771
|
+
if dropped_internal_estimated:
|
|
772
|
+
warnings.append(
|
|
773
|
+
f"{warning_prefix}: dropped protected estimated_files entries: "
|
|
774
|
+
+ ", ".join(dropped_internal_estimated)
|
|
775
|
+
)
|
|
776
|
+
filtered_estimated, dropped_forbidden_estimated = _drop_forbidden(filtered_estimated)
|
|
777
|
+
if dropped_forbidden_estimated:
|
|
778
|
+
warnings.append(
|
|
779
|
+
f"{warning_prefix}: dropped forbidden estimated_files entries: "
|
|
780
|
+
+ ", ".join(dropped_forbidden_estimated)
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
filtered_write_scope: list[str] = []
|
|
784
|
+
dropped_internal_write_scope: list[str] = []
|
|
785
|
+
for path in normalized_write_scope:
|
|
786
|
+
if is_agent_internal_scope_path(path):
|
|
787
|
+
dropped_internal_write_scope.append(path)
|
|
788
|
+
continue
|
|
789
|
+
filtered_write_scope.append(path)
|
|
790
|
+
if dropped_internal_write_scope:
|
|
791
|
+
warnings.append(
|
|
792
|
+
f"{warning_prefix}: dropped protected write_scope entries: "
|
|
793
|
+
+ ", ".join(dropped_internal_write_scope)
|
|
794
|
+
)
|
|
795
|
+
filtered_write_scope, dropped_forbidden_write_scope = _drop_forbidden(filtered_write_scope)
|
|
796
|
+
if dropped_forbidden_write_scope:
|
|
797
|
+
warnings.append(
|
|
798
|
+
f"{warning_prefix}: dropped forbidden write_scope entries: "
|
|
799
|
+
+ ", ".join(dropped_forbidden_write_scope)
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
filtered_estimated = _dedupe_keep_order(filtered_estimated)
|
|
803
|
+
filtered_write_scope = _dedupe_keep_order(filtered_write_scope)
|
|
804
|
+
lifecycle = classify_task_lifecycle(
|
|
805
|
+
title=title,
|
|
806
|
+
description=description,
|
|
807
|
+
acceptance_criteria=acceptance_criteria,
|
|
808
|
+
estimated_files=filtered_estimated,
|
|
809
|
+
write_scope=filtered_write_scope,
|
|
810
|
+
)
|
|
811
|
+
if lifecycle.kind == TASK_KIND_ANALYSIS_ONLY:
|
|
812
|
+
if filtered_estimated or filtered_write_scope:
|
|
813
|
+
warnings.append(
|
|
814
|
+
f"{warning_prefix}: cleared file mutation scope for analysis-only/report-only task"
|
|
815
|
+
)
|
|
816
|
+
return TaskFileScopeNormalization(
|
|
817
|
+
estimated_files=[],
|
|
818
|
+
write_scope=[],
|
|
819
|
+
warnings=warnings,
|
|
820
|
+
requires_runnable_scope=False,
|
|
821
|
+
task_kind=lifecycle.kind,
|
|
822
|
+
task_kind_reason=lifecycle.reason_code,
|
|
823
|
+
zero_diff_policy=lifecycle.zero_diff_policy,
|
|
824
|
+
)
|
|
825
|
+
expanded_write_scope: list[str] = list(filtered_write_scope)
|
|
826
|
+
added_to_write_scope: list[str] = []
|
|
827
|
+
for path in filtered_estimated:
|
|
828
|
+
if path in expanded_write_scope:
|
|
829
|
+
continue
|
|
830
|
+
expanded_write_scope.append(path)
|
|
831
|
+
added_to_write_scope.append(path)
|
|
832
|
+
if added_to_write_scope:
|
|
833
|
+
warnings.append(
|
|
834
|
+
f"{warning_prefix}: expanded write_scope to include estimated_files: "
|
|
835
|
+
+ ", ".join(added_to_write_scope)
|
|
836
|
+
)
|
|
837
|
+
requires_runnable_scope = task_requires_runnable_file_scope(
|
|
838
|
+
title=title,
|
|
839
|
+
description=description,
|
|
840
|
+
acceptance_criteria=acceptance_criteria,
|
|
841
|
+
estimated_files=filtered_estimated,
|
|
842
|
+
write_scope=expanded_write_scope,
|
|
843
|
+
)
|
|
844
|
+
if requires_runnable_scope and not has_runnable_local_file_scope(
|
|
845
|
+
estimated_files=filtered_estimated,
|
|
846
|
+
write_scope=expanded_write_scope,
|
|
847
|
+
):
|
|
848
|
+
warnings.append(f"{warning_prefix}: {MUTATING_TASK_SCOPE_WARNING}")
|
|
849
|
+
return TaskFileScopeNormalization(
|
|
850
|
+
estimated_files=filtered_estimated,
|
|
851
|
+
write_scope=expanded_write_scope,
|
|
852
|
+
warnings=warnings,
|
|
853
|
+
requires_runnable_scope=requires_runnable_scope,
|
|
854
|
+
task_kind=lifecycle.kind,
|
|
855
|
+
task_kind_reason=lifecycle.reason_code,
|
|
856
|
+
zero_diff_policy=lifecycle.zero_diff_policy,
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def task_readiness_warning(*, task_id: str, title: str = "") -> str:
|
|
861
|
+
label = str(task_id or "").strip() or "-"
|
|
862
|
+
title_text = str(title or "").strip()
|
|
863
|
+
if title_text:
|
|
864
|
+
return f"Task {label} ({title_text}): {EXECUTION_UNREADY_SCOPE_WARNING}"
|
|
865
|
+
return f"Task {label}: {EXECUTION_UNREADY_SCOPE_WARNING}"
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def manual_task_scope_error_message(*, title: str) -> str:
|
|
869
|
+
title_text = str(title or "").strip()
|
|
870
|
+
if title_text:
|
|
871
|
+
return (
|
|
872
|
+
f"Task '{title_text}' looks like runnable or ambiguous work but lacks runnable "
|
|
873
|
+
"file scope. Include repo-relative file paths in /task, use /plan edit / the "
|
|
874
|
+
"planner flow to produce execution-ready scope, or mark the task as "
|
|
875
|
+
"analysis-only/report-only if no file mutation is intended."
|
|
876
|
+
)
|
|
877
|
+
return (
|
|
878
|
+
"Task looks like runnable or ambiguous work but lacks runnable file scope. Include "
|
|
879
|
+
"repo-relative file paths in /task, use /plan edit / the planner flow to produce "
|
|
880
|
+
"execution-ready scope, or mark the task as analysis-only/report-only if no file "
|
|
881
|
+
"mutation is intended."
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def _canonical_task_status(status: str) -> str:
|
|
886
|
+
value = (status or "").strip().lower()
|
|
887
|
+
if value == "todo":
|
|
888
|
+
return "planned"
|
|
889
|
+
return value or "planned"
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
def _parse_only_ids(only: str | None) -> set[str] | None:
|
|
893
|
+
if only is None:
|
|
894
|
+
return None
|
|
895
|
+
ids = {part.strip() for part in only.split(",") if part.strip()}
|
|
896
|
+
return ids or None
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def status_is_execution_candidate(
|
|
900
|
+
status: str,
|
|
901
|
+
*,
|
|
902
|
+
retry_failed: bool,
|
|
903
|
+
retry_changes_requested: bool,
|
|
904
|
+
retry_merge_conflicts: bool,
|
|
905
|
+
) -> bool:
|
|
906
|
+
if status in {"planned", "ready_for_merge"}:
|
|
907
|
+
return True
|
|
908
|
+
if status in {"failed", "verify_failed", "candidate_rejected"} and retry_failed:
|
|
909
|
+
return True
|
|
910
|
+
if status == "changes_requested" and retry_changes_requested:
|
|
911
|
+
return True
|
|
912
|
+
if status == "merge_conflict" and retry_merge_conflicts:
|
|
913
|
+
return True
|
|
914
|
+
return False
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
_status_is_execution_candidate = status_is_execution_candidate
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
def task_is_missing_runnable_scope(task: dict[str, Any]) -> bool:
|
|
921
|
+
estimated_files, write_scope = _normalize_existing_task_scope_fields(
|
|
922
|
+
estimated_files=task.get("estimated_files"),
|
|
923
|
+
write_scope=task.get("write_scope"),
|
|
924
|
+
)
|
|
925
|
+
return task_requires_runnable_file_scope(
|
|
926
|
+
title=str(task.get("title") or "").strip(),
|
|
927
|
+
description=str(task.get("description") or "").strip(),
|
|
928
|
+
acceptance_criteria=normalized_text_list(task.get("acceptance_criteria")),
|
|
929
|
+
estimated_files=estimated_files,
|
|
930
|
+
write_scope=write_scope,
|
|
931
|
+
) and not has_runnable_local_file_scope(
|
|
932
|
+
estimated_files=estimated_files,
|
|
933
|
+
write_scope=write_scope,
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def find_execution_unready_mutating_tasks(
|
|
938
|
+
plan: dict[str, Any],
|
|
939
|
+
*,
|
|
940
|
+
retry_failed: bool = False,
|
|
941
|
+
retry_changes_requested: bool = False,
|
|
942
|
+
retry_merge_conflicts: bool = False,
|
|
943
|
+
only: str | None = None,
|
|
944
|
+
) -> list[ExecutionUnreadyTask]:
|
|
945
|
+
only_ids = _parse_only_ids(only)
|
|
946
|
+
tasks_raw = plan.get("tasks")
|
|
947
|
+
if not isinstance(tasks_raw, list):
|
|
948
|
+
return []
|
|
949
|
+
|
|
950
|
+
issues: list[ExecutionUnreadyTask] = []
|
|
951
|
+
for index, task in enumerate(tasks_raw):
|
|
952
|
+
if not isinstance(task, dict):
|
|
953
|
+
continue
|
|
954
|
+
task_id = str(task.get("id") or "").strip() or f"task[{index}]"
|
|
955
|
+
status = _canonical_task_status(str(task.get("status") or ""))
|
|
956
|
+
# Merge-phase tasks are handled before scheduler filtering, so mirror run_swarm's
|
|
957
|
+
# execution surface instead of treating --only as a complete preflight filter.
|
|
958
|
+
if (
|
|
959
|
+
only_ids is not None
|
|
960
|
+
and task_id not in only_ids
|
|
961
|
+
and status != "ready_for_merge"
|
|
962
|
+
and not (retry_merge_conflicts and status == "merge_conflict")
|
|
963
|
+
):
|
|
964
|
+
continue
|
|
965
|
+
if not status_is_execution_candidate(
|
|
966
|
+
status,
|
|
967
|
+
retry_failed=retry_failed,
|
|
968
|
+
retry_changes_requested=retry_changes_requested,
|
|
969
|
+
retry_merge_conflicts=retry_merge_conflicts,
|
|
970
|
+
):
|
|
971
|
+
continue
|
|
972
|
+
title = str(task.get("title") or "").strip()
|
|
973
|
+
if not task_is_missing_runnable_scope(task):
|
|
974
|
+
continue
|
|
975
|
+
issues.append(
|
|
976
|
+
ExecutionUnreadyTask(
|
|
977
|
+
task_id=task_id,
|
|
978
|
+
title=title,
|
|
979
|
+
status=status,
|
|
980
|
+
warning=task_readiness_warning(task_id=task_id, title=title),
|
|
981
|
+
)
|
|
982
|
+
)
|
|
983
|
+
return issues
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
def format_execution_readiness_block(issues: list[ExecutionUnreadyTask]) -> str:
|
|
987
|
+
if not issues:
|
|
988
|
+
return ""
|
|
989
|
+
preview = "; ".join(issue.warning for issue in issues[:5])
|
|
990
|
+
if len(issues) > 5:
|
|
991
|
+
preview += f"; +{len(issues) - 5} more"
|
|
992
|
+
return "Execution blocked: " + preview
|