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,348 @@
|
|
|
1
|
+
"""Swarm-layer write-scope path guard (defense in depth).
|
|
2
|
+
|
|
3
|
+
The agent layer already guards writes (deny prefixes + allow globs inside
|
|
4
|
+
``agent/tools_assembly.py``), but from the swarm's perspective that is an
|
|
5
|
+
honor system: a bug there would let a worker write outside its task's
|
|
6
|
+
``write_scope`` and silently poison sibling tasks or the merge. This module
|
|
7
|
+
is the swarm-owned second gate, applied structurally at the tool dispatch
|
|
8
|
+
boundary (every ``ToolDef.run`` is wrapped), so it holds even when the
|
|
9
|
+
agent-layer checks are buggy or bypassed.
|
|
10
|
+
|
|
11
|
+
Policy:
|
|
12
|
+
- WRITES must stay inside the task's normalized ``write_scope`` patterns
|
|
13
|
+
(plus the run-scoped scratch/temp roots); READS are never guarded.
|
|
14
|
+
- Violations are recorded as typed :class:`WriteScopeViolation` records and
|
|
15
|
+
surface as a tool error to the model; the swarm worker fails the task
|
|
16
|
+
closed when any violation was recorded, regardless of the agent exit code.
|
|
17
|
+
- Hardening: raw ``..`` traversal is rejected before resolution, containment
|
|
18
|
+
is re-verified with ``Path.resolve()`` (realpath) independently of the
|
|
19
|
+
tool's own resolution, writes through symlinked components are rejected
|
|
20
|
+
(a symlink inside the worktree silently rewrites which relative path gets
|
|
21
|
+
scope-checked), and matching is case-insensitive on win32-style
|
|
22
|
+
case-insensitive filesystems.
|
|
23
|
+
|
|
24
|
+
Out of scope (documented, unchanged): ``shell_run``/``shell_background``
|
|
25
|
+
cannot be path-guarded at dispatch ? the sandbox profile and the post-run
|
|
26
|
+
``assess_scope_changes`` pass remain the controls for shell writes.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import fnmatch
|
|
32
|
+
import sys
|
|
33
|
+
import threading
|
|
34
|
+
from collections.abc import Callable, Iterator, Sequence
|
|
35
|
+
from dataclasses import dataclass
|
|
36
|
+
from pathlib import Path, PurePosixPath
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
from .agent.errors import AgentRuntimeError
|
|
40
|
+
from .diff_paths import iter_patch_paths
|
|
41
|
+
from .task_scope import (
|
|
42
|
+
ancestor_directory_scope_patterns,
|
|
43
|
+
is_non_material_untracked_path,
|
|
44
|
+
scope_path_matches_pattern,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Tool name -> ((path_field, base_field), ...) write targets. fs_copy guards
|
|
48
|
+
# only the destination and fs_move guards both ends, mirroring the agent
|
|
49
|
+
# layer's contract.
|
|
50
|
+
_WRITE_TOOL_PATH_FIELDS: dict[str, tuple[tuple[str, str], ...]] = {
|
|
51
|
+
"fs_write": (("path", "path_base"),),
|
|
52
|
+
"fs_edit": (("path", "path_base"),),
|
|
53
|
+
"fs_mkdir": (("path", "path_base"),),
|
|
54
|
+
"fs_delete": (("path", "path_base"),),
|
|
55
|
+
"fs_move": (
|
|
56
|
+
("source_path", "source_path_base"),
|
|
57
|
+
("destination_path", "destination_path_base"),
|
|
58
|
+
),
|
|
59
|
+
"fs_copy": (("destination_path", "destination_path_base"),),
|
|
60
|
+
}
|
|
61
|
+
_PATCH_TOOL_NAMES = frozenset({"git_apply_patch"})
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SwarmWriteScopeViolationError(AgentRuntimeError):
|
|
65
|
+
"""Raised at the tool dispatch boundary for an out-of-scope write.
|
|
66
|
+
|
|
67
|
+
Subclasses ``AgentRuntimeError`` so the turn engine degrades it to a
|
|
68
|
+
structured tool error the model can react to; the recorded violation
|
|
69
|
+
still fails the task at the swarm layer afterwards.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True, slots=True)
|
|
74
|
+
class WriteScopeViolation:
|
|
75
|
+
tool_name: str
|
|
76
|
+
raw_path: str
|
|
77
|
+
reason: str
|
|
78
|
+
detail: str
|
|
79
|
+
resolved_relpath: str | None = None
|
|
80
|
+
|
|
81
|
+
def to_payload(self) -> dict[str, Any]:
|
|
82
|
+
return {
|
|
83
|
+
"kind": "write_scope_guard_violation",
|
|
84
|
+
"tool": self.tool_name,
|
|
85
|
+
"path": self.raw_path,
|
|
86
|
+
"resolved_relpath": self.resolved_relpath,
|
|
87
|
+
"reason": self.reason,
|
|
88
|
+
"detail": self.detail,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _normalize_slashes(value: str) -> str:
|
|
93
|
+
return value.replace("\\", "/").strip()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class SwarmWriteScopeGuard:
|
|
97
|
+
"""Per-task hard write guard keyed by write_scope + run-scoped temp roots."""
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
*,
|
|
102
|
+
worktree_root: Path,
|
|
103
|
+
allowed_patterns: Sequence[str],
|
|
104
|
+
extra_allowed_roots: Sequence[Path] = (),
|
|
105
|
+
case_insensitive: bool | None = None,
|
|
106
|
+
) -> None:
|
|
107
|
+
self.worktree_root = Path(worktree_root).resolve()
|
|
108
|
+
self.allowed_patterns = tuple(
|
|
109
|
+
str(item).strip() for item in allowed_patterns if str(item).strip()
|
|
110
|
+
)
|
|
111
|
+
self.extra_allowed_roots = tuple(Path(item).resolve() for item in extra_allowed_roots)
|
|
112
|
+
self.case_insensitive = (
|
|
113
|
+
sys.platform == "win32" if case_insensitive is None else bool(case_insensitive)
|
|
114
|
+
)
|
|
115
|
+
self._ancestor_dirs_cf = {
|
|
116
|
+
_normalize_slashes(path).casefold()
|
|
117
|
+
for path in ancestor_directory_scope_patterns(
|
|
118
|
+
list(self.allowed_patterns), root=self.worktree_root
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
self._lock = threading.Lock()
|
|
122
|
+
self._violations: list[WriteScopeViolation] = []
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def violations(self) -> list[WriteScopeViolation]:
|
|
126
|
+
with self._lock:
|
|
127
|
+
return list(self._violations)
|
|
128
|
+
|
|
129
|
+
def check_tool_call(
|
|
130
|
+
self,
|
|
131
|
+
tool_name: str,
|
|
132
|
+
arguments: dict[str, Any],
|
|
133
|
+
*,
|
|
134
|
+
resolve_rel_path: Callable[..., str] | None = None,
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Veto an out-of-scope write before the tool runs.
|
|
137
|
+
|
|
138
|
+
``resolve_rel_path`` is the live tool-layer resolver (active-workdir
|
|
139
|
+
aware); when provided it determines what the tool would actually
|
|
140
|
+
write, and the guard independently re-verifies containment on top.
|
|
141
|
+
"""
|
|
142
|
+
for raw_path, resolved_rel in self._iter_write_targets(
|
|
143
|
+
tool_name, arguments, resolve_rel_path=resolve_rel_path
|
|
144
|
+
):
|
|
145
|
+
violation = self._check_target(
|
|
146
|
+
tool_name=tool_name,
|
|
147
|
+
raw_path=raw_path,
|
|
148
|
+
resolved_rel=resolved_rel,
|
|
149
|
+
)
|
|
150
|
+
if violation is not None:
|
|
151
|
+
with self._lock:
|
|
152
|
+
self._violations.append(violation)
|
|
153
|
+
raise SwarmWriteScopeViolationError(
|
|
154
|
+
"Write blocked by the swarm write-scope guard "
|
|
155
|
+
f"({violation.reason}): {violation.raw_path}. {violation.detail}"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def _iter_write_targets(
|
|
159
|
+
self,
|
|
160
|
+
tool_name: str,
|
|
161
|
+
arguments: dict[str, Any],
|
|
162
|
+
*,
|
|
163
|
+
resolve_rel_path: Callable[..., str] | None,
|
|
164
|
+
) -> Iterator[tuple[str, str | None]]:
|
|
165
|
+
if tool_name in _PATCH_TOOL_NAMES:
|
|
166
|
+
patch_text = str(arguments.get("patch") or "")
|
|
167
|
+
for path in iter_patch_paths(patch_text):
|
|
168
|
+
yield path, None
|
|
169
|
+
return
|
|
170
|
+
fields = _WRITE_TOOL_PATH_FIELDS.get(tool_name)
|
|
171
|
+
if fields is None:
|
|
172
|
+
return
|
|
173
|
+
for path_field, base_field in fields:
|
|
174
|
+
raw_path = arguments.get(path_field)
|
|
175
|
+
if raw_path is None or not str(raw_path).strip():
|
|
176
|
+
continue
|
|
177
|
+
resolved_rel: str | None = None
|
|
178
|
+
if resolve_rel_path is not None:
|
|
179
|
+
try:
|
|
180
|
+
resolved_rel = resolve_rel_path(
|
|
181
|
+
raw_path=raw_path,
|
|
182
|
+
raw_base=arguments.get(base_field),
|
|
183
|
+
field_name=path_field,
|
|
184
|
+
base_field_name=base_field,
|
|
185
|
+
)
|
|
186
|
+
except AgentRuntimeError as exc:
|
|
187
|
+
violation = WriteScopeViolation(
|
|
188
|
+
tool_name=tool_name,
|
|
189
|
+
raw_path=str(raw_path),
|
|
190
|
+
reason="path_escape",
|
|
191
|
+
detail=f"tool path resolution rejected the target: {exc}",
|
|
192
|
+
)
|
|
193
|
+
with self._lock:
|
|
194
|
+
self._violations.append(violation)
|
|
195
|
+
raise SwarmWriteScopeViolationError(
|
|
196
|
+
"Write blocked by the swarm write-scope guard "
|
|
197
|
+
f"(path_escape): {raw_path}. {exc}"
|
|
198
|
+
) from exc
|
|
199
|
+
yield str(raw_path), resolved_rel
|
|
200
|
+
|
|
201
|
+
def _check_target(
|
|
202
|
+
self,
|
|
203
|
+
*,
|
|
204
|
+
tool_name: str,
|
|
205
|
+
raw_path: str,
|
|
206
|
+
resolved_rel: str | None,
|
|
207
|
+
) -> WriteScopeViolation | None:
|
|
208
|
+
raw_text = _normalize_slashes(str(raw_path))
|
|
209
|
+
if "\x00" in raw_text:
|
|
210
|
+
return WriteScopeViolation(
|
|
211
|
+
tool_name=tool_name,
|
|
212
|
+
raw_path=str(raw_path),
|
|
213
|
+
reason="invalid_path",
|
|
214
|
+
detail="write target contains a NUL byte.",
|
|
215
|
+
)
|
|
216
|
+
if any(part == ".." for part in PurePosixPath(raw_text).parts):
|
|
217
|
+
return WriteScopeViolation(
|
|
218
|
+
tool_name=tool_name,
|
|
219
|
+
raw_path=str(raw_path),
|
|
220
|
+
reason="parent_traversal",
|
|
221
|
+
detail="write targets must not contain '..' components.",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
raw_as_path = Path(raw_text)
|
|
225
|
+
if raw_as_path.is_absolute():
|
|
226
|
+
resolved_abs = raw_as_path.resolve()
|
|
227
|
+
if self._is_under_extra_allowed_root(resolved_abs):
|
|
228
|
+
return None
|
|
229
|
+
rel = self._relative_to_root(resolved_abs)
|
|
230
|
+
if rel is None:
|
|
231
|
+
return WriteScopeViolation(
|
|
232
|
+
tool_name=tool_name,
|
|
233
|
+
raw_path=str(raw_path),
|
|
234
|
+
reason="path_escape",
|
|
235
|
+
detail="absolute write target resolves outside the task worktree.",
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
rel = _normalize_slashes(resolved_rel) if resolved_rel else raw_text
|
|
239
|
+
while rel.startswith("./"):
|
|
240
|
+
rel = rel[2:]
|
|
241
|
+
|
|
242
|
+
candidate = self.worktree_root / rel
|
|
243
|
+
symlink_component = self._first_symlink_component(rel)
|
|
244
|
+
if symlink_component is not None:
|
|
245
|
+
return WriteScopeViolation(
|
|
246
|
+
tool_name=tool_name,
|
|
247
|
+
raw_path=str(raw_path),
|
|
248
|
+
resolved_relpath=rel,
|
|
249
|
+
reason="symlink_component",
|
|
250
|
+
detail=(
|
|
251
|
+
"write path passes through a symlink "
|
|
252
|
+
f"({symlink_component}); symlinked write targets are rejected."
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
resolved_candidate = candidate.resolve()
|
|
256
|
+
if self._is_under_extra_allowed_root(resolved_candidate):
|
|
257
|
+
return None
|
|
258
|
+
if self._relative_to_root(resolved_candidate) is None:
|
|
259
|
+
return WriteScopeViolation(
|
|
260
|
+
tool_name=tool_name,
|
|
261
|
+
raw_path=str(raw_path),
|
|
262
|
+
resolved_relpath=rel,
|
|
263
|
+
reason="path_escape",
|
|
264
|
+
detail="write target resolves outside the task worktree.",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if self._rel_path_in_scope(rel, tool_name=tool_name):
|
|
268
|
+
return None
|
|
269
|
+
scope_preview = ", ".join(self.allowed_patterns[:8]) or "(empty write scope)"
|
|
270
|
+
return WriteScopeViolation(
|
|
271
|
+
tool_name=tool_name,
|
|
272
|
+
raw_path=str(raw_path),
|
|
273
|
+
resolved_relpath=rel,
|
|
274
|
+
reason="out_of_scope",
|
|
275
|
+
detail=f"write target is outside the task write scope: {scope_preview}.",
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
def _relative_to_root(self, path: Path) -> str | None:
|
|
279
|
+
try:
|
|
280
|
+
return path.relative_to(self.worktree_root).as_posix()
|
|
281
|
+
except ValueError:
|
|
282
|
+
if not self.case_insensitive:
|
|
283
|
+
return None
|
|
284
|
+
path_cf = str(path).replace("\\", "/").casefold()
|
|
285
|
+
root_cf = str(self.worktree_root).replace("\\", "/").casefold().rstrip("/")
|
|
286
|
+
if path_cf == root_cf:
|
|
287
|
+
return "."
|
|
288
|
+
if path_cf.startswith(root_cf + "/"):
|
|
289
|
+
return str(path).replace("\\", "/")[len(root_cf) + 1 :]
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
def _is_under_extra_allowed_root(self, resolved: Path) -> bool:
|
|
293
|
+
for allowed_root in self.extra_allowed_roots:
|
|
294
|
+
try:
|
|
295
|
+
resolved.relative_to(allowed_root)
|
|
296
|
+
return True
|
|
297
|
+
except ValueError:
|
|
298
|
+
continue
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
def _first_symlink_component(self, rel: str) -> str | None:
|
|
302
|
+
current = self.worktree_root
|
|
303
|
+
for part in PurePosixPath(rel).parts:
|
|
304
|
+
current = current / part
|
|
305
|
+
try:
|
|
306
|
+
if current.is_symlink():
|
|
307
|
+
return current.relative_to(self.worktree_root).as_posix()
|
|
308
|
+
except OSError:
|
|
309
|
+
return None
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
def _rel_path_in_scope(self, rel: str, *, tool_name: str) -> bool:
|
|
313
|
+
rel_clean = _normalize_slashes(rel).rstrip("/")
|
|
314
|
+
if not rel_clean:
|
|
315
|
+
return False
|
|
316
|
+
if tool_name == "fs_delete" and is_non_material_untracked_path(rel_clean):
|
|
317
|
+
# Mirror the agent layer's carve-out: deleting non-material
|
|
318
|
+
# untracked scratch output outside scope stays allowed.
|
|
319
|
+
return True
|
|
320
|
+
rel_cf = rel_clean.casefold()
|
|
321
|
+
if tool_name == "fs_mkdir" and rel_cf in self._ancestor_dirs_cf:
|
|
322
|
+
return True
|
|
323
|
+
for pattern in self.allowed_patterns:
|
|
324
|
+
if scope_path_matches_pattern(rel_clean, pattern, root=self.worktree_root):
|
|
325
|
+
return True
|
|
326
|
+
if not self.case_insensitive:
|
|
327
|
+
return False
|
|
328
|
+
for pattern in self.allowed_patterns:
|
|
329
|
+
if self._matches_casefolded(rel_cf, pattern):
|
|
330
|
+
return True
|
|
331
|
+
return False
|
|
332
|
+
|
|
333
|
+
def _matches_casefolded(self, rel_cf: str, pattern: str) -> bool:
|
|
334
|
+
pattern_cf = _normalize_slashes(pattern).rstrip("/").casefold()
|
|
335
|
+
if not pattern_cf:
|
|
336
|
+
return False
|
|
337
|
+
if any(ch in pattern_cf for ch in "*?["):
|
|
338
|
+
variants = [pattern_cf]
|
|
339
|
+
if "/**/" in pattern_cf:
|
|
340
|
+
variants.append(pattern_cf.replace("/**/", "/"))
|
|
341
|
+
return any(fnmatch.fnmatchcase(rel_cf, variant) for variant in variants)
|
|
342
|
+
if rel_cf == pattern_cf:
|
|
343
|
+
return True
|
|
344
|
+
try:
|
|
345
|
+
is_dir_scope = (self.worktree_root / pattern).is_dir()
|
|
346
|
+
except OSError:
|
|
347
|
+
is_dir_scope = False
|
|
348
|
+
return is_dir_scope and rel_cf.startswith(pattern_cf + "/")
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .task_readiness import is_clearly_non_mutating_task, normalized_text_list
|
|
8
|
+
|
|
9
|
+
_ORDERING_SIGNAL_RE = re.compile(
|
|
10
|
+
r"\b(?:after|based\s+on|depend(?:s|ed|ing)?\s+on|follow[- ]?up|following|"
|
|
11
|
+
r"next\s+(?:task|step|change|item|phase|work)|once|previous|prior|subsequent|then)\b",
|
|
12
|
+
re.IGNORECASE,
|
|
13
|
+
)
|
|
14
|
+
_EXPLICIT_PREVIOUS_RE = re.compile(
|
|
15
|
+
r"\b(?:after\s+that|after\s+the\s+previous|after\s+the\s+prior|follow[- ]?up|"
|
|
16
|
+
r"following\s+that|next\s+(?:task|step|change|item|phase|work)|"
|
|
17
|
+
r"previous\s+task|prior\s+task|then)\b",
|
|
18
|
+
re.IGNORECASE,
|
|
19
|
+
)
|
|
20
|
+
_WORD_RE = re.compile(r"[a-z0-9][a-z0-9_-]*", re.IGNORECASE)
|
|
21
|
+
_NON_DEPENDENCY_STATUSES = frozenset({"cancelled", "canceled", "done", "skipped", "superseded"})
|
|
22
|
+
_TOKEN_STOPWORDS = frozenset(
|
|
23
|
+
{
|
|
24
|
+
"add",
|
|
25
|
+
"after",
|
|
26
|
+
"and",
|
|
27
|
+
"app",
|
|
28
|
+
"change",
|
|
29
|
+
"create",
|
|
30
|
+
"docs",
|
|
31
|
+
"file",
|
|
32
|
+
"fix",
|
|
33
|
+
"for",
|
|
34
|
+
"from",
|
|
35
|
+
"implementation",
|
|
36
|
+
"make",
|
|
37
|
+
"modify",
|
|
38
|
+
"new",
|
|
39
|
+
"src",
|
|
40
|
+
"task",
|
|
41
|
+
"test",
|
|
42
|
+
"tests",
|
|
43
|
+
"the",
|
|
44
|
+
"then",
|
|
45
|
+
"this",
|
|
46
|
+
"update",
|
|
47
|
+
"with",
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class InferredTaskDependency:
|
|
54
|
+
task_id: str
|
|
55
|
+
depends_on: str
|
|
56
|
+
reason: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def infer_ordered_predecessor_dependency(
|
|
60
|
+
*,
|
|
61
|
+
tasks: list[dict[str, Any]],
|
|
62
|
+
task: dict[str, Any],
|
|
63
|
+
) -> InferredTaskDependency | None:
|
|
64
|
+
task_id = str(task.get("id") or "").strip()
|
|
65
|
+
if not task_id:
|
|
66
|
+
return None
|
|
67
|
+
if _task_is_read_only(task):
|
|
68
|
+
return None
|
|
69
|
+
deps = _task_dependencies(task)
|
|
70
|
+
if deps:
|
|
71
|
+
return None
|
|
72
|
+
text = _task_text(task)
|
|
73
|
+
if not text or not _ORDERING_SIGNAL_RE.search(text):
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
previous_tasks: list[dict[str, Any]] = []
|
|
77
|
+
for candidate in tasks:
|
|
78
|
+
if candidate is task or str(candidate.get("id") or "").strip() == task_id:
|
|
79
|
+
break
|
|
80
|
+
previous_tasks.append(candidate)
|
|
81
|
+
|
|
82
|
+
current_tokens = _dependency_tokens(text)
|
|
83
|
+
explicit_previous = bool(_EXPLICIT_PREVIOUS_RE.search(text))
|
|
84
|
+
for previous in reversed(previous_tasks):
|
|
85
|
+
previous_id = str(previous.get("id") or "").strip()
|
|
86
|
+
if not previous_id or previous_id in deps:
|
|
87
|
+
continue
|
|
88
|
+
if _canonical_status(str(previous.get("status") or "")) in _NON_DEPENDENCY_STATUSES:
|
|
89
|
+
continue
|
|
90
|
+
if _task_is_read_only(previous) and not explicit_previous:
|
|
91
|
+
continue
|
|
92
|
+
previous_tokens = _dependency_tokens(_task_text(previous))
|
|
93
|
+
if explicit_previous or current_tokens & previous_tokens:
|
|
94
|
+
return InferredTaskDependency(
|
|
95
|
+
task_id=task_id,
|
|
96
|
+
depends_on=previous_id,
|
|
97
|
+
reason="ordered predecessor cue",
|
|
98
|
+
)
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def apply_ordered_dependency_inference(
|
|
103
|
+
*,
|
|
104
|
+
tasks: list[dict[str, Any]],
|
|
105
|
+
touched_task_ids: set[str],
|
|
106
|
+
) -> tuple[bool, list[InferredTaskDependency]]:
|
|
107
|
+
changed = False
|
|
108
|
+
inferred: list[InferredTaskDependency] = []
|
|
109
|
+
for task in tasks:
|
|
110
|
+
if not isinstance(task, dict):
|
|
111
|
+
continue
|
|
112
|
+
task_id = str(task.get("id") or "").strip()
|
|
113
|
+
if task_id not in touched_task_ids:
|
|
114
|
+
continue
|
|
115
|
+
if _canonical_status(str(task.get("status") or "")) != "planned":
|
|
116
|
+
continue
|
|
117
|
+
dependency = infer_ordered_predecessor_dependency(tasks=tasks, task=task)
|
|
118
|
+
if dependency is None:
|
|
119
|
+
continue
|
|
120
|
+
deps = _task_dependencies(task)
|
|
121
|
+
if dependency.depends_on in deps:
|
|
122
|
+
continue
|
|
123
|
+
task["dependencies"] = [*deps, dependency.depends_on]
|
|
124
|
+
inferred.append(dependency)
|
|
125
|
+
changed = True
|
|
126
|
+
return changed, inferred
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _task_dependencies(task: dict[str, Any]) -> list[str]:
|
|
130
|
+
deps = task.get("dependencies") or []
|
|
131
|
+
if not isinstance(deps, list):
|
|
132
|
+
return []
|
|
133
|
+
return [str(dep).strip() for dep in deps if str(dep).strip()]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _task_is_read_only(task: dict[str, Any]) -> bool:
|
|
137
|
+
return is_clearly_non_mutating_task(
|
|
138
|
+
title=str(task.get("title") or "").strip(),
|
|
139
|
+
description=str(task.get("description") or "").strip(),
|
|
140
|
+
acceptance_criteria=normalized_text_list(task.get("acceptance_criteria")),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _task_text(task: dict[str, Any]) -> str:
|
|
145
|
+
parts = [
|
|
146
|
+
str(task.get("title") or ""),
|
|
147
|
+
str(task.get("description") or ""),
|
|
148
|
+
*normalized_text_list(task.get("acceptance_criteria")),
|
|
149
|
+
*normalized_text_list(task.get("estimated_files")),
|
|
150
|
+
*normalized_text_list(task.get("write_scope")),
|
|
151
|
+
]
|
|
152
|
+
return "\n".join(parts)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _dependency_tokens(text: str) -> set[str]:
|
|
156
|
+
normalized = re.sub(r"[./\\_-]+", " ", str(text or "").casefold())
|
|
157
|
+
tokens: set[str] = set()
|
|
158
|
+
for raw in _WORD_RE.findall(normalized):
|
|
159
|
+
token = raw.casefold()
|
|
160
|
+
if len(token) <= 2 or token in _TOKEN_STOPWORDS:
|
|
161
|
+
continue
|
|
162
|
+
tokens.add(token)
|
|
163
|
+
return tokens
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _canonical_status(status: str) -> str:
|
|
167
|
+
value = str(status or "").strip().lower()
|
|
168
|
+
if value == "todo":
|
|
169
|
+
return "planned"
|
|
170
|
+
return value or "planned"
|