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,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..atomic_io import atomic_write_json
|
|
8
|
+
from .models import ExtensionState, ProjectExtensionOverrides, normalize_extension_id
|
|
9
|
+
from .paths import extensions_state_path, project_extensions_path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _load_json_object(path: Path) -> dict[str, Any] | None:
|
|
13
|
+
if not path.exists():
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
18
|
+
except json.JSONDecodeError as e:
|
|
19
|
+
raise RuntimeError(f"Invalid JSON in {path}") from e
|
|
20
|
+
except OSError as e:
|
|
21
|
+
raise RuntimeError(f"Failed to read {path}") from e
|
|
22
|
+
|
|
23
|
+
if not isinstance(raw, dict):
|
|
24
|
+
raise RuntimeError(f"Invalid JSON format in {path}: expected object.")
|
|
25
|
+
return raw
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_global_state() -> ExtensionState:
|
|
29
|
+
raw = _load_json_object(extensions_state_path())
|
|
30
|
+
if raw is None:
|
|
31
|
+
return ExtensionState()
|
|
32
|
+
return ExtensionState.model_validate(raw)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def save_global_state(state: ExtensionState) -> None:
|
|
36
|
+
atomic_write_json(extensions_state_path(), state.model_dump(mode="json"))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_project_state(repo_root: Path) -> ExtensionState:
|
|
40
|
+
raw = _load_json_object(project_extensions_path(repo_root))
|
|
41
|
+
if raw is None:
|
|
42
|
+
return ExtensionState()
|
|
43
|
+
return ExtensionState.model_validate(raw)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def save_project_state(repo_root: Path, state: ExtensionState) -> None:
|
|
47
|
+
atomic_write_json(project_extensions_path(repo_root), state.model_dump(mode="json"))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def load_project_overrides(repo_root: Path) -> ProjectExtensionOverrides:
|
|
51
|
+
raw = _load_json_object(project_extensions_path(repo_root))
|
|
52
|
+
if raw is None:
|
|
53
|
+
return ProjectExtensionOverrides()
|
|
54
|
+
return ProjectExtensionOverrides.model_validate(raw)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def compute_effective_enabled(
|
|
58
|
+
global_state: ExtensionState,
|
|
59
|
+
project_overrides: ProjectExtensionOverrides,
|
|
60
|
+
) -> set[str]:
|
|
61
|
+
enabled: set[str] = {
|
|
62
|
+
normalize_extension_id(ext_id)
|
|
63
|
+
for ext_id in global_state.enabled
|
|
64
|
+
if normalize_extension_id(ext_id)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for ext_id, installed in global_state.installed.items():
|
|
68
|
+
if installed.enabled:
|
|
69
|
+
normalized = normalize_extension_id(ext_id)
|
|
70
|
+
if normalized:
|
|
71
|
+
enabled.add(normalized)
|
|
72
|
+
|
|
73
|
+
for ext_id in project_overrides.disabled:
|
|
74
|
+
normalized = normalize_extension_id(ext_id)
|
|
75
|
+
if normalized:
|
|
76
|
+
enabled.discard(normalized)
|
|
77
|
+
|
|
78
|
+
for ext_id in project_overrides.enabled:
|
|
79
|
+
normalized = normalize_extension_id(ext_id)
|
|
80
|
+
if normalized:
|
|
81
|
+
enabled.add(normalized)
|
|
82
|
+
|
|
83
|
+
return enabled
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
10
|
+
|
|
11
|
+
from ..atomic_io import atomic_write_json
|
|
12
|
+
from .paths import workspace_trust_path as _workspace_trust_path
|
|
13
|
+
|
|
14
|
+
WORKSPACE_TRUST_SCHEMA_VERSION = 1
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WorkspaceTrustEntry(BaseModel):
|
|
18
|
+
model_config = ConfigDict(extra="forbid")
|
|
19
|
+
|
|
20
|
+
workspace_root: str
|
|
21
|
+
granted_at: str
|
|
22
|
+
overrides_sha256: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class WorkspaceTrustState(BaseModel):
|
|
26
|
+
model_config = ConfigDict(extra="forbid")
|
|
27
|
+
|
|
28
|
+
schema_version: int = WORKSPACE_TRUST_SCHEMA_VERSION
|
|
29
|
+
trusted: dict[str, WorkspaceTrustEntry] = Field(default_factory=dict)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def workspace_trust_path(*, user_config_dir: Path | None = None) -> Path:
|
|
33
|
+
return _workspace_trust_path(user_config_dir=user_config_dir)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_workspace_trust(*, user_config_dir: Path | None = None) -> WorkspaceTrustState:
|
|
37
|
+
path = workspace_trust_path(user_config_dir=user_config_dir)
|
|
38
|
+
if not path.exists():
|
|
39
|
+
return WorkspaceTrustState()
|
|
40
|
+
try:
|
|
41
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
42
|
+
except json.JSONDecodeError as exc:
|
|
43
|
+
raise RuntimeError(f"Invalid JSON in {path}") from exc
|
|
44
|
+
except OSError as exc:
|
|
45
|
+
raise RuntimeError(f"Failed to read {path}") from exc
|
|
46
|
+
if not isinstance(raw, dict):
|
|
47
|
+
raise RuntimeError(f"Invalid JSON format in {path}: expected object.")
|
|
48
|
+
return WorkspaceTrustState.model_validate(raw)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _canonical_workspace_root(repo_root: Path) -> str:
|
|
52
|
+
resolved = repo_root.expanduser().resolve(strict=False)
|
|
53
|
+
text = os.fspath(resolved)
|
|
54
|
+
if os.name == "nt":
|
|
55
|
+
drive, tail = os.path.splitdrive(text)
|
|
56
|
+
text = drive.lower() + tail
|
|
57
|
+
return text.rstrip("\\/") or text
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def workspace_trust_key(repo_root: Path) -> str:
|
|
61
|
+
"""SHA-256 of the canonical absolute path."""
|
|
62
|
+
canonical = _canonical_workspace_root(repo_root)
|
|
63
|
+
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def is_workspace_trusted(
|
|
67
|
+
*,
|
|
68
|
+
repo_root: Path,
|
|
69
|
+
overrides_sha256: str,
|
|
70
|
+
user_config_dir: Path | None = None,
|
|
71
|
+
) -> bool:
|
|
72
|
+
"""Return True iff this repo and overrides-file hash were explicitly trusted."""
|
|
73
|
+
state = load_workspace_trust(user_config_dir=user_config_dir)
|
|
74
|
+
entry = state.trusted.get(workspace_trust_key(repo_root))
|
|
75
|
+
return entry is not None and entry.overrides_sha256 == overrides_sha256
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def grant_workspace_trust(
|
|
79
|
+
*,
|
|
80
|
+
repo_root: Path,
|
|
81
|
+
overrides_sha256: str,
|
|
82
|
+
user_config_dir: Path | None = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Write a workspace trust entry atomically."""
|
|
85
|
+
state = load_workspace_trust(user_config_dir=user_config_dir)
|
|
86
|
+
key = workspace_trust_key(repo_root)
|
|
87
|
+
updated = WorkspaceTrustState(
|
|
88
|
+
schema_version=WORKSPACE_TRUST_SCHEMA_VERSION,
|
|
89
|
+
trusted={
|
|
90
|
+
**state.trusted,
|
|
91
|
+
key: WorkspaceTrustEntry(
|
|
92
|
+
workspace_root=_canonical_workspace_root(repo_root),
|
|
93
|
+
granted_at=datetime.now(UTC).replace(microsecond=0).isoformat(),
|
|
94
|
+
overrides_sha256=overrides_sha256,
|
|
95
|
+
),
|
|
96
|
+
},
|
|
97
|
+
)
|
|
98
|
+
atomic_write_json(
|
|
99
|
+
workspace_trust_path(user_config_dir=user_config_dir),
|
|
100
|
+
updated.model_dump(mode="json"),
|
|
101
|
+
)
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""Capture a failed task's work before its workspace is cleaned up.
|
|
2
|
+
|
|
3
|
+
Swarm failure cleanup removes the task worktree and branch so the next run is not
|
|
4
|
+
blocked by leftover state. That is correct housekeeping and wrong data handling: the
|
|
5
|
+
worktree is the only place some of the work exists. An uncommitted edit, or a new
|
|
6
|
+
file the worker never got to commit, disappeared with the directory.
|
|
7
|
+
|
|
8
|
+
So every cleanup path first calls :func:`preserve_failed_task_evidence`, which
|
|
9
|
+
writes the task's full diff (committed *and* uncommitted, plus untracked file
|
|
10
|
+
contents) and its verification log into ``<run>/execution/evidence/<task>/``. That
|
|
11
|
+
directory lives in the run, not in the worktree, so cleanup can then proceed exactly
|
|
12
|
+
as configured.
|
|
13
|
+
|
|
14
|
+
Capture is best effort by construction: it must never raise, never mutate the
|
|
15
|
+
workspace it is reading (no ``git add``, no index writes), and never be the reason a
|
|
16
|
+
cleanup is skipped. Whatever it could not capture is recorded in ``errors`` and in
|
|
17
|
+
``evidence.json`` rather than silently dropped.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import subprocess
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from datetime import UTC, datetime
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from .execution_shared import safe_task_file_component
|
|
31
|
+
from .git_safe import build_git_cmd
|
|
32
|
+
|
|
33
|
+
EVIDENCE_DIR_NAME = "evidence"
|
|
34
|
+
PATCH_FILE_NAME = "patch.diff"
|
|
35
|
+
VERIFICATION_LOG_FILE_NAME = "verification.log"
|
|
36
|
+
METADATA_FILE_NAME = "evidence.json"
|
|
37
|
+
|
|
38
|
+
# Untracked files are the work most at risk (a new module the worker never
|
|
39
|
+
# committed), so their contents are captured, but a stray build directory must not
|
|
40
|
+
# turn evidence capture into a disk-filling copy.
|
|
41
|
+
MAX_UNTRACKED_FILES = 200
|
|
42
|
+
MAX_UNTRACKED_FILE_BYTES = 256 * 1024
|
|
43
|
+
MAX_UNTRACKED_TOTAL_BYTES = 8 * 1024 * 1024
|
|
44
|
+
_GIT_TIMEOUT_S = 120
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class PreservedTaskEvidence:
|
|
49
|
+
"""Where the evidence landed, and what could not be captured."""
|
|
50
|
+
|
|
51
|
+
task_id: str
|
|
52
|
+
directory: Path | None
|
|
53
|
+
patch_path: Path | None = None
|
|
54
|
+
verification_log_path: Path | None = None
|
|
55
|
+
metadata_path: Path | None = None
|
|
56
|
+
captured_untracked_files: tuple[str, ...] = ()
|
|
57
|
+
truncated: bool = False
|
|
58
|
+
errors: tuple[str, ...] = ()
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def captured(self) -> bool:
|
|
62
|
+
return self.patch_path is not None or self.verification_log_path is not None
|
|
63
|
+
|
|
64
|
+
def to_payload(self) -> dict[str, object]:
|
|
65
|
+
return {
|
|
66
|
+
"task_id": self.task_id,
|
|
67
|
+
"directory": os.fspath(self.directory) if self.directory else None,
|
|
68
|
+
"patch_path": os.fspath(self.patch_path) if self.patch_path else None,
|
|
69
|
+
"verification_log_path": (
|
|
70
|
+
os.fspath(self.verification_log_path) if self.verification_log_path else None
|
|
71
|
+
),
|
|
72
|
+
"captured_untracked_files": list(self.captured_untracked_files),
|
|
73
|
+
"truncated": bool(self.truncated),
|
|
74
|
+
"errors": list(self.errors),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
GitRunner = Callable[[Path, list[str]], "subprocess.CompletedProcess[str]"]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _default_git_runner(root: Path, args: list[str]) -> subprocess.CompletedProcess[str]:
|
|
82
|
+
return subprocess.run(
|
|
83
|
+
build_git_cmd(root, args),
|
|
84
|
+
check=False,
|
|
85
|
+
capture_output=True,
|
|
86
|
+
text=True,
|
|
87
|
+
timeout=_GIT_TIMEOUT_S,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def evidence_dir_for(execution_dir: Path, task_id: str) -> Path:
|
|
92
|
+
return Path(execution_dir) / EVIDENCE_DIR_NAME / safe_task_file_component(task_id)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def preserve_failed_task_evidence(
|
|
96
|
+
*,
|
|
97
|
+
execution_dir: Path,
|
|
98
|
+
task_id: str,
|
|
99
|
+
worktree_path: Path | None,
|
|
100
|
+
base_branch: str | None = None,
|
|
101
|
+
branch: str | None = None,
|
|
102
|
+
reason: str = "",
|
|
103
|
+
verification_log_path: Path | None = None,
|
|
104
|
+
verification_summary: str | None = None,
|
|
105
|
+
extra: dict[str, object] | None = None,
|
|
106
|
+
git_runner: GitRunner | None = None,
|
|
107
|
+
) -> PreservedTaskEvidence:
|
|
108
|
+
"""Write a failed task's diff and verification log into the run's artifacts.
|
|
109
|
+
|
|
110
|
+
Returns what was captured. Never raises: a capture failure is reported through
|
|
111
|
+
:attr:`PreservedTaskEvidence.errors` so the caller can warn and still clean up.
|
|
112
|
+
"""
|
|
113
|
+
errors: list[str] = []
|
|
114
|
+
directory: Path | None = None
|
|
115
|
+
try:
|
|
116
|
+
directory = evidence_dir_for(execution_dir, task_id)
|
|
117
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
118
|
+
except OSError as e:
|
|
119
|
+
return PreservedTaskEvidence(
|
|
120
|
+
task_id=str(task_id),
|
|
121
|
+
directory=None,
|
|
122
|
+
errors=(f"could not create evidence directory: {e}",),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
patch_path: Path | None = None
|
|
126
|
+
captured_untracked: tuple[str, ...] = ()
|
|
127
|
+
truncated = False
|
|
128
|
+
runner = git_runner or _default_git_runner
|
|
129
|
+
resolved_worktree = Path(worktree_path) if worktree_path is not None else None
|
|
130
|
+
|
|
131
|
+
if resolved_worktree is not None and resolved_worktree.exists():
|
|
132
|
+
capture = _capture_worktree_patch(
|
|
133
|
+
worktree_path=resolved_worktree,
|
|
134
|
+
base_branch=base_branch,
|
|
135
|
+
runner=runner,
|
|
136
|
+
)
|
|
137
|
+
errors.extend(capture.errors)
|
|
138
|
+
captured_untracked = capture.untracked_files
|
|
139
|
+
truncated = capture.truncated
|
|
140
|
+
try:
|
|
141
|
+
target = directory / PATCH_FILE_NAME
|
|
142
|
+
target.write_text(capture.text, encoding="utf-8")
|
|
143
|
+
patch_path = target
|
|
144
|
+
except OSError as e:
|
|
145
|
+
errors.append(f"could not write evidence patch: {e}")
|
|
146
|
+
elif resolved_worktree is None:
|
|
147
|
+
errors.append("no task workspace path was available to capture a diff from")
|
|
148
|
+
else:
|
|
149
|
+
errors.append(f"task workspace no longer exists: {os.fspath(resolved_worktree)}")
|
|
150
|
+
|
|
151
|
+
stored_log: Path | None = None
|
|
152
|
+
try:
|
|
153
|
+
stored_log = _write_verification_log(
|
|
154
|
+
directory=directory,
|
|
155
|
+
source=verification_log_path,
|
|
156
|
+
summary=verification_summary,
|
|
157
|
+
)
|
|
158
|
+
except OSError as e:
|
|
159
|
+
errors.append(f"could not write verification log: {e}")
|
|
160
|
+
|
|
161
|
+
metadata_path: Path | None = None
|
|
162
|
+
payload: dict[str, object] = {
|
|
163
|
+
"schema_version": 1,
|
|
164
|
+
"task_id": str(task_id),
|
|
165
|
+
"captured_at": datetime.now(UTC).isoformat(),
|
|
166
|
+
"reason": str(reason or ""),
|
|
167
|
+
"branch": str(branch or ""),
|
|
168
|
+
"base_branch": str(base_branch or ""),
|
|
169
|
+
"worktree_path": os.fspath(resolved_worktree) if resolved_worktree else None,
|
|
170
|
+
"patch_file": PATCH_FILE_NAME if patch_path is not None else None,
|
|
171
|
+
"verification_log_file": (VERIFICATION_LOG_FILE_NAME if stored_log is not None else None),
|
|
172
|
+
"verification_summary": verification_summary,
|
|
173
|
+
"captured_untracked_files": list(captured_untracked),
|
|
174
|
+
"truncated": bool(truncated),
|
|
175
|
+
"errors": list(errors),
|
|
176
|
+
}
|
|
177
|
+
if extra:
|
|
178
|
+
payload["extra"] = dict(extra)
|
|
179
|
+
try:
|
|
180
|
+
metadata_path = directory / METADATA_FILE_NAME
|
|
181
|
+
metadata_path.write_text(
|
|
182
|
+
json.dumps(payload, indent=2, sort_keys=True, default=str) + "\n",
|
|
183
|
+
encoding="utf-8",
|
|
184
|
+
)
|
|
185
|
+
except OSError as e:
|
|
186
|
+
metadata_path = None
|
|
187
|
+
errors.append(f"could not write evidence metadata: {e}")
|
|
188
|
+
|
|
189
|
+
return PreservedTaskEvidence(
|
|
190
|
+
task_id=str(task_id),
|
|
191
|
+
directory=directory,
|
|
192
|
+
patch_path=patch_path,
|
|
193
|
+
verification_log_path=stored_log,
|
|
194
|
+
metadata_path=metadata_path,
|
|
195
|
+
captured_untracked_files=captured_untracked,
|
|
196
|
+
truncated=truncated,
|
|
197
|
+
errors=tuple(errors),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@dataclass
|
|
202
|
+
class _PatchCapture:
|
|
203
|
+
text: str
|
|
204
|
+
untracked_files: tuple[str, ...] = ()
|
|
205
|
+
truncated: bool = False
|
|
206
|
+
errors: list[str] = field(default_factory=list)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _capture_worktree_patch(
|
|
210
|
+
*,
|
|
211
|
+
worktree_path: Path,
|
|
212
|
+
base_branch: str | None,
|
|
213
|
+
runner: GitRunner,
|
|
214
|
+
) -> _PatchCapture:
|
|
215
|
+
"""Build one text artifact holding everything the worktree still has.
|
|
216
|
+
|
|
217
|
+
``git diff <base>`` is used rather than ``format-patch``: it compares the working
|
|
218
|
+
tree against the base, so committed and uncommitted changes are both in the
|
|
219
|
+
output without staging anything.
|
|
220
|
+
"""
|
|
221
|
+
sections: list[str] = [
|
|
222
|
+
"# Preserved Task Evidence",
|
|
223
|
+
f"# worktree: {os.fspath(worktree_path)}",
|
|
224
|
+
f"# base branch: {base_branch or '(unknown)'}",
|
|
225
|
+
"",
|
|
226
|
+
]
|
|
227
|
+
errors: list[str] = []
|
|
228
|
+
|
|
229
|
+
status_ok, status_text = _git_text(runner, worktree_path, ["status", "--porcelain"])
|
|
230
|
+
if status_ok:
|
|
231
|
+
sections.extend(
|
|
232
|
+
["## git status --porcelain", "", status_text.rstrip("\n") or "(clean)", ""]
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
errors.append(f"git status failed: {status_text.strip() or 'unknown error'}")
|
|
236
|
+
|
|
237
|
+
if base_branch:
|
|
238
|
+
diff_ok, diff_text = _git_text(runner, worktree_path, ["diff", str(base_branch)])
|
|
239
|
+
label = f"## git diff {base_branch} (tracked changes vs base, committed and uncommitted)"
|
|
240
|
+
if diff_ok:
|
|
241
|
+
sections.extend([label, "", diff_text.rstrip("\n") or "(no tracked changes)", ""])
|
|
242
|
+
else:
|
|
243
|
+
errors.append(f"git diff against {base_branch} failed: {diff_text.strip()}")
|
|
244
|
+
|
|
245
|
+
# Always capture the working-tree delta too: when the base ref is unknown or the
|
|
246
|
+
# diff above failed, this is the only record of uncommitted work.
|
|
247
|
+
head_ok, head_text = _git_text(runner, worktree_path, ["diff", "HEAD"])
|
|
248
|
+
if head_ok:
|
|
249
|
+
sections.extend(
|
|
250
|
+
[
|
|
251
|
+
"## git diff HEAD (uncommitted tracked changes)",
|
|
252
|
+
"",
|
|
253
|
+
head_text.rstrip("\n") or "(none)",
|
|
254
|
+
"",
|
|
255
|
+
]
|
|
256
|
+
)
|
|
257
|
+
else:
|
|
258
|
+
errors.append(f"git diff HEAD failed: {head_text.strip() or 'unknown error'}")
|
|
259
|
+
|
|
260
|
+
untracked_ok, untracked_text = _git_text(
|
|
261
|
+
runner,
|
|
262
|
+
worktree_path,
|
|
263
|
+
["ls-files", "--others", "--exclude-standard"],
|
|
264
|
+
)
|
|
265
|
+
captured: list[str] = []
|
|
266
|
+
truncated = False
|
|
267
|
+
if untracked_ok:
|
|
268
|
+
candidates = [line.strip() for line in untracked_text.splitlines() if line.strip()]
|
|
269
|
+
if len(candidates) > MAX_UNTRACKED_FILES:
|
|
270
|
+
truncated = True
|
|
271
|
+
sections.append(
|
|
272
|
+
f"## NOTE: {len(candidates)} untracked files found; "
|
|
273
|
+
f"capturing the first {MAX_UNTRACKED_FILES}."
|
|
274
|
+
)
|
|
275
|
+
sections.append("")
|
|
276
|
+
candidates = candidates[:MAX_UNTRACKED_FILES]
|
|
277
|
+
total_bytes = 0
|
|
278
|
+
for relative in candidates:
|
|
279
|
+
path = worktree_path / relative
|
|
280
|
+
try:
|
|
281
|
+
if not path.is_file():
|
|
282
|
+
continue
|
|
283
|
+
size = path.stat().st_size
|
|
284
|
+
except OSError as e:
|
|
285
|
+
errors.append(f"could not stat untracked file {relative}: {e}")
|
|
286
|
+
continue
|
|
287
|
+
if size > MAX_UNTRACKED_FILE_BYTES or total_bytes + size > MAX_UNTRACKED_TOTAL_BYTES:
|
|
288
|
+
truncated = True
|
|
289
|
+
sections.extend(
|
|
290
|
+
[f"## untracked file: {relative}", "", f"(skipped: {size} bytes)", ""]
|
|
291
|
+
)
|
|
292
|
+
continue
|
|
293
|
+
try:
|
|
294
|
+
content = path.read_text(encoding="utf-8", errors="replace")
|
|
295
|
+
except OSError as e:
|
|
296
|
+
errors.append(f"could not read untracked file {relative}: {e}")
|
|
297
|
+
continue
|
|
298
|
+
total_bytes += size
|
|
299
|
+
captured.append(relative)
|
|
300
|
+
sections.extend([f"## untracked file: {relative}", "", content.rstrip("\n"), ""])
|
|
301
|
+
else:
|
|
302
|
+
errors.append(f"git ls-files failed: {untracked_text.strip() or 'unknown error'}")
|
|
303
|
+
|
|
304
|
+
if errors:
|
|
305
|
+
sections.extend(["## capture warnings", "", *[f"- {item}" for item in errors], ""])
|
|
306
|
+
|
|
307
|
+
return _PatchCapture(
|
|
308
|
+
text="\n".join(sections) + "\n",
|
|
309
|
+
untracked_files=tuple(captured),
|
|
310
|
+
truncated=truncated,
|
|
311
|
+
errors=errors,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _git_text(runner: GitRunner, root: Path, args: list[str]) -> tuple[bool, str]:
|
|
316
|
+
try:
|
|
317
|
+
cp = runner(root, args)
|
|
318
|
+
except (OSError, subprocess.SubprocessError) as e:
|
|
319
|
+
return False, str(e)
|
|
320
|
+
stdout = cp.stdout or ""
|
|
321
|
+
if cp.returncode != 0:
|
|
322
|
+
return False, (cp.stderr or stdout or "").strip()
|
|
323
|
+
return True, stdout
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _write_verification_log(
|
|
327
|
+
*,
|
|
328
|
+
directory: Path,
|
|
329
|
+
source: Path | None,
|
|
330
|
+
summary: str | None,
|
|
331
|
+
) -> Path | None:
|
|
332
|
+
target = directory / VERIFICATION_LOG_FILE_NAME
|
|
333
|
+
if source is not None:
|
|
334
|
+
resolved = Path(source)
|
|
335
|
+
if resolved.exists():
|
|
336
|
+
try:
|
|
337
|
+
text = resolved.read_text(encoding="utf-8", errors="replace")
|
|
338
|
+
except OSError as e:
|
|
339
|
+
text = f"(could not read verification artifact {os.fspath(resolved)}: {e})\n"
|
|
340
|
+
header = f"# Verification log copied from {os.fspath(resolved)}\n\n"
|
|
341
|
+
target.write_text(header + text, encoding="utf-8")
|
|
342
|
+
return target
|
|
343
|
+
if summary and str(summary).strip():
|
|
344
|
+
target.write_text(
|
|
345
|
+
"# Verification log\n\n"
|
|
346
|
+
"No verification artifact file was produced for this task.\n\n"
|
|
347
|
+
f"Summary: {summary}\n",
|
|
348
|
+
encoding="utf-8",
|
|
349
|
+
)
|
|
350
|
+
return target
|
|
351
|
+
target.write_text(
|
|
352
|
+
"# Verification log\n\nVerification produced no artifact and no summary for this task.\n",
|
|
353
|
+
encoding="utf-8",
|
|
354
|
+
)
|
|
355
|
+
return target
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
__all__ = [
|
|
359
|
+
"EVIDENCE_DIR_NAME",
|
|
360
|
+
"MAX_UNTRACKED_FILES",
|
|
361
|
+
"MAX_UNTRACKED_FILE_BYTES",
|
|
362
|
+
"MAX_UNTRACKED_TOTAL_BYTES",
|
|
363
|
+
"METADATA_FILE_NAME",
|
|
364
|
+
"PATCH_FILE_NAME",
|
|
365
|
+
"VERIFICATION_LOG_FILE_NAME",
|
|
366
|
+
"PreservedTaskEvidence",
|
|
367
|
+
"evidence_dir_for",
|
|
368
|
+
"preserve_failed_task_evidence",
|
|
369
|
+
]
|