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,652 @@
|
|
|
1
|
+
"""Validate-and-repair policy for planner payloads.
|
|
2
|
+
|
|
3
|
+
The planner is asked for one JSON object per turn. When the model returns
|
|
4
|
+
something the schema rejects, the honest first move is to tell the model exactly
|
|
5
|
+
what was wrong and ask again -- not to quietly reshape the payload host-side and
|
|
6
|
+
pretend the model produced it. This module holds the facts that bound that
|
|
7
|
+
retry loop, plus the vocabulary for reporting what happened when the loop ends.
|
|
8
|
+
|
|
9
|
+
It is pure: no LLM calls, no filesystem, no clock. Everything here is a function
|
|
10
|
+
of a plan dict, a payload dict, or a config object.
|
|
11
|
+
|
|
12
|
+
* ``resolve_plan_repair_policy`` -- attempt caps and the kill-switch.
|
|
13
|
+
* ``repair_retry_instruction`` -- the escalating-strictness follow-up text. Each
|
|
14
|
+
attempt restates the same schema demand in blunter terms and drops more of the
|
|
15
|
+
model's latitude, because a model that ignored the polite version has already
|
|
16
|
+
demonstrated the polite version does not work on it.
|
|
17
|
+
* ``host_repaired_field_paths`` -- which fields host-side repair had to change,
|
|
18
|
+
so a salvaged payload is labelled rather than passed off as the model's own.
|
|
19
|
+
* ``execution_readiness_errors`` -- the R1-R5 acceptance rules rendered as
|
|
20
|
+
re-prompt material, so an unexecutable plan is a retry trigger and not a
|
|
21
|
+
surprise at ``forge exec`` time.
|
|
22
|
+
* ``ClarificationLoopTracker`` -- consecutive clarification rounds for one goal.
|
|
23
|
+
Past the cap the caller forces a concrete draft instead of asking forever.
|
|
24
|
+
* ``assess_plan_status`` -- draft vs execution_ready, decided by the *same* rule
|
|
25
|
+
the execution gate uses, so a plan that will be blocked later says so now.
|
|
26
|
+
|
|
27
|
+
Nothing here inspects prose or vocabulary: every decision is driven by payload
|
|
28
|
+
shape, plan structure, and the acceptance rules already in
|
|
29
|
+
:mod:`alysis_code.plan_validation`.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from collections.abc import Mapping, Sequence
|
|
35
|
+
from dataclasses import dataclass, field
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
from .branding import env_get
|
|
39
|
+
from .plan_validation import PlanAcceptanceIssue, find_plan_acceptance_issues
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Policy (kill-switch mirrors the empty-response-stall / evidence-v2 idiom)
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
DEFAULT_MAX_PAYLOAD_ATTEMPTS = 3
|
|
46
|
+
DEFAULT_MAX_CLARIFICATION_ROUNDS = 2
|
|
47
|
+
|
|
48
|
+
# Hard ceilings. A misconfigured value must not turn one planner turn into an
|
|
49
|
+
# unbounded spend; the loop is a repair mechanism, not a search strategy.
|
|
50
|
+
MAX_PAYLOAD_ATTEMPTS_CEILING = 6
|
|
51
|
+
MAX_CLARIFICATION_ROUNDS_CEILING = 6
|
|
52
|
+
|
|
53
|
+
PLAN_STATUS_DRAFT = "draft"
|
|
54
|
+
PLAN_STATUS_EXECUTION_READY = "execution_ready"
|
|
55
|
+
|
|
56
|
+
PLAN_STATUS_KEY = "plan_status"
|
|
57
|
+
PLAN_STATUS_DETAIL_KEY = "plan_status_detail"
|
|
58
|
+
PLAN_REPAIR_KEY = "plan_repair"
|
|
59
|
+
|
|
60
|
+
# Terminal states of one planner turn. Exactly one of these is always reached.
|
|
61
|
+
TERMINAL_VALIDATED = "validated"
|
|
62
|
+
TERMINAL_HOST_REPAIRED = "host_repaired"
|
|
63
|
+
TERMINAL_FORCED_DRAFT = "forced_draft"
|
|
64
|
+
TERMINAL_FAILED = "failed"
|
|
65
|
+
|
|
66
|
+
_TRUTHY = {"on", "1", "true", "yes", "enabled"}
|
|
67
|
+
_FALSEY = {"off", "0", "false", "no", "disabled"}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class PlanRepairPolicy:
|
|
72
|
+
"""Bounds for one planner turn's validate-and-repair loop."""
|
|
73
|
+
|
|
74
|
+
enabled: bool = True
|
|
75
|
+
max_payload_attempts: int = DEFAULT_MAX_PAYLOAD_ATTEMPTS
|
|
76
|
+
max_clarification_rounds: int = DEFAULT_MAX_CLARIFICATION_ROUNDS
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def max_repair_retries(self) -> int:
|
|
80
|
+
"""Follow-up requests allowed after the first response."""
|
|
81
|
+
return max(self.max_payload_attempts - 1, 0)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def plan_repair_enabled(cfg: Any | None) -> bool:
|
|
85
|
+
"""Kill-switch for the general validate-and-repair loop.
|
|
86
|
+
|
|
87
|
+
``ALYSIS_PLAN_REPAIR`` wins over config; default is on. When off the
|
|
88
|
+
policy collapses to the legacy shape: one schema retry, host-side repair
|
|
89
|
+
applied eagerly, no execution-readiness retry, no clarification cap.
|
|
90
|
+
"""
|
|
91
|
+
flag = _env_flag("ALYSIS_PLAN_REPAIR")
|
|
92
|
+
if flag is not None:
|
|
93
|
+
return flag
|
|
94
|
+
return bool(getattr(cfg, "plan_repair_enabled", True))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def resolve_plan_repair_policy(cfg: Any | None) -> PlanRepairPolicy:
|
|
98
|
+
"""Resolve attempt caps from env, then config, then the defaults."""
|
|
99
|
+
enabled = plan_repair_enabled(cfg)
|
|
100
|
+
attempts = _bounded_int(
|
|
101
|
+
_env_int("ALYSIS_PLAN_REPAIR_ATTEMPTS"),
|
|
102
|
+
getattr(cfg, "plan_repair_max_attempts", None),
|
|
103
|
+
fallback=DEFAULT_MAX_PAYLOAD_ATTEMPTS,
|
|
104
|
+
ceiling=MAX_PAYLOAD_ATTEMPTS_CEILING,
|
|
105
|
+
)
|
|
106
|
+
clarifications = _bounded_int(
|
|
107
|
+
_env_int("ALYSIS_PLAN_REPAIR_CLARIFICATION_ROUNDS"),
|
|
108
|
+
getattr(cfg, "plan_repair_max_clarification_rounds", None),
|
|
109
|
+
fallback=DEFAULT_MAX_CLARIFICATION_ROUNDS,
|
|
110
|
+
ceiling=MAX_CLARIFICATION_ROUNDS_CEILING,
|
|
111
|
+
)
|
|
112
|
+
if not enabled:
|
|
113
|
+
# Legacy shape: the single retry that predates this module.
|
|
114
|
+
return PlanRepairPolicy(
|
|
115
|
+
enabled=False,
|
|
116
|
+
max_payload_attempts=2,
|
|
117
|
+
max_clarification_rounds=0,
|
|
118
|
+
)
|
|
119
|
+
return PlanRepairPolicy(
|
|
120
|
+
enabled=True,
|
|
121
|
+
max_payload_attempts=attempts,
|
|
122
|
+
max_clarification_rounds=clarifications,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _env_flag(name: str) -> bool | None:
|
|
127
|
+
raw = env_get(name)
|
|
128
|
+
if raw is None:
|
|
129
|
+
return None
|
|
130
|
+
normalized = str(raw).strip().lower()
|
|
131
|
+
if normalized in _FALSEY:
|
|
132
|
+
return False
|
|
133
|
+
if normalized in _TRUTHY:
|
|
134
|
+
return True
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _env_int(name: str) -> int | None:
|
|
139
|
+
raw = env_get(name)
|
|
140
|
+
if raw is None:
|
|
141
|
+
return None
|
|
142
|
+
try:
|
|
143
|
+
return int(str(raw).strip())
|
|
144
|
+
except (TypeError, ValueError):
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _bounded_int(*candidates: Any, fallback: int, ceiling: int) -> int:
|
|
149
|
+
for candidate in candidates:
|
|
150
|
+
if candidate is None:
|
|
151
|
+
continue
|
|
152
|
+
try:
|
|
153
|
+
parsed = int(candidate)
|
|
154
|
+
except (TypeError, ValueError):
|
|
155
|
+
continue
|
|
156
|
+
if parsed < 1:
|
|
157
|
+
continue
|
|
158
|
+
return min(parsed, ceiling)
|
|
159
|
+
return min(max(fallback, 1), ceiling)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
# Escalating-strictness retry text
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
# Rungs for the intermediate follow-ups, in order. The last rung repeats when the
|
|
167
|
+
# attempt cap is raised past the ladder, so a bigger budget never runs out of text.
|
|
168
|
+
_STRICTNESS_LADDER: tuple[str, ...] = (
|
|
169
|
+
(
|
|
170
|
+
"Return the corrected payload as ONE JSON object. Keep every field that was "
|
|
171
|
+
"already correct; change only what the validation errors name."
|
|
172
|
+
),
|
|
173
|
+
(
|
|
174
|
+
"STRICT MODE. The previous correction also failed validation. Output MUST begin "
|
|
175
|
+
"with '{' and end with '}'. No markdown fence, no prose before or after, no "
|
|
176
|
+
"commentary field, no keys outside the documented schema. Every array field must "
|
|
177
|
+
"be a JSON array of strings even when it holds a single value. If you are unsure "
|
|
178
|
+
"about an optional field, omit the key entirely rather than guessing its shape."
|
|
179
|
+
),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Reserved for the last allowed attempt, whatever the cap is: it is the only rung
|
|
183
|
+
# that tells the model what happens when it fails again.
|
|
184
|
+
_FINAL_STRICTNESS = (
|
|
185
|
+
"FINAL ATTEMPT. Emit the smallest payload that validates. Drop every optional key "
|
|
186
|
+
"you are not certain about, keep only 'assistant_message' plus the plan_update "
|
|
187
|
+
"fields you can state exactly, and re-check each value's type against the schema "
|
|
188
|
+
"before answering. After this attempt the host repairs the payload itself and the "
|
|
189
|
+
"resulting plan is recorded as host-repaired rather than model-authored."
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def repair_strictness_instruction(*, attempt: int, max_attempts: int) -> str:
|
|
194
|
+
"""Instruction text for follow-up ``attempt`` (2-based: the first retry is 2)."""
|
|
195
|
+
if int(attempt) >= int(max_attempts):
|
|
196
|
+
return _FINAL_STRICTNESS
|
|
197
|
+
rung = min(max(int(attempt) - 2, 0), len(_STRICTNESS_LADDER) - 1)
|
|
198
|
+
return _STRICTNESS_LADDER[rung]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def repair_retry_instruction(
|
|
202
|
+
*,
|
|
203
|
+
attempt: int,
|
|
204
|
+
max_attempts: int,
|
|
205
|
+
validation_errors: Sequence[str],
|
|
206
|
+
previous_response: str | None = None,
|
|
207
|
+
kind: str = "schema",
|
|
208
|
+
) -> str:
|
|
209
|
+
"""Follow-up block naming the exact errors, the attempt number, and the strictness rung.
|
|
210
|
+
|
|
211
|
+
``previous_response`` is optional so a caller that already quotes the model's
|
|
212
|
+
last output does not quote it twice in one prompt.
|
|
213
|
+
"""
|
|
214
|
+
errors = [str(item).strip() for item in validation_errors if str(item).strip()]
|
|
215
|
+
if kind == "execution_readiness":
|
|
216
|
+
headline = (
|
|
217
|
+
"Your previous planner output parsed, but the plan it produces cannot be "
|
|
218
|
+
"executed. Every listed rule must be satisfied or the plan is rejected at "
|
|
219
|
+
"execution time."
|
|
220
|
+
)
|
|
221
|
+
error_label = "Execution-readiness errors"
|
|
222
|
+
else:
|
|
223
|
+
headline = "Your previous planner output did not match the required schema."
|
|
224
|
+
error_label = "Validation errors"
|
|
225
|
+
rendered_errors = "\n".join(f"- {item}" for item in errors[:10]) or "- (unspecified)"
|
|
226
|
+
if len(errors) > 10:
|
|
227
|
+
rendered_errors += f"\n- (+{len(errors) - 10} more)"
|
|
228
|
+
previous_block = (
|
|
229
|
+
f"Previous response:\n{previous_response}\n\n" if previous_response is not None else ""
|
|
230
|
+
)
|
|
231
|
+
return (
|
|
232
|
+
f"{headline}\n"
|
|
233
|
+
f"Correction attempt {attempt} of {max_attempts}.\n"
|
|
234
|
+
f"{error_label}:\n{rendered_errors}\n\n"
|
|
235
|
+
f"{previous_block}"
|
|
236
|
+
f"{repair_strictness_instruction(attempt=attempt, max_attempts=max_attempts)}\n"
|
|
237
|
+
"Return the full corrected payload, not a diff or a patch. Preserve the latest "
|
|
238
|
+
"user intent, target roots, and decoy/forbidden constraints from the prompt."
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
# Host-side repair accounting
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
# A payload with thousands of leaves is already pathological; the cap keeps one
|
|
247
|
+
# bad response from writing an unbounded field list into the plan.
|
|
248
|
+
_MAX_REPAIRED_FIELD_PATHS = 60
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def host_repaired_field_paths(*, raw: Any, repaired: Any) -> list[str]:
|
|
252
|
+
"""Dotted paths where host-side repair changed the model's payload.
|
|
253
|
+
|
|
254
|
+
Both sides are pre-validation, so shared normalisation does not show up here:
|
|
255
|
+
what remains is exactly what the repairer had to touch.
|
|
256
|
+
"""
|
|
257
|
+
paths: list[str] = []
|
|
258
|
+
_collect_diff_paths(raw, repaired, prefix="", out=paths)
|
|
259
|
+
deduped = list(dict.fromkeys(paths))
|
|
260
|
+
return sorted(deduped[:_MAX_REPAIRED_FIELD_PATHS])
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _collect_diff_paths(left: Any, right: Any, *, prefix: str, out: list[str]) -> None:
|
|
264
|
+
if len(out) >= _MAX_REPAIRED_FIELD_PATHS:
|
|
265
|
+
return
|
|
266
|
+
if isinstance(left, Mapping) and isinstance(right, Mapping):
|
|
267
|
+
for key in sorted({*left.keys(), *right.keys()}, key=str):
|
|
268
|
+
child = f"{prefix}.{key}" if prefix else str(key)
|
|
269
|
+
if key not in left or key not in right:
|
|
270
|
+
out.append(child)
|
|
271
|
+
continue
|
|
272
|
+
_collect_diff_paths(left[key], right[key], prefix=child, out=out)
|
|
273
|
+
return
|
|
274
|
+
if _is_plain_sequence(left) and _is_plain_sequence(right):
|
|
275
|
+
if len(left) != len(right):
|
|
276
|
+
out.append(prefix or "(root)")
|
|
277
|
+
return
|
|
278
|
+
for index, (left_item, right_item) in enumerate(zip(left, right, strict=False)):
|
|
279
|
+
_collect_diff_paths(
|
|
280
|
+
left_item,
|
|
281
|
+
right_item,
|
|
282
|
+
prefix=f"{prefix}[{index}]",
|
|
283
|
+
out=out,
|
|
284
|
+
)
|
|
285
|
+
return
|
|
286
|
+
if left != right:
|
|
287
|
+
out.append(prefix or "(root)")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _is_plain_sequence(value: Any) -> bool:
|
|
291
|
+
return isinstance(value, Sequence) and not isinstance(value, str | bytes)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# ---------------------------------------------------------------------------
|
|
295
|
+
# Execution readiness as a retry trigger
|
|
296
|
+
# ---------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def format_acceptance_issue(issue: PlanAcceptanceIssue) -> str:
|
|
300
|
+
parts = [str(issue.rule_id)]
|
|
301
|
+
if issue.task_id:
|
|
302
|
+
parts.append(f"task={issue.task_id}")
|
|
303
|
+
parts.append(f"observed={issue.observed}")
|
|
304
|
+
if issue.detail:
|
|
305
|
+
parts.append(f"detail={issue.detail}")
|
|
306
|
+
return " ".join(parts)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def execution_readiness_errors(plan: Mapping[str, Any]) -> list[str]:
|
|
310
|
+
"""R1-R5 acceptance failures for ``plan``, rendered for a re-prompt."""
|
|
311
|
+
try:
|
|
312
|
+
issues = find_plan_acceptance_issues(dict(plan))
|
|
313
|
+
except Exception: # noqa: BLE001 - readiness reporting must not break the turn
|
|
314
|
+
return []
|
|
315
|
+
return [format_acceptance_issue(issue) for issue in issues]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def plan_update_proposes_task_work(plan_update: Any) -> bool:
|
|
319
|
+
"""True when the payload asks for task work that execution would have to run.
|
|
320
|
+
|
|
321
|
+
A clarifying turn -- questions, a goal edit, requirement notes -- proposes no
|
|
322
|
+
task work, so holding it to the execution-acceptance rules would burn the
|
|
323
|
+
retry budget on a payload that was never meant to be executable.
|
|
324
|
+
"""
|
|
325
|
+
if not isinstance(plan_update, Mapping):
|
|
326
|
+
return False
|
|
327
|
+
for key in ("tasks_add", "tasks_update"):
|
|
328
|
+
entries = plan_update.get(key)
|
|
329
|
+
if isinstance(entries, Sequence) and not isinstance(entries, str | bytes) and len(entries):
|
|
330
|
+
return True
|
|
331
|
+
return False
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# ---------------------------------------------------------------------------
|
|
335
|
+
# Clarification loop cap
|
|
336
|
+
# ---------------------------------------------------------------------------
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def clarification_goal_key(*, plan: Mapping[str, Any] | None, user_text: str) -> str:
|
|
340
|
+
"""Stable key for "the same goal", so a new goal restarts the count.
|
|
341
|
+
|
|
342
|
+
The plan's own goal is the anchor when it has one; before that exists the
|
|
343
|
+
first message of the streak is all there is to key on.
|
|
344
|
+
"""
|
|
345
|
+
goal = str((plan or {}).get("project_goal") or "").strip()
|
|
346
|
+
if goal:
|
|
347
|
+
return " ".join(goal.split()).casefold()[:200]
|
|
348
|
+
return " ".join(str(user_text or "").split()).casefold()[:200]
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@dataclass
|
|
352
|
+
class ClarificationLoopTracker:
|
|
353
|
+
"""Consecutive clarification-only planner turns for one goal."""
|
|
354
|
+
|
|
355
|
+
max_rounds: int = DEFAULT_MAX_CLARIFICATION_ROUNDS
|
|
356
|
+
goal_key: str = ""
|
|
357
|
+
rounds: int = 0
|
|
358
|
+
|
|
359
|
+
def rounds_for(self, goal_key: str) -> int:
|
|
360
|
+
"""Rounds already recorded for ``goal_key`` (0 when the goal changed)."""
|
|
361
|
+
return self.rounds if goal_key == self.goal_key else 0
|
|
362
|
+
|
|
363
|
+
def record(self, *, goal_key: str, awaiting_clarification: bool) -> int:
|
|
364
|
+
"""Record one planner turn's outcome and return the running round count."""
|
|
365
|
+
if goal_key != self.goal_key:
|
|
366
|
+
self.goal_key = goal_key
|
|
367
|
+
self.rounds = 0
|
|
368
|
+
if not awaiting_clarification:
|
|
369
|
+
self.rounds = 0
|
|
370
|
+
return 0
|
|
371
|
+
self.rounds += 1
|
|
372
|
+
return self.rounds
|
|
373
|
+
|
|
374
|
+
def cap_reached(self, goal_key: str) -> bool:
|
|
375
|
+
if self.max_rounds <= 0:
|
|
376
|
+
return False
|
|
377
|
+
return self.rounds_for(goal_key) >= self.max_rounds
|
|
378
|
+
|
|
379
|
+
def reset(self) -> None:
|
|
380
|
+
self.goal_key = ""
|
|
381
|
+
self.rounds = 0
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def payload_awaits_clarification(validated: Mapping[str, Any] | None) -> bool:
|
|
385
|
+
"""True when the payload asks the user something and proposes no plan work."""
|
|
386
|
+
if not isinstance(validated, Mapping):
|
|
387
|
+
return False
|
|
388
|
+
if validated.get("plan_update"):
|
|
389
|
+
return False
|
|
390
|
+
questions = validated.get("questions")
|
|
391
|
+
if not isinstance(questions, Sequence) or isinstance(questions, str | bytes):
|
|
392
|
+
return False
|
|
393
|
+
return any(str(item).strip() for item in questions)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# ---------------------------------------------------------------------------
|
|
397
|
+
# Turn report
|
|
398
|
+
# ---------------------------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@dataclass(frozen=True)
|
|
402
|
+
class PlannerRepairReport:
|
|
403
|
+
"""What the validate-and-repair loop did during one planner turn."""
|
|
404
|
+
|
|
405
|
+
terminal_state: str = TERMINAL_VALIDATED
|
|
406
|
+
attempts: int = 1
|
|
407
|
+
max_attempts: int = DEFAULT_MAX_PAYLOAD_ATTEMPTS
|
|
408
|
+
schema_retries: int = 0
|
|
409
|
+
readiness_retries: int = 0
|
|
410
|
+
host_repaired: bool = False
|
|
411
|
+
host_repaired_fields: list[str] = field(default_factory=list)
|
|
412
|
+
execution_readiness_errors: list[str] = field(default_factory=list)
|
|
413
|
+
clarification_rounds: int = 0
|
|
414
|
+
forced_draft: bool = False
|
|
415
|
+
validation_errors: list[str] = field(default_factory=list)
|
|
416
|
+
|
|
417
|
+
@property
|
|
418
|
+
def degraded(self) -> bool:
|
|
419
|
+
"""True when the turn did not produce what a clean planner turn would.
|
|
420
|
+
|
|
421
|
+
A host-repaired payload, a forced draft, and a payload that never became
|
|
422
|
+
execution-ready all qualify: each is worth keeping in the plan's history,
|
|
423
|
+
because each explains a plan that is not what the planner was asked for.
|
|
424
|
+
"""
|
|
425
|
+
return bool(self.host_repaired or self.forced_draft or self.execution_readiness_errors)
|
|
426
|
+
|
|
427
|
+
def to_dict(self) -> dict[str, Any]:
|
|
428
|
+
return {
|
|
429
|
+
"terminal_state": self.terminal_state,
|
|
430
|
+
"attempts": int(self.attempts),
|
|
431
|
+
"max_attempts": int(self.max_attempts),
|
|
432
|
+
"schema_retries": int(self.schema_retries),
|
|
433
|
+
"readiness_retries": int(self.readiness_retries),
|
|
434
|
+
"host_repaired": bool(self.host_repaired),
|
|
435
|
+
"host_repaired_fields": list(self.host_repaired_fields),
|
|
436
|
+
"execution_readiness_errors": list(self.execution_readiness_errors),
|
|
437
|
+
"clarification_rounds": int(self.clarification_rounds),
|
|
438
|
+
"forced_draft": bool(self.forced_draft),
|
|
439
|
+
"validation_errors": list(self.validation_errors),
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
# ---------------------------------------------------------------------------
|
|
444
|
+
# Plan status
|
|
445
|
+
# ---------------------------------------------------------------------------
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@dataclass(frozen=True)
|
|
449
|
+
class PlanStatusAssessment:
|
|
450
|
+
"""Whether a saved plan can be executed, and why not when it cannot."""
|
|
451
|
+
|
|
452
|
+
status: str
|
|
453
|
+
blocking_reasons: list[str] = field(default_factory=list)
|
|
454
|
+
warnings: list[str] = field(default_factory=list)
|
|
455
|
+
|
|
456
|
+
@property
|
|
457
|
+
def execution_ready(self) -> bool:
|
|
458
|
+
return self.status == PLAN_STATUS_EXECUTION_READY
|
|
459
|
+
|
|
460
|
+
def to_dict(self) -> dict[str, Any]:
|
|
461
|
+
return {
|
|
462
|
+
"status": self.status,
|
|
463
|
+
"blocking_reasons": list(self.blocking_reasons),
|
|
464
|
+
"warnings": list(self.warnings),
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def assess_plan_status(
|
|
469
|
+
plan: Mapping[str, Any],
|
|
470
|
+
*,
|
|
471
|
+
validation_warnings: Sequence[str] | None = None,
|
|
472
|
+
) -> PlanStatusAssessment:
|
|
473
|
+
"""Classify a plan as draft or execution_ready.
|
|
474
|
+
|
|
475
|
+
The rule is the execution gate's own rule, so the two can never disagree: a
|
|
476
|
+
plan is ``execution_ready`` exactly when ``forge exec`` would not reject it.
|
|
477
|
+
``validate_plan`` warnings (a missing ``acceptance_criteria``, say) are
|
|
478
|
+
recorded but do not by themselves make a plan a draft -- they are advisory,
|
|
479
|
+
and calling them blocking here would contradict the gate.
|
|
480
|
+
"""
|
|
481
|
+
blocking = execution_readiness_errors(plan)
|
|
482
|
+
warnings = [str(item).strip() for item in (validation_warnings or []) if str(item).strip()]
|
|
483
|
+
status = PLAN_STATUS_DRAFT if blocking else PLAN_STATUS_EXECUTION_READY
|
|
484
|
+
return PlanStatusAssessment(
|
|
485
|
+
status=status,
|
|
486
|
+
blocking_reasons=blocking,
|
|
487
|
+
warnings=warnings,
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def apply_plan_status(
|
|
492
|
+
plan: dict[str, Any],
|
|
493
|
+
*,
|
|
494
|
+
validation_warnings: Sequence[str] | None = None,
|
|
495
|
+
assessment: PlanStatusAssessment | None = None,
|
|
496
|
+
) -> PlanStatusAssessment:
|
|
497
|
+
"""Write the draft/execution_ready status onto ``plan`` and return it."""
|
|
498
|
+
resolved = (
|
|
499
|
+
assessment
|
|
500
|
+
if assessment is not None
|
|
501
|
+
else assess_plan_status(plan, validation_warnings=validation_warnings)
|
|
502
|
+
)
|
|
503
|
+
plan[PLAN_STATUS_KEY] = resolved.status
|
|
504
|
+
plan[PLAN_STATUS_DETAIL_KEY] = {
|
|
505
|
+
"blocking_reasons": list(resolved.blocking_reasons),
|
|
506
|
+
"warnings": list(resolved.warnings),
|
|
507
|
+
}
|
|
508
|
+
return resolved
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def resolved_plan_status(plan: Mapping[str, Any]) -> PlanStatusAssessment:
|
|
512
|
+
"""The plan's status as recorded, or assessed live when it was never recorded.
|
|
513
|
+
|
|
514
|
+
Status and reasons always come from the same source: a plan saved before this
|
|
515
|
+
field existed must not report a status from one place and empty reasons from
|
|
516
|
+
another.
|
|
517
|
+
"""
|
|
518
|
+
recorded = str(plan.get(PLAN_STATUS_KEY) or "").strip()
|
|
519
|
+
detail = plan.get(PLAN_STATUS_DETAIL_KEY)
|
|
520
|
+
if recorded in {PLAN_STATUS_DRAFT, PLAN_STATUS_EXECUTION_READY} and isinstance(detail, Mapping):
|
|
521
|
+
return PlanStatusAssessment(
|
|
522
|
+
status=recorded,
|
|
523
|
+
blocking_reasons=[str(item) for item in detail.get("blocking_reasons") or []],
|
|
524
|
+
warnings=[str(item) for item in detail.get("warnings") or []],
|
|
525
|
+
)
|
|
526
|
+
return assess_plan_status(plan)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def plan_status(plan: Mapping[str, Any]) -> str:
|
|
530
|
+
"""Recorded status, or the live assessment for a plan saved before this existed."""
|
|
531
|
+
return resolved_plan_status(plan).status
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def plan_status_detail(plan: Mapping[str, Any]) -> dict[str, Any]:
|
|
535
|
+
assessment = resolved_plan_status(plan)
|
|
536
|
+
return {
|
|
537
|
+
"blocking_reasons": list(assessment.blocking_reasons),
|
|
538
|
+
"warnings": list(assessment.warnings),
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
# ---------------------------------------------------------------------------
|
|
543
|
+
# Repair metadata on the plan
|
|
544
|
+
# ---------------------------------------------------------------------------
|
|
545
|
+
|
|
546
|
+
# Keep the on-plan history bounded: the most recent turns are what a reader
|
|
547
|
+
# needs, and plan.json is re-serialised on every save.
|
|
548
|
+
_MAX_RECORDED_REPAIRS = 20
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def record_plan_repair(plan: dict[str, Any], report: PlannerRepairReport) -> dict[str, Any]:
|
|
552
|
+
"""Append one turn's repair report to the plan's repair metadata.
|
|
553
|
+
|
|
554
|
+
Only degraded turns are recorded. A turn the model got right on its own adds
|
|
555
|
+
no history, so the presence of this block always means something was salvaged.
|
|
556
|
+
"""
|
|
557
|
+
metadata = plan.get(PLAN_REPAIR_KEY)
|
|
558
|
+
if not isinstance(metadata, dict):
|
|
559
|
+
metadata = {}
|
|
560
|
+
entries = metadata.get("entries")
|
|
561
|
+
if not isinstance(entries, list):
|
|
562
|
+
entries = []
|
|
563
|
+
|
|
564
|
+
if report.degraded:
|
|
565
|
+
entries.append(report.to_dict())
|
|
566
|
+
entries = entries[-_MAX_RECORDED_REPAIRS:]
|
|
567
|
+
|
|
568
|
+
host_fields: list[str] = []
|
|
569
|
+
for entry in entries:
|
|
570
|
+
if isinstance(entry, Mapping):
|
|
571
|
+
host_fields.extend(str(item) for item in entry.get("host_repaired_fields") or [])
|
|
572
|
+
|
|
573
|
+
metadata["entries"] = entries
|
|
574
|
+
metadata["host_repaired_fields"] = sorted(dict.fromkeys(host_fields))
|
|
575
|
+
metadata["host_repaired"] = any(
|
|
576
|
+
bool(entry.get("host_repaired")) for entry in entries if isinstance(entry, Mapping)
|
|
577
|
+
)
|
|
578
|
+
metadata["forced_draft"] = any(
|
|
579
|
+
bool(entry.get("forced_draft")) for entry in entries if isinstance(entry, Mapping)
|
|
580
|
+
)
|
|
581
|
+
if entries:
|
|
582
|
+
plan[PLAN_REPAIR_KEY] = metadata
|
|
583
|
+
return metadata
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def plan_repair_metadata(plan: Mapping[str, Any]) -> dict[str, Any]:
|
|
587
|
+
"""Repair metadata for ``plan``, normalised for reporting surfaces."""
|
|
588
|
+
metadata = plan.get(PLAN_REPAIR_KEY)
|
|
589
|
+
if not isinstance(metadata, Mapping):
|
|
590
|
+
return {
|
|
591
|
+
"host_repaired": False,
|
|
592
|
+
"host_repaired_fields": [],
|
|
593
|
+
"forced_draft": False,
|
|
594
|
+
"entries": [],
|
|
595
|
+
}
|
|
596
|
+
entries = [dict(entry) for entry in metadata.get("entries") or [] if isinstance(entry, Mapping)]
|
|
597
|
+
return {
|
|
598
|
+
"host_repaired": bool(metadata.get("host_repaired")),
|
|
599
|
+
"host_repaired_fields": [str(item) for item in metadata.get("host_repaired_fields") or []],
|
|
600
|
+
"forced_draft": bool(metadata.get("forced_draft")),
|
|
601
|
+
"entries": entries,
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def plan_repair_event_payload(plan: Mapping[str, Any]) -> dict[str, Any]:
|
|
606
|
+
"""The status + repair fields every ``plan_saved``/``plan_invalid`` event carries."""
|
|
607
|
+
metadata = plan_repair_metadata(plan)
|
|
608
|
+
assessment = resolved_plan_status(plan)
|
|
609
|
+
return {
|
|
610
|
+
"plan_status": assessment.status,
|
|
611
|
+
"plan_status_blocking_reasons": list(assessment.blocking_reasons),
|
|
612
|
+
"host_repaired": metadata["host_repaired"],
|
|
613
|
+
"host_repaired_fields": metadata["host_repaired_fields"],
|
|
614
|
+
"forced_draft": metadata["forced_draft"],
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
__all__ = [
|
|
619
|
+
"DEFAULT_MAX_CLARIFICATION_ROUNDS",
|
|
620
|
+
"DEFAULT_MAX_PAYLOAD_ATTEMPTS",
|
|
621
|
+
"PLAN_REPAIR_KEY",
|
|
622
|
+
"PLAN_STATUS_DETAIL_KEY",
|
|
623
|
+
"PLAN_STATUS_DRAFT",
|
|
624
|
+
"PLAN_STATUS_EXECUTION_READY",
|
|
625
|
+
"PLAN_STATUS_KEY",
|
|
626
|
+
"TERMINAL_FAILED",
|
|
627
|
+
"TERMINAL_FORCED_DRAFT",
|
|
628
|
+
"TERMINAL_HOST_REPAIRED",
|
|
629
|
+
"TERMINAL_VALIDATED",
|
|
630
|
+
"ClarificationLoopTracker",
|
|
631
|
+
"PlanRepairPolicy",
|
|
632
|
+
"PlanStatusAssessment",
|
|
633
|
+
"PlannerRepairReport",
|
|
634
|
+
"apply_plan_status",
|
|
635
|
+
"assess_plan_status",
|
|
636
|
+
"clarification_goal_key",
|
|
637
|
+
"execution_readiness_errors",
|
|
638
|
+
"format_acceptance_issue",
|
|
639
|
+
"host_repaired_field_paths",
|
|
640
|
+
"payload_awaits_clarification",
|
|
641
|
+
"plan_repair_enabled",
|
|
642
|
+
"plan_repair_event_payload",
|
|
643
|
+
"plan_repair_metadata",
|
|
644
|
+
"plan_status",
|
|
645
|
+
"plan_status_detail",
|
|
646
|
+
"plan_update_proposes_task_work",
|
|
647
|
+
"record_plan_repair",
|
|
648
|
+
"resolved_plan_status",
|
|
649
|
+
"repair_retry_instruction",
|
|
650
|
+
"repair_strictness_instruction",
|
|
651
|
+
"resolve_plan_repair_policy",
|
|
652
|
+
]
|