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,1228 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path, PurePosixPath
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .direction_change import detect_direction_change, filter_obsolete_direction_paths
|
|
9
|
+
from .file_classification import (
|
|
10
|
+
CODE_SCAN_SKIP_DIR_NAMES,
|
|
11
|
+
is_code_implementation_path,
|
|
12
|
+
is_symbol_scannable_path,
|
|
13
|
+
symbol_definition_regex,
|
|
14
|
+
)
|
|
15
|
+
from .planning_constraints import (
|
|
16
|
+
filter_scope_entries_for_planning_constraints,
|
|
17
|
+
planning_constraints_from_plan,
|
|
18
|
+
update_plan_planning_constraints,
|
|
19
|
+
)
|
|
20
|
+
from .swarm_scheduler import canonical_task_status
|
|
21
|
+
from .task_readiness import (
|
|
22
|
+
TASK_KIND_ANALYSIS_ONLY,
|
|
23
|
+
classify_task_lifecycle,
|
|
24
|
+
has_runnable_local_file_scope,
|
|
25
|
+
task_readiness_warning,
|
|
26
|
+
task_requires_runnable_file_scope,
|
|
27
|
+
)
|
|
28
|
+
from .task_scope import (
|
|
29
|
+
extract_forbidden_repo_path_hints,
|
|
30
|
+
extract_repo_path_hints,
|
|
31
|
+
is_internal_alysis_path,
|
|
32
|
+
scope_path_matches_pattern,
|
|
33
|
+
split_normalized_repo_path_list,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
_GLOB_CHARS = ("*", "?")
|
|
37
|
+
_TASK_ID_HINT_RE = re.compile(r"\bT\d+\b", re.IGNORECASE)
|
|
38
|
+
_NON_EXECUTABLE_OBSOLETE_STATUSES = frozenset({"superseded", "invalidated"})
|
|
39
|
+
_FRAMEWORK_DYNAMIC_ROUTE_SEGMENT_RE = re.compile(
|
|
40
|
+
r"^(?:\[\[?\.\.\.[A-Za-z0-9_-]+\]?\]|\[[A-Za-z0-9_-]+\]|\([A-Za-z0-9_.-]+\))$"
|
|
41
|
+
)
|
|
42
|
+
_SYMBOL_CANDIDATE_RE = re.compile(r"\b[A-Za-z_][A-Za-z0-9_]{2,}\b")
|
|
43
|
+
_SYMBOL_CANDIDATE_STOPWORDS = frozenset(
|
|
44
|
+
{
|
|
45
|
+
"add",
|
|
46
|
+
"and",
|
|
47
|
+
"blank",
|
|
48
|
+
"blanks",
|
|
49
|
+
"bug",
|
|
50
|
+
"class",
|
|
51
|
+
"code",
|
|
52
|
+
"def",
|
|
53
|
+
"description",
|
|
54
|
+
"file",
|
|
55
|
+
"fix",
|
|
56
|
+
"for",
|
|
57
|
+
"from",
|
|
58
|
+
"function",
|
|
59
|
+
"implement",
|
|
60
|
+
"module",
|
|
61
|
+
"old",
|
|
62
|
+
"output",
|
|
63
|
+
"return",
|
|
64
|
+
"test",
|
|
65
|
+
"tests",
|
|
66
|
+
"the",
|
|
67
|
+
"update",
|
|
68
|
+
"when",
|
|
69
|
+
"with",
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
_FILENAME_TOKEN_RE = re.compile(r"\b[a-z][a-z0-9_]{2,}\b", re.IGNORECASE)
|
|
73
|
+
_EXPLICIT_MISSING_PATH_ACTION_RE = re.compile(
|
|
74
|
+
r"\b(?:add|create|creating|generate|generating|introduce|new|scaffold|write|writing)\b",
|
|
75
|
+
re.IGNORECASE,
|
|
76
|
+
)
|
|
77
|
+
_MAX_SYMBOL_SCAN_FILES = 1500
|
|
78
|
+
_MAX_SYMBOL_SCAN_BYTES = 512_000
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class PlanReconciliationResult:
|
|
83
|
+
changed: bool
|
|
84
|
+
warnings: list[str]
|
|
85
|
+
task_updates: dict[str, dict[str, Any]]
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def updated_task_ids(self) -> list[str]:
|
|
89
|
+
return list(self.task_updates)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True)
|
|
93
|
+
class _PathAnchorGroup:
|
|
94
|
+
task_ids: tuple[str, ...]
|
|
95
|
+
paths: tuple[str, ...]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def summarize_plan_reconciliation(result: PlanReconciliationResult) -> str:
|
|
99
|
+
if result.task_updates:
|
|
100
|
+
return "reconciled tasks: " + ", ".join(result.updated_task_ids)
|
|
101
|
+
if result.warnings:
|
|
102
|
+
return f"reconciliation warnings: {len(result.warnings)}"
|
|
103
|
+
return "reconciliation made no changes"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def reconcile_plan_with_workspace(
|
|
107
|
+
plan: dict[str, Any],
|
|
108
|
+
*,
|
|
109
|
+
workspace_root: Path,
|
|
110
|
+
workspace_context: dict[str, Any] | None = None,
|
|
111
|
+
user_text: str | None = None,
|
|
112
|
+
transcript_tail: list[dict[str, Any]] | None = None,
|
|
113
|
+
target_task_ids: set[str] | None = None,
|
|
114
|
+
) -> PlanReconciliationResult:
|
|
115
|
+
tasks_raw = plan.get("tasks")
|
|
116
|
+
if not isinstance(tasks_raw, list):
|
|
117
|
+
return PlanReconciliationResult(
|
|
118
|
+
changed=False,
|
|
119
|
+
warnings=["Plan reconciliation skipped: tasks field is missing or invalid."],
|
|
120
|
+
task_updates={},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
root = workspace_root.expanduser().resolve()
|
|
124
|
+
known_paths = _known_workspace_paths(workspace_context)
|
|
125
|
+
greenfield = _workspace_context_is_greenfield(workspace_context)
|
|
126
|
+
normalized_target_task_ids = (
|
|
127
|
+
{str(task_id).strip() for task_id in target_task_ids if str(task_id).strip()}
|
|
128
|
+
if target_task_ids is not None
|
|
129
|
+
else None
|
|
130
|
+
)
|
|
131
|
+
anchor_groups = _latest_path_anchor_groups(
|
|
132
|
+
transcript_tail=transcript_tail,
|
|
133
|
+
user_text=user_text,
|
|
134
|
+
)
|
|
135
|
+
changed = False
|
|
136
|
+
warnings: list[str] = []
|
|
137
|
+
task_updates: dict[str, dict[str, Any]] = {}
|
|
138
|
+
planning_constraints = None
|
|
139
|
+
constraints_changed = False
|
|
140
|
+
candidate_constraint_messages = _candidate_user_messages(
|
|
141
|
+
transcript_tail=transcript_tail,
|
|
142
|
+
user_text=user_text,
|
|
143
|
+
)[:4]
|
|
144
|
+
for constraint_text in reversed(candidate_constraint_messages):
|
|
145
|
+
planning_constraints, message_changed = update_plan_planning_constraints(
|
|
146
|
+
plan,
|
|
147
|
+
text=constraint_text,
|
|
148
|
+
workspace_context=workspace_context,
|
|
149
|
+
direction_change=detect_direction_change(constraint_text),
|
|
150
|
+
)
|
|
151
|
+
constraints_changed = constraints_changed or message_changed
|
|
152
|
+
if planning_constraints is None:
|
|
153
|
+
planning_constraints = planning_constraints_from_plan(plan)
|
|
154
|
+
if constraints_changed:
|
|
155
|
+
changed = True
|
|
156
|
+
warnings.append("Recorded planning scope constraints from latest user direction.")
|
|
157
|
+
|
|
158
|
+
task_entries = [(index, task) for index, task in enumerate(tasks_raw) if isinstance(task, dict)]
|
|
159
|
+
task_count = len(task_entries)
|
|
160
|
+
|
|
161
|
+
for task_position, (index, task) in enumerate(task_entries):
|
|
162
|
+
task_id = _task_label(task=task, index=index)
|
|
163
|
+
if normalized_target_task_ids is not None and task_id not in normalized_target_task_ids:
|
|
164
|
+
continue
|
|
165
|
+
if (
|
|
166
|
+
canonical_task_status(str(task.get("status") or ""))
|
|
167
|
+
in _NON_EXECUTABLE_OBSOLETE_STATUSES
|
|
168
|
+
):
|
|
169
|
+
continue
|
|
170
|
+
current_estimated = _string_list(task.get("estimated_files"))
|
|
171
|
+
current_write_scope = _string_list(task.get("write_scope"))
|
|
172
|
+
allow_create_targets = _task_allows_create_targets(task=task, greenfield=greenfield)
|
|
173
|
+
forbidden_path_identities = _task_forbidden_path_identities(
|
|
174
|
+
task=task,
|
|
175
|
+
user_text=user_text,
|
|
176
|
+
)
|
|
177
|
+
task_explicit_path_hints = [
|
|
178
|
+
path
|
|
179
|
+
for path in _task_explicit_path_hints(
|
|
180
|
+
task=task,
|
|
181
|
+
latest_user_text=str(user_text or ""),
|
|
182
|
+
)
|
|
183
|
+
if _path_identity_key(path) not in forbidden_path_identities
|
|
184
|
+
]
|
|
185
|
+
task_anchor_paths = [
|
|
186
|
+
path
|
|
187
|
+
for path in _select_task_anchor_paths(
|
|
188
|
+
task=task,
|
|
189
|
+
task_position=task_position,
|
|
190
|
+
task_count=task_count,
|
|
191
|
+
anchor_groups=anchor_groups,
|
|
192
|
+
)
|
|
193
|
+
if _path_identity_key(path) not in forbidden_path_identities
|
|
194
|
+
]
|
|
195
|
+
task_anchor_set = set(task_anchor_paths)
|
|
196
|
+
protected_symbol_remap_path_identities = _missing_path_identities(
|
|
197
|
+
[*task_explicit_path_hints, *task_anchor_paths],
|
|
198
|
+
workspace_root=root,
|
|
199
|
+
known_paths=known_paths,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
normalized_estimated, dropped_estimated = split_normalized_repo_path_list(
|
|
203
|
+
task.get("estimated_files"),
|
|
204
|
+
root=root,
|
|
205
|
+
)
|
|
206
|
+
normalized_write_scope, dropped_write_scope = split_normalized_repo_path_list(
|
|
207
|
+
task.get("write_scope"),
|
|
208
|
+
root=root,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
if dropped_estimated:
|
|
212
|
+
warnings.append(
|
|
213
|
+
f"Task {task_id}: dropped invalid estimated_files entries: "
|
|
214
|
+
+ ", ".join(dropped_estimated)
|
|
215
|
+
)
|
|
216
|
+
if dropped_write_scope:
|
|
217
|
+
warnings.append(
|
|
218
|
+
f"Task {task_id}: dropped invalid write_scope entries: "
|
|
219
|
+
+ ", ".join(dropped_write_scope)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
estimated_files, dropped_internal_estimated = _drop_protected_paths(normalized_estimated)
|
|
223
|
+
write_scope, dropped_internal_write_scope = _drop_protected_paths(normalized_write_scope)
|
|
224
|
+
if dropped_internal_estimated:
|
|
225
|
+
warnings.append(
|
|
226
|
+
f"Task {task_id}: dropped protected estimated_files entries: "
|
|
227
|
+
+ ", ".join(dropped_internal_estimated)
|
|
228
|
+
)
|
|
229
|
+
if dropped_internal_write_scope:
|
|
230
|
+
warnings.append(
|
|
231
|
+
f"Task {task_id}: dropped protected write_scope entries: "
|
|
232
|
+
+ ", ".join(dropped_internal_write_scope)
|
|
233
|
+
)
|
|
234
|
+
estimated_files, dropped_forbidden_estimated = _drop_forbidden_paths(
|
|
235
|
+
estimated_files,
|
|
236
|
+
forbidden_path_identities,
|
|
237
|
+
)
|
|
238
|
+
if dropped_forbidden_estimated:
|
|
239
|
+
warnings.append(
|
|
240
|
+
f"Task {task_id}: dropped forbidden estimated_files entries: "
|
|
241
|
+
+ ", ".join(dropped_forbidden_estimated)
|
|
242
|
+
)
|
|
243
|
+
write_scope, dropped_forbidden_write_scope = _drop_forbidden_paths(
|
|
244
|
+
write_scope,
|
|
245
|
+
forbidden_path_identities,
|
|
246
|
+
)
|
|
247
|
+
if dropped_forbidden_write_scope:
|
|
248
|
+
warnings.append(
|
|
249
|
+
f"Task {task_id}: dropped forbidden write_scope entries: "
|
|
250
|
+
+ ", ".join(dropped_forbidden_write_scope)
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if not estimated_files:
|
|
254
|
+
inferred_paths, _ = _infer_estimated_files(
|
|
255
|
+
task=task,
|
|
256
|
+
workspace_root=root,
|
|
257
|
+
known_paths=known_paths,
|
|
258
|
+
allowed_missing_paths=task_anchor_set,
|
|
259
|
+
latest_user_text=str(user_text or ""),
|
|
260
|
+
)
|
|
261
|
+
if inferred_paths:
|
|
262
|
+
estimated_files = inferred_paths
|
|
263
|
+
warnings.append(
|
|
264
|
+
f"Task {task_id}: inferred estimated_files from task text: "
|
|
265
|
+
+ ", ".join(inferred_paths)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
estimated_files, dropped_ungrounded_estimated = _drop_ungrounded_suspicious_paths(
|
|
269
|
+
paths=estimated_files,
|
|
270
|
+
workspace_root=root,
|
|
271
|
+
known_paths=known_paths,
|
|
272
|
+
allowed_missing_paths=task_anchor_set,
|
|
273
|
+
)
|
|
274
|
+
if dropped_ungrounded_estimated:
|
|
275
|
+
warnings.append(
|
|
276
|
+
f"Task {task_id}: dropped suspicious estimated_files entries not grounded in the "
|
|
277
|
+
"latest user request: " + ", ".join(dropped_ungrounded_estimated)
|
|
278
|
+
)
|
|
279
|
+
if not estimated_files:
|
|
280
|
+
inferred_paths, _ = _infer_estimated_files(
|
|
281
|
+
task=task,
|
|
282
|
+
workspace_root=root,
|
|
283
|
+
known_paths=known_paths,
|
|
284
|
+
allowed_missing_paths=task_anchor_set,
|
|
285
|
+
latest_user_text=str(user_text or ""),
|
|
286
|
+
)
|
|
287
|
+
if inferred_paths:
|
|
288
|
+
estimated_files = inferred_paths
|
|
289
|
+
warnings.append(
|
|
290
|
+
f"Task {task_id}: restored grounded estimated_files from task text: "
|
|
291
|
+
+ ", ".join(inferred_paths)
|
|
292
|
+
)
|
|
293
|
+
explicit_task_paths, _ = _infer_estimated_files(
|
|
294
|
+
task=task,
|
|
295
|
+
workspace_root=root,
|
|
296
|
+
known_paths=known_paths,
|
|
297
|
+
allowed_missing_paths=task_anchor_set,
|
|
298
|
+
latest_user_text=str(user_text or ""),
|
|
299
|
+
)
|
|
300
|
+
added_explicit_estimated = [
|
|
301
|
+
path for path in explicit_task_paths if path not in estimated_files
|
|
302
|
+
]
|
|
303
|
+
if added_explicit_estimated:
|
|
304
|
+
estimated_files = _dedupe_keep_order([*estimated_files, *added_explicit_estimated])
|
|
305
|
+
warnings.append(
|
|
306
|
+
f"Task {task_id}: added explicit task path hints to estimated_files: "
|
|
307
|
+
+ ", ".join(added_explicit_estimated)
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
estimated_files, estimated_warning = _apply_task_anchor_paths(
|
|
311
|
+
current_paths=estimated_files,
|
|
312
|
+
anchor_paths=task_anchor_paths,
|
|
313
|
+
field_name="estimated_files",
|
|
314
|
+
)
|
|
315
|
+
if estimated_warning:
|
|
316
|
+
warnings.append(f"Task {task_id}: {estimated_warning}")
|
|
317
|
+
estimated_files, dropped_forbidden_estimated = _drop_forbidden_paths(
|
|
318
|
+
estimated_files,
|
|
319
|
+
forbidden_path_identities,
|
|
320
|
+
)
|
|
321
|
+
if dropped_forbidden_estimated:
|
|
322
|
+
warnings.append(
|
|
323
|
+
f"Task {task_id}: dropped forbidden estimated_files entries: "
|
|
324
|
+
+ ", ".join(dropped_forbidden_estimated)
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
for path in estimated_files:
|
|
328
|
+
if path in task_anchor_set:
|
|
329
|
+
continue
|
|
330
|
+
if not allow_create_targets and _is_suspicious_path(
|
|
331
|
+
path=path,
|
|
332
|
+
workspace_root=root,
|
|
333
|
+
known_paths=known_paths,
|
|
334
|
+
):
|
|
335
|
+
warnings.append(
|
|
336
|
+
f"Task {task_id}: estimated_files entry may be suspicious or missing: {path}"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
if not write_scope and estimated_files:
|
|
340
|
+
write_scope = list(estimated_files)
|
|
341
|
+
warnings.append(
|
|
342
|
+
f"Task {task_id}: seeded write_scope from estimated_files: "
|
|
343
|
+
+ ", ".join(estimated_files)
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
write_scope, dropped_ungrounded_write_scope = _drop_ungrounded_suspicious_paths(
|
|
347
|
+
paths=write_scope,
|
|
348
|
+
workspace_root=root,
|
|
349
|
+
known_paths=known_paths,
|
|
350
|
+
allowed_missing_paths=task_anchor_set,
|
|
351
|
+
)
|
|
352
|
+
if dropped_ungrounded_write_scope:
|
|
353
|
+
warnings.append(
|
|
354
|
+
f"Task {task_id}: dropped suspicious write_scope entries not grounded in the "
|
|
355
|
+
"latest user request: " + ", ".join(dropped_ungrounded_write_scope)
|
|
356
|
+
)
|
|
357
|
+
if not write_scope and estimated_files:
|
|
358
|
+
write_scope = list(estimated_files)
|
|
359
|
+
warnings.append(
|
|
360
|
+
f"Task {task_id}: reseeded write_scope from grounded estimated_files: "
|
|
361
|
+
+ ", ".join(estimated_files)
|
|
362
|
+
)
|
|
363
|
+
added_explicit_write_scope = [
|
|
364
|
+
path
|
|
365
|
+
for path in explicit_task_paths
|
|
366
|
+
if path in estimated_files and path not in write_scope
|
|
367
|
+
]
|
|
368
|
+
if added_explicit_write_scope:
|
|
369
|
+
write_scope = _dedupe_keep_order([*write_scope, *added_explicit_write_scope])
|
|
370
|
+
warnings.append(
|
|
371
|
+
f"Task {task_id}: added explicit task path hints to write_scope: "
|
|
372
|
+
+ ", ".join(added_explicit_write_scope)
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
write_scope, write_scope_warning = _apply_task_anchor_paths(
|
|
376
|
+
current_paths=write_scope,
|
|
377
|
+
anchor_paths=task_anchor_paths,
|
|
378
|
+
field_name="write_scope",
|
|
379
|
+
)
|
|
380
|
+
if write_scope_warning:
|
|
381
|
+
warnings.append(f"Task {task_id}: {write_scope_warning}")
|
|
382
|
+
write_scope, dropped_forbidden_write_scope = _drop_forbidden_paths(
|
|
383
|
+
write_scope,
|
|
384
|
+
forbidden_path_identities,
|
|
385
|
+
)
|
|
386
|
+
if dropped_forbidden_write_scope:
|
|
387
|
+
warnings.append(
|
|
388
|
+
f"Task {task_id}: dropped forbidden write_scope entries: "
|
|
389
|
+
+ ", ".join(dropped_forbidden_write_scope)
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
if not task_anchor_paths:
|
|
393
|
+
symbol_paths = _resolve_symbol_grounded_paths(
|
|
394
|
+
task=task,
|
|
395
|
+
workspace_root=root,
|
|
396
|
+
latest_user_text=str(user_text or ""),
|
|
397
|
+
)
|
|
398
|
+
grounded_paths = symbol_paths or _resolve_named_code_file_paths(
|
|
399
|
+
task=task,
|
|
400
|
+
workspace_root=root,
|
|
401
|
+
latest_user_text=str(user_text or ""),
|
|
402
|
+
)
|
|
403
|
+
if grounded_paths:
|
|
404
|
+
estimated_files, estimated_symbol_warning = _apply_symbol_grounded_paths(
|
|
405
|
+
current_paths=estimated_files,
|
|
406
|
+
symbol_paths=grounded_paths,
|
|
407
|
+
workspace_root=root,
|
|
408
|
+
task=task,
|
|
409
|
+
field_name="estimated_files",
|
|
410
|
+
protected_missing_path_identities=protected_symbol_remap_path_identities,
|
|
411
|
+
)
|
|
412
|
+
if estimated_symbol_warning:
|
|
413
|
+
warnings.append(f"Task {task_id}: {estimated_symbol_warning}")
|
|
414
|
+
write_scope, write_symbol_warning = _apply_symbol_grounded_paths(
|
|
415
|
+
current_paths=write_scope,
|
|
416
|
+
symbol_paths=grounded_paths,
|
|
417
|
+
workspace_root=root,
|
|
418
|
+
task=task,
|
|
419
|
+
field_name="write_scope",
|
|
420
|
+
protected_missing_path_identities=protected_symbol_remap_path_identities,
|
|
421
|
+
)
|
|
422
|
+
if write_symbol_warning:
|
|
423
|
+
warnings.append(f"Task {task_id}: {write_symbol_warning}")
|
|
424
|
+
|
|
425
|
+
estimated_files, estimated_constraint_violations = (
|
|
426
|
+
filter_scope_entries_for_planning_constraints(
|
|
427
|
+
estimated_files,
|
|
428
|
+
task={
|
|
429
|
+
**task,
|
|
430
|
+
"estimated_files": estimated_files,
|
|
431
|
+
"write_scope": estimated_files,
|
|
432
|
+
},
|
|
433
|
+
constraints=planning_constraints,
|
|
434
|
+
)
|
|
435
|
+
)
|
|
436
|
+
if estimated_constraint_violations:
|
|
437
|
+
warnings.append(
|
|
438
|
+
f"Task {task_id}: dropped estimated_files outside planning constraints: "
|
|
439
|
+
+ ", ".join(
|
|
440
|
+
f"{item.path} ({item.classification}; {item.reason_code})"
|
|
441
|
+
for item in estimated_constraint_violations[:6]
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
write_scope, write_constraint_violations = filter_scope_entries_for_planning_constraints(
|
|
445
|
+
write_scope,
|
|
446
|
+
task={**task, "estimated_files": write_scope, "write_scope": write_scope},
|
|
447
|
+
constraints=planning_constraints,
|
|
448
|
+
)
|
|
449
|
+
if write_constraint_violations:
|
|
450
|
+
warnings.append(
|
|
451
|
+
f"Task {task_id}: dropped write_scope outside planning constraints: "
|
|
452
|
+
+ ", ".join(
|
|
453
|
+
f"{item.path} ({item.classification}; {item.reason_code})"
|
|
454
|
+
for item in write_constraint_violations[:6]
|
|
455
|
+
)
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
for path in write_scope:
|
|
459
|
+
if path in task_anchor_set:
|
|
460
|
+
continue
|
|
461
|
+
if not allow_create_targets and _is_suspicious_path(
|
|
462
|
+
path=path,
|
|
463
|
+
workspace_root=root,
|
|
464
|
+
known_paths=known_paths,
|
|
465
|
+
):
|
|
466
|
+
warnings.append(
|
|
467
|
+
f"Task {task_id}: write_scope entry may be suspicious or missing: {path}"
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
lifecycle = classify_task_lifecycle(
|
|
471
|
+
title=str(task.get("title") or "").strip(),
|
|
472
|
+
description=str(task.get("description") or "").strip(),
|
|
473
|
+
acceptance_criteria=_string_list(task.get("acceptance_criteria")),
|
|
474
|
+
estimated_files=estimated_files,
|
|
475
|
+
write_scope=write_scope,
|
|
476
|
+
explicit_analysis_only=True if task.get("analysis_only") is True else None,
|
|
477
|
+
)
|
|
478
|
+
if lifecycle.kind == TASK_KIND_ANALYSIS_ONLY:
|
|
479
|
+
if estimated_files or write_scope:
|
|
480
|
+
warnings.append(
|
|
481
|
+
f"Task {task_id}: cleared file mutation scope for analysis-only/report-only task"
|
|
482
|
+
)
|
|
483
|
+
estimated_files = []
|
|
484
|
+
write_scope = []
|
|
485
|
+
|
|
486
|
+
if task_requires_runnable_file_scope(
|
|
487
|
+
title=str(task.get("title") or "").strip(),
|
|
488
|
+
description=str(task.get("description") or "").strip(),
|
|
489
|
+
acceptance_criteria=_string_list(task.get("acceptance_criteria")),
|
|
490
|
+
estimated_files=estimated_files,
|
|
491
|
+
write_scope=write_scope,
|
|
492
|
+
) and not has_runnable_local_file_scope(
|
|
493
|
+
estimated_files=estimated_files,
|
|
494
|
+
write_scope=write_scope,
|
|
495
|
+
):
|
|
496
|
+
warnings.append(
|
|
497
|
+
task_readiness_warning(
|
|
498
|
+
task_id=task_id,
|
|
499
|
+
title=str(task.get("title") or "").strip(),
|
|
500
|
+
)
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
patch: dict[str, Any] = {}
|
|
504
|
+
if estimated_files != current_estimated:
|
|
505
|
+
task["estimated_files"] = estimated_files
|
|
506
|
+
patch["estimated_files"] = estimated_files
|
|
507
|
+
if write_scope != current_write_scope:
|
|
508
|
+
task["write_scope"] = write_scope
|
|
509
|
+
patch["write_scope"] = write_scope
|
|
510
|
+
scope_changed = "estimated_files" in patch or "write_scope" in patch
|
|
511
|
+
should_persist_lifecycle = (
|
|
512
|
+
scope_changed
|
|
513
|
+
or "task_kind" in task
|
|
514
|
+
or "task_kind_reason" in task
|
|
515
|
+
or lifecycle.kind == TASK_KIND_ANALYSIS_ONLY
|
|
516
|
+
)
|
|
517
|
+
if should_persist_lifecycle and task.get("task_kind") != lifecycle.kind:
|
|
518
|
+
task["task_kind"] = lifecycle.kind
|
|
519
|
+
patch["task_kind"] = lifecycle.kind
|
|
520
|
+
if should_persist_lifecycle and task.get("task_kind_reason") != lifecycle.reason_code:
|
|
521
|
+
task["task_kind_reason"] = lifecycle.reason_code
|
|
522
|
+
patch["task_kind_reason"] = lifecycle.reason_code
|
|
523
|
+
if lifecycle.kind == TASK_KIND_ANALYSIS_ONLY and task.get("analysis_only") is not True:
|
|
524
|
+
task["analysis_only"] = True
|
|
525
|
+
patch["analysis_only"] = True
|
|
526
|
+
elif (
|
|
527
|
+
lifecycle.kind != TASK_KIND_ANALYSIS_ONLY
|
|
528
|
+
and task.get("analysis_only") is True
|
|
529
|
+
and task.get("task_kind") != TASK_KIND_ANALYSIS_ONLY
|
|
530
|
+
):
|
|
531
|
+
task.pop("analysis_only", None)
|
|
532
|
+
patch["analysis_only"] = None
|
|
533
|
+
if patch:
|
|
534
|
+
task_updates[task_id] = patch
|
|
535
|
+
changed = True
|
|
536
|
+
|
|
537
|
+
return PlanReconciliationResult(
|
|
538
|
+
changed=changed,
|
|
539
|
+
warnings=_dedupe_keep_order(warnings),
|
|
540
|
+
task_updates=task_updates,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _task_label(*, task: dict[str, Any], index: int) -> str:
|
|
545
|
+
task_id = str(task.get("id") or "").strip()
|
|
546
|
+
if task_id:
|
|
547
|
+
return task_id
|
|
548
|
+
title = str(task.get("title") or "").strip()
|
|
549
|
+
if title:
|
|
550
|
+
return title
|
|
551
|
+
return f"task[{index}]"
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _string_list(value: Any) -> list[str]:
|
|
555
|
+
if not isinstance(value, list):
|
|
556
|
+
return []
|
|
557
|
+
out: list[str] = []
|
|
558
|
+
for item in value:
|
|
559
|
+
text = str(item).strip()
|
|
560
|
+
if text:
|
|
561
|
+
out.append(text)
|
|
562
|
+
return out
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _dedupe_keep_order(values: list[str]) -> list[str]:
|
|
566
|
+
seen: set[str] = set()
|
|
567
|
+
out: list[str] = []
|
|
568
|
+
for value in values:
|
|
569
|
+
key = value.casefold()
|
|
570
|
+
if key in seen:
|
|
571
|
+
continue
|
|
572
|
+
seen.add(key)
|
|
573
|
+
out.append(value)
|
|
574
|
+
return out
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _path_identity_key(value: str) -> str:
|
|
578
|
+
normalized = str(value or "").strip().replace("\\", "/").rstrip("/")
|
|
579
|
+
if normalized.casefold() in {"readme", "readme.md"}:
|
|
580
|
+
return "__readme_alias__"
|
|
581
|
+
return normalized.casefold()
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _drop_forbidden_paths(
|
|
585
|
+
paths: list[str], forbidden_paths: set[str]
|
|
586
|
+
) -> tuple[list[str], list[str]]:
|
|
587
|
+
if not forbidden_paths:
|
|
588
|
+
return _dedupe_keep_order(paths), []
|
|
589
|
+
kept: list[str] = []
|
|
590
|
+
dropped: list[str] = []
|
|
591
|
+
for path in _dedupe_keep_order(paths):
|
|
592
|
+
if _path_identity_key(path) in forbidden_paths:
|
|
593
|
+
dropped.append(path)
|
|
594
|
+
continue
|
|
595
|
+
kept.append(path)
|
|
596
|
+
return kept, dropped
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _task_forbidden_path_identities(*, task: dict[str, Any], user_text: str | None) -> set[str]:
|
|
600
|
+
acceptance = _string_list(task.get("acceptance_criteria"))
|
|
601
|
+
text = "\n".join(
|
|
602
|
+
[
|
|
603
|
+
str(task.get("title") or ""),
|
|
604
|
+
str(task.get("description") or ""),
|
|
605
|
+
*acceptance,
|
|
606
|
+
str(user_text or ""),
|
|
607
|
+
]
|
|
608
|
+
)
|
|
609
|
+
return {_path_identity_key(path) for path in extract_forbidden_repo_path_hints(text)}
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def _task_explicit_path_hints(
|
|
613
|
+
*,
|
|
614
|
+
task: dict[str, Any],
|
|
615
|
+
latest_user_text: str = "",
|
|
616
|
+
) -> list[str]:
|
|
617
|
+
task_text = "\n".join(
|
|
618
|
+
[
|
|
619
|
+
str(task.get("title") or ""),
|
|
620
|
+
str(task.get("description") or ""),
|
|
621
|
+
*[str(item) for item in _string_list(task.get("acceptance_criteria"))],
|
|
622
|
+
]
|
|
623
|
+
)
|
|
624
|
+
hints, _obsolete_hints = filter_obsolete_direction_paths(
|
|
625
|
+
extract_repo_path_hints(task_text),
|
|
626
|
+
latest_user_text=latest_user_text,
|
|
627
|
+
task_text=task_text,
|
|
628
|
+
)
|
|
629
|
+
out: list[str] = []
|
|
630
|
+
for hint in hints:
|
|
631
|
+
if _has_glob(hint):
|
|
632
|
+
continue
|
|
633
|
+
if is_internal_alysis_path(hint) or hint == ".git" or hint.startswith(".git/"):
|
|
634
|
+
continue
|
|
635
|
+
out.append(hint)
|
|
636
|
+
return _dedupe_keep_order(out)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def _missing_path_identities(
|
|
640
|
+
paths: list[str],
|
|
641
|
+
*,
|
|
642
|
+
workspace_root: Path,
|
|
643
|
+
known_paths: set[str],
|
|
644
|
+
) -> set[str]:
|
|
645
|
+
identities: set[str] = set()
|
|
646
|
+
for path in _dedupe_keep_order(paths):
|
|
647
|
+
if _has_glob(path) or path in known_paths:
|
|
648
|
+
continue
|
|
649
|
+
try:
|
|
650
|
+
exists = (workspace_root / path).resolve().exists()
|
|
651
|
+
except OSError:
|
|
652
|
+
exists = False
|
|
653
|
+
if exists:
|
|
654
|
+
continue
|
|
655
|
+
identities.add(_path_identity_key(path))
|
|
656
|
+
return identities
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _candidate_user_messages(
|
|
660
|
+
*,
|
|
661
|
+
transcript_tail: list[dict[str, Any]] | None,
|
|
662
|
+
user_text: str | None,
|
|
663
|
+
) -> list[str]:
|
|
664
|
+
messages: list[str] = []
|
|
665
|
+
latest = str(user_text or "").strip()
|
|
666
|
+
if latest:
|
|
667
|
+
messages.append(latest)
|
|
668
|
+
for item in reversed(transcript_tail or []):
|
|
669
|
+
if not isinstance(item, dict):
|
|
670
|
+
continue
|
|
671
|
+
role = str(item.get("role") or "").strip().lower()
|
|
672
|
+
if role != "user":
|
|
673
|
+
continue
|
|
674
|
+
content = str(item.get("content") or "").strip()
|
|
675
|
+
if not content:
|
|
676
|
+
continue
|
|
677
|
+
if messages and content == messages[-1]:
|
|
678
|
+
continue
|
|
679
|
+
messages.append(content)
|
|
680
|
+
return messages
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _extract_task_ids(text: str) -> tuple[str, ...]:
|
|
684
|
+
seen: set[str] = set()
|
|
685
|
+
ids: list[str] = []
|
|
686
|
+
for match in _TASK_ID_HINT_RE.findall(text or ""):
|
|
687
|
+
task_id = match.upper()
|
|
688
|
+
if task_id in seen:
|
|
689
|
+
continue
|
|
690
|
+
seen.add(task_id)
|
|
691
|
+
ids.append(task_id)
|
|
692
|
+
return tuple(ids)
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _extract_path_anchor_groups(text: str) -> list[_PathAnchorGroup]:
|
|
696
|
+
groups: list[_PathAnchorGroup] = []
|
|
697
|
+
for raw_line in (text or "").splitlines():
|
|
698
|
+
line = raw_line.strip()
|
|
699
|
+
if not line:
|
|
700
|
+
continue
|
|
701
|
+
forbidden = {_path_identity_key(path) for path in extract_forbidden_repo_path_hints(line)}
|
|
702
|
+
paths = tuple(
|
|
703
|
+
path
|
|
704
|
+
for path in filter_obsolete_direction_paths(
|
|
705
|
+
extract_repo_path_hints(line), latest_user_text=text, task_text=line
|
|
706
|
+
)[0]
|
|
707
|
+
if _path_identity_key(path) not in forbidden
|
|
708
|
+
)
|
|
709
|
+
if not paths:
|
|
710
|
+
continue
|
|
711
|
+
groups.append(
|
|
712
|
+
_PathAnchorGroup(
|
|
713
|
+
task_ids=_extract_task_ids(line),
|
|
714
|
+
paths=paths,
|
|
715
|
+
)
|
|
716
|
+
)
|
|
717
|
+
if groups:
|
|
718
|
+
return groups
|
|
719
|
+
forbidden = {_path_identity_key(path) for path in extract_forbidden_repo_path_hints(text or "")}
|
|
720
|
+
global_paths = tuple(
|
|
721
|
+
path
|
|
722
|
+
for path in filter_obsolete_direction_paths(
|
|
723
|
+
extract_repo_path_hints(text or ""), latest_user_text=text, task_text=text
|
|
724
|
+
)[0]
|
|
725
|
+
if _path_identity_key(path) not in forbidden
|
|
726
|
+
)
|
|
727
|
+
if not global_paths:
|
|
728
|
+
return []
|
|
729
|
+
return [_PathAnchorGroup(task_ids=(), paths=global_paths)]
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def _latest_path_anchor_groups(
|
|
733
|
+
*,
|
|
734
|
+
transcript_tail: list[dict[str, Any]] | None,
|
|
735
|
+
user_text: str | None,
|
|
736
|
+
) -> list[_PathAnchorGroup]:
|
|
737
|
+
for message in _candidate_user_messages(
|
|
738
|
+
transcript_tail=transcript_tail,
|
|
739
|
+
user_text=user_text,
|
|
740
|
+
):
|
|
741
|
+
groups = _extract_path_anchor_groups(message)
|
|
742
|
+
if groups:
|
|
743
|
+
return groups
|
|
744
|
+
return []
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def _select_task_anchor_paths(
|
|
748
|
+
*,
|
|
749
|
+
task: dict[str, Any],
|
|
750
|
+
task_position: int,
|
|
751
|
+
task_count: int,
|
|
752
|
+
anchor_groups: list[_PathAnchorGroup],
|
|
753
|
+
) -> list[str]:
|
|
754
|
+
if not anchor_groups:
|
|
755
|
+
return []
|
|
756
|
+
task_id = str(task.get("id") or "").strip().upper()
|
|
757
|
+
if task_id:
|
|
758
|
+
matched = [
|
|
759
|
+
path for group in anchor_groups if task_id in group.task_ids for path in group.paths
|
|
760
|
+
]
|
|
761
|
+
if matched:
|
|
762
|
+
return _dedupe_keep_order(matched)
|
|
763
|
+
|
|
764
|
+
if task_count == 1:
|
|
765
|
+
return _dedupe_keep_order([path for group in anchor_groups for path in group.paths])
|
|
766
|
+
|
|
767
|
+
anonymous_groups = [group for group in anchor_groups if not group.task_ids]
|
|
768
|
+
if len(anonymous_groups) == task_count:
|
|
769
|
+
return list(anonymous_groups[task_position].paths)
|
|
770
|
+
return []
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def _drop_protected_paths(paths: list[str]) -> tuple[list[str], list[str]]:
|
|
774
|
+
kept: list[str] = []
|
|
775
|
+
dropped: list[str] = []
|
|
776
|
+
for path in paths:
|
|
777
|
+
if is_internal_alysis_path(path) or path == ".git" or path.startswith(".git/"):
|
|
778
|
+
dropped.append(path)
|
|
779
|
+
continue
|
|
780
|
+
kept.append(path)
|
|
781
|
+
return _dedupe_keep_order(kept), dropped
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def _workspace_context_is_greenfield(workspace_context: dict[str, Any] | None) -> bool:
|
|
785
|
+
return bool(isinstance(workspace_context, dict) and workspace_context.get("greenfield"))
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def _task_allows_create_targets(*, task: dict[str, Any], greenfield: bool) -> bool:
|
|
789
|
+
if greenfield:
|
|
790
|
+
return True
|
|
791
|
+
text = "\n".join(
|
|
792
|
+
[
|
|
793
|
+
str(task.get("title") or ""),
|
|
794
|
+
str(task.get("description") or ""),
|
|
795
|
+
*[str(item) for item in _string_list(task.get("acceptance_criteria"))],
|
|
796
|
+
]
|
|
797
|
+
)
|
|
798
|
+
return bool(_EXPLICIT_MISSING_PATH_ACTION_RE.search(text))
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _apply_task_anchor_paths(
|
|
802
|
+
*,
|
|
803
|
+
current_paths: list[str],
|
|
804
|
+
anchor_paths: list[str],
|
|
805
|
+
field_name: str,
|
|
806
|
+
) -> tuple[list[str], str | None]:
|
|
807
|
+
normalized_current = _dedupe_keep_order(current_paths)
|
|
808
|
+
normalized_anchor = _dedupe_keep_order(anchor_paths)
|
|
809
|
+
if not normalized_anchor:
|
|
810
|
+
return normalized_current, None
|
|
811
|
+
if not normalized_current:
|
|
812
|
+
return normalized_anchor, (
|
|
813
|
+
f"restored {field_name} from explicit user grounding: " + ", ".join(normalized_anchor)
|
|
814
|
+
)
|
|
815
|
+
if any(path in normalized_current for path in normalized_anchor):
|
|
816
|
+
missing = [path for path in normalized_anchor if path not in normalized_current]
|
|
817
|
+
if not missing:
|
|
818
|
+
return normalized_current, None
|
|
819
|
+
return normalized_current + missing, (
|
|
820
|
+
f"added explicit user-grounded {field_name} entries: " + ", ".join(missing)
|
|
821
|
+
)
|
|
822
|
+
return normalized_anchor, (
|
|
823
|
+
f"replaced {field_name} with explicit user-grounded paths: "
|
|
824
|
+
+ ", ".join(normalized_current)
|
|
825
|
+
+ " -> "
|
|
826
|
+
+ ", ".join(normalized_anchor)
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def _has_glob(path: str) -> bool:
|
|
831
|
+
return any(char in path for char in _GLOB_CHARS)
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def _has_framework_dynamic_route_segment(path: str) -> bool:
|
|
835
|
+
try:
|
|
836
|
+
parts = PurePosixPath(path.replace("\\", "/")).parts
|
|
837
|
+
except ValueError:
|
|
838
|
+
return False
|
|
839
|
+
return any(_FRAMEWORK_DYNAMIC_ROUTE_SEGMENT_RE.fullmatch(part) for part in parts)
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def _known_workspace_paths(workspace_context: dict[str, Any] | None) -> set[str]:
|
|
843
|
+
if not isinstance(workspace_context, dict):
|
|
844
|
+
return set()
|
|
845
|
+
known: set[str] = set()
|
|
846
|
+
for rel_path in _string_list(workspace_context.get("readme_paths")):
|
|
847
|
+
known.add(rel_path)
|
|
848
|
+
for rel_path in _string_list(workspace_context.get("observed_paths")):
|
|
849
|
+
known.add(rel_path)
|
|
850
|
+
conventions_path = str(workspace_context.get("conventions_path") or "").strip()
|
|
851
|
+
if conventions_path:
|
|
852
|
+
known.add(conventions_path)
|
|
853
|
+
for entry in workspace_context.get("manifests") or []:
|
|
854
|
+
if not isinstance(entry, dict):
|
|
855
|
+
continue
|
|
856
|
+
rel_path = str(entry.get("path") or "").strip()
|
|
857
|
+
if rel_path:
|
|
858
|
+
known.add(rel_path)
|
|
859
|
+
for entry in workspace_context.get("top_level_entries") or []:
|
|
860
|
+
if not isinstance(entry, dict):
|
|
861
|
+
continue
|
|
862
|
+
rel_path = str(entry.get("path") or "").strip()
|
|
863
|
+
if rel_path:
|
|
864
|
+
known.add(rel_path)
|
|
865
|
+
return known
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def _is_suspicious_path(
|
|
869
|
+
*,
|
|
870
|
+
path: str,
|
|
871
|
+
workspace_root: Path,
|
|
872
|
+
known_paths: set[str],
|
|
873
|
+
) -> bool:
|
|
874
|
+
if path in known_paths:
|
|
875
|
+
return False
|
|
876
|
+
if _has_glob(path) or _has_framework_dynamic_route_segment(path):
|
|
877
|
+
return False
|
|
878
|
+
candidate = (workspace_root / path).resolve()
|
|
879
|
+
return not candidate.exists() and not candidate.parent.exists()
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def _explicit_missing_path_is_grounded_in_task_text(*, task_text: str, path: str) -> bool:
|
|
883
|
+
normalized_path = str(path or "").strip().replace("\\", "/")
|
|
884
|
+
if not normalized_path:
|
|
885
|
+
return False
|
|
886
|
+
tail = normalized_path.rsplit("/", 1)[-1]
|
|
887
|
+
if "." not in tail:
|
|
888
|
+
return False
|
|
889
|
+
for raw_line in (task_text or "").splitlines():
|
|
890
|
+
line = raw_line.strip()
|
|
891
|
+
if not line or normalized_path.casefold() not in line.casefold():
|
|
892
|
+
continue
|
|
893
|
+
if _EXPLICIT_MISSING_PATH_ACTION_RE.search(line):
|
|
894
|
+
return True
|
|
895
|
+
return False
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
def _infer_estimated_files(
|
|
899
|
+
*,
|
|
900
|
+
task: dict[str, Any],
|
|
901
|
+
workspace_root: Path,
|
|
902
|
+
known_paths: set[str],
|
|
903
|
+
allowed_missing_paths: set[str] | None = None,
|
|
904
|
+
latest_user_text: str = "",
|
|
905
|
+
) -> tuple[list[str], list[str]]:
|
|
906
|
+
title = str(task.get("title") or "").strip()
|
|
907
|
+
description = str(task.get("description") or "").strip()
|
|
908
|
+
acceptance = _string_list(task.get("acceptance_criteria"))
|
|
909
|
+
task_text = "\n".join([title, description, *acceptance]).strip()
|
|
910
|
+
hints = extract_repo_path_hints(task_text)
|
|
911
|
+
forbidden_hints = {
|
|
912
|
+
_path_identity_key(path)
|
|
913
|
+
for path in extract_forbidden_repo_path_hints(
|
|
914
|
+
"\n".join([task_text, str(latest_user_text or "")])
|
|
915
|
+
)
|
|
916
|
+
}
|
|
917
|
+
hints, obsolete_hints = filter_obsolete_direction_paths(
|
|
918
|
+
hints,
|
|
919
|
+
latest_user_text=latest_user_text,
|
|
920
|
+
task_text=task_text,
|
|
921
|
+
)
|
|
922
|
+
allowed_missing = {path for path in (allowed_missing_paths or set()) if path}
|
|
923
|
+
inferred: list[str] = []
|
|
924
|
+
ignored: list[str] = list(obsolete_hints)
|
|
925
|
+
for hint in hints:
|
|
926
|
+
if _has_glob(hint):
|
|
927
|
+
continue
|
|
928
|
+
if _path_identity_key(hint) in forbidden_hints:
|
|
929
|
+
ignored.append(hint)
|
|
930
|
+
continue
|
|
931
|
+
if is_internal_alysis_path(hint) or hint == ".git" or hint.startswith(".git/"):
|
|
932
|
+
ignored.append(hint)
|
|
933
|
+
continue
|
|
934
|
+
if hint in allowed_missing:
|
|
935
|
+
inferred.append(hint)
|
|
936
|
+
continue
|
|
937
|
+
if _is_suspicious_path(path=hint, workspace_root=workspace_root, known_paths=known_paths):
|
|
938
|
+
if _explicit_missing_path_is_grounded_in_task_text(
|
|
939
|
+
task_text=task_text,
|
|
940
|
+
path=hint,
|
|
941
|
+
):
|
|
942
|
+
inferred.append(hint)
|
|
943
|
+
continue
|
|
944
|
+
ignored.append(hint)
|
|
945
|
+
continue
|
|
946
|
+
inferred.append(hint)
|
|
947
|
+
return _dedupe_keep_order(inferred), _dedupe_keep_order(ignored)
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def _task_grounding_text(task: dict[str, Any], *, latest_user_text: str = "") -> str:
|
|
951
|
+
user_text = str(latest_user_text or "").strip()
|
|
952
|
+
title = str(task.get("title") or "").strip()
|
|
953
|
+
description = str(task.get("description") or "").strip()
|
|
954
|
+
if (
|
|
955
|
+
user_text
|
|
956
|
+
and title == "Implement requested repository change"
|
|
957
|
+
and "Use local search/read tools to locate" in description
|
|
958
|
+
):
|
|
959
|
+
return user_text
|
|
960
|
+
return "\n".join(
|
|
961
|
+
[
|
|
962
|
+
title,
|
|
963
|
+
description,
|
|
964
|
+
*[str(item) for item in _string_list(task.get("acceptance_criteria"))],
|
|
965
|
+
]
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def _task_symbol_candidates(
|
|
970
|
+
task: dict[str, Any],
|
|
971
|
+
*,
|
|
972
|
+
latest_user_text: str = "",
|
|
973
|
+
) -> list[str]:
|
|
974
|
+
text = _task_grounding_text(task, latest_user_text=latest_user_text)
|
|
975
|
+
candidates: list[str] = []
|
|
976
|
+
for raw in _SYMBOL_CANDIDATE_RE.findall(text):
|
|
977
|
+
symbol = raw.strip()
|
|
978
|
+
if not symbol:
|
|
979
|
+
continue
|
|
980
|
+
lowered = symbol.casefold()
|
|
981
|
+
if lowered in _SYMBOL_CANDIDATE_STOPWORDS:
|
|
982
|
+
continue
|
|
983
|
+
if symbol.isupper() and "_" not in symbol:
|
|
984
|
+
continue
|
|
985
|
+
if "_" not in symbol and not any(char.isupper() for char in symbol[1:]):
|
|
986
|
+
continue
|
|
987
|
+
candidates.append(symbol)
|
|
988
|
+
return _dedupe_keep_order(candidates)[:6]
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
def _task_filename_candidates(
|
|
992
|
+
task: dict[str, Any],
|
|
993
|
+
*,
|
|
994
|
+
latest_user_text: str = "",
|
|
995
|
+
) -> list[str]:
|
|
996
|
+
text = _task_grounding_text(task, latest_user_text=latest_user_text)
|
|
997
|
+
candidates: list[str] = []
|
|
998
|
+
for raw in _FILENAME_TOKEN_RE.findall(text):
|
|
999
|
+
token = raw.strip().casefold()
|
|
1000
|
+
if not token or token in _SYMBOL_CANDIDATE_STOPWORDS:
|
|
1001
|
+
continue
|
|
1002
|
+
candidates.append(token)
|
|
1003
|
+
return _dedupe_keep_order(candidates)[:10]
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
def _iter_symbol_scan_files(workspace_root: Path) -> list[Path]:
|
|
1007
|
+
files: list[Path] = []
|
|
1008
|
+
try:
|
|
1009
|
+
iterator = workspace_root.rglob("*")
|
|
1010
|
+
except OSError:
|
|
1011
|
+
return files
|
|
1012
|
+
for path in iterator:
|
|
1013
|
+
if len(files) >= _MAX_SYMBOL_SCAN_FILES:
|
|
1014
|
+
break
|
|
1015
|
+
try:
|
|
1016
|
+
rel_parts = path.resolve().relative_to(workspace_root).parts
|
|
1017
|
+
except (OSError, ValueError):
|
|
1018
|
+
continue
|
|
1019
|
+
if any(part.casefold() in CODE_SCAN_SKIP_DIR_NAMES for part in rel_parts):
|
|
1020
|
+
continue
|
|
1021
|
+
if not path.is_file():
|
|
1022
|
+
continue
|
|
1023
|
+
rel_path = "/".join(part for part in rel_parts if part)
|
|
1024
|
+
if not is_symbol_scannable_path(rel_path):
|
|
1025
|
+
continue
|
|
1026
|
+
files.append(path)
|
|
1027
|
+
return files
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
def _symbol_definition_regex(symbol: str, suffix: str) -> re.Pattern[str]:
|
|
1031
|
+
return symbol_definition_regex(symbol, suffix)
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def _file_text(path: Path) -> str:
|
|
1035
|
+
try:
|
|
1036
|
+
if path.stat().st_size > _MAX_SYMBOL_SCAN_BYTES:
|
|
1037
|
+
return ""
|
|
1038
|
+
return path.read_text(encoding="utf-8", errors="ignore")
|
|
1039
|
+
except OSError:
|
|
1040
|
+
return ""
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
def _resolve_symbol_grounded_paths(
|
|
1044
|
+
*,
|
|
1045
|
+
task: dict[str, Any],
|
|
1046
|
+
workspace_root: Path,
|
|
1047
|
+
latest_user_text: str = "",
|
|
1048
|
+
) -> list[str]:
|
|
1049
|
+
symbols = _task_symbol_candidates(task, latest_user_text=latest_user_text)
|
|
1050
|
+
if not symbols:
|
|
1051
|
+
return []
|
|
1052
|
+
|
|
1053
|
+
paths_by_symbol: dict[str, list[str]] = {symbol: [] for symbol in symbols}
|
|
1054
|
+
for path in _iter_symbol_scan_files(workspace_root):
|
|
1055
|
+
text = _file_text(path)
|
|
1056
|
+
if not text:
|
|
1057
|
+
continue
|
|
1058
|
+
suffix = path.suffix.casefold()
|
|
1059
|
+
for symbol in symbols:
|
|
1060
|
+
if not _symbol_definition_regex(symbol, suffix).search(text):
|
|
1061
|
+
continue
|
|
1062
|
+
try:
|
|
1063
|
+
rel_path = path.resolve().relative_to(workspace_root).as_posix()
|
|
1064
|
+
except (OSError, ValueError):
|
|
1065
|
+
continue
|
|
1066
|
+
paths_by_symbol[symbol].append(rel_path)
|
|
1067
|
+
|
|
1068
|
+
grounded: list[str] = []
|
|
1069
|
+
for symbol in symbols:
|
|
1070
|
+
symbol_paths = _dedupe_keep_order(paths_by_symbol.get(symbol, []))
|
|
1071
|
+
if len(symbol_paths) == 1:
|
|
1072
|
+
grounded.extend(symbol_paths)
|
|
1073
|
+
return _dedupe_keep_order(grounded)[:3]
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
def _resolve_named_code_file_paths(
|
|
1077
|
+
*,
|
|
1078
|
+
task: dict[str, Any],
|
|
1079
|
+
workspace_root: Path,
|
|
1080
|
+
latest_user_text: str = "",
|
|
1081
|
+
) -> list[str]:
|
|
1082
|
+
candidates = _task_filename_candidates(task, latest_user_text=latest_user_text)
|
|
1083
|
+
if not candidates:
|
|
1084
|
+
return []
|
|
1085
|
+
|
|
1086
|
+
paths_by_stem: dict[str, list[str]] = {candidate: [] for candidate in candidates}
|
|
1087
|
+
for path in _iter_symbol_scan_files(workspace_root):
|
|
1088
|
+
stem = path.stem.casefold()
|
|
1089
|
+
if stem not in paths_by_stem:
|
|
1090
|
+
continue
|
|
1091
|
+
try:
|
|
1092
|
+
rel_path = path.resolve().relative_to(workspace_root).as_posix()
|
|
1093
|
+
except (OSError, ValueError):
|
|
1094
|
+
continue
|
|
1095
|
+
if not is_code_implementation_path(rel_path):
|
|
1096
|
+
continue
|
|
1097
|
+
paths_by_stem[stem].append(rel_path)
|
|
1098
|
+
|
|
1099
|
+
grounded: list[str] = []
|
|
1100
|
+
for candidate in candidates:
|
|
1101
|
+
candidate_paths = _dedupe_keep_order(paths_by_stem.get(candidate, []))
|
|
1102
|
+
if len(candidate_paths) == 1:
|
|
1103
|
+
grounded.extend(candidate_paths)
|
|
1104
|
+
return _dedupe_keep_order(grounded)[:3]
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def _is_concrete_code_implementation_path(path: str) -> bool:
|
|
1108
|
+
return not _has_glob(path) and is_code_implementation_path(path)
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
def _is_code_implementation_path(path: str) -> bool:
|
|
1112
|
+
return is_code_implementation_path(path)
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
def _path_contains_any_task_symbol(
|
|
1116
|
+
*,
|
|
1117
|
+
workspace_root: Path,
|
|
1118
|
+
path: str,
|
|
1119
|
+
task: dict[str, Any],
|
|
1120
|
+
) -> bool:
|
|
1121
|
+
symbols = _task_symbol_candidates(task)
|
|
1122
|
+
if not symbols:
|
|
1123
|
+
return False
|
|
1124
|
+
candidate = workspace_root / path
|
|
1125
|
+
text = _file_text(candidate)
|
|
1126
|
+
if not text:
|
|
1127
|
+
return False
|
|
1128
|
+
return any(re.search(rf"\b{re.escape(symbol)}\b", text) for symbol in symbols)
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
def _apply_symbol_grounded_paths(
|
|
1132
|
+
*,
|
|
1133
|
+
current_paths: list[str],
|
|
1134
|
+
symbol_paths: list[str],
|
|
1135
|
+
workspace_root: Path,
|
|
1136
|
+
task: dict[str, Any],
|
|
1137
|
+
field_name: str,
|
|
1138
|
+
protected_missing_path_identities: set[str] | None = None,
|
|
1139
|
+
) -> tuple[list[str], str | None]:
|
|
1140
|
+
normalized_current = _dedupe_keep_order(current_paths)
|
|
1141
|
+
normalized_symbol_paths = _dedupe_keep_order(symbol_paths)
|
|
1142
|
+
if not normalized_symbol_paths:
|
|
1143
|
+
return normalized_current, None
|
|
1144
|
+
if any(path in normalized_current for path in normalized_symbol_paths):
|
|
1145
|
+
return normalized_current, None
|
|
1146
|
+
if all(
|
|
1147
|
+
any(
|
|
1148
|
+
scope_path_matches_pattern(symbol_path, current_path, root=workspace_root)
|
|
1149
|
+
for current_path in normalized_current
|
|
1150
|
+
)
|
|
1151
|
+
for symbol_path in normalized_symbol_paths
|
|
1152
|
+
):
|
|
1153
|
+
return normalized_current, None
|
|
1154
|
+
|
|
1155
|
+
implementation_paths = [
|
|
1156
|
+
path for path in normalized_current if _is_concrete_code_implementation_path(path)
|
|
1157
|
+
]
|
|
1158
|
+
protected_identities = protected_missing_path_identities or set()
|
|
1159
|
+
protected_missing_paths = [
|
|
1160
|
+
path for path in implementation_paths if _path_identity_key(path) in protected_identities
|
|
1161
|
+
]
|
|
1162
|
+
if protected_missing_paths:
|
|
1163
|
+
return normalized_current, (
|
|
1164
|
+
f"kept explicit missing {field_name} path(s) and did not retarget them to "
|
|
1165
|
+
"repository symbol definition path(s): "
|
|
1166
|
+
+ ", ".join(protected_missing_paths)
|
|
1167
|
+
+ " (candidate: "
|
|
1168
|
+
+ ", ".join(normalized_symbol_paths)
|
|
1169
|
+
+ ")"
|
|
1170
|
+
)
|
|
1171
|
+
if implementation_paths and any(
|
|
1172
|
+
_path_contains_any_task_symbol(
|
|
1173
|
+
workspace_root=workspace_root,
|
|
1174
|
+
path=path,
|
|
1175
|
+
task=task,
|
|
1176
|
+
)
|
|
1177
|
+
for path in implementation_paths
|
|
1178
|
+
):
|
|
1179
|
+
return normalized_current, None
|
|
1180
|
+
|
|
1181
|
+
if implementation_paths:
|
|
1182
|
+
replacement: list[str] = []
|
|
1183
|
+
inserted_symbols = False
|
|
1184
|
+
for path in normalized_current:
|
|
1185
|
+
if _is_concrete_code_implementation_path(path):
|
|
1186
|
+
if not inserted_symbols:
|
|
1187
|
+
replacement.extend(normalized_symbol_paths)
|
|
1188
|
+
inserted_symbols = True
|
|
1189
|
+
continue
|
|
1190
|
+
replacement.append(path)
|
|
1191
|
+
replacement = _dedupe_keep_order(replacement)
|
|
1192
|
+
return replacement, (
|
|
1193
|
+
f"replaced symbol-mismatched {field_name} entries with repository symbol "
|
|
1194
|
+
"definition path(s): "
|
|
1195
|
+
+ ", ".join(implementation_paths)
|
|
1196
|
+
+ " -> "
|
|
1197
|
+
+ ", ".join(normalized_symbol_paths)
|
|
1198
|
+
)
|
|
1199
|
+
|
|
1200
|
+
replacement = _dedupe_keep_order([*normalized_current, *normalized_symbol_paths])
|
|
1201
|
+
return replacement, (
|
|
1202
|
+
f"added repository symbol definition path(s) to {field_name}: "
|
|
1203
|
+
+ ", ".join(normalized_symbol_paths)
|
|
1204
|
+
)
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
def _drop_ungrounded_suspicious_paths(
|
|
1208
|
+
*,
|
|
1209
|
+
paths: list[str],
|
|
1210
|
+
workspace_root: Path,
|
|
1211
|
+
known_paths: set[str],
|
|
1212
|
+
allowed_missing_paths: set[str] | None = None,
|
|
1213
|
+
) -> tuple[list[str], list[str]]:
|
|
1214
|
+
allowed_missing = {path for path in (allowed_missing_paths or set()) if path}
|
|
1215
|
+
if not allowed_missing:
|
|
1216
|
+
return _dedupe_keep_order(paths), []
|
|
1217
|
+
|
|
1218
|
+
kept: list[str] = []
|
|
1219
|
+
dropped: list[str] = []
|
|
1220
|
+
for path in _dedupe_keep_order(paths):
|
|
1221
|
+
if path in allowed_missing:
|
|
1222
|
+
kept.append(path)
|
|
1223
|
+
continue
|
|
1224
|
+
if _is_suspicious_path(path=path, workspace_root=workspace_root, known_paths=known_paths):
|
|
1225
|
+
dropped.append(path)
|
|
1226
|
+
continue
|
|
1227
|
+
kept.append(path)
|
|
1228
|
+
return kept, dropped
|