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
alysis_code/git_ops.py
ADDED
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .git_safe import build_git_cmd, build_git_process_env
|
|
9
|
+
from .runtime_artifacts import is_runtime_artifact_path, runtime_artifact_git_exclude_entries
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GitOpsError(RuntimeError):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
DEFAULT_COMMIT_AUTHOR_NAME = "alysis-agent"
|
|
17
|
+
DEFAULT_COMMIT_AUTHOR_EMAIL = "alysis-agent@local"
|
|
18
|
+
_EMPTY_TREE_HASH = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
|
19
|
+
_RUNTIME_ARTIFACT_EXCLUDE_BEGIN = "# BEGIN alysis runtime artifacts"
|
|
20
|
+
_RUNTIME_ARTIFACT_EXCLUDE_END = "# END alysis runtime artifacts"
|
|
21
|
+
_LEGACY_BROAD_TARGET_EXCLUDE_ENTRY = "target/"
|
|
22
|
+
_READ_ONLY_GIT_TIMEOUT_S = 5.0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _git_identity_config(
|
|
26
|
+
*,
|
|
27
|
+
author_name: str = DEFAULT_COMMIT_AUTHOR_NAME,
|
|
28
|
+
author_email: str = DEFAULT_COMMIT_AUTHOR_EMAIL,
|
|
29
|
+
) -> dict[str, str]:
|
|
30
|
+
return {"user.name": author_name, "user.email": author_email}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def ensure_git_available() -> None:
|
|
34
|
+
if shutil.which("git") is None:
|
|
35
|
+
raise GitOpsError("git is not available on PATH")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _run_git(
|
|
39
|
+
root: Path,
|
|
40
|
+
args: list[str],
|
|
41
|
+
*,
|
|
42
|
+
input_text: str | None = None,
|
|
43
|
+
extra_config: dict[str, str] | None = None,
|
|
44
|
+
timeout_s: float | None = None,
|
|
45
|
+
) -> subprocess.CompletedProcess[str]:
|
|
46
|
+
cmd = build_git_cmd(root, args, extra_config=extra_config)
|
|
47
|
+
try:
|
|
48
|
+
return subprocess.run(
|
|
49
|
+
cmd,
|
|
50
|
+
check=False,
|
|
51
|
+
capture_output=True,
|
|
52
|
+
text=True,
|
|
53
|
+
input=input_text,
|
|
54
|
+
env=build_git_process_env(),
|
|
55
|
+
timeout=timeout_s,
|
|
56
|
+
)
|
|
57
|
+
except subprocess.TimeoutExpired:
|
|
58
|
+
return subprocess.CompletedProcess(
|
|
59
|
+
args=cmd,
|
|
60
|
+
returncode=124,
|
|
61
|
+
stdout="",
|
|
62
|
+
stderr="git command timed out",
|
|
63
|
+
)
|
|
64
|
+
except OSError as e:
|
|
65
|
+
raise GitOpsError("failed to run git") from e
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _run_git_checked(
|
|
69
|
+
root: Path,
|
|
70
|
+
args: list[str],
|
|
71
|
+
*,
|
|
72
|
+
input_text: str | None = None,
|
|
73
|
+
extra_config: dict[str, str] | None = None,
|
|
74
|
+
error_message: str,
|
|
75
|
+
timeout_s: float | None = None,
|
|
76
|
+
) -> subprocess.CompletedProcess[str]:
|
|
77
|
+
cp = _run_git(
|
|
78
|
+
root,
|
|
79
|
+
args,
|
|
80
|
+
input_text=input_text,
|
|
81
|
+
extra_config=extra_config,
|
|
82
|
+
timeout_s=timeout_s,
|
|
83
|
+
)
|
|
84
|
+
if cp.returncode != 0:
|
|
85
|
+
detail = (cp.stderr or cp.stdout).strip()
|
|
86
|
+
raise GitOpsError(f"{error_message}: {detail or 'unknown error'}")
|
|
87
|
+
return cp
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def ensure_git_repo(root: Path) -> None:
|
|
91
|
+
_run_git_checked(
|
|
92
|
+
root,
|
|
93
|
+
["rev-parse", "--git-dir"],
|
|
94
|
+
error_message="not a git repository",
|
|
95
|
+
timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def current_branch(root: Path) -> str:
|
|
100
|
+
symbolic = _run_git(
|
|
101
|
+
root, ["symbolic-ref", "--short", "HEAD"], timeout_s=_READ_ONLY_GIT_TIMEOUT_S
|
|
102
|
+
)
|
|
103
|
+
if symbolic.returncode == 0:
|
|
104
|
+
branch = symbolic.stdout.strip()
|
|
105
|
+
if branch:
|
|
106
|
+
return branch
|
|
107
|
+
|
|
108
|
+
cp = _run_git_checked(
|
|
109
|
+
root,
|
|
110
|
+
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
111
|
+
error_message="failed to determine current branch",
|
|
112
|
+
timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
|
|
113
|
+
)
|
|
114
|
+
return cp.stdout.strip()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def has_head_commit(root: Path) -> bool:
|
|
118
|
+
cp = _run_git(root, ["rev-parse", "--verify", "HEAD"], timeout_s=_READ_ONLY_GIT_TIMEOUT_S)
|
|
119
|
+
return cp.returncode == 0
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def head_commit(root: Path) -> str | None:
|
|
123
|
+
cp = _run_git(root, ["rev-parse", "HEAD"], timeout_s=_READ_ONLY_GIT_TIMEOUT_S)
|
|
124
|
+
if cp.returncode != 0:
|
|
125
|
+
return None
|
|
126
|
+
value = (cp.stdout or "").strip()
|
|
127
|
+
return value or None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def git_path(root: Path, pathspec: str) -> Path:
|
|
131
|
+
cp = _run_git_checked(
|
|
132
|
+
root,
|
|
133
|
+
["rev-parse", "--git-path", pathspec],
|
|
134
|
+
error_message=f"failed to resolve git path for {pathspec}",
|
|
135
|
+
timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
|
|
136
|
+
)
|
|
137
|
+
resolved = (cp.stdout or "").strip()
|
|
138
|
+
if not resolved:
|
|
139
|
+
raise GitOpsError(f"failed to resolve git path for {pathspec}: empty result")
|
|
140
|
+
path = Path(resolved)
|
|
141
|
+
if not path.is_absolute():
|
|
142
|
+
path = root / path
|
|
143
|
+
return path
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def ensure_info_exclude_entries(root: Path, entries: list[str]) -> Path:
|
|
147
|
+
exclude_path = git_path(root, "info/exclude")
|
|
148
|
+
exclude_path.parent.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
|
|
150
|
+
normalized_entries = [entry.strip() for entry in entries if entry.strip()]
|
|
151
|
+
if not normalized_entries:
|
|
152
|
+
return exclude_path
|
|
153
|
+
|
|
154
|
+
if exclude_path.exists():
|
|
155
|
+
raw_lines = exclude_path.read_text(encoding="utf-8").splitlines()
|
|
156
|
+
else:
|
|
157
|
+
raw_lines = []
|
|
158
|
+
|
|
159
|
+
existing = {line.strip() for line in raw_lines if line.strip()}
|
|
160
|
+
to_append = [entry for entry in normalized_entries if entry not in existing]
|
|
161
|
+
if not to_append:
|
|
162
|
+
return exclude_path
|
|
163
|
+
|
|
164
|
+
with exclude_path.open("a", encoding="utf-8") as fh:
|
|
165
|
+
for entry in to_append:
|
|
166
|
+
fh.write(entry + "\n")
|
|
167
|
+
return exclude_path
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def ensure_runtime_artifact_excludes(root: Path) -> Path:
|
|
171
|
+
exclude_path = git_path(root, "info/exclude")
|
|
172
|
+
exclude_path.parent.mkdir(parents=True, exist_ok=True)
|
|
173
|
+
|
|
174
|
+
desired_entries = list(runtime_artifact_git_exclude_entries(root))
|
|
175
|
+
if exclude_path.exists():
|
|
176
|
+
raw_lines = exclude_path.read_text(encoding="utf-8").splitlines()
|
|
177
|
+
else:
|
|
178
|
+
raw_lines = []
|
|
179
|
+
|
|
180
|
+
preserved_lines: list[str] = []
|
|
181
|
+
inside_managed_block = False
|
|
182
|
+
for line in raw_lines:
|
|
183
|
+
stripped = line.strip()
|
|
184
|
+
if stripped == _RUNTIME_ARTIFACT_EXCLUDE_BEGIN:
|
|
185
|
+
inside_managed_block = True
|
|
186
|
+
continue
|
|
187
|
+
if stripped == _RUNTIME_ARTIFACT_EXCLUDE_END:
|
|
188
|
+
inside_managed_block = False
|
|
189
|
+
continue
|
|
190
|
+
if inside_managed_block:
|
|
191
|
+
continue
|
|
192
|
+
if stripped == _LEGACY_BROAD_TARGET_EXCLUDE_ENTRY:
|
|
193
|
+
continue
|
|
194
|
+
preserved_lines.append(line)
|
|
195
|
+
|
|
196
|
+
while preserved_lines and not preserved_lines[-1].strip():
|
|
197
|
+
preserved_lines.pop()
|
|
198
|
+
if preserved_lines:
|
|
199
|
+
preserved_lines.append("")
|
|
200
|
+
preserved_lines.extend(
|
|
201
|
+
[
|
|
202
|
+
_RUNTIME_ARTIFACT_EXCLUDE_BEGIN,
|
|
203
|
+
*desired_entries,
|
|
204
|
+
_RUNTIME_ARTIFACT_EXCLUDE_END,
|
|
205
|
+
]
|
|
206
|
+
)
|
|
207
|
+
exclude_path.write_text("\n".join(preserved_lines) + "\n", encoding="utf-8")
|
|
208
|
+
return exclude_path
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def ensure_clean_for_pr(root: Path) -> None:
|
|
212
|
+
staged = _run_git_checked(
|
|
213
|
+
root,
|
|
214
|
+
["diff", "--cached", "--name-only"],
|
|
215
|
+
error_message="failed to inspect staged changes",
|
|
216
|
+
timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
|
|
217
|
+
).stdout.strip()
|
|
218
|
+
if staged:
|
|
219
|
+
raise GitOpsError("working tree is not clean: staged changes exist")
|
|
220
|
+
|
|
221
|
+
unstaged = _run_git_checked(
|
|
222
|
+
root,
|
|
223
|
+
["diff", "--name-only"],
|
|
224
|
+
error_message="failed to inspect unstaged changes",
|
|
225
|
+
timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
|
|
226
|
+
).stdout.strip()
|
|
227
|
+
if unstaged:
|
|
228
|
+
raise GitOpsError("working tree is not clean: unstaged tracked changes exist")
|
|
229
|
+
|
|
230
|
+
# Only untracked-and-not-ignored files appear with --exclude-standard.
|
|
231
|
+
ensure_runtime_artifact_excludes(root)
|
|
232
|
+
untracked = _run_git_checked(
|
|
233
|
+
root,
|
|
234
|
+
["ls-files", "--others", "--exclude-standard"],
|
|
235
|
+
error_message="failed to inspect untracked files",
|
|
236
|
+
timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
|
|
237
|
+
).stdout.strip()
|
|
238
|
+
if untracked:
|
|
239
|
+
raise GitOpsError(
|
|
240
|
+
"working tree has untracked files not ignored by git; please clean them first"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def branch_exists(root: Path, branch: str) -> bool:
|
|
245
|
+
cp = _run_git(
|
|
246
|
+
root,
|
|
247
|
+
["show-ref", "--verify", "--quiet", f"refs/heads/{branch}"],
|
|
248
|
+
timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
|
|
249
|
+
)
|
|
250
|
+
return cp.returncode == 0
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def status_porcelain(root: Path) -> list[str]:
|
|
254
|
+
cp = _run_git_checked(
|
|
255
|
+
root,
|
|
256
|
+
["status", "--porcelain=v1", "--untracked-files=normal"],
|
|
257
|
+
error_message="failed to inspect worktree status",
|
|
258
|
+
timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
|
|
259
|
+
)
|
|
260
|
+
return [line.rstrip() for line in cp.stdout.splitlines() if line.strip()]
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def untracked_files(root: Path) -> list[str]:
|
|
264
|
+
ensure_runtime_artifact_excludes(root)
|
|
265
|
+
cp = _run_git_checked(
|
|
266
|
+
root,
|
|
267
|
+
["ls-files", "--others", "--exclude-standard"],
|
|
268
|
+
error_message="failed to inspect untracked files",
|
|
269
|
+
timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
|
|
270
|
+
)
|
|
271
|
+
return [line.strip() for line in cp.stdout.splitlines() if line.strip()]
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def reset_hard(root: Path, *, target: str = "HEAD") -> None:
|
|
275
|
+
_run_git_checked(
|
|
276
|
+
root,
|
|
277
|
+
["reset", "--hard", target],
|
|
278
|
+
error_message=f"failed to reset worktree to {target}",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def clean_untracked(root: Path) -> None:
|
|
283
|
+
_run_git_checked(
|
|
284
|
+
root,
|
|
285
|
+
["clean", "-fd"],
|
|
286
|
+
error_message="failed to clean untracked files",
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def checkout_branch(root: Path, branch: str, *, base_branch: str) -> None:
|
|
291
|
+
if branch_exists(root, branch):
|
|
292
|
+
_run_git_checked(root, ["checkout", branch], error_message=f"failed to checkout {branch}")
|
|
293
|
+
return
|
|
294
|
+
_run_git_checked(
|
|
295
|
+
root,
|
|
296
|
+
["checkout", "-b", branch, base_branch],
|
|
297
|
+
error_message=f"failed to create branch {branch}",
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def stage_all(root: Path) -> None:
|
|
302
|
+
_run_git_checked(root, ["add", "-A"], error_message="failed to stage changes")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def staged_files(root: Path) -> list[str]:
|
|
306
|
+
cp = _run_git_checked(
|
|
307
|
+
root,
|
|
308
|
+
["diff", "--cached", "--name-only"],
|
|
309
|
+
error_message="failed to list staged files",
|
|
310
|
+
timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
|
|
311
|
+
)
|
|
312
|
+
return [line.strip() for line in cp.stdout.splitlines() if line.strip()]
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _staged_runtime_artifact_paths(
|
|
316
|
+
root: Path,
|
|
317
|
+
*,
|
|
318
|
+
current_paths: list[str] | None = None,
|
|
319
|
+
) -> list[str]:
|
|
320
|
+
staged = current_paths if current_paths is not None else staged_files(root)
|
|
321
|
+
return [path for path in staged if is_runtime_artifact_path(path, root=root)]
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _normalize_rel_prefix(value: str) -> str:
|
|
325
|
+
cleaned = value.strip().replace("\\", "/")
|
|
326
|
+
while cleaned.startswith("./"):
|
|
327
|
+
cleaned = cleaned[2:]
|
|
328
|
+
return cleaned.rstrip("/")
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _matches_prefix(rel_path: str, prefix: str) -> bool:
|
|
332
|
+
return rel_path == prefix or rel_path.startswith(prefix + "/")
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def unstage_staged_prefixes(root: Path, prefixes: list[str]) -> list[str]:
|
|
336
|
+
current = staged_files(root)
|
|
337
|
+
normalized = [_normalize_rel_prefix(p) for p in prefixes if p.strip()]
|
|
338
|
+
if not normalized:
|
|
339
|
+
return []
|
|
340
|
+
|
|
341
|
+
to_unstage: list[str] = []
|
|
342
|
+
for path in current:
|
|
343
|
+
rel = _normalize_rel_prefix(path)
|
|
344
|
+
if any(_matches_prefix(rel, pref) for pref in normalized):
|
|
345
|
+
to_unstage.append(path)
|
|
346
|
+
|
|
347
|
+
if not to_unstage:
|
|
348
|
+
return []
|
|
349
|
+
|
|
350
|
+
_run_git_checked(
|
|
351
|
+
root,
|
|
352
|
+
["reset", "HEAD", "--", *to_unstage],
|
|
353
|
+
error_message="failed to unstage protected paths",
|
|
354
|
+
)
|
|
355
|
+
return to_unstage
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def unstage_staged_paths(root: Path, paths: list[str]) -> list[str]:
|
|
359
|
+
normalized = {_normalize_rel_prefix(path) for path in paths if path.strip()}
|
|
360
|
+
if not normalized:
|
|
361
|
+
return []
|
|
362
|
+
|
|
363
|
+
current = staged_files(root)
|
|
364
|
+
to_unstage = [path for path in current if _normalize_rel_prefix(path) in normalized]
|
|
365
|
+
if not to_unstage:
|
|
366
|
+
return []
|
|
367
|
+
|
|
368
|
+
_run_git_checked(
|
|
369
|
+
root,
|
|
370
|
+
["reset", "HEAD", "--", *to_unstage],
|
|
371
|
+
error_message="failed to unstage filtered files",
|
|
372
|
+
)
|
|
373
|
+
return to_unstage
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def unstage_staged_runtime_artifacts(
|
|
377
|
+
root: Path,
|
|
378
|
+
*,
|
|
379
|
+
current_paths: list[str] | None = None,
|
|
380
|
+
) -> list[str]:
|
|
381
|
+
return unstage_staged_paths(
|
|
382
|
+
root,
|
|
383
|
+
_staged_runtime_artifact_paths(root, current_paths=current_paths),
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def ensure_not_staged_prefixes(root: Path, prefixes: list[str]) -> None:
|
|
388
|
+
current = staged_files(root)
|
|
389
|
+
normalized = [_normalize_rel_prefix(p) for p in prefixes if p.strip()]
|
|
390
|
+
for path in current:
|
|
391
|
+
rel = _normalize_rel_prefix(path)
|
|
392
|
+
for pref in normalized:
|
|
393
|
+
if _matches_prefix(rel, pref):
|
|
394
|
+
raise GitOpsError(f"staged file under protected path: {path}")
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def ensure_not_staged_paths(root: Path, paths: list[str]) -> None:
|
|
398
|
+
current = staged_files(root)
|
|
399
|
+
normalized = {_normalize_rel_prefix(path) for path in paths if path.strip()}
|
|
400
|
+
for path in current:
|
|
401
|
+
if _normalize_rel_prefix(path) in normalized:
|
|
402
|
+
raise GitOpsError(f"staged filtered file: {path}")
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def ensure_not_staged_runtime_artifacts(
|
|
406
|
+
root: Path,
|
|
407
|
+
*,
|
|
408
|
+
current_paths: list[str] | None = None,
|
|
409
|
+
) -> None:
|
|
410
|
+
current = _staged_runtime_artifact_paths(root, current_paths=current_paths)
|
|
411
|
+
if current:
|
|
412
|
+
preview = ", ".join(current[:20])
|
|
413
|
+
if len(current) > 20:
|
|
414
|
+
preview += ", ..."
|
|
415
|
+
raise GitOpsError(f"staged runtime artifact path: {preview}")
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def commit_all(
|
|
419
|
+
root: Path,
|
|
420
|
+
*,
|
|
421
|
+
message: str,
|
|
422
|
+
author_name: str = DEFAULT_COMMIT_AUTHOR_NAME,
|
|
423
|
+
author_email: str = DEFAULT_COMMIT_AUTHOR_EMAIL,
|
|
424
|
+
) -> str:
|
|
425
|
+
_run_git_checked(
|
|
426
|
+
root,
|
|
427
|
+
["commit", "-m", message],
|
|
428
|
+
extra_config=_git_identity_config(
|
|
429
|
+
author_name=author_name,
|
|
430
|
+
author_email=author_email,
|
|
431
|
+
),
|
|
432
|
+
error_message="failed to create commit",
|
|
433
|
+
)
|
|
434
|
+
cp = _run_git_checked(
|
|
435
|
+
root,
|
|
436
|
+
["rev-parse", "HEAD"],
|
|
437
|
+
error_message="failed to resolve commit hash",
|
|
438
|
+
)
|
|
439
|
+
return cp.stdout.strip()
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def format_patch_stdout(root: Path, *, base_branch: str) -> str:
|
|
443
|
+
cp = _run_git_checked(
|
|
444
|
+
root,
|
|
445
|
+
["format-patch", f"{base_branch}..HEAD", "--stdout"],
|
|
446
|
+
error_message="failed to generate patch",
|
|
447
|
+
timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
|
|
448
|
+
)
|
|
449
|
+
return cp.stdout
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def diff_text_since(
|
|
453
|
+
root: Path,
|
|
454
|
+
*,
|
|
455
|
+
before_commit: str | None,
|
|
456
|
+
after_commit: str | None,
|
|
457
|
+
) -> str:
|
|
458
|
+
if not after_commit or after_commit == before_commit:
|
|
459
|
+
return ""
|
|
460
|
+
base = before_commit or _EMPTY_TREE_HASH
|
|
461
|
+
cp = _run_git_checked(
|
|
462
|
+
root,
|
|
463
|
+
["diff", "--binary", "-M", base, after_commit],
|
|
464
|
+
error_message="failed to generate committed diff",
|
|
465
|
+
timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
|
|
466
|
+
)
|
|
467
|
+
return cp.stdout
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def changed_files_between(root: Path, *, revspec: str) -> list[str]:
|
|
471
|
+
cp = _run_git_checked(
|
|
472
|
+
root,
|
|
473
|
+
["diff", "--name-only", revspec],
|
|
474
|
+
error_message=f"failed to list changed files for {revspec}",
|
|
475
|
+
timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
|
|
476
|
+
)
|
|
477
|
+
return [line.strip() for line in cp.stdout.splitlines() if line.strip()]
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def changed_files_since(
|
|
481
|
+
root: Path,
|
|
482
|
+
*,
|
|
483
|
+
before_commit: str | None,
|
|
484
|
+
after_commit: str | None,
|
|
485
|
+
) -> list[str]:
|
|
486
|
+
if not after_commit or after_commit == before_commit:
|
|
487
|
+
return []
|
|
488
|
+
base = before_commit or _EMPTY_TREE_HASH
|
|
489
|
+
cp = _run_git_checked(
|
|
490
|
+
root,
|
|
491
|
+
["diff", "--name-only", base, after_commit],
|
|
492
|
+
error_message="failed to list committed changed files",
|
|
493
|
+
timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
|
|
494
|
+
)
|
|
495
|
+
return [line.strip() for line in cp.stdout.splitlines() if line.strip()]
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def added_files_since(
|
|
499
|
+
root: Path,
|
|
500
|
+
*,
|
|
501
|
+
before_commit: str | None,
|
|
502
|
+
after_commit: str | None,
|
|
503
|
+
) -> list[str]:
|
|
504
|
+
if not after_commit or after_commit == before_commit:
|
|
505
|
+
return []
|
|
506
|
+
base = before_commit or _EMPTY_TREE_HASH
|
|
507
|
+
cp = _run_git_checked(
|
|
508
|
+
root,
|
|
509
|
+
["diff", "--diff-filter=A", "--name-only", base, after_commit],
|
|
510
|
+
error_message="failed to list added committed files",
|
|
511
|
+
timeout_s=_READ_ONLY_GIT_TIMEOUT_S,
|
|
512
|
+
)
|
|
513
|
+
return [line.strip() for line in cp.stdout.splitlines() if line.strip()]
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def reset_mixed(root: Path, *, target: str = "HEAD") -> None:
|
|
517
|
+
_run_git_checked(
|
|
518
|
+
root,
|
|
519
|
+
["reset", "--mixed", target],
|
|
520
|
+
error_message=f"failed to reset worktree to {target} while preserving changes",
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def merge_no_ff(root: Path, *, base_branch: str, task_branch: str, message: str) -> str:
|
|
525
|
+
_run_git_checked(
|
|
526
|
+
root,
|
|
527
|
+
["checkout", base_branch],
|
|
528
|
+
error_message=f"failed to checkout base branch {base_branch}",
|
|
529
|
+
)
|
|
530
|
+
_run_git_checked(
|
|
531
|
+
root,
|
|
532
|
+
["merge", "--no-ff", task_branch, "-m", message],
|
|
533
|
+
extra_config=_git_identity_config(),
|
|
534
|
+
error_message=f"failed to merge branch {task_branch}",
|
|
535
|
+
)
|
|
536
|
+
cp = _run_git_checked(
|
|
537
|
+
root,
|
|
538
|
+
["rev-parse", "HEAD"],
|
|
539
|
+
error_message="failed to resolve merge commit hash",
|
|
540
|
+
)
|
|
541
|
+
return cp.stdout.strip()
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def delete_branch(root: Path, branch: str) -> None:
|
|
545
|
+
_run_git_checked(
|
|
546
|
+
root,
|
|
547
|
+
["branch", "-d", branch],
|
|
548
|
+
error_message=f"failed to delete branch {branch}",
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def generate_task_branch_name(task_id: str, title: str) -> str:
|
|
553
|
+
tid = task_id.strip().lower() or "task"
|
|
554
|
+
slug = re.sub(r"[^a-z0-9]+", "-", title.strip().lower())
|
|
555
|
+
slug = slug.strip("-")
|
|
556
|
+
if not slug:
|
|
557
|
+
slug = "task"
|
|
558
|
+
if len(slug) > 40:
|
|
559
|
+
slug = slug[:40].rstrip("-")
|
|
560
|
+
return f"feat/{tid}-{slug}"
|
alysis_code/git_safe.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .branding import env_get_from
|
|
10
|
+
|
|
11
|
+
_TRUE_VALUES = {"1", "true", "yes", "on", "enable", "enabled"}
|
|
12
|
+
_NON_INTERACTIVE_GIT_ENV = {
|
|
13
|
+
"GIT_TERMINAL_PROMPT": "0",
|
|
14
|
+
"GIT_ASKPASS": "",
|
|
15
|
+
"SSH_ASKPASS": "",
|
|
16
|
+
"GCM_INTERACTIVE": "never",
|
|
17
|
+
"GIT_EDITOR": "true",
|
|
18
|
+
"GIT_MERGE_AUTOEDIT": "no",
|
|
19
|
+
"PAGER": "cat",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def git_hooks_enabled(env: Mapping[str, str] | None = None) -> bool:
|
|
24
|
+
source = env if env is not None else os.environ
|
|
25
|
+
raw = str(env_get_from(source, "ALYSIS_GIT_HOOKS") or "").strip().lower()
|
|
26
|
+
return raw in _TRUE_VALUES
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def resolve_disabled_hooks_dir(root: Path, env: Mapping[str, str] | None = None) -> Path:
|
|
30
|
+
source = env if env is not None else os.environ
|
|
31
|
+
override = str(env_get_from(source, "ALYSIS_GIT_HOOKS_PATH") or "").strip()
|
|
32
|
+
if override:
|
|
33
|
+
target = Path(override).expanduser()
|
|
34
|
+
else:
|
|
35
|
+
root_hash = hashlib.sha256(os.fspath(root.resolve()).encode("utf-8")).hexdigest()[:16]
|
|
36
|
+
target = Path(tempfile.gettempdir()) / "alysis-agent" / "hooks-disabled" / root_hash
|
|
37
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
return target
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_git_process_env(env: Mapping[str, str] | None = None) -> dict[str, str]:
|
|
42
|
+
source = dict(os.environ if env is None else env)
|
|
43
|
+
source.update(_NON_INTERACTIVE_GIT_ENV)
|
|
44
|
+
return source
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_git_cmd(
|
|
48
|
+
root: Path,
|
|
49
|
+
args: list[str],
|
|
50
|
+
*,
|
|
51
|
+
extra_config: dict[str, str] | None = None,
|
|
52
|
+
env: Mapping[str, str] | None = None,
|
|
53
|
+
) -> list[str]:
|
|
54
|
+
config = dict(extra_config or {})
|
|
55
|
+
if not git_hooks_enabled(env):
|
|
56
|
+
config.setdefault("core.hooksPath", os.fspath(resolve_disabled_hooks_dir(root, env)))
|
|
57
|
+
|
|
58
|
+
cmd: list[str] = ["git", "-C", os.fspath(root)]
|
|
59
|
+
for key, value in config.items():
|
|
60
|
+
cmd.extend(["-c", f"{key}={value}"])
|
|
61
|
+
cmd.extend(args)
|
|
62
|
+
return cmd
|