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,1215 @@
|
|
|
1
|
+
# ruff: noqa: F401,F403,F405,I001
|
|
2
|
+
# Legacy split module: dependencies are synced by cli_surface.py.
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .cli_common import *
|
|
6
|
+
from .forge_asset_view import forge_asset_view_count
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _run_plan_mode_approval_loop(
|
|
10
|
+
*,
|
|
11
|
+
session: Any,
|
|
12
|
+
console: Console,
|
|
13
|
+
user_message: str,
|
|
14
|
+
max_iterations: int | None = None,
|
|
15
|
+
) -> str | None:
|
|
16
|
+
from ..chat import _run_plan_mode_approval_loop_impl
|
|
17
|
+
|
|
18
|
+
return _run_plan_mode_approval_loop_impl(
|
|
19
|
+
_cli_module_for_legacy_impl(),
|
|
20
|
+
session=session,
|
|
21
|
+
console=console,
|
|
22
|
+
user_message=user_message,
|
|
23
|
+
max_iterations=max_iterations,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _planning_help_panel() -> Panel:
|
|
28
|
+
rows = [
|
|
29
|
+
("/help", "show planning commands"),
|
|
30
|
+
("/goal <text>", "set project goal"),
|
|
31
|
+
(
|
|
32
|
+
"/task <title>",
|
|
33
|
+
"add a task; ambiguous/mutating work must name repo-relative file paths",
|
|
34
|
+
),
|
|
35
|
+
("/done", "save + validate the plan, then return to chat"),
|
|
36
|
+
]
|
|
37
|
+
if _patchable("_is_narrow_terminal", _is_narrow_terminal)():
|
|
38
|
+
lines = [f"{cmd} - {desc}" for cmd, desc in rows]
|
|
39
|
+
lines.append("")
|
|
40
|
+
for footer_line in _forge_help_footer_lines():
|
|
41
|
+
lines.append(f"[dim]{footer_line}[/dim]")
|
|
42
|
+
text = "\n".join(lines)
|
|
43
|
+
return _Panel(text, title="Planning Help", border_style="bright_black")
|
|
44
|
+
table = _Table(
|
|
45
|
+
show_header=False,
|
|
46
|
+
box=None,
|
|
47
|
+
expand=True,
|
|
48
|
+
padding=(0, 1),
|
|
49
|
+
collapse_padding=True,
|
|
50
|
+
)
|
|
51
|
+
table.add_column("command", style=STYLE_EMPHASIS, no_wrap=True, ratio=2)
|
|
52
|
+
table.add_column("description", style="dim", no_wrap=False, ratio=5, overflow="fold")
|
|
53
|
+
for cmd, desc in rows:
|
|
54
|
+
table.add_row(cmd, desc)
|
|
55
|
+
content = _table_grid(expand=True)
|
|
56
|
+
content.add_row(table)
|
|
57
|
+
content.add_row("")
|
|
58
|
+
for footer_line in _forge_help_footer_lines():
|
|
59
|
+
content.add_row(_forge_bar_text(text=footer_line, style="dim"))
|
|
60
|
+
return _Panel(content, title="Planning Help", border_style="bright_black")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _forge_help_panel() -> Panel:
|
|
64
|
+
return _chat_commands_panel(ui_mode="forge")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _forge_enter_panel(
|
|
68
|
+
*,
|
|
69
|
+
paths: RunPaths,
|
|
70
|
+
plan: dict[str, Any],
|
|
71
|
+
entry_kind: str,
|
|
72
|
+
assistant_enabled: bool,
|
|
73
|
+
workspace_summary_lines: list[str] | None = None,
|
|
74
|
+
model: str | None = None,
|
|
75
|
+
mode: str | None = None,
|
|
76
|
+
) -> Any:
|
|
77
|
+
from rich.console import Group
|
|
78
|
+
|
|
79
|
+
_ = workspace_summary_lines
|
|
80
|
+
task_count = len(plan.get("tasks") or [])
|
|
81
|
+
task_label = "task" if task_count == 1 else "tasks"
|
|
82
|
+
assistant_label = "assistant on" if assistant_enabled else "assistant off"
|
|
83
|
+
lines = [
|
|
84
|
+
_forge_bar_text(
|
|
85
|
+
text=f"Forge ready · run {paths.run_id} · {task_count} {task_label} · {assistant_label}",
|
|
86
|
+
style=STYLE_EMPHASIS,
|
|
87
|
+
),
|
|
88
|
+
]
|
|
89
|
+
context_bits = []
|
|
90
|
+
if str(model or "").strip():
|
|
91
|
+
context_bits.append(f"model {str(model).strip()}")
|
|
92
|
+
if str(mode or "").strip():
|
|
93
|
+
context_bits.append(f"mode {str(mode).strip()}")
|
|
94
|
+
if context_bits:
|
|
95
|
+
lines.append(_forge_bar_text(text=" · ".join(context_bits), style="dim"))
|
|
96
|
+
lines.append(
|
|
97
|
+
_forge_bar_text(
|
|
98
|
+
text=_forge_entry_status_text(entry_kind=entry_kind),
|
|
99
|
+
style="dim",
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
lines.append(
|
|
103
|
+
_forge_bar_text(
|
|
104
|
+
text="/show for summary · /execute plan when ready · /help for commands",
|
|
105
|
+
style="dim",
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
return Group(*lines)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _forge_requirement_text(item: Any) -> str:
|
|
112
|
+
return requirement_text(item)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _forge_has_nonempty_requirement(plan: dict[str, Any]) -> bool:
|
|
116
|
+
requirements = plan.get("requirements") or []
|
|
117
|
+
if not isinstance(requirements, list):
|
|
118
|
+
return False
|
|
119
|
+
return any(bool(_forge_requirement_text(req)) for req in requirements)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _forge_has_usable_plan_input(plan: dict[str, Any]) -> bool:
|
|
123
|
+
tasks = plan.get("tasks") or []
|
|
124
|
+
has_tasks = isinstance(tasks, list) and len(tasks) > 0
|
|
125
|
+
return has_tasks or _forge_has_nonempty_requirement(plan)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _forge_no_execution_ready_tasks_message(plan: dict[str, Any]) -> str | None:
|
|
129
|
+
tasks = plan.get("tasks") or []
|
|
130
|
+
if isinstance(tasks, list) and tasks:
|
|
131
|
+
return None
|
|
132
|
+
requirements = plan.get("requirements") or []
|
|
133
|
+
if not isinstance(requirements, list):
|
|
134
|
+
return None
|
|
135
|
+
if not any(_forge_requirement_text(req) for req in requirements):
|
|
136
|
+
return None
|
|
137
|
+
if any(requirement_is_execution_ready(req) for req in requirements):
|
|
138
|
+
return None
|
|
139
|
+
return (
|
|
140
|
+
"Execution blocked: planner fallback captured requirements, but no execution-ready "
|
|
141
|
+
"tasks exist. Re-run the planner after the model/API is available, or add scoped "
|
|
142
|
+
"tasks with repo-relative file paths before executing."
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _validate_forge_plan_shape(candidate: Any) -> str | None:
|
|
147
|
+
if not isinstance(candidate, dict):
|
|
148
|
+
return "Plan JSON must be an object."
|
|
149
|
+
for key in ("tasks", "assets", "requirements"):
|
|
150
|
+
if key in candidate and not isinstance(candidate.get(key), list):
|
|
151
|
+
return f"Invalid plan JSON: '{key}' must be an array."
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _validate_forge_plan_for_paths(paths: RunPaths, plan: dict[str, Any]) -> list[str]:
|
|
156
|
+
warnings = _patchable("validate_plan", validate_plan)(plan)
|
|
157
|
+
if not hasattr(paths, "assets_index_path"):
|
|
158
|
+
return warnings
|
|
159
|
+
try:
|
|
160
|
+
cfg = load_config()
|
|
161
|
+
max_primary = int(cfg.assets.planner.max_primary_per_task)
|
|
162
|
+
except Exception as e: # noqa: BLE001
|
|
163
|
+
warnings.append(f"Asset validation config unavailable; using default limit: {e}")
|
|
164
|
+
max_primary = 8
|
|
165
|
+
try:
|
|
166
|
+
warnings.extend(
|
|
167
|
+
validate_plan_against_assets(
|
|
168
|
+
plan,
|
|
169
|
+
AssetIndex(paths),
|
|
170
|
+
max_primary_per_task=max_primary,
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
except Exception as e: # noqa: BLE001
|
|
174
|
+
warnings.append(f"Asset validation skipped: {e}")
|
|
175
|
+
return warnings
|
|
176
|
+
return warnings
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _edit_forge_plan_json(
|
|
180
|
+
*,
|
|
181
|
+
console: Console,
|
|
182
|
+
paths: RunPaths,
|
|
183
|
+
forge_state: _ForgeChatState,
|
|
184
|
+
) -> None:
|
|
185
|
+
current_plan = forge_state.plan
|
|
186
|
+
if current_plan is None:
|
|
187
|
+
_print_forge_warning_messages(
|
|
188
|
+
console=console,
|
|
189
|
+
label="Plan edit",
|
|
190
|
+
warnings=["No Forge plan loaded."],
|
|
191
|
+
)
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
save_plan(paths, current_plan)
|
|
195
|
+
backup_path = paths.plan_json_path.with_suffix(".json.bak")
|
|
196
|
+
try:
|
|
197
|
+
backup_path.write_text(paths.plan_json_path.read_text(encoding="utf-8"), encoding="utf-8")
|
|
198
|
+
except Exception as e: # noqa: BLE001
|
|
199
|
+
_print_forge_warning_messages(
|
|
200
|
+
console=console,
|
|
201
|
+
label="Plan edit",
|
|
202
|
+
warnings=[f"Backup warning: {e}"],
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
edited_text = typer.edit(filename=os.fspath(paths.plan_json_path), extension=".json")
|
|
207
|
+
except TypeError:
|
|
208
|
+
edited_text = typer.edit(os.fspath(paths.plan_json_path), extension=".json")
|
|
209
|
+
except Exception as e: # noqa: BLE001
|
|
210
|
+
_print_forge_warning_messages(
|
|
211
|
+
console=console,
|
|
212
|
+
label="Plan edit",
|
|
213
|
+
warnings=[f"Editor unavailable: {e}"],
|
|
214
|
+
)
|
|
215
|
+
_print_forge_meta(console=console, message=f"Edit directly · {paths.plan_json_path}")
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
if edited_text is None:
|
|
219
|
+
_print_forge_meta(console=console, message="Plan edit canceled.")
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
edited_raw = paths.plan_json_path.read_text(encoding="utf-8")
|
|
224
|
+
except Exception as e: # noqa: BLE001
|
|
225
|
+
_print_forge_error(console=console, message=f"Failed to read updated plan file: {e}")
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
candidate = json.loads(edited_raw)
|
|
230
|
+
except json.JSONDecodeError as e:
|
|
231
|
+
_print_forge_error(console=console, message=f"Invalid JSON: {e}")
|
|
232
|
+
_print_forge_meta(console=console, message="Current in-memory plan was not changed.")
|
|
233
|
+
try:
|
|
234
|
+
if backup_path.exists():
|
|
235
|
+
paths.plan_json_path.write_text(
|
|
236
|
+
backup_path.read_text(encoding="utf-8"), encoding="utf-8"
|
|
237
|
+
)
|
|
238
|
+
except Exception as restore_error: # noqa: BLE001
|
|
239
|
+
_print_forge_warning_messages(
|
|
240
|
+
console=console,
|
|
241
|
+
label="Plan edit",
|
|
242
|
+
warnings=[f"Failed to restore previous plan JSON: {restore_error}"],
|
|
243
|
+
)
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
shape_error = _validate_forge_plan_shape(candidate)
|
|
247
|
+
if shape_error:
|
|
248
|
+
_print_forge_error(console=console, message=shape_error)
|
|
249
|
+
_print_forge_meta(console=console, message="Current in-memory plan was not changed.")
|
|
250
|
+
try:
|
|
251
|
+
if backup_path.exists():
|
|
252
|
+
paths.plan_json_path.write_text(
|
|
253
|
+
backup_path.read_text(encoding="utf-8"), encoding="utf-8"
|
|
254
|
+
)
|
|
255
|
+
except Exception as restore_error: # noqa: BLE001
|
|
256
|
+
_print_forge_warning_messages(
|
|
257
|
+
console=console,
|
|
258
|
+
label="Plan edit",
|
|
259
|
+
warnings=[f"Failed to restore previous plan JSON: {restore_error}"],
|
|
260
|
+
)
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
assert isinstance(candidate, dict)
|
|
264
|
+
validation_warnings = _validate_forge_plan_for_paths(paths, candidate)
|
|
265
|
+
save_plan(paths, candidate)
|
|
266
|
+
forge_state.plan = candidate
|
|
267
|
+
_print_forge_meta(console=console, message=f"Updated plan · {paths.plan_json_path}")
|
|
268
|
+
if validation_warnings:
|
|
269
|
+
_print_forge_warning_messages(
|
|
270
|
+
console=console,
|
|
271
|
+
label="Plan validation",
|
|
272
|
+
warnings=validation_warnings,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _show_forge_plan_summary(*, console: Console, paths: RunPaths, plan: dict[str, Any]) -> None:
|
|
277
|
+
goal = str(plan.get("project_goal") or "").strip() or "(not set)"
|
|
278
|
+
summary = str(plan.get("summary") or "").strip() or "(not set)"
|
|
279
|
+
tasks = plan.get("tasks") or []
|
|
280
|
+
task_count = len(tasks) if isinstance(tasks, list) else 0
|
|
281
|
+
asset_count = forge_asset_view_count(paths, plan)
|
|
282
|
+
_print_forge_meta(
|
|
283
|
+
console=console,
|
|
284
|
+
message=f"Run {paths.run_id} · {task_count} tasks · {asset_count} assets",
|
|
285
|
+
style=STYLE_EMPHASIS,
|
|
286
|
+
)
|
|
287
|
+
_print_forge_meta(console=console, message=f"Goal · {goal}", style=STYLE_CONTENT)
|
|
288
|
+
_print_forge_meta(console=console, message=f"Summary · {summary}", style=STYLE_CONTENT)
|
|
289
|
+
requirements = plan.get("requirements") or []
|
|
290
|
+
req_count = len(requirements) if isinstance(requirements, list) else 0
|
|
291
|
+
_print_forge_meta(console=console, message=f"Requirements · {req_count}", style="dim")
|
|
292
|
+
if req_count:
|
|
293
|
+
for idx, requirement in enumerate(requirements[:8], start=1):
|
|
294
|
+
req_text = _forge_requirement_text(requirement)
|
|
295
|
+
if not req_text:
|
|
296
|
+
continue
|
|
297
|
+
if len(req_text) > 120:
|
|
298
|
+
req_text = req_text[:117].rstrip() + "..."
|
|
299
|
+
_print_forge_meta(console=console, message=f"{idx}. {req_text}", style=STYLE_CONTENT)
|
|
300
|
+
if req_count > 8:
|
|
301
|
+
_print_forge_meta(
|
|
302
|
+
console=console,
|
|
303
|
+
message=f"... ({req_count - 8} more requirements)",
|
|
304
|
+
style="dim",
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
table = _forge_task_table()
|
|
308
|
+
table.add_column("id", style="dim", no_wrap=True, ratio=1)
|
|
309
|
+
table.add_column("status", style="dim", no_wrap=True, ratio=1)
|
|
310
|
+
table.add_column("title", style=STYLE_CONTENT, no_wrap=False, ratio=4, overflow="fold")
|
|
311
|
+
table.add_column("dependencies", style="dim", no_wrap=False, ratio=2, overflow="fold")
|
|
312
|
+
table.add_column("mcp", style="dim", no_wrap=False, ratio=2, overflow="fold")
|
|
313
|
+
if tasks:
|
|
314
|
+
for task in tasks:
|
|
315
|
+
deps = task.get("dependencies") or []
|
|
316
|
+
deps_label = ", ".join(str(dep) for dep in deps) if deps else "-"
|
|
317
|
+
table.add_row(
|
|
318
|
+
str(task.get("id") or "-"),
|
|
319
|
+
_forge_task_status_markup(str(task.get("status") or "planned")),
|
|
320
|
+
str(task.get("title") or "-"),
|
|
321
|
+
deps_label,
|
|
322
|
+
_forge_task_mcp_summary_label(task if isinstance(task, dict) else {}),
|
|
323
|
+
)
|
|
324
|
+
else:
|
|
325
|
+
table.add_row("-", "-", "(no tasks yet)", "-", "off")
|
|
326
|
+
console.print(table)
|
|
327
|
+
_print_forge_meta(console=console, message=f"Assets · {asset_count}", style="dim")
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _show_forge_plan_markdown(
|
|
331
|
+
*,
|
|
332
|
+
console: Console,
|
|
333
|
+
paths: RunPaths,
|
|
334
|
+
plan: dict[str, Any],
|
|
335
|
+
) -> None:
|
|
336
|
+
# Ensure PLAN.md reflects in-memory plan changes before showing it.
|
|
337
|
+
save_plan(paths, plan)
|
|
338
|
+
plan_md_path = paths.plan_md_path
|
|
339
|
+
try:
|
|
340
|
+
markdown_text = plan_md_path.read_text(encoding="utf-8")
|
|
341
|
+
except Exception as e: # noqa: BLE001
|
|
342
|
+
_print_forge_error(console=console, message=f"Failed to read PLAN.md: {e}")
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
lines = markdown_text.splitlines()
|
|
346
|
+
preview_limit = 200
|
|
347
|
+
console.print(f"PLAN.md: {plan_md_path}")
|
|
348
|
+
|
|
349
|
+
if _patchable("_is_interactive_terminal", _is_interactive_terminal)():
|
|
350
|
+
try:
|
|
351
|
+
# Use plain-text pager output for maximum terminal compatibility.
|
|
352
|
+
pager_text = f"Press q to exit this view.\n\n{markdown_text}"
|
|
353
|
+
with console.pager(styles=False):
|
|
354
|
+
console.print(pager_text, markup=False, highlight=False)
|
|
355
|
+
return
|
|
356
|
+
except KeyboardInterrupt:
|
|
357
|
+
console.print("")
|
|
358
|
+
_print_forge_meta(console=console, message="Closed PLAN.md pager.")
|
|
359
|
+
return
|
|
360
|
+
except Exception as e: # noqa: BLE001
|
|
361
|
+
_print_forge_warning_messages(
|
|
362
|
+
console=console,
|
|
363
|
+
label="PLAN.md",
|
|
364
|
+
warnings=[f"Pager unavailable: {e}", "Showing preview instead."],
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
preview = "\n".join(lines[:preview_limit]).rstrip()
|
|
368
|
+
if preview:
|
|
369
|
+
console.print(preview, markup=False, highlight=False)
|
|
370
|
+
else:
|
|
371
|
+
console.print("(PLAN.md is empty)")
|
|
372
|
+
if len(lines) > preview_limit:
|
|
373
|
+
console.print(
|
|
374
|
+
f"[dim]... ({len(lines) - preview_limit} more lines in {plan_md_path.name})[/dim]"
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _forge_enrich_plan_enabled() -> bool:
|
|
379
|
+
raw = env_get("ALYSIS_FORGE_ENRICH_PLAN")
|
|
380
|
+
if raw is not None:
|
|
381
|
+
parsed = _parse_bool_text(raw)
|
|
382
|
+
return bool(parsed) if parsed is not None else False
|
|
383
|
+
return not _is_non_interactive_terminal()
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _forge_should_try_enrichment(validation_warnings: list[str]) -> bool:
|
|
387
|
+
target_tokens = (
|
|
388
|
+
"missing acceptance_criteria",
|
|
389
|
+
"missing estimated_files",
|
|
390
|
+
EXECUTION_UNREADY_SCOPE_WARNING.casefold(),
|
|
391
|
+
)
|
|
392
|
+
lowered = [str(item).casefold() for item in validation_warnings]
|
|
393
|
+
return any(any(token in warning for token in target_tokens) for warning in lowered)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _set_forge_planner_follow_up_state(
|
|
397
|
+
*,
|
|
398
|
+
planner_state: _ForgePlannerSessionState,
|
|
399
|
+
questions: list[str] | None,
|
|
400
|
+
awaiting_clarification: bool,
|
|
401
|
+
) -> None:
|
|
402
|
+
clean_questions = [
|
|
403
|
+
str(question).strip() for question in questions or [] if str(question).strip()
|
|
404
|
+
]
|
|
405
|
+
planner_state.pending_questions = clean_questions
|
|
406
|
+
planner_state.awaiting_clarification = bool(awaiting_clarification and clean_questions)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _sync_forge_planner_follow_up_state_from_result(
|
|
410
|
+
*,
|
|
411
|
+
planner_state: _ForgePlannerSessionState,
|
|
412
|
+
planner_result: Any,
|
|
413
|
+
goal_key: str | None = None,
|
|
414
|
+
) -> None:
|
|
415
|
+
questions = getattr(planner_result, "questions", None)
|
|
416
|
+
plan_update = getattr(planner_result, "plan_update", None)
|
|
417
|
+
error = str(getattr(planner_result, "error", "") or "").strip()
|
|
418
|
+
awaiting = bool(not error and not plan_update)
|
|
419
|
+
_set_forge_planner_follow_up_state(
|
|
420
|
+
planner_state=planner_state,
|
|
421
|
+
questions=questions if isinstance(questions, list) else list(questions or []),
|
|
422
|
+
awaiting_clarification=awaiting,
|
|
423
|
+
)
|
|
424
|
+
if goal_key is None:
|
|
425
|
+
return
|
|
426
|
+
tracker = getattr(planner_state, "clarification_loop", None)
|
|
427
|
+
if tracker is None:
|
|
428
|
+
return
|
|
429
|
+
# A turn that produced a plan update resets the streak; only a genuine
|
|
430
|
+
# question-and-nothing-else turn counts toward the cap.
|
|
431
|
+
tracker.record(
|
|
432
|
+
goal_key=goal_key,
|
|
433
|
+
awaiting_clarification=awaiting and bool(planner_state.awaiting_clarification),
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _workspace_context_payload_for_paths(
|
|
438
|
+
*,
|
|
439
|
+
paths: RunPaths,
|
|
440
|
+
refresh_if_stale: bool = False,
|
|
441
|
+
) -> dict[str, Any] | None:
|
|
442
|
+
try:
|
|
443
|
+
scan = ensure_workspace_context_artifacts(paths, refresh_if_stale=refresh_if_stale)
|
|
444
|
+
except ForgeError:
|
|
445
|
+
return None
|
|
446
|
+
payload = scan.to_dict()
|
|
447
|
+
payload["greenfield"] = bool(getattr(paths, "greenfield", False))
|
|
448
|
+
return _augment_workspace_context_with_mcp_execution_context(
|
|
449
|
+
workspace_context=payload,
|
|
450
|
+
workspace_root=paths.root,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _reconcile_plan_for_paths(
|
|
455
|
+
*,
|
|
456
|
+
paths: RunPaths,
|
|
457
|
+
plan: dict[str, Any],
|
|
458
|
+
refresh_if_stale: bool = False,
|
|
459
|
+
user_text: str | None = None,
|
|
460
|
+
transcript_tail: list[dict[str, Any]] | None = None,
|
|
461
|
+
target_task_ids: list[str] | tuple[str, ...] | None = None,
|
|
462
|
+
) -> tuple[Any, dict[str, Any] | None]:
|
|
463
|
+
workspace_context = _workspace_context_payload_for_paths(
|
|
464
|
+
paths=paths,
|
|
465
|
+
refresh_if_stale=refresh_if_stale,
|
|
466
|
+
)
|
|
467
|
+
result = reconcile_plan_with_workspace(
|
|
468
|
+
plan,
|
|
469
|
+
workspace_root=paths.root,
|
|
470
|
+
workspace_context=workspace_context,
|
|
471
|
+
user_text=user_text,
|
|
472
|
+
transcript_tail=transcript_tail,
|
|
473
|
+
target_task_ids=(
|
|
474
|
+
{str(task_id).strip() for task_id in target_task_ids if str(task_id).strip()}
|
|
475
|
+
if target_task_ids is not None
|
|
476
|
+
else None
|
|
477
|
+
),
|
|
478
|
+
)
|
|
479
|
+
return result, workspace_context
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
@dataclass(frozen=True)
|
|
483
|
+
class _ForgePlannerTurnControllerResult:
|
|
484
|
+
status: str
|
|
485
|
+
planner_result: Any | None = None
|
|
486
|
+
summary_line: str | None = None
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
@dataclass(frozen=True)
|
|
490
|
+
class _ForgePlannerUpdateSanitizationResult:
|
|
491
|
+
plan_update: dict[str, Any] | None
|
|
492
|
+
warnings: list[str]
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _sanitize_forge_enrichment_plan_update(
|
|
496
|
+
*,
|
|
497
|
+
plan: dict[str, Any],
|
|
498
|
+
plan_update: dict[str, Any] | None,
|
|
499
|
+
) -> _ForgePlannerUpdateSanitizationResult:
|
|
500
|
+
if not isinstance(plan_update, dict):
|
|
501
|
+
return _ForgePlannerUpdateSanitizationResult(plan_update=None, warnings=[])
|
|
502
|
+
|
|
503
|
+
warnings: list[str] = []
|
|
504
|
+
allowed_top_level = {"tasks_update"}
|
|
505
|
+
dropped_top_level = sorted(set(plan_update) - allowed_top_level)
|
|
506
|
+
if dropped_top_level:
|
|
507
|
+
warnings.append(
|
|
508
|
+
"Ignored plan enrichment fields outside narrow execution-readiness scope: "
|
|
509
|
+
+ ", ".join(dropped_top_level)
|
|
510
|
+
+ "."
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
tasks_by_id = {
|
|
514
|
+
str(task.get("id") or "").strip(): task
|
|
515
|
+
for task in plan.get("tasks") or []
|
|
516
|
+
if isinstance(task, dict) and str(task.get("id") or "").strip()
|
|
517
|
+
}
|
|
518
|
+
raw_updates = plan_update.get("tasks_update")
|
|
519
|
+
if not isinstance(raw_updates, list):
|
|
520
|
+
return _ForgePlannerUpdateSanitizationResult(plan_update=None, warnings=warnings)
|
|
521
|
+
|
|
522
|
+
sanitized_updates: list[dict[str, Any]] = []
|
|
523
|
+
for raw_patch in raw_updates:
|
|
524
|
+
if not isinstance(raw_patch, dict):
|
|
525
|
+
warnings.append("Ignored non-object plan enrichment tasks_update entry.")
|
|
526
|
+
continue
|
|
527
|
+
task_id = str(raw_patch.get("id") or "").strip()
|
|
528
|
+
if not task_id:
|
|
529
|
+
warnings.append("Ignored plan enrichment tasks_update entry without an id.")
|
|
530
|
+
continue
|
|
531
|
+
current_task = tasks_by_id.get(task_id)
|
|
532
|
+
sanitized_patch: dict[str, Any] = {"id": task_id}
|
|
533
|
+
|
|
534
|
+
if "acceptance_criteria" in raw_patch:
|
|
535
|
+
current_acceptance = (
|
|
536
|
+
list(current_task.get("acceptance_criteria") or [])
|
|
537
|
+
if isinstance(current_task, dict)
|
|
538
|
+
else []
|
|
539
|
+
)
|
|
540
|
+
new_acceptance = list(raw_patch.get("acceptance_criteria") or [])
|
|
541
|
+
if current_acceptance:
|
|
542
|
+
warnings.append(
|
|
543
|
+
f"Ignored plan enrichment acceptance_criteria update for {task_id} "
|
|
544
|
+
"because that field is already populated."
|
|
545
|
+
)
|
|
546
|
+
elif new_acceptance:
|
|
547
|
+
sanitized_patch["acceptance_criteria"] = new_acceptance
|
|
548
|
+
else:
|
|
549
|
+
warnings.append(
|
|
550
|
+
f"Ignored empty plan enrichment acceptance_criteria update for {task_id}."
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
if "estimated_files" in raw_patch:
|
|
554
|
+
current_estimated_files = (
|
|
555
|
+
list(current_task.get("estimated_files") or [])
|
|
556
|
+
if isinstance(current_task, dict)
|
|
557
|
+
else []
|
|
558
|
+
)
|
|
559
|
+
new_estimated_files = list(raw_patch.get("estimated_files") or [])
|
|
560
|
+
if current_estimated_files:
|
|
561
|
+
warnings.append(
|
|
562
|
+
f"Ignored plan enrichment estimated_files update for {task_id} "
|
|
563
|
+
"because that field is already populated."
|
|
564
|
+
)
|
|
565
|
+
elif new_estimated_files:
|
|
566
|
+
sanitized_patch["estimated_files"] = new_estimated_files
|
|
567
|
+
else:
|
|
568
|
+
warnings.append(
|
|
569
|
+
f"Ignored empty plan enrichment estimated_files update for {task_id}."
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
dropped_patch_fields = sorted(
|
|
573
|
+
set(raw_patch)
|
|
574
|
+
- {
|
|
575
|
+
"id",
|
|
576
|
+
"acceptance_criteria",
|
|
577
|
+
"estimated_files",
|
|
578
|
+
}
|
|
579
|
+
)
|
|
580
|
+
if dropped_patch_fields:
|
|
581
|
+
warnings.append(
|
|
582
|
+
f"Ignored plan enrichment tasks_update fields for {task_id}: "
|
|
583
|
+
+ ", ".join(dropped_patch_fields)
|
|
584
|
+
+ "."
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
if len(sanitized_patch) > 1:
|
|
588
|
+
sanitized_updates.append(sanitized_patch)
|
|
589
|
+
else:
|
|
590
|
+
warnings.append(
|
|
591
|
+
f"Ignored plan enrichment tasks_update for {task_id} because it did not add "
|
|
592
|
+
"missing execution-readiness fields."
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
sanitized_update = {"tasks_update": sanitized_updates} if sanitized_updates else None
|
|
596
|
+
return _ForgePlannerUpdateSanitizationResult(
|
|
597
|
+
plan_update=sanitized_update,
|
|
598
|
+
warnings=list(dict.fromkeys(warnings)),
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _run_forge_planner_turn_controller(
|
|
603
|
+
*,
|
|
604
|
+
console: Console,
|
|
605
|
+
paths: RunPaths,
|
|
606
|
+
plan: dict[str, Any],
|
|
607
|
+
planner_state: _ForgePlannerSessionState,
|
|
608
|
+
user_text: str,
|
|
609
|
+
cfg_loader: Callable[[], AppConfig],
|
|
610
|
+
unavailable_message_builder: Callable[[Exception], str],
|
|
611
|
+
emit_meta: Callable[[str], None],
|
|
612
|
+
emit_warning_group: Callable[[str, list[str]], None],
|
|
613
|
+
api_key_override: str | None,
|
|
614
|
+
render_reply: Callable[[str, list[str] | None], None],
|
|
615
|
+
selection_label: str = "planner",
|
|
616
|
+
planning_relevant: bool = True,
|
|
617
|
+
refresh_workspace_context: bool = False,
|
|
618
|
+
trace_callback: Callable[[str, bool], None] | None = None,
|
|
619
|
+
stream: bool = False,
|
|
620
|
+
on_text_delta: Callable[[str], None] | None = None,
|
|
621
|
+
on_usage_events: Callable[[list[dict[str, Any]]], None] | None = None,
|
|
622
|
+
error_fallback: Callable[[], None] | None = None,
|
|
623
|
+
) -> _ForgePlannerTurnControllerResult:
|
|
624
|
+
planner_state.transcript.append({"role": "user", "content": user_text})
|
|
625
|
+
append_planner_chat(paths, role="user", message=user_text)
|
|
626
|
+
|
|
627
|
+
if planner_state.cfg is None:
|
|
628
|
+
try:
|
|
629
|
+
planner_state.cfg = cfg_loader()
|
|
630
|
+
except Exception as e: # noqa: BLE001
|
|
631
|
+
assistant_message = unavailable_message_builder(e)
|
|
632
|
+
emit_meta(assistant_message)
|
|
633
|
+
append_transcript_note(paths, role="assistant", message=assistant_message)
|
|
634
|
+
append_planner_chat(paths, role="assistant", message=assistant_message)
|
|
635
|
+
planner_state.transcript.append({"role": "assistant", "content": assistant_message})
|
|
636
|
+
append_planner_summary(paths, "no plan_update proposed")
|
|
637
|
+
append_transcript_note(
|
|
638
|
+
paths,
|
|
639
|
+
role="system",
|
|
640
|
+
message="Planner proposed no plan update.",
|
|
641
|
+
)
|
|
642
|
+
_set_forge_planner_follow_up_state(
|
|
643
|
+
planner_state=planner_state,
|
|
644
|
+
questions=[],
|
|
645
|
+
awaiting_clarification=False,
|
|
646
|
+
)
|
|
647
|
+
return _ForgePlannerTurnControllerResult(status="unavailable")
|
|
648
|
+
|
|
649
|
+
if refresh_workspace_context or planner_state.workspace_context is None:
|
|
650
|
+
planner_state.workspace_context = _workspace_context_payload_for_paths(
|
|
651
|
+
paths=paths,
|
|
652
|
+
refresh_if_stale=refresh_workspace_context,
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
planner_knowledge = _patchable("prepare_planner_knowledge", prepare_planner_knowledge)(
|
|
656
|
+
paths=paths,
|
|
657
|
+
plan=plan,
|
|
658
|
+
user_text=user_text,
|
|
659
|
+
selection_label=selection_label,
|
|
660
|
+
)
|
|
661
|
+
planner_workspace_root = resolve_knowledge_workspace_root(paths)
|
|
662
|
+
clarification_tracker = getattr(planner_state, "clarification_loop", None)
|
|
663
|
+
goal_key = clarification_goal_key(plan=plan, user_text=user_text)
|
|
664
|
+
if clarification_tracker is not None:
|
|
665
|
+
clarification_tracker.max_rounds = resolve_plan_repair_policy(
|
|
666
|
+
planner_state.cfg
|
|
667
|
+
).max_clarification_rounds
|
|
668
|
+
clarification_rounds = clarification_tracker.rounds_for(goal_key)
|
|
669
|
+
else:
|
|
670
|
+
clarification_rounds = 0
|
|
671
|
+
planner_result = _patchable("run_planner_turn", run_planner_turn)(
|
|
672
|
+
cfg=planner_state.cfg,
|
|
673
|
+
api_key_override=api_key_override,
|
|
674
|
+
plan=plan,
|
|
675
|
+
transcript_tail=planner_state.transcript,
|
|
676
|
+
workspace_context=planner_state.workspace_context,
|
|
677
|
+
user_text=user_text,
|
|
678
|
+
stream=stream,
|
|
679
|
+
on_text_delta=on_text_delta if stream else None,
|
|
680
|
+
relevant_knowledge_section=planner_knowledge.render_prompt_section(
|
|
681
|
+
workspace_root=planner_workspace_root
|
|
682
|
+
),
|
|
683
|
+
prefer_context="forge",
|
|
684
|
+
awaiting_clarification=planner_state.awaiting_clarification,
|
|
685
|
+
pending_questions=planner_state.pending_questions,
|
|
686
|
+
clarification_rounds=clarification_rounds,
|
|
687
|
+
run_paths=paths,
|
|
688
|
+
)
|
|
689
|
+
usage_events = list(getattr(planner_result, "usage_events", []) or [])
|
|
690
|
+
if usage_events and on_usage_events is not None:
|
|
691
|
+
try:
|
|
692
|
+
on_usage_events(usage_events)
|
|
693
|
+
except Exception:
|
|
694
|
+
pass
|
|
695
|
+
render_reply(
|
|
696
|
+
str(getattr(planner_result, "assistant_message", "") or ""),
|
|
697
|
+
list(getattr(planner_result, "questions", []) or []),
|
|
698
|
+
)
|
|
699
|
+
assistant_message = str(getattr(planner_result, "assistant_message", "") or "")
|
|
700
|
+
append_transcript_note(paths, role="assistant", message=assistant_message)
|
|
701
|
+
append_planner_chat(paths, role="assistant", message=assistant_message)
|
|
702
|
+
planner_state.transcript.append({"role": "assistant", "content": assistant_message})
|
|
703
|
+
|
|
704
|
+
request_retry_count = int(getattr(planner_result, "request_retry_count", 0) or 0)
|
|
705
|
+
planner_error = str(getattr(planner_result, "error", "") or "").strip()
|
|
706
|
+
_sync_forge_planner_follow_up_state_from_result(
|
|
707
|
+
planner_state=planner_state,
|
|
708
|
+
planner_result=planner_result,
|
|
709
|
+
goal_key=goal_key,
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
if planner_error:
|
|
713
|
+
if trace_callback is not None:
|
|
714
|
+
retry_word = "retry" if request_retry_count == 1 else "retries"
|
|
715
|
+
error_trace_message = (
|
|
716
|
+
"Planner returned an error after "
|
|
717
|
+
f"{request_retry_count} transient {retry_word}; using fallback handling."
|
|
718
|
+
if request_retry_count > 0
|
|
719
|
+
else "Planner returned an error; using fallback handling."
|
|
720
|
+
)
|
|
721
|
+
trace_callback(error_trace_message, False)
|
|
722
|
+
elif trace_callback is not None:
|
|
723
|
+
trace_callback("Planner response ready.", False)
|
|
724
|
+
|
|
725
|
+
if request_retry_count > 0 and not planner_error:
|
|
726
|
+
retry_word = "retry" if request_retry_count == 1 else "retries"
|
|
727
|
+
retry_notice = (
|
|
728
|
+
f"Planner request recovered after {request_retry_count} transient {retry_word}."
|
|
729
|
+
)
|
|
730
|
+
emit_warning_group("Planner", [retry_notice])
|
|
731
|
+
append_transcript_note(paths, role="system", message=f"Planner warning: {retry_notice}")
|
|
732
|
+
if trace_callback is not None:
|
|
733
|
+
trace_callback(retry_notice, True)
|
|
734
|
+
|
|
735
|
+
questions = [
|
|
736
|
+
str(question).strip() for question in getattr(planner_result, "questions", []) or []
|
|
737
|
+
]
|
|
738
|
+
if questions:
|
|
739
|
+
append_transcript_note(
|
|
740
|
+
paths,
|
|
741
|
+
role="system",
|
|
742
|
+
message=f"Planner questions: {'; '.join(questions)}",
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
if planner_error:
|
|
746
|
+
retry_word = "retry" if request_retry_count == 1 else "retries"
|
|
747
|
+
planner_error_summary = (
|
|
748
|
+
f"planner error after {request_retry_count} transient {retry_word}: {planner_error}"
|
|
749
|
+
if request_retry_count > 0
|
|
750
|
+
else f"planner error: {planner_error}"
|
|
751
|
+
)
|
|
752
|
+
planner_error_note = (
|
|
753
|
+
f"Planner error after {request_retry_count} transient {retry_word}: {planner_error}"
|
|
754
|
+
if request_retry_count > 0
|
|
755
|
+
else f"Planner error: {planner_error}"
|
|
756
|
+
)
|
|
757
|
+
planner_error_warnings = (
|
|
758
|
+
[f"Planner request failed after {request_retry_count} transient {retry_word}."]
|
|
759
|
+
if request_retry_count > 0
|
|
760
|
+
else []
|
|
761
|
+
)
|
|
762
|
+
planner_error_warnings.append(f"Final planner error: {planner_error}")
|
|
763
|
+
emit_warning_group("Planner", planner_error_warnings)
|
|
764
|
+
append_planner_summary(paths, planner_error_summary)
|
|
765
|
+
append_transcript_note(paths, role="system", message=planner_error_note)
|
|
766
|
+
if error_fallback is not None:
|
|
767
|
+
error_fallback()
|
|
768
|
+
return _ForgePlannerTurnControllerResult(
|
|
769
|
+
status="error",
|
|
770
|
+
planner_result=planner_result,
|
|
771
|
+
summary_line=planner_error_summary,
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
raw_plan_update = getattr(planner_result, "plan_update", None)
|
|
775
|
+
if raw_plan_update:
|
|
776
|
+
if not planning_relevant:
|
|
777
|
+
append_planner_summary(paths, "plan_update ignored: message not planning-relevant")
|
|
778
|
+
append_transcript_note(
|
|
779
|
+
paths,
|
|
780
|
+
role="system",
|
|
781
|
+
message="Planner update ignored because the user message was not planning-relevant.",
|
|
782
|
+
)
|
|
783
|
+
emit_meta(
|
|
784
|
+
"Planner update ignored because this message did not look "
|
|
785
|
+
"planning-related; plan unchanged."
|
|
786
|
+
)
|
|
787
|
+
return _ForgePlannerTurnControllerResult(
|
|
788
|
+
status="ignored",
|
|
789
|
+
planner_result=planner_result,
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
apply_result = apply_guarded_planner_plan_update(
|
|
793
|
+
plan,
|
|
794
|
+
raw_plan_update,
|
|
795
|
+
latest_user_text=user_text,
|
|
796
|
+
workspace_context=planner_state.workspace_context,
|
|
797
|
+
)
|
|
798
|
+
summary_line = summarize_plan_update(apply_result)
|
|
799
|
+
reconciliation_result = None
|
|
800
|
+
if apply_result.changed:
|
|
801
|
+
reconciliation_target_ids = list(
|
|
802
|
+
dict.fromkeys([*apply_result.added_task_ids, *apply_result.updated_task_ids])
|
|
803
|
+
)
|
|
804
|
+
reconciliation_kwargs: dict[str, Any] = {
|
|
805
|
+
"paths": paths,
|
|
806
|
+
"plan": plan,
|
|
807
|
+
"refresh_if_stale": False,
|
|
808
|
+
"user_text": user_text,
|
|
809
|
+
"transcript_tail": planner_state.transcript,
|
|
810
|
+
}
|
|
811
|
+
if reconciliation_target_ids:
|
|
812
|
+
reconciliation_kwargs["target_task_ids"] = reconciliation_target_ids
|
|
813
|
+
reconciliation_result, planner_state.workspace_context = _reconcile_plan_for_paths(
|
|
814
|
+
**reconciliation_kwargs
|
|
815
|
+
)
|
|
816
|
+
# Record what the repair loop had to salvage, and stamp the plan with an
|
|
817
|
+
# explicit draft/execution_ready status, before it is written. Downstream
|
|
818
|
+
# surfaces read the status instead of rediscovering it at exec time.
|
|
819
|
+
record_plan_repair(plan, getattr(planner_result, "repair", None) or PlannerRepairReport())
|
|
820
|
+
apply_plan_status(plan, validation_warnings=validate_plan(plan))
|
|
821
|
+
save_plan(paths, plan)
|
|
822
|
+
if apply_result.changed:
|
|
823
|
+
if trace_callback is not None:
|
|
824
|
+
trace_callback("Applied planner update to the Forge plan.", False)
|
|
825
|
+
emit_meta("Applied planner update to plan.")
|
|
826
|
+
if apply_result.warnings:
|
|
827
|
+
emit_warning_group("Planner", list(apply_result.warnings))
|
|
828
|
+
if reconciliation_result is not None and reconciliation_result.warnings:
|
|
829
|
+
emit_warning_group("Plan reconciliation", list(reconciliation_result.warnings))
|
|
830
|
+
else:
|
|
831
|
+
if trace_callback is not None:
|
|
832
|
+
trace_callback("Planner update was a no-op.", True)
|
|
833
|
+
emit_meta("Planner update contained no applicable changes.")
|
|
834
|
+
if apply_result.warnings:
|
|
835
|
+
emit_warning_group("Planner", list(apply_result.warnings))
|
|
836
|
+
append_planner_summary(paths, summary_line)
|
|
837
|
+
append_transcript_note(paths, role="system", message=f"Planner update: {summary_line}")
|
|
838
|
+
for warning in apply_result.warnings:
|
|
839
|
+
append_transcript_note(paths, role="system", message=f"Planner warning: {warning}")
|
|
840
|
+
if reconciliation_result is not None:
|
|
841
|
+
for warning in reconciliation_result.warnings:
|
|
842
|
+
append_transcript_note(
|
|
843
|
+
paths,
|
|
844
|
+
role="system",
|
|
845
|
+
message=f"Plan reconciliation warning: {warning}",
|
|
846
|
+
)
|
|
847
|
+
return _ForgePlannerTurnControllerResult(
|
|
848
|
+
status="applied" if apply_result.changed else "noop",
|
|
849
|
+
planner_result=planner_result,
|
|
850
|
+
summary_line=summary_line,
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
append_planner_summary(paths, "no plan_update proposed")
|
|
854
|
+
append_transcript_note(paths, role="system", message="Planner proposed no plan update.")
|
|
855
|
+
return _ForgePlannerTurnControllerResult(status="no_update", planner_result=planner_result)
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def _write_plan_validation_artifact(
|
|
859
|
+
*,
|
|
860
|
+
paths: RunPaths,
|
|
861
|
+
reconciliation_result: Any,
|
|
862
|
+
validation_warnings: list[str],
|
|
863
|
+
) -> None:
|
|
864
|
+
validation_path = paths.notes_dir / "plan_validation.md"
|
|
865
|
+
lines: list[str] = [
|
|
866
|
+
"# Plan Validation",
|
|
867
|
+
"",
|
|
868
|
+
"## Reconciliation",
|
|
869
|
+
"",
|
|
870
|
+
]
|
|
871
|
+
|
|
872
|
+
changed = bool(getattr(reconciliation_result, "changed", False))
|
|
873
|
+
updated_task_ids = list(getattr(reconciliation_result, "updated_task_ids", []) or [])
|
|
874
|
+
reconciliation_warnings = list(getattr(reconciliation_result, "warnings", []) or [])
|
|
875
|
+
lines.append(f"- Changed: `{'yes' if changed else 'no'}`")
|
|
876
|
+
if updated_task_ids:
|
|
877
|
+
lines.append(f"- Updated Tasks: {', '.join(updated_task_ids)}")
|
|
878
|
+
else:
|
|
879
|
+
lines.append("- Updated Tasks: (none)")
|
|
880
|
+
if reconciliation_warnings:
|
|
881
|
+
lines.append("- Warnings:")
|
|
882
|
+
for warning in reconciliation_warnings:
|
|
883
|
+
lines.append(f" - {warning}")
|
|
884
|
+
else:
|
|
885
|
+
lines.append("- No reconciliation warnings.")
|
|
886
|
+
|
|
887
|
+
lines.extend(["", "## Validation", ""])
|
|
888
|
+
if validation_warnings:
|
|
889
|
+
for warning in validation_warnings:
|
|
890
|
+
lines.append(f"- {warning}")
|
|
891
|
+
else:
|
|
892
|
+
lines.append("- No validation warnings.")
|
|
893
|
+
|
|
894
|
+
validation_path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def _capture_forge_requirement_from_planner_fallback(
|
|
898
|
+
*,
|
|
899
|
+
plan: dict[str, Any],
|
|
900
|
+
paths: RunPaths,
|
|
901
|
+
console: Console,
|
|
902
|
+
user_text: str,
|
|
903
|
+
planning_relevant: bool | None = None,
|
|
904
|
+
) -> bool:
|
|
905
|
+
if planning_relevant is False:
|
|
906
|
+
append_transcript_note(
|
|
907
|
+
paths,
|
|
908
|
+
role="system",
|
|
909
|
+
message="Skipped fallback requirement capture for non-planning message.",
|
|
910
|
+
)
|
|
911
|
+
_print_forge_meta(
|
|
912
|
+
console=console,
|
|
913
|
+
message="Planner skipped this message because it looked off-topic; plan unchanged.",
|
|
914
|
+
)
|
|
915
|
+
return False
|
|
916
|
+
|
|
917
|
+
if _set_forge_project_goal_if_empty(
|
|
918
|
+
plan=plan,
|
|
919
|
+
paths=paths,
|
|
920
|
+
console=console,
|
|
921
|
+
goal=user_text,
|
|
922
|
+
transcript_message="Captured project goal (planner produced no structured update).",
|
|
923
|
+
display_message="Captured project goal because the planner produced no structured update.",
|
|
924
|
+
):
|
|
925
|
+
return True
|
|
926
|
+
|
|
927
|
+
add_requirement(
|
|
928
|
+
plan,
|
|
929
|
+
user_text,
|
|
930
|
+
execution_ready=False,
|
|
931
|
+
source="planner_error_fallback",
|
|
932
|
+
)
|
|
933
|
+
save_plan(paths, plan)
|
|
934
|
+
append_transcript_note(
|
|
935
|
+
paths,
|
|
936
|
+
role="system",
|
|
937
|
+
message="Captured requirement note (planner produced no structured update).",
|
|
938
|
+
)
|
|
939
|
+
_print_forge_meta(
|
|
940
|
+
console=console,
|
|
941
|
+
message="Captured requirement note because the planner produced no structured update.",
|
|
942
|
+
)
|
|
943
|
+
return True
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
def _set_forge_project_goal_if_empty(
|
|
947
|
+
*,
|
|
948
|
+
plan: dict[str, Any],
|
|
949
|
+
paths: RunPaths,
|
|
950
|
+
console: Console,
|
|
951
|
+
goal: str,
|
|
952
|
+
transcript_message: str = "Updated project goal.",
|
|
953
|
+
display_message: str = "Project goal set.",
|
|
954
|
+
) -> bool:
|
|
955
|
+
normalized_goal = str(goal or "").strip()
|
|
956
|
+
if not normalized_goal or str(plan.get("project_goal") or "").strip():
|
|
957
|
+
return False
|
|
958
|
+
plan["project_goal"] = normalized_goal
|
|
959
|
+
if not str(plan.get("summary") or "").strip():
|
|
960
|
+
plan["summary"] = normalized_goal
|
|
961
|
+
save_plan(paths, plan)
|
|
962
|
+
append_transcript_note(paths, role="system", message=transcript_message)
|
|
963
|
+
_print_forge_meta(console=console, message=display_message)
|
|
964
|
+
return True
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def _finalize_forge_plan(
|
|
968
|
+
*,
|
|
969
|
+
console: Console,
|
|
970
|
+
paths: RunPaths,
|
|
971
|
+
plan: dict[str, Any],
|
|
972
|
+
transcript_tail: list[dict[str, Any]] | None = None,
|
|
973
|
+
) -> None:
|
|
974
|
+
finalize_plan(plan)
|
|
975
|
+
reconciliation_result, _ = _patchable(
|
|
976
|
+
"_reconcile_plan_for_paths",
|
|
977
|
+
_reconcile_plan_for_paths,
|
|
978
|
+
)(
|
|
979
|
+
paths=paths,
|
|
980
|
+
plan=plan,
|
|
981
|
+
refresh_if_stale=True,
|
|
982
|
+
transcript_tail=transcript_tail,
|
|
983
|
+
)
|
|
984
|
+
save_plan(paths, plan)
|
|
985
|
+
validation_warnings = _validate_forge_plan_for_paths(paths, plan)
|
|
986
|
+
if reconciliation_result.warnings:
|
|
987
|
+
_print_forge_warning_messages(
|
|
988
|
+
console=console,
|
|
989
|
+
label="Plan reconciliation",
|
|
990
|
+
warnings=list(reconciliation_result.warnings),
|
|
991
|
+
)
|
|
992
|
+
for warning in reconciliation_result.warnings:
|
|
993
|
+
append_transcript_note(
|
|
994
|
+
paths,
|
|
995
|
+
role="system",
|
|
996
|
+
message=f"Plan reconciliation warning: {warning}",
|
|
997
|
+
)
|
|
998
|
+
_write_plan_validation_artifact(
|
|
999
|
+
paths=paths,
|
|
1000
|
+
reconciliation_result=reconciliation_result,
|
|
1001
|
+
validation_warnings=validation_warnings,
|
|
1002
|
+
)
|
|
1003
|
+
if validation_warnings:
|
|
1004
|
+
_print_forge_warning_messages(
|
|
1005
|
+
console=console,
|
|
1006
|
+
label="Plan validation",
|
|
1007
|
+
warnings=validation_warnings,
|
|
1008
|
+
)
|
|
1009
|
+
for warning in validation_warnings:
|
|
1010
|
+
append_transcript_note(
|
|
1011
|
+
paths,
|
|
1012
|
+
role="system",
|
|
1013
|
+
message=f"Plan validation warning: {warning}",
|
|
1014
|
+
)
|
|
1015
|
+
_print_forge_meta(console=console, message=f"Plan saved · {paths.plan_md_path}")
|
|
1016
|
+
_print_forge_meta(console=console, message=f"Structured plan · {paths.plan_json_path}")
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def _enter_forge_mode(
|
|
1020
|
+
*,
|
|
1021
|
+
root: Path,
|
|
1022
|
+
console: Console,
|
|
1023
|
+
forge_state: _ForgeChatState,
|
|
1024
|
+
model: str | None = None,
|
|
1025
|
+
mode: str | None = None,
|
|
1026
|
+
planner_assistant_default: bool | None = None,
|
|
1027
|
+
) -> bool:
|
|
1028
|
+
forge_binding: WorkspaceBinding | None = None
|
|
1029
|
+
try:
|
|
1030
|
+
forge_binding = resolve_workspace_binding(
|
|
1031
|
+
root,
|
|
1032
|
+
create_if_missing=False,
|
|
1033
|
+
allow_broad_workspace=False,
|
|
1034
|
+
source="forge_entry",
|
|
1035
|
+
)
|
|
1036
|
+
ensure_workspace_policy(
|
|
1037
|
+
forge_binding,
|
|
1038
|
+
action=WorkspaceAction.FORGE,
|
|
1039
|
+
allow_broad_workspace=False,
|
|
1040
|
+
)
|
|
1041
|
+
except WorkspaceBindingError as e:
|
|
1042
|
+
if forge_binding is not None:
|
|
1043
|
+
console.print("[yellow]Forge requires a narrower workspace before planning.[/yellow]")
|
|
1044
|
+
console.print(f"- requested path: {forge_binding.requested_path}")
|
|
1045
|
+
console.print(f"- workspace root: {forge_binding.workspace_context.workspace_root}")
|
|
1046
|
+
console.print(f"- focus dir: {forge_binding.workspace_context.focus_path}")
|
|
1047
|
+
console.print(f"- risk level: {forge_binding.risk_level}")
|
|
1048
|
+
for reason in forge_binding.risk_reasons:
|
|
1049
|
+
console.print(f"- reason: {reason}")
|
|
1050
|
+
console.print(
|
|
1051
|
+
workspace_policy_violation_message(forge_binding, action=WorkspaceAction.FORGE)
|
|
1052
|
+
)
|
|
1053
|
+
else:
|
|
1054
|
+
_print_forge_error(console=console, message=f"Forge error: {e}")
|
|
1055
|
+
return False
|
|
1056
|
+
|
|
1057
|
+
workspace_scan = None
|
|
1058
|
+
try:
|
|
1059
|
+
entry_selection = _select_forge_chat_entry(
|
|
1060
|
+
forge_state=forge_state,
|
|
1061
|
+
workspace_binding=forge_binding,
|
|
1062
|
+
)
|
|
1063
|
+
except ForgeError as e:
|
|
1064
|
+
_print_forge_error(console=console, message=f"Forge error: {e}")
|
|
1065
|
+
return False
|
|
1066
|
+
paths = entry_selection.paths
|
|
1067
|
+
plan = entry_selection.plan
|
|
1068
|
+
forge_state.paths = paths
|
|
1069
|
+
forge_state.plan = plan
|
|
1070
|
+
entry_goal = str(getattr(forge_state, "entry_goal", "") or "").strip()
|
|
1071
|
+
forge_state.entry_goal = ""
|
|
1072
|
+
try:
|
|
1073
|
+
workspace_scan = refresh_workspace_context_artifacts(paths)
|
|
1074
|
+
except ForgeError as e:
|
|
1075
|
+
_print_forge_error(console=console, message=f"Forge error: {e}")
|
|
1076
|
+
return False
|
|
1077
|
+
if entry_selection.entry_kind in {"session_local_resume", "session_local_resume_rebound"}:
|
|
1078
|
+
refresh_current_run_pointer_if_tracking_same_run(paths)
|
|
1079
|
+
|
|
1080
|
+
forge_state.ui_mode = "forge"
|
|
1081
|
+
if planner_assistant_default is None:
|
|
1082
|
+
forge_state.assistant_enabled = _patchable(
|
|
1083
|
+
"_prompt_forge_entry_plan_assistant",
|
|
1084
|
+
_prompt_forge_entry_plan_assistant,
|
|
1085
|
+
)(console=console)
|
|
1086
|
+
else:
|
|
1087
|
+
forge_state.assistant_enabled = bool(planner_assistant_default)
|
|
1088
|
+
if entry_goal:
|
|
1089
|
+
_set_forge_project_goal_if_empty(
|
|
1090
|
+
plan=plan,
|
|
1091
|
+
paths=paths,
|
|
1092
|
+
console=console,
|
|
1093
|
+
goal=entry_goal,
|
|
1094
|
+
transcript_message="Set project goal from /forge shortcut.",
|
|
1095
|
+
display_message="Project goal set from /forge.",
|
|
1096
|
+
)
|
|
1097
|
+
forge_state.planner_session = _ForgePlannerSessionState(
|
|
1098
|
+
workspace_context=_augment_workspace_context_with_mcp_execution_context(
|
|
1099
|
+
workspace_context={
|
|
1100
|
+
**workspace_scan.to_dict(),
|
|
1101
|
+
"greenfield": bool(getattr(paths, "greenfield", False)),
|
|
1102
|
+
},
|
|
1103
|
+
workspace_root=paths.root,
|
|
1104
|
+
)
|
|
1105
|
+
)
|
|
1106
|
+
console.print(
|
|
1107
|
+
_forge_enter_panel(
|
|
1108
|
+
paths=paths,
|
|
1109
|
+
plan=plan,
|
|
1110
|
+
entry_kind=entry_selection.entry_kind,
|
|
1111
|
+
assistant_enabled=forge_state.assistant_enabled,
|
|
1112
|
+
workspace_summary_lines=format_workspace_context_summary_lines(workspace_scan),
|
|
1113
|
+
model=model,
|
|
1114
|
+
mode=mode,
|
|
1115
|
+
)
|
|
1116
|
+
)
|
|
1117
|
+
if (
|
|
1118
|
+
not forge_state.assistant_enabled
|
|
1119
|
+
and planner_assistant_default is None
|
|
1120
|
+
and _is_non_interactive_terminal()
|
|
1121
|
+
and env_get("ALYSIS_FORGE_PLAN_ASSISTANT") is None
|
|
1122
|
+
):
|
|
1123
|
+
_print_forge_meta(
|
|
1124
|
+
console=console,
|
|
1125
|
+
message=(
|
|
1126
|
+
"Plan Assistant defaulted off in non-interactive mode; set "
|
|
1127
|
+
"ALYSIS_FORGE_PLAN_ASSISTANT=1 to enable it."
|
|
1128
|
+
),
|
|
1129
|
+
)
|
|
1130
|
+
return True
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
def _is_forge_enter_command(*, cmd: str, arg: str) -> bool:
|
|
1134
|
+
parsed = _parse_forge_enter_command(cmd=cmd, arg=arg)
|
|
1135
|
+
return parsed is not None and parsed.usage_error is None
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
def _handle_forge_chat_command(
|
|
1139
|
+
*,
|
|
1140
|
+
input_text: str,
|
|
1141
|
+
forge_state: _ForgeChatState,
|
|
1142
|
+
session: Any,
|
|
1143
|
+
console: Console,
|
|
1144
|
+
) -> str:
|
|
1145
|
+
from ..chat import _handle_forge_chat_command_impl
|
|
1146
|
+
|
|
1147
|
+
return _handle_forge_chat_command_impl(
|
|
1148
|
+
_cli_module_for_legacy_impl(),
|
|
1149
|
+
input_text=input_text,
|
|
1150
|
+
forge_state=forge_state,
|
|
1151
|
+
session=session,
|
|
1152
|
+
console=console,
|
|
1153
|
+
)
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
def _build_forge_exec_instruction(
|
|
1157
|
+
*,
|
|
1158
|
+
plan: dict[str, Any],
|
|
1159
|
+
task: dict[str, Any],
|
|
1160
|
+
cfg: AppConfig,
|
|
1161
|
+
role_model: str,
|
|
1162
|
+
) -> str:
|
|
1163
|
+
return _shared_build_task_execution_instruction(
|
|
1164
|
+
plan=plan,
|
|
1165
|
+
task=task,
|
|
1166
|
+
cfg=cfg,
|
|
1167
|
+
role_model=role_model,
|
|
1168
|
+
)
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
def _build_forge_exec_instruction_bundle(
|
|
1172
|
+
*,
|
|
1173
|
+
plan: dict[str, Any],
|
|
1174
|
+
task: dict[str, Any],
|
|
1175
|
+
root: Path,
|
|
1176
|
+
cfg: AppConfig,
|
|
1177
|
+
role_model: str,
|
|
1178
|
+
mode: str,
|
|
1179
|
+
yes: bool,
|
|
1180
|
+
deny_write_prefixes: list[str] | None = None,
|
|
1181
|
+
allow_write_globs: list[str] | None = None,
|
|
1182
|
+
non_interactive: bool = False,
|
|
1183
|
+
verification_enabled: bool = True,
|
|
1184
|
+
authoritative_verification_commands: list[str] | None = None,
|
|
1185
|
+
api_key: str | None = None,
|
|
1186
|
+
subagents_enabled: bool = False,
|
|
1187
|
+
leading_sections: list[str] | None = None,
|
|
1188
|
+
relevant_assets_section: str | None = None,
|
|
1189
|
+
) -> Any:
|
|
1190
|
+
return _shared_build_task_execution_instruction_bundle(
|
|
1191
|
+
plan=plan,
|
|
1192
|
+
task=task,
|
|
1193
|
+
root=root,
|
|
1194
|
+
cfg=cfg,
|
|
1195
|
+
role_model=role_model,
|
|
1196
|
+
mode=mode,
|
|
1197
|
+
yes=yes,
|
|
1198
|
+
deny_write_prefixes=deny_write_prefixes,
|
|
1199
|
+
allow_write_globs=allow_write_globs,
|
|
1200
|
+
non_interactive=non_interactive,
|
|
1201
|
+
verification_enabled=verification_enabled,
|
|
1202
|
+
authoritative_verification_commands=authoritative_verification_commands,
|
|
1203
|
+
api_key=api_key,
|
|
1204
|
+
subagents_enabled=subagents_enabled,
|
|
1205
|
+
leading_sections=leading_sections,
|
|
1206
|
+
relevant_assets_section=relevant_assets_section,
|
|
1207
|
+
managed_execution_startup_headroom=True,
|
|
1208
|
+
)
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
def _snapshot_runtime_tree(root: Path) -> dict[str, str]:
|
|
1212
|
+
return _shared_snapshot_runtime_tree(root)
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
__all__ = [name for name in globals() if (not name.startswith("__") or name == "__version__")]
|