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,1190 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import warnings
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .forge import RunPaths
|
|
11
|
+
from .knowledge_base import write_decision_entry, write_fact_entry
|
|
12
|
+
from .surface.base import Surface
|
|
13
|
+
from .surface.noop_surface import NoopSurface
|
|
14
|
+
|
|
15
|
+
KNOWLEDGE_CAPTURE_SCHEMA_VERSION = 1
|
|
16
|
+
KNOWLEDGE_CAPTURE_FENCE = "knowledge_capture_json"
|
|
17
|
+
_CAPTURE_BLOCK_RE = re.compile(
|
|
18
|
+
rf"```{KNOWLEDGE_CAPTURE_FENCE}\s*(\{{.*?\}})\s*```",
|
|
19
|
+
re.DOTALL,
|
|
20
|
+
)
|
|
21
|
+
_ALLOWED_TOP_LEVEL_KEYS = frozenset({"schema_version", "facts", "decisions", "open_questions"})
|
|
22
|
+
_ALLOWED_FACT_KEYS = frozenset({"title", "summary", "paths", "tags"})
|
|
23
|
+
_ALLOWED_DECISION_KEYS = frozenset({"decision_key", "title", "summary", "status", "paths", "tags"})
|
|
24
|
+
_DECISION_STATUSES = frozenset({"active", "invalidated"})
|
|
25
|
+
_MAX_FACTS = 5
|
|
26
|
+
_MAX_DECISIONS = 4
|
|
27
|
+
_MAX_OPEN_QUESTIONS = 4
|
|
28
|
+
_MAX_TITLE_CHARS = 120
|
|
29
|
+
_MAX_SUMMARY_CHARS = 400
|
|
30
|
+
_MAX_PATHS = 8
|
|
31
|
+
_MAX_TAGS = 6
|
|
32
|
+
_MAX_TAG_CHARS = 32
|
|
33
|
+
_MAX_OPEN_QUESTION_CHARS = 160
|
|
34
|
+
_MAX_DECISION_KEY_CHARS = 64
|
|
35
|
+
_TAG_RE = re.compile(r"^[a-z0-9][a-z0-9._-]*$")
|
|
36
|
+
_DECISION_KEY_RE = re.compile(r"^[a-z0-9][a-z0-9._-]*$")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _safe_component(value: str) -> str:
|
|
40
|
+
safe = "".join(c if c.isalnum() or c in {"-", "_"} else "_" for c in str(value).strip())
|
|
41
|
+
return safe or "item"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _repo_rel(root: Path, path: Path) -> str:
|
|
45
|
+
return path.resolve().relative_to(root.resolve()).as_posix()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _normalize_repo_rel_path(value: str) -> str | None:
|
|
49
|
+
cleaned = str(value).strip().replace("\\", "/")
|
|
50
|
+
while cleaned.startswith("./"):
|
|
51
|
+
cleaned = cleaned[2:]
|
|
52
|
+
cleaned = cleaned.strip().rstrip("/")
|
|
53
|
+
if not cleaned:
|
|
54
|
+
return None
|
|
55
|
+
if cleaned.startswith("/"):
|
|
56
|
+
return None
|
|
57
|
+
if cleaned == ".." or cleaned.startswith("../") or "/../" in cleaned:
|
|
58
|
+
return None
|
|
59
|
+
if cleaned == ".alysis" or cleaned.startswith(".alysis/"):
|
|
60
|
+
return None
|
|
61
|
+
return cleaned
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _bounded_text(value: Any, *, field: str, max_chars: int) -> str:
|
|
65
|
+
text = str(value or "").strip()
|
|
66
|
+
if not text:
|
|
67
|
+
raise ValueError(f"{field} must be non-empty")
|
|
68
|
+
if len(text) > max_chars:
|
|
69
|
+
raise ValueError(f"{field} exceeds {max_chars} chars")
|
|
70
|
+
return text
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _normalize_tags(value: Any) -> tuple[str, ...]:
|
|
74
|
+
if value is None:
|
|
75
|
+
return ()
|
|
76
|
+
if not isinstance(value, list):
|
|
77
|
+
raise ValueError("tags must be an array")
|
|
78
|
+
tags: list[str] = []
|
|
79
|
+
for raw in value[:_MAX_TAGS]:
|
|
80
|
+
tag = str(raw or "").strip().lower()
|
|
81
|
+
if not tag:
|
|
82
|
+
continue
|
|
83
|
+
if len(tag) > _MAX_TAG_CHARS:
|
|
84
|
+
raise ValueError(f"tag exceeds {_MAX_TAG_CHARS} chars: {tag}")
|
|
85
|
+
if not _TAG_RE.match(tag):
|
|
86
|
+
raise ValueError(f"invalid tag: {tag}")
|
|
87
|
+
if tag not in tags:
|
|
88
|
+
tags.append(tag)
|
|
89
|
+
return tuple(tags)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _normalize_paths(value: Any) -> tuple[str, ...]:
|
|
93
|
+
if value is None:
|
|
94
|
+
return ()
|
|
95
|
+
if not isinstance(value, list):
|
|
96
|
+
raise ValueError("paths must be an array")
|
|
97
|
+
paths: list[str] = []
|
|
98
|
+
for raw in value[:_MAX_PATHS]:
|
|
99
|
+
normalized = _normalize_repo_rel_path(str(raw or ""))
|
|
100
|
+
if normalized is None:
|
|
101
|
+
raise ValueError(f"invalid repo-relative path: {raw}")
|
|
102
|
+
if normalized not in paths:
|
|
103
|
+
paths.append(normalized)
|
|
104
|
+
return tuple(paths)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _normalize_open_questions(value: Any) -> tuple[str, ...]:
|
|
108
|
+
if value is None:
|
|
109
|
+
return ()
|
|
110
|
+
if not isinstance(value, list):
|
|
111
|
+
raise ValueError("open_questions must be an array")
|
|
112
|
+
out: list[str] = []
|
|
113
|
+
for raw in value[:_MAX_OPEN_QUESTIONS]:
|
|
114
|
+
text = str(raw or "").strip()
|
|
115
|
+
if not text:
|
|
116
|
+
continue
|
|
117
|
+
if len(text) > _MAX_OPEN_QUESTION_CHARS:
|
|
118
|
+
raise ValueError(f"open question exceeds {_MAX_OPEN_QUESTION_CHARS} chars: {text[:40]}")
|
|
119
|
+
out.append(text)
|
|
120
|
+
return tuple(out)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _emit_surface_warning(
|
|
124
|
+
surface: object,
|
|
125
|
+
message: str,
|
|
126
|
+
*,
|
|
127
|
+
worker_id: str | None = None,
|
|
128
|
+
role: str | None = None,
|
|
129
|
+
) -> bool:
|
|
130
|
+
surface_cls = getattr(surface, "__class__", None)
|
|
131
|
+
handler = getattr(surface, "emit_warning", None)
|
|
132
|
+
if callable(handler):
|
|
133
|
+
cls_handler = getattr(surface_cls, "emit_warning", None)
|
|
134
|
+
if cls_handler is not getattr(NoopSurface, "emit_warning", None):
|
|
135
|
+
handler(message, worker_id=worker_id, role=role)
|
|
136
|
+
return True
|
|
137
|
+
handler = getattr(surface, "on_warning", None)
|
|
138
|
+
if not callable(handler):
|
|
139
|
+
return False
|
|
140
|
+
cls_handler = getattr(surface_cls, "on_warning", None)
|
|
141
|
+
if cls_handler is getattr(NoopSurface, "on_warning", None):
|
|
142
|
+
return False
|
|
143
|
+
handler(message)
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass(frozen=True)
|
|
148
|
+
class CapturedFact:
|
|
149
|
+
title: str
|
|
150
|
+
summary: str
|
|
151
|
+
paths: tuple[str, ...]
|
|
152
|
+
tags: tuple[str, ...]
|
|
153
|
+
|
|
154
|
+
def to_payload(self) -> dict[str, Any]:
|
|
155
|
+
return {
|
|
156
|
+
"title": self.title,
|
|
157
|
+
"summary": self.summary,
|
|
158
|
+
"paths": list(self.paths),
|
|
159
|
+
"tags": list(self.tags),
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass(frozen=True)
|
|
164
|
+
class CapturedDecision:
|
|
165
|
+
decision_key: str
|
|
166
|
+
title: str
|
|
167
|
+
summary: str
|
|
168
|
+
status: str
|
|
169
|
+
paths: tuple[str, ...]
|
|
170
|
+
tags: tuple[str, ...]
|
|
171
|
+
|
|
172
|
+
def to_payload(self) -> dict[str, Any]:
|
|
173
|
+
return {
|
|
174
|
+
"decision_key": self.decision_key,
|
|
175
|
+
"title": self.title,
|
|
176
|
+
"summary": self.summary,
|
|
177
|
+
"status": self.status,
|
|
178
|
+
"paths": list(self.paths),
|
|
179
|
+
"tags": list(self.tags),
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass(frozen=True)
|
|
184
|
+
class KnowledgeCapturePayload:
|
|
185
|
+
schema_version: int
|
|
186
|
+
facts: tuple[CapturedFact, ...]
|
|
187
|
+
decisions: tuple[CapturedDecision, ...]
|
|
188
|
+
open_questions: tuple[str, ...]
|
|
189
|
+
|
|
190
|
+
def to_payload(self) -> dict[str, Any]:
|
|
191
|
+
return {
|
|
192
|
+
"schema_version": self.schema_version,
|
|
193
|
+
"facts": [item.to_payload() for item in self.facts],
|
|
194
|
+
"decisions": [item.to_payload() for item in self.decisions],
|
|
195
|
+
"open_questions": list(self.open_questions),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@dataclass(frozen=True)
|
|
200
|
+
class KnowledgeCaptureValidation:
|
|
201
|
+
valid: bool
|
|
202
|
+
errors: tuple[str, ...]
|
|
203
|
+
warnings: tuple[str, ...]
|
|
204
|
+
payload: KnowledgeCapturePayload | None = None
|
|
205
|
+
|
|
206
|
+
def to_payload(self) -> dict[str, Any]:
|
|
207
|
+
return {
|
|
208
|
+
"valid": self.valid,
|
|
209
|
+
"errors": list(self.errors),
|
|
210
|
+
"warnings": list(self.warnings),
|
|
211
|
+
"parsed_capture": self.payload.to_payload() if self.payload is not None else None,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@dataclass(frozen=True)
|
|
216
|
+
class PersistedKnowledgeCapture:
|
|
217
|
+
artifact_dir: Path
|
|
218
|
+
assistant_message_path: Path
|
|
219
|
+
capture_block_path: Path | None
|
|
220
|
+
parsed_capture_path: Path | None
|
|
221
|
+
validation_path: Path
|
|
222
|
+
promotion_path: Path
|
|
223
|
+
summary_path: Path
|
|
224
|
+
valid: bool
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass(frozen=True)
|
|
228
|
+
class KnowledgeCapturePromotionResult:
|
|
229
|
+
artifact_dir: Path
|
|
230
|
+
promotion_path: Path
|
|
231
|
+
capture_valid: bool
|
|
232
|
+
promotion_attempted: bool
|
|
233
|
+
promotion_succeeded: bool
|
|
234
|
+
promotion_skipped_reason: str | None
|
|
235
|
+
promotion_errors: tuple[str, ...]
|
|
236
|
+
fact_entry_ids: tuple[str, ...]
|
|
237
|
+
decision_entry_ids: tuple[str, ...]
|
|
238
|
+
|
|
239
|
+
def to_payload(self) -> dict[str, Any]:
|
|
240
|
+
return {
|
|
241
|
+
"capture_valid": self.capture_valid,
|
|
242
|
+
"promotion_attempted": self.promotion_attempted,
|
|
243
|
+
"promotion_succeeded": self.promotion_succeeded,
|
|
244
|
+
"promotion_skipped_reason": self.promotion_skipped_reason,
|
|
245
|
+
"promotion_errors": list(self.promotion_errors),
|
|
246
|
+
"fact_entry_ids": list(self.fact_entry_ids),
|
|
247
|
+
"decision_entry_ids": list(self.decision_entry_ids),
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _load_json_payload(path: Path) -> dict[str, Any]:
|
|
252
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
253
|
+
if not isinstance(raw, dict):
|
|
254
|
+
raise ValueError(f"expected JSON object in {path}")
|
|
255
|
+
return raw
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _promotion_result_from_payload(
|
|
259
|
+
*,
|
|
260
|
+
artifact_dir: Path,
|
|
261
|
+
promotion_path: Path,
|
|
262
|
+
payload: dict[str, Any],
|
|
263
|
+
) -> KnowledgeCapturePromotionResult:
|
|
264
|
+
return KnowledgeCapturePromotionResult(
|
|
265
|
+
artifact_dir=artifact_dir,
|
|
266
|
+
promotion_path=promotion_path,
|
|
267
|
+
capture_valid=bool(payload.get("capture_valid")),
|
|
268
|
+
promotion_attempted=bool(payload.get("promotion_attempted")),
|
|
269
|
+
promotion_succeeded=bool(payload.get("promotion_succeeded")),
|
|
270
|
+
promotion_skipped_reason=str(payload.get("promotion_skipped_reason") or "").strip() or None,
|
|
271
|
+
promotion_errors=tuple(
|
|
272
|
+
str(item).strip() for item in payload.get("promotion_errors") or [] if str(item).strip()
|
|
273
|
+
),
|
|
274
|
+
fact_entry_ids=tuple(
|
|
275
|
+
str(item).strip() for item in payload.get("fact_entry_ids") or [] if str(item).strip()
|
|
276
|
+
),
|
|
277
|
+
decision_entry_ids=tuple(
|
|
278
|
+
str(item).strip()
|
|
279
|
+
for item in payload.get("decision_entry_ids") or []
|
|
280
|
+
if str(item).strip()
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _write_promotion_result(
|
|
286
|
+
*,
|
|
287
|
+
artifact_dir: Path,
|
|
288
|
+
promotion_path: Path,
|
|
289
|
+
capture_valid: bool,
|
|
290
|
+
promotion_attempted: bool,
|
|
291
|
+
promotion_succeeded: bool,
|
|
292
|
+
promotion_skipped_reason: str | None,
|
|
293
|
+
promotion_errors: list[str] | tuple[str, ...],
|
|
294
|
+
fact_entry_ids: list[str] | tuple[str, ...],
|
|
295
|
+
decision_entry_ids: list[str] | tuple[str, ...],
|
|
296
|
+
) -> KnowledgeCapturePromotionResult:
|
|
297
|
+
result = KnowledgeCapturePromotionResult(
|
|
298
|
+
artifact_dir=artifact_dir,
|
|
299
|
+
promotion_path=promotion_path,
|
|
300
|
+
capture_valid=capture_valid,
|
|
301
|
+
promotion_attempted=promotion_attempted,
|
|
302
|
+
promotion_succeeded=promotion_succeeded,
|
|
303
|
+
promotion_skipped_reason=str(promotion_skipped_reason or "").strip() or None,
|
|
304
|
+
promotion_errors=tuple(str(item).strip() for item in promotion_errors if str(item).strip()),
|
|
305
|
+
fact_entry_ids=tuple(str(item).strip() for item in fact_entry_ids if str(item).strip()),
|
|
306
|
+
decision_entry_ids=tuple(
|
|
307
|
+
str(item).strip() for item in decision_entry_ids if str(item).strip()
|
|
308
|
+
),
|
|
309
|
+
)
|
|
310
|
+
_write_json(promotion_path, result.to_payload())
|
|
311
|
+
return result
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _load_promotion_result(artifact_dir: Path) -> KnowledgeCapturePromotionResult | None:
|
|
315
|
+
promotion_path = artifact_dir / "promotion.json"
|
|
316
|
+
if not promotion_path.exists():
|
|
317
|
+
return None
|
|
318
|
+
try:
|
|
319
|
+
payload = _load_json_payload(promotion_path)
|
|
320
|
+
except (OSError, UnicodeDecodeError, ValueError, json.JSONDecodeError):
|
|
321
|
+
return None
|
|
322
|
+
return _promotion_result_from_payload(
|
|
323
|
+
artifact_dir=artifact_dir,
|
|
324
|
+
promotion_path=promotion_path,
|
|
325
|
+
payload=payload,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class RecordingSurface:
|
|
330
|
+
def __init__(self, delegate: Surface) -> None:
|
|
331
|
+
self._delegate = delegate
|
|
332
|
+
self.final_assistant_message = ""
|
|
333
|
+
|
|
334
|
+
@property
|
|
335
|
+
def canonical_message_tool_events(self) -> bool:
|
|
336
|
+
"""Preserve the delegate's single-delivery event contract."""
|
|
337
|
+
return bool(getattr(self._delegate, "canonical_message_tool_events", False))
|
|
338
|
+
|
|
339
|
+
def on_status_update(self, status) -> None: # type: ignore[no-untyped-def]
|
|
340
|
+
self._delegate.on_status_update(status)
|
|
341
|
+
|
|
342
|
+
def emit_status_update(
|
|
343
|
+
self,
|
|
344
|
+
*,
|
|
345
|
+
tokens_in: int | None = None,
|
|
346
|
+
tokens_out: int | None = None,
|
|
347
|
+
cached_tokens: int | None = None,
|
|
348
|
+
cost_usd: float | None = None,
|
|
349
|
+
mode: str | None = None,
|
|
350
|
+
model: str | None = None,
|
|
351
|
+
step: int | None = None,
|
|
352
|
+
step_budget: int | None = None,
|
|
353
|
+
) -> None:
|
|
354
|
+
handler = getattr(self._delegate, "emit_status_update", None)
|
|
355
|
+
if callable(handler):
|
|
356
|
+
handler(
|
|
357
|
+
tokens_in=tokens_in,
|
|
358
|
+
tokens_out=tokens_out,
|
|
359
|
+
cached_tokens=cached_tokens,
|
|
360
|
+
cost_usd=cost_usd,
|
|
361
|
+
mode=mode,
|
|
362
|
+
model=model,
|
|
363
|
+
step=step,
|
|
364
|
+
step_budget=step_budget,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
def on_user_message(self, text: str) -> None:
|
|
368
|
+
self._delegate.on_user_message(text)
|
|
369
|
+
|
|
370
|
+
def on_progress_update(self, message: str) -> None:
|
|
371
|
+
self._delegate.on_progress_update(message)
|
|
372
|
+
|
|
373
|
+
def on_assistant_token(self, delta: str) -> None:
|
|
374
|
+
self._delegate.on_assistant_token(delta)
|
|
375
|
+
|
|
376
|
+
def on_assistant_message_done(self, text: str) -> None:
|
|
377
|
+
self.final_assistant_message = str(text or "")
|
|
378
|
+
self._delegate.on_assistant_message_done(text)
|
|
379
|
+
|
|
380
|
+
def emit_message_delta(
|
|
381
|
+
self,
|
|
382
|
+
text: str,
|
|
383
|
+
*,
|
|
384
|
+
worker_id: str | None = None,
|
|
385
|
+
role: str | None = None,
|
|
386
|
+
) -> None:
|
|
387
|
+
handler = getattr(self._delegate, "emit_message_delta", None)
|
|
388
|
+
if callable(handler):
|
|
389
|
+
handler(text, worker_id=worker_id, role=role)
|
|
390
|
+
|
|
391
|
+
def emit_message_end(
|
|
392
|
+
self,
|
|
393
|
+
text: str = "",
|
|
394
|
+
*,
|
|
395
|
+
worker_id: str | None = None,
|
|
396
|
+
role: str | None = None,
|
|
397
|
+
) -> None:
|
|
398
|
+
self.final_assistant_message = str(text or "")
|
|
399
|
+
handler = getattr(self._delegate, "emit_message_end", None)
|
|
400
|
+
if callable(handler):
|
|
401
|
+
handler(text, worker_id=worker_id, role=role)
|
|
402
|
+
|
|
403
|
+
def on_subagent_start(self, event) -> None: # type: ignore[no-untyped-def]
|
|
404
|
+
handler = getattr(self._delegate, "on_subagent_start", None)
|
|
405
|
+
if callable(handler):
|
|
406
|
+
handler(event)
|
|
407
|
+
|
|
408
|
+
def on_subagent_end(self, event) -> None: # type: ignore[no-untyped-def]
|
|
409
|
+
handler = getattr(self._delegate, "on_subagent_end", None)
|
|
410
|
+
if callable(handler):
|
|
411
|
+
handler(event)
|
|
412
|
+
|
|
413
|
+
def on_tool_start(self, event) -> None: # type: ignore[no-untyped-def]
|
|
414
|
+
self._delegate.on_tool_start(event)
|
|
415
|
+
|
|
416
|
+
def on_tool_output(self, event) -> None: # type: ignore[no-untyped-def]
|
|
417
|
+
self._delegate.on_tool_output(event)
|
|
418
|
+
|
|
419
|
+
def on_tool_end(self, event) -> None: # type: ignore[no-untyped-def]
|
|
420
|
+
self._delegate.on_tool_end(event)
|
|
421
|
+
|
|
422
|
+
def emit_tool_call_started(
|
|
423
|
+
self,
|
|
424
|
+
call_id: str,
|
|
425
|
+
name: str,
|
|
426
|
+
arguments_preview: str,
|
|
427
|
+
*,
|
|
428
|
+
worker_id: str | None = None,
|
|
429
|
+
role: str | None = None,
|
|
430
|
+
) -> None:
|
|
431
|
+
handler = getattr(self._delegate, "emit_tool_call_started", None)
|
|
432
|
+
if callable(handler):
|
|
433
|
+
handler(
|
|
434
|
+
call_id,
|
|
435
|
+
name,
|
|
436
|
+
arguments_preview,
|
|
437
|
+
worker_id=worker_id,
|
|
438
|
+
role=role,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
def emit_tool_call_progress(
|
|
442
|
+
self,
|
|
443
|
+
call_id: str,
|
|
444
|
+
text: str,
|
|
445
|
+
*,
|
|
446
|
+
worker_id: str | None = None,
|
|
447
|
+
role: str | None = None,
|
|
448
|
+
) -> None:
|
|
449
|
+
handler = getattr(self._delegate, "emit_tool_call_progress", None)
|
|
450
|
+
if callable(handler):
|
|
451
|
+
handler(call_id, text, worker_id=worker_id, role=role)
|
|
452
|
+
|
|
453
|
+
def emit_tool_call_completed(
|
|
454
|
+
self,
|
|
455
|
+
call_id: str,
|
|
456
|
+
success: bool,
|
|
457
|
+
result_preview: str,
|
|
458
|
+
*,
|
|
459
|
+
worker_id: str | None = None,
|
|
460
|
+
role: str | None = None,
|
|
461
|
+
) -> None:
|
|
462
|
+
handler = getattr(self._delegate, "emit_tool_call_completed", None)
|
|
463
|
+
if callable(handler):
|
|
464
|
+
handler(
|
|
465
|
+
call_id,
|
|
466
|
+
success,
|
|
467
|
+
result_preview,
|
|
468
|
+
worker_id=worker_id,
|
|
469
|
+
role=role,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
def on_patch_generated(self, event) -> None: # type: ignore[no-untyped-def]
|
|
473
|
+
self._delegate.on_patch_generated(event)
|
|
474
|
+
|
|
475
|
+
def on_warning(self, warning: str) -> None:
|
|
476
|
+
self.emit_warning(warning)
|
|
477
|
+
|
|
478
|
+
def emit_warning(
|
|
479
|
+
self,
|
|
480
|
+
message: str,
|
|
481
|
+
*,
|
|
482
|
+
worker_id: str | None = None,
|
|
483
|
+
role: str | None = None,
|
|
484
|
+
) -> None:
|
|
485
|
+
if _emit_surface_warning(self._delegate, message, worker_id=worker_id, role=role):
|
|
486
|
+
return
|
|
487
|
+
warnings.warn(message, stacklevel=2)
|
|
488
|
+
|
|
489
|
+
def on_error(self, err: str) -> None:
|
|
490
|
+
self.emit_error("execution_error", err, True)
|
|
491
|
+
|
|
492
|
+
def emit_error(
|
|
493
|
+
self,
|
|
494
|
+
code: str,
|
|
495
|
+
message: str,
|
|
496
|
+
recoverable: bool,
|
|
497
|
+
*,
|
|
498
|
+
worker_id: str | None = None,
|
|
499
|
+
role: str | None = None,
|
|
500
|
+
) -> None:
|
|
501
|
+
delegate_cls = getattr(self._delegate, "__class__", None)
|
|
502
|
+
handler = getattr(self._delegate, "emit_error", None)
|
|
503
|
+
if callable(handler):
|
|
504
|
+
cls_handler = getattr(delegate_cls, "emit_error", None)
|
|
505
|
+
if cls_handler is not getattr(NoopSurface, "emit_error", None):
|
|
506
|
+
handler(code, message, recoverable, worker_id=worker_id, role=role)
|
|
507
|
+
return
|
|
508
|
+
fallback = getattr(self._delegate, "on_error", None)
|
|
509
|
+
if callable(fallback):
|
|
510
|
+
fallback(message)
|
|
511
|
+
|
|
512
|
+
def emit_info(
|
|
513
|
+
self,
|
|
514
|
+
message: str,
|
|
515
|
+
*,
|
|
516
|
+
worker_id: str | None = None,
|
|
517
|
+
role: str | None = None,
|
|
518
|
+
) -> None:
|
|
519
|
+
delegate_cls = getattr(self._delegate, "__class__", None)
|
|
520
|
+
handler = getattr(self._delegate, "emit_info", None)
|
|
521
|
+
if callable(handler):
|
|
522
|
+
cls_handler = getattr(delegate_cls, "emit_info", None)
|
|
523
|
+
if cls_handler is not getattr(NoopSurface, "emit_info", None):
|
|
524
|
+
handler(message, worker_id=worker_id, role=role)
|
|
525
|
+
return
|
|
526
|
+
fallback = getattr(self._delegate, "on_progress_update", None)
|
|
527
|
+
if callable(fallback):
|
|
528
|
+
fallback(message)
|
|
529
|
+
|
|
530
|
+
def emit_mode_changed(self, mode: str) -> None:
|
|
531
|
+
handler = getattr(self._delegate, "emit_mode_changed", None)
|
|
532
|
+
if callable(handler):
|
|
533
|
+
handler(mode)
|
|
534
|
+
|
|
535
|
+
def emit_persona_changed(self, persona: str, effective_mode: str, source: str = "user") -> None:
|
|
536
|
+
handler = getattr(self._delegate, "emit_persona_changed", None)
|
|
537
|
+
if callable(handler):
|
|
538
|
+
handler(persona, effective_mode, source)
|
|
539
|
+
|
|
540
|
+
def request_approval(self, request): # type: ignore[no-untyped-def]
|
|
541
|
+
return self._delegate.request_approval(request)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def render_execution_knowledge_capture_rules() -> list[str]:
|
|
545
|
+
return [
|
|
546
|
+
"- Keep the normal human-readable final response.",
|
|
547
|
+
f"- If you learned reusable repo knowledge, append one fenced `{KNOWLEDGE_CAPTURE_FENCE}` block after the summary.",
|
|
548
|
+
"- Use JSON with `schema_version`, `facts`, `decisions`, and optional `open_questions`; omit the block if there is nothing durable to capture.",
|
|
549
|
+
"- Facts are immutable observations with `title`, `summary`, optional repo-relative `paths`, and optional `tags`.",
|
|
550
|
+
"- Decisions use `decision_key`, `title`, `summary`, `status` (`active` or `invalidated`), and optional repo-relative `paths`/`tags`.",
|
|
551
|
+
f"- Keep it bounded: at most {_MAX_FACTS} facts, {_MAX_DECISIONS} decisions, and short strings.",
|
|
552
|
+
]
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def extract_knowledge_capture_block(text: str) -> str | None:
|
|
556
|
+
matches = list(_CAPTURE_BLOCK_RE.finditer(str(text or "")))
|
|
557
|
+
if not matches:
|
|
558
|
+
return None
|
|
559
|
+
return matches[-1].group(1).strip()
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def validate_knowledge_capture(*, final_text: str) -> KnowledgeCaptureValidation:
|
|
563
|
+
clean = str(final_text or "").strip()
|
|
564
|
+
if not clean:
|
|
565
|
+
return KnowledgeCaptureValidation(
|
|
566
|
+
valid=False,
|
|
567
|
+
errors=("final assistant message was not captured",),
|
|
568
|
+
warnings=(),
|
|
569
|
+
payload=None,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
block = extract_knowledge_capture_block(clean)
|
|
573
|
+
if block is None:
|
|
574
|
+
return KnowledgeCaptureValidation(
|
|
575
|
+
valid=False,
|
|
576
|
+
errors=(f"missing fenced `{KNOWLEDGE_CAPTURE_FENCE}` block",),
|
|
577
|
+
warnings=(),
|
|
578
|
+
payload=None,
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
try:
|
|
582
|
+
raw = json.loads(block)
|
|
583
|
+
except json.JSONDecodeError as exc:
|
|
584
|
+
return KnowledgeCaptureValidation(
|
|
585
|
+
valid=False,
|
|
586
|
+
errors=(f"invalid structured knowledge JSON: {exc}",),
|
|
587
|
+
warnings=(),
|
|
588
|
+
payload=None,
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
if not isinstance(raw, dict):
|
|
592
|
+
return KnowledgeCaptureValidation(
|
|
593
|
+
valid=False,
|
|
594
|
+
errors=("structured knowledge capture must be a JSON object",),
|
|
595
|
+
warnings=(),
|
|
596
|
+
payload=None,
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
unknown_top_level = sorted(set(raw) - _ALLOWED_TOP_LEVEL_KEYS)
|
|
600
|
+
if unknown_top_level:
|
|
601
|
+
return KnowledgeCaptureValidation(
|
|
602
|
+
valid=False,
|
|
603
|
+
errors=(f"unknown structured knowledge keys: {', '.join(unknown_top_level)}",),
|
|
604
|
+
warnings=(),
|
|
605
|
+
payload=None,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
schema_version = raw.get("schema_version")
|
|
609
|
+
if schema_version != KNOWLEDGE_CAPTURE_SCHEMA_VERSION:
|
|
610
|
+
return KnowledgeCaptureValidation(
|
|
611
|
+
valid=False,
|
|
612
|
+
errors=(f"schema_version must be {KNOWLEDGE_CAPTURE_SCHEMA_VERSION}",),
|
|
613
|
+
warnings=(),
|
|
614
|
+
payload=None,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
facts_raw = raw.get("facts", [])
|
|
618
|
+
if not isinstance(facts_raw, list):
|
|
619
|
+
return KnowledgeCaptureValidation(
|
|
620
|
+
valid=False,
|
|
621
|
+
errors=("facts must be an array",),
|
|
622
|
+
warnings=(),
|
|
623
|
+
payload=None,
|
|
624
|
+
)
|
|
625
|
+
if len(facts_raw) > _MAX_FACTS:
|
|
626
|
+
return KnowledgeCaptureValidation(
|
|
627
|
+
valid=False,
|
|
628
|
+
errors=(f"facts exceeds max count {_MAX_FACTS}",),
|
|
629
|
+
warnings=(),
|
|
630
|
+
payload=None,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
facts: list[CapturedFact] = []
|
|
634
|
+
for idx, item in enumerate(facts_raw, start=1):
|
|
635
|
+
if not isinstance(item, dict):
|
|
636
|
+
return KnowledgeCaptureValidation(
|
|
637
|
+
valid=False,
|
|
638
|
+
errors=(f"fact #{idx} must be an object",),
|
|
639
|
+
warnings=(),
|
|
640
|
+
payload=None,
|
|
641
|
+
)
|
|
642
|
+
unknown_keys = sorted(set(item) - _ALLOWED_FACT_KEYS)
|
|
643
|
+
if unknown_keys:
|
|
644
|
+
return KnowledgeCaptureValidation(
|
|
645
|
+
valid=False,
|
|
646
|
+
errors=(f"fact #{idx} has unknown keys: {', '.join(unknown_keys)}",),
|
|
647
|
+
warnings=(),
|
|
648
|
+
payload=None,
|
|
649
|
+
)
|
|
650
|
+
try:
|
|
651
|
+
facts.append(
|
|
652
|
+
CapturedFact(
|
|
653
|
+
title=_bounded_text(
|
|
654
|
+
item.get("title"), field=f"fact #{idx} title", max_chars=_MAX_TITLE_CHARS
|
|
655
|
+
),
|
|
656
|
+
summary=_bounded_text(
|
|
657
|
+
item.get("summary"),
|
|
658
|
+
field=f"fact #{idx} summary",
|
|
659
|
+
max_chars=_MAX_SUMMARY_CHARS,
|
|
660
|
+
),
|
|
661
|
+
paths=_normalize_paths(item.get("paths")),
|
|
662
|
+
tags=_normalize_tags(item.get("tags")),
|
|
663
|
+
)
|
|
664
|
+
)
|
|
665
|
+
except ValueError as exc:
|
|
666
|
+
return KnowledgeCaptureValidation(
|
|
667
|
+
valid=False,
|
|
668
|
+
errors=(str(exc),),
|
|
669
|
+
warnings=(),
|
|
670
|
+
payload=None,
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
decisions_raw = raw.get("decisions", [])
|
|
674
|
+
if not isinstance(decisions_raw, list):
|
|
675
|
+
return KnowledgeCaptureValidation(
|
|
676
|
+
valid=False,
|
|
677
|
+
errors=("decisions must be an array",),
|
|
678
|
+
warnings=(),
|
|
679
|
+
payload=None,
|
|
680
|
+
)
|
|
681
|
+
if len(decisions_raw) > _MAX_DECISIONS:
|
|
682
|
+
return KnowledgeCaptureValidation(
|
|
683
|
+
valid=False,
|
|
684
|
+
errors=(f"decisions exceeds max count {_MAX_DECISIONS}",),
|
|
685
|
+
warnings=(),
|
|
686
|
+
payload=None,
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
decisions: list[CapturedDecision] = []
|
|
690
|
+
seen_decision_keys: set[str] = set()
|
|
691
|
+
for idx, item in enumerate(decisions_raw, start=1):
|
|
692
|
+
if not isinstance(item, dict):
|
|
693
|
+
return KnowledgeCaptureValidation(
|
|
694
|
+
valid=False,
|
|
695
|
+
errors=(f"decision #{idx} must be an object",),
|
|
696
|
+
warnings=(),
|
|
697
|
+
payload=None,
|
|
698
|
+
)
|
|
699
|
+
unknown_keys = sorted(set(item) - _ALLOWED_DECISION_KEYS)
|
|
700
|
+
if unknown_keys:
|
|
701
|
+
return KnowledgeCaptureValidation(
|
|
702
|
+
valid=False,
|
|
703
|
+
errors=(f"decision #{idx} has unknown keys: {', '.join(unknown_keys)}",),
|
|
704
|
+
warnings=(),
|
|
705
|
+
payload=None,
|
|
706
|
+
)
|
|
707
|
+
try:
|
|
708
|
+
decision_key = _bounded_text(
|
|
709
|
+
item.get("decision_key"),
|
|
710
|
+
field=f"decision #{idx} decision_key",
|
|
711
|
+
max_chars=_MAX_DECISION_KEY_CHARS,
|
|
712
|
+
).lower()
|
|
713
|
+
except ValueError as exc:
|
|
714
|
+
return KnowledgeCaptureValidation(
|
|
715
|
+
valid=False,
|
|
716
|
+
errors=(str(exc),),
|
|
717
|
+
warnings=(),
|
|
718
|
+
payload=None,
|
|
719
|
+
)
|
|
720
|
+
if not _DECISION_KEY_RE.match(decision_key):
|
|
721
|
+
return KnowledgeCaptureValidation(
|
|
722
|
+
valid=False,
|
|
723
|
+
errors=(f"invalid decision_key: {decision_key}",),
|
|
724
|
+
warnings=(),
|
|
725
|
+
payload=None,
|
|
726
|
+
)
|
|
727
|
+
if decision_key in seen_decision_keys:
|
|
728
|
+
return KnowledgeCaptureValidation(
|
|
729
|
+
valid=False,
|
|
730
|
+
errors=(f"duplicate decision_key: {decision_key}",),
|
|
731
|
+
warnings=(),
|
|
732
|
+
payload=None,
|
|
733
|
+
)
|
|
734
|
+
seen_decision_keys.add(decision_key)
|
|
735
|
+
status = str(item.get("status") or "").strip().lower()
|
|
736
|
+
if status not in _DECISION_STATUSES:
|
|
737
|
+
return KnowledgeCaptureValidation(
|
|
738
|
+
valid=False,
|
|
739
|
+
errors=(
|
|
740
|
+
f"decision #{idx} status must be one of: {', '.join(sorted(_DECISION_STATUSES))}",
|
|
741
|
+
),
|
|
742
|
+
warnings=(),
|
|
743
|
+
payload=None,
|
|
744
|
+
)
|
|
745
|
+
try:
|
|
746
|
+
decisions.append(
|
|
747
|
+
CapturedDecision(
|
|
748
|
+
decision_key=decision_key,
|
|
749
|
+
title=_bounded_text(
|
|
750
|
+
item.get("title"),
|
|
751
|
+
field=f"decision #{idx} title",
|
|
752
|
+
max_chars=_MAX_TITLE_CHARS,
|
|
753
|
+
),
|
|
754
|
+
summary=_bounded_text(
|
|
755
|
+
item.get("summary"),
|
|
756
|
+
field=f"decision #{idx} summary",
|
|
757
|
+
max_chars=_MAX_SUMMARY_CHARS,
|
|
758
|
+
),
|
|
759
|
+
status=status,
|
|
760
|
+
paths=_normalize_paths(item.get("paths")),
|
|
761
|
+
tags=_normalize_tags(item.get("tags")),
|
|
762
|
+
)
|
|
763
|
+
)
|
|
764
|
+
except ValueError as exc:
|
|
765
|
+
return KnowledgeCaptureValidation(
|
|
766
|
+
valid=False,
|
|
767
|
+
errors=(str(exc),),
|
|
768
|
+
warnings=(),
|
|
769
|
+
payload=None,
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
try:
|
|
773
|
+
open_questions = _normalize_open_questions(raw.get("open_questions"))
|
|
774
|
+
except ValueError as exc:
|
|
775
|
+
return KnowledgeCaptureValidation(
|
|
776
|
+
valid=False,
|
|
777
|
+
errors=(str(exc),),
|
|
778
|
+
warnings=(),
|
|
779
|
+
payload=None,
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
return KnowledgeCaptureValidation(
|
|
783
|
+
valid=True,
|
|
784
|
+
errors=(),
|
|
785
|
+
warnings=(),
|
|
786
|
+
payload=KnowledgeCapturePayload(
|
|
787
|
+
schema_version=KNOWLEDGE_CAPTURE_SCHEMA_VERSION,
|
|
788
|
+
facts=tuple(facts),
|
|
789
|
+
decisions=tuple(decisions),
|
|
790
|
+
open_questions=open_questions,
|
|
791
|
+
),
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def _write_text(path: Path, text: str) -> Path:
|
|
796
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
797
|
+
path.write_text(text, encoding="utf-8")
|
|
798
|
+
return path
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _write_json(path: Path, payload: dict[str, Any]) -> Path:
|
|
802
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
803
|
+
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
804
|
+
return path
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def _repo_rel_optional(root: Path, path: Path | None) -> str | None:
|
|
808
|
+
if path is None:
|
|
809
|
+
return None
|
|
810
|
+
return _repo_rel(root, path)
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def _rooted_repo_path(root: Path, raw: Any) -> Path | None:
|
|
814
|
+
text = str(raw or "").strip()
|
|
815
|
+
if not text:
|
|
816
|
+
return None
|
|
817
|
+
candidate = Path(text)
|
|
818
|
+
if candidate.is_absolute():
|
|
819
|
+
return candidate
|
|
820
|
+
return root / candidate
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def _build_capture_payload(raw: Any) -> KnowledgeCapturePayload:
|
|
824
|
+
if not isinstance(raw, dict):
|
|
825
|
+
raise ValueError("parsed structured capture must be a JSON object")
|
|
826
|
+
facts: list[CapturedFact] = []
|
|
827
|
+
for idx, item in enumerate(raw.get("facts") or [], start=1):
|
|
828
|
+
if not isinstance(item, dict):
|
|
829
|
+
raise ValueError(f"fact #{idx} must be an object")
|
|
830
|
+
facts.append(
|
|
831
|
+
CapturedFact(
|
|
832
|
+
title=_bounded_text(
|
|
833
|
+
item.get("title"),
|
|
834
|
+
field=f"fact #{idx} title",
|
|
835
|
+
max_chars=_MAX_TITLE_CHARS,
|
|
836
|
+
),
|
|
837
|
+
summary=_bounded_text(
|
|
838
|
+
item.get("summary"),
|
|
839
|
+
field=f"fact #{idx} summary",
|
|
840
|
+
max_chars=_MAX_SUMMARY_CHARS,
|
|
841
|
+
),
|
|
842
|
+
paths=_normalize_paths(item.get("paths")),
|
|
843
|
+
tags=_normalize_tags(item.get("tags")),
|
|
844
|
+
)
|
|
845
|
+
)
|
|
846
|
+
decisions: list[CapturedDecision] = []
|
|
847
|
+
for idx, item in enumerate(raw.get("decisions") or [], start=1):
|
|
848
|
+
if not isinstance(item, dict):
|
|
849
|
+
raise ValueError(f"decision #{idx} must be an object")
|
|
850
|
+
status = str(item.get("status") or "").strip().lower()
|
|
851
|
+
if status not in _DECISION_STATUSES:
|
|
852
|
+
raise ValueError(
|
|
853
|
+
f"decision #{idx} status must be one of: {', '.join(sorted(_DECISION_STATUSES))}"
|
|
854
|
+
)
|
|
855
|
+
decisions.append(
|
|
856
|
+
CapturedDecision(
|
|
857
|
+
decision_key=_bounded_text(
|
|
858
|
+
item.get("decision_key"),
|
|
859
|
+
field=f"decision #{idx} decision_key",
|
|
860
|
+
max_chars=_MAX_DECISION_KEY_CHARS,
|
|
861
|
+
).lower(),
|
|
862
|
+
title=_bounded_text(
|
|
863
|
+
item.get("title"),
|
|
864
|
+
field=f"decision #{idx} title",
|
|
865
|
+
max_chars=_MAX_TITLE_CHARS,
|
|
866
|
+
),
|
|
867
|
+
summary=_bounded_text(
|
|
868
|
+
item.get("summary"),
|
|
869
|
+
field=f"decision #{idx} summary",
|
|
870
|
+
max_chars=_MAX_SUMMARY_CHARS,
|
|
871
|
+
),
|
|
872
|
+
status=status,
|
|
873
|
+
paths=_normalize_paths(item.get("paths")),
|
|
874
|
+
tags=_normalize_tags(item.get("tags")),
|
|
875
|
+
)
|
|
876
|
+
)
|
|
877
|
+
return KnowledgeCapturePayload(
|
|
878
|
+
schema_version=int(raw.get("schema_version") or KNOWLEDGE_CAPTURE_SCHEMA_VERSION),
|
|
879
|
+
facts=tuple(facts),
|
|
880
|
+
decisions=tuple(decisions),
|
|
881
|
+
open_questions=_normalize_open_questions(raw.get("open_questions")),
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def persist_execution_knowledge_capture(
|
|
886
|
+
*,
|
|
887
|
+
paths: RunPaths,
|
|
888
|
+
task: dict[str, Any],
|
|
889
|
+
source: str,
|
|
890
|
+
assistant_message: str | None,
|
|
891
|
+
artifact_dir: Path,
|
|
892
|
+
report_path: Path | None,
|
|
893
|
+
patch_path: Path | None,
|
|
894
|
+
verify_artifact_path: Path | None,
|
|
895
|
+
budget_artifact_path: Path | None,
|
|
896
|
+
session_artifact_dir: Path | None,
|
|
897
|
+
) -> PersistedKnowledgeCapture:
|
|
898
|
+
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
899
|
+
assistant_message_path = _write_text(
|
|
900
|
+
artifact_dir / "assistant_message.md",
|
|
901
|
+
(str(assistant_message or "").rstrip() + "\n")
|
|
902
|
+
if str(assistant_message or "").strip()
|
|
903
|
+
else "(none captured)\n",
|
|
904
|
+
)
|
|
905
|
+
capture_block = extract_knowledge_capture_block(str(assistant_message or ""))
|
|
906
|
+
capture_block_path = (
|
|
907
|
+
_write_text(artifact_dir / "capture_block.txt", capture_block.rstrip() + "\n")
|
|
908
|
+
if capture_block is not None
|
|
909
|
+
else None
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
validation = validate_knowledge_capture(final_text=str(assistant_message or ""))
|
|
913
|
+
parsed_capture_path = (
|
|
914
|
+
_write_json(artifact_dir / "parsed_capture.json", validation.payload.to_payload())
|
|
915
|
+
if validation.payload is not None
|
|
916
|
+
else None
|
|
917
|
+
)
|
|
918
|
+
capture_summary_path = artifact_dir / "summary.md"
|
|
919
|
+
validation_path = _write_json(
|
|
920
|
+
artifact_dir / "validation.json",
|
|
921
|
+
{
|
|
922
|
+
"schema_version": KNOWLEDGE_CAPTURE_SCHEMA_VERSION,
|
|
923
|
+
"task_id": str(task.get("id") or "").strip() or None,
|
|
924
|
+
"source": source,
|
|
925
|
+
"capture_present": capture_block is not None,
|
|
926
|
+
"assistant_message_path": _repo_rel(paths.root, assistant_message_path),
|
|
927
|
+
"capture_block_path": _repo_rel_optional(paths.root, capture_block_path),
|
|
928
|
+
"parsed_capture_path": _repo_rel_optional(paths.root, parsed_capture_path),
|
|
929
|
+
"report_path": _repo_rel_optional(paths.root, report_path),
|
|
930
|
+
"patch_path": _repo_rel_optional(paths.root, patch_path),
|
|
931
|
+
"verify_artifact_path": _repo_rel_optional(paths.root, verify_artifact_path),
|
|
932
|
+
"budget_artifact_path": _repo_rel_optional(paths.root, budget_artifact_path),
|
|
933
|
+
"session_artifact_dir": _repo_rel_optional(paths.root, session_artifact_dir),
|
|
934
|
+
"promotable_fact_count": len(validation.payload.facts) if validation.payload else 0,
|
|
935
|
+
"promotable_decision_count": len(validation.payload.decisions)
|
|
936
|
+
if validation.payload
|
|
937
|
+
else 0,
|
|
938
|
+
**validation.to_payload(),
|
|
939
|
+
},
|
|
940
|
+
)
|
|
941
|
+
promotion_path = artifact_dir / "promotion.json"
|
|
942
|
+
_write_promotion_result(
|
|
943
|
+
artifact_dir=artifact_dir,
|
|
944
|
+
promotion_path=promotion_path,
|
|
945
|
+
capture_valid=validation.valid,
|
|
946
|
+
promotion_attempted=False,
|
|
947
|
+
promotion_succeeded=False,
|
|
948
|
+
promotion_skipped_reason=None,
|
|
949
|
+
promotion_errors=(),
|
|
950
|
+
fact_entry_ids=(),
|
|
951
|
+
decision_entry_ids=(),
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
summary_lines = [
|
|
955
|
+
"# Structured Knowledge Capture",
|
|
956
|
+
"",
|
|
957
|
+
f"- Source: `{source}`",
|
|
958
|
+
f"- Valid: `{'yes' if validation.valid else 'no'}`",
|
|
959
|
+
f"- Promotable Facts: `{len(validation.payload.facts) if validation.payload else 0}`",
|
|
960
|
+
f"- Promotable Decisions: `{len(validation.payload.decisions) if validation.payload else 0}`",
|
|
961
|
+
f"- Assistant Message: `{_repo_rel(paths.root, assistant_message_path)}`",
|
|
962
|
+
(
|
|
963
|
+
f"- Capture Block: `{_repo_rel(paths.root, capture_block_path)}`"
|
|
964
|
+
if capture_block_path is not None
|
|
965
|
+
else "- Capture Block: (none)"
|
|
966
|
+
),
|
|
967
|
+
(
|
|
968
|
+
f"- Parsed Capture: `{_repo_rel(paths.root, parsed_capture_path)}`"
|
|
969
|
+
if parsed_capture_path is not None
|
|
970
|
+
else "- Parsed Capture: (none)"
|
|
971
|
+
),
|
|
972
|
+
f"- Validation: `{_repo_rel(paths.root, validation_path)}`",
|
|
973
|
+
f"- Promotion State: `{_repo_rel(paths.root, promotion_path)}`",
|
|
974
|
+
]
|
|
975
|
+
if validation.errors:
|
|
976
|
+
summary_lines.extend(["", "## Errors", ""])
|
|
977
|
+
summary_lines.extend(f"- {item}" for item in validation.errors)
|
|
978
|
+
if validation.warnings:
|
|
979
|
+
summary_lines.extend(["", "## Warnings", ""])
|
|
980
|
+
summary_lines.extend(f"- {item}" for item in validation.warnings)
|
|
981
|
+
if validation.payload is not None and validation.payload.open_questions:
|
|
982
|
+
summary_lines.extend(["", "## Open Questions", ""])
|
|
983
|
+
summary_lines.extend(f"- {item}" for item in validation.payload.open_questions)
|
|
984
|
+
summary_path = _write_text(capture_summary_path, "\n".join(summary_lines).rstrip() + "\n")
|
|
985
|
+
|
|
986
|
+
return PersistedKnowledgeCapture(
|
|
987
|
+
artifact_dir=artifact_dir,
|
|
988
|
+
assistant_message_path=assistant_message_path,
|
|
989
|
+
capture_block_path=capture_block_path,
|
|
990
|
+
parsed_capture_path=parsed_capture_path,
|
|
991
|
+
validation_path=validation_path,
|
|
992
|
+
promotion_path=promotion_path,
|
|
993
|
+
summary_path=summary_path,
|
|
994
|
+
valid=validation.valid,
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def promote_validated_knowledge_capture(
|
|
999
|
+
*,
|
|
1000
|
+
paths: RunPaths,
|
|
1001
|
+
task: dict[str, Any],
|
|
1002
|
+
artifact_dir: Path,
|
|
1003
|
+
) -> KnowledgeCapturePromotionResult:
|
|
1004
|
+
existing = _load_promotion_result(artifact_dir)
|
|
1005
|
+
if existing is not None and (
|
|
1006
|
+
existing.promotion_succeeded
|
|
1007
|
+
or existing.promotion_attempted
|
|
1008
|
+
or existing.promotion_skipped_reason is not None
|
|
1009
|
+
):
|
|
1010
|
+
return existing
|
|
1011
|
+
|
|
1012
|
+
promotion_path = artifact_dir / "promotion.json"
|
|
1013
|
+
validation_path = artifact_dir / "validation.json"
|
|
1014
|
+
if not validation_path.exists():
|
|
1015
|
+
return _write_promotion_result(
|
|
1016
|
+
artifact_dir=artifact_dir,
|
|
1017
|
+
promotion_path=promotion_path,
|
|
1018
|
+
capture_valid=False,
|
|
1019
|
+
promotion_attempted=False,
|
|
1020
|
+
promotion_succeeded=False,
|
|
1021
|
+
promotion_skipped_reason="validation artifact missing",
|
|
1022
|
+
promotion_errors=(),
|
|
1023
|
+
fact_entry_ids=(),
|
|
1024
|
+
decision_entry_ids=(),
|
|
1025
|
+
)
|
|
1026
|
+
try:
|
|
1027
|
+
validation_payload = _load_json_payload(validation_path)
|
|
1028
|
+
except (OSError, UnicodeDecodeError, ValueError, json.JSONDecodeError) as exc:
|
|
1029
|
+
return _write_promotion_result(
|
|
1030
|
+
artifact_dir=artifact_dir,
|
|
1031
|
+
promotion_path=promotion_path,
|
|
1032
|
+
capture_valid=False,
|
|
1033
|
+
promotion_attempted=False,
|
|
1034
|
+
promotion_succeeded=False,
|
|
1035
|
+
promotion_skipped_reason=f"validation artifact unreadable: {exc}",
|
|
1036
|
+
promotion_errors=(),
|
|
1037
|
+
fact_entry_ids=(),
|
|
1038
|
+
decision_entry_ids=(),
|
|
1039
|
+
)
|
|
1040
|
+
|
|
1041
|
+
capture_valid = bool(validation_payload.get("valid"))
|
|
1042
|
+
parsed_raw = validation_payload.get("parsed_capture")
|
|
1043
|
+
if not capture_valid or parsed_raw is None:
|
|
1044
|
+
return _write_promotion_result(
|
|
1045
|
+
artifact_dir=artifact_dir,
|
|
1046
|
+
promotion_path=promotion_path,
|
|
1047
|
+
capture_valid=capture_valid,
|
|
1048
|
+
promotion_attempted=False,
|
|
1049
|
+
promotion_succeeded=False,
|
|
1050
|
+
promotion_skipped_reason="structured capture is not valid for canonical promotion",
|
|
1051
|
+
promotion_errors=(),
|
|
1052
|
+
fact_entry_ids=(),
|
|
1053
|
+
decision_entry_ids=(),
|
|
1054
|
+
)
|
|
1055
|
+
try:
|
|
1056
|
+
payload = _build_capture_payload(parsed_raw)
|
|
1057
|
+
except ValueError as exc:
|
|
1058
|
+
return _write_promotion_result(
|
|
1059
|
+
artifact_dir=artifact_dir,
|
|
1060
|
+
promotion_path=promotion_path,
|
|
1061
|
+
capture_valid=False,
|
|
1062
|
+
promotion_attempted=False,
|
|
1063
|
+
promotion_succeeded=False,
|
|
1064
|
+
promotion_skipped_reason=f"parsed capture artifact invalid: {exc}",
|
|
1065
|
+
promotion_errors=(),
|
|
1066
|
+
fact_entry_ids=(),
|
|
1067
|
+
decision_entry_ids=(),
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
if not payload.facts and not payload.decisions:
|
|
1071
|
+
return _write_promotion_result(
|
|
1072
|
+
artifact_dir=artifact_dir,
|
|
1073
|
+
promotion_path=promotion_path,
|
|
1074
|
+
capture_valid=True,
|
|
1075
|
+
promotion_attempted=False,
|
|
1076
|
+
promotion_succeeded=False,
|
|
1077
|
+
promotion_skipped_reason="capture contained no promotable facts or decisions",
|
|
1078
|
+
promotion_errors=(),
|
|
1079
|
+
fact_entry_ids=(),
|
|
1080
|
+
decision_entry_ids=(),
|
|
1081
|
+
)
|
|
1082
|
+
|
|
1083
|
+
report_path = _rooted_repo_path(paths.root, validation_payload.get("report_path"))
|
|
1084
|
+
patch_path = _rooted_repo_path(paths.root, validation_payload.get("patch_path"))
|
|
1085
|
+
verify_artifact_path = _rooted_repo_path(
|
|
1086
|
+
paths.root, validation_payload.get("verify_artifact_path")
|
|
1087
|
+
)
|
|
1088
|
+
budget_artifact_path = _rooted_repo_path(
|
|
1089
|
+
paths.root, validation_payload.get("budget_artifact_path")
|
|
1090
|
+
)
|
|
1091
|
+
session_artifact_dir = _rooted_repo_path(
|
|
1092
|
+
paths.root, validation_payload.get("session_artifact_dir")
|
|
1093
|
+
)
|
|
1094
|
+
source = str(validation_payload.get("source") or "").strip() or "execution"
|
|
1095
|
+
capture_artifact_path = artifact_dir / "summary.md"
|
|
1096
|
+
|
|
1097
|
+
fact_entry_ids: list[str] = []
|
|
1098
|
+
decision_entry_ids: list[str] = []
|
|
1099
|
+
promotion_errors: list[str] = []
|
|
1100
|
+
for item in payload.facts:
|
|
1101
|
+
try:
|
|
1102
|
+
entry = write_fact_entry(
|
|
1103
|
+
paths=paths,
|
|
1104
|
+
task=task,
|
|
1105
|
+
source=source,
|
|
1106
|
+
title=item.title,
|
|
1107
|
+
summary=item.summary,
|
|
1108
|
+
paths_in_scope=item.paths,
|
|
1109
|
+
report_path=report_path,
|
|
1110
|
+
patch_path=patch_path,
|
|
1111
|
+
verify_artifact_path=verify_artifact_path,
|
|
1112
|
+
budget_artifact_path=budget_artifact_path,
|
|
1113
|
+
session_artifact_dir=session_artifact_dir,
|
|
1114
|
+
capture_artifact_path=capture_artifact_path,
|
|
1115
|
+
tags=["structured_capture", *item.tags],
|
|
1116
|
+
)
|
|
1117
|
+
except Exception as exc: # noqa: BLE001
|
|
1118
|
+
promotion_errors.append(f"failed to publish fact '{item.title}': {exc}")
|
|
1119
|
+
continue
|
|
1120
|
+
fact_entry_ids.append(entry.id)
|
|
1121
|
+
for item in payload.decisions:
|
|
1122
|
+
try:
|
|
1123
|
+
entry = write_decision_entry(
|
|
1124
|
+
paths=paths,
|
|
1125
|
+
task=task,
|
|
1126
|
+
source=source,
|
|
1127
|
+
decision_key=item.decision_key,
|
|
1128
|
+
title=item.title,
|
|
1129
|
+
summary=item.summary,
|
|
1130
|
+
status=item.status,
|
|
1131
|
+
paths_in_scope=item.paths,
|
|
1132
|
+
report_path=report_path,
|
|
1133
|
+
patch_path=patch_path,
|
|
1134
|
+
verify_artifact_path=verify_artifact_path,
|
|
1135
|
+
budget_artifact_path=budget_artifact_path,
|
|
1136
|
+
session_artifact_dir=session_artifact_dir,
|
|
1137
|
+
capture_artifact_path=capture_artifact_path,
|
|
1138
|
+
tags=["structured_capture", *item.tags],
|
|
1139
|
+
)
|
|
1140
|
+
except Exception as exc: # noqa: BLE001
|
|
1141
|
+
promotion_errors.append(f"failed to publish decision '{item.decision_key}': {exc}")
|
|
1142
|
+
continue
|
|
1143
|
+
decision_entry_ids.append(entry.id)
|
|
1144
|
+
|
|
1145
|
+
return _write_promotion_result(
|
|
1146
|
+
artifact_dir=artifact_dir,
|
|
1147
|
+
promotion_path=promotion_path,
|
|
1148
|
+
capture_valid=True,
|
|
1149
|
+
promotion_attempted=True,
|
|
1150
|
+
promotion_succeeded=not promotion_errors,
|
|
1151
|
+
promotion_skipped_reason=None,
|
|
1152
|
+
promotion_errors=promotion_errors,
|
|
1153
|
+
fact_entry_ids=fact_entry_ids,
|
|
1154
|
+
decision_entry_ids=decision_entry_ids,
|
|
1155
|
+
)
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
def mark_knowledge_capture_promotion_skipped(
|
|
1159
|
+
*,
|
|
1160
|
+
artifact_dir: Path,
|
|
1161
|
+
reason: str,
|
|
1162
|
+
) -> KnowledgeCapturePromotionResult | None:
|
|
1163
|
+
if not artifact_dir.exists():
|
|
1164
|
+
return None
|
|
1165
|
+
existing = _load_promotion_result(artifact_dir)
|
|
1166
|
+
if existing is not None and (
|
|
1167
|
+
existing.promotion_succeeded
|
|
1168
|
+
or existing.promotion_attempted
|
|
1169
|
+
or existing.promotion_skipped_reason is not None
|
|
1170
|
+
):
|
|
1171
|
+
return existing
|
|
1172
|
+
validation_path = artifact_dir / "validation.json"
|
|
1173
|
+
capture_valid = False
|
|
1174
|
+
if validation_path.exists():
|
|
1175
|
+
try:
|
|
1176
|
+
payload = _load_json_payload(validation_path)
|
|
1177
|
+
capture_valid = bool(payload.get("valid"))
|
|
1178
|
+
except (OSError, UnicodeDecodeError, ValueError, json.JSONDecodeError):
|
|
1179
|
+
capture_valid = False
|
|
1180
|
+
return _write_promotion_result(
|
|
1181
|
+
artifact_dir=artifact_dir,
|
|
1182
|
+
promotion_path=artifact_dir / "promotion.json",
|
|
1183
|
+
capture_valid=capture_valid,
|
|
1184
|
+
promotion_attempted=False,
|
|
1185
|
+
promotion_succeeded=False,
|
|
1186
|
+
promotion_skipped_reason=reason,
|
|
1187
|
+
promotion_errors=(),
|
|
1188
|
+
fact_entry_ids=(),
|
|
1189
|
+
decision_entry_ids=(),
|
|
1190
|
+
)
|