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,926 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Protocol
|
|
7
|
+
|
|
8
|
+
from . import workspace_isolation as _workspace_isolation
|
|
9
|
+
from .execution_shared import copy_workspace_snapshot, sync_snapshot_changed_files
|
|
10
|
+
from .forge import RunPaths
|
|
11
|
+
from .git_ops import GitOpsError, ensure_runtime_artifact_excludes
|
|
12
|
+
from .runtime_artifacts import is_runtime_artifact_path
|
|
13
|
+
|
|
14
|
+
_cleanup_workspace_path = _workspace_isolation._cleanup_workspace_path
|
|
15
|
+
_inspect_existing_git_workspace = _workspace_isolation._inspect_existing_git_workspace
|
|
16
|
+
_reset_git_workspace_to_target = _workspace_isolation._reset_git_workspace_to_target
|
|
17
|
+
_retry_readonly_removal = _workspace_isolation._retry_readonly_removal
|
|
18
|
+
_run_git_checked = _workspace_isolation._run_git_checked
|
|
19
|
+
_sanitize_existing_git_workspace = _workspace_isolation._sanitize_existing_git_workspace
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class SwarmBackendStartupResult:
|
|
24
|
+
warnings: list[str]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class PreparedTaskWorkspace:
|
|
29
|
+
backend_name: str
|
|
30
|
+
task_id: str
|
|
31
|
+
branch: str
|
|
32
|
+
base_branch: str
|
|
33
|
+
worktree_path: Path
|
|
34
|
+
control_root: Path
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class PreparedBatchCandidateWorkspace:
|
|
39
|
+
backend_name: str
|
|
40
|
+
batch_label: str
|
|
41
|
+
branch: str
|
|
42
|
+
base_branch: str
|
|
43
|
+
worktree_path: Path
|
|
44
|
+
control_root: Path
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class SwarmApplyResult:
|
|
49
|
+
backend_name: str
|
|
50
|
+
task_id: str
|
|
51
|
+
branch: str
|
|
52
|
+
merge_commit_hash: str
|
|
53
|
+
action: str = "merged"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SwarmBackend(Protocol):
|
|
57
|
+
name: str
|
|
58
|
+
capabilities: frozenset[str]
|
|
59
|
+
requires_head_commit: bool
|
|
60
|
+
supports_remote_sync: bool
|
|
61
|
+
|
|
62
|
+
def prepare_startup(self, root: Path) -> SwarmBackendStartupResult: ...
|
|
63
|
+
|
|
64
|
+
def prune(self, root: Path) -> None: ...
|
|
65
|
+
|
|
66
|
+
def has_head_commit(self, root: Path) -> bool: ...
|
|
67
|
+
|
|
68
|
+
def default_base_branch(self, root: Path) -> str: ...
|
|
69
|
+
|
|
70
|
+
def branch_exists(self, root: Path, branch: str) -> bool: ...
|
|
71
|
+
|
|
72
|
+
def task_workspace_path(self, *, run_dir: Path, task_id: str) -> Path: ...
|
|
73
|
+
|
|
74
|
+
def candidate_workspace_path(self, *, run_dir: Path, batch_label: str) -> Path: ...
|
|
75
|
+
|
|
76
|
+
def prepare_task_workspace(
|
|
77
|
+
self,
|
|
78
|
+
*,
|
|
79
|
+
root: Path,
|
|
80
|
+
run_dir: Path,
|
|
81
|
+
task_id: str,
|
|
82
|
+
branch: str,
|
|
83
|
+
base_branch: str,
|
|
84
|
+
) -> PreparedTaskWorkspace: ...
|
|
85
|
+
|
|
86
|
+
def prepare_candidate_workspace(
|
|
87
|
+
self,
|
|
88
|
+
*,
|
|
89
|
+
root: Path,
|
|
90
|
+
run_dir: Path,
|
|
91
|
+
batch_label: str,
|
|
92
|
+
branch: str,
|
|
93
|
+
base_branch: str,
|
|
94
|
+
) -> PreparedBatchCandidateWorkspace: ...
|
|
95
|
+
|
|
96
|
+
def load_task_workspace(
|
|
97
|
+
self,
|
|
98
|
+
*,
|
|
99
|
+
root: Path,
|
|
100
|
+
run_dir: Path,
|
|
101
|
+
task_id: str,
|
|
102
|
+
branch: str,
|
|
103
|
+
base_branch: str,
|
|
104
|
+
) -> PreparedTaskWorkspace: ...
|
|
105
|
+
|
|
106
|
+
def apply_task_success(
|
|
107
|
+
self,
|
|
108
|
+
*,
|
|
109
|
+
root: Path,
|
|
110
|
+
prepared_workspace: PreparedTaskWorkspace,
|
|
111
|
+
message: str,
|
|
112
|
+
changed_files: list[str] | None = None,
|
|
113
|
+
) -> SwarmApplyResult: ...
|
|
114
|
+
|
|
115
|
+
def apply_task_to_candidate(
|
|
116
|
+
self,
|
|
117
|
+
*,
|
|
118
|
+
root: Path,
|
|
119
|
+
candidate_workspace: PreparedBatchCandidateWorkspace,
|
|
120
|
+
prepared_workspace: PreparedTaskWorkspace,
|
|
121
|
+
message: str,
|
|
122
|
+
changed_files: list[str] | None = None,
|
|
123
|
+
) -> None: ...
|
|
124
|
+
|
|
125
|
+
def cleanup_task_workspace(
|
|
126
|
+
self,
|
|
127
|
+
*,
|
|
128
|
+
root: Path,
|
|
129
|
+
prepared_workspace: PreparedTaskWorkspace,
|
|
130
|
+
keep_worktrees: bool,
|
|
131
|
+
) -> list[str]: ...
|
|
132
|
+
|
|
133
|
+
def cleanup_candidate_workspace(
|
|
134
|
+
self,
|
|
135
|
+
*,
|
|
136
|
+
root: Path,
|
|
137
|
+
candidate_workspace: PreparedBatchCandidateWorkspace,
|
|
138
|
+
keep_worktrees: bool,
|
|
139
|
+
) -> list[str]: ...
|
|
140
|
+
|
|
141
|
+
def cleanup_failed_task_workspace(
|
|
142
|
+
self,
|
|
143
|
+
*,
|
|
144
|
+
root: Path,
|
|
145
|
+
prepared_workspace: PreparedTaskWorkspace,
|
|
146
|
+
keep_worktrees: bool,
|
|
147
|
+
) -> list[str]: ...
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _require_materialized_candidate_workspace(
|
|
151
|
+
*,
|
|
152
|
+
worktree_path: Path,
|
|
153
|
+
expected_branch: str,
|
|
154
|
+
) -> None:
|
|
155
|
+
if not worktree_path.exists():
|
|
156
|
+
raise GitOpsError(
|
|
157
|
+
"candidate workspace was not materialized by ensure_worktree_fn; "
|
|
158
|
+
f"expected a git worktree at {worktree_path} on branch {expected_branch}"
|
|
159
|
+
)
|
|
160
|
+
valid, _clean, reason = _inspect_existing_git_workspace(
|
|
161
|
+
worktree_path=worktree_path,
|
|
162
|
+
expected_branch=expected_branch,
|
|
163
|
+
)
|
|
164
|
+
if valid:
|
|
165
|
+
return
|
|
166
|
+
raise GitOpsError(
|
|
167
|
+
"candidate workspace was not materialized correctly by ensure_worktree_fn: "
|
|
168
|
+
f"{reason}. Expected a git worktree at {worktree_path} on branch {expected_branch}"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _tracked_runtime_artifact_paths(root: Path) -> list[str]:
|
|
173
|
+
cp = _run_git_checked(
|
|
174
|
+
root,
|
|
175
|
+
["ls-files", "-z"],
|
|
176
|
+
error_message="failed to inspect tracked files",
|
|
177
|
+
)
|
|
178
|
+
return sorted(
|
|
179
|
+
path
|
|
180
|
+
for path in cp.stdout.split("\0")
|
|
181
|
+
if path.strip() and is_runtime_artifact_path(path, root=root)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _failed_cleanup_marker_path(*, worktree_path: Path) -> Path:
|
|
186
|
+
return worktree_path.parent / "failed_cleanup.json"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@dataclass(frozen=True)
|
|
190
|
+
class GitWorktreeSwarmBackend:
|
|
191
|
+
ensure_worktree_fn: Any
|
|
192
|
+
merge_runner: Any
|
|
193
|
+
remove_worktree_fn: Any
|
|
194
|
+
delete_branch_fn: Any
|
|
195
|
+
prune_fn: Any
|
|
196
|
+
branch_exists_fn: Any
|
|
197
|
+
current_branch_fn: Any
|
|
198
|
+
has_head_commit_fn: Any
|
|
199
|
+
|
|
200
|
+
name: str = "git_worktree"
|
|
201
|
+
capabilities: frozenset[str] = frozenset({"git", "merge", "worktree"})
|
|
202
|
+
requires_head_commit: bool = True
|
|
203
|
+
supports_remote_sync: bool = True
|
|
204
|
+
|
|
205
|
+
def _clear_failed_cleanup_marker(self, *, worktree_path: Path) -> None:
|
|
206
|
+
marker_path = _failed_cleanup_marker_path(worktree_path=worktree_path)
|
|
207
|
+
if not marker_path.exists():
|
|
208
|
+
return
|
|
209
|
+
try:
|
|
210
|
+
marker_path.unlink()
|
|
211
|
+
except OSError as e:
|
|
212
|
+
raise GitOpsError(f"failed to remove cleanup marker {marker_path}: {e}") from e
|
|
213
|
+
|
|
214
|
+
def _write_failed_cleanup_marker(
|
|
215
|
+
self,
|
|
216
|
+
*,
|
|
217
|
+
worktree_path: Path,
|
|
218
|
+
branch: str,
|
|
219
|
+
cleanup_errors: list[str],
|
|
220
|
+
unresolved_state: list[str],
|
|
221
|
+
) -> None:
|
|
222
|
+
marker_path = _failed_cleanup_marker_path(worktree_path=worktree_path)
|
|
223
|
+
marker_path.parent.mkdir(parents=True, exist_ok=True)
|
|
224
|
+
payload = {
|
|
225
|
+
"branch": branch,
|
|
226
|
+
"cleanup_errors": cleanup_errors,
|
|
227
|
+
"unresolved_state": unresolved_state,
|
|
228
|
+
}
|
|
229
|
+
try:
|
|
230
|
+
marker_path.write_text(
|
|
231
|
+
json.dumps(payload, indent=2, sort_keys=True) + "\n",
|
|
232
|
+
encoding="utf-8",
|
|
233
|
+
)
|
|
234
|
+
except OSError as e:
|
|
235
|
+
raise GitOpsError(f"failed to write cleanup marker {marker_path}: {e}") from e
|
|
236
|
+
|
|
237
|
+
def _record_failed_cleanup_state(
|
|
238
|
+
self,
|
|
239
|
+
*,
|
|
240
|
+
root: Path,
|
|
241
|
+
worktree_path: Path,
|
|
242
|
+
branch: str,
|
|
243
|
+
cleanup_errors: list[str],
|
|
244
|
+
) -> list[str]:
|
|
245
|
+
unresolved_state: list[str] = []
|
|
246
|
+
if worktree_path.exists():
|
|
247
|
+
unresolved_state.append(f"worktree path still exists: {worktree_path}")
|
|
248
|
+
branch_name = str(branch or "").strip()
|
|
249
|
+
if branch_name:
|
|
250
|
+
try:
|
|
251
|
+
if self.branch_exists(root, branch_name):
|
|
252
|
+
unresolved_state.append(f"task branch still exists: {branch_name}")
|
|
253
|
+
except Exception as e: # noqa: BLE001
|
|
254
|
+
cleanup_errors.append(f"branch existence check failed: {e}")
|
|
255
|
+
if cleanup_errors or unresolved_state:
|
|
256
|
+
self._write_failed_cleanup_marker(
|
|
257
|
+
worktree_path=worktree_path,
|
|
258
|
+
branch=branch_name,
|
|
259
|
+
cleanup_errors=list(cleanup_errors),
|
|
260
|
+
unresolved_state=unresolved_state,
|
|
261
|
+
)
|
|
262
|
+
return unresolved_state
|
|
263
|
+
self._clear_failed_cleanup_marker(worktree_path=worktree_path)
|
|
264
|
+
return []
|
|
265
|
+
|
|
266
|
+
def _cleanup_failed_branch(self, *, root: Path, branch: str) -> None:
|
|
267
|
+
branch_name = str(branch or "").strip()
|
|
268
|
+
if not branch_name or not self.branch_exists(root, branch_name):
|
|
269
|
+
return
|
|
270
|
+
try:
|
|
271
|
+
self.delete_branch_fn(root, branch_name)
|
|
272
|
+
except Exception: # noqa: BLE001
|
|
273
|
+
if not self.branch_exists(root, branch_name):
|
|
274
|
+
return
|
|
275
|
+
_run_git_checked(
|
|
276
|
+
root,
|
|
277
|
+
["branch", "-D", branch_name],
|
|
278
|
+
error_message=f"failed to force delete rejected task branch {branch_name}",
|
|
279
|
+
)
|
|
280
|
+
return
|
|
281
|
+
if self.branch_exists(root, branch_name):
|
|
282
|
+
_run_git_checked(
|
|
283
|
+
root,
|
|
284
|
+
["branch", "-D", branch_name],
|
|
285
|
+
error_message=f"failed to force delete rejected task branch {branch_name}",
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
def _retry_incomplete_failed_cleanup(
|
|
289
|
+
self,
|
|
290
|
+
*,
|
|
291
|
+
root: Path,
|
|
292
|
+
worktree_path: Path,
|
|
293
|
+
branch: str,
|
|
294
|
+
) -> None:
|
|
295
|
+
marker_path = _failed_cleanup_marker_path(worktree_path=worktree_path)
|
|
296
|
+
if not marker_path.exists():
|
|
297
|
+
return
|
|
298
|
+
try:
|
|
299
|
+
original_marker_payload = json.loads(marker_path.read_text(encoding="utf-8"))
|
|
300
|
+
except Exception: # noqa: BLE001
|
|
301
|
+
original_marker_payload = None
|
|
302
|
+
|
|
303
|
+
retry_errors: list[str] = []
|
|
304
|
+
if worktree_path.exists():
|
|
305
|
+
try:
|
|
306
|
+
self.remove_worktree_fn(
|
|
307
|
+
root=root,
|
|
308
|
+
worktree_repo_path=worktree_path,
|
|
309
|
+
force=True,
|
|
310
|
+
)
|
|
311
|
+
except Exception as e: # noqa: BLE001
|
|
312
|
+
retry_errors.append(f"worktree cleanup failed: {e}")
|
|
313
|
+
if branch:
|
|
314
|
+
try:
|
|
315
|
+
self._cleanup_failed_branch(root=root, branch=branch)
|
|
316
|
+
except Exception as e: # noqa: BLE001
|
|
317
|
+
retry_errors.append(f"branch cleanup failed: {e}")
|
|
318
|
+
|
|
319
|
+
unresolved_state = self._record_failed_cleanup_state(
|
|
320
|
+
root=root,
|
|
321
|
+
worktree_path=worktree_path,
|
|
322
|
+
branch=branch,
|
|
323
|
+
cleanup_errors=retry_errors,
|
|
324
|
+
)
|
|
325
|
+
if retry_errors or unresolved_state:
|
|
326
|
+
marker_note = f" See {marker_path} for details."
|
|
327
|
+
previous_note = ""
|
|
328
|
+
if isinstance(original_marker_payload, dict):
|
|
329
|
+
previous_errors = original_marker_payload.get("cleanup_errors")
|
|
330
|
+
if isinstance(previous_errors, list) and previous_errors:
|
|
331
|
+
previous_note = f" Previous cleanup errors: {'; '.join(str(item) for item in previous_errors)}."
|
|
332
|
+
unresolved_note = ""
|
|
333
|
+
if unresolved_state:
|
|
334
|
+
unresolved_note = f" Unresolved state: {'; '.join(unresolved_state)}."
|
|
335
|
+
retry_note = ""
|
|
336
|
+
if retry_errors:
|
|
337
|
+
retry_note = f" Retry cleanup errors: {'; '.join(retry_errors)}."
|
|
338
|
+
raise GitOpsError(
|
|
339
|
+
"previous failed task cleanup left unresolved git-worktree state; rerun blocked "
|
|
340
|
+
f"until cleanup succeeds.{previous_note}{retry_note}{unresolved_note}{marker_note}"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
def prepare_startup(self, root: Path) -> SwarmBackendStartupResult:
|
|
344
|
+
warnings: list[str] = []
|
|
345
|
+
try:
|
|
346
|
+
self.prune(root)
|
|
347
|
+
except Exception as e: # noqa: BLE001
|
|
348
|
+
warnings.append(f"git worktree prune failed: {e}")
|
|
349
|
+
return SwarmBackendStartupResult(warnings=warnings)
|
|
350
|
+
|
|
351
|
+
def prune(self, root: Path) -> None:
|
|
352
|
+
self.prune_fn(root)
|
|
353
|
+
|
|
354
|
+
def has_head_commit(self, root: Path) -> bool:
|
|
355
|
+
return bool(self.has_head_commit_fn(root))
|
|
356
|
+
|
|
357
|
+
def default_base_branch(self, root: Path) -> str:
|
|
358
|
+
return str(self.current_branch_fn(root))
|
|
359
|
+
|
|
360
|
+
def branch_exists(self, root: Path, branch: str) -> bool:
|
|
361
|
+
return bool(self.branch_exists_fn(root, branch))
|
|
362
|
+
|
|
363
|
+
def task_workspace_path(self, *, run_dir: Path, task_id: str) -> Path:
|
|
364
|
+
return run_dir / "worktrees" / task_id / "repo"
|
|
365
|
+
|
|
366
|
+
def candidate_workspace_path(self, *, run_dir: Path, batch_label: str) -> Path:
|
|
367
|
+
return run_dir / "worktrees" / "_batch_candidates" / batch_label / "repo"
|
|
368
|
+
|
|
369
|
+
def prepare_task_workspace(
|
|
370
|
+
self,
|
|
371
|
+
*,
|
|
372
|
+
root: Path,
|
|
373
|
+
run_dir: Path,
|
|
374
|
+
task_id: str,
|
|
375
|
+
branch: str,
|
|
376
|
+
base_branch: str,
|
|
377
|
+
) -> PreparedTaskWorkspace:
|
|
378
|
+
worktree_path = self.task_workspace_path(run_dir=run_dir, task_id=task_id)
|
|
379
|
+
self._retry_incomplete_failed_cleanup(
|
|
380
|
+
root=root,
|
|
381
|
+
worktree_path=worktree_path,
|
|
382
|
+
branch=branch,
|
|
383
|
+
)
|
|
384
|
+
self.ensure_worktree_fn(
|
|
385
|
+
root=root,
|
|
386
|
+
worktree_repo_path=worktree_path,
|
|
387
|
+
branch=branch,
|
|
388
|
+
base_branch=base_branch,
|
|
389
|
+
)
|
|
390
|
+
return PreparedTaskWorkspace(
|
|
391
|
+
backend_name=self.name,
|
|
392
|
+
task_id=task_id,
|
|
393
|
+
branch=branch,
|
|
394
|
+
base_branch=base_branch,
|
|
395
|
+
worktree_path=worktree_path,
|
|
396
|
+
control_root=root,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
def prepare_candidate_workspace(
|
|
400
|
+
self,
|
|
401
|
+
*,
|
|
402
|
+
root: Path,
|
|
403
|
+
run_dir: Path,
|
|
404
|
+
batch_label: str,
|
|
405
|
+
branch: str,
|
|
406
|
+
base_branch: str,
|
|
407
|
+
) -> PreparedBatchCandidateWorkspace:
|
|
408
|
+
worktree_path = self.candidate_workspace_path(run_dir=run_dir, batch_label=batch_label)
|
|
409
|
+
self._retry_incomplete_failed_cleanup(
|
|
410
|
+
root=root,
|
|
411
|
+
worktree_path=worktree_path,
|
|
412
|
+
branch=branch,
|
|
413
|
+
)
|
|
414
|
+
self.ensure_worktree_fn(
|
|
415
|
+
root=root,
|
|
416
|
+
worktree_repo_path=worktree_path,
|
|
417
|
+
branch=branch,
|
|
418
|
+
base_branch=base_branch,
|
|
419
|
+
)
|
|
420
|
+
# Candidate verification runs real git operations inside the candidate repo.
|
|
421
|
+
# Custom ensure_worktree_fn implementations must materialize the repo path
|
|
422
|
+
# and check out the requested candidate branch before returning.
|
|
423
|
+
_require_materialized_candidate_workspace(
|
|
424
|
+
worktree_path=worktree_path,
|
|
425
|
+
expected_branch=branch,
|
|
426
|
+
)
|
|
427
|
+
_reset_git_workspace_to_target(worktree_path=worktree_path, target=base_branch)
|
|
428
|
+
return PreparedBatchCandidateWorkspace(
|
|
429
|
+
backend_name=self.name,
|
|
430
|
+
batch_label=batch_label,
|
|
431
|
+
branch=branch,
|
|
432
|
+
base_branch=base_branch,
|
|
433
|
+
worktree_path=worktree_path,
|
|
434
|
+
control_root=worktree_path,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
def load_task_workspace(
|
|
438
|
+
self,
|
|
439
|
+
*,
|
|
440
|
+
root: Path,
|
|
441
|
+
run_dir: Path,
|
|
442
|
+
task_id: str,
|
|
443
|
+
branch: str,
|
|
444
|
+
base_branch: str,
|
|
445
|
+
) -> PreparedTaskWorkspace:
|
|
446
|
+
return PreparedTaskWorkspace(
|
|
447
|
+
backend_name=self.name,
|
|
448
|
+
task_id=task_id,
|
|
449
|
+
branch=branch,
|
|
450
|
+
base_branch=base_branch,
|
|
451
|
+
worktree_path=self.task_workspace_path(run_dir=run_dir, task_id=task_id),
|
|
452
|
+
control_root=root,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
def apply_task_success(
|
|
456
|
+
self,
|
|
457
|
+
*,
|
|
458
|
+
root: Path,
|
|
459
|
+
prepared_workspace: PreparedTaskWorkspace,
|
|
460
|
+
message: str,
|
|
461
|
+
changed_files: list[str] | None = None,
|
|
462
|
+
) -> SwarmApplyResult:
|
|
463
|
+
_ = changed_files
|
|
464
|
+
merge_commit_hash = self.merge_runner(
|
|
465
|
+
root,
|
|
466
|
+
base_branch=prepared_workspace.base_branch,
|
|
467
|
+
task_branch=prepared_workspace.branch,
|
|
468
|
+
message=message,
|
|
469
|
+
)
|
|
470
|
+
return SwarmApplyResult(
|
|
471
|
+
backend_name=self.name,
|
|
472
|
+
task_id=prepared_workspace.task_id,
|
|
473
|
+
branch=prepared_workspace.branch,
|
|
474
|
+
merge_commit_hash=merge_commit_hash,
|
|
475
|
+
action="merged",
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
def apply_task_to_candidate(
|
|
479
|
+
self,
|
|
480
|
+
*,
|
|
481
|
+
root: Path,
|
|
482
|
+
candidate_workspace: PreparedBatchCandidateWorkspace,
|
|
483
|
+
prepared_workspace: PreparedTaskWorkspace,
|
|
484
|
+
message: str,
|
|
485
|
+
changed_files: list[str] | None = None,
|
|
486
|
+
) -> None:
|
|
487
|
+
_ = root
|
|
488
|
+
_ = changed_files
|
|
489
|
+
self.merge_runner(
|
|
490
|
+
candidate_workspace.control_root,
|
|
491
|
+
base_branch=candidate_workspace.branch,
|
|
492
|
+
task_branch=prepared_workspace.branch,
|
|
493
|
+
message=message,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
def cleanup_task_workspace(
|
|
497
|
+
self,
|
|
498
|
+
*,
|
|
499
|
+
root: Path,
|
|
500
|
+
prepared_workspace: PreparedTaskWorkspace,
|
|
501
|
+
keep_worktrees: bool,
|
|
502
|
+
) -> list[str]:
|
|
503
|
+
if keep_worktrees:
|
|
504
|
+
return []
|
|
505
|
+
|
|
506
|
+
cleanup_errors: list[str] = []
|
|
507
|
+
if prepared_workspace.worktree_path.exists():
|
|
508
|
+
try:
|
|
509
|
+
self.remove_worktree_fn(
|
|
510
|
+
root=root,
|
|
511
|
+
worktree_repo_path=prepared_workspace.worktree_path,
|
|
512
|
+
force=True,
|
|
513
|
+
)
|
|
514
|
+
except Exception as e: # noqa: BLE001
|
|
515
|
+
cleanup_errors.append(f"worktree cleanup failed: {e}")
|
|
516
|
+
if prepared_workspace.branch:
|
|
517
|
+
try:
|
|
518
|
+
self.delete_branch_fn(root, prepared_workspace.branch)
|
|
519
|
+
except Exception as e: # noqa: BLE001
|
|
520
|
+
cleanup_errors.append(f"branch cleanup failed: {e}")
|
|
521
|
+
return cleanup_errors
|
|
522
|
+
|
|
523
|
+
def cleanup_candidate_workspace(
|
|
524
|
+
self,
|
|
525
|
+
*,
|
|
526
|
+
root: Path,
|
|
527
|
+
candidate_workspace: PreparedBatchCandidateWorkspace,
|
|
528
|
+
keep_worktrees: bool,
|
|
529
|
+
) -> list[str]:
|
|
530
|
+
if keep_worktrees:
|
|
531
|
+
return []
|
|
532
|
+
|
|
533
|
+
cleanup_errors: list[str] = []
|
|
534
|
+
if candidate_workspace.worktree_path.exists():
|
|
535
|
+
try:
|
|
536
|
+
self.remove_worktree_fn(
|
|
537
|
+
root=root,
|
|
538
|
+
worktree_repo_path=candidate_workspace.worktree_path,
|
|
539
|
+
force=True,
|
|
540
|
+
)
|
|
541
|
+
except Exception as e: # noqa: BLE001
|
|
542
|
+
cleanup_errors.append(f"candidate worktree cleanup failed: {e}")
|
|
543
|
+
if candidate_workspace.branch:
|
|
544
|
+
try:
|
|
545
|
+
self.delete_branch_fn(root, candidate_workspace.branch)
|
|
546
|
+
except Exception as e: # noqa: BLE001
|
|
547
|
+
cleanup_errors.append(f"candidate branch cleanup failed: {e}")
|
|
548
|
+
try:
|
|
549
|
+
unresolved_state = self._record_failed_cleanup_state(
|
|
550
|
+
root=root,
|
|
551
|
+
worktree_path=candidate_workspace.worktree_path,
|
|
552
|
+
branch=candidate_workspace.branch,
|
|
553
|
+
cleanup_errors=cleanup_errors,
|
|
554
|
+
)
|
|
555
|
+
if unresolved_state and not cleanup_errors:
|
|
556
|
+
cleanup_errors.append(f"cleanup incomplete: {'; '.join(unresolved_state)}")
|
|
557
|
+
except Exception as e: # noqa: BLE001
|
|
558
|
+
cleanup_errors.append(f"candidate cleanup marker failed: {e}")
|
|
559
|
+
return cleanup_errors
|
|
560
|
+
|
|
561
|
+
def cleanup_failed_task_workspace(
|
|
562
|
+
self,
|
|
563
|
+
*,
|
|
564
|
+
root: Path,
|
|
565
|
+
prepared_workspace: PreparedTaskWorkspace,
|
|
566
|
+
keep_worktrees: bool,
|
|
567
|
+
) -> list[str]:
|
|
568
|
+
if keep_worktrees:
|
|
569
|
+
return []
|
|
570
|
+
|
|
571
|
+
cleanup_errors: list[str] = []
|
|
572
|
+
if prepared_workspace.worktree_path.exists():
|
|
573
|
+
try:
|
|
574
|
+
self.remove_worktree_fn(
|
|
575
|
+
root=root,
|
|
576
|
+
worktree_repo_path=prepared_workspace.worktree_path,
|
|
577
|
+
force=True,
|
|
578
|
+
)
|
|
579
|
+
except Exception as e: # noqa: BLE001
|
|
580
|
+
cleanup_errors.append(f"worktree cleanup failed: {e}")
|
|
581
|
+
if prepared_workspace.branch:
|
|
582
|
+
try:
|
|
583
|
+
self._cleanup_failed_branch(root=root, branch=prepared_workspace.branch)
|
|
584
|
+
except Exception as e: # noqa: BLE001
|
|
585
|
+
cleanup_errors.append(f"branch cleanup failed: {e}")
|
|
586
|
+
try:
|
|
587
|
+
unresolved_state = self._record_failed_cleanup_state(
|
|
588
|
+
root=root,
|
|
589
|
+
worktree_path=prepared_workspace.worktree_path,
|
|
590
|
+
branch=prepared_workspace.branch,
|
|
591
|
+
cleanup_errors=cleanup_errors,
|
|
592
|
+
)
|
|
593
|
+
if unresolved_state and not cleanup_errors:
|
|
594
|
+
cleanup_errors.append(f"cleanup incomplete: {'; '.join(unresolved_state)}")
|
|
595
|
+
except Exception as e: # noqa: BLE001
|
|
596
|
+
cleanup_errors.append(f"cleanup marker failed: {e}")
|
|
597
|
+
return cleanup_errors
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
@dataclass(frozen=True)
|
|
601
|
+
class SnapshotSwarmBackend:
|
|
602
|
+
merge_runner: Any
|
|
603
|
+
branch_exists_fn: Any
|
|
604
|
+
current_branch_fn: Any
|
|
605
|
+
snapshot_base_branch: str = "snapshot-base"
|
|
606
|
+
name: str = "snapshot_workspace"
|
|
607
|
+
capabilities: frozenset[str] = frozenset({"git", "snapshot", "sync"})
|
|
608
|
+
requires_head_commit: bool = False
|
|
609
|
+
supports_remote_sync: bool = False
|
|
610
|
+
|
|
611
|
+
def prepare_startup(self, root: Path) -> SwarmBackendStartupResult:
|
|
612
|
+
_ = root
|
|
613
|
+
return SwarmBackendStartupResult(warnings=[])
|
|
614
|
+
|
|
615
|
+
def prune(self, root: Path) -> None:
|
|
616
|
+
_ = root
|
|
617
|
+
|
|
618
|
+
def has_head_commit(self, root: Path) -> bool:
|
|
619
|
+
_ = root
|
|
620
|
+
return False
|
|
621
|
+
|
|
622
|
+
def default_base_branch(self, root: Path) -> str:
|
|
623
|
+
try:
|
|
624
|
+
branch = str(self.current_branch_fn(root) or "").strip()
|
|
625
|
+
except Exception: # noqa: BLE001
|
|
626
|
+
branch = ""
|
|
627
|
+
return branch or self.snapshot_base_branch
|
|
628
|
+
|
|
629
|
+
def branch_exists(self, root: Path, branch: str) -> bool:
|
|
630
|
+
return bool(self.branch_exists_fn(root, branch))
|
|
631
|
+
|
|
632
|
+
def task_workspace_path(self, *, run_dir: Path, task_id: str) -> Path:
|
|
633
|
+
return run_dir / "worktrees" / task_id / "repo"
|
|
634
|
+
|
|
635
|
+
def candidate_workspace_path(self, *, run_dir: Path, batch_label: str) -> Path:
|
|
636
|
+
return run_dir / "worktrees" / "_batch_candidates" / batch_label / "repo"
|
|
637
|
+
|
|
638
|
+
def prepare_task_workspace(
|
|
639
|
+
self,
|
|
640
|
+
*,
|
|
641
|
+
root: Path,
|
|
642
|
+
run_dir: Path,
|
|
643
|
+
task_id: str,
|
|
644
|
+
branch: str,
|
|
645
|
+
base_branch: str,
|
|
646
|
+
) -> PreparedTaskWorkspace:
|
|
647
|
+
worktree_path = self.task_workspace_path(run_dir=run_dir, task_id=task_id)
|
|
648
|
+
if worktree_path.exists():
|
|
649
|
+
valid, clean, _reason = _inspect_existing_git_workspace(
|
|
650
|
+
worktree_path=worktree_path,
|
|
651
|
+
expected_branch=branch,
|
|
652
|
+
)
|
|
653
|
+
if valid:
|
|
654
|
+
if not clean:
|
|
655
|
+
_sanitize_existing_git_workspace(worktree_path=worktree_path)
|
|
656
|
+
if not _tracked_runtime_artifact_paths(worktree_path):
|
|
657
|
+
return PreparedTaskWorkspace(
|
|
658
|
+
backend_name=self.name,
|
|
659
|
+
task_id=task_id,
|
|
660
|
+
branch=branch,
|
|
661
|
+
base_branch=base_branch,
|
|
662
|
+
worktree_path=worktree_path,
|
|
663
|
+
control_root=worktree_path,
|
|
664
|
+
)
|
|
665
|
+
_cleanup_workspace_path(worktree_path)
|
|
666
|
+
copy_workspace_snapshot(src_root=root, dest_root=worktree_path)
|
|
667
|
+
self._init_snapshot_repo(
|
|
668
|
+
worktree_path=worktree_path,
|
|
669
|
+
base_branch=base_branch,
|
|
670
|
+
task_branch=branch,
|
|
671
|
+
task_id=task_id,
|
|
672
|
+
)
|
|
673
|
+
return PreparedTaskWorkspace(
|
|
674
|
+
backend_name=self.name,
|
|
675
|
+
task_id=task_id,
|
|
676
|
+
branch=branch,
|
|
677
|
+
base_branch=base_branch,
|
|
678
|
+
worktree_path=worktree_path,
|
|
679
|
+
control_root=worktree_path,
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
def prepare_candidate_workspace(
|
|
683
|
+
self,
|
|
684
|
+
*,
|
|
685
|
+
root: Path,
|
|
686
|
+
run_dir: Path,
|
|
687
|
+
batch_label: str,
|
|
688
|
+
branch: str,
|
|
689
|
+
base_branch: str,
|
|
690
|
+
) -> PreparedBatchCandidateWorkspace:
|
|
691
|
+
worktree_path = self.candidate_workspace_path(run_dir=run_dir, batch_label=batch_label)
|
|
692
|
+
if worktree_path.exists():
|
|
693
|
+
_cleanup_workspace_path(worktree_path)
|
|
694
|
+
copy_workspace_snapshot(src_root=root, dest_root=worktree_path)
|
|
695
|
+
self._init_snapshot_repo(
|
|
696
|
+
worktree_path=worktree_path,
|
|
697
|
+
base_branch=base_branch,
|
|
698
|
+
task_branch=branch,
|
|
699
|
+
task_id=batch_label,
|
|
700
|
+
)
|
|
701
|
+
return PreparedBatchCandidateWorkspace(
|
|
702
|
+
backend_name=self.name,
|
|
703
|
+
batch_label=batch_label,
|
|
704
|
+
branch=branch,
|
|
705
|
+
base_branch=base_branch,
|
|
706
|
+
worktree_path=worktree_path,
|
|
707
|
+
control_root=worktree_path,
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
def load_task_workspace(
|
|
711
|
+
self,
|
|
712
|
+
*,
|
|
713
|
+
root: Path,
|
|
714
|
+
run_dir: Path,
|
|
715
|
+
task_id: str,
|
|
716
|
+
branch: str,
|
|
717
|
+
base_branch: str,
|
|
718
|
+
) -> PreparedTaskWorkspace:
|
|
719
|
+
_ = root
|
|
720
|
+
worktree_path = self.task_workspace_path(run_dir=run_dir, task_id=task_id)
|
|
721
|
+
if not worktree_path.exists():
|
|
722
|
+
raise GitOpsError(
|
|
723
|
+
"snapshot workspace is missing for ready_for_merge task; rerun the task first"
|
|
724
|
+
)
|
|
725
|
+
return PreparedTaskWorkspace(
|
|
726
|
+
backend_name=self.name,
|
|
727
|
+
task_id=task_id,
|
|
728
|
+
branch=branch,
|
|
729
|
+
base_branch=base_branch,
|
|
730
|
+
worktree_path=worktree_path,
|
|
731
|
+
control_root=worktree_path,
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
def apply_task_success(
|
|
735
|
+
self,
|
|
736
|
+
*,
|
|
737
|
+
root: Path,
|
|
738
|
+
prepared_workspace: PreparedTaskWorkspace,
|
|
739
|
+
message: str,
|
|
740
|
+
changed_files: list[str] | None = None,
|
|
741
|
+
) -> SwarmApplyResult:
|
|
742
|
+
merge_commit_hash = self.merge_runner(
|
|
743
|
+
prepared_workspace.control_root,
|
|
744
|
+
base_branch=prepared_workspace.base_branch,
|
|
745
|
+
task_branch=prepared_workspace.branch,
|
|
746
|
+
message=message,
|
|
747
|
+
)
|
|
748
|
+
effective_changed_files = changed_files or self._changed_files_since_base(
|
|
749
|
+
worktree_path=prepared_workspace.control_root,
|
|
750
|
+
base_branch=prepared_workspace.base_branch,
|
|
751
|
+
)
|
|
752
|
+
sync_snapshot_changed_files(
|
|
753
|
+
snapshot_root=prepared_workspace.worktree_path,
|
|
754
|
+
workspace_root=root,
|
|
755
|
+
changed_files=effective_changed_files,
|
|
756
|
+
)
|
|
757
|
+
return SwarmApplyResult(
|
|
758
|
+
backend_name=self.name,
|
|
759
|
+
task_id=prepared_workspace.task_id,
|
|
760
|
+
branch=prepared_workspace.branch,
|
|
761
|
+
merge_commit_hash=merge_commit_hash,
|
|
762
|
+
action="applied",
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
def apply_task_to_candidate(
|
|
766
|
+
self,
|
|
767
|
+
*,
|
|
768
|
+
root: Path,
|
|
769
|
+
candidate_workspace: PreparedBatchCandidateWorkspace,
|
|
770
|
+
prepared_workspace: PreparedTaskWorkspace,
|
|
771
|
+
message: str,
|
|
772
|
+
changed_files: list[str] | None = None,
|
|
773
|
+
) -> None:
|
|
774
|
+
_ = root
|
|
775
|
+
_ = message
|
|
776
|
+
effective_changed_files = changed_files or self._changed_files_since_base(
|
|
777
|
+
worktree_path=prepared_workspace.control_root,
|
|
778
|
+
base_branch=prepared_workspace.base_branch,
|
|
779
|
+
)
|
|
780
|
+
sync_snapshot_changed_files(
|
|
781
|
+
snapshot_root=prepared_workspace.worktree_path,
|
|
782
|
+
workspace_root=candidate_workspace.worktree_path,
|
|
783
|
+
changed_files=effective_changed_files,
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
def cleanup_task_workspace(
|
|
787
|
+
self,
|
|
788
|
+
*,
|
|
789
|
+
root: Path,
|
|
790
|
+
prepared_workspace: PreparedTaskWorkspace,
|
|
791
|
+
keep_worktrees: bool,
|
|
792
|
+
) -> list[str]:
|
|
793
|
+
_ = root
|
|
794
|
+
if keep_worktrees:
|
|
795
|
+
return []
|
|
796
|
+
if not prepared_workspace.worktree_path.exists():
|
|
797
|
+
return []
|
|
798
|
+
try:
|
|
799
|
+
_cleanup_workspace_path(prepared_workspace.worktree_path)
|
|
800
|
+
except (GitOpsError, OSError) as e:
|
|
801
|
+
return [f"snapshot cleanup failed: {e}"]
|
|
802
|
+
return []
|
|
803
|
+
|
|
804
|
+
def cleanup_candidate_workspace(
|
|
805
|
+
self,
|
|
806
|
+
*,
|
|
807
|
+
root: Path,
|
|
808
|
+
candidate_workspace: PreparedBatchCandidateWorkspace,
|
|
809
|
+
keep_worktrees: bool,
|
|
810
|
+
) -> list[str]:
|
|
811
|
+
_ = root
|
|
812
|
+
if keep_worktrees:
|
|
813
|
+
return []
|
|
814
|
+
if not candidate_workspace.worktree_path.exists():
|
|
815
|
+
return []
|
|
816
|
+
try:
|
|
817
|
+
_cleanup_workspace_path(candidate_workspace.worktree_path)
|
|
818
|
+
except (GitOpsError, OSError) as e:
|
|
819
|
+
return [f"snapshot candidate cleanup failed: {e}"]
|
|
820
|
+
return []
|
|
821
|
+
|
|
822
|
+
def cleanup_failed_task_workspace(
|
|
823
|
+
self,
|
|
824
|
+
*,
|
|
825
|
+
root: Path,
|
|
826
|
+
prepared_workspace: PreparedTaskWorkspace,
|
|
827
|
+
keep_worktrees: bool,
|
|
828
|
+
) -> list[str]:
|
|
829
|
+
_ = root
|
|
830
|
+
if keep_worktrees:
|
|
831
|
+
return []
|
|
832
|
+
if not prepared_workspace.worktree_path.exists():
|
|
833
|
+
return []
|
|
834
|
+
try:
|
|
835
|
+
_sanitize_existing_git_workspace(worktree_path=prepared_workspace.worktree_path)
|
|
836
|
+
except Exception as e: # noqa: BLE001
|
|
837
|
+
try:
|
|
838
|
+
_cleanup_workspace_path(prepared_workspace.worktree_path)
|
|
839
|
+
except GitOpsError as cleanup_error:
|
|
840
|
+
return [
|
|
841
|
+
"snapshot failure cleanup failed: "
|
|
842
|
+
f"{e}; fallback removal also failed: {cleanup_error}"
|
|
843
|
+
]
|
|
844
|
+
return [f"snapshot failure cleanup reset failed; removed workspace instead: {e}"]
|
|
845
|
+
return []
|
|
846
|
+
|
|
847
|
+
def _init_snapshot_repo(
|
|
848
|
+
self,
|
|
849
|
+
*,
|
|
850
|
+
worktree_path: Path,
|
|
851
|
+
base_branch: str,
|
|
852
|
+
task_branch: str,
|
|
853
|
+
task_id: str,
|
|
854
|
+
) -> None:
|
|
855
|
+
worktree_path.mkdir(parents=True, exist_ok=True)
|
|
856
|
+
_run_git_checked(
|
|
857
|
+
worktree_path, ["init", "-q"], error_message="failed to init snapshot repo"
|
|
858
|
+
)
|
|
859
|
+
_run_git_checked(
|
|
860
|
+
worktree_path,
|
|
861
|
+
["config", "user.name", "alysis-agent"],
|
|
862
|
+
error_message="failed to configure snapshot git user.name",
|
|
863
|
+
)
|
|
864
|
+
_run_git_checked(
|
|
865
|
+
worktree_path,
|
|
866
|
+
["config", "user.email", "alysis-agent@local"],
|
|
867
|
+
error_message="failed to configure snapshot git user.email",
|
|
868
|
+
)
|
|
869
|
+
ensure_runtime_artifact_excludes(worktree_path)
|
|
870
|
+
_run_git_checked(
|
|
871
|
+
worktree_path,
|
|
872
|
+
["checkout", "-b", base_branch],
|
|
873
|
+
error_message=f"failed to create snapshot base branch {base_branch}",
|
|
874
|
+
)
|
|
875
|
+
_run_git_checked(
|
|
876
|
+
worktree_path, ["add", "-A"], error_message="failed to stage snapshot files"
|
|
877
|
+
)
|
|
878
|
+
_run_git_checked(
|
|
879
|
+
worktree_path,
|
|
880
|
+
["commit", "--allow-empty", "-m", f"Snapshot baseline for {task_id}"],
|
|
881
|
+
error_message="failed to create snapshot baseline commit",
|
|
882
|
+
extra_config={"user.name": "alysis-agent", "user.email": "alysis-agent@local"},
|
|
883
|
+
)
|
|
884
|
+
_run_git_checked(
|
|
885
|
+
worktree_path,
|
|
886
|
+
["checkout", "-b", task_branch],
|
|
887
|
+
error_message=f"failed to create snapshot task branch {task_branch}",
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
def _changed_files_since_base(self, *, worktree_path: Path, base_branch: str) -> list[str]:
|
|
891
|
+
cp = _run_git_checked(
|
|
892
|
+
worktree_path,
|
|
893
|
+
["diff", "--name-only", f"{base_branch}..HEAD"],
|
|
894
|
+
error_message=f"failed to list snapshot changes from {base_branch}",
|
|
895
|
+
)
|
|
896
|
+
return [line.strip() for line in cp.stdout.splitlines() if line.strip()]
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def select_swarm_backend(
|
|
900
|
+
*,
|
|
901
|
+
paths: RunPaths,
|
|
902
|
+
ensure_worktree_fn: Any,
|
|
903
|
+
merge_runner: Any,
|
|
904
|
+
remove_worktree_fn: Any,
|
|
905
|
+
delete_branch_fn: Any,
|
|
906
|
+
prune_fn: Any,
|
|
907
|
+
branch_exists_fn: Any,
|
|
908
|
+
current_branch_fn: Any,
|
|
909
|
+
has_head_commit_fn: Any,
|
|
910
|
+
) -> SwarmBackend:
|
|
911
|
+
if paths.workspace_kind in {"plain_dir", "git_repo_no_head"} or not paths.has_head_commit:
|
|
912
|
+
return SnapshotSwarmBackend(
|
|
913
|
+
merge_runner=merge_runner,
|
|
914
|
+
branch_exists_fn=branch_exists_fn,
|
|
915
|
+
current_branch_fn=current_branch_fn,
|
|
916
|
+
)
|
|
917
|
+
return GitWorktreeSwarmBackend(
|
|
918
|
+
ensure_worktree_fn=ensure_worktree_fn,
|
|
919
|
+
merge_runner=merge_runner,
|
|
920
|
+
remove_worktree_fn=remove_worktree_fn,
|
|
921
|
+
delete_branch_fn=delete_branch_fn,
|
|
922
|
+
prune_fn=prune_fn,
|
|
923
|
+
branch_exists_fn=branch_exists_fn,
|
|
924
|
+
current_branch_fn=current_branch_fn,
|
|
925
|
+
has_head_commit_fn=has_head_commit_fn,
|
|
926
|
+
)
|