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,475 @@
|
|
|
1
|
+
"""Stable machine-readable event protocol for Forge commands.
|
|
2
|
+
|
|
3
|
+
Human Rich output stays the default. When a Forge command runs with ``--machine`` it
|
|
4
|
+
suppresses Rich output and writes newline-delimited JSON to stdout instead: exactly one
|
|
5
|
+
JSON object per line, using this versioned envelope::
|
|
6
|
+
|
|
7
|
+
{"v": 1, "event": "<name>", "ts": "<iso8601>", "run_id": "<id>|null", "data": {...}}
|
|
8
|
+
|
|
9
|
+
Consumers parse the envelope, not prose. Adding a field to ``data`` is a compatible
|
|
10
|
+
change; renaming an event or changing the envelope shape requires bumping ``v``.
|
|
11
|
+
|
|
12
|
+
INVARIANT: every Forge command invocation emits exactly one terminal event
|
|
13
|
+
(``run_completed`` or ``error``) before the process exits -- on success, on handled
|
|
14
|
+
failure, on an unhandled exception, on ``typer.Exit`` and on ``KeyboardInterrupt``.
|
|
15
|
+
:class:`ForgeMachineSession` is what guarantees it, so command bodies never have to
|
|
16
|
+
remember to emit one from their own error paths. Events emitted after the terminal one
|
|
17
|
+
are dropped rather than appended, so the "exactly one" half of the invariant holds even
|
|
18
|
+
if a late code path tries to say more.
|
|
19
|
+
|
|
20
|
+
Exit-code contract (see also :mod:`alysis_code.cli_impl.commands.forge`):
|
|
21
|
+
|
|
22
|
+
- ``0`` -- the command did its job. A review that rejects work exits 0.
|
|
23
|
+
- ``1`` -- the command ran to completion but the work was not accepted (task failed,
|
|
24
|
+
verification blocked, swarm left tasks unfinished).
|
|
25
|
+
- ``2`` -- the command itself failed (bad config, missing run, unhandled exception).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import sys
|
|
32
|
+
import threading
|
|
33
|
+
from collections.abc import Callable, Iterator, Mapping
|
|
34
|
+
from contextlib import contextmanager
|
|
35
|
+
from datetime import UTC, datetime
|
|
36
|
+
from typing import Any, TextIO
|
|
37
|
+
|
|
38
|
+
from .verification_repair import TASK_STATUS_COMPLETED_UNVERIFIED
|
|
39
|
+
|
|
40
|
+
SCHEMA_VERSION = 1
|
|
41
|
+
|
|
42
|
+
EVENT_RUN_STARTED = "run_started"
|
|
43
|
+
EVENT_PLAN_SAVED = "plan_saved"
|
|
44
|
+
EVENT_PLAN_INVALID = "plan_invalid"
|
|
45
|
+
EVENT_TASK_STARTED = "task_started"
|
|
46
|
+
EVENT_TASK_COMPLETED = "task_completed"
|
|
47
|
+
EVENT_TASK_FAILED = "task_failed"
|
|
48
|
+
EVENT_SCOPE_AMENDED = "scope_amended"
|
|
49
|
+
EVENT_VERIFICATION_RESULT = "verification_result"
|
|
50
|
+
EVENT_VERIFICATION_UNAVAILABLE = "verification_unavailable"
|
|
51
|
+
EVENT_REVIEW_RESULT = "review_result"
|
|
52
|
+
EVENT_RUN_COMPLETED = "run_completed"
|
|
53
|
+
EVENT_ERROR = "error"
|
|
54
|
+
|
|
55
|
+
EVENT_NAMES: frozenset[str] = frozenset(
|
|
56
|
+
{
|
|
57
|
+
EVENT_RUN_STARTED,
|
|
58
|
+
EVENT_PLAN_SAVED,
|
|
59
|
+
EVENT_PLAN_INVALID,
|
|
60
|
+
EVENT_TASK_STARTED,
|
|
61
|
+
EVENT_TASK_COMPLETED,
|
|
62
|
+
EVENT_TASK_FAILED,
|
|
63
|
+
EVENT_SCOPE_AMENDED,
|
|
64
|
+
EVENT_VERIFICATION_RESULT,
|
|
65
|
+
EVENT_VERIFICATION_UNAVAILABLE,
|
|
66
|
+
EVENT_REVIEW_RESULT,
|
|
67
|
+
EVENT_RUN_COMPLETED,
|
|
68
|
+
EVENT_ERROR,
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
TERMINAL_EVENT_NAMES: frozenset[str] = frozenset({EVENT_RUN_COMPLETED, EVENT_ERROR})
|
|
73
|
+
|
|
74
|
+
# Exit codes, kept here so producers and consumers read them from one place.
|
|
75
|
+
EXIT_OK = 0
|
|
76
|
+
EXIT_NOT_ACCEPTED = 1
|
|
77
|
+
EXIT_ERROR = 2
|
|
78
|
+
EXIT_INTERRUPTED = 130
|
|
79
|
+
|
|
80
|
+
# Task statuses the plan uses, mapped to the task lifecycle events.
|
|
81
|
+
# ``completed_unverified`` is a completion, not a failure: the work landed and was
|
|
82
|
+
# kept, and only the check for it was missing. Reporting it as ``task_failed`` is
|
|
83
|
+
# what made "no test runner in this repo" look like broken work.
|
|
84
|
+
_TASK_STARTED_STATUSES = frozenset({"in_progress"})
|
|
85
|
+
_TASK_COMPLETED_STATUSES = frozenset(
|
|
86
|
+
{"done", "already_satisfied", TASK_STATUS_COMPLETED_UNVERIFIED}
|
|
87
|
+
)
|
|
88
|
+
_TASK_FAILED_STATUSES = frozenset(
|
|
89
|
+
{"failed", "verify_failed", "changes_requested", "merge_conflict", "interrupted", "blocked"}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ForgeEventProtocolError(ValueError):
|
|
94
|
+
"""Raised when an event name outside the declared schema is emitted."""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _now_iso() -> str:
|
|
98
|
+
return datetime.now(UTC).isoformat()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def task_event_for_status(status: str) -> str | None:
|
|
102
|
+
"""Return the lifecycle event a plan task status implies, if any.
|
|
103
|
+
|
|
104
|
+
Intermediate statuses (``planned``, ``todo``, ``ready_for_merge``) map to ``None``:
|
|
105
|
+
they are bookkeeping, not something a consumer should render as a task transition.
|
|
106
|
+
"""
|
|
107
|
+
normalized = str(status or "").strip().lower()
|
|
108
|
+
if normalized in _TASK_STARTED_STATUSES:
|
|
109
|
+
return EVENT_TASK_STARTED
|
|
110
|
+
if normalized in _TASK_COMPLETED_STATUSES:
|
|
111
|
+
return EVENT_TASK_COMPLETED
|
|
112
|
+
if normalized in _TASK_FAILED_STATUSES:
|
|
113
|
+
return EVENT_TASK_FAILED
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class ForgeEventEmitter:
|
|
118
|
+
"""Writes the NDJSON event stream for one Forge command invocation.
|
|
119
|
+
|
|
120
|
+
A disabled emitter is a no-op, so command bodies can call ``emit`` unconditionally
|
|
121
|
+
and human mode stays byte-for-byte unchanged.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(
|
|
125
|
+
self,
|
|
126
|
+
*,
|
|
127
|
+
command: str,
|
|
128
|
+
enabled: bool = True,
|
|
129
|
+
stream: TextIO | None = None,
|
|
130
|
+
run_id: str | None = None,
|
|
131
|
+
clock: Callable[[], str] | None = None,
|
|
132
|
+
) -> None:
|
|
133
|
+
self.command = str(command)
|
|
134
|
+
self.enabled = bool(enabled)
|
|
135
|
+
self._stream = stream
|
|
136
|
+
self._run_id = str(run_id).strip() or None if run_id else None
|
|
137
|
+
self._clock = clock or _now_iso
|
|
138
|
+
self._lock = threading.RLock()
|
|
139
|
+
self._terminal_event: str | None = None
|
|
140
|
+
self._emitted = 0
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def run_id(self) -> str | None:
|
|
144
|
+
return self._run_id
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def terminal_event(self) -> str | None:
|
|
148
|
+
"""Name of the terminal event already emitted, or ``None``."""
|
|
149
|
+
return self._terminal_event
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def emitted_count(self) -> int:
|
|
153
|
+
return self._emitted
|
|
154
|
+
|
|
155
|
+
def set_run_id(self, run_id: str | None) -> None:
|
|
156
|
+
"""Attach a run id to every subsequent event once the run is known."""
|
|
157
|
+
with self._lock:
|
|
158
|
+
normalized = str(run_id).strip() if run_id is not None else ""
|
|
159
|
+
self._run_id = normalized or None
|
|
160
|
+
|
|
161
|
+
def emit(
|
|
162
|
+
self,
|
|
163
|
+
event: str,
|
|
164
|
+
data: Mapping[str, Any] | None = None,
|
|
165
|
+
*,
|
|
166
|
+
run_id: str | None = None,
|
|
167
|
+
) -> bool:
|
|
168
|
+
"""Emit one event. Returns True when a line was written."""
|
|
169
|
+
name = str(event or "").strip()
|
|
170
|
+
if name not in EVENT_NAMES:
|
|
171
|
+
raise ForgeEventProtocolError(
|
|
172
|
+
f"Unknown forge event: {event!r}. Declared events: {sorted(EVENT_NAMES)}"
|
|
173
|
+
)
|
|
174
|
+
with self._lock:
|
|
175
|
+
if not self.enabled:
|
|
176
|
+
return False
|
|
177
|
+
if self._terminal_event is not None:
|
|
178
|
+
# The terminal event has been written; the stream is closed for business.
|
|
179
|
+
return False
|
|
180
|
+
if run_id is not None:
|
|
181
|
+
self.set_run_id(run_id)
|
|
182
|
+
payload = {
|
|
183
|
+
"v": SCHEMA_VERSION,
|
|
184
|
+
"event": name,
|
|
185
|
+
"ts": self._clock(),
|
|
186
|
+
"run_id": self._run_id,
|
|
187
|
+
"data": dict(data or {}),
|
|
188
|
+
}
|
|
189
|
+
self._write(payload)
|
|
190
|
+
self._emitted += 1
|
|
191
|
+
if name in TERMINAL_EVENT_NAMES:
|
|
192
|
+
self._terminal_event = name
|
|
193
|
+
return True
|
|
194
|
+
|
|
195
|
+
def run_completed(
|
|
196
|
+
self,
|
|
197
|
+
*,
|
|
198
|
+
ok: bool,
|
|
199
|
+
exit_code: int | None = None,
|
|
200
|
+
data: Mapping[str, Any] | None = None,
|
|
201
|
+
) -> bool:
|
|
202
|
+
"""Terminal event: the command ran to completion."""
|
|
203
|
+
resolved_exit = (
|
|
204
|
+
exit_code if exit_code is not None else (EXIT_OK if ok else EXIT_NOT_ACCEPTED)
|
|
205
|
+
)
|
|
206
|
+
payload: dict[str, Any] = {
|
|
207
|
+
"command": self.command,
|
|
208
|
+
"ok": bool(ok),
|
|
209
|
+
"exit_code": int(resolved_exit),
|
|
210
|
+
}
|
|
211
|
+
payload.update(dict(data or {}))
|
|
212
|
+
return self.emit(EVENT_RUN_COMPLETED, payload)
|
|
213
|
+
|
|
214
|
+
def error(
|
|
215
|
+
self,
|
|
216
|
+
*,
|
|
217
|
+
message: str,
|
|
218
|
+
kind: str = "error",
|
|
219
|
+
exit_code: int = EXIT_ERROR,
|
|
220
|
+
data: Mapping[str, Any] | None = None,
|
|
221
|
+
) -> bool:
|
|
222
|
+
"""Terminal event: the command itself failed."""
|
|
223
|
+
payload: dict[str, Any] = {
|
|
224
|
+
"command": self.command,
|
|
225
|
+
"kind": str(kind),
|
|
226
|
+
"message": str(message),
|
|
227
|
+
"exit_code": int(exit_code),
|
|
228
|
+
}
|
|
229
|
+
payload.update(dict(data or {}))
|
|
230
|
+
return self.emit(EVENT_ERROR, payload)
|
|
231
|
+
|
|
232
|
+
def _write(self, payload: Mapping[str, Any]) -> None:
|
|
233
|
+
stream = self._stream if self._stream is not None else sys.stdout
|
|
234
|
+
# ensure_ascii keeps the line encodable on every console codepage; default=str
|
|
235
|
+
# keeps a stray Path or datetime from turning one event into a crash.
|
|
236
|
+
line = json.dumps(payload, ensure_ascii=True, default=str, separators=(",", ":"))
|
|
237
|
+
try:
|
|
238
|
+
stream.write(line + "\n")
|
|
239
|
+
stream.flush()
|
|
240
|
+
except (OSError, ValueError):
|
|
241
|
+
# A closed or broken stdout must not take the command down with it.
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
_ACTIVE_LOCK = threading.RLock()
|
|
246
|
+
_ACTIVE_EMITTER: ForgeEventEmitter | None = None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def active_emitter() -> ForgeEventEmitter | None:
|
|
250
|
+
"""Return the emitter for the Forge command currently running, if any.
|
|
251
|
+
|
|
252
|
+
Deep execution code (the swarm orchestrator) uses this instead of threading an
|
|
253
|
+
emitter through dozens of call sites. One process runs one Forge command, so a
|
|
254
|
+
process-global registration is the honest model here.
|
|
255
|
+
"""
|
|
256
|
+
with _ACTIVE_LOCK:
|
|
257
|
+
return _ACTIVE_EMITTER
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def set_active_emitter(emitter: ForgeEventEmitter | None) -> ForgeEventEmitter | None:
|
|
261
|
+
"""Register the active emitter and return the one it replaced."""
|
|
262
|
+
global _ACTIVE_EMITTER
|
|
263
|
+
with _ACTIVE_LOCK:
|
|
264
|
+
previous = _ACTIVE_EMITTER
|
|
265
|
+
_ACTIVE_EMITTER = emitter
|
|
266
|
+
return previous
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def machine_output_active() -> bool:
|
|
270
|
+
"""True when stdout belongs to the NDJSON stream and Rich output must stay silent."""
|
|
271
|
+
emitter = active_emitter()
|
|
272
|
+
return emitter is not None and emitter.enabled
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def emit_active(
|
|
276
|
+
event: str,
|
|
277
|
+
data: Mapping[str, Any] | None = None,
|
|
278
|
+
*,
|
|
279
|
+
run_id: str | None = None,
|
|
280
|
+
) -> bool:
|
|
281
|
+
"""Emit through the active emitter; a no-op when no Forge command is machine-mode."""
|
|
282
|
+
emitter = active_emitter()
|
|
283
|
+
if emitter is None:
|
|
284
|
+
return False
|
|
285
|
+
return emitter.emit(event, data, run_id=run_id)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def emit_task_status(
|
|
289
|
+
task_id: str,
|
|
290
|
+
status: str,
|
|
291
|
+
data: Mapping[str, Any] | None = None,
|
|
292
|
+
) -> bool:
|
|
293
|
+
"""Emit the task lifecycle event a plan status transition implies."""
|
|
294
|
+
event = task_event_for_status(status)
|
|
295
|
+
if event is None:
|
|
296
|
+
return False
|
|
297
|
+
payload: dict[str, Any] = {"task_id": str(task_id), "status": str(status)}
|
|
298
|
+
payload.update(dict(data or {}))
|
|
299
|
+
return emit_active(event, payload)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _exit_code_from_exception(exc: BaseException) -> int | None:
|
|
303
|
+
"""Extract the exit code from typer.Exit / click.exceptions.Exit / SystemExit."""
|
|
304
|
+
for attribute in ("exit_code", "code"):
|
|
305
|
+
if not hasattr(exc, attribute):
|
|
306
|
+
continue
|
|
307
|
+
raw = getattr(exc, attribute)
|
|
308
|
+
if raw is None:
|
|
309
|
+
return 0
|
|
310
|
+
if isinstance(raw, bool):
|
|
311
|
+
return int(raw)
|
|
312
|
+
if isinstance(raw, int):
|
|
313
|
+
return raw
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class ForgeMachineSession:
|
|
318
|
+
"""Context manager that enforces the one-terminal-event invariant.
|
|
319
|
+
|
|
320
|
+
Wrap a command body in this and every exit path -- return, ``typer.Exit``,
|
|
321
|
+
``KeyboardInterrupt``, unhandled exception -- ends with exactly one terminal event.
|
|
322
|
+
Exceptions are never swallowed: the event is written, then the exception propagates
|
|
323
|
+
so exit codes and tracebacks behave exactly as they do in human mode.
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
def __init__(self, emitter: ForgeEventEmitter, *, register_active: bool = True) -> None:
|
|
327
|
+
self.emitter = emitter
|
|
328
|
+
self._register_active = bool(register_active)
|
|
329
|
+
self._previous: ForgeEventEmitter | None = None
|
|
330
|
+
self._registered = False
|
|
331
|
+
|
|
332
|
+
def __enter__(self) -> ForgeEventEmitter:
|
|
333
|
+
if self._register_active:
|
|
334
|
+
self._previous = set_active_emitter(self.emitter)
|
|
335
|
+
self._registered = True
|
|
336
|
+
return self.emitter
|
|
337
|
+
|
|
338
|
+
def __exit__(
|
|
339
|
+
self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: Any
|
|
340
|
+
) -> bool:
|
|
341
|
+
try:
|
|
342
|
+
self._finalize(exc)
|
|
343
|
+
finally:
|
|
344
|
+
if self._registered:
|
|
345
|
+
set_active_emitter(self._previous)
|
|
346
|
+
self._registered = False
|
|
347
|
+
return False
|
|
348
|
+
|
|
349
|
+
def _finalize(self, exc: BaseException | None) -> None:
|
|
350
|
+
if not self.emitter.enabled or self.emitter.terminal_event is not None:
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
if exc is None:
|
|
354
|
+
self.emitter.run_completed(ok=True, exit_code=EXIT_OK)
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
if isinstance(exc, KeyboardInterrupt):
|
|
358
|
+
self.emitter.error(
|
|
359
|
+
message="Interrupted.",
|
|
360
|
+
kind="interrupted",
|
|
361
|
+
exit_code=EXIT_INTERRUPTED,
|
|
362
|
+
)
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
exit_code = _exit_code_from_exception(exc)
|
|
366
|
+
if exit_code is not None:
|
|
367
|
+
# A command that exits without describing itself still gets a truthful
|
|
368
|
+
# terminal event, mapped through the exit-code contract in this module.
|
|
369
|
+
if exit_code == EXIT_OK:
|
|
370
|
+
self.emitter.run_completed(ok=True, exit_code=EXIT_OK)
|
|
371
|
+
elif exit_code == EXIT_NOT_ACCEPTED:
|
|
372
|
+
self.emitter.run_completed(ok=False, exit_code=EXIT_NOT_ACCEPTED)
|
|
373
|
+
else:
|
|
374
|
+
self.emitter.error(
|
|
375
|
+
message=str(exc) or f"Command exited with code {exit_code}.",
|
|
376
|
+
kind="exit",
|
|
377
|
+
exit_code=exit_code,
|
|
378
|
+
)
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
self.emitter.error(
|
|
382
|
+
message=str(exc) or exc.__class__.__name__,
|
|
383
|
+
kind="exception",
|
|
384
|
+
exit_code=EXIT_ERROR,
|
|
385
|
+
data={"exception_type": exc.__class__.__name__},
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@contextmanager
|
|
390
|
+
def machine_session(
|
|
391
|
+
command: str,
|
|
392
|
+
*,
|
|
393
|
+
machine: bool,
|
|
394
|
+
stream: TextIO | None = None,
|
|
395
|
+
run_id: str | None = None,
|
|
396
|
+
clock: Callable[[], str] | None = None,
|
|
397
|
+
) -> Iterator[ForgeEventEmitter]:
|
|
398
|
+
"""Open an event session for one Forge command invocation.
|
|
399
|
+
|
|
400
|
+
When ``machine`` is false the yielded emitter is inert and nothing is written, so
|
|
401
|
+
the same command body serves both the human TUI and the machine protocol.
|
|
402
|
+
"""
|
|
403
|
+
emitter = ForgeEventEmitter(
|
|
404
|
+
command=command,
|
|
405
|
+
enabled=machine,
|
|
406
|
+
stream=stream,
|
|
407
|
+
run_id=run_id,
|
|
408
|
+
clock=clock,
|
|
409
|
+
)
|
|
410
|
+
with ForgeMachineSession(emitter, register_active=machine) as active:
|
|
411
|
+
yield active
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def parse_event_line(line: str) -> dict[str, Any] | None:
|
|
415
|
+
"""Parse one output line as a Forge event, or return None if it is not one.
|
|
416
|
+
|
|
417
|
+
Tolerant by design: job logs interleave worker output with the event stream, so a
|
|
418
|
+
line that is not a well-formed event envelope is simply not an event.
|
|
419
|
+
"""
|
|
420
|
+
text = str(line or "").strip()
|
|
421
|
+
if not text.startswith("{") or not text.endswith("}"):
|
|
422
|
+
return None
|
|
423
|
+
try:
|
|
424
|
+
payload = json.loads(text)
|
|
425
|
+
except (ValueError, TypeError):
|
|
426
|
+
return None
|
|
427
|
+
if not isinstance(payload, dict):
|
|
428
|
+
return None
|
|
429
|
+
if payload.get("v") != SCHEMA_VERSION:
|
|
430
|
+
return None
|
|
431
|
+
event = payload.get("event")
|
|
432
|
+
if not isinstance(event, str) or event not in EVENT_NAMES:
|
|
433
|
+
return None
|
|
434
|
+
if not isinstance(payload.get("data"), dict):
|
|
435
|
+
return None
|
|
436
|
+
return payload
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def is_terminal_event(payload: Mapping[str, Any]) -> bool:
|
|
440
|
+
return str(payload.get("event") or "") in TERMINAL_EVENT_NAMES
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
__all__ = [
|
|
444
|
+
"EVENT_ERROR",
|
|
445
|
+
"EVENT_NAMES",
|
|
446
|
+
"EVENT_PLAN_INVALID",
|
|
447
|
+
"EVENT_PLAN_SAVED",
|
|
448
|
+
"EVENT_REVIEW_RESULT",
|
|
449
|
+
"EVENT_RUN_COMPLETED",
|
|
450
|
+
"EVENT_RUN_STARTED",
|
|
451
|
+
"EVENT_SCOPE_AMENDED",
|
|
452
|
+
"EVENT_TASK_COMPLETED",
|
|
453
|
+
"EVENT_TASK_FAILED",
|
|
454
|
+
"EVENT_TASK_STARTED",
|
|
455
|
+
"EVENT_VERIFICATION_RESULT",
|
|
456
|
+
"EVENT_VERIFICATION_UNAVAILABLE",
|
|
457
|
+
"EXIT_ERROR",
|
|
458
|
+
"EXIT_INTERRUPTED",
|
|
459
|
+
"EXIT_NOT_ACCEPTED",
|
|
460
|
+
"EXIT_OK",
|
|
461
|
+
"SCHEMA_VERSION",
|
|
462
|
+
"TERMINAL_EVENT_NAMES",
|
|
463
|
+
"ForgeEventEmitter",
|
|
464
|
+
"ForgeEventProtocolError",
|
|
465
|
+
"ForgeMachineSession",
|
|
466
|
+
"active_emitter",
|
|
467
|
+
"emit_active",
|
|
468
|
+
"emit_task_status",
|
|
469
|
+
"is_terminal_event",
|
|
470
|
+
"machine_output_active",
|
|
471
|
+
"machine_session",
|
|
472
|
+
"parse_event_line",
|
|
473
|
+
"set_active_emitter",
|
|
474
|
+
"task_event_for_status",
|
|
475
|
+
]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def split_frontmatter(text: str) -> tuple[str | None, str]:
|
|
8
|
+
if not text.startswith("---"):
|
|
9
|
+
return None, text
|
|
10
|
+
lines = text.splitlines()
|
|
11
|
+
if not lines or lines[0].strip() != "---":
|
|
12
|
+
return None, text
|
|
13
|
+
end_idx = None
|
|
14
|
+
for idx in range(1, len(lines)):
|
|
15
|
+
if lines[idx].strip() == "---":
|
|
16
|
+
end_idx = idx
|
|
17
|
+
break
|
|
18
|
+
if end_idx is None:
|
|
19
|
+
return None, text
|
|
20
|
+
frontmatter = "\n".join(lines[1:end_idx])
|
|
21
|
+
body = "\n".join(lines[end_idx + 1 :])
|
|
22
|
+
return frontmatter, body
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def parse_frontmatter_yaml(
|
|
26
|
+
frontmatter: str,
|
|
27
|
+
*,
|
|
28
|
+
allowed_keys: set[str] | None = None,
|
|
29
|
+
list_fields: set[str] | None = None,
|
|
30
|
+
string_fields: set[str] | None = None,
|
|
31
|
+
bool_fields: set[str] | None = None,
|
|
32
|
+
) -> dict[str, Any]:
|
|
33
|
+
allowed = set(allowed_keys or ())
|
|
34
|
+
list_keys = set(list_fields or ())
|
|
35
|
+
string_keys = set(string_fields or ())
|
|
36
|
+
bool_keys = set(bool_fields or ())
|
|
37
|
+
lines = frontmatter.splitlines()
|
|
38
|
+
out: dict[str, Any] = {}
|
|
39
|
+
i = 0
|
|
40
|
+
while i < len(lines):
|
|
41
|
+
raw_line = lines[i]
|
|
42
|
+
line = raw_line.strip()
|
|
43
|
+
i += 1
|
|
44
|
+
if not line or line.startswith("#"):
|
|
45
|
+
continue
|
|
46
|
+
if ":" not in raw_line:
|
|
47
|
+
continue
|
|
48
|
+
key_part, value_part = raw_line.split(":", 1)
|
|
49
|
+
key = key_part.strip()
|
|
50
|
+
if allowed and key not in allowed:
|
|
51
|
+
continue
|
|
52
|
+
value = value_part.strip()
|
|
53
|
+
if key in list_keys:
|
|
54
|
+
if value:
|
|
55
|
+
out[key] = coerce_frontmatter_list(value)
|
|
56
|
+
continue
|
|
57
|
+
items: list[str] = []
|
|
58
|
+
while i < len(lines):
|
|
59
|
+
next_line = lines[i]
|
|
60
|
+
stripped = next_line.strip()
|
|
61
|
+
if not stripped:
|
|
62
|
+
i += 1
|
|
63
|
+
continue
|
|
64
|
+
if not stripped.startswith("- "):
|
|
65
|
+
break
|
|
66
|
+
items.append(stripped[2:].strip().strip('"').strip("'"))
|
|
67
|
+
i += 1
|
|
68
|
+
out[key] = [item for item in items if item]
|
|
69
|
+
continue
|
|
70
|
+
if key in bool_keys:
|
|
71
|
+
lowered = value.lower()
|
|
72
|
+
if lowered in {"true", "yes", "on", "1"}:
|
|
73
|
+
out[key] = True
|
|
74
|
+
elif lowered in {"false", "no", "off", "0"}:
|
|
75
|
+
out[key] = False
|
|
76
|
+
continue
|
|
77
|
+
if key in string_keys or not allowed:
|
|
78
|
+
out[key] = value.strip().strip('"').strip("'")
|
|
79
|
+
return out
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def coerce_frontmatter_list(raw: Any) -> list[str]:
|
|
83
|
+
if isinstance(raw, list):
|
|
84
|
+
return [str(item).strip() for item in raw if str(item).strip()]
|
|
85
|
+
text = str(raw or "").strip()
|
|
86
|
+
if not text:
|
|
87
|
+
return []
|
|
88
|
+
if text.startswith("[") and text.endswith("]"):
|
|
89
|
+
try:
|
|
90
|
+
parsed = json.loads(text)
|
|
91
|
+
except json.JSONDecodeError:
|
|
92
|
+
parsed = None
|
|
93
|
+
if isinstance(parsed, list):
|
|
94
|
+
return [str(item).strip() for item in parsed if str(item).strip()]
|
|
95
|
+
return [item.strip() for item in text.split(",") if item.strip()]
|