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,2616 @@
|
|
|
1
|
+
# ruff: noqa: F821
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.markdown import Markdown
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
|
|
14
|
+
from ...config import AppConfig, ConfigError
|
|
15
|
+
from ...forge_completion import build_forge_completion_report
|
|
16
|
+
from ...llm.base import effective_tools_for_client
|
|
17
|
+
from ...personas import is_persona_name, normalize_persona, persona_modes_enabled
|
|
18
|
+
from ...plan_repair import apply_plan_status
|
|
19
|
+
from ...plan_validation import PlannerFailedError, raise_for_execution_ready_plan
|
|
20
|
+
from ...surface.styles import (
|
|
21
|
+
STYLE_CHROME,
|
|
22
|
+
STYLE_EMPHASIS,
|
|
23
|
+
STYLE_SUCCESS,
|
|
24
|
+
STYLE_WARN,
|
|
25
|
+
)
|
|
26
|
+
from ...swarm_orchestrator import acquire_swarm_mutation_guard
|
|
27
|
+
from ...usage_tracker import UsageRecord, aggregate_usage_from_session_logs
|
|
28
|
+
from .state import _ChatExecutionRequest, _ChatPlanModeState, _ForgeChatState
|
|
29
|
+
|
|
30
|
+
_PROTECTED_COMMAND_GLOBAL_NAMES: set[str] = set()
|
|
31
|
+
_SYNCED_COMMAND_GLOBAL_VALUES: dict[str, Any] = {}
|
|
32
|
+
_COMMAND_SYNC_MISSING = object()
|
|
33
|
+
_PATCHABLE_COMMAND_GLOBAL_NAMES: set[str] = {
|
|
34
|
+
"_apply_config_menu_changes_to_session",
|
|
35
|
+
"_is_non_interactive_terminal",
|
|
36
|
+
"load_config",
|
|
37
|
+
}
|
|
38
|
+
_LOGGER = logging.getLogger("alysis_code.cli_impl.chat.commands")
|
|
39
|
+
_TERMINALS_DISPLAY_LINE_LIMIT = 200
|
|
40
|
+
_TERMINALS_USAGE_LINES = (
|
|
41
|
+
"Usage: /terminals",
|
|
42
|
+
" /terminals list",
|
|
43
|
+
" /terminals show <process_id>",
|
|
44
|
+
" /terminals kill <process_id>",
|
|
45
|
+
" /terminals help",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _merge_usage_payloads_into_session(
|
|
50
|
+
*, session: Any, payloads: list[dict[str, Any]] | tuple[dict[str, Any], ...]
|
|
51
|
+
) -> int:
|
|
52
|
+
summary = getattr(session, "usage_summary", None)
|
|
53
|
+
add_event_payload = getattr(summary, "add_event_payload", None)
|
|
54
|
+
store = getattr(session, "store", None)
|
|
55
|
+
append = getattr(store, "append", None)
|
|
56
|
+
merged = 0
|
|
57
|
+
for payload in payloads:
|
|
58
|
+
if not isinstance(payload, dict):
|
|
59
|
+
continue
|
|
60
|
+
if str(payload.get("event_type") or "") != "llm_usage":
|
|
61
|
+
continue
|
|
62
|
+
if callable(add_event_payload):
|
|
63
|
+
try:
|
|
64
|
+
add_event_payload(payload)
|
|
65
|
+
except Exception:
|
|
66
|
+
continue
|
|
67
|
+
if callable(append):
|
|
68
|
+
try:
|
|
69
|
+
append("llm_usage", dict(payload))
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
merged += 1
|
|
73
|
+
return merged
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _merge_usage_records_into_session(
|
|
77
|
+
*, session: Any, records: list[UsageRecord] | tuple[UsageRecord, ...]
|
|
78
|
+
) -> int:
|
|
79
|
+
summary = getattr(session, "usage_summary", None)
|
|
80
|
+
add_record = getattr(summary, "add_record", None)
|
|
81
|
+
store = getattr(session, "store", None)
|
|
82
|
+
append = getattr(store, "append", None)
|
|
83
|
+
merged = 0
|
|
84
|
+
for record in records:
|
|
85
|
+
if not isinstance(record, UsageRecord):
|
|
86
|
+
continue
|
|
87
|
+
if callable(add_record):
|
|
88
|
+
try:
|
|
89
|
+
add_record(record)
|
|
90
|
+
except Exception:
|
|
91
|
+
continue
|
|
92
|
+
if callable(append):
|
|
93
|
+
try:
|
|
94
|
+
append("llm_usage", record.to_payload())
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
merged += 1
|
|
98
|
+
return merged
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _merge_forge_execution_usage_into_session(*, session: Any, paths: Any) -> int:
|
|
102
|
+
logs_dir = getattr(paths, "execution_logs_dir", None)
|
|
103
|
+
if logs_dir is None:
|
|
104
|
+
return 0
|
|
105
|
+
try:
|
|
106
|
+
imported = set(getattr(session, "_alysis_imported_forge_usage_logs", set()) or set())
|
|
107
|
+
log_paths = sorted(Path(logs_dir).glob("*.jsonl"))
|
|
108
|
+
except Exception:
|
|
109
|
+
return 0
|
|
110
|
+
selected: list[Path] = []
|
|
111
|
+
selected_keys: list[str] = []
|
|
112
|
+
for path in log_paths:
|
|
113
|
+
try:
|
|
114
|
+
key = os.fspath(path.resolve())
|
|
115
|
+
except Exception:
|
|
116
|
+
key = os.fspath(path)
|
|
117
|
+
if key in imported:
|
|
118
|
+
continue
|
|
119
|
+
selected.append(path)
|
|
120
|
+
selected_keys.append(key)
|
|
121
|
+
if not selected:
|
|
122
|
+
return 0
|
|
123
|
+
try:
|
|
124
|
+
summary = aggregate_usage_from_session_logs(selected)
|
|
125
|
+
records = summary.records()
|
|
126
|
+
except Exception:
|
|
127
|
+
return 0
|
|
128
|
+
merged = _merge_usage_records_into_session(session=session, records=records)
|
|
129
|
+
if merged:
|
|
130
|
+
imported.update(selected_keys)
|
|
131
|
+
try:
|
|
132
|
+
session._alysis_imported_forge_usage_logs = imported
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
return merged
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _print_alysis_trial_models(console: Console, session: Any, *, current: str) -> None:
|
|
139
|
+
"""List the models the hosted Alysis Code trial serves, when on that profile.
|
|
140
|
+
|
|
141
|
+
Best-effort and silent otherwise: only runs when the active profile is the
|
|
142
|
+
hosted `alysis` trial, discovers the live allowlist via the proxy, and
|
|
143
|
+
never raises into the chat loop.
|
|
144
|
+
"""
|
|
145
|
+
cfg = getattr(session, "cfg", None)
|
|
146
|
+
if cfg is None:
|
|
147
|
+
return
|
|
148
|
+
try:
|
|
149
|
+
from ...alysis_cloud import PROFILE_KEY
|
|
150
|
+
|
|
151
|
+
active = str((getattr(cfg, "extra_fields", {}) or {}).get("active_profile") or "").strip()
|
|
152
|
+
if active != PROFILE_KEY:
|
|
153
|
+
return
|
|
154
|
+
from ... import account_login
|
|
155
|
+
|
|
156
|
+
models = account_login.list_trial_models(cfg)
|
|
157
|
+
except Exception: # noqa: BLE001 - listing is best-effort
|
|
158
|
+
return
|
|
159
|
+
if not models:
|
|
160
|
+
return
|
|
161
|
+
console.print("Models on your Alysis Code trial:")
|
|
162
|
+
for model_id in models:
|
|
163
|
+
marker = " (current)" if model_id == current else ""
|
|
164
|
+
console.print(f" • {model_id}{marker}")
|
|
165
|
+
console.print("[dim]Switch with: /model <name>[/dim]")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _print_forge_lock_wait_notice(console: Console, info: dict[str, Any]) -> None:
|
|
169
|
+
diagnostic = str(info.get("diagnostic") or "").strip()
|
|
170
|
+
warnings = ["Another Forge execution is mutating this workspace; queued and waiting."]
|
|
171
|
+
if diagnostic:
|
|
172
|
+
warnings.append(diagnostic)
|
|
173
|
+
_print_forge_warning_messages(
|
|
174
|
+
console=console, label="Forge execution queued", warnings=warnings
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _sync_command_globals(source_globals: dict[str, Any]) -> None:
|
|
179
|
+
module_globals = globals()
|
|
180
|
+
if not _PROTECTED_COMMAND_GLOBAL_NAMES:
|
|
181
|
+
for local_name, local_value in module_globals.items():
|
|
182
|
+
if callable(local_value):
|
|
183
|
+
_PROTECTED_COMMAND_GLOBAL_NAMES.add(local_name)
|
|
184
|
+
for name, value in source_globals.items():
|
|
185
|
+
if name.startswith("__"):
|
|
186
|
+
continue
|
|
187
|
+
if name in _PROTECTED_COMMAND_GLOBAL_NAMES and name not in _PATCHABLE_COMMAND_GLOBAL_NAMES:
|
|
188
|
+
continue
|
|
189
|
+
previous_synced_value = _SYNCED_COMMAND_GLOBAL_VALUES.get(name, _COMMAND_SYNC_MISSING)
|
|
190
|
+
if (
|
|
191
|
+
previous_synced_value is not _COMMAND_SYNC_MISSING
|
|
192
|
+
and module_globals.get(name, _COMMAND_SYNC_MISSING) is not previous_synced_value
|
|
193
|
+
):
|
|
194
|
+
# Preserve an explicit override applied at the command-module
|
|
195
|
+
# compatibility surface. Without tracking the last value injected
|
|
196
|
+
# by the facade, the next command dispatch silently replaced a
|
|
197
|
+
# monkeypatch after the first command of the process. That made
|
|
198
|
+
# identical classic-chat calls behave differently depending on
|
|
199
|
+
# which command test ran first.
|
|
200
|
+
continue
|
|
201
|
+
module_globals[name] = value
|
|
202
|
+
_SYNCED_COMMAND_GLOBAL_VALUES[name] = value
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _handle_chat_command(
|
|
206
|
+
*,
|
|
207
|
+
input_text: str,
|
|
208
|
+
root: Path,
|
|
209
|
+
session: Any,
|
|
210
|
+
pending_images: list[str],
|
|
211
|
+
console: Console,
|
|
212
|
+
forge_state: _ForgeChatState,
|
|
213
|
+
plan_mode_state: _ChatPlanModeState,
|
|
214
|
+
plan_mode_escape_supported: bool = False,
|
|
215
|
+
plan_mode_action_prompt: Any | None = None,
|
|
216
|
+
forge_planner_reply_sink: Any | None = None,
|
|
217
|
+
forge_execution_report_sink: Any | None = None,
|
|
218
|
+
) -> str | _ChatExecutionRequest:
|
|
219
|
+
trimmed = input_text.strip()
|
|
220
|
+
if not trimmed:
|
|
221
|
+
return "handled"
|
|
222
|
+
|
|
223
|
+
parts = trimmed.split(maxsplit=1)
|
|
224
|
+
cmd = parts[0].lower()
|
|
225
|
+
arg = parts[1].strip() if len(parts) > 1 else ""
|
|
226
|
+
|
|
227
|
+
if _is_forge_ui_mode(forge_state.ui_mode):
|
|
228
|
+
forge_action = _handle_forge_chat_command(
|
|
229
|
+
input_text=input_text,
|
|
230
|
+
forge_state=forge_state,
|
|
231
|
+
session=session,
|
|
232
|
+
console=console,
|
|
233
|
+
forge_planner_reply_sink=forge_planner_reply_sink,
|
|
234
|
+
forge_execution_report_sink=forge_execution_report_sink,
|
|
235
|
+
)
|
|
236
|
+
if forge_action != "unhandled":
|
|
237
|
+
return forge_action
|
|
238
|
+
|
|
239
|
+
parsed_forge_enter = _parse_forge_enter_command(cmd=cmd, arg=arg)
|
|
240
|
+
|
|
241
|
+
if cmd in {"exit", "quit", "/exit", "/quit"}:
|
|
242
|
+
return "exit"
|
|
243
|
+
if cmd == "/back":
|
|
244
|
+
console.print("Already in chat.")
|
|
245
|
+
return "handled"
|
|
246
|
+
if parsed_forge_enter is not None:
|
|
247
|
+
if parsed_forge_enter.usage_error is not None:
|
|
248
|
+
console.print(f"[red]{parsed_forge_enter.usage_error}[/red]")
|
|
249
|
+
for line in _forge_enter_usage_lines():
|
|
250
|
+
console.print(line)
|
|
251
|
+
return "handled"
|
|
252
|
+
if _is_forge_ui_mode(forge_state.ui_mode):
|
|
253
|
+
console.print("Forge is already active.")
|
|
254
|
+
return "handled"
|
|
255
|
+
forge_state.entry_request_mode = parsed_forge_enter.entry_mode
|
|
256
|
+
forge_state.entry_goal = parsed_forge_enter.initial_goal
|
|
257
|
+
try:
|
|
258
|
+
forge_root = _resolve_forge_entry_root(session=session, fallback_root=root)
|
|
259
|
+
except Exception as e: # noqa: BLE001
|
|
260
|
+
console.print(f"[red]Failed to enter Forge:[/red] {e}")
|
|
261
|
+
return "handled"
|
|
262
|
+
forge_model = getattr(getattr(session, "client", None), "model", None)
|
|
263
|
+
forge_mode = getattr(session, "mode", None)
|
|
264
|
+
enter_kwargs = {
|
|
265
|
+
"root": forge_root,
|
|
266
|
+
"console": console,
|
|
267
|
+
"forge_state": forge_state,
|
|
268
|
+
"model": str(forge_model) if forge_model else None,
|
|
269
|
+
"mode": str(forge_mode) if forge_mode else None,
|
|
270
|
+
}
|
|
271
|
+
if parsed_forge_enter.planner_assistant_default is not None:
|
|
272
|
+
# Explicit choice from the TUI intro popup's "Use the planner?" prompt
|
|
273
|
+
# (or a direct ``/forge --planner`` / ``/forge --no-planner``) wins.
|
|
274
|
+
enter_kwargs["planner_assistant_default"] = parsed_forge_enter.planner_assistant_default
|
|
275
|
+
elif bool(getattr(session, "_alysis_tui_interactive", False)):
|
|
276
|
+
# Bare/goal/resume entry in the alt-screen TUI can't run the classic
|
|
277
|
+
# stdin plan-assistant prompt, so default it on (unchanged behavior).
|
|
278
|
+
enter_kwargs["planner_assistant_default"] = True
|
|
279
|
+
_enter_forge_mode(**enter_kwargs)
|
|
280
|
+
return "handled"
|
|
281
|
+
if cmd == "/":
|
|
282
|
+
console.print(_chat_quick_commands_panel(ui_mode=forge_state.ui_mode))
|
|
283
|
+
return "handled"
|
|
284
|
+
if cmd == "/help":
|
|
285
|
+
console.print(_chat_help_panel(ui_mode=forge_state.ui_mode))
|
|
286
|
+
return "handled"
|
|
287
|
+
if cmd in {"/status"}:
|
|
288
|
+
_print_chat_status(console=console, session=session, pending_images=pending_images)
|
|
289
|
+
return "handled"
|
|
290
|
+
if cmd == "/subagents":
|
|
291
|
+
if arg.strip():
|
|
292
|
+
console.print("[yellow]Usage:[/yellow] /subagents")
|
|
293
|
+
return "handled"
|
|
294
|
+
scheduler = getattr(session, "child_scheduler", None)
|
|
295
|
+
if scheduler is None:
|
|
296
|
+
console.print("no active subagents")
|
|
297
|
+
return "handled"
|
|
298
|
+
try:
|
|
299
|
+
status = scheduler.status()
|
|
300
|
+
except Exception as exc: # noqa: BLE001 - inspection is best-effort
|
|
301
|
+
console.print(f"[red]/subagents failed:[/red] {exc}")
|
|
302
|
+
return "handled"
|
|
303
|
+
raw_children = status.get("children") if isinstance(status, dict) else None
|
|
304
|
+
active_children = [
|
|
305
|
+
child
|
|
306
|
+
for child in (raw_children if isinstance(raw_children, list) else [])
|
|
307
|
+
if isinstance(child, dict)
|
|
308
|
+
and str(child.get("state") or "").strip().lower()
|
|
309
|
+
in {"spawned", "queued", "waiting", "running"}
|
|
310
|
+
]
|
|
311
|
+
if not active_children:
|
|
312
|
+
console.print("no active subagents")
|
|
313
|
+
return "handled"
|
|
314
|
+
lines: list[str] = []
|
|
315
|
+
for child in active_children:
|
|
316
|
+
lines.append(
|
|
317
|
+
" | ".join(
|
|
318
|
+
(
|
|
319
|
+
str(child.get("subagent") or "subagent"),
|
|
320
|
+
str(child.get("state") or "unknown"),
|
|
321
|
+
str(child.get("workspace_view") or "shared"),
|
|
322
|
+
f"{int(child.get('steps_completed') or 0)} steps",
|
|
323
|
+
str(child.get("activity") or "working"),
|
|
324
|
+
)
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
console.print("\n".join(lines))
|
|
328
|
+
return "handled"
|
|
329
|
+
if cmd == "/ask":
|
|
330
|
+
remainder = arg.strip()
|
|
331
|
+
if not remainder:
|
|
332
|
+
console.print("[yellow]Usage:[/yellow] /ask <question> — one read-only turn")
|
|
333
|
+
return "handled"
|
|
334
|
+
current_mode = str(getattr(session, "mode", "review") or "review").strip().lower()
|
|
335
|
+
if current_mode == "readonly":
|
|
336
|
+
return _ChatExecutionRequest(instruction=remainder)
|
|
337
|
+
# One-turn read-only override: the existing mode machinery applies
|
|
338
|
+
# readonly before the turn and restores the previous mode afterwards,
|
|
339
|
+
# exception-safe, so "just look, don't touch" is host-enforced.
|
|
340
|
+
return _ChatExecutionRequest(
|
|
341
|
+
instruction=remainder,
|
|
342
|
+
mode_override="readonly",
|
|
343
|
+
restore_mode_after=current_mode,
|
|
344
|
+
)
|
|
345
|
+
if cmd == "/chat":
|
|
346
|
+
# Retired in favor of the Ask persona; the chat_only plumbing stays
|
|
347
|
+
# accepted-and-ignored for one release (no producer).
|
|
348
|
+
console.print(
|
|
349
|
+
"[yellow]/chat is retired.[/yellow] Use /mode ask for a persistent "
|
|
350
|
+
"read-only Q&A persona, or /ask <question> for one read-only turn."
|
|
351
|
+
)
|
|
352
|
+
return "handled"
|
|
353
|
+
if cmd in {"/terminals"}:
|
|
354
|
+
_handle_terminals_command(arg=arg, session=session, console=console)
|
|
355
|
+
return "handled"
|
|
356
|
+
if cmd in {"/pwd"}:
|
|
357
|
+
_print_chat_pwd(console=console, session=session)
|
|
358
|
+
return "handled"
|
|
359
|
+
if cmd in {"/cd"}:
|
|
360
|
+
requested_path = arg.strip()
|
|
361
|
+
if not requested_path:
|
|
362
|
+
console.print("[yellow]Usage:[/yellow] /cd <path>")
|
|
363
|
+
return "handled"
|
|
364
|
+
try:
|
|
365
|
+
result = set_session_active_workdir(
|
|
366
|
+
session,
|
|
367
|
+
requested_path,
|
|
368
|
+
source="chat_command",
|
|
369
|
+
)
|
|
370
|
+
except Exception as e: # noqa: BLE001
|
|
371
|
+
console.print(f"[red]Failed to change active workdir:[/red] {e}")
|
|
372
|
+
return "handled"
|
|
373
|
+
console.print(
|
|
374
|
+
f"Active workdir: {result['active_workdir']} ({result['active_workdir_relpath']})"
|
|
375
|
+
)
|
|
376
|
+
return "handled"
|
|
377
|
+
if cmd in {"/resume"}:
|
|
378
|
+
cfg = getattr(session, "cfg", None)
|
|
379
|
+
if not isinstance(cfg, AppConfig):
|
|
380
|
+
console.print("[red]Resume unavailable:[/red] missing session config.")
|
|
381
|
+
return "handled"
|
|
382
|
+
|
|
383
|
+
store = getattr(session, "store", None)
|
|
384
|
+
sessions_dir_raw = getattr(store, "sessions_dir", None)
|
|
385
|
+
sessions_dir = (
|
|
386
|
+
Path(sessions_dir_raw) if sessions_dir_raw is not None else resolve_sessions_dir(cfg)
|
|
387
|
+
)
|
|
388
|
+
current_session_id = str(getattr(store, "session_id", "") or "")
|
|
389
|
+
|
|
390
|
+
candidates = _collect_chat_resume_candidates(
|
|
391
|
+
sessions_dir=sessions_dir,
|
|
392
|
+
current_session_id=current_session_id,
|
|
393
|
+
workspace_root=getattr(store, "workspace_root", None),
|
|
394
|
+
git_root=getattr(store, "git_root", None),
|
|
395
|
+
)
|
|
396
|
+
target_session_id: str | None = None
|
|
397
|
+
raw_arg = arg.strip()
|
|
398
|
+
# Only the bare-picker form needs candidates; an explicit
|
|
399
|
+
# "/resume <id>" must still reach the direct-id escape hatch below even
|
|
400
|
+
# when owner/workspace scoping filtered the list down to nothing.
|
|
401
|
+
if not candidates and not raw_arg:
|
|
402
|
+
console.print("No previous sessions available to resume.")
|
|
403
|
+
return "handled"
|
|
404
|
+
|
|
405
|
+
if not raw_arg:
|
|
406
|
+
target_session_id, picker_available = _select_chat_resume_interactive(
|
|
407
|
+
current_session_id=current_session_id,
|
|
408
|
+
sessions=candidates,
|
|
409
|
+
console=console,
|
|
410
|
+
)
|
|
411
|
+
if not picker_available:
|
|
412
|
+
console.print(
|
|
413
|
+
_chat_resume_panel(
|
|
414
|
+
current_session_id=current_session_id,
|
|
415
|
+
sessions=candidates,
|
|
416
|
+
)
|
|
417
|
+
)
|
|
418
|
+
if _is_non_interactive_terminal():
|
|
419
|
+
console.print("Use /resume <index|session_id> to continue a previous session.")
|
|
420
|
+
return "handled"
|
|
421
|
+
try:
|
|
422
|
+
fallback_choice = typer.prompt(
|
|
423
|
+
"Resume session (index/session_id, Enter to cancel)",
|
|
424
|
+
default="",
|
|
425
|
+
).strip()
|
|
426
|
+
except (EOFError, KeyboardInterrupt):
|
|
427
|
+
console.print("")
|
|
428
|
+
return "handled"
|
|
429
|
+
if not fallback_choice:
|
|
430
|
+
return "handled"
|
|
431
|
+
target_session_id = _resolve_chat_resume_target(
|
|
432
|
+
raw_value=fallback_choice,
|
|
433
|
+
sessions=candidates,
|
|
434
|
+
)
|
|
435
|
+
if target_session_id is None and not fallback_choice.isdigit():
|
|
436
|
+
target_session_id = _resolve_chat_resume_direct_session_id(
|
|
437
|
+
raw_value=fallback_choice,
|
|
438
|
+
sessions_dir=sessions_dir,
|
|
439
|
+
)
|
|
440
|
+
if target_session_id is None:
|
|
441
|
+
console.print(
|
|
442
|
+
"[red]Invalid session.[/red] "
|
|
443
|
+
"Use /resume for picker or /resume <index|session_id>."
|
|
444
|
+
)
|
|
445
|
+
return "handled"
|
|
446
|
+
elif target_session_id is None:
|
|
447
|
+
return "handled"
|
|
448
|
+
else:
|
|
449
|
+
target_session_id = _resolve_chat_resume_target(
|
|
450
|
+
raw_value=raw_arg,
|
|
451
|
+
sessions=candidates,
|
|
452
|
+
)
|
|
453
|
+
if target_session_id is None and not raw_arg.isdigit():
|
|
454
|
+
target_session_id = _resolve_chat_resume_direct_session_id(
|
|
455
|
+
raw_value=raw_arg,
|
|
456
|
+
sessions_dir=sessions_dir,
|
|
457
|
+
)
|
|
458
|
+
if target_session_id is None:
|
|
459
|
+
console.print(
|
|
460
|
+
"[red]Invalid session.[/red] "
|
|
461
|
+
"Use /resume for picker or /resume <index|session_id>."
|
|
462
|
+
)
|
|
463
|
+
return "handled"
|
|
464
|
+
|
|
465
|
+
resumed, message, history_messages = _resume_chat_session(
|
|
466
|
+
session=session,
|
|
467
|
+
target_session_id=target_session_id,
|
|
468
|
+
)
|
|
469
|
+
console.print(message)
|
|
470
|
+
if resumed:
|
|
471
|
+
pending_images.clear()
|
|
472
|
+
# session_start restored the base mode; the persona (and with it
|
|
473
|
+
# the narrowed mode, write scope, and model role) rides on top.
|
|
474
|
+
_reapply_resumed_persona(session=session, console=console)
|
|
475
|
+
_render_chat_resume_history(
|
|
476
|
+
session=session,
|
|
477
|
+
messages=history_messages,
|
|
478
|
+
)
|
|
479
|
+
return "handled"
|
|
480
|
+
|
|
481
|
+
if cmd == "/usage":
|
|
482
|
+
raw_usage_arg = arg.strip()
|
|
483
|
+
if not raw_usage_arg:
|
|
484
|
+
_print_chat_usage(console=console, session=session)
|
|
485
|
+
return "handled"
|
|
486
|
+
parts_usage = raw_usage_arg.split(maxsplit=1)
|
|
487
|
+
if parts_usage[0].strip().lower() != "hud":
|
|
488
|
+
console.print("Usage: /usage (no args = print) or /usage hud on|off|status")
|
|
489
|
+
return "handled"
|
|
490
|
+
usage_arg = parts_usage[1].strip().lower() if len(parts_usage) > 1 else ""
|
|
491
|
+
if not usage_arg:
|
|
492
|
+
usage_arg, picker_available = _select_chat_usage_hud_interactive(
|
|
493
|
+
current_enabled=_chat_usage_hud_enabled(session),
|
|
494
|
+
console=console,
|
|
495
|
+
)
|
|
496
|
+
if not picker_available:
|
|
497
|
+
state = "on" if _chat_usage_hud_enabled(session) else "off"
|
|
498
|
+
console.print(f"Usage HUD: {state}")
|
|
499
|
+
console.print("[dim]Usage: /usage hud on|off|status[/dim]")
|
|
500
|
+
return "handled"
|
|
501
|
+
if usage_arg is None:
|
|
502
|
+
return "handled"
|
|
503
|
+
usage_arg = str(usage_arg).strip().lower()
|
|
504
|
+
if usage_arg == "status":
|
|
505
|
+
state = "on" if _chat_usage_hud_enabled(session) else "off"
|
|
506
|
+
console.print(f"Usage HUD: {state}")
|
|
507
|
+
return "handled"
|
|
508
|
+
parsed = _parse_bool_text(usage_arg)
|
|
509
|
+
if parsed is None:
|
|
510
|
+
console.print("[yellow]Usage:[/yellow] /usage hud on|off|status")
|
|
511
|
+
return "handled"
|
|
512
|
+
_set_chat_usage_hud_enabled(session, parsed)
|
|
513
|
+
console.print(f"Usage HUD set for this session: {'on' if parsed else 'off'}")
|
|
514
|
+
return "handled"
|
|
515
|
+
if cmd in {"/toolbar"}:
|
|
516
|
+
cfg_obj = getattr(session, "cfg", None)
|
|
517
|
+
raw_items = getattr(cfg_obj, "toolbar_items", None) if cfg_obj is not None else None
|
|
518
|
+
active_items: list[str] = []
|
|
519
|
+
seen_items: set[str] = set()
|
|
520
|
+
for raw_item in list(raw_items or list(_DEFAULT_TOOLBAR_ITEMS)):
|
|
521
|
+
item = str(raw_item).strip().lower()
|
|
522
|
+
if not item or item in seen_items or item not in _VALID_TOOLBAR_ITEMS:
|
|
523
|
+
continue
|
|
524
|
+
seen_items.add(item)
|
|
525
|
+
active_items.append(item)
|
|
526
|
+
active_set = set(active_items)
|
|
527
|
+
available_items = [item for item in _CHAT_TOOLBAR_ITEM_ORDER if item not in active_set]
|
|
528
|
+
valid_items_display = ", ".join(_CHAT_TOOLBAR_ITEM_ORDER)
|
|
529
|
+
|
|
530
|
+
toolbar_arg = arg.strip()
|
|
531
|
+
if not toolbar_arg:
|
|
532
|
+
active_display = ", ".join(active_items) if active_items else "(none)"
|
|
533
|
+
available_display = ", ".join(available_items) if available_items else "(none)"
|
|
534
|
+
lines = [
|
|
535
|
+
f"Active: {active_display}",
|
|
536
|
+
f"Available: {available_display}",
|
|
537
|
+
"",
|
|
538
|
+
"/toolbar add <item> add an item",
|
|
539
|
+
"/toolbar remove <item> remove an item",
|
|
540
|
+
"/toolbar reset restore defaults",
|
|
541
|
+
"/toolbar save persist to config",
|
|
542
|
+
]
|
|
543
|
+
console.print(
|
|
544
|
+
_Panel("\n".join(lines), title="Toolbar Items", border_style="bright_black")
|
|
545
|
+
)
|
|
546
|
+
return "handled"
|
|
547
|
+
|
|
548
|
+
toolbar_parts = toolbar_arg.split(maxsplit=1)
|
|
549
|
+
action = toolbar_parts[0].lower()
|
|
550
|
+
item_arg = toolbar_parts[1].strip().lower() if len(toolbar_parts) > 1 else ""
|
|
551
|
+
|
|
552
|
+
if action == "add":
|
|
553
|
+
if not item_arg:
|
|
554
|
+
console.print("[yellow]Usage:[/yellow] /toolbar add <item>")
|
|
555
|
+
return "handled"
|
|
556
|
+
if item_arg not in _VALID_TOOLBAR_ITEMS:
|
|
557
|
+
console.print(
|
|
558
|
+
f"[red]Invalid toolbar item:[/red] {item_arg}. "
|
|
559
|
+
f"Valid items: {valid_items_display}"
|
|
560
|
+
)
|
|
561
|
+
return "handled"
|
|
562
|
+
if item_arg in active_set:
|
|
563
|
+
console.print(f"Toolbar item already active: {item_arg}")
|
|
564
|
+
return "handled"
|
|
565
|
+
if not isinstance(cfg_obj, AppConfig):
|
|
566
|
+
console.print("[red]Toolbar settings unavailable:[/red] missing session config.")
|
|
567
|
+
return "handled"
|
|
568
|
+
cfg_obj.toolbar_items = [*active_items, item_arg]
|
|
569
|
+
console.print(f"Toolbar item added for this session: {item_arg}")
|
|
570
|
+
return "handled"
|
|
571
|
+
|
|
572
|
+
if action == "remove":
|
|
573
|
+
if not item_arg:
|
|
574
|
+
console.print("[yellow]Usage:[/yellow] /toolbar remove <item>")
|
|
575
|
+
return "handled"
|
|
576
|
+
if item_arg not in _VALID_TOOLBAR_ITEMS:
|
|
577
|
+
console.print(
|
|
578
|
+
f"[red]Invalid toolbar item:[/red] {item_arg}. "
|
|
579
|
+
f"Valid items: {valid_items_display}"
|
|
580
|
+
)
|
|
581
|
+
return "handled"
|
|
582
|
+
if item_arg not in active_set:
|
|
583
|
+
console.print(f"Toolbar item is not active: {item_arg}")
|
|
584
|
+
return "handled"
|
|
585
|
+
if not isinstance(cfg_obj, AppConfig):
|
|
586
|
+
console.print("[red]Toolbar settings unavailable:[/red] missing session config.")
|
|
587
|
+
return "handled"
|
|
588
|
+
cfg_obj.toolbar_items = [item for item in active_items if item != item_arg]
|
|
589
|
+
console.print(f"Toolbar item removed for this session: {item_arg}")
|
|
590
|
+
return "handled"
|
|
591
|
+
|
|
592
|
+
if action == "reset":
|
|
593
|
+
if not isinstance(cfg_obj, AppConfig):
|
|
594
|
+
console.print("[red]Toolbar settings unavailable:[/red] missing session config.")
|
|
595
|
+
return "handled"
|
|
596
|
+
cfg_obj.toolbar_items = list(_DEFAULT_TOOLBAR_ITEMS)
|
|
597
|
+
console.print(
|
|
598
|
+
"Toolbar items reset for this session: "
|
|
599
|
+
+ ", ".join(str(item) for item in cfg_obj.toolbar_items)
|
|
600
|
+
)
|
|
601
|
+
return "handled"
|
|
602
|
+
|
|
603
|
+
if action == "save":
|
|
604
|
+
if not isinstance(cfg_obj, AppConfig):
|
|
605
|
+
console.print("[red]Toolbar settings unavailable:[/red] missing session config.")
|
|
606
|
+
return "handled"
|
|
607
|
+
save_config(cfg_obj)
|
|
608
|
+
console.print(f"Saved toolbar settings: {config_path()}")
|
|
609
|
+
return "handled"
|
|
610
|
+
|
|
611
|
+
console.print("[yellow]Usage:[/yellow] /toolbar [add|remove|reset|save]")
|
|
612
|
+
return "handled"
|
|
613
|
+
if cmd == "/skill":
|
|
614
|
+
if _is_forge_ui_mode(forge_state.ui_mode):
|
|
615
|
+
console.print("Skills are unavailable in Forge.")
|
|
616
|
+
return "handled"
|
|
617
|
+
raw_skill_arg = arg.strip()
|
|
618
|
+
enabled, ordered_skills, issues = _session_skill_listing(session)
|
|
619
|
+
if not raw_skill_arg:
|
|
620
|
+
if ordered_skills:
|
|
621
|
+
console.print(
|
|
622
|
+
_skills_table(skills=ordered_skills, title=f"Skills ({len(ordered_skills)})")
|
|
623
|
+
)
|
|
624
|
+
elif enabled:
|
|
625
|
+
console.print("No skills discovered in the supported skill roots.")
|
|
626
|
+
else:
|
|
627
|
+
console.print("Skills are disabled for this session config.")
|
|
628
|
+
for issue in issues:
|
|
629
|
+
source_path = getattr(issue, "source_path", None)
|
|
630
|
+
message = str(getattr(issue, "message", "") or "").strip()
|
|
631
|
+
if source_path is not None and message:
|
|
632
|
+
console.print(f"[yellow]Skipped skill:[/yellow] {source_path} ({message})")
|
|
633
|
+
return "handled"
|
|
634
|
+
registry_obj = getattr(session, "skill_registry", None)
|
|
635
|
+
registry = registry_obj if isinstance(registry_obj, dict) else {}
|
|
636
|
+
if not registry and ordered_skills:
|
|
637
|
+
registry = {
|
|
638
|
+
str(getattr(skill, "name", "")).casefold(): skill for skill in ordered_skills
|
|
639
|
+
}
|
|
640
|
+
parts_skill = raw_skill_arg.split(maxsplit=1)
|
|
641
|
+
skill_name = parts_skill[0].strip()
|
|
642
|
+
skill = resolve_skill_by_name(registry, skill_name)
|
|
643
|
+
catalog_entries_obj = getattr(session, "skill_catalog_entries", ())
|
|
644
|
+
catalog_entries = (
|
|
645
|
+
tuple(item for item in catalog_entries_obj if item is not None)
|
|
646
|
+
if isinstance(catalog_entries_obj, tuple)
|
|
647
|
+
else tuple(item for item in (catalog_entries_obj or []) if item is not None)
|
|
648
|
+
)
|
|
649
|
+
catalog_entry = resolve_skill_catalog_entry(entries=catalog_entries, raw_name=skill_name)
|
|
650
|
+
if skill is None:
|
|
651
|
+
console.print(f"[red]Skill not found:[/red] {skill_name}")
|
|
652
|
+
if ordered_skills:
|
|
653
|
+
console.print(
|
|
654
|
+
"Available skills: "
|
|
655
|
+
+ ", ".join(str(getattr(skill_obj, "name", "")) for skill_obj in ordered_skills)
|
|
656
|
+
)
|
|
657
|
+
return "handled"
|
|
658
|
+
if len(parts_skill) == 1 or not parts_skill[1].strip():
|
|
659
|
+
console.print(render_skill_info_text(skill, catalog_entry=catalog_entry))
|
|
660
|
+
return "handled"
|
|
661
|
+
if not enabled:
|
|
662
|
+
console.print("Skills are disabled for this session config.")
|
|
663
|
+
return "handled"
|
|
664
|
+
skill_task = parts_skill[1].strip()
|
|
665
|
+
return _ChatExecutionRequest(
|
|
666
|
+
instruction=skill_task,
|
|
667
|
+
ephemeral_user_messages=(
|
|
668
|
+
build_explicit_skill_context_message(skill=skill, task_text=skill_task),
|
|
669
|
+
),
|
|
670
|
+
)
|
|
671
|
+
if cmd in {"/context", "/ctx"}:
|
|
672
|
+
_print_chat_context(console=console, session=session)
|
|
673
|
+
return "handled"
|
|
674
|
+
if cmd in {"/report", "/feedback"}:
|
|
675
|
+
cfg = (
|
|
676
|
+
getattr(session, "cfg", None)
|
|
677
|
+
if isinstance(getattr(session, "cfg", None), AppConfig)
|
|
678
|
+
else None
|
|
679
|
+
)
|
|
680
|
+
try:
|
|
681
|
+
result = create_feedback_bundle(
|
|
682
|
+
workspace_root=root,
|
|
683
|
+
feedback_text=arg or None,
|
|
684
|
+
cfg=cfg,
|
|
685
|
+
active_session=session,
|
|
686
|
+
active_run_paths=(
|
|
687
|
+
forge_state.paths if _is_forge_ui_mode(forge_state.ui_mode) else None
|
|
688
|
+
),
|
|
689
|
+
pending_images=list(pending_images),
|
|
690
|
+
)
|
|
691
|
+
except (ConfigError, FeedbackReportError) as exc:
|
|
692
|
+
console.print(f"[red]Feedback report failed:[/red] {exc}")
|
|
693
|
+
return "handled"
|
|
694
|
+
console.print(f"Feedback bundle directory: {result.bundle_dir}")
|
|
695
|
+
console.print(f"Feedback bundle archive: {result.zip_path}")
|
|
696
|
+
try:
|
|
697
|
+
issue_result = create_feedback_github_issue_draft(
|
|
698
|
+
bundle_result=result,
|
|
699
|
+
feedback_text=arg or None,
|
|
700
|
+
cfg=cfg,
|
|
701
|
+
)
|
|
702
|
+
except Exception as exc: # noqa: BLE001 - GitHub issue drafting is best-effort.
|
|
703
|
+
console.print(f"[yellow]GitHub issue draft skipped:[/yellow] {exc}")
|
|
704
|
+
else:
|
|
705
|
+
for line in feedback_github_issue_status_lines(issue_result):
|
|
706
|
+
console.print(line)
|
|
707
|
+
return "handled"
|
|
708
|
+
if cmd in {"/compact"}:
|
|
709
|
+
if _is_forge_ui_mode(forge_state.ui_mode):
|
|
710
|
+
console.print("Compaction is disabled in Forge.")
|
|
711
|
+
return "handled"
|
|
712
|
+
compactor = getattr(session, "conversation_compactor", None)
|
|
713
|
+
if compactor is None:
|
|
714
|
+
console.print("Compaction unavailable for this session (disabled or not supported).")
|
|
715
|
+
return "handled"
|
|
716
|
+
focus = arg.strip() or None
|
|
717
|
+
messages_obj = getattr(session, "messages", [])
|
|
718
|
+
messages = messages_obj if isinstance(messages_obj, list) else []
|
|
719
|
+
tool_list_obj = getattr(session, "tool_list", None)
|
|
720
|
+
tool_list = tool_list_obj if isinstance(tool_list_obj, list) else None
|
|
721
|
+
tool_list = effective_tools_for_client(getattr(session, "client", None), tool_list)
|
|
722
|
+
tokens_before = _estimate_request_tokens(messages, tool_list)
|
|
723
|
+
state = getattr(compactor, "state", None)
|
|
724
|
+
chunks_before = int(getattr(state, "history_chunk_index", 0) or 0)
|
|
725
|
+
pins_before = len(getattr(state, "pins", []) or [])
|
|
726
|
+
compact_fn = getattr(compactor, "compact_now", None)
|
|
727
|
+
if not callable(compact_fn):
|
|
728
|
+
console.print("Compaction unavailable for this session (missing compact_now).")
|
|
729
|
+
return "handled"
|
|
730
|
+
refresh_calibration = getattr(session, "refresh_compactor_calibration_filters", None)
|
|
731
|
+
if callable(refresh_calibration):
|
|
732
|
+
refresh_calibration()
|
|
733
|
+
new_messages, changed = compact_fn(
|
|
734
|
+
messages=messages,
|
|
735
|
+
tool_list=tool_list,
|
|
736
|
+
main_model=str(getattr(getattr(session, "client", None), "model", "") or ""),
|
|
737
|
+
cache_policy=getattr(
|
|
738
|
+
getattr(session, "client", None),
|
|
739
|
+
"prompt_cache_policy_metadata",
|
|
740
|
+
None,
|
|
741
|
+
),
|
|
742
|
+
focus=focus,
|
|
743
|
+
)
|
|
744
|
+
if isinstance(new_messages, list):
|
|
745
|
+
session.messages = new_messages
|
|
746
|
+
if changed:
|
|
747
|
+
invalidate_request_context = getattr(session, "invalidate_request_context", None)
|
|
748
|
+
if callable(invalidate_request_context):
|
|
749
|
+
invalidate_request_context(reason="manual_compaction")
|
|
750
|
+
elif hasattr(session, "request_context_measurement"):
|
|
751
|
+
session.request_context_measurement = None
|
|
752
|
+
tokens_after = _estimate_request_tokens(getattr(session, "messages", []), tool_list)
|
|
753
|
+
chunks_after = int(
|
|
754
|
+
getattr(getattr(compactor, "state", None), "history_chunk_index", 0) or 0
|
|
755
|
+
)
|
|
756
|
+
pins_after = len(getattr(getattr(compactor, "state", None), "pins", []) or [])
|
|
757
|
+
|
|
758
|
+
root = Path(getattr(session, "root", Path(".")))
|
|
759
|
+
store_obj = getattr(session, "store", None)
|
|
760
|
+
session_id = str(getattr(store_obj, "session_id", "") or "")
|
|
761
|
+
history_artifact_base = None
|
|
762
|
+
if hasattr(compactor, "history_dir"):
|
|
763
|
+
history_dir_obj = getattr(compactor, "history_dir", None)
|
|
764
|
+
if isinstance(history_dir_obj, Path):
|
|
765
|
+
history_artifact_base = history_dir_obj.parent
|
|
766
|
+
if history_artifact_base is None and session_id:
|
|
767
|
+
history_artifact_base = root / ".alysis" / "sessions" / _safe_component(session_id)
|
|
768
|
+
tool_output_artifact_base = getattr(store_obj, "session_artifact_root", None)
|
|
769
|
+
|
|
770
|
+
table = _Table(title="Compaction")
|
|
771
|
+
table.add_column("field")
|
|
772
|
+
table.add_column("value")
|
|
773
|
+
table.add_row("focus", focus or "-")
|
|
774
|
+
table.add_row("main_model", str(getattr(getattr(session, "client", None), "model", "-")))
|
|
775
|
+
table.add_row(
|
|
776
|
+
"compactor_model",
|
|
777
|
+
str(getattr(getattr(compactor, "compactor_client", None), "model", "-")),
|
|
778
|
+
)
|
|
779
|
+
table.add_row("compaction_profile", str(getattr(compactor, "profile_name", "-")))
|
|
780
|
+
table.add_row("tokens_before", str(tokens_before))
|
|
781
|
+
table.add_row("tokens_after", str(tokens_after))
|
|
782
|
+
table.add_row("tokens_delta", str(tokens_after - tokens_before))
|
|
783
|
+
table.add_row("chunks_created", str(max(0, chunks_after - chunks_before)))
|
|
784
|
+
table.add_row("pins", f"{pins_after} (delta {pins_after - pins_before:+d})")
|
|
785
|
+
table.add_row(
|
|
786
|
+
"history_artifacts_dir",
|
|
787
|
+
_artifact_display_ref(
|
|
788
|
+
root=root,
|
|
789
|
+
store_obj=store_obj,
|
|
790
|
+
artifact_path=history_artifact_base,
|
|
791
|
+
),
|
|
792
|
+
)
|
|
793
|
+
table.add_row(
|
|
794
|
+
"tool_output_artifacts_dir",
|
|
795
|
+
_artifact_display_ref(
|
|
796
|
+
root=root,
|
|
797
|
+
store_obj=store_obj,
|
|
798
|
+
artifact_path=(
|
|
799
|
+
tool_output_artifact_base / "tool_outputs"
|
|
800
|
+
if isinstance(tool_output_artifact_base, Path)
|
|
801
|
+
else None
|
|
802
|
+
),
|
|
803
|
+
),
|
|
804
|
+
)
|
|
805
|
+
console.print(table)
|
|
806
|
+
_refresh_chat_hud_context_cache(session)
|
|
807
|
+
if not changed:
|
|
808
|
+
console.print("Nothing to compact (no eligible history beyond recent window).")
|
|
809
|
+
return "handled"
|
|
810
|
+
if cmd in {"/model-info"}:
|
|
811
|
+
model_name = arg or str(getattr(getattr(session, "client", None), "model", "")).strip()
|
|
812
|
+
if not model_name:
|
|
813
|
+
console.print("Model info unavailable (missing model name).")
|
|
814
|
+
return "handled"
|
|
815
|
+
registry = getattr(session, "model_registry", None)
|
|
816
|
+
if registry is None:
|
|
817
|
+
console.print("Model info unavailable for this session.")
|
|
818
|
+
return "handled"
|
|
819
|
+
meta = registry.get(model_name)
|
|
820
|
+
table = _Table(title=f"Model Info ({model_name})")
|
|
821
|
+
table.add_column("field")
|
|
822
|
+
table.add_column("value")
|
|
823
|
+
field_sources = getattr(meta, "field_sources", {}) or {}
|
|
824
|
+
table.add_row("resolved_model", meta.model_name)
|
|
825
|
+
table.add_row("source", meta.source)
|
|
826
|
+
table.add_row(
|
|
827
|
+
"context_window_tokens",
|
|
828
|
+
(
|
|
829
|
+
f"{meta.context_window_tokens} "
|
|
830
|
+
f"({field_sources.get('context_window_tokens', 'unknown')})"
|
|
831
|
+
),
|
|
832
|
+
)
|
|
833
|
+
table.add_row(
|
|
834
|
+
"max_output_tokens",
|
|
835
|
+
f"{meta.max_output_tokens} ({field_sources.get('max_output_tokens', 'unknown')})",
|
|
836
|
+
)
|
|
837
|
+
table.add_row(
|
|
838
|
+
"supports_vision",
|
|
839
|
+
f"{meta.supports_vision} ({field_sources.get('supports_vision', 'unknown')})",
|
|
840
|
+
)
|
|
841
|
+
table.add_row(
|
|
842
|
+
"input_cost_per_token",
|
|
843
|
+
f"{meta.input_cost_per_token} ({field_sources.get('input_cost_per_token', 'unknown')})",
|
|
844
|
+
)
|
|
845
|
+
table.add_row(
|
|
846
|
+
"output_cost_per_token",
|
|
847
|
+
(
|
|
848
|
+
f"{meta.output_cost_per_token} "
|
|
849
|
+
f"({field_sources.get('output_cost_per_token', 'unknown')})"
|
|
850
|
+
),
|
|
851
|
+
)
|
|
852
|
+
table.add_row("registry_last_error", str(getattr(registry, "last_error", None)))
|
|
853
|
+
table.add_row("warnings", "; ".join(getattr(meta, "warnings", ())[:5]) or "-")
|
|
854
|
+
for key, value in _bundled_catalog_provenance_rows(meta=meta, registry=registry):
|
|
855
|
+
table.add_row(key, value)
|
|
856
|
+
raw_keys = sorted(meta.raw_metadata.keys())
|
|
857
|
+
preview_keys = ", ".join(raw_keys[:20]) if raw_keys else "(none)"
|
|
858
|
+
if len(raw_keys) > 20:
|
|
859
|
+
preview_keys += ", ..."
|
|
860
|
+
table.add_row("raw_metadata_keys", preview_keys)
|
|
861
|
+
console.print(table)
|
|
862
|
+
return "handled"
|
|
863
|
+
if cmd in {"/config"}:
|
|
864
|
+
if not arg:
|
|
865
|
+
if _is_non_interactive_terminal():
|
|
866
|
+
console.print(_chat_config_panel(session=session))
|
|
867
|
+
return "handled"
|
|
868
|
+
from ..config_menu import run_config_menu
|
|
869
|
+
from .loop import _ConfigReloadRequiresRestart
|
|
870
|
+
|
|
871
|
+
result = run_config_menu()
|
|
872
|
+
if not result.saved:
|
|
873
|
+
console.print("Closed without saving.")
|
|
874
|
+
return "handled"
|
|
875
|
+
change_count = len(result.changes) + (1 if result.api_key_changed else 0)
|
|
876
|
+
previous_cfg = getattr(session, "cfg", None)
|
|
877
|
+
try:
|
|
878
|
+
from ...profiles import connection_fingerprint
|
|
879
|
+
|
|
880
|
+
reloaded = load_config()
|
|
881
|
+
if previous_cfg is None or connection_fingerprint(
|
|
882
|
+
reloaded
|
|
883
|
+
) != connection_fingerprint(previous_cfg):
|
|
884
|
+
console.print(
|
|
885
|
+
"[yellow]Model access changed. This session is closing so the old and "
|
|
886
|
+
"new connection states are never mixed. Start `alysis chat` again.[/yellow]"
|
|
887
|
+
)
|
|
888
|
+
return "exit"
|
|
889
|
+
_apply_config_menu_changes_to_session(session=session, cfg=reloaded)
|
|
890
|
+
except _ConfigReloadRequiresRestart as e:
|
|
891
|
+
console.print(
|
|
892
|
+
f"[yellow]Config saved. {e} This session is closing; start "
|
|
893
|
+
"`alysis chat` again.[/yellow]"
|
|
894
|
+
)
|
|
895
|
+
return "exit"
|
|
896
|
+
except Exception as e: # noqa: BLE001
|
|
897
|
+
console.print(f"[red]Config saved, but session reload failed:[/red] {e}")
|
|
898
|
+
return "handled"
|
|
899
|
+
if change_count > 0:
|
|
900
|
+
change_word = "change" if change_count == 1 else "changes"
|
|
901
|
+
console.print(
|
|
902
|
+
f"Saved {change_count} {change_word}. New settings apply on the next user turn."
|
|
903
|
+
)
|
|
904
|
+
else:
|
|
905
|
+
console.print("Config saved (no changes).")
|
|
906
|
+
return "handled"
|
|
907
|
+
if arg.lower() in {"show", "list", "help"}:
|
|
908
|
+
console.print(_chat_config_panel(session=session))
|
|
909
|
+
return "handled"
|
|
910
|
+
parts_config = arg.split()
|
|
911
|
+
action = parts_config[0].lower()
|
|
912
|
+
if action == "set":
|
|
913
|
+
config_key = parts_config[1].lower() if len(parts_config) >= 2 else ""
|
|
914
|
+
if config_key in {"model", "base_url"} and len(parts_config) == 3:
|
|
915
|
+
from ... import config as config_mod
|
|
916
|
+
|
|
917
|
+
try:
|
|
918
|
+
cfg_to_save = config_mod.load_config()
|
|
919
|
+
from ...profiles import connection_fingerprint
|
|
920
|
+
|
|
921
|
+
config_mod.ensure_subscription_menu_managed_key(cfg_to_save, config_key)
|
|
922
|
+
previous_fingerprint = connection_fingerprint(cfg_to_save)
|
|
923
|
+
config_mod.set_config_value(cfg_to_save, config_key, parts_config[2])
|
|
924
|
+
config_mod.save_config(cfg_to_save)
|
|
925
|
+
if connection_fingerprint(cfg_to_save) != previous_fingerprint:
|
|
926
|
+
console.print(
|
|
927
|
+
"[yellow]Model access changed. This session is closing so the old "
|
|
928
|
+
"and new connection states are never mixed. Start `alysis chat` "
|
|
929
|
+
"again.[/yellow]"
|
|
930
|
+
)
|
|
931
|
+
return "exit"
|
|
932
|
+
_apply_config_menu_changes_to_session(session=session, cfg=cfg_to_save)
|
|
933
|
+
except Exception as e: # noqa: BLE001
|
|
934
|
+
console.print(f"[red]Config update failed:[/red] {e}")
|
|
935
|
+
return "handled"
|
|
936
|
+
console.print(f"Saved {config_key}. New setting applies on the next user turn.")
|
|
937
|
+
return "handled"
|
|
938
|
+
if len(parts_config) < 4 or len(parts_config) > 7:
|
|
939
|
+
_print_chat_config_usage(console=console)
|
|
940
|
+
return "handled"
|
|
941
|
+
model_name, model_error = _resolve_chat_model_ref(
|
|
942
|
+
session=session,
|
|
943
|
+
raw_model_ref=parts_config[1],
|
|
944
|
+
)
|
|
945
|
+
if model_error is not None or model_name is None:
|
|
946
|
+
console.print(f"[red]{model_error or 'Invalid model reference.'}[/red]")
|
|
947
|
+
return "handled"
|
|
948
|
+
|
|
949
|
+
context_window_tokens = parse_positive_int(parts_config[2])
|
|
950
|
+
if context_window_tokens is None:
|
|
951
|
+
console.print("[red]Invalid context_window_tokens.[/red] Use a positive integer.")
|
|
952
|
+
return "handled"
|
|
953
|
+
|
|
954
|
+
max_output_tokens = parse_positive_int(parts_config[3])
|
|
955
|
+
if max_output_tokens is None:
|
|
956
|
+
console.print("[red]Invalid max_output_tokens.[/red] Use a positive integer.")
|
|
957
|
+
return "handled"
|
|
958
|
+
|
|
959
|
+
updates: dict[str, Any] = {
|
|
960
|
+
"context_window_tokens": context_window_tokens,
|
|
961
|
+
"max_output_tokens": max_output_tokens,
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if len(parts_config) >= 5:
|
|
965
|
+
supports_vision = _parse_bool_text(parts_config[4])
|
|
966
|
+
if supports_vision is None:
|
|
967
|
+
console.print("[red]Invalid supports_vision.[/red] Use true/false.")
|
|
968
|
+
return "handled"
|
|
969
|
+
updates["supports_vision"] = supports_vision
|
|
970
|
+
|
|
971
|
+
if len(parts_config) >= 6:
|
|
972
|
+
input_cost = parse_non_negative_float(parts_config[5])
|
|
973
|
+
if input_cost is None:
|
|
974
|
+
console.print(
|
|
975
|
+
"[red]Invalid input_cost_per_token.[/red] Use a non-negative number."
|
|
976
|
+
)
|
|
977
|
+
return "handled"
|
|
978
|
+
updates["input_cost_per_token"] = input_cost
|
|
979
|
+
|
|
980
|
+
if len(parts_config) >= 7:
|
|
981
|
+
output_cost = parse_non_negative_float(parts_config[6])
|
|
982
|
+
if output_cost is None:
|
|
983
|
+
console.print(
|
|
984
|
+
"[red]Invalid output_cost_per_token.[/red] Use a non-negative number."
|
|
985
|
+
)
|
|
986
|
+
return "handled"
|
|
987
|
+
updates["output_cost_per_token"] = output_cost
|
|
988
|
+
|
|
989
|
+
try:
|
|
990
|
+
saved_path, merged = _save_chat_model_metadata_override(
|
|
991
|
+
session=session,
|
|
992
|
+
model_name=model_name,
|
|
993
|
+
fields=updates,
|
|
994
|
+
)
|
|
995
|
+
except ConfigError as e:
|
|
996
|
+
console.print(f"[red]Config error:[/red] {e}")
|
|
997
|
+
return "handled"
|
|
998
|
+
_refresh_chat_hud_context_cache(session)
|
|
999
|
+
console.print(f"Saved model metadata override for {model_name}.")
|
|
1000
|
+
console.print(
|
|
1001
|
+
"context_window_tokens="
|
|
1002
|
+
f"{merged.get('context_window_tokens')} | max_output_tokens="
|
|
1003
|
+
f"{merged.get('max_output_tokens')} | supports_vision="
|
|
1004
|
+
f"{merged.get('supports_vision', 'unset')}"
|
|
1005
|
+
)
|
|
1006
|
+
console.print(f"Config: {saved_path}")
|
|
1007
|
+
return "handled"
|
|
1008
|
+
|
|
1009
|
+
if action in {"clear", "rm", "delete"}:
|
|
1010
|
+
if len(parts_config) != 2:
|
|
1011
|
+
_print_chat_config_usage(console=console)
|
|
1012
|
+
return "handled"
|
|
1013
|
+
model_name, model_error = _resolve_chat_model_ref(
|
|
1014
|
+
session=session,
|
|
1015
|
+
raw_model_ref=parts_config[1],
|
|
1016
|
+
)
|
|
1017
|
+
if model_error is not None or model_name is None:
|
|
1018
|
+
console.print(f"[red]{model_error or 'Invalid model reference.'}[/red]")
|
|
1019
|
+
return "handled"
|
|
1020
|
+
try:
|
|
1021
|
+
saved_path, cleared = _clear_chat_model_metadata_override(
|
|
1022
|
+
session=session,
|
|
1023
|
+
model_name=model_name,
|
|
1024
|
+
)
|
|
1025
|
+
except ConfigError as e:
|
|
1026
|
+
console.print(f"[red]Config error:[/red] {e}")
|
|
1027
|
+
return "handled"
|
|
1028
|
+
if not cleared:
|
|
1029
|
+
console.print(f"No stored override found for {model_name}.")
|
|
1030
|
+
return "handled"
|
|
1031
|
+
_refresh_chat_hud_context_cache(session)
|
|
1032
|
+
console.print(f"Cleared model metadata override for {model_name}.")
|
|
1033
|
+
console.print(f"Config: {saved_path}")
|
|
1034
|
+
return "handled"
|
|
1035
|
+
|
|
1036
|
+
console.print(
|
|
1037
|
+
"[yellow]Unknown /config action.[/yellow] Use /config, /config set, /config clear."
|
|
1038
|
+
)
|
|
1039
|
+
return "handled"
|
|
1040
|
+
if cmd in {"/assets"}:
|
|
1041
|
+
try:
|
|
1042
|
+
run_paths = load_current_run_paths(Path(resolve_session_active_workdir_path(session)))
|
|
1043
|
+
except ForgeError:
|
|
1044
|
+
console.print(
|
|
1045
|
+
"No forge run is active for this workspace. Use /forge plan to start one."
|
|
1046
|
+
)
|
|
1047
|
+
return "handled"
|
|
1048
|
+
_open_assets_modal(session=session, console=console, run_paths=run_paths)
|
|
1049
|
+
return "handled"
|
|
1050
|
+
if cmd in {"/mode"}:
|
|
1051
|
+
current_mode = str(getattr(session, "mode", "review")).strip().lower()
|
|
1052
|
+
next_mode: str | None
|
|
1053
|
+
if not arg:
|
|
1054
|
+
next_mode, picker_available = _select_chat_mode_interactive(
|
|
1055
|
+
current_mode=current_mode,
|
|
1056
|
+
console=console,
|
|
1057
|
+
)
|
|
1058
|
+
if not picker_available:
|
|
1059
|
+
console.print(_chat_mode_panel(current_mode=current_mode))
|
|
1060
|
+
return "handled"
|
|
1061
|
+
if next_mode is None:
|
|
1062
|
+
return "handled"
|
|
1063
|
+
else:
|
|
1064
|
+
if persona_modes_enabled(getattr(session, "cfg", None)) and is_persona_name(
|
|
1065
|
+
arg.strip().lower(), getattr(session, "persona_registry", None)
|
|
1066
|
+
):
|
|
1067
|
+
# Personas have their own command; keep the two vocabularies
|
|
1068
|
+
# from blurring back together.
|
|
1069
|
+
console.print(f"Personas have their own command: /persona {arg.strip().lower()}")
|
|
1070
|
+
return "handled"
|
|
1071
|
+
next_mode = _resolve_chat_mode_alias(arg)
|
|
1072
|
+
if next_mode is None or next_mode not in _CHAT_MODES:
|
|
1073
|
+
console.print(
|
|
1074
|
+
"[red]Invalid mode.[/red] Try: /mode 1, /mode 2, /mode 3, /mode 4 "
|
|
1075
|
+
"(or safe, fast, read, full)."
|
|
1076
|
+
)
|
|
1077
|
+
return "handled"
|
|
1078
|
+
if _chat_plan_mode_enabled(plan_mode_state):
|
|
1079
|
+
if next_mode == "readonly":
|
|
1080
|
+
console.print("Mode already set: Read-Only (Plan Mode is on)")
|
|
1081
|
+
return "handled"
|
|
1082
|
+
console.print(
|
|
1083
|
+
"Cannot change execution mode while Plan Mode is on. Use /plan off first."
|
|
1084
|
+
)
|
|
1085
|
+
return "handled"
|
|
1086
|
+
if next_mode == current_mode:
|
|
1087
|
+
console.print(f"Mode already set: {_chat_mode_display(next_mode)}")
|
|
1088
|
+
if next_mode == "fullaccess":
|
|
1089
|
+
_print_fullaccess_mode_warning(console=console)
|
|
1090
|
+
return "handled"
|
|
1091
|
+
try:
|
|
1092
|
+
# An explicit execution-mode choice is the user redefining their
|
|
1093
|
+
# base: restore any persona-narrowed write scope BEFORE the
|
|
1094
|
+
# rebuild so the new tool surface reflects the user's own scope.
|
|
1095
|
+
_clear_persona_restore(session)
|
|
1096
|
+
_apply_chat_effective_mode(
|
|
1097
|
+
session=session,
|
|
1098
|
+
next_mode=next_mode,
|
|
1099
|
+
persist_default_mode=True,
|
|
1100
|
+
)
|
|
1101
|
+
except Exception as e: # noqa: BLE001
|
|
1102
|
+
console.print(f"[red]Failed to change mode:[/red] {e}")
|
|
1103
|
+
return "handled"
|
|
1104
|
+
console.print(f"Mode set for this session: {_chat_mode_display(next_mode)}")
|
|
1105
|
+
if next_mode == "fullaccess":
|
|
1106
|
+
_print_fullaccess_mode_warning(console=console)
|
|
1107
|
+
return "handled"
|
|
1108
|
+
if cmd in {"/persona"}:
|
|
1109
|
+
if not persona_modes_enabled(getattr(session, "cfg", None)):
|
|
1110
|
+
console.print(
|
|
1111
|
+
"Persona modes are disabled (persona_modes_enabled=false or "
|
|
1112
|
+
"ALYSIS_PERSONA_MODES=off)."
|
|
1113
|
+
)
|
|
1114
|
+
return "handled"
|
|
1115
|
+
persona_registry = getattr(session, "persona_registry", None)
|
|
1116
|
+
current_persona = normalize_persona(getattr(session, "persona", "code"), persona_registry)
|
|
1117
|
+
persona_selection: str | None
|
|
1118
|
+
if not arg:
|
|
1119
|
+
persona_selection, picker_available = _select_chat_persona_interactive(
|
|
1120
|
+
current_persona=current_persona,
|
|
1121
|
+
console=console,
|
|
1122
|
+
custom=persona_registry,
|
|
1123
|
+
)
|
|
1124
|
+
if not picker_available:
|
|
1125
|
+
console.print(
|
|
1126
|
+
_chat_persona_panel(current_persona=current_persona, custom=persona_registry)
|
|
1127
|
+
)
|
|
1128
|
+
return "handled"
|
|
1129
|
+
if persona_selection is None:
|
|
1130
|
+
return "handled"
|
|
1131
|
+
else:
|
|
1132
|
+
persona_selection = arg.strip().lower()
|
|
1133
|
+
if not is_persona_name(persona_selection, persona_registry):
|
|
1134
|
+
console.print(
|
|
1135
|
+
"[red]Invalid persona.[/red] Try: /persona code, architect, ask, debug"
|
|
1136
|
+
" — or a custom persona from .alysis_personas."
|
|
1137
|
+
)
|
|
1138
|
+
return "handled"
|
|
1139
|
+
# A persona is a convention layered on the gate: the clamp inside
|
|
1140
|
+
# _apply_chat_persona can only keep or lower the execution mode, so
|
|
1141
|
+
# this path needs no fullaccess warning and never persists
|
|
1142
|
+
# default_mode.
|
|
1143
|
+
if _chat_plan_mode_enabled(plan_mode_state):
|
|
1144
|
+
console.print("Cannot change persona while Plan Mode is on. Use /plan off first.")
|
|
1145
|
+
return "handled"
|
|
1146
|
+
persona_name = normalize_persona(persona_selection, persona_registry)
|
|
1147
|
+
if persona_name == current_persona:
|
|
1148
|
+
console.print(f"Persona already set: {persona_name}")
|
|
1149
|
+
return "handled"
|
|
1150
|
+
try:
|
|
1151
|
+
_apply_chat_persona(
|
|
1152
|
+
session=session,
|
|
1153
|
+
persona=persona_name,
|
|
1154
|
+
source="user",
|
|
1155
|
+
)
|
|
1156
|
+
except Exception as e: # noqa: BLE001
|
|
1157
|
+
console.print(f"[red]Failed to change persona:[/red] {e}")
|
|
1158
|
+
return "handled"
|
|
1159
|
+
console.print(f"Persona set for this session: {persona_name}")
|
|
1160
|
+
return "handled"
|
|
1161
|
+
if cmd in {"/plan"}:
|
|
1162
|
+
if _is_forge_ui_mode(forge_state.ui_mode):
|
|
1163
|
+
_print_forge_plan_command_guidance(console=console)
|
|
1164
|
+
return "handled"
|
|
1165
|
+
raw_plan_arg = arg.strip()
|
|
1166
|
+
plan_action, plan_task = _parse_chat_plan_command(raw_plan_arg)
|
|
1167
|
+
if plan_action == "draft":
|
|
1168
|
+
if _chat_plan_mode_enabled(plan_mode_state):
|
|
1169
|
+
for line in _chat_plan_draft_blocked_by_mode_lines(
|
|
1170
|
+
plan_mode_escape_supported=plan_mode_escape_supported
|
|
1171
|
+
):
|
|
1172
|
+
console.print(line)
|
|
1173
|
+
return "handled"
|
|
1174
|
+
current_mode = str(getattr(session, "mode", "review")).strip().lower()
|
|
1175
|
+
if current_mode == "readonly":
|
|
1176
|
+
for line in _chat_plan_readonly_mode_guidance_lines():
|
|
1177
|
+
console.print(line)
|
|
1178
|
+
return "handled"
|
|
1179
|
+
if not plan_task:
|
|
1180
|
+
try:
|
|
1181
|
+
plan_task = typer.prompt("Plan task", default="").strip()
|
|
1182
|
+
except KeyboardInterrupt:
|
|
1183
|
+
console.print("")
|
|
1184
|
+
return "handled"
|
|
1185
|
+
except (EOFError, typer.Abort):
|
|
1186
|
+
# No interactive stdin available (the TUI feeds an empty/EOF
|
|
1187
|
+
# stdin so interactive prompts cancel cleanly). typer.prompt
|
|
1188
|
+
# raises click.Abort here, whose str() is empty -- if it
|
|
1189
|
+
# escaped it surfaced as a bare "Command error:" with no
|
|
1190
|
+
# detail. Fall through to the usage hint below instead.
|
|
1191
|
+
plan_task = ""
|
|
1192
|
+
if not plan_task:
|
|
1193
|
+
for line in _chat_plan_usage_lines():
|
|
1194
|
+
console.print(line)
|
|
1195
|
+
return "handled"
|
|
1196
|
+
approved_instruction = _run_plan_mode_approval_loop(
|
|
1197
|
+
session=session,
|
|
1198
|
+
console=console,
|
|
1199
|
+
user_message=plan_task,
|
|
1200
|
+
action_prompt=plan_mode_action_prompt,
|
|
1201
|
+
)
|
|
1202
|
+
if approved_instruction is None:
|
|
1203
|
+
return "handled"
|
|
1204
|
+
console.print("")
|
|
1205
|
+
return _ChatExecutionRequest(instruction=approved_instruction)
|
|
1206
|
+
if plan_action == "status":
|
|
1207
|
+
if plan_task:
|
|
1208
|
+
for line in _chat_plan_usage_lines():
|
|
1209
|
+
console.print(line)
|
|
1210
|
+
return "handled"
|
|
1211
|
+
_render_chat_plan_mode_status(console=console, plan_mode_state=plan_mode_state)
|
|
1212
|
+
return "handled"
|
|
1213
|
+
if plan_action == "approve":
|
|
1214
|
+
if plan_task:
|
|
1215
|
+
for line in _chat_plan_usage_lines():
|
|
1216
|
+
console.print(line)
|
|
1217
|
+
return "handled"
|
|
1218
|
+
if not _chat_plan_mode_enabled(plan_mode_state):
|
|
1219
|
+
console.print(
|
|
1220
|
+
"Plan Mode is off. Use /plan <task> for the default draft/review/approve flow that can execute after approval."
|
|
1221
|
+
)
|
|
1222
|
+
console.print(
|
|
1223
|
+
"Use /plan mode only if you explicitly want secondary readonly planning chat."
|
|
1224
|
+
)
|
|
1225
|
+
return "handled"
|
|
1226
|
+
latest_task = _chat_plan_mode_latest_task(plan_mode_state)
|
|
1227
|
+
latest_draft = _chat_plan_mode_latest_draft(plan_mode_state)
|
|
1228
|
+
if latest_task is None or latest_draft is None:
|
|
1229
|
+
for line in _plan_mode_no_stored_draft_lines():
|
|
1230
|
+
console.print(line)
|
|
1231
|
+
if plan_mode_escape_supported:
|
|
1232
|
+
console.print("Press Esc at an empty prompt to leave interactively.")
|
|
1233
|
+
return "handled"
|
|
1234
|
+
restore_mode = _chat_plan_mode_restore_mode(plan_mode_state) or "readonly"
|
|
1235
|
+
if restore_mode == "readonly":
|
|
1236
|
+
for line in _plan_mode_readonly_origin_lines():
|
|
1237
|
+
console.print(line)
|
|
1238
|
+
if plan_mode_escape_supported:
|
|
1239
|
+
console.print("Press Esc at an empty prompt to leave interactively.")
|
|
1240
|
+
return "handled"
|
|
1241
|
+
approved_instruction = instruction_with_approved_plan(
|
|
1242
|
+
user_message=latest_task,
|
|
1243
|
+
approved_plan=latest_draft,
|
|
1244
|
+
)
|
|
1245
|
+
restored = _disable_chat_plan_mode(
|
|
1246
|
+
session=session,
|
|
1247
|
+
console=console,
|
|
1248
|
+
plan_mode_state=plan_mode_state,
|
|
1249
|
+
clear_draft=True,
|
|
1250
|
+
)
|
|
1251
|
+
if restored is None:
|
|
1252
|
+
return "handled"
|
|
1253
|
+
console.print(
|
|
1254
|
+
f"Executing latest stored Plan Mode draft for: {_chat_plan_task_preview(latest_task)}"
|
|
1255
|
+
)
|
|
1256
|
+
return _ChatExecutionRequest(instruction=approved_instruction)
|
|
1257
|
+
if plan_action in {"mode", "readonly", "on"}:
|
|
1258
|
+
if plan_task:
|
|
1259
|
+
for line in _chat_plan_usage_lines():
|
|
1260
|
+
console.print(line)
|
|
1261
|
+
return "handled"
|
|
1262
|
+
parsed = True
|
|
1263
|
+
elif plan_action == "off":
|
|
1264
|
+
if plan_task:
|
|
1265
|
+
for line in _chat_plan_usage_lines():
|
|
1266
|
+
console.print(line)
|
|
1267
|
+
return "handled"
|
|
1268
|
+
parsed = False
|
|
1269
|
+
else:
|
|
1270
|
+
for line in _chat_plan_usage_lines():
|
|
1271
|
+
console.print(line)
|
|
1272
|
+
return "handled"
|
|
1273
|
+
current_mode = str(getattr(session, "mode", "review")).strip().lower() or "review"
|
|
1274
|
+
if parsed:
|
|
1275
|
+
if _chat_plan_mode_enabled(plan_mode_state):
|
|
1276
|
+
console.print(
|
|
1277
|
+
_chat_plan_already_on_message(
|
|
1278
|
+
plan_mode_escape_supported=plan_mode_escape_supported
|
|
1279
|
+
)
|
|
1280
|
+
)
|
|
1281
|
+
return "handled"
|
|
1282
|
+
_clear_chat_plan_mode_draft_state(plan_mode_state)
|
|
1283
|
+
plan_mode_state.enabled = True
|
|
1284
|
+
plan_mode_state.restore_mode = current_mode
|
|
1285
|
+
if current_mode != "readonly":
|
|
1286
|
+
try:
|
|
1287
|
+
_apply_chat_effective_mode(
|
|
1288
|
+
session=session,
|
|
1289
|
+
next_mode="readonly",
|
|
1290
|
+
persist_default_mode=False,
|
|
1291
|
+
)
|
|
1292
|
+
except Exception as e: # noqa: BLE001
|
|
1293
|
+
plan_mode_state.enabled = False
|
|
1294
|
+
plan_mode_state.restore_mode = None
|
|
1295
|
+
console.print(f"[red]Failed to enable Plan Mode:[/red] {e}")
|
|
1296
|
+
return "handled"
|
|
1297
|
+
if plan_mode_escape_supported:
|
|
1298
|
+
console.print(
|
|
1299
|
+
"Plan Mode set for this session: on "
|
|
1300
|
+
"(persistent readonly planning overlay; no execution by itself; press Esc at an empty prompt or use /plan off to leave)"
|
|
1301
|
+
)
|
|
1302
|
+
else:
|
|
1303
|
+
console.print(
|
|
1304
|
+
"Plan Mode set for this session: on (persistent readonly planning overlay; no execution by itself)"
|
|
1305
|
+
)
|
|
1306
|
+
for line in _plan_mode_entry_guidance_lines(restore_mode=current_mode):
|
|
1307
|
+
console.print(line)
|
|
1308
|
+
return "handled"
|
|
1309
|
+
if not _chat_plan_mode_enabled(plan_mode_state):
|
|
1310
|
+
console.print("Plan Mode already off.")
|
|
1311
|
+
return "handled"
|
|
1312
|
+
_disable_chat_plan_mode(
|
|
1313
|
+
session=session,
|
|
1314
|
+
console=console,
|
|
1315
|
+
plan_mode_state=plan_mode_state,
|
|
1316
|
+
clear_draft=True,
|
|
1317
|
+
)
|
|
1318
|
+
return "handled"
|
|
1319
|
+
if cmd in {"/stream"}:
|
|
1320
|
+
stream_arg = arg.strip().lower()
|
|
1321
|
+
if not stream_arg or stream_arg == "status":
|
|
1322
|
+
enabled = bool(getattr(session, "stream", True))
|
|
1323
|
+
console.print(
|
|
1324
|
+
f"Streaming is {'on' if enabled else 'off'}. "
|
|
1325
|
+
"Use /stream on|off or bare /stream in the TUI."
|
|
1326
|
+
)
|
|
1327
|
+
return "handled"
|
|
1328
|
+
stream_value = _parse_bool_text(stream_arg)
|
|
1329
|
+
if stream_value is None:
|
|
1330
|
+
console.print(
|
|
1331
|
+
"[red]Invalid stream value.[/red] Try: /stream on, /stream off, or /stream status."
|
|
1332
|
+
)
|
|
1333
|
+
return "handled"
|
|
1334
|
+
_set_chat_stream_enabled(session=session, enabled=stream_value)
|
|
1335
|
+
console.print(f"Streaming set for this session: {'on' if stream_value else 'off'}")
|
|
1336
|
+
return "handled"
|
|
1337
|
+
if cmd in {"/trace"}:
|
|
1338
|
+
trace_arg = arg.strip()
|
|
1339
|
+
if not trace_arg:
|
|
1340
|
+
next_level, picker_available = _select_chat_trace_interactive(
|
|
1341
|
+
current_level=_chat_trace_level(session),
|
|
1342
|
+
console=console,
|
|
1343
|
+
)
|
|
1344
|
+
if not picker_available:
|
|
1345
|
+
level = _chat_trace_level(session)
|
|
1346
|
+
console.print(
|
|
1347
|
+
f"Reasoning trace: {level}. Usage: /trace off, /trace compact, or /trace full"
|
|
1348
|
+
)
|
|
1349
|
+
return "handled"
|
|
1350
|
+
if next_level is None:
|
|
1351
|
+
return "handled"
|
|
1352
|
+
else:
|
|
1353
|
+
next_level = _resolve_trace_level(trace_arg)
|
|
1354
|
+
if next_level is None:
|
|
1355
|
+
console.print(
|
|
1356
|
+
"[red]Invalid trace level.[/red] Try: /trace off, /trace compact, or /trace full."
|
|
1357
|
+
)
|
|
1358
|
+
return "handled"
|
|
1359
|
+
applied = _set_chat_trace_level(session=session, level=next_level)
|
|
1360
|
+
console.print(f"Reasoning trace set for this session: {applied}")
|
|
1361
|
+
return "handled"
|
|
1362
|
+
if cmd in {"/clear"}:
|
|
1363
|
+
if _is_forge_ui_mode(forge_state.ui_mode):
|
|
1364
|
+
console.print("Cannot /clear inside Forge. Use /back or /done first.")
|
|
1365
|
+
return "handled"
|
|
1366
|
+
try:
|
|
1367
|
+
_clear_chat_conversation(session=session, pending_images=pending_images)
|
|
1368
|
+
except Exception as e: # noqa: BLE001
|
|
1369
|
+
console.print(f"[red]Failed to clear conversation:[/red] {e}")
|
|
1370
|
+
return "handled"
|
|
1371
|
+
console.print("Conversation cleared.")
|
|
1372
|
+
return "handled"
|
|
1373
|
+
if cmd in {"/image"}:
|
|
1374
|
+
if not arg:
|
|
1375
|
+
try:
|
|
1376
|
+
saved = paste_clipboard_image(root=root, output_path=None)
|
|
1377
|
+
except ClipboardError as e:
|
|
1378
|
+
console.print(f"[red]Clipboard error:[/red] {e}")
|
|
1379
|
+
return "handled"
|
|
1380
|
+
pending_images.append(os.fspath(saved))
|
|
1381
|
+
console.print(f"Pasted clipboard image: {saved}")
|
|
1382
|
+
return "handled"
|
|
1383
|
+
pending_images.append(arg)
|
|
1384
|
+
console.print(f"Queued image: {arg}")
|
|
1385
|
+
return "handled"
|
|
1386
|
+
if cmd in {"/paste-image"}:
|
|
1387
|
+
out_path = arg or None
|
|
1388
|
+
try:
|
|
1389
|
+
saved = paste_clipboard_image(root=root, output_path=out_path)
|
|
1390
|
+
except ClipboardError as e:
|
|
1391
|
+
console.print(f"[red]Clipboard error:[/red] {e}")
|
|
1392
|
+
return "handled"
|
|
1393
|
+
pending_images.append(os.fspath(saved))
|
|
1394
|
+
console.print(f"Pasted clipboard image: {saved}")
|
|
1395
|
+
return "handled"
|
|
1396
|
+
if cmd in {"/images"}:
|
|
1397
|
+
if not pending_images:
|
|
1398
|
+
console.print("No queued images.")
|
|
1399
|
+
return "handled"
|
|
1400
|
+
for idx, img in enumerate(pending_images, start=1):
|
|
1401
|
+
console.print(f"{idx}. {img}")
|
|
1402
|
+
return "handled"
|
|
1403
|
+
if cmd in {"/clear-images"}:
|
|
1404
|
+
pending_images.clear()
|
|
1405
|
+
console.print("Cleared queued images.")
|
|
1406
|
+
return "handled"
|
|
1407
|
+
if cmd in {"/model"}:
|
|
1408
|
+
subscription_profile = False
|
|
1409
|
+
try:
|
|
1410
|
+
from ...profiles import get_active_profile
|
|
1411
|
+
|
|
1412
|
+
cfg = getattr(session, "cfg", None)
|
|
1413
|
+
profile = get_active_profile(cfg) if cfg is not None else None
|
|
1414
|
+
subscription_profile = bool(profile is not None and profile.auth_provider)
|
|
1415
|
+
except Exception:
|
|
1416
|
+
subscription_profile = False
|
|
1417
|
+
if not arg:
|
|
1418
|
+
model = getattr(getattr(session, "client", None), "model", "")
|
|
1419
|
+
console.print(f"Current model: {model}")
|
|
1420
|
+
if subscription_profile:
|
|
1421
|
+
console.print(
|
|
1422
|
+
"[dim]Open /config → Default Model to choose from your live "
|
|
1423
|
+
"subscription models and supported reasoning efforts.[/dim]"
|
|
1424
|
+
)
|
|
1425
|
+
else:
|
|
1426
|
+
_print_alysis_trial_models(console, session, current=str(model))
|
|
1427
|
+
return "handled"
|
|
1428
|
+
if subscription_profile:
|
|
1429
|
+
console.print(
|
|
1430
|
+
"[yellow]Subscription model changes are managed in /config so the model and "
|
|
1431
|
+
"reasoning effort stay compatible and persist together.[/yellow]"
|
|
1432
|
+
)
|
|
1433
|
+
return "handled"
|
|
1434
|
+
previous_cfg = getattr(session, "cfg", None)
|
|
1435
|
+
copy_cfg = getattr(previous_cfg, "model_copy", None)
|
|
1436
|
+
if previous_cfg is None or not callable(copy_cfg):
|
|
1437
|
+
console.print(
|
|
1438
|
+
"[yellow]Model was not changed because this session cannot safely refresh "
|
|
1439
|
+
"its provider route. Restart chat with the desired model.[/yellow]"
|
|
1440
|
+
)
|
|
1441
|
+
return "handled"
|
|
1442
|
+
client = getattr(session, "client", None)
|
|
1443
|
+
previous_client_model = getattr(client, "model", None)
|
|
1444
|
+
previous_route_identity = getattr(client, "route_identity", None)
|
|
1445
|
+
candidate_cfg = copy_cfg(update={"model": arg}, deep=True)
|
|
1446
|
+
try:
|
|
1447
|
+
from . import loop as _model_loop
|
|
1448
|
+
|
|
1449
|
+
_model_loop._apply_config_menu_changes_to_session(
|
|
1450
|
+
session=session,
|
|
1451
|
+
cfg=candidate_cfg,
|
|
1452
|
+
)
|
|
1453
|
+
except Exception as exc: # noqa: BLE001 - restore the verified route atomically
|
|
1454
|
+
session.cfg = previous_cfg
|
|
1455
|
+
if client is not None:
|
|
1456
|
+
client.model = previous_client_model
|
|
1457
|
+
if previous_route_identity is not None or hasattr(client, "route_identity"):
|
|
1458
|
+
client.route_identity = previous_route_identity
|
|
1459
|
+
console.print(
|
|
1460
|
+
f"[yellow]Model was not changed because provider capabilities could not be "
|
|
1461
|
+
f"refreshed: {exc}[/yellow]"
|
|
1462
|
+
)
|
|
1463
|
+
return "handled"
|
|
1464
|
+
_refresh_chat_hud_context_cache(session)
|
|
1465
|
+
console.print(f"Model set for this session: {arg}")
|
|
1466
|
+
return "handled"
|
|
1467
|
+
if cmd in {"/login"}:
|
|
1468
|
+
from ..commands.auth import login_connection_interactively, login_connection_rows
|
|
1469
|
+
|
|
1470
|
+
selected = str(arg or "").strip()
|
|
1471
|
+
if not selected:
|
|
1472
|
+
from ..config_menu import _run_config_picker
|
|
1473
|
+
|
|
1474
|
+
rows = login_connection_rows()
|
|
1475
|
+
selected = str(
|
|
1476
|
+
_run_config_picker(
|
|
1477
|
+
console=console,
|
|
1478
|
+
title="Log in",
|
|
1479
|
+
subtitle="Choose how Alysis Code should connect.",
|
|
1480
|
+
rows=rows,
|
|
1481
|
+
current_value=rows[0][0],
|
|
1482
|
+
)
|
|
1483
|
+
or ""
|
|
1484
|
+
).strip()
|
|
1485
|
+
if not selected:
|
|
1486
|
+
return "handled"
|
|
1487
|
+
previous_cfg = getattr(session, "cfg", None)
|
|
1488
|
+
try:
|
|
1489
|
+
login_connection_interactively(selected, console=console)
|
|
1490
|
+
except typer.Exit:
|
|
1491
|
+
return "handled"
|
|
1492
|
+
try:
|
|
1493
|
+
from ... import config as _login_config
|
|
1494
|
+
from ...profiles import connection_fingerprint
|
|
1495
|
+
from . import loop as _login_loop
|
|
1496
|
+
|
|
1497
|
+
reloaded_cfg = _login_config.load_config()
|
|
1498
|
+
if previous_cfg is None or connection_fingerprint(
|
|
1499
|
+
previous_cfg
|
|
1500
|
+
) != connection_fingerprint(reloaded_cfg):
|
|
1501
|
+
console.print(
|
|
1502
|
+
"[yellow]Connection saved. This session is closing so the provider "
|
|
1503
|
+
"transport, authentication, and trace capability can be rebuilt safely. "
|
|
1504
|
+
"Start `alysis chat` again.[/yellow]"
|
|
1505
|
+
)
|
|
1506
|
+
return "exit"
|
|
1507
|
+
_login_loop._apply_config_menu_changes_to_session(
|
|
1508
|
+
session=session,
|
|
1509
|
+
cfg=reloaded_cfg,
|
|
1510
|
+
)
|
|
1511
|
+
console.print("[green]The selected connection is active for this session.[/green]")
|
|
1512
|
+
except Exception as exc: # noqa: BLE001 - never keep a possibly stale transport alive
|
|
1513
|
+
console.print(
|
|
1514
|
+
f"[yellow]Connection saved, but live reload failed: {exc}. This session is "
|
|
1515
|
+
"closing; start `alysis chat` again.[/yellow]"
|
|
1516
|
+
)
|
|
1517
|
+
return "exit"
|
|
1518
|
+
return "handled"
|
|
1519
|
+
if cmd in {"/logout"}:
|
|
1520
|
+
from ...account_login import logout as _alysis_logout
|
|
1521
|
+
|
|
1522
|
+
cfg = getattr(session, "cfg", None)
|
|
1523
|
+
if cfg is not None and _alysis_logout(cfg):
|
|
1524
|
+
console.print(
|
|
1525
|
+
"[green]Logged out.[/green] Your Alysis Code key was revoked; "
|
|
1526
|
+
"hosted models will refuse further requests until you /login again."
|
|
1527
|
+
)
|
|
1528
|
+
else:
|
|
1529
|
+
console.print("You're not logged in to an Alysis Code account.")
|
|
1530
|
+
return "handled"
|
|
1531
|
+
|
|
1532
|
+
if cmd[:1] in "/:":
|
|
1533
|
+
suggestion = _suggest_chat_command(parts[0], ui_mode=forge_state.ui_mode)
|
|
1534
|
+
if suggestion:
|
|
1535
|
+
console.print(
|
|
1536
|
+
f"[yellow]Unknown command:[/yellow] {parts[0]}. "
|
|
1537
|
+
f"Did you mean {suggestion}? Try /help."
|
|
1538
|
+
)
|
|
1539
|
+
return "handled"
|
|
1540
|
+
console.print(f"[yellow]Unknown command:[/yellow] {parts[0]}. Try /help.")
|
|
1541
|
+
return "handled"
|
|
1542
|
+
|
|
1543
|
+
if not _is_forge_ui_mode(forge_state.ui_mode):
|
|
1544
|
+
navigation_request = _parse_chat_workdir_navigation_request(
|
|
1545
|
+
input_text=input_text,
|
|
1546
|
+
session=session,
|
|
1547
|
+
)
|
|
1548
|
+
if navigation_request is not None:
|
|
1549
|
+
if len(navigation_request) == 3:
|
|
1550
|
+
_requested_path, _trailing_instruction, error_message = navigation_request
|
|
1551
|
+
console.print(f"[red]Failed to change active workdir:[/red] {error_message}")
|
|
1552
|
+
return "handled"
|
|
1553
|
+
|
|
1554
|
+
requested_path, trailing_instruction = navigation_request
|
|
1555
|
+
try:
|
|
1556
|
+
result = set_session_active_workdir(
|
|
1557
|
+
session,
|
|
1558
|
+
requested_path,
|
|
1559
|
+
source="natural_language_chat",
|
|
1560
|
+
)
|
|
1561
|
+
except Exception as e: # noqa: BLE001
|
|
1562
|
+
console.print(f"[red]Failed to change active workdir:[/red] {e}")
|
|
1563
|
+
return "handled"
|
|
1564
|
+
console.print(
|
|
1565
|
+
f"Active workdir: {result['active_workdir']} ({result['active_workdir_relpath']})"
|
|
1566
|
+
)
|
|
1567
|
+
if not trailing_instruction:
|
|
1568
|
+
return "handled"
|
|
1569
|
+
if _chat_plan_mode_enabled(plan_mode_state):
|
|
1570
|
+
return _resolve_interactive_plan_mode_request(
|
|
1571
|
+
session=session,
|
|
1572
|
+
console=console,
|
|
1573
|
+
plan_mode_state=plan_mode_state,
|
|
1574
|
+
user_message=trailing_instruction,
|
|
1575
|
+
plan_mode_escape_supported=plan_mode_escape_supported,
|
|
1576
|
+
)
|
|
1577
|
+
return _ChatExecutionRequest(instruction=trailing_instruction)
|
|
1578
|
+
|
|
1579
|
+
return "send"
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
def _handle_terminals_command(*, arg: str, session: Any, console: Console) -> None:
|
|
1583
|
+
parts = arg.split()
|
|
1584
|
+
subcommand = parts[0].lower() if parts else "list"
|
|
1585
|
+
|
|
1586
|
+
if subcommand == "help":
|
|
1587
|
+
_print_terminals_usage(console=console)
|
|
1588
|
+
return
|
|
1589
|
+
|
|
1590
|
+
terminal_manager = getattr(session, "terminal_manager", None)
|
|
1591
|
+
if terminal_manager is None:
|
|
1592
|
+
console.print("Background terminals are unavailable in this session.")
|
|
1593
|
+
return
|
|
1594
|
+
|
|
1595
|
+
try:
|
|
1596
|
+
if subcommand == "list":
|
|
1597
|
+
if len(parts) > 1:
|
|
1598
|
+
console.print("Usage: /terminals list")
|
|
1599
|
+
return
|
|
1600
|
+
_print_terminals_list(console=console, terminal_manager=terminal_manager)
|
|
1601
|
+
return
|
|
1602
|
+
|
|
1603
|
+
if subcommand == "show":
|
|
1604
|
+
if len(parts) != 2:
|
|
1605
|
+
console.print("Usage: /terminals show <process_id>")
|
|
1606
|
+
return
|
|
1607
|
+
_print_terminals_show(
|
|
1608
|
+
console=console,
|
|
1609
|
+
terminal_manager=terminal_manager,
|
|
1610
|
+
process_id=parts[1],
|
|
1611
|
+
)
|
|
1612
|
+
return
|
|
1613
|
+
|
|
1614
|
+
if subcommand == "kill":
|
|
1615
|
+
if len(parts) != 2:
|
|
1616
|
+
console.print("Usage: /terminals kill <process_id>")
|
|
1617
|
+
return
|
|
1618
|
+
mode = str(getattr(session, "mode", "") or "").strip().lower()
|
|
1619
|
+
if mode == "readonly":
|
|
1620
|
+
console.print("Cannot kill processes in readonly mode.")
|
|
1621
|
+
return
|
|
1622
|
+
_print_terminals_kill(
|
|
1623
|
+
console=console,
|
|
1624
|
+
terminal_manager=terminal_manager,
|
|
1625
|
+
process_id=parts[1],
|
|
1626
|
+
)
|
|
1627
|
+
return
|
|
1628
|
+
|
|
1629
|
+
_print_terminals_usage(console=console)
|
|
1630
|
+
except Exception as exc: # noqa: BLE001
|
|
1631
|
+
# Slash commands should report failures without unwinding into the chat loop.
|
|
1632
|
+
_LOGGER.exception("Terminal slash command failed")
|
|
1633
|
+
console.print(f"[red]Background terminal command failed:[/red] {exc}")
|
|
1634
|
+
|
|
1635
|
+
|
|
1636
|
+
def _print_terminals_usage(*, console: Console) -> None:
|
|
1637
|
+
for line in _TERMINALS_USAGE_LINES:
|
|
1638
|
+
console.print(line)
|
|
1639
|
+
|
|
1640
|
+
|
|
1641
|
+
def _print_terminals_list(*, console: Console, terminal_manager: Any) -> None:
|
|
1642
|
+
summaries = terminal_manager.list()
|
|
1643
|
+
if not summaries:
|
|
1644
|
+
console.print("No background processes.")
|
|
1645
|
+
return
|
|
1646
|
+
|
|
1647
|
+
table = _Table(title="Background Terminals")
|
|
1648
|
+
table.add_column("process_id", no_wrap=True)
|
|
1649
|
+
table.add_column("cmd", overflow="fold")
|
|
1650
|
+
table.add_column("status", no_wrap=True)
|
|
1651
|
+
table.add_column("exit_code", no_wrap=True)
|
|
1652
|
+
table.add_column("runtime", no_wrap=True)
|
|
1653
|
+
for summary in summaries:
|
|
1654
|
+
table.add_row(
|
|
1655
|
+
Text(str(summary.process_id)),
|
|
1656
|
+
Text(str(summary.cmd)),
|
|
1657
|
+
Text(str(summary.status)),
|
|
1658
|
+
Text("-" if summary.exit_code is None else str(summary.exit_code)),
|
|
1659
|
+
Text(_format_terminal_runtime_s(float(summary.runtime_s))),
|
|
1660
|
+
)
|
|
1661
|
+
console.print(table)
|
|
1662
|
+
|
|
1663
|
+
|
|
1664
|
+
def _print_terminals_show(
|
|
1665
|
+
*,
|
|
1666
|
+
console: Console,
|
|
1667
|
+
terminal_manager: Any,
|
|
1668
|
+
process_id: str,
|
|
1669
|
+
) -> None:
|
|
1670
|
+
try:
|
|
1671
|
+
snapshot = terminal_manager.read(process_id, since=0)
|
|
1672
|
+
except KeyError:
|
|
1673
|
+
console.print(_terminal_text("No such background process: ", process_id))
|
|
1674
|
+
return
|
|
1675
|
+
|
|
1676
|
+
console.print(_terminal_text("process_id: ", snapshot.process_id))
|
|
1677
|
+
console.print(_terminal_text("status: ", snapshot.status))
|
|
1678
|
+
console.print(
|
|
1679
|
+
_terminal_text("exit_code: ", "-" if snapshot.exit_code is None else snapshot.exit_code)
|
|
1680
|
+
)
|
|
1681
|
+
if snapshot.failure_reason is not None:
|
|
1682
|
+
console.print(_terminal_text("failure_reason: ", snapshot.failure_reason))
|
|
1683
|
+
console.print(
|
|
1684
|
+
_terminal_text("runtime: ", _format_terminal_runtime_s(float(snapshot.runtime_s)))
|
|
1685
|
+
)
|
|
1686
|
+
console.print(_terminal_text("dropped_lines: ", snapshot.dropped_lines))
|
|
1687
|
+
|
|
1688
|
+
lines = list(snapshot.lines)
|
|
1689
|
+
if len(lines) > _TERMINALS_DISPLAY_LINE_LIMIT:
|
|
1690
|
+
console.print("… (older lines truncated)")
|
|
1691
|
+
lines = lines[-_TERMINALS_DISPLAY_LINE_LIMIT:]
|
|
1692
|
+
if not lines:
|
|
1693
|
+
console.print("No output.")
|
|
1694
|
+
return
|
|
1695
|
+
for line in lines:
|
|
1696
|
+
text = str(line.text).rstrip("\r\n")
|
|
1697
|
+
console.print(_terminal_text(f"{line.seq} {line.stream}: ", text))
|
|
1698
|
+
|
|
1699
|
+
|
|
1700
|
+
def _print_terminals_kill(
|
|
1701
|
+
*,
|
|
1702
|
+
console: Console,
|
|
1703
|
+
terminal_manager: Any,
|
|
1704
|
+
process_id: str,
|
|
1705
|
+
) -> None:
|
|
1706
|
+
try:
|
|
1707
|
+
snapshot = terminal_manager.kill(process_id)
|
|
1708
|
+
except KeyError:
|
|
1709
|
+
console.print(_terminal_text("No such background process: ", process_id))
|
|
1710
|
+
return
|
|
1711
|
+
code = "-" if snapshot.exit_code is None else str(snapshot.exit_code)
|
|
1712
|
+
console.print(
|
|
1713
|
+
Text.assemble(
|
|
1714
|
+
("Killed ",),
|
|
1715
|
+
(process_id,),
|
|
1716
|
+
(" (status=",),
|
|
1717
|
+
(snapshot.status,),
|
|
1718
|
+
(", exit_code=",),
|
|
1719
|
+
(code,),
|
|
1720
|
+
(")",),
|
|
1721
|
+
)
|
|
1722
|
+
)
|
|
1723
|
+
|
|
1724
|
+
|
|
1725
|
+
def _terminal_text(prefix: str, value: object) -> Text:
|
|
1726
|
+
return Text.assemble((prefix,), (str(value),))
|
|
1727
|
+
|
|
1728
|
+
|
|
1729
|
+
def _format_terminal_runtime_s(runtime_s: float) -> str:
|
|
1730
|
+
if runtime_s < 0:
|
|
1731
|
+
runtime_s = 0.0
|
|
1732
|
+
if runtime_s < 60:
|
|
1733
|
+
return f"{runtime_s:.1f}s"
|
|
1734
|
+
minutes = int(runtime_s // 60)
|
|
1735
|
+
seconds = int(runtime_s % 60)
|
|
1736
|
+
return f"{minutes}m{seconds:02d}s"
|
|
1737
|
+
|
|
1738
|
+
|
|
1739
|
+
def _handle_forge_chat_command(
|
|
1740
|
+
*,
|
|
1741
|
+
input_text: str,
|
|
1742
|
+
forge_state: _ForgeChatState,
|
|
1743
|
+
session: Any,
|
|
1744
|
+
console: Console,
|
|
1745
|
+
forge_planner_reply_sink: Any | None = None,
|
|
1746
|
+
forge_execution_report_sink: Any | None = None,
|
|
1747
|
+
) -> str:
|
|
1748
|
+
paths = forge_state.paths
|
|
1749
|
+
plan = forge_state.plan
|
|
1750
|
+
if paths is None or plan is None:
|
|
1751
|
+
_print_forge_warning_messages(
|
|
1752
|
+
console=console,
|
|
1753
|
+
label="Forge",
|
|
1754
|
+
warnings=["Forge state missing; returning to chat mode."],
|
|
1755
|
+
)
|
|
1756
|
+
forge_state.ui_mode = "chat"
|
|
1757
|
+
return "handled"
|
|
1758
|
+
|
|
1759
|
+
trimmed = input_text.strip()
|
|
1760
|
+
if not trimmed:
|
|
1761
|
+
return "handled"
|
|
1762
|
+
|
|
1763
|
+
parts = trimmed.split(maxsplit=1)
|
|
1764
|
+
cmd = parts[0].lower()
|
|
1765
|
+
arg = parts[1].strip() if len(parts) > 1 else ""
|
|
1766
|
+
|
|
1767
|
+
if cmd in {"/back"}:
|
|
1768
|
+
forge_state.ui_mode = "chat"
|
|
1769
|
+
append_transcript_note(paths, role="system", message="Exited Forge with /back.")
|
|
1770
|
+
_print_forge_meta(
|
|
1771
|
+
console=console,
|
|
1772
|
+
message="Left Forge without the final save/validate step. Back to chat.",
|
|
1773
|
+
)
|
|
1774
|
+
return "handled"
|
|
1775
|
+
|
|
1776
|
+
if cmd in {"/done", "done"}:
|
|
1777
|
+
append_transcript_note(paths, role="user", message=trimmed)
|
|
1778
|
+
append_transcript_note(paths, role="system", message="Forge finalized with /done.")
|
|
1779
|
+
_finalize_forge_plan(
|
|
1780
|
+
console=console,
|
|
1781
|
+
paths=paths,
|
|
1782
|
+
plan=plan,
|
|
1783
|
+
transcript_tail=forge_state.planner_transcript,
|
|
1784
|
+
)
|
|
1785
|
+
forge_state.ui_mode = "chat"
|
|
1786
|
+
_print_forge_meta(
|
|
1787
|
+
console=console,
|
|
1788
|
+
message="Saved and validated the plan. Back to chat.",
|
|
1789
|
+
)
|
|
1790
|
+
return "handled"
|
|
1791
|
+
|
|
1792
|
+
if cmd in {"/help"}:
|
|
1793
|
+
console.print(_chat_help_panel(ui_mode="forge"))
|
|
1794
|
+
return "handled"
|
|
1795
|
+
|
|
1796
|
+
if cmd in {"/show"}:
|
|
1797
|
+
_show_forge_plan_summary(console=console, paths=paths, plan=plan)
|
|
1798
|
+
return "handled"
|
|
1799
|
+
|
|
1800
|
+
if cmd in {"/assets"}:
|
|
1801
|
+
_open_assets_modal(session=session, console=console, run_paths=paths)
|
|
1802
|
+
return "handled"
|
|
1803
|
+
|
|
1804
|
+
if cmd in {"/plan"}:
|
|
1805
|
+
append_transcript_note(paths, role="user", message=trimmed)
|
|
1806
|
+
plan_cmd = arg.strip().lower()
|
|
1807
|
+
if not plan_cmd:
|
|
1808
|
+
plan_cmd, picker_available = _select_forge_plan_command_interactive(console=console)
|
|
1809
|
+
if not picker_available:
|
|
1810
|
+
_print_forge_warning_messages(
|
|
1811
|
+
console=console,
|
|
1812
|
+
label="Usage",
|
|
1813
|
+
warnings=["/plan tasks|table|markdown|md|edit"],
|
|
1814
|
+
)
|
|
1815
|
+
return "handled"
|
|
1816
|
+
if plan_cmd is None:
|
|
1817
|
+
return "handled"
|
|
1818
|
+
|
|
1819
|
+
if plan_cmd in {"view", "tasks", "table"}:
|
|
1820
|
+
_show_forge_plan_summary(console=console, paths=paths, plan=plan)
|
|
1821
|
+
return "handled"
|
|
1822
|
+
if plan_cmd in {"markdown", "md"}:
|
|
1823
|
+
_show_forge_plan_markdown(console=console, paths=paths, plan=plan)
|
|
1824
|
+
return "handled"
|
|
1825
|
+
if plan_cmd in {"edit", "edit-json"}:
|
|
1826
|
+
_edit_forge_plan_json(console=console, paths=paths, forge_state=forge_state)
|
|
1827
|
+
updated_plan = forge_state.plan
|
|
1828
|
+
if isinstance(updated_plan, dict):
|
|
1829
|
+
plan = updated_plan
|
|
1830
|
+
return "handled"
|
|
1831
|
+
append_transcript_note(
|
|
1832
|
+
paths,
|
|
1833
|
+
role="system",
|
|
1834
|
+
message="Rejected invalid Forge /plan usage.",
|
|
1835
|
+
)
|
|
1836
|
+
_print_forge_plan_command_guidance(console=console)
|
|
1837
|
+
return "handled"
|
|
1838
|
+
|
|
1839
|
+
if cmd in {"/execute"}:
|
|
1840
|
+
forge_state.swarm_run_attempted = False
|
|
1841
|
+
if arg.strip().lower() != "plan":
|
|
1842
|
+
append_transcript_note(paths, role="user", message=trimmed)
|
|
1843
|
+
append_transcript_note(
|
|
1844
|
+
paths,
|
|
1845
|
+
role="system",
|
|
1846
|
+
message="Rejected invalid /execute usage.",
|
|
1847
|
+
)
|
|
1848
|
+
_print_forge_warning_messages(
|
|
1849
|
+
console=console,
|
|
1850
|
+
label="Usage",
|
|
1851
|
+
warnings=["/execute plan"],
|
|
1852
|
+
)
|
|
1853
|
+
return "handled"
|
|
1854
|
+
|
|
1855
|
+
if not _forge_has_usable_plan_input(plan):
|
|
1856
|
+
_print_forge_warning_messages(
|
|
1857
|
+
console=console,
|
|
1858
|
+
label="Forge",
|
|
1859
|
+
warnings=[
|
|
1860
|
+
"Plan is empty. Add requirements or tasks first (paste text, /task, or /plan edit)."
|
|
1861
|
+
],
|
|
1862
|
+
)
|
|
1863
|
+
append_transcript_note(paths, role="user", message=trimmed)
|
|
1864
|
+
append_transcript_note(
|
|
1865
|
+
paths,
|
|
1866
|
+
role="system",
|
|
1867
|
+
message="Rejected /execute plan because plan had no requirements/tasks.",
|
|
1868
|
+
)
|
|
1869
|
+
return "handled"
|
|
1870
|
+
|
|
1871
|
+
try:
|
|
1872
|
+
run_mutation_guard = acquire_swarm_mutation_guard(
|
|
1873
|
+
paths,
|
|
1874
|
+
mode="forge_swarm:chat",
|
|
1875
|
+
on_wait=lambda info: _print_forge_lock_wait_notice(console, info),
|
|
1876
|
+
)
|
|
1877
|
+
except ForgeError as e:
|
|
1878
|
+
_print_forge_error(
|
|
1879
|
+
console=console,
|
|
1880
|
+
message=f"Forge execution failed: {e}",
|
|
1881
|
+
)
|
|
1882
|
+
return "handled"
|
|
1883
|
+
|
|
1884
|
+
try:
|
|
1885
|
+
append_transcript_note(paths, role="user", message=trimmed)
|
|
1886
|
+
|
|
1887
|
+
if bool(getattr(run_mutation_guard, "acquired_after_wait", False)):
|
|
1888
|
+
plan.clear()
|
|
1889
|
+
plan.update(load_plan(paths))
|
|
1890
|
+
workspace_context = _workspace_context_payload_for_paths(
|
|
1891
|
+
paths=paths,
|
|
1892
|
+
refresh_if_stale=True,
|
|
1893
|
+
)
|
|
1894
|
+
finalize_plan(plan)
|
|
1895
|
+
reconciliation_result, workspace_context = _reconcile_plan_for_paths(
|
|
1896
|
+
paths=paths,
|
|
1897
|
+
plan=plan,
|
|
1898
|
+
refresh_if_stale=False,
|
|
1899
|
+
transcript_tail=forge_state.planner_transcript,
|
|
1900
|
+
)
|
|
1901
|
+
save_plan(paths, plan)
|
|
1902
|
+
validation_warnings = _validate_forge_plan_for_paths(paths, plan)
|
|
1903
|
+
if reconciliation_result.warnings:
|
|
1904
|
+
_print_forge_warning_messages(
|
|
1905
|
+
console=console,
|
|
1906
|
+
label="Plan reconciliation",
|
|
1907
|
+
warnings=list(reconciliation_result.warnings),
|
|
1908
|
+
)
|
|
1909
|
+
for warning in reconciliation_result.warnings:
|
|
1910
|
+
append_transcript_note(
|
|
1911
|
+
paths,
|
|
1912
|
+
role="system",
|
|
1913
|
+
message=f"Plan reconciliation warning: {warning}",
|
|
1914
|
+
)
|
|
1915
|
+
_write_plan_validation_artifact(
|
|
1916
|
+
paths=paths,
|
|
1917
|
+
reconciliation_result=reconciliation_result,
|
|
1918
|
+
validation_warnings=validation_warnings,
|
|
1919
|
+
)
|
|
1920
|
+
no_execution_ready_tasks_message = _forge_no_execution_ready_tasks_message(plan)
|
|
1921
|
+
if no_execution_ready_tasks_message is not None:
|
|
1922
|
+
_print_forge_error(
|
|
1923
|
+
console=console,
|
|
1924
|
+
message=no_execution_ready_tasks_message,
|
|
1925
|
+
)
|
|
1926
|
+
append_transcript_note(
|
|
1927
|
+
paths,
|
|
1928
|
+
role="system",
|
|
1929
|
+
message=f"Rejected /execute plan: {no_execution_ready_tasks_message}",
|
|
1930
|
+
)
|
|
1931
|
+
return "handled"
|
|
1932
|
+
|
|
1933
|
+
enrich_attempted = False
|
|
1934
|
+
enrich_applied = False
|
|
1935
|
+
enrich_summary = "enrichment not needed"
|
|
1936
|
+
show_enrichment_details = _chat_trace_level(session) == "full"
|
|
1937
|
+
if _forge_should_try_enrichment(validation_warnings):
|
|
1938
|
+
if not _forge_enrich_plan_enabled():
|
|
1939
|
+
enrich_summary = "enrichment skipped: disabled"
|
|
1940
|
+
else:
|
|
1941
|
+
planner_api_key = str(
|
|
1942
|
+
getattr(getattr(session, "client", None), "api_key", "") or ""
|
|
1943
|
+
).strip()
|
|
1944
|
+
session_cfg = getattr(session, "cfg", None)
|
|
1945
|
+
if planner_api_key and isinstance(session_cfg, AppConfig):
|
|
1946
|
+
enrich_attempted = True
|
|
1947
|
+
enrichment_user_text = (
|
|
1948
|
+
"Enrich the existing plan for execution: do not ask questions; only fill "
|
|
1949
|
+
"missing acceptance_criteria and estimated_files via tasks_update; do not "
|
|
1950
|
+
"change project_goal, summary, requirements, tasks_add, or tasks_remove."
|
|
1951
|
+
)
|
|
1952
|
+
planner_knowledge = prepare_planner_knowledge(
|
|
1953
|
+
paths=paths,
|
|
1954
|
+
plan=plan,
|
|
1955
|
+
user_text=enrichment_user_text,
|
|
1956
|
+
selection_label="planner_enrichment",
|
|
1957
|
+
)
|
|
1958
|
+
planner_workspace_root = resolve_knowledge_workspace_root(paths)
|
|
1959
|
+
planner_result = run_planner_turn(
|
|
1960
|
+
cfg=clone_cfg(session_cfg),
|
|
1961
|
+
api_key_override=planner_api_key,
|
|
1962
|
+
plan=plan,
|
|
1963
|
+
transcript_tail=forge_state.planner_transcript,
|
|
1964
|
+
workspace_context=workspace_context,
|
|
1965
|
+
user_text=enrichment_user_text,
|
|
1966
|
+
relevant_knowledge_section=planner_knowledge.render_prompt_section(
|
|
1967
|
+
workspace_root=planner_workspace_root
|
|
1968
|
+
),
|
|
1969
|
+
run_paths=paths,
|
|
1970
|
+
)
|
|
1971
|
+
_merge_usage_payloads_into_session(
|
|
1972
|
+
session=session,
|
|
1973
|
+
payloads=list(getattr(planner_result, "usage_events", []) or []),
|
|
1974
|
+
)
|
|
1975
|
+
request_retry_count = int(
|
|
1976
|
+
getattr(planner_result, "request_retry_count", 0) or 0
|
|
1977
|
+
)
|
|
1978
|
+
if request_retry_count > 0 and not planner_result.error:
|
|
1979
|
+
retry_word = "retry" if request_retry_count == 1 else "retries"
|
|
1980
|
+
retry_notice = (
|
|
1981
|
+
f"Planner request recovered after {request_retry_count} transient "
|
|
1982
|
+
f"{retry_word}."
|
|
1983
|
+
)
|
|
1984
|
+
if show_enrichment_details:
|
|
1985
|
+
_print_forge_warning_messages(
|
|
1986
|
+
console=console,
|
|
1987
|
+
label="Plan enrichment",
|
|
1988
|
+
warnings=[retry_notice],
|
|
1989
|
+
)
|
|
1990
|
+
if show_enrichment_details:
|
|
1991
|
+
append_transcript_note(
|
|
1992
|
+
paths,
|
|
1993
|
+
role="system",
|
|
1994
|
+
message=f"Plan enrichment warning: {retry_notice}",
|
|
1995
|
+
)
|
|
1996
|
+
if planner_result.error:
|
|
1997
|
+
retry_word = "retry" if request_retry_count == 1 else "retries"
|
|
1998
|
+
enrichment_error_summary = (
|
|
1999
|
+
f"enrichment skipped after {request_retry_count} transient "
|
|
2000
|
+
f"{retry_word}: {planner_result.error}"
|
|
2001
|
+
if request_retry_count > 0
|
|
2002
|
+
else f"enrichment skipped: {planner_result.error}"
|
|
2003
|
+
)
|
|
2004
|
+
enrichment_error_warnings = (
|
|
2005
|
+
[
|
|
2006
|
+
f"Planner request failed after {request_retry_count} transient "
|
|
2007
|
+
f"{retry_word}."
|
|
2008
|
+
]
|
|
2009
|
+
if request_retry_count > 0
|
|
2010
|
+
else []
|
|
2011
|
+
)
|
|
2012
|
+
enrichment_error_warnings.append(
|
|
2013
|
+
f"Final planner error: {planner_result.error}"
|
|
2014
|
+
)
|
|
2015
|
+
_print_forge_warning_messages(
|
|
2016
|
+
console=console,
|
|
2017
|
+
label="Plan enrichment",
|
|
2018
|
+
warnings=enrichment_error_warnings,
|
|
2019
|
+
)
|
|
2020
|
+
append_transcript_note(
|
|
2021
|
+
paths,
|
|
2022
|
+
role="system",
|
|
2023
|
+
message=(
|
|
2024
|
+
f"Plan enrichment error after {request_retry_count} transient "
|
|
2025
|
+
f"{retry_word}: {planner_result.error}"
|
|
2026
|
+
if request_retry_count > 0
|
|
2027
|
+
else f"Plan enrichment error: {planner_result.error}"
|
|
2028
|
+
),
|
|
2029
|
+
)
|
|
2030
|
+
enrich_summary = enrichment_error_summary
|
|
2031
|
+
elif planner_result.plan_update:
|
|
2032
|
+
sanitized_enrichment = _sanitize_forge_enrichment_plan_update(
|
|
2033
|
+
plan=plan,
|
|
2034
|
+
plan_update=planner_result.plan_update,
|
|
2035
|
+
)
|
|
2036
|
+
if sanitized_enrichment.warnings:
|
|
2037
|
+
if show_enrichment_details:
|
|
2038
|
+
_print_forge_warning_messages(
|
|
2039
|
+
console=console,
|
|
2040
|
+
label="Plan enrichment",
|
|
2041
|
+
warnings=list(sanitized_enrichment.warnings),
|
|
2042
|
+
)
|
|
2043
|
+
if show_enrichment_details:
|
|
2044
|
+
for warning in sanitized_enrichment.warnings:
|
|
2045
|
+
append_transcript_note(
|
|
2046
|
+
paths,
|
|
2047
|
+
role="system",
|
|
2048
|
+
message=f"Plan enrichment warning: {warning}",
|
|
2049
|
+
)
|
|
2050
|
+
if sanitized_enrichment.plan_update is None:
|
|
2051
|
+
enrich_summary = "enrichment produced no applicable changes"
|
|
2052
|
+
else:
|
|
2053
|
+
apply_result = apply_guarded_planner_plan_update(
|
|
2054
|
+
plan,
|
|
2055
|
+
sanitized_enrichment.plan_update,
|
|
2056
|
+
latest_user_text=enrichment_user_text,
|
|
2057
|
+
workspace_context=workspace_context,
|
|
2058
|
+
)
|
|
2059
|
+
reconciliation_target_ids = list(
|
|
2060
|
+
dict.fromkeys(
|
|
2061
|
+
[
|
|
2062
|
+
*apply_result.added_task_ids,
|
|
2063
|
+
*apply_result.updated_task_ids,
|
|
2064
|
+
]
|
|
2065
|
+
)
|
|
2066
|
+
)
|
|
2067
|
+
if apply_result.warnings:
|
|
2068
|
+
if show_enrichment_details:
|
|
2069
|
+
_print_forge_warning_messages(
|
|
2070
|
+
console=console,
|
|
2071
|
+
label="Plan enrichment",
|
|
2072
|
+
warnings=list(apply_result.warnings),
|
|
2073
|
+
)
|
|
2074
|
+
if show_enrichment_details:
|
|
2075
|
+
for warning in apply_result.warnings:
|
|
2076
|
+
append_transcript_note(
|
|
2077
|
+
paths,
|
|
2078
|
+
role="system",
|
|
2079
|
+
message=f"Plan enrichment warning: {warning}",
|
|
2080
|
+
)
|
|
2081
|
+
if apply_result.changed:
|
|
2082
|
+
enrich_applied = True
|
|
2083
|
+
reconciliation_result, workspace_context = (
|
|
2084
|
+
_reconcile_plan_for_paths(
|
|
2085
|
+
paths=paths,
|
|
2086
|
+
plan=plan,
|
|
2087
|
+
refresh_if_stale=False,
|
|
2088
|
+
transcript_tail=forge_state.planner_transcript,
|
|
2089
|
+
target_task_ids=reconciliation_target_ids,
|
|
2090
|
+
)
|
|
2091
|
+
)
|
|
2092
|
+
save_plan(paths, plan)
|
|
2093
|
+
validation_warnings = _validate_forge_plan_for_paths(
|
|
2094
|
+
paths, plan
|
|
2095
|
+
)
|
|
2096
|
+
_write_plan_validation_artifact(
|
|
2097
|
+
paths=paths,
|
|
2098
|
+
reconciliation_result=reconciliation_result,
|
|
2099
|
+
validation_warnings=validation_warnings,
|
|
2100
|
+
)
|
|
2101
|
+
enrich_summary = (
|
|
2102
|
+
f"enrichment applied: {summarize_plan_update(apply_result)}"
|
|
2103
|
+
)
|
|
2104
|
+
else:
|
|
2105
|
+
enrich_summary = "enrichment produced no applicable changes"
|
|
2106
|
+
else:
|
|
2107
|
+
enrich_summary = "enrichment produced no plan_update"
|
|
2108
|
+
else:
|
|
2109
|
+
enrich_summary = "enrichment skipped: missing planner api key/config"
|
|
2110
|
+
|
|
2111
|
+
append_planner_summary(paths, enrich_summary)
|
|
2112
|
+
if enrich_applied:
|
|
2113
|
+
_print_forge_meta(console=console, message="Refined plan.")
|
|
2114
|
+
|
|
2115
|
+
if validation_warnings:
|
|
2116
|
+
_print_forge_warning_messages(
|
|
2117
|
+
console=console,
|
|
2118
|
+
label="Plan validation",
|
|
2119
|
+
warnings=validation_warnings,
|
|
2120
|
+
)
|
|
2121
|
+
for warning in validation_warnings:
|
|
2122
|
+
append_transcript_note(
|
|
2123
|
+
paths,
|
|
2124
|
+
role="system",
|
|
2125
|
+
message=f"Plan validation warning: {warning}",
|
|
2126
|
+
)
|
|
2127
|
+
elif (
|
|
2128
|
+
enrich_attempted
|
|
2129
|
+
and enrich_applied
|
|
2130
|
+
and not validation_warnings
|
|
2131
|
+
and show_enrichment_details
|
|
2132
|
+
):
|
|
2133
|
+
append_transcript_note(
|
|
2134
|
+
paths,
|
|
2135
|
+
role="system",
|
|
2136
|
+
message="Plan enrichment cleared the remaining validation warnings.",
|
|
2137
|
+
)
|
|
2138
|
+
|
|
2139
|
+
# Stamp draft/execution_ready before the gate runs, so the plan on disk
|
|
2140
|
+
# already says what the gate is about to decide.
|
|
2141
|
+
status_assessment = apply_plan_status(plan, validation_warnings=validation_warnings)
|
|
2142
|
+
save_plan(paths, plan)
|
|
2143
|
+
|
|
2144
|
+
try:
|
|
2145
|
+
raise_for_execution_ready_plan(plan)
|
|
2146
|
+
except PlannerFailedError as e:
|
|
2147
|
+
block_message = str(e)
|
|
2148
|
+
_print_forge_error(
|
|
2149
|
+
console=console,
|
|
2150
|
+
message=block_message,
|
|
2151
|
+
)
|
|
2152
|
+
_print_forge_meta(
|
|
2153
|
+
console=console,
|
|
2154
|
+
message=(
|
|
2155
|
+
"This plan is saved as a draft, not an execution-ready plan. "
|
|
2156
|
+
"Refine it with the planner or edit it, then run /execute plan again."
|
|
2157
|
+
),
|
|
2158
|
+
)
|
|
2159
|
+
append_transcript_note(
|
|
2160
|
+
paths,
|
|
2161
|
+
role="system",
|
|
2162
|
+
message=(
|
|
2163
|
+
f"Rejected /execute plan (plan_status={status_assessment.status}): "
|
|
2164
|
+
f"{block_message}"
|
|
2165
|
+
),
|
|
2166
|
+
)
|
|
2167
|
+
return "handled"
|
|
2168
|
+
|
|
2169
|
+
session_cfg = getattr(session, "cfg", None)
|
|
2170
|
+
if not isinstance(session_cfg, AppConfig):
|
|
2171
|
+
_print_forge_error(
|
|
2172
|
+
console=console,
|
|
2173
|
+
message="Forge execution failed: missing session config.",
|
|
2174
|
+
)
|
|
2175
|
+
return "handled"
|
|
2176
|
+
swarm_cfg = clone_cfg(session_cfg)
|
|
2177
|
+
client_obj = getattr(session, "client", None)
|
|
2178
|
+
api_key_override = str(getattr(client_obj, "api_key", "") or "").strip() or None
|
|
2179
|
+
store_obj = getattr(session, "store", None)
|
|
2180
|
+
no_log = not bool(getattr(store_obj, "enabled", True))
|
|
2181
|
+
yes_value = bool(getattr(session, "yes", False))
|
|
2182
|
+
swarm_trace_level = _chat_trace_level(session)
|
|
2183
|
+
swarm_trace_sink = _make_forge_swarm_trace_sink(
|
|
2184
|
+
session=session,
|
|
2185
|
+
paths=paths,
|
|
2186
|
+
console=console,
|
|
2187
|
+
)
|
|
2188
|
+
|
|
2189
|
+
# Swarm knobs: the TUI launch gate may have chosen non-default values
|
|
2190
|
+
# (stored on ``forge_state.swarm_knobs``); the classic/non-TUI path
|
|
2191
|
+
# leaves it empty so the historical defaults (parallel 2 · scope strict
|
|
2192
|
+
# · verify warn · no review) apply unchanged. Every value is validated
|
|
2193
|
+
# and clamped here so a malformed entry can never reach ``run_swarm``.
|
|
2194
|
+
_knobs = getattr(forge_state, "swarm_knobs", None) or {}
|
|
2195
|
+
try:
|
|
2196
|
+
_parallel = int(_knobs.get("parallel", 2))
|
|
2197
|
+
except (TypeError, ValueError):
|
|
2198
|
+
_parallel = 2
|
|
2199
|
+
_parallel = max(1, min(_parallel, 8))
|
|
2200
|
+
_scope_mode = str(_knobs.get("scope_mode", "strict")).strip().lower()
|
|
2201
|
+
if _scope_mode not in {"strict", "warn"}:
|
|
2202
|
+
_scope_mode = "strict"
|
|
2203
|
+
_verify_mode = str(_knobs.get("verify_mode", "warn")).strip().lower()
|
|
2204
|
+
if _verify_mode not in {"warn", "strict", "off"}:
|
|
2205
|
+
_verify_mode = "warn"
|
|
2206
|
+
_review = bool(_knobs.get("review", False))
|
|
2207
|
+
|
|
2208
|
+
forge_state.swarm_run_attempted = True
|
|
2209
|
+
try:
|
|
2210
|
+
code = run_swarm(
|
|
2211
|
+
paths=paths,
|
|
2212
|
+
plan=plan,
|
|
2213
|
+
cfg=swarm_cfg,
|
|
2214
|
+
mode="auto",
|
|
2215
|
+
yes=yes_value,
|
|
2216
|
+
max_steps=None,
|
|
2217
|
+
api_key_override=api_key_override,
|
|
2218
|
+
no_log=no_log,
|
|
2219
|
+
parallel=_parallel,
|
|
2220
|
+
base_branch=None,
|
|
2221
|
+
max_tasks=None,
|
|
2222
|
+
max_attempts=None,
|
|
2223
|
+
dry_run=False,
|
|
2224
|
+
keep_worktrees=False,
|
|
2225
|
+
retry_failed=False,
|
|
2226
|
+
retry_changes_requested=False,
|
|
2227
|
+
only=None,
|
|
2228
|
+
retry_merge_conflicts=False,
|
|
2229
|
+
scope_mode=_scope_mode,
|
|
2230
|
+
verify_mode=_verify_mode,
|
|
2231
|
+
integration_mode=None,
|
|
2232
|
+
verify_cmd=None,
|
|
2233
|
+
review=_review,
|
|
2234
|
+
console=console,
|
|
2235
|
+
trace_level=swarm_trace_level,
|
|
2236
|
+
trace_sink=swarm_trace_sink,
|
|
2237
|
+
run_mutation_guard=run_mutation_guard,
|
|
2238
|
+
)
|
|
2239
|
+
except (ConfigError, ForgeError, GitOpsError) as e:
|
|
2240
|
+
_print_forge_error(
|
|
2241
|
+
console=console,
|
|
2242
|
+
message=f"Forge execution failed: {e}",
|
|
2243
|
+
)
|
|
2244
|
+
append_transcript_note(
|
|
2245
|
+
paths,
|
|
2246
|
+
role="system",
|
|
2247
|
+
message=f"Forge /execute plan failed: {e}",
|
|
2248
|
+
)
|
|
2249
|
+
return "handled"
|
|
2250
|
+
except Exception as e: # noqa: BLE001
|
|
2251
|
+
_print_forge_error(
|
|
2252
|
+
console=console,
|
|
2253
|
+
message=f"Forge execution failed: {e}",
|
|
2254
|
+
)
|
|
2255
|
+
append_transcript_note(
|
|
2256
|
+
paths,
|
|
2257
|
+
role="system",
|
|
2258
|
+
message=f"Forge /execute plan failed: {e}",
|
|
2259
|
+
)
|
|
2260
|
+
return "handled"
|
|
2261
|
+
|
|
2262
|
+
_merge_forge_execution_usage_into_session(session=session, paths=paths)
|
|
2263
|
+
summary_path = paths.execution_dir / "swarm_summary.md"
|
|
2264
|
+
summary_json_path = paths.execution_dir / "swarm_summary.json"
|
|
2265
|
+
run_status = ""
|
|
2266
|
+
run_clean: bool | None = None
|
|
2267
|
+
if summary_json_path.exists():
|
|
2268
|
+
try:
|
|
2269
|
+
summary_payload = json.loads(summary_json_path.read_text(encoding="utf-8"))
|
|
2270
|
+
run_status = str(summary_payload.get("status") or "").strip()
|
|
2271
|
+
raw_clean = summary_payload.get("clean")
|
|
2272
|
+
if isinstance(raw_clean, bool):
|
|
2273
|
+
run_clean = raw_clean
|
|
2274
|
+
except Exception: # noqa: BLE001
|
|
2275
|
+
run_status = ""
|
|
2276
|
+
try:
|
|
2277
|
+
plan_after_execution = load_plan(paths)
|
|
2278
|
+
except Exception: # noqa: BLE001
|
|
2279
|
+
plan_after_execution = plan
|
|
2280
|
+
total_tasks = len(plan_after_execution.get("tasks") or [])
|
|
2281
|
+
done_tasks, failed_tasks, remaining_tasks = _forge_task_status_counts(
|
|
2282
|
+
plan_after_execution
|
|
2283
|
+
)
|
|
2284
|
+
if total_tasks <= 0:
|
|
2285
|
+
headline = "Execution finished."
|
|
2286
|
+
elif run_clean is False and run_status:
|
|
2287
|
+
headline = (
|
|
2288
|
+
f"Execution finished with verification status `{run_status}` · "
|
|
2289
|
+
f"{done_tasks} done · {failed_tasks} failed · {remaining_tasks} remaining."
|
|
2290
|
+
)
|
|
2291
|
+
elif failed_tasks == 0 and remaining_tasks == 0 and code == 0:
|
|
2292
|
+
headline = f"Execution complete · {total_tasks} tasks finished."
|
|
2293
|
+
elif failed_tasks == 0:
|
|
2294
|
+
headline = (
|
|
2295
|
+
f"Execution finished · {done_tasks} done · {remaining_tasks} still in progress."
|
|
2296
|
+
)
|
|
2297
|
+
elif done_tasks > 0:
|
|
2298
|
+
headline = (
|
|
2299
|
+
f"Execution finished with issues · {done_tasks} done · "
|
|
2300
|
+
f"{failed_tasks} failed · {remaining_tasks} remaining."
|
|
2301
|
+
)
|
|
2302
|
+
else:
|
|
2303
|
+
headline = (
|
|
2304
|
+
f"Execution finished with issues · {failed_tasks} failed · "
|
|
2305
|
+
f"{remaining_tasks} remaining."
|
|
2306
|
+
)
|
|
2307
|
+
done_ok = total_tasks > 0 and failed_tasks == 0 and remaining_tasks == 0 and code == 0
|
|
2308
|
+
console.print(
|
|
2309
|
+
_forge_phase_rule(console=console, label="DONE"),
|
|
2310
|
+
highlight=False,
|
|
2311
|
+
)
|
|
2312
|
+
dot = "●" if _forge_supports_unicode_glyphs(console) else "*"
|
|
2313
|
+
console.print(
|
|
2314
|
+
Text.assemble(
|
|
2315
|
+
("│ ", STYLE_CHROME),
|
|
2316
|
+
(f"{dot} ", STYLE_SUCCESS if done_ok else STYLE_WARN),
|
|
2317
|
+
(headline, STYLE_EMPHASIS),
|
|
2318
|
+
),
|
|
2319
|
+
highlight=False,
|
|
2320
|
+
)
|
|
2321
|
+
_print_forge_meta(
|
|
2322
|
+
console=console,
|
|
2323
|
+
message=(
|
|
2324
|
+
f"Tasks · {total_tasks} total · {done_tasks} done · "
|
|
2325
|
+
f"{failed_tasks} failed · {remaining_tasks} remaining"
|
|
2326
|
+
),
|
|
2327
|
+
)
|
|
2328
|
+
_print_forge_meta(console=console, message=f"Summary · {summary_path}")
|
|
2329
|
+
|
|
2330
|
+
# The run's actual answer: what each task did, where the files
|
|
2331
|
+
# landed, whether the output is verified, and how to try it. Built
|
|
2332
|
+
# deterministically from run artifacts (no model call). The TUI
|
|
2333
|
+
# passes a sink so this renders as a real assistant block instead
|
|
2334
|
+
# of joining the dim captured console text; classic chat prints it
|
|
2335
|
+
# inline as markdown.
|
|
2336
|
+
completion_report = ""
|
|
2337
|
+
try:
|
|
2338
|
+
completion_report = build_forge_completion_report(
|
|
2339
|
+
paths=paths,
|
|
2340
|
+
plan=plan_after_execution,
|
|
2341
|
+
run_status=run_status,
|
|
2342
|
+
run_clean=run_clean,
|
|
2343
|
+
exit_code=code,
|
|
2344
|
+
)
|
|
2345
|
+
except Exception: # noqa: BLE001
|
|
2346
|
+
completion_report = ""
|
|
2347
|
+
if completion_report:
|
|
2348
|
+
try:
|
|
2349
|
+
(paths.execution_dir / "completion_report.md").write_text(
|
|
2350
|
+
completion_report + "\n", encoding="utf-8"
|
|
2351
|
+
)
|
|
2352
|
+
except Exception: # noqa: BLE001
|
|
2353
|
+
pass
|
|
2354
|
+
if forge_execution_report_sink is not None:
|
|
2355
|
+
try:
|
|
2356
|
+
forge_execution_report_sink(completion_report)
|
|
2357
|
+
except Exception: # noqa: BLE001
|
|
2358
|
+
pass
|
|
2359
|
+
else:
|
|
2360
|
+
console.print()
|
|
2361
|
+
console.print(Markdown(completion_report), highlight=False)
|
|
2362
|
+
|
|
2363
|
+
# The run just rewrote the workspace; drop the cached pre-execution
|
|
2364
|
+
# scans so follow-up planner questions ("how do I run it?") are
|
|
2365
|
+
# answered against what now actually exists on disk.
|
|
2366
|
+
forge_state.workspace_context = None
|
|
2367
|
+
if getattr(session, "planner_workspace_context", None) is not None:
|
|
2368
|
+
try:
|
|
2369
|
+
session.planner_workspace_context = None
|
|
2370
|
+
except Exception: # noqa: BLE001
|
|
2371
|
+
pass
|
|
2372
|
+
|
|
2373
|
+
append_transcript_note(
|
|
2374
|
+
paths,
|
|
2375
|
+
role="system",
|
|
2376
|
+
message=(
|
|
2377
|
+
"Forge /execute plan completed. "
|
|
2378
|
+
f"Tasks: total={total_tasks}, done={done_tasks}, "
|
|
2379
|
+
f"failed={failed_tasks}, remaining={remaining_tasks}."
|
|
2380
|
+
),
|
|
2381
|
+
)
|
|
2382
|
+
return "handled"
|
|
2383
|
+
finally:
|
|
2384
|
+
run_mutation_guard.release()
|
|
2385
|
+
|
|
2386
|
+
if cmd in {"/assistant"}:
|
|
2387
|
+
assistant_cmd = arg.lower()
|
|
2388
|
+
if not assistant_cmd:
|
|
2389
|
+
assistant_cmd, picker_available = _select_forge_assistant_interactive(
|
|
2390
|
+
enabled=forge_state.assistant_enabled,
|
|
2391
|
+
console=console,
|
|
2392
|
+
)
|
|
2393
|
+
if not picker_available:
|
|
2394
|
+
append_transcript_note(paths, role="user", message=trimmed)
|
|
2395
|
+
append_transcript_note(
|
|
2396
|
+
paths,
|
|
2397
|
+
role="system",
|
|
2398
|
+
message="Rejected invalid /assistant usage.",
|
|
2399
|
+
)
|
|
2400
|
+
_print_forge_warning_messages(
|
|
2401
|
+
console=console,
|
|
2402
|
+
label="Usage",
|
|
2403
|
+
warnings=["/assistant on|off|status"],
|
|
2404
|
+
)
|
|
2405
|
+
return "handled"
|
|
2406
|
+
if assistant_cmd is None:
|
|
2407
|
+
return "handled"
|
|
2408
|
+
if assistant_cmd == "on":
|
|
2409
|
+
forge_state.assistant_enabled = True
|
|
2410
|
+
append_transcript_note(paths, role="user", message=trimmed)
|
|
2411
|
+
append_transcript_note(paths, role="system", message="Planner assistant enabled.")
|
|
2412
|
+
_print_forge_meta(console=console, message="Planner assistant: ON")
|
|
2413
|
+
return "handled"
|
|
2414
|
+
if assistant_cmd == "off":
|
|
2415
|
+
forge_state.assistant_enabled = False
|
|
2416
|
+
forge_state.planner_awaiting_clarification = False
|
|
2417
|
+
forge_state.planner_pending_questions = []
|
|
2418
|
+
append_transcript_note(paths, role="user", message=trimmed)
|
|
2419
|
+
append_transcript_note(paths, role="system", message="Planner assistant disabled.")
|
|
2420
|
+
_print_forge_meta(console=console, message="Planner assistant: OFF")
|
|
2421
|
+
return "handled"
|
|
2422
|
+
if assistant_cmd == "status":
|
|
2423
|
+
state = "ON" if forge_state.assistant_enabled else "OFF"
|
|
2424
|
+
append_transcript_note(paths, role="user", message=trimmed)
|
|
2425
|
+
append_transcript_note(
|
|
2426
|
+
paths,
|
|
2427
|
+
role="system",
|
|
2428
|
+
message=f"Planner assistant status requested ({state}).",
|
|
2429
|
+
)
|
|
2430
|
+
_print_forge_meta(console=console, message=f"Planner assistant: {state}")
|
|
2431
|
+
return "handled"
|
|
2432
|
+
append_transcript_note(paths, role="user", message=trimmed)
|
|
2433
|
+
append_transcript_note(paths, role="system", message="Rejected invalid /assistant usage.")
|
|
2434
|
+
_print_forge_warning_messages(
|
|
2435
|
+
console=console,
|
|
2436
|
+
label="Usage",
|
|
2437
|
+
warnings=["/assistant on|off|status"],
|
|
2438
|
+
)
|
|
2439
|
+
return "handled"
|
|
2440
|
+
|
|
2441
|
+
if cmd == "/goal":
|
|
2442
|
+
append_transcript_note(paths, role="user", message=trimmed)
|
|
2443
|
+
if not arg:
|
|
2444
|
+
append_transcript_note(paths, role="system", message="Rejected empty /goal.")
|
|
2445
|
+
_print_forge_warning_messages(
|
|
2446
|
+
console=console,
|
|
2447
|
+
label="Usage",
|
|
2448
|
+
warnings=["/goal <text>"],
|
|
2449
|
+
)
|
|
2450
|
+
return "handled"
|
|
2451
|
+
goal = arg
|
|
2452
|
+
plan["project_goal"] = goal
|
|
2453
|
+
if not str(plan.get("summary") or "").strip():
|
|
2454
|
+
plan["summary"] = goal
|
|
2455
|
+
save_plan(paths, plan)
|
|
2456
|
+
append_transcript_note(paths, role="system", message="Updated project goal.")
|
|
2457
|
+
_print_forge_meta(console=console, message="Project goal updated.")
|
|
2458
|
+
return "handled"
|
|
2459
|
+
|
|
2460
|
+
if cmd == "/task":
|
|
2461
|
+
append_transcript_note(paths, role="user", message=trimmed)
|
|
2462
|
+
if not arg:
|
|
2463
|
+
append_transcript_note(paths, role="system", message="Rejected empty /task.")
|
|
2464
|
+
_print_forge_warning_messages(
|
|
2465
|
+
console=console,
|
|
2466
|
+
label="Usage",
|
|
2467
|
+
warnings=["/task <title>"],
|
|
2468
|
+
)
|
|
2469
|
+
return "handled"
|
|
2470
|
+
title = arg
|
|
2471
|
+
try:
|
|
2472
|
+
task = add_task(
|
|
2473
|
+
plan,
|
|
2474
|
+
title=title,
|
|
2475
|
+
description=f"Manual planning chat task: {title}",
|
|
2476
|
+
)
|
|
2477
|
+
except ForgeError as e:
|
|
2478
|
+
append_transcript_note(
|
|
2479
|
+
paths,
|
|
2480
|
+
role="system",
|
|
2481
|
+
message=f"Rejected /task because it lacked runnable file scope: {e}",
|
|
2482
|
+
)
|
|
2483
|
+
_print_forge_warning_messages(
|
|
2484
|
+
console=console,
|
|
2485
|
+
label="Task rejected",
|
|
2486
|
+
warnings=[str(e)],
|
|
2487
|
+
)
|
|
2488
|
+
return "handled"
|
|
2489
|
+
save_plan(paths, plan)
|
|
2490
|
+
append_transcript_note(paths, role="system", message=f"Added task {task['id']}.")
|
|
2491
|
+
_print_forge_meta(
|
|
2492
|
+
console=console,
|
|
2493
|
+
message=f"Added task {task['id']} · {task['title']}",
|
|
2494
|
+
)
|
|
2495
|
+
return "handled"
|
|
2496
|
+
|
|
2497
|
+
if cmd[:1] in "/:":
|
|
2498
|
+
return "unhandled"
|
|
2499
|
+
|
|
2500
|
+
# In the TUI the user line is already echoed (and the reply is routed to the
|
|
2501
|
+
# assistant-markdown renderer via the sink), so skip the captured "You:" label
|
|
2502
|
+
# that would otherwise double the echo as flat text.
|
|
2503
|
+
if forge_planner_reply_sink is None:
|
|
2504
|
+
_render_labeled_chat_message(console=console, label="You", message=trimmed)
|
|
2505
|
+
|
|
2506
|
+
append_transcript_note(paths, role="user", message=trimmed)
|
|
2507
|
+
|
|
2508
|
+
if forge_state.assistant_enabled:
|
|
2509
|
+
planning_relevant = True
|
|
2510
|
+
session_cfg = getattr(session, "cfg", None)
|
|
2511
|
+
planner_api_key = str(
|
|
2512
|
+
getattr(getattr(session, "client", None), "api_key", "") or ""
|
|
2513
|
+
).strip()
|
|
2514
|
+
api_key_override = planner_api_key or None
|
|
2515
|
+
session_stream = bool(getattr(session, "stream", False))
|
|
2516
|
+
# With a reply sink (TUI) the reply is rendered as a single assistant block
|
|
2517
|
+
# on completion, so suppress the token-delta trace that would otherwise
|
|
2518
|
+
# spill the streaming text as separate trace lines.
|
|
2519
|
+
on_text_delta = (
|
|
2520
|
+
_make_plan_mode_delta_trace_callback(session=session)
|
|
2521
|
+
if (session_stream and forge_planner_reply_sink is None)
|
|
2522
|
+
else None
|
|
2523
|
+
)
|
|
2524
|
+
stream_planner = session_stream and on_text_delta is not None
|
|
2525
|
+
_emit_forge_planner_trace(
|
|
2526
|
+
session=session,
|
|
2527
|
+
message="Planner assistant is analyzing your request.",
|
|
2528
|
+
)
|
|
2529
|
+
if stream_planner:
|
|
2530
|
+
_emit_forge_planner_trace(
|
|
2531
|
+
session=session,
|
|
2532
|
+
message="Streaming planner response...",
|
|
2533
|
+
full_only=True,
|
|
2534
|
+
)
|
|
2535
|
+
_run_forge_planner_turn_controller(
|
|
2536
|
+
console=console,
|
|
2537
|
+
paths=paths,
|
|
2538
|
+
plan=plan,
|
|
2539
|
+
planner_state=forge_state.planner_session,
|
|
2540
|
+
user_text=trimmed,
|
|
2541
|
+
cfg_loader=(
|
|
2542
|
+
lambda: (
|
|
2543
|
+
clone_cfg(session_cfg)
|
|
2544
|
+
if isinstance(session_cfg, AppConfig)
|
|
2545
|
+
else (_ for _ in ()).throw(ConfigError("session config missing"))
|
|
2546
|
+
)
|
|
2547
|
+
),
|
|
2548
|
+
unavailable_message_builder=lambda _error: (
|
|
2549
|
+
"Planner assistant is unavailable because session config is missing."
|
|
2550
|
+
),
|
|
2551
|
+
emit_meta=lambda message: _print_forge_meta(console=console, message=message),
|
|
2552
|
+
emit_warning_group=lambda label, warnings: _print_forge_warning_messages(
|
|
2553
|
+
console=console,
|
|
2554
|
+
label=label,
|
|
2555
|
+
warnings=warnings,
|
|
2556
|
+
),
|
|
2557
|
+
api_key_override=api_key_override,
|
|
2558
|
+
render_reply=(
|
|
2559
|
+
forge_planner_reply_sink
|
|
2560
|
+
if forge_planner_reply_sink is not None
|
|
2561
|
+
else (
|
|
2562
|
+
lambda message, questions: _render_planner_reply(
|
|
2563
|
+
console=console,
|
|
2564
|
+
message=message,
|
|
2565
|
+
questions=questions,
|
|
2566
|
+
)
|
|
2567
|
+
)
|
|
2568
|
+
),
|
|
2569
|
+
selection_label="planner",
|
|
2570
|
+
planning_relevant=planning_relevant,
|
|
2571
|
+
refresh_workspace_context=(
|
|
2572
|
+
forge_state.workspace_context is None
|
|
2573
|
+
and hasattr(paths, "root")
|
|
2574
|
+
and hasattr(paths, "workspace_context_json_path")
|
|
2575
|
+
),
|
|
2576
|
+
trace_callback=lambda message, full_only=False: _emit_forge_planner_trace(
|
|
2577
|
+
session=session,
|
|
2578
|
+
message=message,
|
|
2579
|
+
full_only=full_only,
|
|
2580
|
+
),
|
|
2581
|
+
stream=stream_planner,
|
|
2582
|
+
on_text_delta=on_text_delta,
|
|
2583
|
+
on_usage_events=lambda payloads: _merge_usage_payloads_into_session(
|
|
2584
|
+
session=session,
|
|
2585
|
+
payloads=payloads,
|
|
2586
|
+
),
|
|
2587
|
+
error_fallback=lambda: _capture_forge_requirement_from_planner_fallback(
|
|
2588
|
+
plan=plan,
|
|
2589
|
+
paths=paths,
|
|
2590
|
+
console=console,
|
|
2591
|
+
user_text=trimmed,
|
|
2592
|
+
planning_relevant=planning_relevant,
|
|
2593
|
+
),
|
|
2594
|
+
)
|
|
2595
|
+
return "handled"
|
|
2596
|
+
|
|
2597
|
+
goal_was_set = _set_forge_project_goal_if_empty(
|
|
2598
|
+
plan=plan,
|
|
2599
|
+
paths=paths,
|
|
2600
|
+
console=console,
|
|
2601
|
+
goal=trimmed,
|
|
2602
|
+
)
|
|
2603
|
+
if goal_was_set:
|
|
2604
|
+
add_requirement(plan, trimmed)
|
|
2605
|
+
save_plan(paths, plan)
|
|
2606
|
+
append_transcript_note(
|
|
2607
|
+
paths,
|
|
2608
|
+
role="system",
|
|
2609
|
+
message="Captured project goal as planning input.",
|
|
2610
|
+
)
|
|
2611
|
+
else:
|
|
2612
|
+
add_requirement(plan, trimmed)
|
|
2613
|
+
save_plan(paths, plan)
|
|
2614
|
+
append_transcript_note(paths, role="system", message="Captured requirement note.")
|
|
2615
|
+
_print_forge_meta(console=console, message="Captured requirement note.")
|
|
2616
|
+
return "handled"
|