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
alysis_code/run_state.py
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"""The lifecycle state of a workspace's current Forge run.
|
|
2
|
+
|
|
3
|
+
``.alysis/current_run.json`` used to be a pure pointer: which run directory is
|
|
4
|
+
current, and how the workspace was bound. That left every surface guessing at the
|
|
5
|
+
one thing users actually ask -- *is this run going, finished, or dead?* -- and a
|
|
6
|
+
crashed process left behind a pointer indistinguishable from a healthy one, so
|
|
7
|
+
UIs showed a phantom "active" run forever.
|
|
8
|
+
|
|
9
|
+
This module owns the answer. The pointer now carries an explicit ``status`` drawn
|
|
10
|
+
from a closed enum:
|
|
11
|
+
|
|
12
|
+
``draft``
|
|
13
|
+
A run exists and its plan is not execution-ready yet.
|
|
14
|
+
``approved``
|
|
15
|
+
The plan passes the same acceptance gate ``forge exec`` applies. Executable.
|
|
16
|
+
``running``
|
|
17
|
+
A process is executing the plan right now, and says which process.
|
|
18
|
+
``interrupted``
|
|
19
|
+
Execution stopped without reaching a terminal state. Resumable.
|
|
20
|
+
``completed`` / ``failed``
|
|
21
|
+
Terminal. Execution finished; the work was, or was not, accepted.
|
|
22
|
+
|
|
23
|
+
``interrupted`` is the state that makes crash recovery honest: nothing writes it
|
|
24
|
+
during a normal run, so it can only be reached by
|
|
25
|
+
:func:`reconcile_status_after_crash` observing ``running`` with no live lock.
|
|
26
|
+
|
|
27
|
+
The module is pure -- dict in, dict out, no filesystem and no imports from
|
|
28
|
+
:mod:`alysis_code.forge` -- so both the pointer writer and the lock layer
|
|
29
|
+
can depend on it without a cycle.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import hashlib
|
|
35
|
+
import json
|
|
36
|
+
from collections.abc import Mapping, Sequence
|
|
37
|
+
from dataclasses import dataclass
|
|
38
|
+
from datetime import UTC, datetime
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
RUN_STATE_SCHEMA_VERSION = 1
|
|
42
|
+
|
|
43
|
+
RUN_STATUS_DRAFT = "draft"
|
|
44
|
+
RUN_STATUS_APPROVED = "approved"
|
|
45
|
+
RUN_STATUS_RUNNING = "running"
|
|
46
|
+
RUN_STATUS_INTERRUPTED = "interrupted"
|
|
47
|
+
RUN_STATUS_COMPLETED = "completed"
|
|
48
|
+
RUN_STATUS_FAILED = "failed"
|
|
49
|
+
|
|
50
|
+
RUN_STATUSES: tuple[str, ...] = (
|
|
51
|
+
RUN_STATUS_DRAFT,
|
|
52
|
+
RUN_STATUS_APPROVED,
|
|
53
|
+
RUN_STATUS_RUNNING,
|
|
54
|
+
RUN_STATUS_INTERRUPTED,
|
|
55
|
+
RUN_STATUS_COMPLETED,
|
|
56
|
+
RUN_STATUS_FAILED,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Statuses whose work is unfinished and can be picked back up by `forge resume`.
|
|
60
|
+
# ``failed`` is included on purpose: a run that stopped on a rejected task is
|
|
61
|
+
# exactly the run a retry flag exists for.
|
|
62
|
+
RESUMABLE_RUN_STATUSES: frozenset[str] = frozenset({RUN_STATUS_INTERRUPTED, RUN_STATUS_FAILED})
|
|
63
|
+
TERMINAL_RUN_STATUSES: frozenset[str] = frozenset({RUN_STATUS_COMPLETED, RUN_STATUS_FAILED})
|
|
64
|
+
|
|
65
|
+
STATUS_KEY = "status"
|
|
66
|
+
STATUS_UPDATED_AT_KEY = "status_updated_at"
|
|
67
|
+
STATUS_REASON_KEY = "status_reason"
|
|
68
|
+
STATUS_HISTORY_KEY = "status_history"
|
|
69
|
+
RUN_OWNER_KEY = "run_owner"
|
|
70
|
+
PLAN_FINGERPRINT_KEY = "plan_fingerprint"
|
|
71
|
+
|
|
72
|
+
# Bounded so a workspace that starts and stops a run a thousand times does not grow
|
|
73
|
+
# an unbounded pointer file. The tail is what diagnoses a crash; the head is noise.
|
|
74
|
+
STATUS_HISTORY_LIMIT = 24
|
|
75
|
+
|
|
76
|
+
PLAN_FINGERPRINT_SCHEMA_VERSION = 1
|
|
77
|
+
|
|
78
|
+
# The plan's *authored definition*. Deliberately excludes everything execution is
|
|
79
|
+
# allowed to move on its own -- ``status``, ``attempts``, ``branch`` -- so a run's
|
|
80
|
+
# own progress can never read as someone having edited the plan underneath it.
|
|
81
|
+
_TASK_DEFINITION_KEYS: tuple[str, ...] = (
|
|
82
|
+
"title",
|
|
83
|
+
"description",
|
|
84
|
+
"acceptance_criteria",
|
|
85
|
+
"dependencies",
|
|
86
|
+
"estimated_files",
|
|
87
|
+
"write_scope",
|
|
88
|
+
"task_kind",
|
|
89
|
+
"mcp_scope",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
_TASK_FIELD_LABELS: dict[str, str] = {
|
|
93
|
+
"title": "title",
|
|
94
|
+
"description": "description",
|
|
95
|
+
"acceptance_criteria": "acceptance criteria",
|
|
96
|
+
"dependencies": "dependencies",
|
|
97
|
+
"estimated_files": "estimated files",
|
|
98
|
+
"write_scope": "write scope",
|
|
99
|
+
"task_kind": "task kind",
|
|
100
|
+
"mcp_scope": "MCP scope",
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _now_iso() -> str:
|
|
105
|
+
return datetime.now(UTC).replace(microsecond=0).isoformat()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def normalize_run_status(value: Any) -> str | None:
|
|
109
|
+
"""Return the enum member ``value`` names, or ``None`` if it names none."""
|
|
110
|
+
text = str(value or "").strip().lower()
|
|
111
|
+
return text if text in RUN_STATUSES else None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def pointer_status(pointer: Mapping[str, Any] | None) -> str:
|
|
115
|
+
"""The recorded status, defaulting to ``draft``.
|
|
116
|
+
|
|
117
|
+
Pointers written before this schema carry no status at all. ``draft`` is the
|
|
118
|
+
honest default for them: it is the only value that claims nothing about work
|
|
119
|
+
this build never observed.
|
|
120
|
+
"""
|
|
121
|
+
if pointer is None:
|
|
122
|
+
return RUN_STATUS_DRAFT
|
|
123
|
+
return normalize_run_status(pointer.get(STATUS_KEY)) or RUN_STATUS_DRAFT
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def status_is_resumable(status: str) -> bool:
|
|
127
|
+
return (normalize_run_status(status) or "") in RESUMABLE_RUN_STATUSES
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def status_is_terminal(status: str) -> bool:
|
|
131
|
+
return (normalize_run_status(status) or "") in TERMINAL_RUN_STATUSES
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def describe_run_status(status: str) -> str:
|
|
135
|
+
normalized = normalize_run_status(status) or RUN_STATUS_DRAFT
|
|
136
|
+
return {
|
|
137
|
+
RUN_STATUS_DRAFT: "plan is not execution-ready yet",
|
|
138
|
+
RUN_STATUS_APPROVED: "plan is execution-ready and not started",
|
|
139
|
+
RUN_STATUS_RUNNING: "a process is executing this plan",
|
|
140
|
+
RUN_STATUS_INTERRUPTED: "execution stopped without finishing",
|
|
141
|
+
RUN_STATUS_COMPLETED: "execution finished and the work was accepted",
|
|
142
|
+
RUN_STATUS_FAILED: "execution finished and the work was not accepted",
|
|
143
|
+
}[normalized]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def build_run_owner(
|
|
147
|
+
*,
|
|
148
|
+
pid: int,
|
|
149
|
+
hostname: str,
|
|
150
|
+
mode: str,
|
|
151
|
+
started_at: str | None = None,
|
|
152
|
+
session_id: str | None = None,
|
|
153
|
+
) -> dict[str, Any]:
|
|
154
|
+
"""The identity a ``running`` pointer publishes about its executing process."""
|
|
155
|
+
return {
|
|
156
|
+
"pid": int(pid),
|
|
157
|
+
"hostname": str(hostname),
|
|
158
|
+
"mode": str(mode),
|
|
159
|
+
"started_at": started_at or _now_iso(),
|
|
160
|
+
"session_id": str(session_id) if session_id else None,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def apply_run_status(
|
|
165
|
+
pointer: Mapping[str, Any] | None,
|
|
166
|
+
status: str,
|
|
167
|
+
*,
|
|
168
|
+
reason: str = "",
|
|
169
|
+
owner: Mapping[str, Any] | None = None,
|
|
170
|
+
plan_fingerprint: Mapping[str, Any] | None = None,
|
|
171
|
+
at: str | None = None,
|
|
172
|
+
) -> dict[str, Any]:
|
|
173
|
+
"""Return ``pointer`` with ``status`` applied and the transition recorded.
|
|
174
|
+
|
|
175
|
+
Pure: the caller decides whether the result is worth writing. ``owner`` is
|
|
176
|
+
cleared on any status other than ``running`` -- a stale owner block on a
|
|
177
|
+
finished run is the same phantom-active problem in a different field.
|
|
178
|
+
"""
|
|
179
|
+
normalized = normalize_run_status(status)
|
|
180
|
+
if normalized is None:
|
|
181
|
+
raise ValueError(
|
|
182
|
+
f"unknown run status {status!r}; expected one of {', '.join(RUN_STATUSES)}"
|
|
183
|
+
)
|
|
184
|
+
stamp = at or _now_iso()
|
|
185
|
+
updated: dict[str, Any] = dict(pointer or {})
|
|
186
|
+
previous = pointer_status(pointer) if pointer else None
|
|
187
|
+
|
|
188
|
+
updated[STATUS_KEY] = normalized
|
|
189
|
+
updated[STATUS_UPDATED_AT_KEY] = stamp
|
|
190
|
+
if reason:
|
|
191
|
+
updated[STATUS_REASON_KEY] = reason
|
|
192
|
+
else:
|
|
193
|
+
updated.pop(STATUS_REASON_KEY, None)
|
|
194
|
+
|
|
195
|
+
if normalized == RUN_STATUS_RUNNING:
|
|
196
|
+
if owner is not None:
|
|
197
|
+
updated[RUN_OWNER_KEY] = dict(owner)
|
|
198
|
+
else:
|
|
199
|
+
updated.pop(RUN_OWNER_KEY, None)
|
|
200
|
+
|
|
201
|
+
if plan_fingerprint is not None:
|
|
202
|
+
updated[PLAN_FINGERPRINT_KEY] = dict(plan_fingerprint)
|
|
203
|
+
|
|
204
|
+
history = list(updated.get(STATUS_HISTORY_KEY) or [])
|
|
205
|
+
entry: dict[str, Any] = {"status": normalized, "at": stamp}
|
|
206
|
+
if previous and previous != normalized:
|
|
207
|
+
entry["from"] = previous
|
|
208
|
+
if reason:
|
|
209
|
+
entry["reason"] = reason
|
|
210
|
+
history.append(entry)
|
|
211
|
+
updated[STATUS_HISTORY_KEY] = history[-STATUS_HISTORY_LIMIT:]
|
|
212
|
+
return updated
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def reconcile_status_after_crash(
|
|
216
|
+
pointer: Mapping[str, Any] | None,
|
|
217
|
+
*,
|
|
218
|
+
lock_is_live: bool,
|
|
219
|
+
at: str | None = None,
|
|
220
|
+
) -> dict[str, Any] | None:
|
|
221
|
+
"""Transition a crash leftover to ``interrupted``, or return ``None``.
|
|
222
|
+
|
|
223
|
+
``running`` with no live lock is the crash signature: the executing process
|
|
224
|
+
holds the workspace lock for its whole run and releases it on the way out, so a
|
|
225
|
+
pointer claiming ``running`` while nothing holds the lock describes a process
|
|
226
|
+
that is not there. Every other combination is left exactly as it is -- this
|
|
227
|
+
function's only job is to stop UIs from showing a run that nobody is running.
|
|
228
|
+
"""
|
|
229
|
+
if pointer is None:
|
|
230
|
+
return None
|
|
231
|
+
if pointer_status(pointer) != RUN_STATUS_RUNNING:
|
|
232
|
+
return None
|
|
233
|
+
if lock_is_live:
|
|
234
|
+
return None
|
|
235
|
+
owner = pointer.get(RUN_OWNER_KEY)
|
|
236
|
+
detail = ""
|
|
237
|
+
if isinstance(owner, Mapping):
|
|
238
|
+
pid = owner.get("pid")
|
|
239
|
+
host = owner.get("hostname")
|
|
240
|
+
if pid and host:
|
|
241
|
+
detail = f" (owner pid {pid} on {host} is gone)"
|
|
242
|
+
return apply_run_status(
|
|
243
|
+
pointer,
|
|
244
|
+
RUN_STATUS_INTERRUPTED,
|
|
245
|
+
reason=f"run was marked running but no execution holds the workspace lock{detail}",
|
|
246
|
+
at=at,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
# Plan fingerprints
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _digest(payload: Any) -> str:
|
|
256
|
+
canonical = json.dumps(payload, sort_keys=True, ensure_ascii=False, separators=(",", ":"))
|
|
257
|
+
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:32]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _task_id(task: Mapping[str, Any]) -> str:
|
|
261
|
+
return str(task.get("id") or "").strip()
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _plan_tasks(plan: Mapping[str, Any]) -> list[Mapping[str, Any]]:
|
|
265
|
+
tasks = plan.get("tasks")
|
|
266
|
+
if not isinstance(tasks, Sequence):
|
|
267
|
+
return []
|
|
268
|
+
return [task for task in tasks if isinstance(task, Mapping) and _task_id(task)]
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _requirement_texts(plan: Mapping[str, Any]) -> list[str]:
|
|
272
|
+
raw = plan.get("requirements")
|
|
273
|
+
if not isinstance(raw, Sequence):
|
|
274
|
+
return []
|
|
275
|
+
texts: list[str] = []
|
|
276
|
+
for item in raw:
|
|
277
|
+
if isinstance(item, Mapping):
|
|
278
|
+
for key in ("text", "requirement", "title", "description", "content"):
|
|
279
|
+
value = item.get(key)
|
|
280
|
+
if isinstance(value, str) and value.strip():
|
|
281
|
+
texts.append(value.strip())
|
|
282
|
+
break
|
|
283
|
+
elif str(item).strip():
|
|
284
|
+
texts.append(str(item).strip())
|
|
285
|
+
return texts
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def plan_fingerprint(plan: Mapping[str, Any]) -> dict[str, Any]:
|
|
289
|
+
"""A comparable digest of everything about a plan that a human authored.
|
|
290
|
+
|
|
291
|
+
Field-level rather than whole-file, so drift can be reported as *what* changed
|
|
292
|
+
("T02 acceptance criteria changed") instead of the useless "the plan changed".
|
|
293
|
+
"""
|
|
294
|
+
task_digests = {
|
|
295
|
+
_task_id(task): {
|
|
296
|
+
key: _digest(task.get(key)) for key in _TASK_DEFINITION_KEYS if key in task
|
|
297
|
+
}
|
|
298
|
+
for task in _plan_tasks(plan)
|
|
299
|
+
}
|
|
300
|
+
payload = {
|
|
301
|
+
"schema_version": PLAN_FINGERPRINT_SCHEMA_VERSION,
|
|
302
|
+
"project_goal": _digest(str(plan.get("project_goal") or "").strip()),
|
|
303
|
+
"summary": _digest(str(plan.get("summary") or "").strip()),
|
|
304
|
+
"requirements": _digest(_requirement_texts(plan)),
|
|
305
|
+
"task_order": [_task_id(task) for task in _plan_tasks(plan)],
|
|
306
|
+
"tasks": task_digests,
|
|
307
|
+
}
|
|
308
|
+
payload["digest"] = _digest(
|
|
309
|
+
{key: value for key, value in payload.items() if key != "schema_version"}
|
|
310
|
+
)
|
|
311
|
+
return payload
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@dataclass(frozen=True)
|
|
315
|
+
class PlanDrift:
|
|
316
|
+
"""What changed between the plan a run was approved against and today's plan."""
|
|
317
|
+
|
|
318
|
+
changed: bool
|
|
319
|
+
goal_changed: bool = False
|
|
320
|
+
summary_changed: bool = False
|
|
321
|
+
requirements_changed: bool = False
|
|
322
|
+
order_changed: bool = False
|
|
323
|
+
tasks_added: tuple[str, ...] = ()
|
|
324
|
+
tasks_removed: tuple[str, ...] = ()
|
|
325
|
+
tasks_changed: tuple[tuple[str, tuple[str, ...]], ...] = ()
|
|
326
|
+
comparable: bool = True
|
|
327
|
+
|
|
328
|
+
@property
|
|
329
|
+
def reasons(self) -> tuple[str, ...]:
|
|
330
|
+
"""One human line per distinct change, for the re-approval prompt."""
|
|
331
|
+
if not self.comparable:
|
|
332
|
+
return ("this run has no recorded plan fingerprint to compare against",)
|
|
333
|
+
lines: list[str] = []
|
|
334
|
+
if self.goal_changed:
|
|
335
|
+
lines.append("project goal changed")
|
|
336
|
+
if self.summary_changed:
|
|
337
|
+
lines.append("plan summary changed")
|
|
338
|
+
if self.requirements_changed:
|
|
339
|
+
lines.append("requirements changed")
|
|
340
|
+
if self.tasks_added:
|
|
341
|
+
lines.append(f"tasks added: {', '.join(self.tasks_added)}")
|
|
342
|
+
if self.tasks_removed:
|
|
343
|
+
lines.append(f"tasks removed: {', '.join(self.tasks_removed)}")
|
|
344
|
+
for task_id, fields in self.tasks_changed:
|
|
345
|
+
labels = ", ".join(_TASK_FIELD_LABELS.get(name, name) for name in fields)
|
|
346
|
+
lines.append(f"{task_id} changed: {labels}")
|
|
347
|
+
if self.order_changed and not (self.tasks_added or self.tasks_removed):
|
|
348
|
+
lines.append("task order changed")
|
|
349
|
+
return tuple(lines)
|
|
350
|
+
|
|
351
|
+
def to_json(self) -> dict[str, Any]:
|
|
352
|
+
return {
|
|
353
|
+
"changed": self.changed,
|
|
354
|
+
"comparable": self.comparable,
|
|
355
|
+
"goal_changed": self.goal_changed,
|
|
356
|
+
"summary_changed": self.summary_changed,
|
|
357
|
+
"requirements_changed": self.requirements_changed,
|
|
358
|
+
"order_changed": self.order_changed,
|
|
359
|
+
"tasks_added": list(self.tasks_added),
|
|
360
|
+
"tasks_removed": list(self.tasks_removed),
|
|
361
|
+
"tasks_changed": [
|
|
362
|
+
{"task_id": task_id, "fields": list(fields)}
|
|
363
|
+
for task_id, fields in self.tasks_changed
|
|
364
|
+
],
|
|
365
|
+
"reasons": list(self.reasons),
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def compare_plan_fingerprints(
|
|
370
|
+
recorded: Mapping[str, Any] | None,
|
|
371
|
+
current: Mapping[str, Any],
|
|
372
|
+
) -> PlanDrift:
|
|
373
|
+
"""Diff a stored fingerprint against a freshly computed one.
|
|
374
|
+
|
|
375
|
+
A missing or unreadable stored fingerprint is reported as *not comparable*
|
|
376
|
+
rather than as "no drift": a run whose baseline was never recorded has not been
|
|
377
|
+
shown to be unchanged, and resume must not claim otherwise.
|
|
378
|
+
"""
|
|
379
|
+
if not isinstance(recorded, Mapping) or not recorded.get("digest"):
|
|
380
|
+
return PlanDrift(changed=True, comparable=False)
|
|
381
|
+
if recorded.get("digest") == current.get("digest"):
|
|
382
|
+
return PlanDrift(changed=False)
|
|
383
|
+
|
|
384
|
+
recorded_tasks = recorded.get("tasks")
|
|
385
|
+
current_tasks = current.get("tasks")
|
|
386
|
+
recorded_tasks = recorded_tasks if isinstance(recorded_tasks, Mapping) else {}
|
|
387
|
+
current_tasks = current_tasks if isinstance(current_tasks, Mapping) else {}
|
|
388
|
+
|
|
389
|
+
added = tuple(sorted(set(current_tasks) - set(recorded_tasks)))
|
|
390
|
+
removed = tuple(sorted(set(recorded_tasks) - set(current_tasks)))
|
|
391
|
+
|
|
392
|
+
changed: list[tuple[str, tuple[str, ...]]] = []
|
|
393
|
+
for task_id in sorted(set(recorded_tasks) & set(current_tasks)):
|
|
394
|
+
before = recorded_tasks.get(task_id)
|
|
395
|
+
after = current_tasks.get(task_id)
|
|
396
|
+
if not isinstance(before, Mapping) or not isinstance(after, Mapping):
|
|
397
|
+
continue
|
|
398
|
+
fields = tuple(
|
|
399
|
+
sorted(key for key in set(before) | set(after) if before.get(key) != after.get(key))
|
|
400
|
+
)
|
|
401
|
+
if fields:
|
|
402
|
+
changed.append((task_id, fields))
|
|
403
|
+
|
|
404
|
+
recorded_order = list(recorded.get("task_order") or [])
|
|
405
|
+
current_order = list(current.get("task_order") or [])
|
|
406
|
+
return PlanDrift(
|
|
407
|
+
changed=True,
|
|
408
|
+
goal_changed=recorded.get("project_goal") != current.get("project_goal"),
|
|
409
|
+
summary_changed=recorded.get("summary") != current.get("summary"),
|
|
410
|
+
requirements_changed=recorded.get("requirements") != current.get("requirements"),
|
|
411
|
+
order_changed=recorded_order != current_order,
|
|
412
|
+
tasks_added=added,
|
|
413
|
+
tasks_removed=removed,
|
|
414
|
+
tasks_changed=tuple(changed),
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
__all__ = [
|
|
419
|
+
"PLAN_FINGERPRINT_KEY",
|
|
420
|
+
"PLAN_FINGERPRINT_SCHEMA_VERSION",
|
|
421
|
+
"RESUMABLE_RUN_STATUSES",
|
|
422
|
+
"RUN_OWNER_KEY",
|
|
423
|
+
"RUN_STATE_SCHEMA_VERSION",
|
|
424
|
+
"RUN_STATUSES",
|
|
425
|
+
"RUN_STATUS_APPROVED",
|
|
426
|
+
"RUN_STATUS_COMPLETED",
|
|
427
|
+
"RUN_STATUS_DRAFT",
|
|
428
|
+
"RUN_STATUS_FAILED",
|
|
429
|
+
"RUN_STATUS_INTERRUPTED",
|
|
430
|
+
"RUN_STATUS_RUNNING",
|
|
431
|
+
"STATUS_HISTORY_KEY",
|
|
432
|
+
"STATUS_KEY",
|
|
433
|
+
"TERMINAL_RUN_STATUSES",
|
|
434
|
+
"PlanDrift",
|
|
435
|
+
"apply_run_status",
|
|
436
|
+
"build_run_owner",
|
|
437
|
+
"compare_plan_fingerprints",
|
|
438
|
+
"describe_run_status",
|
|
439
|
+
"normalize_run_status",
|
|
440
|
+
"plan_fingerprint",
|
|
441
|
+
"pointer_status",
|
|
442
|
+
"reconcile_status_after_crash",
|
|
443
|
+
"status_is_resumable",
|
|
444
|
+
"status_is_terminal",
|
|
445
|
+
]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path, PurePosixPath
|
|
5
|
+
|
|
6
|
+
ROOT_RUNTIME_ARTIFACT_DIR_NAMES = frozenset(
|
|
7
|
+
{".alysis", ".alysis_images", ".git", "alysis-feedback"}
|
|
8
|
+
)
|
|
9
|
+
RUNTIME_CACHE_DIR_NAMES = frozenset(
|
|
10
|
+
{
|
|
11
|
+
"__pycache__",
|
|
12
|
+
".mypy_cache",
|
|
13
|
+
".pytest_cache",
|
|
14
|
+
".ruff_cache",
|
|
15
|
+
}
|
|
16
|
+
)
|
|
17
|
+
RUST_RUNTIME_ARTIFACT_DIR_NAME = "target"
|
|
18
|
+
RUST_MANIFEST_FILENAME = "Cargo.toml"
|
|
19
|
+
RUNTIME_ARTIFACT_FILE_NAMES = frozenset({".coverage"})
|
|
20
|
+
RUNTIME_ARTIFACT_FILE_SUFFIXES = frozenset({".pyc", ".pyo"})
|
|
21
|
+
RUNTIME_ARTIFACT_DIR_NAMES = ROOT_RUNTIME_ARTIFACT_DIR_NAMES | RUNTIME_CACHE_DIR_NAMES
|
|
22
|
+
RUNTIME_ARTIFACT_GIT_EXCLUDE_ENTRIES = (
|
|
23
|
+
"/.alysis/",
|
|
24
|
+
"/.alysis_images/",
|
|
25
|
+
"/alysis-feedback/",
|
|
26
|
+
"__pycache__/",
|
|
27
|
+
".mypy_cache/",
|
|
28
|
+
".pytest_cache/",
|
|
29
|
+
".ruff_cache/",
|
|
30
|
+
".coverage",
|
|
31
|
+
"*.pyc",
|
|
32
|
+
"*.pyo",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def normalize_runtime_artifact_path(value: str) -> str:
|
|
37
|
+
cleaned = str(value).strip().replace("\\", "/")
|
|
38
|
+
while cleaned.startswith("./"):
|
|
39
|
+
cleaned = cleaned[2:]
|
|
40
|
+
return cleaned
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _is_rust_target_runtime_artifact(
|
|
44
|
+
*,
|
|
45
|
+
parts: tuple[str, ...],
|
|
46
|
+
root: Path | None,
|
|
47
|
+
) -> bool:
|
|
48
|
+
if not parts:
|
|
49
|
+
return False
|
|
50
|
+
if root is None:
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
root_resolved = root.resolve()
|
|
54
|
+
if parts[0] == RUST_RUNTIME_ARTIFACT_DIR_NAME:
|
|
55
|
+
return (root_resolved / RUST_MANIFEST_FILENAME).is_file()
|
|
56
|
+
for index, segment in enumerate(parts[:-1]):
|
|
57
|
+
if segment != RUST_RUNTIME_ARTIFACT_DIR_NAME:
|
|
58
|
+
continue
|
|
59
|
+
target_parent = root_resolved.joinpath(*parts[:index])
|
|
60
|
+
if (target_parent / RUST_MANIFEST_FILENAME).is_file():
|
|
61
|
+
return True
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _iter_rust_manifest_dirs(root: Path) -> tuple[Path, ...]:
|
|
66
|
+
root_resolved = root.resolve()
|
|
67
|
+
manifest_dirs: set[Path] = set()
|
|
68
|
+
for current_root, dirnames, filenames in os.walk(root_resolved):
|
|
69
|
+
dirnames[:] = [
|
|
70
|
+
dirname
|
|
71
|
+
for dirname in dirnames
|
|
72
|
+
if dirname not in ROOT_RUNTIME_ARTIFACT_DIR_NAMES
|
|
73
|
+
and dirname not in RUNTIME_CACHE_DIR_NAMES
|
|
74
|
+
and dirname != RUST_RUNTIME_ARTIFACT_DIR_NAME
|
|
75
|
+
]
|
|
76
|
+
if RUST_MANIFEST_FILENAME not in filenames:
|
|
77
|
+
continue
|
|
78
|
+
manifest_dirs.add(Path(current_root))
|
|
79
|
+
return tuple(sorted(manifest_dirs))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def runtime_artifact_git_exclude_entries(root: Path) -> tuple[str, ...]:
|
|
83
|
+
root_resolved = root.resolve()
|
|
84
|
+
rust_target_entries: list[str] = []
|
|
85
|
+
for manifest_dir in _iter_rust_manifest_dirs(root_resolved):
|
|
86
|
+
relative_dir = manifest_dir.relative_to(root_resolved).as_posix()
|
|
87
|
+
if relative_dir in {"", "."}:
|
|
88
|
+
rust_target_entries.append("/target/")
|
|
89
|
+
else:
|
|
90
|
+
rust_target_entries.append(f"/{relative_dir}/target/")
|
|
91
|
+
return (*RUNTIME_ARTIFACT_GIT_EXCLUDE_ENTRIES, *rust_target_entries)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def has_grounded_rust_target_runtime_artifacts(root: Path) -> bool:
|
|
95
|
+
return bool(_iter_rust_manifest_dirs(root))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def is_runtime_artifact_path(path: str, *, root: Path | None = None) -> bool:
|
|
99
|
+
cleaned = normalize_runtime_artifact_path(path)
|
|
100
|
+
if not cleaned:
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
parts = tuple(segment for segment in PurePosixPath(cleaned).parts if segment not in {"", "."})
|
|
104
|
+
if not parts:
|
|
105
|
+
return False
|
|
106
|
+
if parts[0] in ROOT_RUNTIME_ARTIFACT_DIR_NAMES:
|
|
107
|
+
return True
|
|
108
|
+
if any(segment in RUNTIME_CACHE_DIR_NAMES for segment in parts):
|
|
109
|
+
return True
|
|
110
|
+
if _is_rust_target_runtime_artifact(parts=parts, root=root):
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
filename = parts[-1]
|
|
114
|
+
if filename in RUNTIME_ARTIFACT_FILE_NAMES:
|
|
115
|
+
return True
|
|
116
|
+
return any(filename.endswith(suffix) for suffix in RUNTIME_ARTIFACT_FILE_SUFFIXES)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .compaction.settings import CompactionSettings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class RuntimeContextFeatures:
|
|
10
|
+
requested_enable_compaction: bool
|
|
11
|
+
requested_tool_output_offload: bool
|
|
12
|
+
requested_conversation_summarization: bool
|
|
13
|
+
tool_output_offload_enabled: bool
|
|
14
|
+
conversation_summarization_enabled: bool
|
|
15
|
+
logging_enabled: bool
|
|
16
|
+
explicit_session_artifact_root: bool
|
|
17
|
+
tool_output_offload_artifact_persistence_available: bool
|
|
18
|
+
settings_enabled: bool
|
|
19
|
+
settings_offload_tool_outputs: bool
|
|
20
|
+
settings_summarize_conversation: bool
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def any_enabled(self) -> bool:
|
|
24
|
+
return self.tool_output_offload_enabled or self.conversation_summarization_enabled
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def resolve_runtime_context_features(
|
|
28
|
+
*,
|
|
29
|
+
settings: CompactionSettings,
|
|
30
|
+
enable_compaction: bool,
|
|
31
|
+
enable_tool_output_offload: bool | None = None,
|
|
32
|
+
enable_conversation_summarization: bool | None = None,
|
|
33
|
+
logging_enabled: bool = True,
|
|
34
|
+
explicit_session_artifact_root: bool = False,
|
|
35
|
+
) -> RuntimeContextFeatures:
|
|
36
|
+
requested_enable_compaction = bool(enable_compaction)
|
|
37
|
+
requested_tool_output_offload = (
|
|
38
|
+
requested_enable_compaction
|
|
39
|
+
if enable_tool_output_offload is None
|
|
40
|
+
else bool(enable_tool_output_offload)
|
|
41
|
+
)
|
|
42
|
+
requested_conversation_summarization = (
|
|
43
|
+
requested_enable_compaction
|
|
44
|
+
if enable_conversation_summarization is None
|
|
45
|
+
else bool(enable_conversation_summarization)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
settings_enabled = bool(settings.enabled)
|
|
49
|
+
settings_offload_tool_outputs = bool(settings.offload_tool_outputs)
|
|
50
|
+
settings_summarize_conversation = bool(settings.summarize_conversation)
|
|
51
|
+
tool_output_offload_artifact_persistence_available = bool(
|
|
52
|
+
logging_enabled or explicit_session_artifact_root
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return RuntimeContextFeatures(
|
|
56
|
+
requested_enable_compaction=requested_enable_compaction,
|
|
57
|
+
requested_tool_output_offload=requested_tool_output_offload,
|
|
58
|
+
requested_conversation_summarization=requested_conversation_summarization,
|
|
59
|
+
tool_output_offload_enabled=(
|
|
60
|
+
requested_tool_output_offload
|
|
61
|
+
and settings_enabled
|
|
62
|
+
and settings_offload_tool_outputs
|
|
63
|
+
and tool_output_offload_artifact_persistence_available
|
|
64
|
+
),
|
|
65
|
+
conversation_summarization_enabled=(
|
|
66
|
+
requested_conversation_summarization
|
|
67
|
+
and settings_enabled
|
|
68
|
+
and settings_summarize_conversation
|
|
69
|
+
),
|
|
70
|
+
logging_enabled=bool(logging_enabled),
|
|
71
|
+
explicit_session_artifact_root=bool(explicit_session_artifact_root),
|
|
72
|
+
tool_output_offload_artifact_persistence_available=(
|
|
73
|
+
tool_output_offload_artifact_persistence_available
|
|
74
|
+
),
|
|
75
|
+
settings_enabled=settings_enabled,
|
|
76
|
+
settings_offload_tool_outputs=settings_offload_tool_outputs,
|
|
77
|
+
settings_summarize_conversation=settings_summarize_conversation,
|
|
78
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
from .config import ConfigError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RuntimeKind(StrEnum):
|
|
9
|
+
INTERACTIVE_CHAT = "interactive_chat"
|
|
10
|
+
ONE_SHOT = "one_shot"
|
|
11
|
+
FORGE_EXEC = "forge_exec"
|
|
12
|
+
SWARM_WORKER = "swarm_worker"
|
|
13
|
+
SUBAGENT = "subagent"
|
|
14
|
+
CONFLICT_AUTO_RESOLVE = "conflict_auto_resolve"
|
|
15
|
+
|
|
16
|
+
def __str__(self) -> str:
|
|
17
|
+
return self.value
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def runtime_kind_values() -> tuple[str, ...]:
|
|
21
|
+
return tuple(kind.value for kind in RuntimeKind)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def normalize_runtime_kind(
|
|
25
|
+
value: RuntimeKind | str | None,
|
|
26
|
+
*,
|
|
27
|
+
fallback: RuntimeKind | None = None,
|
|
28
|
+
) -> RuntimeKind:
|
|
29
|
+
if isinstance(value, RuntimeKind):
|
|
30
|
+
return value
|
|
31
|
+
if value is None:
|
|
32
|
+
if fallback is None:
|
|
33
|
+
raise ConfigError("Missing runtime kind.")
|
|
34
|
+
return fallback
|
|
35
|
+
normalized = str(value).strip().lower()
|
|
36
|
+
for kind in RuntimeKind:
|
|
37
|
+
if kind.value == normalized:
|
|
38
|
+
return kind
|
|
39
|
+
allowed = ", ".join(runtime_kind_values())
|
|
40
|
+
raise ConfigError(f"Invalid runtime kind: {value!r}. Expected one of: {allowed}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def resolve_session_runtime_kind(
|
|
44
|
+
*,
|
|
45
|
+
runtime_kind: RuntimeKind | str | None,
|
|
46
|
+
one_shot_execution: bool,
|
|
47
|
+
subagent_depth: int,
|
|
48
|
+
) -> RuntimeKind:
|
|
49
|
+
fallback = RuntimeKind.ONE_SHOT if one_shot_execution else RuntimeKind.INTERACTIVE_CHAT
|
|
50
|
+
if subagent_depth > 0:
|
|
51
|
+
fallback = RuntimeKind.SUBAGENT
|
|
52
|
+
return normalize_runtime_kind(runtime_kind, fallback=fallback)
|