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,408 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from .workspace_binding import (
|
|
11
|
+
WorkspaceAction,
|
|
12
|
+
WorkspaceBinding,
|
|
13
|
+
WorkspaceBindingError,
|
|
14
|
+
WorkspaceCandidate,
|
|
15
|
+
WorkspaceRiskLevel,
|
|
16
|
+
discover_workspace_candidates,
|
|
17
|
+
ensure_workspace_policy,
|
|
18
|
+
resolve_workspace_binding,
|
|
19
|
+
workspace_policy,
|
|
20
|
+
workspace_policy_violation_message,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_ACTION_USE_CURRENT = "use_current"
|
|
24
|
+
_ACTION_CHOOSE_PROJECT = "choose_project"
|
|
25
|
+
_ACTION_CREATE_FOLDER = "create_folder"
|
|
26
|
+
_ACTION_ENTER_PATH = "enter_path"
|
|
27
|
+
_ACTION_CANCEL = "cancel"
|
|
28
|
+
_DEFAULT_ACTION = _ACTION_CHOOSE_PROJECT
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def guarded_workspace_action_rows(
|
|
32
|
+
*,
|
|
33
|
+
binding: WorkspaceBinding,
|
|
34
|
+
candidates: tuple[WorkspaceCandidate, ...],
|
|
35
|
+
allow_use_current_action: bool = True,
|
|
36
|
+
) -> list[tuple[str, str, str]]:
|
|
37
|
+
current_label = _display_path(binding.requested_path)
|
|
38
|
+
rows: list[tuple[str, str, str]] = []
|
|
39
|
+
if allow_use_current_action:
|
|
40
|
+
rows.append(
|
|
41
|
+
(
|
|
42
|
+
_ACTION_USE_CURRENT,
|
|
43
|
+
"1) use current directory anyway",
|
|
44
|
+
f"Bind to {current_label} despite the guarded risk.",
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
choose_label = "2) choose an existing project folder"
|
|
48
|
+
create_label = "3) create a new folder here"
|
|
49
|
+
enter_label = "4) enter another path"
|
|
50
|
+
cancel_label = "5) cancel"
|
|
51
|
+
else:
|
|
52
|
+
choose_label = "1) choose an existing project folder"
|
|
53
|
+
create_label = "2) create a new folder here"
|
|
54
|
+
enter_label = "3) enter another path"
|
|
55
|
+
cancel_label = "4) cancel"
|
|
56
|
+
rows.extend(
|
|
57
|
+
[
|
|
58
|
+
(
|
|
59
|
+
_ACTION_CHOOSE_PROJECT,
|
|
60
|
+
choose_label,
|
|
61
|
+
(
|
|
62
|
+
f"Pick from {len(candidates)} shallow project candidate(s)."
|
|
63
|
+
if candidates
|
|
64
|
+
else "Pick another existing folder manually."
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
(
|
|
68
|
+
_ACTION_CREATE_FOLDER,
|
|
69
|
+
create_label,
|
|
70
|
+
f"Create a new child directory under {current_label} and bind to it.",
|
|
71
|
+
),
|
|
72
|
+
(
|
|
73
|
+
_ACTION_ENTER_PATH,
|
|
74
|
+
enter_label,
|
|
75
|
+
"Type an explicit existing path or a relative child path.",
|
|
76
|
+
),
|
|
77
|
+
(
|
|
78
|
+
_ACTION_CANCEL,
|
|
79
|
+
cancel_label,
|
|
80
|
+
"Abort startup without creating a session.",
|
|
81
|
+
),
|
|
82
|
+
]
|
|
83
|
+
)
|
|
84
|
+
return rows
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def workspace_candidate_rows(
|
|
88
|
+
*,
|
|
89
|
+
base_path: Path,
|
|
90
|
+
candidates: tuple[WorkspaceCandidate, ...],
|
|
91
|
+
) -> list[tuple[str, str, str]]:
|
|
92
|
+
rows: list[tuple[str, str, str]] = []
|
|
93
|
+
for idx, candidate in enumerate(candidates, start=1):
|
|
94
|
+
try:
|
|
95
|
+
label_path = os.fspath(candidate.path.relative_to(base_path))
|
|
96
|
+
except ValueError:
|
|
97
|
+
label_path = _display_path(candidate.path)
|
|
98
|
+
rows.append(
|
|
99
|
+
(
|
|
100
|
+
os.fspath(candidate.path),
|
|
101
|
+
f"{idx}) {label_path}",
|
|
102
|
+
candidate.summary,
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
return rows
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def resolve_startup_workspace_binding(
|
|
109
|
+
*,
|
|
110
|
+
requested_path: Path,
|
|
111
|
+
interactive: bool,
|
|
112
|
+
create_if_missing: bool = False,
|
|
113
|
+
allow_broad_workspace: bool = False,
|
|
114
|
+
source: str = "explicit_path",
|
|
115
|
+
action: str = WorkspaceAction.CHAT,
|
|
116
|
+
console: Any | None = None,
|
|
117
|
+
select_action_interactive: Callable[..., tuple[str | None, bool]] | None = None,
|
|
118
|
+
select_candidate_interactive: Callable[..., tuple[Path | None, bool]] | None = None,
|
|
119
|
+
prompt_text: Callable[..., str] | None = None,
|
|
120
|
+
) -> WorkspaceBinding:
|
|
121
|
+
prompter = prompt_text or typer.prompt
|
|
122
|
+
policy = workspace_policy(action)
|
|
123
|
+
binding = resolve_workspace_binding(
|
|
124
|
+
requested_path,
|
|
125
|
+
create_if_missing=create_if_missing,
|
|
126
|
+
allow_broad_workspace=allow_broad_workspace,
|
|
127
|
+
source=source,
|
|
128
|
+
)
|
|
129
|
+
try:
|
|
130
|
+
return ensure_workspace_policy(
|
|
131
|
+
binding,
|
|
132
|
+
action=policy.action,
|
|
133
|
+
allow_broad_workspace=allow_broad_workspace,
|
|
134
|
+
)
|
|
135
|
+
except WorkspaceBindingError:
|
|
136
|
+
pass
|
|
137
|
+
if binding.risk_level == WorkspaceRiskLevel.BLOCKED:
|
|
138
|
+
raise WorkspaceBindingError(
|
|
139
|
+
workspace_policy_violation_message(binding, action=policy.action)
|
|
140
|
+
)
|
|
141
|
+
if not interactive or not policy.interactive_guarded_resolution:
|
|
142
|
+
raise WorkspaceBindingError(
|
|
143
|
+
workspace_policy_violation_message(binding, action=policy.action)
|
|
144
|
+
)
|
|
145
|
+
return _resolve_guarded_binding_interactively(
|
|
146
|
+
binding=binding,
|
|
147
|
+
policy_action=policy.action,
|
|
148
|
+
allow_use_current_action=policy.interactive_allow_use_current,
|
|
149
|
+
console=console,
|
|
150
|
+
select_action_interactive=select_action_interactive,
|
|
151
|
+
select_candidate_interactive=select_candidate_interactive,
|
|
152
|
+
prompt_text=prompter,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _resolve_guarded_binding_interactively(
|
|
157
|
+
*,
|
|
158
|
+
binding: WorkspaceBinding,
|
|
159
|
+
policy_action: str,
|
|
160
|
+
allow_use_current_action: bool,
|
|
161
|
+
console: Any | None,
|
|
162
|
+
select_action_interactive: Callable[..., tuple[str | None, bool]] | None,
|
|
163
|
+
select_candidate_interactive: Callable[..., tuple[Path | None, bool]] | None,
|
|
164
|
+
prompt_text: Callable[..., str],
|
|
165
|
+
) -> WorkspaceBinding:
|
|
166
|
+
while True:
|
|
167
|
+
candidates = discover_workspace_candidates(binding.workspace_context.workspace_root)
|
|
168
|
+
selected_action = _choose_guarded_action(
|
|
169
|
+
binding=binding,
|
|
170
|
+
candidates=candidates,
|
|
171
|
+
allow_use_current_action=allow_use_current_action,
|
|
172
|
+
console=console,
|
|
173
|
+
select_action_interactive=select_action_interactive,
|
|
174
|
+
prompt_text=prompt_text,
|
|
175
|
+
)
|
|
176
|
+
if selected_action == _ACTION_CANCEL:
|
|
177
|
+
raise WorkspaceBindingError("Workspace binding cancelled.")
|
|
178
|
+
if selected_action == _ACTION_USE_CURRENT:
|
|
179
|
+
if not allow_use_current_action:
|
|
180
|
+
raise WorkspaceBindingError(
|
|
181
|
+
"This command requires a narrower workspace. "
|
|
182
|
+
"Pass --allow-broad-workspace to continue."
|
|
183
|
+
)
|
|
184
|
+
return resolve_workspace_binding(
|
|
185
|
+
binding.requested_path,
|
|
186
|
+
allow_broad_workspace=True,
|
|
187
|
+
source=binding.binding_source,
|
|
188
|
+
)
|
|
189
|
+
if selected_action == _ACTION_CHOOSE_PROJECT:
|
|
190
|
+
selected_path = _choose_workspace_candidate(
|
|
191
|
+
base_path=binding.workspace_context.workspace_root,
|
|
192
|
+
candidates=candidates,
|
|
193
|
+
console=console,
|
|
194
|
+
select_candidate_interactive=select_candidate_interactive,
|
|
195
|
+
prompt_text=prompt_text,
|
|
196
|
+
)
|
|
197
|
+
if selected_path is None:
|
|
198
|
+
continue
|
|
199
|
+
return resolve_startup_workspace_binding(
|
|
200
|
+
requested_path=selected_path,
|
|
201
|
+
interactive=True,
|
|
202
|
+
create_if_missing=False,
|
|
203
|
+
allow_broad_workspace=False,
|
|
204
|
+
source="startup_candidate",
|
|
205
|
+
action=policy_action,
|
|
206
|
+
console=console,
|
|
207
|
+
select_action_interactive=select_action_interactive,
|
|
208
|
+
select_candidate_interactive=select_candidate_interactive,
|
|
209
|
+
prompt_text=prompt_text,
|
|
210
|
+
)
|
|
211
|
+
if selected_action == _ACTION_CREATE_FOLDER:
|
|
212
|
+
created_path = _prompt_new_child_path(
|
|
213
|
+
base_path=binding.workspace_context.workspace_root,
|
|
214
|
+
prompt_text=prompt_text,
|
|
215
|
+
)
|
|
216
|
+
if created_path is None:
|
|
217
|
+
continue
|
|
218
|
+
return resolve_startup_workspace_binding(
|
|
219
|
+
requested_path=created_path,
|
|
220
|
+
interactive=True,
|
|
221
|
+
create_if_missing=True,
|
|
222
|
+
allow_broad_workspace=False,
|
|
223
|
+
source="startup_create_path",
|
|
224
|
+
action=policy_action,
|
|
225
|
+
console=console,
|
|
226
|
+
select_action_interactive=select_action_interactive,
|
|
227
|
+
select_candidate_interactive=select_candidate_interactive,
|
|
228
|
+
prompt_text=prompt_text,
|
|
229
|
+
)
|
|
230
|
+
if selected_action == _ACTION_ENTER_PATH:
|
|
231
|
+
entered_path = _prompt_workspace_path(
|
|
232
|
+
base_path=binding.workspace_context.workspace_root,
|
|
233
|
+
prompt_text=prompt_text,
|
|
234
|
+
)
|
|
235
|
+
if entered_path is None:
|
|
236
|
+
continue
|
|
237
|
+
return resolve_startup_workspace_binding(
|
|
238
|
+
requested_path=entered_path,
|
|
239
|
+
interactive=True,
|
|
240
|
+
create_if_missing=False,
|
|
241
|
+
allow_broad_workspace=False,
|
|
242
|
+
source="startup_entered_path",
|
|
243
|
+
action=policy_action,
|
|
244
|
+
console=console,
|
|
245
|
+
select_action_interactive=select_action_interactive,
|
|
246
|
+
select_candidate_interactive=select_candidate_interactive,
|
|
247
|
+
prompt_text=prompt_text,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _choose_guarded_action(
|
|
252
|
+
*,
|
|
253
|
+
binding: WorkspaceBinding,
|
|
254
|
+
candidates: tuple[WorkspaceCandidate, ...],
|
|
255
|
+
allow_use_current_action: bool,
|
|
256
|
+
console: Any | None,
|
|
257
|
+
select_action_interactive: Callable[..., tuple[str | None, bool]] | None,
|
|
258
|
+
prompt_text: Callable[..., str],
|
|
259
|
+
) -> str:
|
|
260
|
+
if callable(select_action_interactive):
|
|
261
|
+
selected, interactive_available = select_action_interactive(
|
|
262
|
+
binding=binding,
|
|
263
|
+
candidates=candidates,
|
|
264
|
+
allow_use_current_action=allow_use_current_action,
|
|
265
|
+
console=console,
|
|
266
|
+
)
|
|
267
|
+
if interactive_available and selected is not None:
|
|
268
|
+
return selected
|
|
269
|
+
if allow_use_current_action:
|
|
270
|
+
prompt = "Workspace action [1=use current|2=choose project|3=create folder|4=enter path|5=cancel]"
|
|
271
|
+
default = "2" if candidates else "3"
|
|
272
|
+
else:
|
|
273
|
+
prompt = "Workspace action [1=choose project|2=create folder|3=enter path|4=cancel]"
|
|
274
|
+
default = "1" if candidates else "2"
|
|
275
|
+
try:
|
|
276
|
+
raw = prompt_text(
|
|
277
|
+
prompt,
|
|
278
|
+
default=default,
|
|
279
|
+
)
|
|
280
|
+
except (EOFError, KeyboardInterrupt):
|
|
281
|
+
return _ACTION_CANCEL
|
|
282
|
+
return _normalize_guarded_action(raw, allow_use_current_action=allow_use_current_action)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _choose_workspace_candidate(
|
|
286
|
+
*,
|
|
287
|
+
base_path: Path,
|
|
288
|
+
candidates: tuple[WorkspaceCandidate, ...],
|
|
289
|
+
console: Any | None,
|
|
290
|
+
select_candidate_interactive: Callable[..., tuple[Path | None, bool]] | None,
|
|
291
|
+
prompt_text: Callable[..., str],
|
|
292
|
+
) -> Path | None:
|
|
293
|
+
if not candidates:
|
|
294
|
+
typed_path = _prompt_workspace_path(base_path=base_path, prompt_text=prompt_text)
|
|
295
|
+
return typed_path
|
|
296
|
+
if callable(select_candidate_interactive):
|
|
297
|
+
selected, interactive_available = select_candidate_interactive(
|
|
298
|
+
base_path=base_path,
|
|
299
|
+
candidates=candidates,
|
|
300
|
+
console=console,
|
|
301
|
+
)
|
|
302
|
+
if interactive_available:
|
|
303
|
+
return selected
|
|
304
|
+
|
|
305
|
+
rows = workspace_candidate_rows(base_path=base_path, candidates=candidates)
|
|
306
|
+
try:
|
|
307
|
+
raw = prompt_text(
|
|
308
|
+
f"Project folder [1-{len(rows)} or blank to cancel]",
|
|
309
|
+
default="1",
|
|
310
|
+
)
|
|
311
|
+
except (EOFError, KeyboardInterrupt):
|
|
312
|
+
return None
|
|
313
|
+
text = str(raw or "").strip()
|
|
314
|
+
if not text:
|
|
315
|
+
return None
|
|
316
|
+
if text.isdigit():
|
|
317
|
+
index = int(text)
|
|
318
|
+
if 1 <= index <= len(rows):
|
|
319
|
+
return Path(rows[index - 1][0])
|
|
320
|
+
raise WorkspaceBindingError("Invalid project selection.")
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _normalize_guarded_action(raw: str, *, allow_use_current_action: bool = True) -> str:
|
|
324
|
+
value = str(raw or "").strip().casefold()
|
|
325
|
+
if allow_use_current_action:
|
|
326
|
+
aliases = {
|
|
327
|
+
"1": _ACTION_USE_CURRENT,
|
|
328
|
+
"use": _ACTION_USE_CURRENT,
|
|
329
|
+
"use_current": _ACTION_USE_CURRENT,
|
|
330
|
+
"current": _ACTION_USE_CURRENT,
|
|
331
|
+
"2": _ACTION_CHOOSE_PROJECT,
|
|
332
|
+
"choose": _ACTION_CHOOSE_PROJECT,
|
|
333
|
+
"project": _ACTION_CHOOSE_PROJECT,
|
|
334
|
+
"choose_project": _ACTION_CHOOSE_PROJECT,
|
|
335
|
+
"3": _ACTION_CREATE_FOLDER,
|
|
336
|
+
"create": _ACTION_CREATE_FOLDER,
|
|
337
|
+
"new": _ACTION_CREATE_FOLDER,
|
|
338
|
+
"create_folder": _ACTION_CREATE_FOLDER,
|
|
339
|
+
"4": _ACTION_ENTER_PATH,
|
|
340
|
+
"enter": _ACTION_ENTER_PATH,
|
|
341
|
+
"path": _ACTION_ENTER_PATH,
|
|
342
|
+
"enter_path": _ACTION_ENTER_PATH,
|
|
343
|
+
"5": _ACTION_CANCEL,
|
|
344
|
+
"cancel": _ACTION_CANCEL,
|
|
345
|
+
"quit": _ACTION_CANCEL,
|
|
346
|
+
}
|
|
347
|
+
else:
|
|
348
|
+
aliases = {
|
|
349
|
+
"1": _ACTION_CHOOSE_PROJECT,
|
|
350
|
+
"choose": _ACTION_CHOOSE_PROJECT,
|
|
351
|
+
"project": _ACTION_CHOOSE_PROJECT,
|
|
352
|
+
"choose_project": _ACTION_CHOOSE_PROJECT,
|
|
353
|
+
"2": _ACTION_CREATE_FOLDER,
|
|
354
|
+
"create": _ACTION_CREATE_FOLDER,
|
|
355
|
+
"new": _ACTION_CREATE_FOLDER,
|
|
356
|
+
"create_folder": _ACTION_CREATE_FOLDER,
|
|
357
|
+
"3": _ACTION_ENTER_PATH,
|
|
358
|
+
"enter": _ACTION_ENTER_PATH,
|
|
359
|
+
"path": _ACTION_ENTER_PATH,
|
|
360
|
+
"enter_path": _ACTION_ENTER_PATH,
|
|
361
|
+
"4": _ACTION_CANCEL,
|
|
362
|
+
"cancel": _ACTION_CANCEL,
|
|
363
|
+
"quit": _ACTION_CANCEL,
|
|
364
|
+
}
|
|
365
|
+
return aliases.get(value, _DEFAULT_ACTION)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _prompt_new_child_path(*, base_path: Path, prompt_text: Callable[..., str]) -> Path | None:
|
|
369
|
+
try:
|
|
370
|
+
raw = prompt_text("New folder name", default="new-project")
|
|
371
|
+
except (EOFError, KeyboardInterrupt):
|
|
372
|
+
return None
|
|
373
|
+
text = str(raw or "").strip()
|
|
374
|
+
if not text:
|
|
375
|
+
return None
|
|
376
|
+
rel = Path(text)
|
|
377
|
+
if rel.is_absolute():
|
|
378
|
+
raise WorkspaceBindingError("New folder name must be relative to the current directory.")
|
|
379
|
+
target = (base_path / rel).expanduser().resolve(strict=False)
|
|
380
|
+
_ensure_within_base(base_path=base_path, candidate=target)
|
|
381
|
+
return target
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _prompt_workspace_path(*, base_path: Path, prompt_text: Callable[..., str]) -> Path | None:
|
|
385
|
+
try:
|
|
386
|
+
raw = prompt_text("Workspace path")
|
|
387
|
+
except (EOFError, KeyboardInterrupt):
|
|
388
|
+
return None
|
|
389
|
+
text = str(raw or "").strip()
|
|
390
|
+
if not text:
|
|
391
|
+
return None
|
|
392
|
+
candidate = Path(text).expanduser()
|
|
393
|
+
if not candidate.is_absolute():
|
|
394
|
+
candidate = base_path / candidate
|
|
395
|
+
return candidate.resolve(strict=False)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _ensure_within_base(*, base_path: Path, candidate: Path) -> None:
|
|
399
|
+
try:
|
|
400
|
+
candidate.relative_to(base_path.resolve())
|
|
401
|
+
except ValueError as e:
|
|
402
|
+
raise WorkspaceBindingError(
|
|
403
|
+
f"Path must stay under {base_path.resolve()}: {candidate}"
|
|
404
|
+
) from e
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _display_path(path: Path) -> str:
|
|
408
|
+
return os.fspath(path.expanduser().resolve(strict=False))
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .git_safe import build_git_process_env
|
|
10
|
+
|
|
11
|
+
_HEAD_REF_PREFIX = "ref:"
|
|
12
|
+
_HEADS_PREFIX = "refs/heads/"
|
|
13
|
+
_OID_RE = re.compile(r"^[0-9a-fA-F]{40,64}$")
|
|
14
|
+
_GIT_PROBE_TIMEOUT_S = 2.0
|
|
15
|
+
|
|
16
|
+
WORKSPACE_KIND_GIT_REPO = "git_repo"
|
|
17
|
+
WORKSPACE_KIND_GIT_REPO_NO_HEAD = "git_repo_no_head"
|
|
18
|
+
WORKSPACE_KIND_PLAIN_DIR = "plain_dir"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class WorkspaceContextError(RuntimeError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class WorkspaceContext:
|
|
27
|
+
input_path: Path
|
|
28
|
+
focus_path: Path
|
|
29
|
+
workspace_root: Path
|
|
30
|
+
git_root: Path | None
|
|
31
|
+
focus_relpath: str
|
|
32
|
+
workspace_kind: str
|
|
33
|
+
has_head_commit: bool
|
|
34
|
+
current_branch: str | None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class _GitResolution:
|
|
39
|
+
workspace_root: Path
|
|
40
|
+
git_root: Path
|
|
41
|
+
has_head_commit: bool
|
|
42
|
+
current_branch: str | None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def resolve_workspace_context(path: Path) -> WorkspaceContext:
|
|
46
|
+
"""Resolve workspace metadata for an existing directory without mutating the filesystem."""
|
|
47
|
+
input_path = _resolve_input_path(path)
|
|
48
|
+
git_resolution = _resolve_git_resolution(input_path)
|
|
49
|
+
if git_resolution is None:
|
|
50
|
+
return WorkspaceContext(
|
|
51
|
+
input_path=input_path,
|
|
52
|
+
focus_path=input_path,
|
|
53
|
+
workspace_root=input_path,
|
|
54
|
+
git_root=None,
|
|
55
|
+
focus_relpath=".",
|
|
56
|
+
workspace_kind=WORKSPACE_KIND_PLAIN_DIR,
|
|
57
|
+
has_head_commit=False,
|
|
58
|
+
current_branch=None,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
workspace_root = git_resolution.workspace_root
|
|
62
|
+
focus_relpath = "."
|
|
63
|
+
if input_path != workspace_root:
|
|
64
|
+
focus_relpath = input_path.relative_to(workspace_root).as_posix()
|
|
65
|
+
return WorkspaceContext(
|
|
66
|
+
input_path=input_path,
|
|
67
|
+
focus_path=input_path,
|
|
68
|
+
workspace_root=workspace_root,
|
|
69
|
+
git_root=git_resolution.git_root,
|
|
70
|
+
focus_relpath=focus_relpath,
|
|
71
|
+
workspace_kind=(
|
|
72
|
+
WORKSPACE_KIND_GIT_REPO
|
|
73
|
+
if git_resolution.has_head_commit
|
|
74
|
+
else WORKSPACE_KIND_GIT_REPO_NO_HEAD
|
|
75
|
+
),
|
|
76
|
+
has_head_commit=git_resolution.has_head_commit,
|
|
77
|
+
current_branch=git_resolution.current_branch,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _resolve_input_path(path: Path) -> Path:
|
|
82
|
+
resolved = path.expanduser().resolve()
|
|
83
|
+
if not resolved.exists():
|
|
84
|
+
raise WorkspaceContextError(f"Workspace path does not exist: {resolved}")
|
|
85
|
+
if not resolved.is_dir():
|
|
86
|
+
raise WorkspaceContextError(f"Workspace path is not a directory: {resolved}")
|
|
87
|
+
return resolved
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _resolve_git_resolution(path: Path) -> _GitResolution | None:
|
|
91
|
+
fs_root = _find_git_root_from_filesystem(path)
|
|
92
|
+
if fs_root is None:
|
|
93
|
+
return None
|
|
94
|
+
git_root = _git_show_toplevel(path)
|
|
95
|
+
if git_root is not None:
|
|
96
|
+
return _git_resolution_from_root(git_root)
|
|
97
|
+
return _git_resolution_from_root(fs_root)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _git_show_toplevel(path: Path) -> Path | None:
|
|
101
|
+
cp = _run_git(path, ["rev-parse", "--show-toplevel"])
|
|
102
|
+
if cp is None or cp.returncode != 0:
|
|
103
|
+
return None
|
|
104
|
+
value = (cp.stdout or "").strip()
|
|
105
|
+
if not value:
|
|
106
|
+
return None
|
|
107
|
+
try:
|
|
108
|
+
return Path(value).resolve()
|
|
109
|
+
except OSError:
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _run_git(path: Path, args: list[str]) -> subprocess.CompletedProcess[str] | None:
|
|
114
|
+
try:
|
|
115
|
+
return subprocess.run(
|
|
116
|
+
["git", "-C", os.fspath(path), *args],
|
|
117
|
+
check=False,
|
|
118
|
+
capture_output=True,
|
|
119
|
+
text=True,
|
|
120
|
+
env=_git_probe_env(),
|
|
121
|
+
timeout=_GIT_PROBE_TIMEOUT_S,
|
|
122
|
+
)
|
|
123
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _git_probe_env() -> dict[str, str]:
|
|
128
|
+
return build_git_process_env()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _git_resolution_from_root(workspace_root: Path) -> _GitResolution:
|
|
132
|
+
current_branch = _git_current_branch(workspace_root)
|
|
133
|
+
has_head_commit = _git_has_head_commit(workspace_root)
|
|
134
|
+
if current_branch is None and not has_head_commit:
|
|
135
|
+
current_branch, has_head_commit = _filesystem_head_state(workspace_root)
|
|
136
|
+
return _GitResolution(
|
|
137
|
+
workspace_root=workspace_root,
|
|
138
|
+
git_root=workspace_root,
|
|
139
|
+
has_head_commit=has_head_commit,
|
|
140
|
+
current_branch=current_branch,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _git_current_branch(workspace_root: Path) -> str | None:
|
|
145
|
+
symbolic = _run_git(workspace_root, ["symbolic-ref", "--quiet", "--short", "HEAD"])
|
|
146
|
+
if symbolic is not None and symbolic.returncode == 0:
|
|
147
|
+
branch = (symbolic.stdout or "").strip()
|
|
148
|
+
if branch:
|
|
149
|
+
return branch
|
|
150
|
+
|
|
151
|
+
cp = _run_git(workspace_root, ["rev-parse", "--abbrev-ref", "HEAD"])
|
|
152
|
+
if cp is None or cp.returncode != 0:
|
|
153
|
+
return None
|
|
154
|
+
branch = (cp.stdout or "").strip()
|
|
155
|
+
if not branch or branch == "HEAD":
|
|
156
|
+
return None
|
|
157
|
+
return branch
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _git_has_head_commit(workspace_root: Path) -> bool:
|
|
161
|
+
cp = _run_git(workspace_root, ["rev-parse", "--verify", "HEAD"])
|
|
162
|
+
return cp is not None and cp.returncode == 0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _find_git_root_from_filesystem(path: Path) -> Path | None:
|
|
166
|
+
for candidate in (path, *path.parents):
|
|
167
|
+
marker = candidate / ".git"
|
|
168
|
+
if marker.exists() and _is_valid_filesystem_git_root(candidate):
|
|
169
|
+
return candidate
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _is_valid_filesystem_git_root(workspace_root: Path) -> bool:
|
|
174
|
+
git_dir = _resolve_git_dir(workspace_root)
|
|
175
|
+
if git_dir is None:
|
|
176
|
+
return False
|
|
177
|
+
return (git_dir / "HEAD").is_file()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _filesystem_head_state(workspace_root: Path) -> tuple[str | None, bool]:
|
|
181
|
+
git_dir = _resolve_git_dir(workspace_root)
|
|
182
|
+
if git_dir is None:
|
|
183
|
+
return None, False
|
|
184
|
+
head_path = git_dir / "HEAD"
|
|
185
|
+
try:
|
|
186
|
+
head = head_path.read_text(encoding="utf-8").strip()
|
|
187
|
+
except OSError:
|
|
188
|
+
return None, False
|
|
189
|
+
if not head:
|
|
190
|
+
return None, False
|
|
191
|
+
if head.startswith(_HEAD_REF_PREFIX):
|
|
192
|
+
ref_name = head[len(_HEAD_REF_PREFIX) :].strip()
|
|
193
|
+
branch = _branch_name_from_ref(ref_name)
|
|
194
|
+
return branch, _git_ref_exists(git_dir, ref_name)
|
|
195
|
+
return None, bool(_OID_RE.fullmatch(head))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _resolve_git_dir(workspace_root: Path) -> Path | None:
|
|
199
|
+
marker = workspace_root / ".git"
|
|
200
|
+
if marker.is_dir():
|
|
201
|
+
return marker.resolve()
|
|
202
|
+
if not marker.is_file():
|
|
203
|
+
return None
|
|
204
|
+
try:
|
|
205
|
+
first_line = marker.read_text(encoding="utf-8").splitlines()[0].strip()
|
|
206
|
+
except (IndexError, OSError, UnicodeDecodeError):
|
|
207
|
+
return None
|
|
208
|
+
if not first_line.lower().startswith("gitdir:"):
|
|
209
|
+
return None
|
|
210
|
+
value = first_line.split(":", 1)[1].strip()
|
|
211
|
+
candidate = Path(value)
|
|
212
|
+
if not candidate.is_absolute():
|
|
213
|
+
candidate = workspace_root / candidate
|
|
214
|
+
try:
|
|
215
|
+
return candidate.resolve()
|
|
216
|
+
except OSError:
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _git_ref_exists(git_dir: Path, ref_name: str) -> bool:
|
|
221
|
+
for base_dir in _git_state_dirs(git_dir):
|
|
222
|
+
ref_path = base_dir / ref_name
|
|
223
|
+
try:
|
|
224
|
+
value = ref_path.read_text(encoding="utf-8").strip()
|
|
225
|
+
except OSError:
|
|
226
|
+
value = ""
|
|
227
|
+
if _OID_RE.fullmatch(value):
|
|
228
|
+
return True
|
|
229
|
+
|
|
230
|
+
packed_refs = base_dir / "packed-refs"
|
|
231
|
+
try:
|
|
232
|
+
for line in packed_refs.read_text(encoding="utf-8").splitlines():
|
|
233
|
+
entry = line.strip()
|
|
234
|
+
if not entry or entry.startswith("#") or entry.startswith("^"):
|
|
235
|
+
continue
|
|
236
|
+
oid, sep, packed_ref = entry.partition(" ")
|
|
237
|
+
if sep and packed_ref == ref_name and _OID_RE.fullmatch(oid):
|
|
238
|
+
return True
|
|
239
|
+
except OSError:
|
|
240
|
+
continue
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _git_state_dirs(git_dir: Path) -> list[Path]:
|
|
245
|
+
dirs = [git_dir]
|
|
246
|
+
commondir_path = git_dir / "commondir"
|
|
247
|
+
try:
|
|
248
|
+
commondir_value = commondir_path.read_text(encoding="utf-8").strip()
|
|
249
|
+
except OSError:
|
|
250
|
+
commondir_value = ""
|
|
251
|
+
if not commondir_value:
|
|
252
|
+
return dirs
|
|
253
|
+
|
|
254
|
+
candidate = Path(commondir_value)
|
|
255
|
+
if not candidate.is_absolute():
|
|
256
|
+
candidate = git_dir / candidate
|
|
257
|
+
try:
|
|
258
|
+
resolved = candidate.resolve()
|
|
259
|
+
except OSError:
|
|
260
|
+
return dirs
|
|
261
|
+
if resolved not in dirs:
|
|
262
|
+
dirs.append(resolved)
|
|
263
|
+
return dirs
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _branch_name_from_ref(ref_name: str) -> str | None:
|
|
267
|
+
ref_name = ref_name.strip()
|
|
268
|
+
if not ref_name:
|
|
269
|
+
return None
|
|
270
|
+
if ref_name.startswith(_HEADS_PREFIX):
|
|
271
|
+
branch = ref_name[len(_HEADS_PREFIX) :].strip()
|
|
272
|
+
return branch or None
|
|
273
|
+
return ref_name
|