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,1409 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
import threading
|
|
7
|
+
from contextlib import suppress
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Literal
|
|
12
|
+
from uuid import uuid4
|
|
13
|
+
|
|
14
|
+
from .atomic_io import atomic_write_text
|
|
15
|
+
from .forge import RunPaths
|
|
16
|
+
|
|
17
|
+
KNOWLEDGE_INDEX_SCHEMA_VERSION = 5
|
|
18
|
+
KnowledgeEntryKind = Literal["task_attempt", "issue", "fact", "decision"]
|
|
19
|
+
_SUPPORTED_KNOWLEDGE_KINDS: tuple[KnowledgeEntryKind, ...] = (
|
|
20
|
+
"task_attempt",
|
|
21
|
+
"issue",
|
|
22
|
+
"fact",
|
|
23
|
+
"decision",
|
|
24
|
+
)
|
|
25
|
+
OPEN_ISSUE_STATUSES = frozenset({"open", "active", "blocked"})
|
|
26
|
+
RESOLVED_ISSUE_STATUSES = frozenset({"resolved", "closed"})
|
|
27
|
+
ACCEPTED_TASK_ATTEMPT_STATUSES = frozenset({"accepted"})
|
|
28
|
+
PENDING_TASK_ATTEMPT_STATUSES = frozenset({"pending"})
|
|
29
|
+
REJECTED_TASK_ATTEMPT_STATUSES = frozenset({"rejected"})
|
|
30
|
+
_LIFECYCLE_DERIVED_ENTRY_KINDS = frozenset({"issue", "decision", "task_attempt"})
|
|
31
|
+
_KNOWLEDGE_INDEX_REBUILD_LOCK = threading.RLock()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _now_iso() -> str:
|
|
35
|
+
return datetime.now(UTC).replace(microsecond=0).isoformat()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _normalize_path_value(value: str) -> str:
|
|
39
|
+
cleaned = str(value).strip().replace("\\", "/")
|
|
40
|
+
while cleaned.startswith("./"):
|
|
41
|
+
cleaned = cleaned[2:]
|
|
42
|
+
return cleaned.rstrip("/")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _sanitize_component(value: str) -> str:
|
|
46
|
+
safe = "".join(c if c.isalnum() or c in {"-", "_"} else "_" for c in str(value).strip())
|
|
47
|
+
return safe or "item"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _timestamp_slug(value: str) -> str:
|
|
51
|
+
return (
|
|
52
|
+
value.replace("-", "")
|
|
53
|
+
.replace(":", "")
|
|
54
|
+
.replace("+00:00", "Z")
|
|
55
|
+
.replace("T", "T")
|
|
56
|
+
.replace(".", "")
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _repo_rel(root: Path, path: Path) -> str:
|
|
61
|
+
return os.fspath(path.resolve().relative_to(root.resolve()))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _trim_preview(text: str, *, max_chars: int = 160) -> str:
|
|
65
|
+
raw = " ".join(line.strip() for line in text.splitlines() if line.strip())
|
|
66
|
+
if len(raw) <= max_chars:
|
|
67
|
+
return raw
|
|
68
|
+
if max_chars <= 3:
|
|
69
|
+
return raw[:max_chars]
|
|
70
|
+
return raw[: max_chars - 3].rstrip() + "..."
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _body_preview(body: str) -> str:
|
|
74
|
+
lines = [
|
|
75
|
+
line.strip()
|
|
76
|
+
for line in body.splitlines()
|
|
77
|
+
if line.strip() and not line.strip().startswith("#")
|
|
78
|
+
]
|
|
79
|
+
return _trim_preview(" ".join(lines))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _frontmatter_scalar(value: Any) -> str:
|
|
83
|
+
if value is None:
|
|
84
|
+
return "null"
|
|
85
|
+
return json.dumps(str(value), ensure_ascii=False)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _parse_frontmatter_scalar(raw: str) -> Any:
|
|
89
|
+
text = raw.strip()
|
|
90
|
+
if not text:
|
|
91
|
+
return ""
|
|
92
|
+
try:
|
|
93
|
+
return json.loads(text)
|
|
94
|
+
except json.JSONDecodeError:
|
|
95
|
+
return text
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _serialize_frontmatter(data: dict[str, Any]) -> str:
|
|
99
|
+
lines = ["---"]
|
|
100
|
+
for key, value in data.items():
|
|
101
|
+
if isinstance(value, (list, tuple)):
|
|
102
|
+
lines.append(f"{key}:")
|
|
103
|
+
for item in value:
|
|
104
|
+
lines.append(f" - {_frontmatter_scalar(item)}")
|
|
105
|
+
continue
|
|
106
|
+
lines.append(f"{key}: {_frontmatter_scalar(value)}")
|
|
107
|
+
lines.append("---")
|
|
108
|
+
return "\n".join(lines)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _parse_frontmatter(text: str) -> tuple[dict[str, Any], str]:
|
|
112
|
+
lines = text.splitlines()
|
|
113
|
+
if not lines or lines[0].strip() != "---":
|
|
114
|
+
raise ValueError("knowledge entry is missing YAML frontmatter")
|
|
115
|
+
frontmatter_lines: list[str] = []
|
|
116
|
+
body_start = None
|
|
117
|
+
for idx in range(1, len(lines)):
|
|
118
|
+
if lines[idx].strip() == "---":
|
|
119
|
+
body_start = idx + 1
|
|
120
|
+
break
|
|
121
|
+
frontmatter_lines.append(lines[idx])
|
|
122
|
+
if body_start is None:
|
|
123
|
+
raise ValueError("knowledge entry frontmatter is not terminated")
|
|
124
|
+
|
|
125
|
+
data: dict[str, Any] = {}
|
|
126
|
+
current_list_key: str | None = None
|
|
127
|
+
for line in frontmatter_lines:
|
|
128
|
+
stripped = line.rstrip()
|
|
129
|
+
if stripped.startswith(" - "):
|
|
130
|
+
if current_list_key is None:
|
|
131
|
+
raise ValueError("knowledge entry list item missing key")
|
|
132
|
+
items = data.setdefault(current_list_key, [])
|
|
133
|
+
if not isinstance(items, list):
|
|
134
|
+
raise ValueError("knowledge entry frontmatter list state is invalid")
|
|
135
|
+
items.append(str(_parse_frontmatter_scalar(stripped[4:])))
|
|
136
|
+
continue
|
|
137
|
+
current_list_key = None
|
|
138
|
+
if ":" not in stripped:
|
|
139
|
+
raise ValueError(f"invalid knowledge frontmatter line: {stripped}")
|
|
140
|
+
key, raw_value = stripped.split(":", 1)
|
|
141
|
+
normalized_key = key.strip()
|
|
142
|
+
if not normalized_key:
|
|
143
|
+
raise ValueError("knowledge entry frontmatter key cannot be empty")
|
|
144
|
+
if raw_value.strip():
|
|
145
|
+
data[normalized_key] = _parse_frontmatter_scalar(raw_value)
|
|
146
|
+
continue
|
|
147
|
+
data[normalized_key] = []
|
|
148
|
+
current_list_key = normalized_key
|
|
149
|
+
|
|
150
|
+
body = "\n".join(lines[body_start:]).rstrip() + "\n"
|
|
151
|
+
return data, body
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass(frozen=True)
|
|
155
|
+
class KnowledgeEntry:
|
|
156
|
+
kind: KnowledgeEntryKind
|
|
157
|
+
id: str
|
|
158
|
+
title: str
|
|
159
|
+
created_at: str
|
|
160
|
+
task_id: str
|
|
161
|
+
source: str
|
|
162
|
+
status: str
|
|
163
|
+
result: str | None = None
|
|
164
|
+
paths: tuple[str, ...] = ()
|
|
165
|
+
related_tasks: tuple[str, ...] = ()
|
|
166
|
+
tags: tuple[str, ...] = ()
|
|
167
|
+
report_path: str | None = None
|
|
168
|
+
patch_path: str | None = None
|
|
169
|
+
verify_artifact_path: str | None = None
|
|
170
|
+
budget_artifact_path: str | None = None
|
|
171
|
+
session_artifact_dir: str | None = None
|
|
172
|
+
signature: str | None = None
|
|
173
|
+
decision_key: str | None = None
|
|
174
|
+
resolves: tuple[str, ...] = ()
|
|
175
|
+
capture_artifact_path: str | None = None
|
|
176
|
+
body: str = ""
|
|
177
|
+
file_path: Path | None = None
|
|
178
|
+
|
|
179
|
+
def to_frontmatter_payload(self) -> dict[str, Any]:
|
|
180
|
+
return {
|
|
181
|
+
"kind": self.kind,
|
|
182
|
+
"id": self.id,
|
|
183
|
+
"title": self.title,
|
|
184
|
+
"created_at": self.created_at,
|
|
185
|
+
"task_id": self.task_id,
|
|
186
|
+
"source": self.source,
|
|
187
|
+
"status": self.status,
|
|
188
|
+
"result": self.result,
|
|
189
|
+
"paths": list(self.paths),
|
|
190
|
+
"related_tasks": list(self.related_tasks),
|
|
191
|
+
"tags": list(self.tags),
|
|
192
|
+
"report_path": self.report_path,
|
|
193
|
+
"patch_path": self.patch_path,
|
|
194
|
+
"verify_artifact_path": self.verify_artifact_path,
|
|
195
|
+
"budget_artifact_path": self.budget_artifact_path,
|
|
196
|
+
"session_artifact_dir": self.session_artifact_dir,
|
|
197
|
+
"signature": self.signature,
|
|
198
|
+
"decision_key": self.decision_key,
|
|
199
|
+
"resolves": list(self.resolves),
|
|
200
|
+
"capture_artifact_path": self.capture_artifact_path,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
def to_markdown(self) -> str:
|
|
204
|
+
return (
|
|
205
|
+
_serialize_frontmatter(self.to_frontmatter_payload()) + "\n" + self.body.rstrip() + "\n"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
@classmethod
|
|
209
|
+
def from_markdown(cls, *, text: str, file_path: Path | None = None) -> KnowledgeEntry:
|
|
210
|
+
frontmatter, body = _parse_frontmatter(text)
|
|
211
|
+
kind = str(frontmatter.get("kind") or "").strip()
|
|
212
|
+
if kind not in _SUPPORTED_KNOWLEDGE_KINDS:
|
|
213
|
+
raise ValueError(f"unsupported knowledge kind: {kind}")
|
|
214
|
+
return cls(
|
|
215
|
+
kind=kind, # type: ignore[arg-type]
|
|
216
|
+
id=str(frontmatter.get("id") or "").strip(),
|
|
217
|
+
title=str(frontmatter.get("title") or "").strip(),
|
|
218
|
+
created_at=str(frontmatter.get("created_at") or "").strip(),
|
|
219
|
+
task_id=str(frontmatter.get("task_id") or "").strip(),
|
|
220
|
+
source=str(frontmatter.get("source") or "").strip(),
|
|
221
|
+
status=_normalize_task_attempt_status(
|
|
222
|
+
kind=kind,
|
|
223
|
+
status=str(frontmatter.get("status") or "").strip(),
|
|
224
|
+
result=_optional_text(frontmatter.get("result")),
|
|
225
|
+
),
|
|
226
|
+
result=_normalize_task_attempt_result(
|
|
227
|
+
kind=kind,
|
|
228
|
+
result=_optional_text(frontmatter.get("result")),
|
|
229
|
+
status=str(frontmatter.get("status") or "").strip(),
|
|
230
|
+
),
|
|
231
|
+
paths=tuple(
|
|
232
|
+
str(item).strip() for item in frontmatter.get("paths") or [] if str(item).strip()
|
|
233
|
+
),
|
|
234
|
+
related_tasks=tuple(
|
|
235
|
+
str(item).strip()
|
|
236
|
+
for item in frontmatter.get("related_tasks") or []
|
|
237
|
+
if str(item).strip()
|
|
238
|
+
),
|
|
239
|
+
tags=tuple(
|
|
240
|
+
str(item).strip() for item in frontmatter.get("tags") or [] if str(item).strip()
|
|
241
|
+
),
|
|
242
|
+
report_path=_optional_text(frontmatter.get("report_path")),
|
|
243
|
+
patch_path=_optional_text(frontmatter.get("patch_path")),
|
|
244
|
+
verify_artifact_path=_optional_text(frontmatter.get("verify_artifact_path")),
|
|
245
|
+
budget_artifact_path=_optional_text(frontmatter.get("budget_artifact_path")),
|
|
246
|
+
session_artifact_dir=_optional_text(frontmatter.get("session_artifact_dir")),
|
|
247
|
+
signature=_optional_text(frontmatter.get("signature")),
|
|
248
|
+
decision_key=_optional_text(frontmatter.get("decision_key")),
|
|
249
|
+
resolves=tuple(
|
|
250
|
+
str(item).strip() for item in frontmatter.get("resolves") or [] if str(item).strip()
|
|
251
|
+
),
|
|
252
|
+
capture_artifact_path=_optional_text(frontmatter.get("capture_artifact_path")),
|
|
253
|
+
body=body,
|
|
254
|
+
file_path=file_path,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@dataclass(frozen=True)
|
|
259
|
+
class KnowledgeIndexEntry:
|
|
260
|
+
id: str
|
|
261
|
+
kind: KnowledgeEntryKind
|
|
262
|
+
title: str
|
|
263
|
+
preview: str
|
|
264
|
+
run_id: str
|
|
265
|
+
task_id: str
|
|
266
|
+
source: str
|
|
267
|
+
status: str
|
|
268
|
+
tags: tuple[str, ...]
|
|
269
|
+
paths: tuple[str, ...]
|
|
270
|
+
related_tasks: tuple[str, ...]
|
|
271
|
+
created_at: str
|
|
272
|
+
knowledge_file_path: str
|
|
273
|
+
result: str | None = None
|
|
274
|
+
signature: str | None = None
|
|
275
|
+
decision_key: str | None = None
|
|
276
|
+
resolves: tuple[str, ...] = ()
|
|
277
|
+
capture_artifact_path: str | None = None
|
|
278
|
+
effective_status: str = ""
|
|
279
|
+
|
|
280
|
+
def to_payload(self) -> dict[str, Any]:
|
|
281
|
+
return {
|
|
282
|
+
"id": self.id,
|
|
283
|
+
"kind": self.kind,
|
|
284
|
+
"title": self.title,
|
|
285
|
+
"preview": self.preview,
|
|
286
|
+
"run_id": self.run_id,
|
|
287
|
+
"task_id": self.task_id,
|
|
288
|
+
"source": self.source,
|
|
289
|
+
"status": self.status,
|
|
290
|
+
"result": self.result,
|
|
291
|
+
"tags": list(self.tags),
|
|
292
|
+
"paths": list(self.paths),
|
|
293
|
+
"related_tasks": list(self.related_tasks),
|
|
294
|
+
"created_at": self.created_at,
|
|
295
|
+
"knowledge_file_path": self.knowledge_file_path,
|
|
296
|
+
"signature": self.signature,
|
|
297
|
+
"decision_key": self.decision_key,
|
|
298
|
+
"resolves": list(self.resolves),
|
|
299
|
+
"capture_artifact_path": self.capture_artifact_path,
|
|
300
|
+
"effective_status": self.effective_status or self.status,
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@dataclass(frozen=True)
|
|
305
|
+
class KnowledgeIndex:
|
|
306
|
+
path: Path
|
|
307
|
+
entries: tuple[KnowledgeIndexEntry, ...]
|
|
308
|
+
generated_at: str
|
|
309
|
+
invalid_entries: tuple[KnowledgeIndexInvalidEntry, ...] = ()
|
|
310
|
+
|
|
311
|
+
def to_payload(self) -> dict[str, Any]:
|
|
312
|
+
return {
|
|
313
|
+
"schema_version": KNOWLEDGE_INDEX_SCHEMA_VERSION,
|
|
314
|
+
"generated_at": self.generated_at,
|
|
315
|
+
"entry_count": len(self.entries),
|
|
316
|
+
"invalid_entry_count": len(self.invalid_entries),
|
|
317
|
+
"invalid_entries": [entry.to_payload() for entry in self.invalid_entries],
|
|
318
|
+
"entries": [entry.to_payload() for entry in self.entries],
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@dataclass(frozen=True)
|
|
323
|
+
class KnowledgeIndexInvalidEntry:
|
|
324
|
+
knowledge_file_path: str
|
|
325
|
+
error: str
|
|
326
|
+
|
|
327
|
+
def to_payload(self) -> dict[str, Any]:
|
|
328
|
+
return {
|
|
329
|
+
"knowledge_file_path": self.knowledge_file_path,
|
|
330
|
+
"error": self.error,
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _optional_text(value: Any) -> str | None:
|
|
335
|
+
text = str(value or "").strip()
|
|
336
|
+
return text or None
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _normalize_optional_signature(value: Any) -> str | None:
|
|
340
|
+
text = str(value or "").strip()
|
|
341
|
+
return text or None
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _normalize_resolve_ids(values: list[str] | tuple[str, ...] | None) -> tuple[str, ...]:
|
|
345
|
+
return tuple(dict.fromkeys(str(item).strip() for item in (values or []) if str(item).strip()))
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _knowledge_index_payload_is_compatible(raw: dict[str, Any]) -> bool:
|
|
349
|
+
if raw.get("schema_version") != KNOWLEDGE_INDEX_SCHEMA_VERSION:
|
|
350
|
+
return False
|
|
351
|
+
entries = raw.get("entries")
|
|
352
|
+
if not isinstance(entries, list):
|
|
353
|
+
return False
|
|
354
|
+
for item in entries:
|
|
355
|
+
if not isinstance(item, dict):
|
|
356
|
+
return False
|
|
357
|
+
kind = str(item.get("kind") or "").strip()
|
|
358
|
+
if (
|
|
359
|
+
kind in _LIFECYCLE_DERIVED_ENTRY_KINDS
|
|
360
|
+
and not str(item.get("effective_status") or "").strip()
|
|
361
|
+
):
|
|
362
|
+
return False
|
|
363
|
+
return True
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _normalize_task_attempt_result(*, kind: str, result: str | None, status: str) -> str | None:
|
|
367
|
+
if kind != "task_attempt":
|
|
368
|
+
return result
|
|
369
|
+
text = str(result or "").strip()
|
|
370
|
+
if text:
|
|
371
|
+
return text
|
|
372
|
+
legacy_status = str(status or "").strip()
|
|
373
|
+
if legacy_status in {"success", "failure"}:
|
|
374
|
+
return legacy_status
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _normalize_task_attempt_status(*, kind: str, status: str, result: str | None) -> str:
|
|
379
|
+
if kind != "task_attempt":
|
|
380
|
+
return status
|
|
381
|
+
text = str(status or "").strip()
|
|
382
|
+
if text in {"accepted", "pending", "rejected"}:
|
|
383
|
+
return text
|
|
384
|
+
if text == "success":
|
|
385
|
+
return "accepted"
|
|
386
|
+
if text == "failure":
|
|
387
|
+
return "rejected"
|
|
388
|
+
normalized_result = str(result or "").strip()
|
|
389
|
+
if normalized_result == "success":
|
|
390
|
+
return "accepted"
|
|
391
|
+
if normalized_result == "failure":
|
|
392
|
+
return "rejected"
|
|
393
|
+
return text
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def derive_run_id_from_knowledge_file_path(knowledge_file_path: str | Path | None) -> str:
|
|
397
|
+
if knowledge_file_path is None:
|
|
398
|
+
return ""
|
|
399
|
+
parts = Path(str(knowledge_file_path)).parts
|
|
400
|
+
for index in range(len(parts) - 2):
|
|
401
|
+
if parts[index] == ".alysis" and parts[index + 1] == "runs":
|
|
402
|
+
return parts[index + 2]
|
|
403
|
+
return ""
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def is_effectively_open_status(status: str | None) -> bool:
|
|
407
|
+
return str(status or "").strip() in OPEN_ISSUE_STATUSES
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def is_effectively_accepted_task_attempt(status: str | None) -> bool:
|
|
411
|
+
return str(status or "").strip() in ACCEPTED_TASK_ATTEMPT_STATUSES
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _derive_effective_issue_statuses(records: list[KnowledgeEntry]) -> dict[str, str]:
|
|
415
|
+
issues = [record for record in records if record.kind == "issue"]
|
|
416
|
+
effective_statuses = {record.id: record.status for record in issues}
|
|
417
|
+
issue_ids = set(effective_statuses)
|
|
418
|
+
for record in issues:
|
|
419
|
+
if record.status not in RESOLVED_ISSUE_STATUSES or not record.resolves:
|
|
420
|
+
continue
|
|
421
|
+
for resolved_id in record.resolves:
|
|
422
|
+
if resolved_id in issue_ids and is_effectively_open_status(
|
|
423
|
+
effective_statuses.get(resolved_id)
|
|
424
|
+
):
|
|
425
|
+
effective_statuses[resolved_id] = "resolved"
|
|
426
|
+
return effective_statuses
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _derive_effective_decision_statuses(records: list[KnowledgeEntry]) -> dict[str, str]:
|
|
430
|
+
effective_statuses = {
|
|
431
|
+
record.id: record.status for record in records if record.kind == "decision"
|
|
432
|
+
}
|
|
433
|
+
decisions_by_key: dict[str, list[KnowledgeEntry]] = {}
|
|
434
|
+
for record in records:
|
|
435
|
+
if record.kind != "decision" or not record.decision_key:
|
|
436
|
+
continue
|
|
437
|
+
decisions_by_key.setdefault(record.decision_key, []).append(record)
|
|
438
|
+
for decision_records in decisions_by_key.values():
|
|
439
|
+
latest = max(
|
|
440
|
+
decision_records,
|
|
441
|
+
key=lambda record: (
|
|
442
|
+
record.created_at,
|
|
443
|
+
record.id,
|
|
444
|
+
record.file_path.as_posix() if record.file_path is not None else "",
|
|
445
|
+
),
|
|
446
|
+
)
|
|
447
|
+
for record in decision_records:
|
|
448
|
+
effective_statuses[record.id] = latest.status
|
|
449
|
+
return effective_statuses
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _derive_effective_task_attempt_statuses(records: list[KnowledgeEntry]) -> dict[str, str]:
|
|
453
|
+
attempts = [record for record in records if record.kind == "task_attempt"]
|
|
454
|
+
effective_statuses = {record.id: record.status for record in attempts}
|
|
455
|
+
ordered_attempts = sorted(
|
|
456
|
+
attempts,
|
|
457
|
+
key=lambda record: (
|
|
458
|
+
record.created_at,
|
|
459
|
+
record.id,
|
|
460
|
+
record.file_path.as_posix() if record.file_path is not None else "",
|
|
461
|
+
),
|
|
462
|
+
)
|
|
463
|
+
attempt_ids = set(effective_statuses)
|
|
464
|
+
for record in ordered_attempts:
|
|
465
|
+
if record.kind != "task_attempt" or not record.resolves:
|
|
466
|
+
continue
|
|
467
|
+
for resolved_id in record.resolves:
|
|
468
|
+
if resolved_id in attempt_ids:
|
|
469
|
+
effective_statuses[resolved_id] = record.status
|
|
470
|
+
return effective_statuses
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _entry_kind_dir(paths: RunPaths, kind: KnowledgeEntryKind) -> Path:
|
|
474
|
+
if kind == "task_attempt":
|
|
475
|
+
return paths.knowledge_task_attempts_dir
|
|
476
|
+
if kind == "issue":
|
|
477
|
+
return paths.knowledge_issues_dir
|
|
478
|
+
if kind == "fact":
|
|
479
|
+
return paths.knowledge_facts_dir
|
|
480
|
+
return paths.knowledge_decisions_dir
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _knowledge_bucket_dir(paths: RunPaths, *, kind: KnowledgeEntryKind, task_id: str) -> Path:
|
|
484
|
+
bucket = _entry_kind_dir(paths, kind)
|
|
485
|
+
task_component = _sanitize_component(task_id or "general")
|
|
486
|
+
directory = bucket / task_component
|
|
487
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
488
|
+
return directory
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _write_text_atomic(path: Path, text: str) -> None:
|
|
492
|
+
atomic_write_text(path, text)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _unique_entry_identity(
|
|
496
|
+
*, kind: KnowledgeEntryKind, task_id: str, source: str, created_at: str
|
|
497
|
+
) -> tuple[str, str]:
|
|
498
|
+
suffix = uuid4().hex[:8]
|
|
499
|
+
identity = f"{kind}-{_sanitize_component(task_id)}-{_sanitize_component(source)}-{_timestamp_slug(created_at)}-{suffix}"
|
|
500
|
+
filename_stem = f"{_timestamp_slug(created_at)}_{_sanitize_component(source)}_{suffix}"
|
|
501
|
+
return identity, filename_stem
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _publish_markdown_entry_atomic(*, directory: Path, stem: str, markdown_text: str) -> Path:
|
|
505
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
506
|
+
final_path = directory / f"{stem}.md"
|
|
507
|
+
if final_path.exists():
|
|
508
|
+
raise FileExistsError(f"knowledge entry already exists: {final_path}")
|
|
509
|
+
fd, temp_name = tempfile.mkstemp(
|
|
510
|
+
dir=directory,
|
|
511
|
+
prefix=f".{stem}.",
|
|
512
|
+
suffix=".tmp",
|
|
513
|
+
text=True,
|
|
514
|
+
)
|
|
515
|
+
temp_path = Path(temp_name)
|
|
516
|
+
try:
|
|
517
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
518
|
+
handle.write(markdown_text)
|
|
519
|
+
os.replace(temp_path, final_path)
|
|
520
|
+
finally:
|
|
521
|
+
with suppress(FileNotFoundError):
|
|
522
|
+
temp_path.unlink()
|
|
523
|
+
return final_path
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def list_workspace_knowledge_entry_paths(root: Path) -> list[Path]:
|
|
527
|
+
knowledge_root = root.resolve() / ".alysis" / "runs"
|
|
528
|
+
if not knowledge_root.exists():
|
|
529
|
+
return []
|
|
530
|
+
paths: list[Path] = []
|
|
531
|
+
for kind_dir_name in ("task_attempts", "issues", "facts", "decisions"):
|
|
532
|
+
for path in sorted(knowledge_root.glob(f"*/knowledge/{kind_dir_name}/**/*.md")):
|
|
533
|
+
if path.is_file():
|
|
534
|
+
paths.append(path)
|
|
535
|
+
return paths
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def load_knowledge_entry(path: Path) -> KnowledgeEntry:
|
|
539
|
+
return KnowledgeEntry.from_markdown(
|
|
540
|
+
text=path.read_text(encoding="utf-8"),
|
|
541
|
+
file_path=path,
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _invalid_entry_error(exc: Exception) -> str:
|
|
546
|
+
return _trim_preview(f"{type(exc).__name__}: {exc}", max_chars=240)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def rebuild_knowledge_index(paths: RunPaths) -> KnowledgeIndex:
|
|
550
|
+
"""Rebuild the shared workspace index without concurrent replace races."""
|
|
551
|
+
with _KNOWLEDGE_INDEX_REBUILD_LOCK:
|
|
552
|
+
return _rebuild_knowledge_index_unlocked(paths)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _rebuild_knowledge_index_unlocked(paths: RunPaths) -> KnowledgeIndex:
|
|
556
|
+
records: list[KnowledgeEntry] = []
|
|
557
|
+
invalid_entries: list[KnowledgeIndexInvalidEntry] = []
|
|
558
|
+
for path in list_workspace_knowledge_entry_paths(paths.root):
|
|
559
|
+
try:
|
|
560
|
+
record = load_knowledge_entry(path)
|
|
561
|
+
except (OSError, UnicodeDecodeError, ValueError) as exc:
|
|
562
|
+
invalid_entries.append(
|
|
563
|
+
KnowledgeIndexInvalidEntry(
|
|
564
|
+
knowledge_file_path=_repo_rel(paths.root, path),
|
|
565
|
+
error=_invalid_entry_error(exc),
|
|
566
|
+
)
|
|
567
|
+
)
|
|
568
|
+
continue
|
|
569
|
+
records.append(record)
|
|
570
|
+
effective_issue_statuses = _derive_effective_issue_statuses(records)
|
|
571
|
+
effective_decision_statuses = _derive_effective_decision_statuses(records)
|
|
572
|
+
effective_task_attempt_statuses = _derive_effective_task_attempt_statuses(records)
|
|
573
|
+
entries: list[KnowledgeIndexEntry] = []
|
|
574
|
+
for record in records:
|
|
575
|
+
if record.file_path is None:
|
|
576
|
+
continue
|
|
577
|
+
entries.append(
|
|
578
|
+
KnowledgeIndexEntry(
|
|
579
|
+
id=record.id,
|
|
580
|
+
kind=record.kind,
|
|
581
|
+
title=record.title,
|
|
582
|
+
preview=_body_preview(record.body),
|
|
583
|
+
run_id=derive_run_id_from_knowledge_file_path(
|
|
584
|
+
_repo_rel(paths.root, record.file_path)
|
|
585
|
+
),
|
|
586
|
+
task_id=record.task_id,
|
|
587
|
+
source=record.source,
|
|
588
|
+
status=record.status,
|
|
589
|
+
result=record.result,
|
|
590
|
+
tags=tuple(record.tags),
|
|
591
|
+
paths=tuple(_normalize_path_value(item) for item in record.paths if item.strip()),
|
|
592
|
+
related_tasks=tuple(record.related_tasks),
|
|
593
|
+
created_at=record.created_at,
|
|
594
|
+
knowledge_file_path=_repo_rel(paths.root, record.file_path),
|
|
595
|
+
signature=record.signature,
|
|
596
|
+
decision_key=record.decision_key,
|
|
597
|
+
resolves=record.resolves,
|
|
598
|
+
capture_artifact_path=record.capture_artifact_path,
|
|
599
|
+
effective_status=(
|
|
600
|
+
effective_task_attempt_statuses.get(record.id)
|
|
601
|
+
or effective_issue_statuses.get(record.id)
|
|
602
|
+
or effective_decision_statuses.get(record.id)
|
|
603
|
+
or record.status
|
|
604
|
+
),
|
|
605
|
+
)
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
entries.sort(
|
|
609
|
+
key=lambda entry: (
|
|
610
|
+
entry.created_at,
|
|
611
|
+
entry.kind,
|
|
612
|
+
entry.id,
|
|
613
|
+
entry.knowledge_file_path,
|
|
614
|
+
),
|
|
615
|
+
reverse=True,
|
|
616
|
+
)
|
|
617
|
+
invalid_entries.sort(
|
|
618
|
+
key=lambda entry: (
|
|
619
|
+
entry.knowledge_file_path,
|
|
620
|
+
entry.error,
|
|
621
|
+
)
|
|
622
|
+
)
|
|
623
|
+
index = KnowledgeIndex(
|
|
624
|
+
path=paths.knowledge_index_path,
|
|
625
|
+
entries=tuple(entries),
|
|
626
|
+
generated_at=_now_iso(),
|
|
627
|
+
invalid_entries=tuple(invalid_entries),
|
|
628
|
+
)
|
|
629
|
+
_write_text_atomic(
|
|
630
|
+
paths.knowledge_index_path,
|
|
631
|
+
json.dumps(index.to_payload(), indent=2, sort_keys=True) + "\n",
|
|
632
|
+
)
|
|
633
|
+
return index
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def load_knowledge_index(paths: RunPaths, *, rebuild: bool = False) -> KnowledgeIndex:
|
|
637
|
+
"""Load the shared index without racing a Windows atomic replacement."""
|
|
638
|
+
with _KNOWLEDGE_INDEX_REBUILD_LOCK:
|
|
639
|
+
return _load_knowledge_index_unlocked(paths, rebuild=rebuild)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _load_knowledge_index_unlocked(paths: RunPaths, *, rebuild: bool = False) -> KnowledgeIndex:
|
|
643
|
+
if rebuild or not paths.knowledge_index_path.exists():
|
|
644
|
+
return rebuild_knowledge_index(paths)
|
|
645
|
+
try:
|
|
646
|
+
raw = json.loads(paths.knowledge_index_path.read_text(encoding="utf-8"))
|
|
647
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError):
|
|
648
|
+
return rebuild_knowledge_index(paths)
|
|
649
|
+
if not isinstance(raw, dict):
|
|
650
|
+
return rebuild_knowledge_index(paths)
|
|
651
|
+
if not _knowledge_index_payload_is_compatible(raw):
|
|
652
|
+
return rebuild_knowledge_index(paths)
|
|
653
|
+
try:
|
|
654
|
+
entries = tuple(
|
|
655
|
+
KnowledgeIndexEntry(
|
|
656
|
+
id=str(item.get("id") or "").strip(),
|
|
657
|
+
kind=str(item.get("kind") or "").strip(), # type: ignore[arg-type]
|
|
658
|
+
title=str(item.get("title") or "").strip(),
|
|
659
|
+
preview=str(item.get("preview") or "").strip(),
|
|
660
|
+
run_id=(
|
|
661
|
+
str(item.get("run_id") or "").strip()
|
|
662
|
+
or derive_run_id_from_knowledge_file_path(item.get("knowledge_file_path"))
|
|
663
|
+
),
|
|
664
|
+
task_id=str(item.get("task_id") or "").strip(),
|
|
665
|
+
source=str(item.get("source") or "").strip(),
|
|
666
|
+
status=_normalize_task_attempt_status(
|
|
667
|
+
kind=str(item.get("kind") or "").strip(),
|
|
668
|
+
status=str(item.get("status") or "").strip(),
|
|
669
|
+
result=_optional_text(item.get("result")),
|
|
670
|
+
),
|
|
671
|
+
result=_normalize_task_attempt_result(
|
|
672
|
+
kind=str(item.get("kind") or "").strip(),
|
|
673
|
+
result=_optional_text(item.get("result")),
|
|
674
|
+
status=str(item.get("status") or "").strip(),
|
|
675
|
+
),
|
|
676
|
+
tags=tuple(str(tag).strip() for tag in item.get("tags") or [] if str(tag).strip()),
|
|
677
|
+
paths=tuple(
|
|
678
|
+
str(path).strip() for path in item.get("paths") or [] if str(path).strip()
|
|
679
|
+
),
|
|
680
|
+
related_tasks=tuple(
|
|
681
|
+
str(task_id).strip()
|
|
682
|
+
for task_id in item.get("related_tasks") or []
|
|
683
|
+
if str(task_id).strip()
|
|
684
|
+
),
|
|
685
|
+
created_at=str(item.get("created_at") or "").strip(),
|
|
686
|
+
knowledge_file_path=str(item.get("knowledge_file_path") or "").strip(),
|
|
687
|
+
signature=_normalize_optional_signature(item.get("signature")),
|
|
688
|
+
decision_key=_optional_text(item.get("decision_key")),
|
|
689
|
+
resolves=_normalize_resolve_ids(item.get("resolves")),
|
|
690
|
+
capture_artifact_path=_optional_text(item.get("capture_artifact_path")),
|
|
691
|
+
effective_status=_normalize_task_attempt_status(
|
|
692
|
+
kind=str(item.get("kind") or "").strip(),
|
|
693
|
+
status=str(item.get("effective_status") or item.get("status") or "").strip(),
|
|
694
|
+
result=_optional_text(item.get("result")),
|
|
695
|
+
),
|
|
696
|
+
)
|
|
697
|
+
for item in raw.get("entries") or []
|
|
698
|
+
if isinstance(item, dict)
|
|
699
|
+
)
|
|
700
|
+
invalid_entries = tuple(
|
|
701
|
+
KnowledgeIndexInvalidEntry(
|
|
702
|
+
knowledge_file_path=str(item.get("knowledge_file_path") or "").strip(),
|
|
703
|
+
error=str(item.get("error") or "").strip(),
|
|
704
|
+
)
|
|
705
|
+
for item in raw.get("invalid_entries") or []
|
|
706
|
+
if isinstance(item, dict)
|
|
707
|
+
)
|
|
708
|
+
except AttributeError:
|
|
709
|
+
return rebuild_knowledge_index(paths)
|
|
710
|
+
return KnowledgeIndex(
|
|
711
|
+
path=paths.knowledge_index_path,
|
|
712
|
+
entries=entries,
|
|
713
|
+
generated_at=str(raw.get("generated_at") or "").strip(),
|
|
714
|
+
invalid_entries=invalid_entries,
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def load_task_attempt_entries_for_task(
|
|
719
|
+
*, paths: RunPaths, task_id: str
|
|
720
|
+
) -> tuple[KnowledgeEntry, ...]:
|
|
721
|
+
directory = paths.knowledge_task_attempts_dir / str(task_id).strip()
|
|
722
|
+
if not directory.exists():
|
|
723
|
+
return ()
|
|
724
|
+
entries: list[KnowledgeEntry] = []
|
|
725
|
+
for path in sorted(directory.glob("*.md")):
|
|
726
|
+
if not path.is_file():
|
|
727
|
+
continue
|
|
728
|
+
try:
|
|
729
|
+
entry = load_knowledge_entry(path)
|
|
730
|
+
except (OSError, UnicodeDecodeError, ValueError):
|
|
731
|
+
continue
|
|
732
|
+
if entry.kind == "task_attempt":
|
|
733
|
+
entries.append(entry)
|
|
734
|
+
return tuple(entries)
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def find_latest_task_attempt_entry(
|
|
738
|
+
*,
|
|
739
|
+
paths: RunPaths,
|
|
740
|
+
task_id: str,
|
|
741
|
+
source: str | None = None,
|
|
742
|
+
include_resolution_entries: bool = False,
|
|
743
|
+
) -> KnowledgeEntry | None:
|
|
744
|
+
entries = load_task_attempt_entries_for_task(paths=paths, task_id=task_id)
|
|
745
|
+
candidates = [
|
|
746
|
+
entry
|
|
747
|
+
for entry in entries
|
|
748
|
+
if (source is None or entry.source == source)
|
|
749
|
+
and (include_resolution_entries or not entry.resolves)
|
|
750
|
+
]
|
|
751
|
+
if not candidates:
|
|
752
|
+
return None
|
|
753
|
+
return max(
|
|
754
|
+
candidates,
|
|
755
|
+
key=lambda entry: (
|
|
756
|
+
entry.created_at,
|
|
757
|
+
entry.id,
|
|
758
|
+
entry.file_path.as_posix() if entry.file_path is not None else "",
|
|
759
|
+
),
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def _has_task_attempt_resolution(
|
|
764
|
+
*,
|
|
765
|
+
paths: RunPaths,
|
|
766
|
+
task_id: str,
|
|
767
|
+
resolved_attempt_id: str,
|
|
768
|
+
acceptance_state: str,
|
|
769
|
+
) -> bool:
|
|
770
|
+
return any(
|
|
771
|
+
resolved_attempt_id in entry.resolves and entry.status == acceptance_state
|
|
772
|
+
for entry in load_task_attempt_entries_for_task(paths=paths, task_id=task_id)
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def write_task_attempt_entry(
|
|
777
|
+
*,
|
|
778
|
+
paths: RunPaths,
|
|
779
|
+
task: dict[str, Any],
|
|
780
|
+
source: str,
|
|
781
|
+
result: str | None,
|
|
782
|
+
summary: str,
|
|
783
|
+
changed_files: list[str],
|
|
784
|
+
verify_summary: str | None,
|
|
785
|
+
report_path: Path | None,
|
|
786
|
+
patch_path: Path | None,
|
|
787
|
+
verify_artifact_path: Path | None,
|
|
788
|
+
budget_artifact_path: Path | None,
|
|
789
|
+
session_artifact_dir: Path | None,
|
|
790
|
+
acceptance_state: str | None = None,
|
|
791
|
+
resolves: list[str] | tuple[str, ...] | None = None,
|
|
792
|
+
created_at: str | None = None,
|
|
793
|
+
extra_tags: list[str] | tuple[str, ...] = (),
|
|
794
|
+
) -> KnowledgeEntry:
|
|
795
|
+
created = created_at or _now_iso()
|
|
796
|
+
task_id = str(task.get("id") or "").strip() or "task"
|
|
797
|
+
task_title = str(task.get("title") or "").strip() or task_id
|
|
798
|
+
entry_id, filename_stem = _unique_entry_identity(
|
|
799
|
+
kind="task_attempt",
|
|
800
|
+
task_id=task_id,
|
|
801
|
+
source=source,
|
|
802
|
+
created_at=created,
|
|
803
|
+
)
|
|
804
|
+
related_tasks = tuple(
|
|
805
|
+
item for item in (str(dep).strip() for dep in task.get("dependencies") or []) if item
|
|
806
|
+
)
|
|
807
|
+
raw_result = str(result or "").strip() or None
|
|
808
|
+
normalized_acceptance_state = str(acceptance_state or "").strip() or (
|
|
809
|
+
"accepted" if raw_result == "success" else "rejected"
|
|
810
|
+
)
|
|
811
|
+
resolve_ids = _normalize_resolve_ids(resolves)
|
|
812
|
+
fallback_paths = [
|
|
813
|
+
*[str(path).strip() for path in task.get("write_scope") or []],
|
|
814
|
+
*[str(path).strip() for path in task.get("estimated_files") or []],
|
|
815
|
+
]
|
|
816
|
+
effective_paths = tuple(
|
|
817
|
+
dict.fromkeys(
|
|
818
|
+
_normalize_path_value(path)
|
|
819
|
+
for path in (list(changed_files) or fallback_paths)
|
|
820
|
+
if _normalize_path_value(str(path))
|
|
821
|
+
)
|
|
822
|
+
)
|
|
823
|
+
body_lines = [
|
|
824
|
+
f"# Task Attempt: {task_id}",
|
|
825
|
+
"",
|
|
826
|
+
f"- Raw Result: `{raw_result}`" if raw_result else "- Raw Result: (resolution only)",
|
|
827
|
+
f"- Acceptance State: `{normalized_acceptance_state}`",
|
|
828
|
+
f"- Source: `{source}`",
|
|
829
|
+
f"- Summary: {summary or '(none)'}",
|
|
830
|
+
"",
|
|
831
|
+
"## Changed Files",
|
|
832
|
+
"",
|
|
833
|
+
]
|
|
834
|
+
if effective_paths:
|
|
835
|
+
body_lines.extend(f"- `{path}`" for path in effective_paths)
|
|
836
|
+
else:
|
|
837
|
+
body_lines.append("- (none)")
|
|
838
|
+
body_lines.extend(
|
|
839
|
+
[
|
|
840
|
+
"",
|
|
841
|
+
"## Lifecycle",
|
|
842
|
+
"",
|
|
843
|
+
]
|
|
844
|
+
)
|
|
845
|
+
if resolve_ids:
|
|
846
|
+
body_lines.extend(f"- Resolves: `{item}`" for item in resolve_ids)
|
|
847
|
+
else:
|
|
848
|
+
body_lines.append("- Resolves: (none)")
|
|
849
|
+
body_lines.extend(
|
|
850
|
+
[
|
|
851
|
+
"",
|
|
852
|
+
"## Verification",
|
|
853
|
+
"",
|
|
854
|
+
f"- Summary: {verify_summary or '(none)'}",
|
|
855
|
+
"",
|
|
856
|
+
"## Related Artifacts",
|
|
857
|
+
"",
|
|
858
|
+
f"- Report: `{_repo_rel(paths.root, report_path)}`"
|
|
859
|
+
if report_path
|
|
860
|
+
else "- Report: (none)",
|
|
861
|
+
f"- Patch: `{_repo_rel(paths.root, patch_path)}`" if patch_path else "- Patch: (none)",
|
|
862
|
+
(
|
|
863
|
+
f"- Verify Artifact: `{_repo_rel(paths.root, verify_artifact_path)}`"
|
|
864
|
+
if verify_artifact_path
|
|
865
|
+
else "- Verify Artifact: (none)"
|
|
866
|
+
),
|
|
867
|
+
(
|
|
868
|
+
f"- Budget Artifact: `{_repo_rel(paths.root, budget_artifact_path)}`"
|
|
869
|
+
if budget_artifact_path
|
|
870
|
+
else "- Budget Artifact: (none)"
|
|
871
|
+
),
|
|
872
|
+
(
|
|
873
|
+
f"- Session Artifacts: `{_repo_rel(paths.root, session_artifact_dir)}`"
|
|
874
|
+
if session_artifact_dir
|
|
875
|
+
else "- Session Artifacts: (none retained)"
|
|
876
|
+
),
|
|
877
|
+
]
|
|
878
|
+
)
|
|
879
|
+
entry = KnowledgeEntry(
|
|
880
|
+
kind="task_attempt",
|
|
881
|
+
id=entry_id,
|
|
882
|
+
title=f"{task_id}: {task_title}",
|
|
883
|
+
created_at=created,
|
|
884
|
+
task_id=task_id,
|
|
885
|
+
source=source,
|
|
886
|
+
status=normalized_acceptance_state,
|
|
887
|
+
result=raw_result,
|
|
888
|
+
paths=effective_paths,
|
|
889
|
+
related_tasks=related_tasks,
|
|
890
|
+
tags=tuple(
|
|
891
|
+
dict.fromkeys(
|
|
892
|
+
[
|
|
893
|
+
source,
|
|
894
|
+
normalized_acceptance_state,
|
|
895
|
+
*([raw_result] if raw_result else []),
|
|
896
|
+
*[str(tag).strip() for tag in extra_tags if str(tag).strip()],
|
|
897
|
+
]
|
|
898
|
+
)
|
|
899
|
+
),
|
|
900
|
+
report_path=_repo_rel(paths.root, report_path) if report_path else None,
|
|
901
|
+
patch_path=_repo_rel(paths.root, patch_path) if patch_path else None,
|
|
902
|
+
verify_artifact_path=(
|
|
903
|
+
_repo_rel(paths.root, verify_artifact_path) if verify_artifact_path else None
|
|
904
|
+
),
|
|
905
|
+
budget_artifact_path=(
|
|
906
|
+
_repo_rel(paths.root, budget_artifact_path) if budget_artifact_path else None
|
|
907
|
+
),
|
|
908
|
+
session_artifact_dir=(
|
|
909
|
+
_repo_rel(paths.root, session_artifact_dir) if session_artifact_dir else None
|
|
910
|
+
),
|
|
911
|
+
resolves=resolve_ids,
|
|
912
|
+
body="\n".join(body_lines).rstrip() + "\n",
|
|
913
|
+
)
|
|
914
|
+
directory = _knowledge_bucket_dir(paths, kind="task_attempt", task_id=task_id)
|
|
915
|
+
file_path = _publish_markdown_entry_atomic(
|
|
916
|
+
directory=directory,
|
|
917
|
+
stem=filename_stem,
|
|
918
|
+
markdown_text=entry.to_markdown(),
|
|
919
|
+
)
|
|
920
|
+
return KnowledgeEntry.from_markdown(
|
|
921
|
+
text=file_path.read_text(encoding="utf-8"), file_path=file_path
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def write_task_attempt_resolution_entry(
|
|
926
|
+
*,
|
|
927
|
+
paths: RunPaths,
|
|
928
|
+
task: dict[str, Any],
|
|
929
|
+
source: str,
|
|
930
|
+
acceptance_state: str,
|
|
931
|
+
resolved_attempt_id: str,
|
|
932
|
+
summary: str,
|
|
933
|
+
changed_files: list[str],
|
|
934
|
+
verify_summary: str | None,
|
|
935
|
+
report_path: Path | None,
|
|
936
|
+
patch_path: Path | None,
|
|
937
|
+
verify_artifact_path: Path | None,
|
|
938
|
+
budget_artifact_path: Path | None,
|
|
939
|
+
session_artifact_dir: Path | None,
|
|
940
|
+
created_at: str | None = None,
|
|
941
|
+
extra_tags: list[str] | tuple[str, ...] = (),
|
|
942
|
+
) -> KnowledgeEntry:
|
|
943
|
+
task_id = str(task.get("id") or "").strip() or "task"
|
|
944
|
+
if _has_task_attempt_resolution(
|
|
945
|
+
paths=paths,
|
|
946
|
+
task_id=task_id,
|
|
947
|
+
resolved_attempt_id=resolved_attempt_id,
|
|
948
|
+
acceptance_state=acceptance_state,
|
|
949
|
+
):
|
|
950
|
+
existing_entries = [
|
|
951
|
+
entry
|
|
952
|
+
for entry in load_task_attempt_entries_for_task(paths=paths, task_id=task_id)
|
|
953
|
+
if entry.source == source
|
|
954
|
+
and entry.status == acceptance_state
|
|
955
|
+
and resolved_attempt_id in entry.resolves
|
|
956
|
+
]
|
|
957
|
+
if existing_entries:
|
|
958
|
+
return max(
|
|
959
|
+
existing_entries,
|
|
960
|
+
key=lambda entry: (
|
|
961
|
+
entry.created_at,
|
|
962
|
+
entry.id,
|
|
963
|
+
entry.file_path.as_posix() if entry.file_path is not None else "",
|
|
964
|
+
),
|
|
965
|
+
)
|
|
966
|
+
return write_task_attempt_entry(
|
|
967
|
+
paths=paths,
|
|
968
|
+
task=task,
|
|
969
|
+
source=source,
|
|
970
|
+
result=None,
|
|
971
|
+
summary=summary,
|
|
972
|
+
changed_files=changed_files,
|
|
973
|
+
verify_summary=verify_summary,
|
|
974
|
+
report_path=report_path,
|
|
975
|
+
patch_path=patch_path,
|
|
976
|
+
verify_artifact_path=verify_artifact_path,
|
|
977
|
+
budget_artifact_path=budget_artifact_path,
|
|
978
|
+
session_artifact_dir=session_artifact_dir,
|
|
979
|
+
acceptance_state=acceptance_state,
|
|
980
|
+
resolves=[resolved_attempt_id],
|
|
981
|
+
created_at=created_at,
|
|
982
|
+
extra_tags=[*extra_tags, "acceptance_resolution"],
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
def write_issue_entry(
|
|
987
|
+
*,
|
|
988
|
+
paths: RunPaths,
|
|
989
|
+
task: dict[str, Any],
|
|
990
|
+
source: str,
|
|
991
|
+
title: str,
|
|
992
|
+
summary: str,
|
|
993
|
+
paths_in_scope: list[str] | tuple[str, ...] = (),
|
|
994
|
+
report_path: Path | None = None,
|
|
995
|
+
patch_path: Path | None = None,
|
|
996
|
+
verify_artifact_path: Path | None = None,
|
|
997
|
+
budget_artifact_path: Path | None = None,
|
|
998
|
+
session_artifact_dir: Path | None = None,
|
|
999
|
+
related_tasks: list[str] | tuple[str, ...] | None = None,
|
|
1000
|
+
tags: list[str] | tuple[str, ...] = (),
|
|
1001
|
+
created_at: str | None = None,
|
|
1002
|
+
status: str = "open",
|
|
1003
|
+
signature: str | None = None,
|
|
1004
|
+
resolves: list[str] | tuple[str, ...] | None = None,
|
|
1005
|
+
) -> KnowledgeEntry:
|
|
1006
|
+
task_id = str(task.get("id") or "").strip() or "task"
|
|
1007
|
+
related = related_tasks or [
|
|
1008
|
+
str(dep).strip() for dep in task.get("dependencies") or [] if str(dep).strip()
|
|
1009
|
+
]
|
|
1010
|
+
return write_issue_entry_for_task_id(
|
|
1011
|
+
paths=paths,
|
|
1012
|
+
task_id=task_id,
|
|
1013
|
+
source=source,
|
|
1014
|
+
title=title,
|
|
1015
|
+
summary=summary,
|
|
1016
|
+
paths_in_scope=paths_in_scope,
|
|
1017
|
+
report_path=report_path,
|
|
1018
|
+
patch_path=patch_path,
|
|
1019
|
+
verify_artifact_path=verify_artifact_path,
|
|
1020
|
+
budget_artifact_path=budget_artifact_path,
|
|
1021
|
+
session_artifact_dir=session_artifact_dir,
|
|
1022
|
+
related_tasks=related,
|
|
1023
|
+
tags=tags,
|
|
1024
|
+
created_at=created_at,
|
|
1025
|
+
status=status,
|
|
1026
|
+
signature=signature,
|
|
1027
|
+
resolves=resolves,
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
def write_issue_entry_for_task_id(
|
|
1032
|
+
*,
|
|
1033
|
+
paths: RunPaths,
|
|
1034
|
+
task_id: str,
|
|
1035
|
+
source: str,
|
|
1036
|
+
title: str,
|
|
1037
|
+
summary: str,
|
|
1038
|
+
paths_in_scope: list[str] | tuple[str, ...] = (),
|
|
1039
|
+
report_path: Path | None = None,
|
|
1040
|
+
patch_path: Path | None = None,
|
|
1041
|
+
verify_artifact_path: Path | None = None,
|
|
1042
|
+
budget_artifact_path: Path | None = None,
|
|
1043
|
+
session_artifact_dir: Path | None = None,
|
|
1044
|
+
related_tasks: list[str] | tuple[str, ...] | None = None,
|
|
1045
|
+
tags: list[str] | tuple[str, ...] = (),
|
|
1046
|
+
created_at: str | None = None,
|
|
1047
|
+
status: str = "open",
|
|
1048
|
+
signature: str | None = None,
|
|
1049
|
+
resolves: list[str] | tuple[str, ...] | None = None,
|
|
1050
|
+
) -> KnowledgeEntry:
|
|
1051
|
+
created = created_at or _now_iso()
|
|
1052
|
+
entry_id, filename_stem = _unique_entry_identity(
|
|
1053
|
+
kind="issue",
|
|
1054
|
+
task_id=task_id,
|
|
1055
|
+
source=source,
|
|
1056
|
+
created_at=created,
|
|
1057
|
+
)
|
|
1058
|
+
normalized_paths = tuple(
|
|
1059
|
+
dict.fromkeys(
|
|
1060
|
+
_normalize_path_value(path)
|
|
1061
|
+
for path in paths_in_scope
|
|
1062
|
+
if _normalize_path_value(str(path))
|
|
1063
|
+
)
|
|
1064
|
+
)
|
|
1065
|
+
related = tuple(
|
|
1066
|
+
dict.fromkeys(str(item).strip() for item in (related_tasks or []) if str(item).strip())
|
|
1067
|
+
)
|
|
1068
|
+
normalized_signature = _normalize_optional_signature(signature)
|
|
1069
|
+
resolve_ids = _normalize_resolve_ids(resolves)
|
|
1070
|
+
body_lines = [
|
|
1071
|
+
f"# Issue: {title}",
|
|
1072
|
+
"",
|
|
1073
|
+
f"- Status: `{status}`",
|
|
1074
|
+
f"- Source: `{source}`",
|
|
1075
|
+
f"- Signature: `{normalized_signature}`" if normalized_signature else "- Signature: (none)",
|
|
1076
|
+
f"- Summary: {summary or '(none)'}",
|
|
1077
|
+
"",
|
|
1078
|
+
"## Related Paths",
|
|
1079
|
+
"",
|
|
1080
|
+
]
|
|
1081
|
+
if normalized_paths:
|
|
1082
|
+
body_lines.extend(f"- `{path}`" for path in normalized_paths)
|
|
1083
|
+
else:
|
|
1084
|
+
body_lines.append("- (none)")
|
|
1085
|
+
body_lines.extend(["", "## Lifecycle", ""])
|
|
1086
|
+
if resolve_ids:
|
|
1087
|
+
body_lines.extend(f"- Resolves: `{item}`" for item in resolve_ids)
|
|
1088
|
+
else:
|
|
1089
|
+
body_lines.append("- Resolves: (none)")
|
|
1090
|
+
body_lines.extend(
|
|
1091
|
+
[
|
|
1092
|
+
"",
|
|
1093
|
+
"## Related Artifacts",
|
|
1094
|
+
"",
|
|
1095
|
+
f"- Report: `{_repo_rel(paths.root, report_path)}`"
|
|
1096
|
+
if report_path
|
|
1097
|
+
else "- Report: (none)",
|
|
1098
|
+
f"- Patch: `{_repo_rel(paths.root, patch_path)}`" if patch_path else "- Patch: (none)",
|
|
1099
|
+
(
|
|
1100
|
+
f"- Verify Artifact: `{_repo_rel(paths.root, verify_artifact_path)}`"
|
|
1101
|
+
if verify_artifact_path
|
|
1102
|
+
else "- Verify Artifact: (none)"
|
|
1103
|
+
),
|
|
1104
|
+
(
|
|
1105
|
+
f"- Budget Artifact: `{_repo_rel(paths.root, budget_artifact_path)}`"
|
|
1106
|
+
if budget_artifact_path
|
|
1107
|
+
else "- Budget Artifact: (none)"
|
|
1108
|
+
),
|
|
1109
|
+
(
|
|
1110
|
+
f"- Session Artifacts: `{_repo_rel(paths.root, session_artifact_dir)}`"
|
|
1111
|
+
if session_artifact_dir
|
|
1112
|
+
else "- Session Artifacts: (none retained)"
|
|
1113
|
+
),
|
|
1114
|
+
]
|
|
1115
|
+
)
|
|
1116
|
+
entry = KnowledgeEntry(
|
|
1117
|
+
kind="issue",
|
|
1118
|
+
id=entry_id,
|
|
1119
|
+
title=title,
|
|
1120
|
+
created_at=created,
|
|
1121
|
+
task_id=task_id,
|
|
1122
|
+
source=source,
|
|
1123
|
+
status=status,
|
|
1124
|
+
paths=normalized_paths,
|
|
1125
|
+
related_tasks=related,
|
|
1126
|
+
tags=tuple(
|
|
1127
|
+
dict.fromkeys([source, status, *[str(tag).strip() for tag in tags if str(tag).strip()]])
|
|
1128
|
+
),
|
|
1129
|
+
report_path=_repo_rel(paths.root, report_path) if report_path else None,
|
|
1130
|
+
patch_path=_repo_rel(paths.root, patch_path) if patch_path else None,
|
|
1131
|
+
verify_artifact_path=(
|
|
1132
|
+
_repo_rel(paths.root, verify_artifact_path) if verify_artifact_path else None
|
|
1133
|
+
),
|
|
1134
|
+
budget_artifact_path=(
|
|
1135
|
+
_repo_rel(paths.root, budget_artifact_path) if budget_artifact_path else None
|
|
1136
|
+
),
|
|
1137
|
+
session_artifact_dir=(
|
|
1138
|
+
_repo_rel(paths.root, session_artifact_dir) if session_artifact_dir else None
|
|
1139
|
+
),
|
|
1140
|
+
signature=normalized_signature,
|
|
1141
|
+
resolves=resolve_ids,
|
|
1142
|
+
body="\n".join(body_lines).rstrip() + "\n",
|
|
1143
|
+
)
|
|
1144
|
+
directory = _knowledge_bucket_dir(paths, kind="issue", task_id=task_id)
|
|
1145
|
+
file_path = _publish_markdown_entry_atomic(
|
|
1146
|
+
directory=directory,
|
|
1147
|
+
stem=filename_stem,
|
|
1148
|
+
markdown_text=entry.to_markdown(),
|
|
1149
|
+
)
|
|
1150
|
+
return KnowledgeEntry.from_markdown(
|
|
1151
|
+
text=file_path.read_text(encoding="utf-8"), file_path=file_path
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
def write_fact_entry(
|
|
1156
|
+
*,
|
|
1157
|
+
paths: RunPaths,
|
|
1158
|
+
task: dict[str, Any],
|
|
1159
|
+
source: str,
|
|
1160
|
+
title: str,
|
|
1161
|
+
summary: str,
|
|
1162
|
+
paths_in_scope: list[str] | tuple[str, ...] = (),
|
|
1163
|
+
report_path: Path | None = None,
|
|
1164
|
+
patch_path: Path | None = None,
|
|
1165
|
+
verify_artifact_path: Path | None = None,
|
|
1166
|
+
budget_artifact_path: Path | None = None,
|
|
1167
|
+
session_artifact_dir: Path | None = None,
|
|
1168
|
+
capture_artifact_path: Path | None = None,
|
|
1169
|
+
related_tasks: list[str] | tuple[str, ...] | None = None,
|
|
1170
|
+
tags: list[str] | tuple[str, ...] = (),
|
|
1171
|
+
created_at: str | None = None,
|
|
1172
|
+
) -> KnowledgeEntry:
|
|
1173
|
+
created = created_at or _now_iso()
|
|
1174
|
+
task_id = str(task.get("id") or "").strip() or "task"
|
|
1175
|
+
entry_id, filename_stem = _unique_entry_identity(
|
|
1176
|
+
kind="fact",
|
|
1177
|
+
task_id=task_id,
|
|
1178
|
+
source=source,
|
|
1179
|
+
created_at=created,
|
|
1180
|
+
)
|
|
1181
|
+
normalized_paths = tuple(
|
|
1182
|
+
dict.fromkeys(
|
|
1183
|
+
_normalize_path_value(path)
|
|
1184
|
+
for path in paths_in_scope
|
|
1185
|
+
if _normalize_path_value(str(path))
|
|
1186
|
+
)
|
|
1187
|
+
)
|
|
1188
|
+
related = tuple(
|
|
1189
|
+
dict.fromkeys(
|
|
1190
|
+
str(item).strip()
|
|
1191
|
+
for item in (
|
|
1192
|
+
related_tasks
|
|
1193
|
+
or [str(dep).strip() for dep in task.get("dependencies") or [] if str(dep).strip()]
|
|
1194
|
+
)
|
|
1195
|
+
if str(item).strip()
|
|
1196
|
+
)
|
|
1197
|
+
)
|
|
1198
|
+
capture_rel = _repo_rel(paths.root, capture_artifact_path) if capture_artifact_path else None
|
|
1199
|
+
body_lines = [
|
|
1200
|
+
f"# Fact: {title}",
|
|
1201
|
+
"",
|
|
1202
|
+
"- Status: `recorded`",
|
|
1203
|
+
f"- Source: `{source}`",
|
|
1204
|
+
f"- Summary: {summary or '(none)'}",
|
|
1205
|
+
"",
|
|
1206
|
+
"## Related Paths",
|
|
1207
|
+
"",
|
|
1208
|
+
]
|
|
1209
|
+
if normalized_paths:
|
|
1210
|
+
body_lines.extend(f"- `{path}`" for path in normalized_paths)
|
|
1211
|
+
else:
|
|
1212
|
+
body_lines.append("- (none)")
|
|
1213
|
+
body_lines.extend(
|
|
1214
|
+
[
|
|
1215
|
+
"",
|
|
1216
|
+
"## Related Artifacts",
|
|
1217
|
+
"",
|
|
1218
|
+
f"- Report: `{_repo_rel(paths.root, report_path)}`"
|
|
1219
|
+
if report_path
|
|
1220
|
+
else "- Report: (none)",
|
|
1221
|
+
f"- Patch: `{_repo_rel(paths.root, patch_path)}`" if patch_path else "- Patch: (none)",
|
|
1222
|
+
(
|
|
1223
|
+
f"- Verify Artifact: `{_repo_rel(paths.root, verify_artifact_path)}`"
|
|
1224
|
+
if verify_artifact_path
|
|
1225
|
+
else "- Verify Artifact: (none)"
|
|
1226
|
+
),
|
|
1227
|
+
(
|
|
1228
|
+
f"- Budget Artifact: `{_repo_rel(paths.root, budget_artifact_path)}`"
|
|
1229
|
+
if budget_artifact_path
|
|
1230
|
+
else "- Budget Artifact: (none)"
|
|
1231
|
+
),
|
|
1232
|
+
(
|
|
1233
|
+
f"- Session Artifacts: `{_repo_rel(paths.root, session_artifact_dir)}`"
|
|
1234
|
+
if session_artifact_dir
|
|
1235
|
+
else "- Session Artifacts: (none retained)"
|
|
1236
|
+
),
|
|
1237
|
+
f"- Capture Artifact: `{capture_rel}`" if capture_rel else "- Capture Artifact: (none)",
|
|
1238
|
+
]
|
|
1239
|
+
)
|
|
1240
|
+
entry = KnowledgeEntry(
|
|
1241
|
+
kind="fact",
|
|
1242
|
+
id=entry_id,
|
|
1243
|
+
title=title,
|
|
1244
|
+
created_at=created,
|
|
1245
|
+
task_id=task_id,
|
|
1246
|
+
source=source,
|
|
1247
|
+
status="recorded",
|
|
1248
|
+
paths=normalized_paths,
|
|
1249
|
+
related_tasks=related,
|
|
1250
|
+
tags=tuple(
|
|
1251
|
+
dict.fromkeys(["fact", source, *[str(tag).strip() for tag in tags if str(tag).strip()]])
|
|
1252
|
+
),
|
|
1253
|
+
report_path=_repo_rel(paths.root, report_path) if report_path else None,
|
|
1254
|
+
patch_path=_repo_rel(paths.root, patch_path) if patch_path else None,
|
|
1255
|
+
verify_artifact_path=(
|
|
1256
|
+
_repo_rel(paths.root, verify_artifact_path) if verify_artifact_path else None
|
|
1257
|
+
),
|
|
1258
|
+
budget_artifact_path=(
|
|
1259
|
+
_repo_rel(paths.root, budget_artifact_path) if budget_artifact_path else None
|
|
1260
|
+
),
|
|
1261
|
+
session_artifact_dir=(
|
|
1262
|
+
_repo_rel(paths.root, session_artifact_dir) if session_artifact_dir else None
|
|
1263
|
+
),
|
|
1264
|
+
capture_artifact_path=capture_rel,
|
|
1265
|
+
body="\n".join(body_lines).rstrip() + "\n",
|
|
1266
|
+
)
|
|
1267
|
+
directory = _knowledge_bucket_dir(paths, kind="fact", task_id=task_id)
|
|
1268
|
+
file_path = _publish_markdown_entry_atomic(
|
|
1269
|
+
directory=directory,
|
|
1270
|
+
stem=filename_stem,
|
|
1271
|
+
markdown_text=entry.to_markdown(),
|
|
1272
|
+
)
|
|
1273
|
+
return KnowledgeEntry.from_markdown(
|
|
1274
|
+
text=file_path.read_text(encoding="utf-8"), file_path=file_path
|
|
1275
|
+
)
|
|
1276
|
+
|
|
1277
|
+
|
|
1278
|
+
def write_decision_entry(
|
|
1279
|
+
*,
|
|
1280
|
+
paths: RunPaths,
|
|
1281
|
+
task: dict[str, Any],
|
|
1282
|
+
source: str,
|
|
1283
|
+
decision_key: str,
|
|
1284
|
+
title: str,
|
|
1285
|
+
summary: str,
|
|
1286
|
+
status: str,
|
|
1287
|
+
paths_in_scope: list[str] | tuple[str, ...] = (),
|
|
1288
|
+
report_path: Path | None = None,
|
|
1289
|
+
patch_path: Path | None = None,
|
|
1290
|
+
verify_artifact_path: Path | None = None,
|
|
1291
|
+
budget_artifact_path: Path | None = None,
|
|
1292
|
+
session_artifact_dir: Path | None = None,
|
|
1293
|
+
capture_artifact_path: Path | None = None,
|
|
1294
|
+
related_tasks: list[str] | tuple[str, ...] | None = None,
|
|
1295
|
+
tags: list[str] | tuple[str, ...] = (),
|
|
1296
|
+
created_at: str | None = None,
|
|
1297
|
+
) -> KnowledgeEntry:
|
|
1298
|
+
created = created_at or _now_iso()
|
|
1299
|
+
task_id = str(task.get("id") or "").strip() or "task"
|
|
1300
|
+
entry_id, filename_stem = _unique_entry_identity(
|
|
1301
|
+
kind="decision",
|
|
1302
|
+
task_id=task_id,
|
|
1303
|
+
source=source,
|
|
1304
|
+
created_at=created,
|
|
1305
|
+
)
|
|
1306
|
+
normalized_paths = tuple(
|
|
1307
|
+
dict.fromkeys(
|
|
1308
|
+
_normalize_path_value(path)
|
|
1309
|
+
for path in paths_in_scope
|
|
1310
|
+
if _normalize_path_value(str(path))
|
|
1311
|
+
)
|
|
1312
|
+
)
|
|
1313
|
+
related = tuple(
|
|
1314
|
+
dict.fromkeys(
|
|
1315
|
+
str(item).strip()
|
|
1316
|
+
for item in (
|
|
1317
|
+
related_tasks
|
|
1318
|
+
or [str(dep).strip() for dep in task.get("dependencies") or [] if str(dep).strip()]
|
|
1319
|
+
)
|
|
1320
|
+
if str(item).strip()
|
|
1321
|
+
)
|
|
1322
|
+
)
|
|
1323
|
+
capture_rel = _repo_rel(paths.root, capture_artifact_path) if capture_artifact_path else None
|
|
1324
|
+
body_lines = [
|
|
1325
|
+
f"# Decision: {title}",
|
|
1326
|
+
"",
|
|
1327
|
+
f"- Decision Key: `{decision_key}`",
|
|
1328
|
+
f"- Status: `{status}`",
|
|
1329
|
+
f"- Source: `{source}`",
|
|
1330
|
+
f"- Summary: {summary or '(none)'}",
|
|
1331
|
+
"",
|
|
1332
|
+
"## Related Paths",
|
|
1333
|
+
"",
|
|
1334
|
+
]
|
|
1335
|
+
if normalized_paths:
|
|
1336
|
+
body_lines.extend(f"- `{path}`" for path in normalized_paths)
|
|
1337
|
+
else:
|
|
1338
|
+
body_lines.append("- (none)")
|
|
1339
|
+
body_lines.extend(
|
|
1340
|
+
[
|
|
1341
|
+
"",
|
|
1342
|
+
"## Related Artifacts",
|
|
1343
|
+
"",
|
|
1344
|
+
f"- Report: `{_repo_rel(paths.root, report_path)}`"
|
|
1345
|
+
if report_path
|
|
1346
|
+
else "- Report: (none)",
|
|
1347
|
+
f"- Patch: `{_repo_rel(paths.root, patch_path)}`" if patch_path else "- Patch: (none)",
|
|
1348
|
+
(
|
|
1349
|
+
f"- Verify Artifact: `{_repo_rel(paths.root, verify_artifact_path)}`"
|
|
1350
|
+
if verify_artifact_path
|
|
1351
|
+
else "- Verify Artifact: (none)"
|
|
1352
|
+
),
|
|
1353
|
+
(
|
|
1354
|
+
f"- Budget Artifact: `{_repo_rel(paths.root, budget_artifact_path)}`"
|
|
1355
|
+
if budget_artifact_path
|
|
1356
|
+
else "- Budget Artifact: (none)"
|
|
1357
|
+
),
|
|
1358
|
+
(
|
|
1359
|
+
f"- Session Artifacts: `{_repo_rel(paths.root, session_artifact_dir)}`"
|
|
1360
|
+
if session_artifact_dir
|
|
1361
|
+
else "- Session Artifacts: (none retained)"
|
|
1362
|
+
),
|
|
1363
|
+
f"- Capture Artifact: `{capture_rel}`" if capture_rel else "- Capture Artifact: (none)",
|
|
1364
|
+
]
|
|
1365
|
+
)
|
|
1366
|
+
entry = KnowledgeEntry(
|
|
1367
|
+
kind="decision",
|
|
1368
|
+
id=entry_id,
|
|
1369
|
+
title=title,
|
|
1370
|
+
created_at=created,
|
|
1371
|
+
task_id=task_id,
|
|
1372
|
+
source=source,
|
|
1373
|
+
status=status,
|
|
1374
|
+
paths=normalized_paths,
|
|
1375
|
+
related_tasks=related,
|
|
1376
|
+
tags=tuple(
|
|
1377
|
+
dict.fromkeys(
|
|
1378
|
+
[
|
|
1379
|
+
"decision",
|
|
1380
|
+
source,
|
|
1381
|
+
status,
|
|
1382
|
+
*[str(tag).strip() for tag in tags if str(tag).strip()],
|
|
1383
|
+
]
|
|
1384
|
+
)
|
|
1385
|
+
),
|
|
1386
|
+
report_path=_repo_rel(paths.root, report_path) if report_path else None,
|
|
1387
|
+
patch_path=_repo_rel(paths.root, patch_path) if patch_path else None,
|
|
1388
|
+
verify_artifact_path=(
|
|
1389
|
+
_repo_rel(paths.root, verify_artifact_path) if verify_artifact_path else None
|
|
1390
|
+
),
|
|
1391
|
+
budget_artifact_path=(
|
|
1392
|
+
_repo_rel(paths.root, budget_artifact_path) if budget_artifact_path else None
|
|
1393
|
+
),
|
|
1394
|
+
session_artifact_dir=(
|
|
1395
|
+
_repo_rel(paths.root, session_artifact_dir) if session_artifact_dir else None
|
|
1396
|
+
),
|
|
1397
|
+
decision_key=decision_key,
|
|
1398
|
+
capture_artifact_path=capture_rel,
|
|
1399
|
+
body="\n".join(body_lines).rstrip() + "\n",
|
|
1400
|
+
)
|
|
1401
|
+
directory = _knowledge_bucket_dir(paths, kind="decision", task_id=task_id)
|
|
1402
|
+
file_path = _publish_markdown_entry_atomic(
|
|
1403
|
+
directory=directory,
|
|
1404
|
+
stem=filename_stem,
|
|
1405
|
+
markdown_text=entry.to_markdown(),
|
|
1406
|
+
)
|
|
1407
|
+
return KnowledgeEntry.from_markdown(
|
|
1408
|
+
text=file_path.read_text(encoding="utf-8"), file_path=file_path
|
|
1409
|
+
)
|