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,1282 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from ...config import ConfigError, clone_cfg, load_config
|
|
11
|
+
from ...forge import (
|
|
12
|
+
ForgeError,
|
|
13
|
+
attach_asset,
|
|
14
|
+
find_task,
|
|
15
|
+
load_current_run_paths,
|
|
16
|
+
load_plan,
|
|
17
|
+
)
|
|
18
|
+
from ...forge_events import (
|
|
19
|
+
EVENT_PLAN_INVALID,
|
|
20
|
+
EVENT_REVIEW_RESULT,
|
|
21
|
+
EVENT_RUN_STARTED,
|
|
22
|
+
EXIT_ERROR,
|
|
23
|
+
ForgeEventEmitter,
|
|
24
|
+
machine_session,
|
|
25
|
+
)
|
|
26
|
+
from ...plan_repair import (
|
|
27
|
+
PLAN_STATUS_DRAFT,
|
|
28
|
+
plan_repair_event_payload,
|
|
29
|
+
plan_repair_metadata,
|
|
30
|
+
plan_status,
|
|
31
|
+
plan_status_detail,
|
|
32
|
+
)
|
|
33
|
+
from ...review_gate import ReviewError, review_task
|
|
34
|
+
from . import _patchable
|
|
35
|
+
from ._shared import Mode, _console, _Table
|
|
36
|
+
from .forge_asset_view import forge_asset_view_count, forge_asset_view_entries
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _cli_module() -> Any:
|
|
40
|
+
module = sys.modules.get("alysis_code.cli")
|
|
41
|
+
if module is not None:
|
|
42
|
+
return module
|
|
43
|
+
from ... import cli
|
|
44
|
+
|
|
45
|
+
return cli
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
forge_app = typer.Typer(
|
|
49
|
+
add_completion=False,
|
|
50
|
+
help=(
|
|
51
|
+
"Forge commands. `forge run` executes a plan sequentially in this checkout and is the "
|
|
52
|
+
"default path; `forge swarm` is the parallel optimization for independent tasks."
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
_MACHINE_CONTEXT_KEY = "alysis_forge_machine"
|
|
57
|
+
|
|
58
|
+
MACHINE_OPTION_HELP = (
|
|
59
|
+
"Emit newline-delimited JSON events on stdout instead of human output. "
|
|
60
|
+
"One JSON object per line; exactly one terminal event (run_completed or error)."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _store_machine_flag(ctx: typer.Context | None, value: bool) -> None:
|
|
65
|
+
if ctx is None or value is not True:
|
|
66
|
+
return
|
|
67
|
+
if not isinstance(getattr(ctx, "obj", None), dict):
|
|
68
|
+
ctx.obj = {}
|
|
69
|
+
ctx.obj[_MACHINE_CONTEXT_KEY] = True
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _machine_enabled(ctx: typer.Context | None, machine: bool) -> bool:
|
|
73
|
+
"""True when either `forge --machine <cmd>` or `forge <cmd> --machine` was used.
|
|
74
|
+
|
|
75
|
+
Only a real ``True`` counts. These commands are also called directly as Python
|
|
76
|
+
functions (for example from the interactive quick-action menu), where unfilled
|
|
77
|
+
parameters arrive as truthy typer ``OptionInfo`` objects rather than booleans --
|
|
78
|
+
that must not silently switch a human invocation into the machine protocol.
|
|
79
|
+
"""
|
|
80
|
+
if machine is True:
|
|
81
|
+
return True
|
|
82
|
+
current: Any = ctx
|
|
83
|
+
while current is not None:
|
|
84
|
+
obj = getattr(current, "obj", None)
|
|
85
|
+
if isinstance(obj, dict) and obj.get(_MACHINE_CONTEXT_KEY):
|
|
86
|
+
return True
|
|
87
|
+
current = getattr(current, "parent", None)
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _forge_error_exit(
|
|
92
|
+
console: Any,
|
|
93
|
+
events: ForgeEventEmitter,
|
|
94
|
+
error: Exception,
|
|
95
|
+
*,
|
|
96
|
+
code: int = EXIT_ERROR,
|
|
97
|
+
kind: str = "forge_error",
|
|
98
|
+
data: dict[str, Any] | None = None,
|
|
99
|
+
) -> typer.Exit:
|
|
100
|
+
"""Report a Forge error on both surfaces and build the matching Exit."""
|
|
101
|
+
console.print(f"[red]Forge error:[/red] {error}")
|
|
102
|
+
events.error(message=str(error), kind=kind, exit_code=code, data=data)
|
|
103
|
+
return typer.Exit(code=code)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _run_lifecycle_snapshot(paths: Any) -> dict[str, Any]:
|
|
107
|
+
"""The run's lifecycle block, so consumers never infer "active" from its absence."""
|
|
108
|
+
from ...forge import read_current_run_pointer
|
|
109
|
+
from ...run_state import describe_run_status, pointer_status, status_is_resumable
|
|
110
|
+
|
|
111
|
+
pointer = read_current_run_pointer(paths.root) or {}
|
|
112
|
+
status = pointer_status(pointer)
|
|
113
|
+
return {
|
|
114
|
+
"run_status": status,
|
|
115
|
+
"run_status_description": describe_run_status(status),
|
|
116
|
+
"run_status_updated_at": pointer.get("status_updated_at"),
|
|
117
|
+
"run_status_reason": pointer.get("status_reason"),
|
|
118
|
+
"run_owner": pointer.get("run_owner"),
|
|
119
|
+
"resumable": status_is_resumable(status),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _plan_snapshot(paths: Any, plan: dict[str, Any]) -> dict[str, Any]:
|
|
124
|
+
tasks = plan.get("tasks") or []
|
|
125
|
+
return {
|
|
126
|
+
**_run_lifecycle_snapshot(paths),
|
|
127
|
+
"run_id": paths.run_id,
|
|
128
|
+
"run_dir": os.fspath(paths.run_dir),
|
|
129
|
+
"plan_json": os.fspath(paths.plan_json_path),
|
|
130
|
+
"plan_md": os.fspath(paths.plan_md_path),
|
|
131
|
+
"project_goal": str(plan.get("project_goal") or "").strip() or None,
|
|
132
|
+
"task_count": len(tasks),
|
|
133
|
+
"tasks": [
|
|
134
|
+
{
|
|
135
|
+
"id": str(task.get("id", "")),
|
|
136
|
+
"status": str(task.get("status", "")),
|
|
137
|
+
"title": str(task.get("title", "")),
|
|
138
|
+
"dependencies": [str(dep) for dep in (task.get("dependencies") or [])],
|
|
139
|
+
}
|
|
140
|
+
for task in tasks
|
|
141
|
+
],
|
|
142
|
+
**plan_repair_event_payload(plan),
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@forge_app.callback()
|
|
147
|
+
def forge_main(
|
|
148
|
+
ctx: typer.Context,
|
|
149
|
+
machine: bool = typer.Option(False, "--machine", help=MACHINE_OPTION_HELP),
|
|
150
|
+
) -> None:
|
|
151
|
+
"""Forge commands. Human output is the default; --machine switches to NDJSON."""
|
|
152
|
+
_store_machine_flag(ctx, machine)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@forge_app.command("plan")
|
|
156
|
+
def forge_plan(
|
|
157
|
+
ctx: typer.Context = None,
|
|
158
|
+
path: Path = typer.Option(
|
|
159
|
+
Path("."),
|
|
160
|
+
"--path",
|
|
161
|
+
help="Workspace path or repository subdirectory.",
|
|
162
|
+
),
|
|
163
|
+
create_path: bool = typer.Option(
|
|
164
|
+
False,
|
|
165
|
+
"--create-path",
|
|
166
|
+
help="Create --path if it does not exist before binding the workspace.",
|
|
167
|
+
),
|
|
168
|
+
allow_broad_workspace: bool = typer.Option(
|
|
169
|
+
False,
|
|
170
|
+
"--allow-broad-workspace",
|
|
171
|
+
help="Allow guarded broad workspaces instead of choosing a narrower project folder.",
|
|
172
|
+
),
|
|
173
|
+
machine: bool = typer.Option(False, "--machine", help=MACHINE_OPTION_HELP),
|
|
174
|
+
) -> None:
|
|
175
|
+
with machine_session("forge.plan", machine=_machine_enabled(ctx, machine)) as events:
|
|
176
|
+
events.emit(EVENT_RUN_STARTED, {"command": "forge.plan", "path": os.fspath(path)})
|
|
177
|
+
cli = _cli_module()
|
|
178
|
+
cli._require_active_subscription_ready(model=None, base_url=None)
|
|
179
|
+
from ..forge import forge_plan_impl
|
|
180
|
+
|
|
181
|
+
return forge_plan_impl(
|
|
182
|
+
cli,
|
|
183
|
+
path,
|
|
184
|
+
create_path,
|
|
185
|
+
allow_broad_workspace,
|
|
186
|
+
cli_ctx=ctx,
|
|
187
|
+
events=events,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@forge_app.command("attach")
|
|
192
|
+
def forge_attach(
|
|
193
|
+
ctx: typer.Context = None,
|
|
194
|
+
source_path: Path = typer.Argument(..., help="File path to attach."),
|
|
195
|
+
path: Path = typer.Option(
|
|
196
|
+
Path("."),
|
|
197
|
+
"--path",
|
|
198
|
+
help="Workspace path or repository subdirectory.",
|
|
199
|
+
),
|
|
200
|
+
machine: bool = typer.Option(False, "--machine", help=MACHINE_OPTION_HELP),
|
|
201
|
+
) -> None:
|
|
202
|
+
with machine_session("forge.attach", machine=_machine_enabled(ctx, machine)) as events:
|
|
203
|
+
console = _console()
|
|
204
|
+
events.emit(
|
|
205
|
+
EVENT_RUN_STARTED,
|
|
206
|
+
{
|
|
207
|
+
"command": "forge.attach",
|
|
208
|
+
"path": os.fspath(path),
|
|
209
|
+
"source_path": os.fspath(source_path),
|
|
210
|
+
},
|
|
211
|
+
)
|
|
212
|
+
if not events.enabled:
|
|
213
|
+
typer.echo(
|
|
214
|
+
"Deprecation: `alysis forge attach` is the legacy asset attachment flow.\n"
|
|
215
|
+
"Use `/assets` from chat or `alysis forge assets add` for the new flow with\n"
|
|
216
|
+
"LLM comprehension and per-task binding. The legacy command continues to work,\n"
|
|
217
|
+
"and attached assets are migrated on the next plan load.",
|
|
218
|
+
err=True,
|
|
219
|
+
)
|
|
220
|
+
try:
|
|
221
|
+
paths, metadata = attach_asset(path, source_path)
|
|
222
|
+
except ForgeError as e:
|
|
223
|
+
raise _forge_error_exit(console, events, e) from e
|
|
224
|
+
|
|
225
|
+
console.print(f"Attached to run: {paths.run_id}")
|
|
226
|
+
console.print(f"- original: {metadata.get('original_path')}")
|
|
227
|
+
console.print(f"- stored: {metadata.get('stored_path')}")
|
|
228
|
+
console.print(f"- size: {metadata.get('size_bytes')} bytes")
|
|
229
|
+
if metadata.get("text_copy_path"):
|
|
230
|
+
console.print(f"- extracted text: {metadata.get('text_copy_path')}")
|
|
231
|
+
|
|
232
|
+
events.set_run_id(paths.run_id)
|
|
233
|
+
events.run_completed(
|
|
234
|
+
ok=True,
|
|
235
|
+
data={
|
|
236
|
+
"deprecated": True,
|
|
237
|
+
"asset": {
|
|
238
|
+
"original_path": metadata.get("original_path"),
|
|
239
|
+
"stored_path": metadata.get("stored_path"),
|
|
240
|
+
"size_bytes": metadata.get("size_bytes"),
|
|
241
|
+
"text_copy_path": metadata.get("text_copy_path"),
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@forge_app.command("show")
|
|
248
|
+
def forge_show(
|
|
249
|
+
ctx: typer.Context = None,
|
|
250
|
+
path: Path = typer.Option(
|
|
251
|
+
Path("."),
|
|
252
|
+
"--path",
|
|
253
|
+
help="Workspace path or repository subdirectory.",
|
|
254
|
+
),
|
|
255
|
+
machine: bool = typer.Option(False, "--machine", help=MACHINE_OPTION_HELP),
|
|
256
|
+
) -> None:
|
|
257
|
+
with machine_session("forge.show", machine=_machine_enabled(ctx, machine)) as events:
|
|
258
|
+
console = _console()
|
|
259
|
+
events.emit(EVENT_RUN_STARTED, {"command": "forge.show", "path": os.fspath(path)})
|
|
260
|
+
try:
|
|
261
|
+
paths = load_current_run_paths(path)
|
|
262
|
+
plan = load_plan(paths)
|
|
263
|
+
except ForgeError as e:
|
|
264
|
+
events.emit(EVENT_PLAN_INVALID, {"reason": str(e), "source": "plan_load"})
|
|
265
|
+
raise _forge_error_exit(console, events, e) from e
|
|
266
|
+
|
|
267
|
+
events.set_run_id(paths.run_id)
|
|
268
|
+
console.rule("[bold]forge show[/bold]")
|
|
269
|
+
goal = str(plan.get("project_goal") or "").strip() or "(not set)"
|
|
270
|
+
console.print(f"Run ID: {paths.run_id}")
|
|
271
|
+
console.print(f"Project goal: {goal}")
|
|
272
|
+
|
|
273
|
+
status = plan_status(plan)
|
|
274
|
+
if status == PLAN_STATUS_DRAFT:
|
|
275
|
+
console.print("[yellow]Plan status:[/yellow] draft (not execution-ready)")
|
|
276
|
+
for reason in plan_status_detail(plan)["blocking_reasons"][:5]:
|
|
277
|
+
console.print(f"- {reason}")
|
|
278
|
+
else:
|
|
279
|
+
console.print("Plan status: execution_ready")
|
|
280
|
+
|
|
281
|
+
# A plan the host had to salvage says so, with the fields it touched.
|
|
282
|
+
repair = plan_repair_metadata(plan)
|
|
283
|
+
if repair["host_repaired"] or repair["forced_draft"]:
|
|
284
|
+
labels = []
|
|
285
|
+
if repair["host_repaired"]:
|
|
286
|
+
labels.append("host-repaired")
|
|
287
|
+
if repair["forced_draft"]:
|
|
288
|
+
labels.append("forced draft after clarification cap")
|
|
289
|
+
console.print("[yellow]Plan repairs:[/yellow] " + ", ".join(labels))
|
|
290
|
+
if repair["host_repaired_fields"]:
|
|
291
|
+
console.print(
|
|
292
|
+
"Host-repaired fields: " + ", ".join(repair["host_repaired_fields"][:10])
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
tasks = plan.get("tasks") or []
|
|
296
|
+
task_table = _Table(title="Tasks")
|
|
297
|
+
task_table.add_column("id")
|
|
298
|
+
task_table.add_column("status")
|
|
299
|
+
task_table.add_column("title")
|
|
300
|
+
task_table.add_column("dependencies")
|
|
301
|
+
if tasks:
|
|
302
|
+
for task in tasks:
|
|
303
|
+
deps = task.get("dependencies") or []
|
|
304
|
+
task_table.add_row(
|
|
305
|
+
str(task.get("id", "")),
|
|
306
|
+
str(task.get("status", "")),
|
|
307
|
+
str(task.get("title", "")),
|
|
308
|
+
", ".join(str(d) for d in deps) if deps else "-",
|
|
309
|
+
)
|
|
310
|
+
console.print(task_table)
|
|
311
|
+
|
|
312
|
+
assets = forge_asset_view_entries(paths, plan)
|
|
313
|
+
asset_table = _Table(title="Assets")
|
|
314
|
+
asset_table.add_column("source")
|
|
315
|
+
asset_table.add_column("stored_path")
|
|
316
|
+
asset_table.add_column("size_bytes")
|
|
317
|
+
if assets:
|
|
318
|
+
for asset in assets:
|
|
319
|
+
asset_table.add_row(
|
|
320
|
+
asset.source,
|
|
321
|
+
asset.stored_path,
|
|
322
|
+
"" if asset.size_bytes is None else str(asset.size_bytes),
|
|
323
|
+
)
|
|
324
|
+
console.print(asset_table)
|
|
325
|
+
if assets:
|
|
326
|
+
names = [asset.display_name for asset in assets if asset.display_name]
|
|
327
|
+
if names:
|
|
328
|
+
console.print(f"Asset files: {', '.join(names)}")
|
|
329
|
+
|
|
330
|
+
snapshot = _plan_snapshot(paths, plan)
|
|
331
|
+
snapshot["assets"] = [
|
|
332
|
+
{
|
|
333
|
+
"source": asset.source,
|
|
334
|
+
"stored_path": asset.stored_path,
|
|
335
|
+
"size_bytes": asset.size_bytes,
|
|
336
|
+
"display_name": asset.display_name,
|
|
337
|
+
}
|
|
338
|
+
for asset in assets
|
|
339
|
+
]
|
|
340
|
+
events.run_completed(ok=True, data=snapshot)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@forge_app.command("status")
|
|
344
|
+
def forge_status(
|
|
345
|
+
ctx: typer.Context = None,
|
|
346
|
+
path: Path = typer.Option(
|
|
347
|
+
Path("."),
|
|
348
|
+
"--path",
|
|
349
|
+
help="Workspace path or repository subdirectory.",
|
|
350
|
+
),
|
|
351
|
+
machine: bool = typer.Option(False, "--machine", help=MACHINE_OPTION_HELP),
|
|
352
|
+
) -> None:
|
|
353
|
+
with machine_session("forge.status", machine=_machine_enabled(ctx, machine)) as events:
|
|
354
|
+
console = _console()
|
|
355
|
+
events.emit(EVENT_RUN_STARTED, {"command": "forge.status", "path": os.fspath(path)})
|
|
356
|
+
try:
|
|
357
|
+
paths = load_current_run_paths(path)
|
|
358
|
+
plan = load_plan(paths)
|
|
359
|
+
except ForgeError as e:
|
|
360
|
+
events.emit(EVENT_PLAN_INVALID, {"reason": str(e), "source": "plan_load"})
|
|
361
|
+
raise _forge_error_exit(console, events, e) from e
|
|
362
|
+
|
|
363
|
+
events.set_run_id(paths.run_id)
|
|
364
|
+
asset_count = forge_asset_view_count(paths, plan)
|
|
365
|
+
table = _Table(title="forge status")
|
|
366
|
+
table.add_column("field")
|
|
367
|
+
table.add_column("value")
|
|
368
|
+
table.add_row("run_id", paths.run_id)
|
|
369
|
+
table.add_row("run_dir", os.fspath(paths.run_dir))
|
|
370
|
+
table.add_row("plan_json", os.fspath(paths.plan_json_path))
|
|
371
|
+
table.add_row("plan_md", os.fspath(paths.plan_md_path))
|
|
372
|
+
table.add_row("tasks", str(len(plan.get("tasks") or [])))
|
|
373
|
+
table.add_row("assets", str(asset_count))
|
|
374
|
+
lifecycle = _run_lifecycle_snapshot(paths)
|
|
375
|
+
table.add_row(
|
|
376
|
+
"run_status",
|
|
377
|
+
f"{lifecycle['run_status']} ({lifecycle['run_status_description']})",
|
|
378
|
+
)
|
|
379
|
+
console.print(table)
|
|
380
|
+
if lifecycle["resumable"]:
|
|
381
|
+
console.print(
|
|
382
|
+
"This run stopped before finishing. Continue it with: alysis forge resume --path ."
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
snapshot = _plan_snapshot(paths, plan)
|
|
386
|
+
snapshot["asset_count"] = asset_count
|
|
387
|
+
events.run_completed(ok=True, data=snapshot)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@forge_app.command("review")
|
|
391
|
+
def forge_review(
|
|
392
|
+
ctx: typer.Context = None,
|
|
393
|
+
task_id: str = typer.Argument(..., help="Task id from plan.json (for example T01)."),
|
|
394
|
+
path: Path = typer.Option(
|
|
395
|
+
Path("."),
|
|
396
|
+
"--path",
|
|
397
|
+
help="Workspace path or repository subdirectory.",
|
|
398
|
+
),
|
|
399
|
+
model: str | None = typer.Option(None, "--model", help="Model override."),
|
|
400
|
+
base_url: str | None = typer.Option(None, "--base-url", help="Base URL override."),
|
|
401
|
+
temperature: float | None = typer.Option(None, "--temperature", help="Sampling temperature."),
|
|
402
|
+
api_key_env: str | None = typer.Option(
|
|
403
|
+
None,
|
|
404
|
+
"--api-key-env",
|
|
405
|
+
help=(
|
|
406
|
+
"Read API key from this environment variable (overrides ALYSIS_API_KEY/OPENAI_API_KEY)."
|
|
407
|
+
),
|
|
408
|
+
),
|
|
409
|
+
api_key_stdin: bool = typer.Option(
|
|
410
|
+
False,
|
|
411
|
+
"--api-key-stdin",
|
|
412
|
+
help="Prompt for API key (hidden input). Key is kept in memory for this run only.",
|
|
413
|
+
),
|
|
414
|
+
api_key: str | None = typer.Option(
|
|
415
|
+
None,
|
|
416
|
+
"--api-key",
|
|
417
|
+
help=(
|
|
418
|
+
"UNSAFE: Provide API key via CLI argument (may leak via shell history / process list). "
|
|
419
|
+
"Prefer --api-key-stdin or --api-key-env."
|
|
420
|
+
),
|
|
421
|
+
),
|
|
422
|
+
machine: bool = typer.Option(False, "--machine", help=MACHINE_OPTION_HELP),
|
|
423
|
+
) -> None:
|
|
424
|
+
with machine_session("forge.review", machine=_machine_enabled(ctx, machine)) as events:
|
|
425
|
+
console = _console()
|
|
426
|
+
events.emit(
|
|
427
|
+
EVENT_RUN_STARTED,
|
|
428
|
+
{"command": "forge.review", "path": os.fspath(path), "task_id": task_id},
|
|
429
|
+
)
|
|
430
|
+
_cli_module()._require_active_subscription_ready(model=model, base_url=base_url)
|
|
431
|
+
cfg = _patchable("load_config", load_config)()
|
|
432
|
+
effective = clone_cfg(cfg)
|
|
433
|
+
if base_url is not None:
|
|
434
|
+
effective.base_url = base_url
|
|
435
|
+
if model is not None:
|
|
436
|
+
effective.model = model
|
|
437
|
+
if temperature is not None:
|
|
438
|
+
_cli_module()._apply_temperature_override(effective, temperature)
|
|
439
|
+
|
|
440
|
+
try:
|
|
441
|
+
if not effective.model:
|
|
442
|
+
raise ConfigError("Model is not set. Run: alysis config set model <MODEL>")
|
|
443
|
+
api_key_override = _cli_module()._resolve_api_key_override(
|
|
444
|
+
api_key=api_key,
|
|
445
|
+
api_key_env=api_key_env,
|
|
446
|
+
api_key_stdin=api_key_stdin,
|
|
447
|
+
)
|
|
448
|
+
paths = load_current_run_paths(path)
|
|
449
|
+
events.set_run_id(paths.run_id)
|
|
450
|
+
plan = load_plan(paths)
|
|
451
|
+
task = find_task(plan, task_id)
|
|
452
|
+
if task is None:
|
|
453
|
+
raise ForgeError(f"Task not found: {task_id}")
|
|
454
|
+
outcome = _patchable("review_task", review_task)(
|
|
455
|
+
paths=paths,
|
|
456
|
+
plan=plan,
|
|
457
|
+
task=task,
|
|
458
|
+
cfg=effective,
|
|
459
|
+
api_key_override=api_key_override,
|
|
460
|
+
)
|
|
461
|
+
except (ConfigError, ForgeError, ReviewError) as e:
|
|
462
|
+
raise _forge_error_exit(console, events, e, data={"task_id": task_id}) from e
|
|
463
|
+
|
|
464
|
+
console.print(f"Task: {task_id} ({task.get('title', '')})")
|
|
465
|
+
console.print(f"Approved: {'yes' if outcome.approved else 'no'}")
|
|
466
|
+
console.print(f"Confidence: {outcome.confidence}")
|
|
467
|
+
console.print(f"Summary: {outcome.summary}")
|
|
468
|
+
console.print(f"Review JSON: {outcome.json_path}")
|
|
469
|
+
console.print(f"Review Markdown: {outcome.markdown_path}")
|
|
470
|
+
|
|
471
|
+
review_payload = {
|
|
472
|
+
"task_id": task_id,
|
|
473
|
+
"title": str(task.get("title", "")),
|
|
474
|
+
"approved": bool(outcome.approved),
|
|
475
|
+
"confidence": outcome.confidence,
|
|
476
|
+
"summary": outcome.summary,
|
|
477
|
+
"blocking_issues": outcome.blocking_issues_count,
|
|
478
|
+
"non_blocking_issues": outcome.non_blocking_issues_count,
|
|
479
|
+
"review_json": os.fspath(outcome.json_path),
|
|
480
|
+
"review_markdown": os.fspath(outcome.markdown_path),
|
|
481
|
+
}
|
|
482
|
+
events.emit(EVENT_REVIEW_RESULT, review_payload)
|
|
483
|
+
# A review that rejects work is a review that did its job. Exit 0 and let the
|
|
484
|
+
# approved flag carry the verdict; nonzero stays reserved for command errors.
|
|
485
|
+
events.run_completed(ok=True, data={"review": review_payload})
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
@forge_app.command("run")
|
|
489
|
+
def forge_run(
|
|
490
|
+
ctx: typer.Context = None,
|
|
491
|
+
path: Path = typer.Option(
|
|
492
|
+
Path("."),
|
|
493
|
+
"--path",
|
|
494
|
+
help="Workspace path or repository subdirectory.",
|
|
495
|
+
),
|
|
496
|
+
allow_broad_workspace: bool = typer.Option(
|
|
497
|
+
False,
|
|
498
|
+
"--allow-broad-workspace",
|
|
499
|
+
help="Allow guarded broad workspaces instead of requiring a narrower project path.",
|
|
500
|
+
),
|
|
501
|
+
only: str | None = typer.Option(
|
|
502
|
+
None,
|
|
503
|
+
"--only",
|
|
504
|
+
help="Comma-separated task ids to execute (dependencies are still enforced).",
|
|
505
|
+
),
|
|
506
|
+
max_tasks: int | None = typer.Option(
|
|
507
|
+
None,
|
|
508
|
+
"--max-tasks",
|
|
509
|
+
min=1,
|
|
510
|
+
help="Stop after this many tasks have been executed.",
|
|
511
|
+
),
|
|
512
|
+
max_attempts: int | None = typer.Option(
|
|
513
|
+
None,
|
|
514
|
+
"--max-attempts",
|
|
515
|
+
min=1,
|
|
516
|
+
help="Skip tasks that already reached this many recorded attempts.",
|
|
517
|
+
),
|
|
518
|
+
retry_failed: bool = typer.Option(
|
|
519
|
+
False,
|
|
520
|
+
"--retry-failed",
|
|
521
|
+
help="Include tasks currently marked failed.",
|
|
522
|
+
),
|
|
523
|
+
retry_changes_requested: bool = typer.Option(
|
|
524
|
+
False,
|
|
525
|
+
"--retry-changes-requested",
|
|
526
|
+
help="Include tasks currently marked changes_requested.",
|
|
527
|
+
),
|
|
528
|
+
keep_going: bool = typer.Option(
|
|
529
|
+
False,
|
|
530
|
+
"--keep-going",
|
|
531
|
+
help=(
|
|
532
|
+
"Continue with the next independent task after a failure instead of stopping. "
|
|
533
|
+
"Default is to stop, because a failed task usually leaves later tasks unsound."
|
|
534
|
+
),
|
|
535
|
+
),
|
|
536
|
+
dry_run: bool = typer.Option(
|
|
537
|
+
False,
|
|
538
|
+
"--dry-run",
|
|
539
|
+
help="Print the execution order and exit without running anything.",
|
|
540
|
+
),
|
|
541
|
+
scope: str = typer.Option(
|
|
542
|
+
"strict",
|
|
543
|
+
"--scope",
|
|
544
|
+
help="Write-scope enforcement: strict by default; use warn or off to opt out.",
|
|
545
|
+
),
|
|
546
|
+
verify: str = typer.Option(
|
|
547
|
+
"warn",
|
|
548
|
+
"--verify",
|
|
549
|
+
help="Per-task verification policy: off, warn, or strict.",
|
|
550
|
+
),
|
|
551
|
+
verify_cmd: list[str] | None = typer.Option(
|
|
552
|
+
None,
|
|
553
|
+
"--verify-cmd",
|
|
554
|
+
help="Override verify command for this run (repeatable).",
|
|
555
|
+
),
|
|
556
|
+
verify_repair_attempts: int | None = typer.Option(
|
|
557
|
+
None,
|
|
558
|
+
"--verify-repair-attempts",
|
|
559
|
+
help=(
|
|
560
|
+
"How many times a failing strict verification is fed back to the agent "
|
|
561
|
+
"to repair before the task fails (default 2; 0 disables)."
|
|
562
|
+
),
|
|
563
|
+
),
|
|
564
|
+
pr: bool = typer.Option(
|
|
565
|
+
True,
|
|
566
|
+
"--pr/--no-pr",
|
|
567
|
+
help=(
|
|
568
|
+
"Run each task as a branch/commit/verify/merge cycle in the main checkout "
|
|
569
|
+
"(default). --no-pr leaves changes uncommitted and runs no verification gate, "
|
|
570
|
+
"so use it only where git flow is unavailable."
|
|
571
|
+
),
|
|
572
|
+
),
|
|
573
|
+
review: bool = typer.Option(
|
|
574
|
+
False,
|
|
575
|
+
"--review",
|
|
576
|
+
help="Run the automated review gate before merging each task (requires --pr).",
|
|
577
|
+
),
|
|
578
|
+
base_branch: str | None = typer.Option(
|
|
579
|
+
None,
|
|
580
|
+
"--base-branch",
|
|
581
|
+
help="Base branch for --pr mode (defaults to the current branch).",
|
|
582
|
+
),
|
|
583
|
+
keep_branch: bool = typer.Option(
|
|
584
|
+
False,
|
|
585
|
+
"--keep-branch",
|
|
586
|
+
help="Keep each task branch after a successful merge.",
|
|
587
|
+
),
|
|
588
|
+
auto_resolve_conflicts: bool = typer.Option(
|
|
589
|
+
False,
|
|
590
|
+
"--auto-resolve-conflicts",
|
|
591
|
+
help=(
|
|
592
|
+
"On merge conflict, start a resolver agent in a dedicated worktree instead of "
|
|
593
|
+
"stopping with a conflict report. Off by default."
|
|
594
|
+
),
|
|
595
|
+
),
|
|
596
|
+
mode: Mode | None = typer.Option(None, "--mode", help="Mode override."),
|
|
597
|
+
model: str | None = typer.Option(None, "--model", help="Model override."),
|
|
598
|
+
base_url: str | None = typer.Option(None, "--base-url", help="Base URL override."),
|
|
599
|
+
temperature: float | None = typer.Option(None, "--temperature", help="Sampling temperature."),
|
|
600
|
+
stream: bool | None = typer.Option(
|
|
601
|
+
None,
|
|
602
|
+
"--stream/--no-stream",
|
|
603
|
+
help="Enable streamed assistant output.",
|
|
604
|
+
),
|
|
605
|
+
max_steps: int | None = typer.Option(
|
|
606
|
+
None,
|
|
607
|
+
"--max-steps",
|
|
608
|
+
help="Optional safety limit on each managed agent task.",
|
|
609
|
+
),
|
|
610
|
+
no_log: bool = typer.Option(False, "--no-log", help="Disable JSONL session logging."),
|
|
611
|
+
api_key_env: str | None = typer.Option(
|
|
612
|
+
None,
|
|
613
|
+
"--api-key-env",
|
|
614
|
+
help=(
|
|
615
|
+
"Read API key from this environment variable (overrides ALYSIS_API_KEY/OPENAI_API_KEY)."
|
|
616
|
+
),
|
|
617
|
+
),
|
|
618
|
+
api_key_stdin: bool = typer.Option(
|
|
619
|
+
False,
|
|
620
|
+
"--api-key-stdin",
|
|
621
|
+
help="Prompt for API key (hidden input). Key is kept in memory for this run only.",
|
|
622
|
+
),
|
|
623
|
+
api_key: str | None = typer.Option(
|
|
624
|
+
None,
|
|
625
|
+
"--api-key",
|
|
626
|
+
help=(
|
|
627
|
+
"UNSAFE: Provide API key via CLI argument (may leak via shell history / process list). "
|
|
628
|
+
"Prefer --api-key-stdin or --api-key-env."
|
|
629
|
+
),
|
|
630
|
+
),
|
|
631
|
+
yes: bool = typer.Option(
|
|
632
|
+
False,
|
|
633
|
+
"--yes",
|
|
634
|
+
help="In auto mode, skip confirmations for sensitive commands (hard blocks still apply).",
|
|
635
|
+
),
|
|
636
|
+
machine: bool = typer.Option(False, "--machine", help=MACHINE_OPTION_HELP),
|
|
637
|
+
) -> None:
|
|
638
|
+
"""Execute every ready task sequentially, in dependency order, in this checkout.
|
|
639
|
+
|
|
640
|
+
This is the default way to execute a Forge plan. One task at a time, no
|
|
641
|
+
worktrees, no batch integration gate: each task branches, commits, verifies
|
|
642
|
+
and merges in the main checkout before the next one starts, so a failure
|
|
643
|
+
stops the run at a state you can inspect and resume from.
|
|
644
|
+
|
|
645
|
+
Use `forge swarm` instead when independent tasks should run in parallel and
|
|
646
|
+
the extra machinery (worktrees, batch integration gate, conflict resolution)
|
|
647
|
+
is worth it.
|
|
648
|
+
"""
|
|
649
|
+
with machine_session("forge.run", machine=_machine_enabled(ctx, machine)) as events:
|
|
650
|
+
events.emit(
|
|
651
|
+
EVENT_RUN_STARTED,
|
|
652
|
+
{
|
|
653
|
+
"command": "forge.run",
|
|
654
|
+
"path": os.fspath(path),
|
|
655
|
+
"parallel": 1,
|
|
656
|
+
"worktrees": False,
|
|
657
|
+
"dry_run": dry_run,
|
|
658
|
+
"only": only,
|
|
659
|
+
"max_tasks": max_tasks,
|
|
660
|
+
"pr": pr,
|
|
661
|
+
"review": review,
|
|
662
|
+
"verify": verify,
|
|
663
|
+
},
|
|
664
|
+
)
|
|
665
|
+
cli = _cli_module()
|
|
666
|
+
cli._require_active_subscription_ready(model=model, base_url=base_url)
|
|
667
|
+
from ..forge import forge_run_impl
|
|
668
|
+
|
|
669
|
+
return forge_run_impl(
|
|
670
|
+
cli,
|
|
671
|
+
path,
|
|
672
|
+
allow_broad_workspace,
|
|
673
|
+
only,
|
|
674
|
+
max_tasks,
|
|
675
|
+
max_attempts,
|
|
676
|
+
retry_failed,
|
|
677
|
+
retry_changes_requested,
|
|
678
|
+
keep_going,
|
|
679
|
+
dry_run,
|
|
680
|
+
scope,
|
|
681
|
+
verify,
|
|
682
|
+
verify_cmd,
|
|
683
|
+
verify_repair_attempts,
|
|
684
|
+
pr,
|
|
685
|
+
review,
|
|
686
|
+
base_branch,
|
|
687
|
+
keep_branch,
|
|
688
|
+
auto_resolve_conflicts,
|
|
689
|
+
mode,
|
|
690
|
+
model,
|
|
691
|
+
base_url,
|
|
692
|
+
temperature,
|
|
693
|
+
stream,
|
|
694
|
+
max_steps,
|
|
695
|
+
no_log,
|
|
696
|
+
api_key_env,
|
|
697
|
+
api_key_stdin,
|
|
698
|
+
api_key,
|
|
699
|
+
yes,
|
|
700
|
+
cli_ctx=ctx,
|
|
701
|
+
events=events,
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
@forge_app.command("unlock")
|
|
706
|
+
def forge_unlock(
|
|
707
|
+
ctx: typer.Context = None,
|
|
708
|
+
path: Path = typer.Option(
|
|
709
|
+
Path("."),
|
|
710
|
+
"--path",
|
|
711
|
+
help="Workspace path or repository subdirectory.",
|
|
712
|
+
),
|
|
713
|
+
force: bool = typer.Option(
|
|
714
|
+
False,
|
|
715
|
+
"--force",
|
|
716
|
+
help=(
|
|
717
|
+
"Clear the lock even when the owner is not provably gone. Use only when you "
|
|
718
|
+
"know that process is dead: two concurrent executions can corrupt the run."
|
|
719
|
+
),
|
|
720
|
+
),
|
|
721
|
+
machine: bool = typer.Option(False, "--machine", help=MACHINE_OPTION_HELP),
|
|
722
|
+
) -> None:
|
|
723
|
+
"""Inspect this workspace's Forge locks and clear the ones that are safe to clear.
|
|
724
|
+
|
|
725
|
+
Exit 0 when the workspace is unlocked (nothing was there, or everything stale was
|
|
726
|
+
cleared). Exit 1 when a lock was kept because its owner could still be alive --
|
|
727
|
+
that is a reportable refusal, not a command failure.
|
|
728
|
+
"""
|
|
729
|
+
with machine_session("forge.unlock", machine=_machine_enabled(ctx, machine)) as events:
|
|
730
|
+
events.emit(
|
|
731
|
+
EVENT_RUN_STARTED,
|
|
732
|
+
{"command": "forge.unlock", "path": os.fspath(path), "force": force is True},
|
|
733
|
+
)
|
|
734
|
+
from ..forge_recovery import forge_unlock_impl
|
|
735
|
+
|
|
736
|
+
return forge_unlock_impl(path=path, force=force, events=events)
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
@forge_app.command("resume")
|
|
740
|
+
def forge_resume(
|
|
741
|
+
ctx: typer.Context = None,
|
|
742
|
+
path: Path = typer.Option(
|
|
743
|
+
Path("."),
|
|
744
|
+
"--path",
|
|
745
|
+
help="Workspace path or repository subdirectory.",
|
|
746
|
+
),
|
|
747
|
+
reapprove: bool = typer.Option(
|
|
748
|
+
False,
|
|
749
|
+
"--reapprove",
|
|
750
|
+
help=(
|
|
751
|
+
"Accept the current plan when it drifted since the run was approved. "
|
|
752
|
+
"Without it, a drifted plan stops the resume and lists what changed."
|
|
753
|
+
),
|
|
754
|
+
),
|
|
755
|
+
dry_run: bool = typer.Option(
|
|
756
|
+
False,
|
|
757
|
+
"--dry-run",
|
|
758
|
+
help="Report what would be resumed and exit without executing anything.",
|
|
759
|
+
),
|
|
760
|
+
allow_broad_workspace: bool = typer.Option(
|
|
761
|
+
False,
|
|
762
|
+
"--allow-broad-workspace",
|
|
763
|
+
help="Allow guarded broad workspaces instead of requiring a narrower project path.",
|
|
764
|
+
),
|
|
765
|
+
only: str | None = typer.Option(
|
|
766
|
+
None,
|
|
767
|
+
"--only",
|
|
768
|
+
help="Comma-separated task ids to execute (dependencies are still enforced).",
|
|
769
|
+
),
|
|
770
|
+
max_tasks: int | None = typer.Option(
|
|
771
|
+
None,
|
|
772
|
+
"--max-tasks",
|
|
773
|
+
min=1,
|
|
774
|
+
help="Stop after this many tasks have been executed.",
|
|
775
|
+
),
|
|
776
|
+
max_attempts: int | None = typer.Option(
|
|
777
|
+
None,
|
|
778
|
+
"--max-attempts",
|
|
779
|
+
min=1,
|
|
780
|
+
help="Skip tasks that already reached this many recorded attempts.",
|
|
781
|
+
),
|
|
782
|
+
keep_going: bool = typer.Option(
|
|
783
|
+
False,
|
|
784
|
+
"--keep-going",
|
|
785
|
+
help="Continue with the next independent task after a failure instead of stopping.",
|
|
786
|
+
),
|
|
787
|
+
scope: str = typer.Option(
|
|
788
|
+
"strict",
|
|
789
|
+
"--scope",
|
|
790
|
+
help="Write-scope enforcement: strict by default; use warn or off to opt out.",
|
|
791
|
+
),
|
|
792
|
+
verify: str = typer.Option(
|
|
793
|
+
"warn",
|
|
794
|
+
"--verify",
|
|
795
|
+
help="Per-task verification policy: off, warn, or strict.",
|
|
796
|
+
),
|
|
797
|
+
verify_cmd: list[str] | None = typer.Option(
|
|
798
|
+
None,
|
|
799
|
+
"--verify-cmd",
|
|
800
|
+
help="Override verify command for this run (repeatable).",
|
|
801
|
+
),
|
|
802
|
+
verify_repair_attempts: int | None = typer.Option(
|
|
803
|
+
None,
|
|
804
|
+
"--verify-repair-attempts",
|
|
805
|
+
help="How many times a failing strict verification is fed back to the agent.",
|
|
806
|
+
),
|
|
807
|
+
pr: bool = typer.Option(
|
|
808
|
+
True,
|
|
809
|
+
"--pr/--no-pr",
|
|
810
|
+
help="Run each task as a branch/commit/verify/merge cycle in the main checkout.",
|
|
811
|
+
),
|
|
812
|
+
review: bool = typer.Option(
|
|
813
|
+
False,
|
|
814
|
+
"--review",
|
|
815
|
+
help="Run the automated review gate before merging each task (requires --pr).",
|
|
816
|
+
),
|
|
817
|
+
base_branch: str | None = typer.Option(
|
|
818
|
+
None,
|
|
819
|
+
"--base-branch",
|
|
820
|
+
help="Base branch for --pr mode (defaults to the current branch).",
|
|
821
|
+
),
|
|
822
|
+
keep_branch: bool = typer.Option(
|
|
823
|
+
False,
|
|
824
|
+
"--keep-branch",
|
|
825
|
+
help="Keep each task branch after a successful merge.",
|
|
826
|
+
),
|
|
827
|
+
auto_resolve_conflicts: bool = typer.Option(
|
|
828
|
+
False,
|
|
829
|
+
"--auto-resolve-conflicts",
|
|
830
|
+
help="On merge conflict, start a resolver agent instead of stopping with a report.",
|
|
831
|
+
),
|
|
832
|
+
mode: Mode | None = typer.Option(None, "--mode", help="Mode override."),
|
|
833
|
+
model: str | None = typer.Option(None, "--model", help="Model override."),
|
|
834
|
+
base_url: str | None = typer.Option(None, "--base-url", help="Base URL override."),
|
|
835
|
+
temperature: float | None = typer.Option(None, "--temperature", help="Sampling temperature."),
|
|
836
|
+
stream: bool | None = typer.Option(
|
|
837
|
+
None,
|
|
838
|
+
"--stream/--no-stream",
|
|
839
|
+
help="Enable streamed assistant output.",
|
|
840
|
+
),
|
|
841
|
+
max_steps: int | None = typer.Option(
|
|
842
|
+
None,
|
|
843
|
+
"--max-steps",
|
|
844
|
+
help="Optional safety limit on each managed agent task.",
|
|
845
|
+
),
|
|
846
|
+
no_log: bool = typer.Option(False, "--no-log", help="Disable JSONL session logging."),
|
|
847
|
+
api_key_env: str | None = typer.Option(
|
|
848
|
+
None,
|
|
849
|
+
"--api-key-env",
|
|
850
|
+
help=(
|
|
851
|
+
"Read API key from this environment variable (overrides ALYSIS_API_KEY/OPENAI_API_KEY)."
|
|
852
|
+
),
|
|
853
|
+
),
|
|
854
|
+
api_key_stdin: bool = typer.Option(
|
|
855
|
+
False,
|
|
856
|
+
"--api-key-stdin",
|
|
857
|
+
help="Prompt for API key (hidden input). Key is kept in memory for this run only.",
|
|
858
|
+
),
|
|
859
|
+
api_key: str | None = typer.Option(
|
|
860
|
+
None,
|
|
861
|
+
"--api-key",
|
|
862
|
+
help=(
|
|
863
|
+
"UNSAFE: Provide API key via CLI argument (may leak via shell history / process list). "
|
|
864
|
+
"Prefer --api-key-stdin or --api-key-env."
|
|
865
|
+
),
|
|
866
|
+
),
|
|
867
|
+
yes: bool = typer.Option(
|
|
868
|
+
False,
|
|
869
|
+
"--yes",
|
|
870
|
+
help="In auto mode, skip confirmations for sensitive commands (hard blocks still apply).",
|
|
871
|
+
),
|
|
872
|
+
machine: bool = typer.Option(False, "--machine", help=MACHINE_OPTION_HELP),
|
|
873
|
+
) -> None:
|
|
874
|
+
"""Continue an interrupted run from its last incomplete task.
|
|
875
|
+
|
|
876
|
+
Re-checks the plan against the fingerprint recorded when the run started. A plan
|
|
877
|
+
that drifted stops the resume and lists exactly what changed; `--reapprove`
|
|
878
|
+
accepts the current plan and continues. Execution itself is `forge run`, with
|
|
879
|
+
retries enabled for the statuses an interruption leaves behind.
|
|
880
|
+
"""
|
|
881
|
+
with machine_session("forge.resume", machine=_machine_enabled(ctx, machine)) as events:
|
|
882
|
+
events.emit(
|
|
883
|
+
EVENT_RUN_STARTED,
|
|
884
|
+
{
|
|
885
|
+
"command": "forge.resume",
|
|
886
|
+
"path": os.fspath(path),
|
|
887
|
+
"reapprove": reapprove is True,
|
|
888
|
+
"dry_run": dry_run is True,
|
|
889
|
+
},
|
|
890
|
+
)
|
|
891
|
+
cli = _cli_module()
|
|
892
|
+
cli._require_active_subscription_ready(model=model, base_url=base_url)
|
|
893
|
+
from ..forge_recovery import forge_resume_impl
|
|
894
|
+
|
|
895
|
+
return forge_resume_impl(
|
|
896
|
+
cli,
|
|
897
|
+
path=path,
|
|
898
|
+
reapprove=reapprove,
|
|
899
|
+
dry_run=dry_run,
|
|
900
|
+
events=events,
|
|
901
|
+
allow_broad_workspace=allow_broad_workspace,
|
|
902
|
+
only=only,
|
|
903
|
+
max_tasks=max_tasks,
|
|
904
|
+
max_attempts=max_attempts,
|
|
905
|
+
keep_going=keep_going,
|
|
906
|
+
scope=scope,
|
|
907
|
+
verify=verify,
|
|
908
|
+
verify_cmd=verify_cmd,
|
|
909
|
+
verify_repair_attempts=verify_repair_attempts,
|
|
910
|
+
pr=pr,
|
|
911
|
+
review=review,
|
|
912
|
+
base_branch=base_branch,
|
|
913
|
+
keep_branch=keep_branch,
|
|
914
|
+
auto_resolve_conflicts=auto_resolve_conflicts,
|
|
915
|
+
mode=mode,
|
|
916
|
+
model=model,
|
|
917
|
+
base_url=base_url,
|
|
918
|
+
temperature=temperature,
|
|
919
|
+
stream=stream,
|
|
920
|
+
max_steps=max_steps,
|
|
921
|
+
no_log=no_log,
|
|
922
|
+
api_key_env=api_key_env,
|
|
923
|
+
api_key_stdin=api_key_stdin,
|
|
924
|
+
api_key=api_key,
|
|
925
|
+
yes=yes,
|
|
926
|
+
cli_ctx=ctx,
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
@forge_app.command("swarm")
|
|
931
|
+
def forge_swarm(
|
|
932
|
+
ctx: typer.Context = None,
|
|
933
|
+
path: Path = typer.Option(
|
|
934
|
+
Path("."),
|
|
935
|
+
"--path",
|
|
936
|
+
help="Workspace path or repository subdirectory.",
|
|
937
|
+
),
|
|
938
|
+
allow_broad_workspace: bool = typer.Option(
|
|
939
|
+
False,
|
|
940
|
+
"--allow-broad-workspace",
|
|
941
|
+
help="Allow guarded broad workspaces instead of requiring a narrower project path.",
|
|
942
|
+
),
|
|
943
|
+
parallel: int = typer.Option(2, "--parallel", min=1, help="Parallel workers per batch."),
|
|
944
|
+
base_branch: str | None = typer.Option(
|
|
945
|
+
None,
|
|
946
|
+
"--base-branch",
|
|
947
|
+
help="Base branch (defaults to current checked out branch).",
|
|
948
|
+
),
|
|
949
|
+
max_tasks: int | None = typer.Option(
|
|
950
|
+
None,
|
|
951
|
+
"--max-tasks",
|
|
952
|
+
min=1,
|
|
953
|
+
help="Maximum number of tasks to execute in this swarm run.",
|
|
954
|
+
),
|
|
955
|
+
max_attempts: int | None = typer.Option(
|
|
956
|
+
None,
|
|
957
|
+
"--max-attempts",
|
|
958
|
+
min=1,
|
|
959
|
+
help="Maximum attempts allowed per task before scheduler skips it.",
|
|
960
|
+
),
|
|
961
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Print schedule and exit."),
|
|
962
|
+
keep_worktrees: bool = typer.Option(
|
|
963
|
+
False,
|
|
964
|
+
"--keep-worktrees",
|
|
965
|
+
help=(
|
|
966
|
+
"Keep failed task worktrees and branches for debugging; default cleanup removes "
|
|
967
|
+
"successful worktrees and rejected failed branch state."
|
|
968
|
+
),
|
|
969
|
+
),
|
|
970
|
+
retry_failed: bool = typer.Option(
|
|
971
|
+
False,
|
|
972
|
+
"--retry-failed",
|
|
973
|
+
help="Include tasks currently marked failed.",
|
|
974
|
+
),
|
|
975
|
+
retry_changes_requested: bool = typer.Option(
|
|
976
|
+
False,
|
|
977
|
+
"--retry-changes-requested",
|
|
978
|
+
help="Include tasks currently marked changes_requested.",
|
|
979
|
+
),
|
|
980
|
+
only: str | None = typer.Option(
|
|
981
|
+
None,
|
|
982
|
+
"--only",
|
|
983
|
+
help="Comma-separated task ids to execute (still enforces dependencies).",
|
|
984
|
+
),
|
|
985
|
+
retry_merge_conflicts: bool = typer.Option(
|
|
986
|
+
False,
|
|
987
|
+
"--retry-merge-conflicts",
|
|
988
|
+
help="Retry tasks marked merge_conflict during merge phase.",
|
|
989
|
+
),
|
|
990
|
+
scope: str = typer.Option(
|
|
991
|
+
"strict",
|
|
992
|
+
"--scope",
|
|
993
|
+
help="Write-scope enforcement: strict by default; use warn or off to opt out.",
|
|
994
|
+
),
|
|
995
|
+
verify: str = typer.Option(
|
|
996
|
+
"warn",
|
|
997
|
+
"--verify",
|
|
998
|
+
help="Verification policy: off, warn, or strict.",
|
|
999
|
+
),
|
|
1000
|
+
verify_cmd: list[str] | None = typer.Option(
|
|
1001
|
+
None,
|
|
1002
|
+
"--verify-cmd",
|
|
1003
|
+
help="Override verify command for this run (repeatable).",
|
|
1004
|
+
),
|
|
1005
|
+
integration_verify: str | None = typer.Option(
|
|
1006
|
+
None,
|
|
1007
|
+
"--integration-verify",
|
|
1008
|
+
help="Batch integration verification policy: off, warn, or strict (defaults to config: warn).",
|
|
1009
|
+
),
|
|
1010
|
+
integration_verify_cmd: list[str] | None = typer.Option(
|
|
1011
|
+
None,
|
|
1012
|
+
"--integration-verify-cmd",
|
|
1013
|
+
help="Override integration verify command for this swarm run (repeatable).",
|
|
1014
|
+
),
|
|
1015
|
+
replan: str | None = typer.Option(
|
|
1016
|
+
None,
|
|
1017
|
+
"--replan",
|
|
1018
|
+
help="Between-batch replanning mode: off, suggest, or apply.",
|
|
1019
|
+
),
|
|
1020
|
+
review: bool = typer.Option(
|
|
1021
|
+
False,
|
|
1022
|
+
"--review",
|
|
1023
|
+
help="Run automated PR review gate before merging task branches.",
|
|
1024
|
+
),
|
|
1025
|
+
mode: Mode | None = typer.Option(None, "--mode", help="Mode override."),
|
|
1026
|
+
model: str | None = typer.Option(None, "--model", help="Model override."),
|
|
1027
|
+
base_url: str | None = typer.Option(None, "--base-url", help="Base URL override."),
|
|
1028
|
+
temperature: float | None = typer.Option(None, "--temperature", help="Sampling temperature."),
|
|
1029
|
+
stream: bool | None = typer.Option(
|
|
1030
|
+
None,
|
|
1031
|
+
"--stream/--no-stream",
|
|
1032
|
+
help="Enable streamed assistant output.",
|
|
1033
|
+
),
|
|
1034
|
+
max_steps: int | None = typer.Option(
|
|
1035
|
+
None,
|
|
1036
|
+
"--max-steps",
|
|
1037
|
+
help="Optional safety limit on each managed agent task.",
|
|
1038
|
+
),
|
|
1039
|
+
no_log: bool = typer.Option(False, "--no-log", help="Disable JSONL session logging."),
|
|
1040
|
+
api_key_env: str | None = typer.Option(
|
|
1041
|
+
None,
|
|
1042
|
+
"--api-key-env",
|
|
1043
|
+
help=(
|
|
1044
|
+
"Read API key from this environment variable (overrides ALYSIS_API_KEY/OPENAI_API_KEY)."
|
|
1045
|
+
),
|
|
1046
|
+
),
|
|
1047
|
+
api_key_stdin: bool = typer.Option(
|
|
1048
|
+
False,
|
|
1049
|
+
"--api-key-stdin",
|
|
1050
|
+
help="Prompt for API key (hidden input). Key is kept in memory for this run only.",
|
|
1051
|
+
),
|
|
1052
|
+
api_key: str | None = typer.Option(
|
|
1053
|
+
None,
|
|
1054
|
+
"--api-key",
|
|
1055
|
+
help=(
|
|
1056
|
+
"UNSAFE: Provide API key via CLI argument (may leak via shell history / process list). "
|
|
1057
|
+
"Prefer --api-key-stdin or --api-key-env."
|
|
1058
|
+
),
|
|
1059
|
+
),
|
|
1060
|
+
yes: bool = typer.Option(
|
|
1061
|
+
False,
|
|
1062
|
+
"--yes",
|
|
1063
|
+
help="In auto mode, skip confirmations for sensitive commands (hard blocks still apply).",
|
|
1064
|
+
),
|
|
1065
|
+
machine: bool = typer.Option(False, "--machine", help=MACHINE_OPTION_HELP),
|
|
1066
|
+
) -> None:
|
|
1067
|
+
"""Execute independent ready tasks in parallel, each in its own git worktree.
|
|
1068
|
+
|
|
1069
|
+
This is the parallel optimization, not the default way to execute a plan. It
|
|
1070
|
+
brings machinery `forge run` does not have -- per-task worktrees, a batch
|
|
1071
|
+
integration gate, and agent-driven merge-conflict resolution -- so it pays off
|
|
1072
|
+
only when the plan really does have independent tasks with non-overlapping
|
|
1073
|
+
write scopes.
|
|
1074
|
+
|
|
1075
|
+
For a dependency chain, or when you want one linear log and a stop-and-inspect
|
|
1076
|
+
failure, use `forge run`. `--parallel 1` delegates there automatically.
|
|
1077
|
+
"""
|
|
1078
|
+
with machine_session("forge.swarm", machine=_machine_enabled(ctx, machine)) as events:
|
|
1079
|
+
events.emit(
|
|
1080
|
+
EVENT_RUN_STARTED,
|
|
1081
|
+
{
|
|
1082
|
+
"command": "forge.swarm",
|
|
1083
|
+
"path": os.fspath(path),
|
|
1084
|
+
"parallel": parallel,
|
|
1085
|
+
"dry_run": dry_run,
|
|
1086
|
+
"only": only,
|
|
1087
|
+
"max_tasks": max_tasks,
|
|
1088
|
+
"review": review,
|
|
1089
|
+
"verify": verify,
|
|
1090
|
+
},
|
|
1091
|
+
)
|
|
1092
|
+
cli = _cli_module()
|
|
1093
|
+
cli._require_active_subscription_ready(model=model, base_url=base_url)
|
|
1094
|
+
from ..forge import forge_swarm_impl
|
|
1095
|
+
|
|
1096
|
+
return forge_swarm_impl(
|
|
1097
|
+
cli,
|
|
1098
|
+
path,
|
|
1099
|
+
allow_broad_workspace,
|
|
1100
|
+
parallel,
|
|
1101
|
+
base_branch,
|
|
1102
|
+
max_tasks,
|
|
1103
|
+
max_attempts,
|
|
1104
|
+
dry_run,
|
|
1105
|
+
keep_worktrees,
|
|
1106
|
+
retry_failed,
|
|
1107
|
+
retry_changes_requested,
|
|
1108
|
+
only,
|
|
1109
|
+
retry_merge_conflicts,
|
|
1110
|
+
scope,
|
|
1111
|
+
verify,
|
|
1112
|
+
verify_cmd,
|
|
1113
|
+
integration_verify,
|
|
1114
|
+
integration_verify_cmd,
|
|
1115
|
+
replan,
|
|
1116
|
+
review,
|
|
1117
|
+
mode,
|
|
1118
|
+
model,
|
|
1119
|
+
base_url,
|
|
1120
|
+
temperature,
|
|
1121
|
+
stream,
|
|
1122
|
+
max_steps,
|
|
1123
|
+
no_log,
|
|
1124
|
+
api_key_env,
|
|
1125
|
+
api_key_stdin,
|
|
1126
|
+
api_key,
|
|
1127
|
+
yes,
|
|
1128
|
+
cli_ctx=ctx,
|
|
1129
|
+
events=events,
|
|
1130
|
+
)
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
@forge_app.command("exec")
|
|
1134
|
+
def forge_exec(
|
|
1135
|
+
ctx: typer.Context = None,
|
|
1136
|
+
task_id: str = typer.Argument(..., help="Task id from plan.json (for example T01)."),
|
|
1137
|
+
path: Path = typer.Option(
|
|
1138
|
+
Path("."),
|
|
1139
|
+
"--path",
|
|
1140
|
+
help="Workspace path or repository subdirectory.",
|
|
1141
|
+
),
|
|
1142
|
+
mode: Mode | None = typer.Option(None, "--mode", help="Mode override."),
|
|
1143
|
+
model: str | None = typer.Option(None, "--model", help="Model override."),
|
|
1144
|
+
base_url: str | None = typer.Option(None, "--base-url", help="Base URL override."),
|
|
1145
|
+
temperature: float | None = typer.Option(None, "--temperature", help="Sampling temperature."),
|
|
1146
|
+
stream: bool | None = typer.Option(
|
|
1147
|
+
None,
|
|
1148
|
+
"--stream/--no-stream",
|
|
1149
|
+
help="Enable streamed assistant output.",
|
|
1150
|
+
),
|
|
1151
|
+
max_steps: int | None = typer.Option(
|
|
1152
|
+
None,
|
|
1153
|
+
"--max-steps",
|
|
1154
|
+
help="Optional safety limit on each managed agent task.",
|
|
1155
|
+
),
|
|
1156
|
+
no_log: bool = typer.Option(False, "--no-log", help="Disable JSONL session logging."),
|
|
1157
|
+
api_key_env: str | None = typer.Option(
|
|
1158
|
+
None,
|
|
1159
|
+
"--api-key-env",
|
|
1160
|
+
help=(
|
|
1161
|
+
"Read API key from this environment variable (overrides ALYSIS_API_KEY/OPENAI_API_KEY)."
|
|
1162
|
+
),
|
|
1163
|
+
),
|
|
1164
|
+
api_key_stdin: bool = typer.Option(
|
|
1165
|
+
False,
|
|
1166
|
+
"--api-key-stdin",
|
|
1167
|
+
help="Prompt for API key (hidden input). Key is kept in memory for this run only.",
|
|
1168
|
+
),
|
|
1169
|
+
api_key: str | None = typer.Option(
|
|
1170
|
+
None,
|
|
1171
|
+
"--api-key",
|
|
1172
|
+
help=(
|
|
1173
|
+
"UNSAFE: Provide API key via CLI argument (may leak via shell history / process list). "
|
|
1174
|
+
"Prefer --api-key-stdin or --api-key-env."
|
|
1175
|
+
),
|
|
1176
|
+
),
|
|
1177
|
+
pr: bool = typer.Option(
|
|
1178
|
+
False,
|
|
1179
|
+
"--pr/--no-pr",
|
|
1180
|
+
help="Run task in PR-like git flow (branch, commit, patch, merge).",
|
|
1181
|
+
),
|
|
1182
|
+
review: bool = typer.Option(
|
|
1183
|
+
False,
|
|
1184
|
+
"--review",
|
|
1185
|
+
help="Run automated PR review gate before merge (requires --pr).",
|
|
1186
|
+
),
|
|
1187
|
+
base_branch: str | None = typer.Option(
|
|
1188
|
+
None,
|
|
1189
|
+
"--base-branch",
|
|
1190
|
+
help="Base branch for --pr mode (defaults to current branch).",
|
|
1191
|
+
),
|
|
1192
|
+
keep_branch: bool = typer.Option(
|
|
1193
|
+
False,
|
|
1194
|
+
"--keep-branch",
|
|
1195
|
+
help="Keep task branch after successful merge in --pr mode.",
|
|
1196
|
+
),
|
|
1197
|
+
scope: str = typer.Option(
|
|
1198
|
+
"strict",
|
|
1199
|
+
"--scope",
|
|
1200
|
+
help="Write-scope enforcement: strict by default; use warn or off to opt out.",
|
|
1201
|
+
),
|
|
1202
|
+
verify: str = typer.Option(
|
|
1203
|
+
"warn",
|
|
1204
|
+
"--verify",
|
|
1205
|
+
help="Verification policy for PR flow: off, warn, or strict.",
|
|
1206
|
+
),
|
|
1207
|
+
verify_cmd: list[str] | None = typer.Option(
|
|
1208
|
+
None,
|
|
1209
|
+
"--verify-cmd",
|
|
1210
|
+
help="Override verify command for this run (repeatable).",
|
|
1211
|
+
),
|
|
1212
|
+
verify_repair_attempts: int | None = typer.Option(
|
|
1213
|
+
None,
|
|
1214
|
+
"--verify-repair-attempts",
|
|
1215
|
+
help=(
|
|
1216
|
+
"How many times a failing strict verification is fed back to the agent "
|
|
1217
|
+
"to repair before the task fails (default 2; 0 disables)."
|
|
1218
|
+
),
|
|
1219
|
+
),
|
|
1220
|
+
auto_resolve_conflicts: bool = typer.Option(
|
|
1221
|
+
False,
|
|
1222
|
+
"--auto-resolve-conflicts",
|
|
1223
|
+
help=(
|
|
1224
|
+
"On merge conflict, start a resolver agent in a dedicated worktree instead of "
|
|
1225
|
+
"stopping with a conflict report. Off by default: the sequential path reports "
|
|
1226
|
+
"the conflict and how to land it."
|
|
1227
|
+
),
|
|
1228
|
+
),
|
|
1229
|
+
yes: bool = typer.Option(
|
|
1230
|
+
False,
|
|
1231
|
+
"--yes",
|
|
1232
|
+
help="In auto mode, skip confirmations for sensitive commands (hard blocks still apply).",
|
|
1233
|
+
),
|
|
1234
|
+
machine: bool = typer.Option(False, "--machine", help=MACHINE_OPTION_HELP),
|
|
1235
|
+
) -> None:
|
|
1236
|
+
"""Execute one plan task in this checkout.
|
|
1237
|
+
|
|
1238
|
+
Same execution core as `forge run`, which is just this applied to every ready
|
|
1239
|
+
task in dependency order. Use `forge run` to execute a whole plan.
|
|
1240
|
+
"""
|
|
1241
|
+
with machine_session("forge.exec", machine=_machine_enabled(ctx, machine)) as events:
|
|
1242
|
+
events.emit(
|
|
1243
|
+
EVENT_RUN_STARTED,
|
|
1244
|
+
{
|
|
1245
|
+
"command": "forge.exec",
|
|
1246
|
+
"path": os.fspath(path),
|
|
1247
|
+
"task_id": task_id,
|
|
1248
|
+
"pr": pr,
|
|
1249
|
+
"review": review,
|
|
1250
|
+
"verify": verify,
|
|
1251
|
+
},
|
|
1252
|
+
)
|
|
1253
|
+
cli = _cli_module()
|
|
1254
|
+
cli._require_active_subscription_ready(model=model, base_url=base_url)
|
|
1255
|
+
from ..forge import forge_exec_impl
|
|
1256
|
+
|
|
1257
|
+
return forge_exec_impl(
|
|
1258
|
+
cli,
|
|
1259
|
+
task_id,
|
|
1260
|
+
path,
|
|
1261
|
+
mode,
|
|
1262
|
+
model,
|
|
1263
|
+
base_url,
|
|
1264
|
+
temperature,
|
|
1265
|
+
stream,
|
|
1266
|
+
max_steps,
|
|
1267
|
+
no_log,
|
|
1268
|
+
api_key_env,
|
|
1269
|
+
api_key_stdin,
|
|
1270
|
+
api_key,
|
|
1271
|
+
pr,
|
|
1272
|
+
review,
|
|
1273
|
+
base_branch,
|
|
1274
|
+
keep_branch,
|
|
1275
|
+
scope,
|
|
1276
|
+
verify,
|
|
1277
|
+
verify_cmd,
|
|
1278
|
+
yes,
|
|
1279
|
+
verify_repair_attempts=verify_repair_attempts,
|
|
1280
|
+
auto_resolve_conflicts=auto_resolve_conflicts,
|
|
1281
|
+
events=events,
|
|
1282
|
+
)
|