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/tools/git.py
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
import threading
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ..diff_paths import iter_patch_paths
|
|
12
|
+
from .fs import (
|
|
13
|
+
FilePrecondition,
|
|
14
|
+
FsError,
|
|
15
|
+
StaleFileError,
|
|
16
|
+
assert_file_precondition,
|
|
17
|
+
capture_file_precondition,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GitError(RuntimeError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GitStaleFileError(GitError, StaleFileError):
|
|
26
|
+
"""A patch precondition conflict recognizable by both tool error contracts."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, path: str) -> None:
|
|
29
|
+
StaleFileError.__init__(self, path)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_GIT_HISTORY_DEFAULT_LOG_LIMIT = 10
|
|
33
|
+
_GIT_HISTORY_MAX_LOG_LIMIT = 50
|
|
34
|
+
_GIT_HISTORY_LOG_BODY_MAX_CHARS = 240
|
|
35
|
+
_GIT_HISTORY_SHOW_BODY_MAX_CHARS = 1200
|
|
36
|
+
_GIT_HISTORY_SHOW_PATCH_MAX_CHARS = 6000
|
|
37
|
+
_GIT_HISTORY_BLAME_MAX_LINES = 200
|
|
38
|
+
_GIT_HISTORY_BLAME_LINE_MAX_CHARS = 240
|
|
39
|
+
_VALID_UNIFIED_HUNK_RE = re.compile(r"^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@(?: .*)?$")
|
|
40
|
+
_PATCH_LOCKS_GUARD = threading.Lock()
|
|
41
|
+
_PATCH_LOCKS: dict[str, threading.RLock] = {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _run_git(
|
|
45
|
+
root: Path,
|
|
46
|
+
args: list[str],
|
|
47
|
+
*,
|
|
48
|
+
input_s: str | None = None,
|
|
49
|
+
env: dict[str, str] | None = None,
|
|
50
|
+
) -> subprocess.CompletedProcess[str]:
|
|
51
|
+
try:
|
|
52
|
+
return subprocess.run(
|
|
53
|
+
["git", "-C", str(root), *args],
|
|
54
|
+
input=input_s,
|
|
55
|
+
check=False,
|
|
56
|
+
capture_output=True,
|
|
57
|
+
text=True,
|
|
58
|
+
env=env,
|
|
59
|
+
)
|
|
60
|
+
except OSError as e:
|
|
61
|
+
raise GitError("git not available") from e
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _normalize_git_error(stderr: str, *, fallback: str) -> str:
|
|
65
|
+
message = stderr.strip()
|
|
66
|
+
lowered = message.lower()
|
|
67
|
+
if "not a git repository" in lowered:
|
|
68
|
+
return "not a git repository"
|
|
69
|
+
if "unknown revision or path not in the working tree" in lowered:
|
|
70
|
+
return "invalid revision or path"
|
|
71
|
+
if "bad revision" in lowered or "bad object" in lowered:
|
|
72
|
+
return "invalid revision"
|
|
73
|
+
if "no such path" in lowered or "no such file" in lowered:
|
|
74
|
+
return "invalid path"
|
|
75
|
+
return message or fallback
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _run_git_checked(
|
|
79
|
+
*,
|
|
80
|
+
root: Path,
|
|
81
|
+
args: list[str],
|
|
82
|
+
input_s: str | None = None,
|
|
83
|
+
env: dict[str, str] | None = None,
|
|
84
|
+
fallback: str,
|
|
85
|
+
) -> subprocess.CompletedProcess[str]:
|
|
86
|
+
cp = _run_git(root, args, input_s=input_s, env=env)
|
|
87
|
+
if cp.returncode != 0:
|
|
88
|
+
raise GitError(_normalize_git_error(cp.stderr, fallback=fallback))
|
|
89
|
+
return cp
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _normalize_patch_text(patch: str) -> str:
|
|
93
|
+
normalized = patch.replace("\r\n", "\n").replace("\r", "\n")
|
|
94
|
+
if normalized and not normalized.endswith("\n"):
|
|
95
|
+
normalized += "\n"
|
|
96
|
+
return normalized
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _validate_patch_shape(patch_text: str) -> None:
|
|
100
|
+
if not patch_text.strip():
|
|
101
|
+
raise GitError("malformed patch: patch is empty")
|
|
102
|
+
|
|
103
|
+
patch_paths = iter_patch_paths(patch_text)
|
|
104
|
+
if not patch_paths:
|
|
105
|
+
raise GitError("malformed patch: no file paths found in patch headers")
|
|
106
|
+
|
|
107
|
+
lines = patch_text.splitlines()
|
|
108
|
+
has_old_header = any(line.startswith("--- ") for line in lines)
|
|
109
|
+
has_new_header = any(line.startswith("+++ ") for line in lines)
|
|
110
|
+
if has_old_header != has_new_header:
|
|
111
|
+
raise GitError("malformed patch: missing paired ---/+++ file headers")
|
|
112
|
+
|
|
113
|
+
invalid_hunk_header = next(
|
|
114
|
+
(
|
|
115
|
+
line
|
|
116
|
+
for line in lines
|
|
117
|
+
if line.startswith("@@") and not _VALID_UNIFIED_HUNK_RE.match(line)
|
|
118
|
+
),
|
|
119
|
+
None,
|
|
120
|
+
)
|
|
121
|
+
if invalid_hunk_header is None:
|
|
122
|
+
return
|
|
123
|
+
if invalid_hunk_header.startswith("@@ ..."):
|
|
124
|
+
raise GitError("malformed patch: placeholder hunk header '@@ ...' is not allowed")
|
|
125
|
+
raise GitError(f"malformed patch: invalid hunk header: {invalid_hunk_header}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _patch_lock(root: Path) -> threading.RLock:
|
|
129
|
+
key = os.path.normcase(os.fspath(root.resolve()))
|
|
130
|
+
with _PATCH_LOCKS_GUARD:
|
|
131
|
+
lock = _PATCH_LOCKS.get(key)
|
|
132
|
+
if lock is None:
|
|
133
|
+
lock = threading.RLock()
|
|
134
|
+
_PATCH_LOCKS[key] = lock
|
|
135
|
+
return lock
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _capture_patch_preconditions(root: Path, patch_text: str) -> tuple[FilePrecondition, ...]:
|
|
139
|
+
preconditions: list[FilePrecondition] = []
|
|
140
|
+
for raw_path in iter_patch_paths(patch_text):
|
|
141
|
+
normalized_path = _resolve_git_path(root, raw_path)
|
|
142
|
+
try:
|
|
143
|
+
preconditions.append(capture_file_precondition(root=root, path=normalized_path))
|
|
144
|
+
except StaleFileError as exc:
|
|
145
|
+
raise GitStaleFileError(exc.path) from exc
|
|
146
|
+
except FsError as exc:
|
|
147
|
+
raise GitError(f"Cannot safely prepare patch path {normalized_path}: {exc}") from exc
|
|
148
|
+
return tuple(preconditions)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _assert_patch_preconditions(
|
|
152
|
+
root: Path,
|
|
153
|
+
preconditions: tuple[FilePrecondition, ...],
|
|
154
|
+
) -> None:
|
|
155
|
+
for precondition in preconditions:
|
|
156
|
+
try:
|
|
157
|
+
assert_file_precondition(root=root, precondition=precondition)
|
|
158
|
+
except StaleFileError as exc:
|
|
159
|
+
raise GitStaleFileError(exc.path) from exc
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _resolve_git_path(root: Path, user_path: str) -> str:
|
|
163
|
+
root_abs = root.resolve()
|
|
164
|
+
target = (root_abs / user_path).resolve()
|
|
165
|
+
try:
|
|
166
|
+
rel = target.relative_to(root_abs)
|
|
167
|
+
except ValueError as e:
|
|
168
|
+
raise GitError(f"Path escapes root: {user_path}") from e
|
|
169
|
+
return os.fspath(rel).replace("\\", "/")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _clip_text(text: str, *, max_chars: int) -> tuple[str, bool]:
|
|
173
|
+
normalized = text.strip()
|
|
174
|
+
if len(normalized) <= max_chars:
|
|
175
|
+
return normalized, False
|
|
176
|
+
return normalized[:max_chars].rstrip() + "...(truncated)", True
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _parse_log_records(stdout: str) -> list[dict[str, str]]:
|
|
180
|
+
commits: list[dict[str, str]] = []
|
|
181
|
+
for raw_record in stdout.split("\x1e"):
|
|
182
|
+
record = raw_record.strip("\x1e\r\n")
|
|
183
|
+
if not record:
|
|
184
|
+
continue
|
|
185
|
+
fields = record.split("\x1f", 6)
|
|
186
|
+
if len(fields) != 7:
|
|
187
|
+
continue
|
|
188
|
+
commit, short_commit, author_name, author_email, authored_at, subject, body = fields
|
|
189
|
+
body_excerpt, body_truncated = _clip_text(body, max_chars=_GIT_HISTORY_LOG_BODY_MAX_CHARS)
|
|
190
|
+
commits.append(
|
|
191
|
+
{
|
|
192
|
+
"commit": commit,
|
|
193
|
+
"short_commit": short_commit,
|
|
194
|
+
"author_name": author_name,
|
|
195
|
+
"author_email": author_email,
|
|
196
|
+
"authored_at": authored_at,
|
|
197
|
+
"subject": subject,
|
|
198
|
+
"body_excerpt": body_excerpt,
|
|
199
|
+
"body_truncated": body_truncated,
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
return commits
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def git_status(*, root: Path) -> dict[str, Any]:
|
|
206
|
+
cp = _run_git_checked(
|
|
207
|
+
root=root,
|
|
208
|
+
args=["status", "--porcelain=v1", "-b"],
|
|
209
|
+
fallback="git status failed",
|
|
210
|
+
)
|
|
211
|
+
return {"status": cp.stdout}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def git_diff(*, root: Path) -> dict[str, Any]:
|
|
215
|
+
cp = _run_git_checked(root=root, args=["diff"], fallback="git diff failed")
|
|
216
|
+
diff = cp.stdout
|
|
217
|
+
if len(diff) > 20000:
|
|
218
|
+
diff = diff[:20000] + "...(truncated)"
|
|
219
|
+
return {"diff": diff}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def git_history(
|
|
223
|
+
*,
|
|
224
|
+
root: Path,
|
|
225
|
+
mode: str,
|
|
226
|
+
path: str | None = None,
|
|
227
|
+
limit: int = _GIT_HISTORY_DEFAULT_LOG_LIMIT,
|
|
228
|
+
ref: str | None = None,
|
|
229
|
+
grep: str | None = None,
|
|
230
|
+
author: str | None = None,
|
|
231
|
+
commit: str | None = None,
|
|
232
|
+
start_line: int | None = None,
|
|
233
|
+
end_line: int | None = None,
|
|
234
|
+
) -> dict[str, Any]:
|
|
235
|
+
normalized_mode = str(mode or "").strip().lower()
|
|
236
|
+
if normalized_mode not in {"log", "show", "blame"}:
|
|
237
|
+
raise GitError(f"Unsupported git_history mode: {mode!r}")
|
|
238
|
+
|
|
239
|
+
normalized_path = None
|
|
240
|
+
if path is not None and str(path).strip():
|
|
241
|
+
normalized_path = _resolve_git_path(root, str(path))
|
|
242
|
+
|
|
243
|
+
if normalized_mode == "log":
|
|
244
|
+
safe_limit = max(1, min(int(limit), _GIT_HISTORY_MAX_LOG_LIMIT))
|
|
245
|
+
ref_value = str(ref or "HEAD").strip() or "HEAD"
|
|
246
|
+
args = [
|
|
247
|
+
"log",
|
|
248
|
+
f"-n{safe_limit + 1}",
|
|
249
|
+
"--date=iso-strict",
|
|
250
|
+
"--format=format:%H%x1f%h%x1f%an%x1f%ae%x1f%aI%x1f%s%x1f%b%x1e",
|
|
251
|
+
]
|
|
252
|
+
if grep:
|
|
253
|
+
args.extend(["--grep", str(grep)])
|
|
254
|
+
if author:
|
|
255
|
+
args.extend(["--author", str(author)])
|
|
256
|
+
args.append(ref_value)
|
|
257
|
+
if normalized_path is not None:
|
|
258
|
+
args.extend(["--", normalized_path])
|
|
259
|
+
cp = _run_git_checked(root=root, args=args, fallback="git log failed")
|
|
260
|
+
commits = _parse_log_records(cp.stdout)
|
|
261
|
+
truncated = len(commits) > safe_limit
|
|
262
|
+
return {
|
|
263
|
+
"mode": "log",
|
|
264
|
+
"path": normalized_path,
|
|
265
|
+
"limit": safe_limit,
|
|
266
|
+
"ref": ref_value,
|
|
267
|
+
"grep": str(grep) if grep else None,
|
|
268
|
+
"author": str(author) if author else None,
|
|
269
|
+
"commits": commits[:safe_limit],
|
|
270
|
+
"truncated": truncated,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if normalized_mode == "show":
|
|
274
|
+
explicit_commit = str(commit or "").strip()
|
|
275
|
+
explicit_ref = str(ref or "").strip()
|
|
276
|
+
if explicit_commit and explicit_ref and explicit_commit != explicit_ref:
|
|
277
|
+
raise GitError("show mode commit and ref must name the same revision")
|
|
278
|
+
commit_value = explicit_commit or explicit_ref
|
|
279
|
+
if not commit_value:
|
|
280
|
+
raise GitError("show mode requires commit or ref")
|
|
281
|
+
meta_cp = _run_git_checked(
|
|
282
|
+
root=root,
|
|
283
|
+
args=[
|
|
284
|
+
"show",
|
|
285
|
+
"--quiet",
|
|
286
|
+
"--no-patch",
|
|
287
|
+
"--date=iso-strict",
|
|
288
|
+
"--format=format:%H%x1f%h%x1f%an%x1f%ae%x1f%aI%x1f%s%x1f%b",
|
|
289
|
+
commit_value,
|
|
290
|
+
],
|
|
291
|
+
fallback="git show failed",
|
|
292
|
+
)
|
|
293
|
+
fields = meta_cp.stdout.split("\x1f", 6)
|
|
294
|
+
if len(fields) != 7:
|
|
295
|
+
raise GitError("git show returned an unexpected format")
|
|
296
|
+
body_excerpt, body_truncated = _clip_text(
|
|
297
|
+
fields[6], max_chars=_GIT_HISTORY_SHOW_BODY_MAX_CHARS
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
patch_args = ["show", "--format=", "--patch", "--no-ext-diff", "--no-color", commit_value]
|
|
301
|
+
if normalized_path is not None:
|
|
302
|
+
patch_args.extend(["--", normalized_path])
|
|
303
|
+
patch_cp = _run_git_checked(root=root, args=patch_args, fallback="git show failed")
|
|
304
|
+
patch_excerpt, patch_truncated = _clip_text(
|
|
305
|
+
patch_cp.stdout, max_chars=_GIT_HISTORY_SHOW_PATCH_MAX_CHARS
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
"mode": "show",
|
|
310
|
+
"path": normalized_path,
|
|
311
|
+
"commit": {
|
|
312
|
+
"commit": fields[0],
|
|
313
|
+
"short_commit": fields[1],
|
|
314
|
+
"author_name": fields[2],
|
|
315
|
+
"author_email": fields[3],
|
|
316
|
+
"authored_at": fields[4],
|
|
317
|
+
"subject": fields[5],
|
|
318
|
+
"body_excerpt": body_excerpt,
|
|
319
|
+
"body_truncated": body_truncated,
|
|
320
|
+
},
|
|
321
|
+
"patch_excerpt": patch_excerpt,
|
|
322
|
+
"patch_truncated": patch_truncated,
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if normalized_path is None:
|
|
326
|
+
raise GitError("blame mode requires path")
|
|
327
|
+
if start_line is None or end_line is None:
|
|
328
|
+
raise GitError("blame mode requires start_line and end_line")
|
|
329
|
+
if start_line < 1:
|
|
330
|
+
raise GitError(f"Invalid start_line: {start_line} (must be >= 1)")
|
|
331
|
+
if end_line < start_line:
|
|
332
|
+
raise GitError(
|
|
333
|
+
f"Invalid line range: end_line ({end_line}) must be >= start_line ({start_line})"
|
|
334
|
+
)
|
|
335
|
+
requested_lines = end_line - start_line + 1
|
|
336
|
+
if requested_lines > _GIT_HISTORY_BLAME_MAX_LINES:
|
|
337
|
+
raise GitError(
|
|
338
|
+
"Requested blame range too large: "
|
|
339
|
+
f"{requested_lines} lines (max {_GIT_HISTORY_BLAME_MAX_LINES})"
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
cp = _run_git_checked(
|
|
343
|
+
root=root,
|
|
344
|
+
args=[
|
|
345
|
+
"blame",
|
|
346
|
+
"--line-porcelain",
|
|
347
|
+
f"-L{start_line},{end_line}",
|
|
348
|
+
"--",
|
|
349
|
+
normalized_path,
|
|
350
|
+
],
|
|
351
|
+
fallback="git blame failed",
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
lines: list[dict[str, Any]] = []
|
|
355
|
+
current: dict[str, Any] | None = None
|
|
356
|
+
for raw_line in cp.stdout.splitlines():
|
|
357
|
+
if raw_line.startswith("\t"):
|
|
358
|
+
if current is None:
|
|
359
|
+
continue
|
|
360
|
+
line_text, line_truncated = _clip_text(
|
|
361
|
+
raw_line[1:], max_chars=_GIT_HISTORY_BLAME_LINE_MAX_CHARS
|
|
362
|
+
)
|
|
363
|
+
lines.append(
|
|
364
|
+
{
|
|
365
|
+
"line_number": int(current["line_number"]),
|
|
366
|
+
"commit": str(current["commit"]),
|
|
367
|
+
"short_commit": str(current["commit"])[:12],
|
|
368
|
+
"author_name": str(current.get("author") or ""),
|
|
369
|
+
"author_email": str(current.get("author-mail") or "").strip("<>"),
|
|
370
|
+
"summary": str(current.get("summary") or ""),
|
|
371
|
+
"content": line_text,
|
|
372
|
+
"content_truncated": line_truncated,
|
|
373
|
+
}
|
|
374
|
+
)
|
|
375
|
+
current = None
|
|
376
|
+
continue
|
|
377
|
+
|
|
378
|
+
parts = raw_line.split()
|
|
379
|
+
if len(parts) >= 4 and len(parts[0]) >= 8:
|
|
380
|
+
try:
|
|
381
|
+
final_lineno = int(parts[2])
|
|
382
|
+
except ValueError:
|
|
383
|
+
final_lineno = 0
|
|
384
|
+
current = {"commit": parts[0], "line_number": final_lineno}
|
|
385
|
+
continue
|
|
386
|
+
|
|
387
|
+
if current is None:
|
|
388
|
+
continue
|
|
389
|
+
key, _, value = raw_line.partition(" ")
|
|
390
|
+
current[key] = value
|
|
391
|
+
|
|
392
|
+
actual_end_line = end_line if not lines else int(lines[-1]["line_number"])
|
|
393
|
+
return {
|
|
394
|
+
"mode": "blame",
|
|
395
|
+
"path": normalized_path,
|
|
396
|
+
"start_line": start_line,
|
|
397
|
+
"end_line": actual_end_line,
|
|
398
|
+
"lines": lines,
|
|
399
|
+
"truncated": False,
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def git_apply_patch(*, root: Path, patch: str) -> dict[str, Any]:
|
|
404
|
+
normalized_patch = _normalize_patch_text(patch)
|
|
405
|
+
_validate_patch_shape(normalized_patch)
|
|
406
|
+
with _patch_lock(root):
|
|
407
|
+
preconditions = _capture_patch_preconditions(root, normalized_patch)
|
|
408
|
+
with tempfile.TemporaryDirectory(prefix="alysis-patch-index-") as temporary:
|
|
409
|
+
patch_env = dict(os.environ)
|
|
410
|
+
patch_env["GIT_INDEX_FILE"] = os.fspath(Path(temporary) / "index")
|
|
411
|
+
no_filters = ["-c", "core.autocrlf=false"]
|
|
412
|
+
_run_git_checked(
|
|
413
|
+
root=root,
|
|
414
|
+
args=[*no_filters, "read-tree", "--empty"],
|
|
415
|
+
env=patch_env,
|
|
416
|
+
fallback="git apply preflight failed",
|
|
417
|
+
)
|
|
418
|
+
existing_paths = [item.path for item in preconditions if item.exists]
|
|
419
|
+
if existing_paths:
|
|
420
|
+
_run_git_checked(
|
|
421
|
+
root=root,
|
|
422
|
+
args=[*no_filters, "add", "-f", "--", *existing_paths],
|
|
423
|
+
env=patch_env,
|
|
424
|
+
fallback="git apply preflight failed",
|
|
425
|
+
)
|
|
426
|
+
_assert_patch_preconditions(root, preconditions)
|
|
427
|
+
try:
|
|
428
|
+
_run_git_checked(
|
|
429
|
+
root=root,
|
|
430
|
+
args=[
|
|
431
|
+
*no_filters,
|
|
432
|
+
"apply",
|
|
433
|
+
"--check",
|
|
434
|
+
"--index",
|
|
435
|
+
"--whitespace=nowarn",
|
|
436
|
+
],
|
|
437
|
+
input_s=normalized_patch,
|
|
438
|
+
env=patch_env,
|
|
439
|
+
fallback="git apply preflight failed",
|
|
440
|
+
)
|
|
441
|
+
except GitError as e:
|
|
442
|
+
raise GitError(f"git apply preflight failed: {e}") from e
|
|
443
|
+
_assert_patch_preconditions(root, preconditions)
|
|
444
|
+
try:
|
|
445
|
+
# The private index records the exact whole-file preconditions.
|
|
446
|
+
# ``--index`` rechecks those blobs inside the mutating Git process,
|
|
447
|
+
# without reading or changing the developer's real index.
|
|
448
|
+
_run_git_checked(
|
|
449
|
+
root=root,
|
|
450
|
+
args=[*no_filters, "apply", "--index", "--whitespace=nowarn"],
|
|
451
|
+
input_s=normalized_patch,
|
|
452
|
+
env=patch_env,
|
|
453
|
+
fallback="git apply failed",
|
|
454
|
+
)
|
|
455
|
+
except GitError as exc:
|
|
456
|
+
# If another writer won after our final user-space check, surface
|
|
457
|
+
# the deterministic stale-file contract rather than a generic
|
|
458
|
+
# index mismatch.
|
|
459
|
+
_assert_patch_preconditions(root, preconditions)
|
|
460
|
+
raise exc
|
|
461
|
+
return {"applied": True}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..ide.protocol import redact_secrets
|
|
10
|
+
from ..session_artifacts import SessionArtifactLayout
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HistorySearchError(RuntimeError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_SNIPPET_MAX_CHARS = 240
|
|
18
|
+
_MAX_RESULTS = 500
|
|
19
|
+
_MAX_FILE_BYTES = 1024 * 1024
|
|
20
|
+
_MAX_TOTAL_BYTES = 8 * 1024 * 1024
|
|
21
|
+
_MAX_TARGET_FILES = 2_000
|
|
22
|
+
_MAX_SNIPPET_CHARS = 4_000
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _safe_component(raw: str) -> str:
|
|
26
|
+
clean = re.sub(r"[^A-Za-z0-9_-]", "_", str(raw).strip())
|
|
27
|
+
return clean or "x"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _clip_text(text: str, limit: int = _SNIPPET_MAX_CHARS) -> str:
|
|
31
|
+
compact = re.sub(r"\s+", " ", text.strip())
|
|
32
|
+
if len(compact) <= limit:
|
|
33
|
+
return compact
|
|
34
|
+
return compact[:limit].rstrip() + "..."
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class _ArtifactSearchRoot:
|
|
39
|
+
path: Path
|
|
40
|
+
layout: SessionArtifactLayout | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _search_roots(
|
|
44
|
+
*,
|
|
45
|
+
root: Path,
|
|
46
|
+
session_id: str,
|
|
47
|
+
session_artifact_root: Path | None,
|
|
48
|
+
) -> list[_ArtifactSearchRoot]:
|
|
49
|
+
roots: list[_ArtifactSearchRoot] = []
|
|
50
|
+
seen: set[Path] = set()
|
|
51
|
+
|
|
52
|
+
if session_artifact_root is not None:
|
|
53
|
+
artifact_root = session_artifact_root.resolve()
|
|
54
|
+
if artifact_root.name == _safe_component(session_id) and artifact_root not in seen:
|
|
55
|
+
roots.append(
|
|
56
|
+
_ArtifactSearchRoot(
|
|
57
|
+
path=artifact_root,
|
|
58
|
+
layout=SessionArtifactLayout(filesystem_root=artifact_root),
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
seen.add(artifact_root)
|
|
62
|
+
|
|
63
|
+
history_root = (root / ".alysis" / "sessions" / _safe_component(session_id)).resolve()
|
|
64
|
+
if history_root not in seen:
|
|
65
|
+
roots.append(_ArtifactSearchRoot(path=history_root))
|
|
66
|
+
seen.add(history_root)
|
|
67
|
+
return roots
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _iter_target_files(
|
|
71
|
+
*,
|
|
72
|
+
roots: list[_ArtifactSearchRoot],
|
|
73
|
+
include_history: bool,
|
|
74
|
+
include_tool_outputs: bool,
|
|
75
|
+
include_memory: bool,
|
|
76
|
+
) -> tuple[list[tuple[str, Path, SessionArtifactLayout | None]], bool]:
|
|
77
|
+
targets: list[tuple[str, Path, SessionArtifactLayout | None]] = []
|
|
78
|
+
for artifact_root in roots:
|
|
79
|
+
base = artifact_root.path
|
|
80
|
+
if include_history:
|
|
81
|
+
for path in sorted((base / "history").glob("chunk_*.jsonl")):
|
|
82
|
+
if _is_safe_artifact_file(path=path, root=base):
|
|
83
|
+
targets.append(("history", path, artifact_root.layout))
|
|
84
|
+
if len(targets) >= _MAX_TARGET_FILES:
|
|
85
|
+
return targets, True
|
|
86
|
+
if include_tool_outputs:
|
|
87
|
+
for path in sorted((base / "tool_outputs").glob("*.json")):
|
|
88
|
+
if _is_safe_artifact_file(path=path, root=base):
|
|
89
|
+
targets.append(("tool_output", path, artifact_root.layout))
|
|
90
|
+
if len(targets) >= _MAX_TARGET_FILES:
|
|
91
|
+
return targets, True
|
|
92
|
+
if include_memory:
|
|
93
|
+
for filename in ("summary.json", "pins.json"):
|
|
94
|
+
path = base / "memory" / filename
|
|
95
|
+
if _is_safe_artifact_file(path=path, root=base):
|
|
96
|
+
targets.append(("memory", path, artifact_root.layout))
|
|
97
|
+
if len(targets) >= _MAX_TARGET_FILES:
|
|
98
|
+
return targets, True
|
|
99
|
+
return targets, False
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _is_safe_artifact_file(*, path: Path, root: Path) -> bool:
|
|
103
|
+
try:
|
|
104
|
+
if path.is_symlink() or not path.is_file():
|
|
105
|
+
return False
|
|
106
|
+
path.resolve().relative_to(root.resolve())
|
|
107
|
+
except (OSError, RuntimeError, ValueError):
|
|
108
|
+
return False
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _display_artifact_path(
|
|
113
|
+
*,
|
|
114
|
+
file_path: Path,
|
|
115
|
+
workspace_root: Path,
|
|
116
|
+
layout: SessionArtifactLayout | None,
|
|
117
|
+
) -> str:
|
|
118
|
+
if layout is not None:
|
|
119
|
+
return layout.display_reference_for_path(
|
|
120
|
+
artifact_path=file_path,
|
|
121
|
+
workspace_root=workspace_root,
|
|
122
|
+
)
|
|
123
|
+
try:
|
|
124
|
+
return os.fspath(file_path.resolve().relative_to(workspace_root)).replace("\\", "/")
|
|
125
|
+
except ValueError:
|
|
126
|
+
return file_path.name
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def history_search(
|
|
130
|
+
*,
|
|
131
|
+
root: Path,
|
|
132
|
+
session_id: str,
|
|
133
|
+
session_artifact_root: Path | None = None,
|
|
134
|
+
pattern: str,
|
|
135
|
+
max_results: int = 50,
|
|
136
|
+
max_file_bytes: int = 200_000,
|
|
137
|
+
max_total_bytes: int = _MAX_TOTAL_BYTES,
|
|
138
|
+
max_snippet_chars: int = _SNIPPET_MAX_CHARS,
|
|
139
|
+
include_history: bool = True,
|
|
140
|
+
include_tool_outputs: bool = True,
|
|
141
|
+
include_memory: bool = True,
|
|
142
|
+
) -> dict[str, Any]:
|
|
143
|
+
root_abs = root.resolve()
|
|
144
|
+
|
|
145
|
+
max_results = max(1, min(int(max_results), _MAX_RESULTS))
|
|
146
|
+
max_file_bytes = max(1, min(int(max_file_bytes), _MAX_FILE_BYTES))
|
|
147
|
+
max_total_bytes = max(1, min(int(max_total_bytes), _MAX_TOTAL_BYTES))
|
|
148
|
+
max_snippet_chars = max(1, min(int(max_snippet_chars), _MAX_SNIPPET_CHARS))
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
compiled = re.compile(pattern)
|
|
152
|
+
except re.error as exc:
|
|
153
|
+
raise HistorySearchError(f"Invalid regex pattern: {exc}") from exc
|
|
154
|
+
|
|
155
|
+
matches: list[dict[str, Any]] = []
|
|
156
|
+
truncated = False
|
|
157
|
+
result_limit_reached = False
|
|
158
|
+
scanned_bytes = 0
|
|
159
|
+
scanned_files = 0
|
|
160
|
+
|
|
161
|
+
search_roots = _search_roots(
|
|
162
|
+
root=root_abs,
|
|
163
|
+
session_id=session_id,
|
|
164
|
+
session_artifact_root=session_artifact_root,
|
|
165
|
+
)
|
|
166
|
+
if not any(search_root.path.exists() for search_root in search_roots):
|
|
167
|
+
return {
|
|
168
|
+
"pattern": str(redact_secrets(pattern)),
|
|
169
|
+
"matches": matches,
|
|
170
|
+
"truncated": truncated,
|
|
171
|
+
"scanned_bytes": scanned_bytes,
|
|
172
|
+
"scanned_files": scanned_files,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
targets, target_list_truncated = _iter_target_files(
|
|
176
|
+
roots=search_roots,
|
|
177
|
+
include_history=include_history,
|
|
178
|
+
include_tool_outputs=include_tool_outputs,
|
|
179
|
+
include_memory=include_memory,
|
|
180
|
+
)
|
|
181
|
+
truncated = target_list_truncated
|
|
182
|
+
|
|
183
|
+
for kind, file_path, layout in targets:
|
|
184
|
+
remaining_bytes = max_total_bytes - scanned_bytes
|
|
185
|
+
if remaining_bytes <= 0:
|
|
186
|
+
truncated = True
|
|
187
|
+
break
|
|
188
|
+
read_limit = min(max_file_bytes, remaining_bytes)
|
|
189
|
+
try:
|
|
190
|
+
with file_path.open("rb") as fh:
|
|
191
|
+
data = fh.read(read_limit + 1)
|
|
192
|
+
except OSError:
|
|
193
|
+
continue
|
|
194
|
+
scanned_files += 1
|
|
195
|
+
if len(data) > read_limit:
|
|
196
|
+
data = data[:read_limit]
|
|
197
|
+
truncated = True
|
|
198
|
+
scanned_bytes += len(data)
|
|
199
|
+
text = data.decode("utf-8", errors="replace")
|
|
200
|
+
|
|
201
|
+
for line_no, line in enumerate(text.splitlines(), start=1):
|
|
202
|
+
if not compiled.search(line):
|
|
203
|
+
continue
|
|
204
|
+
matches.append(
|
|
205
|
+
{
|
|
206
|
+
"kind": kind,
|
|
207
|
+
"path": _display_artifact_path(
|
|
208
|
+
file_path=file_path,
|
|
209
|
+
workspace_root=root_abs,
|
|
210
|
+
layout=layout,
|
|
211
|
+
),
|
|
212
|
+
"line": line_no,
|
|
213
|
+
"text": str(redact_secrets(_clip_text(line, max_snippet_chars))),
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
if len(matches) >= max_results:
|
|
217
|
+
truncated = True
|
|
218
|
+
result_limit_reached = True
|
|
219
|
+
break
|
|
220
|
+
if result_limit_reached:
|
|
221
|
+
break
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
"pattern": str(redact_secrets(pattern)),
|
|
225
|
+
"matches": matches,
|
|
226
|
+
"truncated": truncated,
|
|
227
|
+
"scanned_bytes": scanned_bytes,
|
|
228
|
+
"scanned_files": scanned_files,
|
|
229
|
+
}
|