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.
Files changed (458) hide show
  1. alysis_code/__init__.py +3 -0
  2. alysis_code/__main__.py +4 -0
  3. alysis_code/_build_info.py +14 -0
  4. alysis_code/account_login.py +468 -0
  5. alysis_code/agent/README.md +35 -0
  6. alysis_code/agent/__init__.py +11 -0
  7. alysis_code/agent/acceptance_contract.py +2217 -0
  8. alysis_code/agent/blast_radius.py +1403 -0
  9. alysis_code/agent/cache_keepalive.py +227 -0
  10. alysis_code/agent/completion_certificate.py +366 -0
  11. alysis_code/agent/completion_gate.py +306 -0
  12. alysis_code/agent/empty_response_stall.py +403 -0
  13. alysis_code/agent/errors.py +28 -0
  14. alysis_code/agent/llm_calls.py +475 -0
  15. alysis_code/agent/mutation_classification.py +227 -0
  16. alysis_code/agent/prompt_context.py +2508 -0
  17. alysis_code/agent/read_ledger.py +253 -0
  18. alysis_code/agent/regression_baseline.py +642 -0
  19. alysis_code/agent/reproduction_first.py +610 -0
  20. alysis_code/agent/sensitive_output.py +629 -0
  21. alysis_code/agent/session.py +3218 -0
  22. alysis_code/agent/steering.py +191 -0
  23. alysis_code/agent/subagent_execution.py +5177 -0
  24. alysis_code/agent/subagent_workspace.py +666 -0
  25. alysis_code/agent/tools_assembly.py +4728 -0
  26. alysis_code/agent/turn/__init__.py +101 -0
  27. alysis_code/agent/turn/core.py +8483 -0
  28. alysis_code/agent/turn/events.py +113 -0
  29. alysis_code/agent/turn/exploration.py +590 -0
  30. alysis_code/agent/turn/interventions.py +65 -0
  31. alysis_code/agent/turn/read_cache.py +420 -0
  32. alysis_code/agent/turn/snapshot.py +179 -0
  33. alysis_code/agent/turn_contract.py +661 -0
  34. alysis_code/agent/turn_path.py +129 -0
  35. alysis_code/agent/verification.py +2885 -0
  36. alysis_code/agent/verification_commands.py +512 -0
  37. alysis_code/agent/verification_evidence.py +738 -0
  38. alysis_code/agent_loop.py +693 -0
  39. alysis_code/agent_runtimes/__init__.py +51 -0
  40. alysis_code/agent_runtimes/base.py +114 -0
  41. alysis_code/agent_runtimes/builtins.py +129 -0
  42. alysis_code/agent_runtimes/codex_cli.py +664 -0
  43. alysis_code/agent_runtimes/host.py +263 -0
  44. alysis_code/agent_runtimes/registry.py +64 -0
  45. alysis_code/agent_runtimes/service.py +150 -0
  46. alysis_code/agentbox_client.py +416 -0
  47. alysis_code/agentbox_integration.py +310 -0
  48. alysis_code/alysis_cloud.py +152 -0
  49. alysis_code/approval_scope.py +276 -0
  50. alysis_code/assets/README.md +33 -0
  51. alysis_code/assets/__init__.py +126 -0
  52. alysis_code/assets/asset_read_core.py +281 -0
  53. alysis_code/assets/budget_allocator.py +456 -0
  54. alysis_code/assets/comprehender.py +759 -0
  55. alysis_code/assets/index.py +654 -0
  56. alysis_code/assets/ingestion.py +275 -0
  57. alysis_code/assets/legacy_migration.py +413 -0
  58. alysis_code/assets/models.py +263 -0
  59. alysis_code/assets/ocr.py +239 -0
  60. alysis_code/assets/owl/ascii/f-000.txt +13 -0
  61. alysis_code/assets/owl/ascii/f-001.txt +13 -0
  62. alysis_code/assets/owl/ascii/f-002.txt +13 -0
  63. alysis_code/assets/owl/ascii/f-003.txt +13 -0
  64. alysis_code/assets/owl/ascii/f-004.txt +13 -0
  65. alysis_code/assets/owl/ascii/f-005.txt +13 -0
  66. alysis_code/assets/owl/ascii/f-006.txt +13 -0
  67. alysis_code/assets/owl/ascii/f-007.txt +13 -0
  68. alysis_code/assets/owl/ascii/f-008.txt +13 -0
  69. alysis_code/assets/owl/ascii/f-009.txt +13 -0
  70. alysis_code/assets/owl/ascii/f-010.txt +13 -0
  71. alysis_code/assets/owl/ascii/f-011.txt +13 -0
  72. alysis_code/assets/owl/ascii/f-012.txt +13 -0
  73. alysis_code/assets/owl/ascii/f-013.txt +13 -0
  74. alysis_code/assets/owl/ascii/f-014.txt +13 -0
  75. alysis_code/assets/owl/ascii/f-015.txt +13 -0
  76. alysis_code/assets/owl/ascii/f-016.txt +13 -0
  77. alysis_code/assets/owl/ascii/f-017.txt +13 -0
  78. alysis_code/assets/owl/ascii/f-018.txt +13 -0
  79. alysis_code/assets/owl/ascii/f-019.txt +13 -0
  80. alysis_code/assets/owl/ascii/f-020.txt +13 -0
  81. alysis_code/assets/owl/index.html +98 -0
  82. alysis_code/assets/owl/show-owl.sh +761 -0
  83. alysis_code/assets/paths.py +49 -0
  84. alysis_code/assets/plan_binding.py +326 -0
  85. alysis_code/assets/planner_context.py +466 -0
  86. alysis_code/assets/planner_tools.py +184 -0
  87. alysis_code/assets/prompts.py +101 -0
  88. alysis_code/assets/replanner_context.py +239 -0
  89. alysis_code/assets/surface.py +521 -0
  90. alysis_code/assets/untrusted_content.py +48 -0
  91. alysis_code/assets/usage_logger.py +94 -0
  92. alysis_code/assets/worker_mirror.py +428 -0
  93. alysis_code/assets/worker_section.py +303 -0
  94. alysis_code/assets/worker_tools.py +468 -0
  95. alysis_code/atomic_io.py +83 -0
  96. alysis_code/auth_diagnostics.py +272 -0
  97. alysis_code/background_runner.py +366 -0
  98. alysis_code/branding.py +270 -0
  99. alysis_code/budget_policy.py +390 -0
  100. alysis_code/build_identity.py +465 -0
  101. alysis_code/builtin_hooks/__init__.py +7 -0
  102. alysis_code/builtin_hooks/notify_done_windows.py +65 -0
  103. alysis_code/bwrap_etc.py +76 -0
  104. alysis_code/cancellation.py +41 -0
  105. alysis_code/capabilities.py +137 -0
  106. alysis_code/chatgpt_codex_static_provider.py +133 -0
  107. alysis_code/cli.py +51 -0
  108. alysis_code/cli_impl/__init__.py +1 -0
  109. alysis_code/cli_impl/assets_cli.py +537 -0
  110. alysis_code/cli_impl/assets_modal.py +412 -0
  111. alysis_code/cli_impl/chat/__init__.py +156 -0
  112. alysis_code/cli_impl/chat/commands.py +2616 -0
  113. alysis_code/cli_impl/chat/loop.py +4508 -0
  114. alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
  115. alysis_code/cli_impl/chat/rendering.py +444 -0
  116. alysis_code/cli_impl/chat/state.py +124 -0
  117. alysis_code/cli_impl/chat_resume.py +830 -0
  118. alysis_code/cli_impl/chat_slash_completer.py +258 -0
  119. alysis_code/cli_impl/commands/__init__.py +11 -0
  120. alysis_code/cli_impl/commands/_shared.py +89 -0
  121. alysis_code/cli_impl/commands/auth.py +623 -0
  122. alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
  123. alysis_code/cli_impl/commands/chat_state.py +158 -0
  124. alysis_code/cli_impl/commands/chat_status.py +1248 -0
  125. alysis_code/cli_impl/commands/chat_terminal.py +942 -0
  126. alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
  127. alysis_code/cli_impl/commands/cli_common.py +1223 -0
  128. alysis_code/cli_impl/commands/cli_surface.py +77 -0
  129. alysis_code/cli_impl/commands/config.py +131 -0
  130. alysis_code/cli_impl/commands/conventions.py +85 -0
  131. alysis_code/cli_impl/commands/execution_helpers.py +350 -0
  132. alysis_code/cli_impl/commands/extensions.py +401 -0
  133. alysis_code/cli_impl/commands/forge.py +1282 -0
  134. alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
  135. alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
  136. alysis_code/cli_impl/commands/hooks.py +737 -0
  137. alysis_code/cli_impl/commands/ide_bridge.py +31 -0
  138. alysis_code/cli_impl/commands/mcp.py +700 -0
  139. alysis_code/cli_impl/commands/profile.py +453 -0
  140. alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
  141. alysis_code/cli_impl/commands/report.py +88 -0
  142. alysis_code/cli_impl/commands/root.py +1118 -0
  143. alysis_code/cli_impl/commands/sandbox.py +184 -0
  144. alysis_code/cli_impl/commands/server.py +54 -0
  145. alysis_code/cli_impl/commands/sessions.py +252 -0
  146. alysis_code/cli_impl/commands/skills.py +404 -0
  147. alysis_code/cli_impl/commands/startup.py +946 -0
  148. alysis_code/cli_impl/commands/tools.py +335 -0
  149. alysis_code/cli_impl/commands/update.py +364 -0
  150. alysis_code/cli_impl/commands/welcome.py +972 -0
  151. alysis_code/cli_impl/config_menu.py +3882 -0
  152. alysis_code/cli_impl/forge.py +4509 -0
  153. alysis_code/cli_impl/forge_recovery.py +485 -0
  154. alysis_code/cli_impl/setup_wizard.py +2409 -0
  155. alysis_code/cli_impl/tui/__init__.py +58 -0
  156. alysis_code/cli_impl/tui/app.py +4551 -0
  157. alysis_code/cli_impl/tui/config.py +32 -0
  158. alysis_code/cli_impl/tui/config_flow.py +2754 -0
  159. alysis_code/cli_impl/tui/config_overlay.py +566 -0
  160. alysis_code/cli_impl/tui/content.py +78 -0
  161. alysis_code/cli_impl/tui/footer.py +218 -0
  162. alysis_code/cli_impl/tui/forge_status.py +136 -0
  163. alysis_code/cli_impl/tui/markdown.py +244 -0
  164. alysis_code/cli_impl/tui/owl.py +109 -0
  165. alysis_code/cli_impl/tui/plan_meta.py +477 -0
  166. alysis_code/cli_impl/tui/setup_app.py +519 -0
  167. alysis_code/cli_impl/tui/setup_flow.py +1622 -0
  168. alysis_code/cli_impl/tui/state.py +101 -0
  169. alysis_code/cli_impl/tui/subagent_identity.py +66 -0
  170. alysis_code/cli_impl/tui/subagent_panel.py +186 -0
  171. alysis_code/cli_impl/tui/surface.py +796 -0
  172. alysis_code/cli_impl/tui/transcript.py +514 -0
  173. alysis_code/cli_impl/tui/update_prompt.py +79 -0
  174. alysis_code/cli_impl/tui/workspace_guard.py +384 -0
  175. alysis_code/clipboard.py +172 -0
  176. alysis_code/code_review.py +1211 -0
  177. alysis_code/compaction/__init__.py +28 -0
  178. alysis_code/compaction/conversation_compactor.py +2932 -0
  179. alysis_code/compaction/importance.py +177 -0
  180. alysis_code/compaction/settings.py +297 -0
  181. alysis_code/compaction/tool_output_offload.py +447 -0
  182. alysis_code/config.py +3509 -0
  183. alysis_code/conflict_auto_resolver.py +895 -0
  184. alysis_code/context/__init__.py +1 -0
  185. alysis_code/context/tool_schema_budgeter.py +220 -0
  186. alysis_code/crash_diagnostics.py +282 -0
  187. alysis_code/custom_tools/README.md +34 -0
  188. alysis_code/custom_tools/__init__.py +43 -0
  189. alysis_code/custom_tools/discovery.py +903 -0
  190. alysis_code/custom_tools/runtime.py +1516 -0
  191. alysis_code/custom_tools/session.py +227 -0
  192. alysis_code/custom_tools/trust.py +232 -0
  193. alysis_code/diff_paths.py +113 -0
  194. alysis_code/direction_change.py +293 -0
  195. alysis_code/dispatch_timing.py +306 -0
  196. alysis_code/durable_service_manager.py +1236 -0
  197. alysis_code/edit_discipline.py +659 -0
  198. alysis_code/error_text.py +73 -0
  199. alysis_code/execution_budget.py +411 -0
  200. alysis_code/execution_context.py +915 -0
  201. alysis_code/execution_deadline.py +1065 -0
  202. alysis_code/execution_shared.py +1904 -0
  203. alysis_code/extensions/README.md +30 -0
  204. alysis_code/extensions/__init__.py +93 -0
  205. alysis_code/extensions/activation.py +138 -0
  206. alysis_code/extensions/install.py +1436 -0
  207. alysis_code/extensions/manifest.py +487 -0
  208. alysis_code/extensions/models.py +74 -0
  209. alysis_code/extensions/paths.py +56 -0
  210. alysis_code/extensions/registry.json +4 -0
  211. alysis_code/extensions/registry.py +52 -0
  212. alysis_code/extensions/state.py +83 -0
  213. alysis_code/extensions/workspace_trust.py +101 -0
  214. alysis_code/failed_task_evidence.py +369 -0
  215. alysis_code/failure_category.py +315 -0
  216. alysis_code/feedback_report.py +1647 -0
  217. alysis_code/file_classification.py +485 -0
  218. alysis_code/forge.py +2064 -0
  219. alysis_code/forge_completion.py +362 -0
  220. alysis_code/forge_events.py +475 -0
  221. alysis_code/frontmatter_utils.py +95 -0
  222. alysis_code/git_evidence.py +1181 -0
  223. alysis_code/git_ops.py +560 -0
  224. alysis_code/git_safe.py +62 -0
  225. alysis_code/git_worktrees.py +190 -0
  226. alysis_code/hooks/README.md +33 -0
  227. alysis_code/hooks/__init__.py +67 -0
  228. alysis_code/hooks/audit.py +171 -0
  229. alysis_code/hooks/config.py +225 -0
  230. alysis_code/hooks/dispatcher.py +1110 -0
  231. alysis_code/hooks/models.py +447 -0
  232. alysis_code/hooks/trust.py +202 -0
  233. alysis_code/host_actions.py +543 -0
  234. alysis_code/host_browser.py +103 -0
  235. alysis_code/ide/__init__.py +5 -0
  236. alysis_code/ide/activity_events.py +399 -0
  237. alysis_code/ide/approvals.py +337 -0
  238. alysis_code/ide/artifacts.py +153 -0
  239. alysis_code/ide/browser_egress_proxy.py +1076 -0
  240. alysis_code/ide/cdp_websocket_transport.py +1192 -0
  241. alysis_code/ide/change_ledger.py +1721 -0
  242. alysis_code/ide/context_blocks.py +979 -0
  243. alysis_code/ide/event_stream.py +531 -0
  244. alysis_code/ide/forge_protocol.py +3112 -0
  245. alysis_code/ide/forge_request_ledger.py +737 -0
  246. alysis_code/ide/health.py +965 -0
  247. alysis_code/ide/managed_browser.py +2251 -0
  248. alysis_code/ide/management_protocol.py +3414 -0
  249. alysis_code/ide/mcp_oauth_coordinator.py +744 -0
  250. alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
  251. alysis_code/ide/prompt_queue.py +1070 -0
  252. alysis_code/ide/protocol.py +191 -0
  253. alysis_code/ide/resumable_swarm.py +1543 -0
  254. alysis_code/ide/session_search.py +295 -0
  255. alysis_code/ide/stdio_bridge.py +9935 -0
  256. alysis_code/ide/structured_state.py +1579 -0
  257. alysis_code/ide/swarm_protocol.py +816 -0
  258. alysis_code/integration_gate.py +506 -0
  259. alysis_code/interactive_input_guard.py +39 -0
  260. alysis_code/interactive_plan_mode.py +26 -0
  261. alysis_code/internal_artifacts.py +179 -0
  262. alysis_code/knowledge_base.py +1409 -0
  263. alysis_code/knowledge_capture.py +1190 -0
  264. alysis_code/knowledge_librarian.py +605 -0
  265. alysis_code/language_policy.py +34 -0
  266. alysis_code/litellm_static_provider.py +535 -0
  267. alysis_code/llm/__init__.py +1 -0
  268. alysis_code/llm/anthropic_messages.py +2288 -0
  269. alysis_code/llm/base.py +71 -0
  270. alysis_code/llm/cache_capabilities.py +985 -0
  271. alysis_code/llm/cache_control_blocks.py +244 -0
  272. alysis_code/llm/cache_policy.py +388 -0
  273. alysis_code/llm/factory.py +373 -0
  274. alysis_code/llm/gemini_generate_content.py +2652 -0
  275. alysis_code/llm/gemini_interactions.py +739 -0
  276. alysis_code/llm/metadata.py +450 -0
  277. alysis_code/llm/openai_compat.py +2947 -0
  278. alysis_code/llm/openai_responses.py +2604 -0
  279. alysis_code/llm/protocols.py +609 -0
  280. alysis_code/llm/provider_limits.py +525 -0
  281. alysis_code/llm/request_plan.py +389 -0
  282. alysis_code/llm/request_shape.py +238 -0
  283. alysis_code/llm/streaming.py +108 -0
  284. alysis_code/llm/temperature_compat.py +78 -0
  285. alysis_code/llm/types.py +195 -0
  286. alysis_code/llm/usage_normalization.py +222 -0
  287. alysis_code/llm_error_display.py +315 -0
  288. alysis_code/logging_redaction.py +326 -0
  289. alysis_code/managed_host_deadline.py +196 -0
  290. alysis_code/mcp/README.md +33 -0
  291. alysis_code/mcp/__init__.py +24 -0
  292. alysis_code/mcp/client.py +1137 -0
  293. alysis_code/mcp/config.py +597 -0
  294. alysis_code/mcp/errors.py +113 -0
  295. alysis_code/mcp/forge_scope.py +154 -0
  296. alysis_code/mcp/jsonrpc.py +214 -0
  297. alysis_code/mcp/manager.py +2308 -0
  298. alysis_code/mcp/models.py +666 -0
  299. alysis_code/mcp/oauth.py +972 -0
  300. alysis_code/mcp/oauth_runtime.py +310 -0
  301. alysis_code/mcp/oauth_store.py +276 -0
  302. alysis_code/mcp/prompts.py +329 -0
  303. alysis_code/mcp/resources.py +295 -0
  304. alysis_code/mcp/roots.py +106 -0
  305. alysis_code/mcp/server_requests.py +75 -0
  306. alysis_code/mcp/token_store.py +859 -0
  307. alysis_code/mcp/transport_http.py +1338 -0
  308. alysis_code/mcp/transport_stdio.py +1267 -0
  309. alysis_code/mcp/untrusted_content.py +119 -0
  310. alysis_code/merge_conflict_reviewer.py +729 -0
  311. alysis_code/model_catalog/__init__.py +1 -0
  312. alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
  313. alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
  314. alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
  315. alysis_code/model_metadata_policy.py +223 -0
  316. alysis_code/model_metadata_utils.py +103 -0
  317. alysis_code/model_registry.py +1420 -0
  318. alysis_code/model_router.py +147 -0
  319. alysis_code/permission_policy.py +1016 -0
  320. alysis_code/personas.py +451 -0
  321. alysis_code/pipeline_facts.py +233 -0
  322. alysis_code/plan_assistant.py +4763 -0
  323. alysis_code/plan_mode.py +393 -0
  324. alysis_code/plan_reconciliation.py +1228 -0
  325. alysis_code/plan_repair.py +652 -0
  326. alysis_code/plan_validation.py +1099 -0
  327. alysis_code/planning_constraints.py +904 -0
  328. alysis_code/policy.py +95 -0
  329. alysis_code/preview_server.py +457 -0
  330. alysis_code/process_reaping.py +566 -0
  331. alysis_code/profile_presets.py +1834 -0
  332. alysis_code/profiles.py +666 -0
  333. alysis_code/provider_auth/__init__.py +29 -0
  334. alysis_code/provider_auth/base.py +99 -0
  335. alysis_code/provider_auth/openai_codex.py +951 -0
  336. alysis_code/provider_auth/registry.py +76 -0
  337. alysis_code/provider_auth/store.py +125 -0
  338. alysis_code/provider_diagnostics.py +1209 -0
  339. alysis_code/provider_model_catalog.py +685 -0
  340. alysis_code/provider_telemetry.py +1699 -0
  341. alysis_code/provider_url.py +75 -0
  342. alysis_code/reasoning_contracts.py +911 -0
  343. alysis_code/remote_sync.py +350 -0
  344. alysis_code/replanning.py +1195 -0
  345. alysis_code/repo_scan.py +1152 -0
  346. alysis_code/request_estimation.py +296 -0
  347. alysis_code/review_gate.py +617 -0
  348. alysis_code/run_lock.py +1141 -0
  349. alysis_code/run_outcome.py +58 -0
  350. alysis_code/run_provenance.py +774 -0
  351. alysis_code/run_state.py +445 -0
  352. alysis_code/runtime_artifacts.py +116 -0
  353. alysis_code/runtime_context_features.py +78 -0
  354. alysis_code/runtime_kind.py +52 -0
  355. alysis_code/safety/__init__.py +11 -0
  356. alysis_code/safety/mcp_sanitize.py +29 -0
  357. alysis_code/safety/safe_http.py +297 -0
  358. alysis_code/safety/subagent_report.py +184 -0
  359. alysis_code/sandbox_doctor.py +682 -0
  360. alysis_code/sandbox_runner.py +1025 -0
  361. alysis_code/sandbox_settings.py +423 -0
  362. alysis_code/serialized_paths.py +355 -0
  363. alysis_code/server/__init__.py +3 -0
  364. alysis_code/server/app.py +367 -0
  365. alysis_code/server/auth.py +34 -0
  366. alysis_code/server/job_config.py +30 -0
  367. alysis_code/server/settings.py +215 -0
  368. alysis_code/server/store.py +193 -0
  369. alysis_code/server/worker_runner.py +657 -0
  370. alysis_code/service_persistence.py +355 -0
  371. alysis_code/session_artifacts.py +108 -0
  372. alysis_code/session_metrics.py +331 -0
  373. alysis_code/session_store.py +624 -0
  374. alysis_code/skills/README.md +34 -0
  375. alysis_code/skills/__init__.py +104 -0
  376. alysis_code/skills/conventions.py +84 -0
  377. alysis_code/skills/discovery.py +176 -0
  378. alysis_code/skills/eval_models.py +232 -0
  379. alysis_code/skills/eval_runner.py +372 -0
  380. alysis_code/skills/evals.py +1344 -0
  381. alysis_code/skills/install.py +293 -0
  382. alysis_code/skills/loader.py +118 -0
  383. alysis_code/skills/matching.py +103 -0
  384. alysis_code/skills/models.py +71 -0
  385. alysis_code/skills/paths.py +56 -0
  386. alysis_code/skills/prompting.py +500 -0
  387. alysis_code/skills/scaffold.py +142 -0
  388. alysis_code/skills/state.py +441 -0
  389. alysis_code/skills/transactions.py +125 -0
  390. alysis_code/skills/validation.py +304 -0
  391. alysis_code/step_budget.py +238 -0
  392. alysis_code/subagent_labels.py +49 -0
  393. alysis_code/subagents.py +1072 -0
  394. alysis_code/surface/__init__.py +80 -0
  395. alysis_code/surface/base.py +305 -0
  396. alysis_code/surface/console.py +387 -0
  397. alysis_code/surface/events.py +372 -0
  398. alysis_code/surface/hidden_surface.py +529 -0
  399. alysis_code/surface/noop_surface.py +219 -0
  400. alysis_code/surface/rich_surface.py +1555 -0
  401. alysis_code/surface/styles.py +67 -0
  402. alysis_code/surface/theme.py +455 -0
  403. alysis_code/surface/types.py +100 -0
  404. alysis_code/swarm_backend.py +926 -0
  405. alysis_code/swarm_orchestrator.py +4020 -0
  406. alysis_code/swarm_scheduler.py +441 -0
  407. alysis_code/swarm_trace.py +429 -0
  408. alysis_code/swarm_worker.py +2119 -0
  409. alysis_code/swarm_write_guard.py +348 -0
  410. alysis_code/task_dependencies.py +170 -0
  411. alysis_code/task_readiness.py +992 -0
  412. alysis_code/task_scope.py +2148 -0
  413. alysis_code/terminal_manager.py +762 -0
  414. alysis_code/terminal_ownership.py +460 -0
  415. alysis_code/text_normalization.py +30 -0
  416. alysis_code/token_budget.py +97 -0
  417. alysis_code/tools/README.md +34 -0
  418. alysis_code/tools/__init__.py +1 -0
  419. alysis_code/tools/artifacts.py +127 -0
  420. alysis_code/tools/availability.py +188 -0
  421. alysis_code/tools/fs.py +1456 -0
  422. alysis_code/tools/git.py +461 -0
  423. alysis_code/tools/history.py +229 -0
  424. alysis_code/tools/http_timeout.py +78 -0
  425. alysis_code/tools/image_generation.py +552 -0
  426. alysis_code/tools/registry.py +2936 -0
  427. alysis_code/tools/repo_map.py +476 -0
  428. alysis_code/tools/search.py +563 -0
  429. alysis_code/tools/shell.py +135 -0
  430. alysis_code/tools/symbols.py +1350 -0
  431. alysis_code/tools/test_discovery.py +643 -0
  432. alysis_code/tools/web.py +482 -0
  433. alysis_code/tools/web_search.py +2012 -0
  434. alysis_code/tools/web_search_dashscope.py +557 -0
  435. alysis_code/tools/web_search_ddgs.py +221 -0
  436. alysis_code/tools/web_search_provider_adapters.py +1429 -0
  437. alysis_code/tools/web_search_tavily.py +194 -0
  438. alysis_code/updates.py +933 -0
  439. alysis_code/usage_tracker.py +1990 -0
  440. alysis_code/verification_command_analysis.py +1004 -0
  441. alysis_code/verification_contract.py +574 -0
  442. alysis_code/verification_failure_summary.py +273 -0
  443. alysis_code/verification_repair.py +385 -0
  444. alysis_code/verify_gate.py +3129 -0
  445. alysis_code/web_research.py +1872 -0
  446. alysis_code/web_search_adapters.py +66 -0
  447. alysis_code/web_search_policy.py +27 -0
  448. alysis_code/workspace_binding.py +389 -0
  449. alysis_code/workspace_binding_ui.py +408 -0
  450. alysis_code/workspace_context.py +273 -0
  451. alysis_code/workspace_isolation.py +138 -0
  452. alysis_code/workspace_provisioning.py +455 -0
  453. alysis_code-0.13.0.dist-info/METADATA +507 -0
  454. alysis_code-0.13.0.dist-info/RECORD +458 -0
  455. alysis_code-0.13.0.dist-info/WHEEL +4 -0
  456. alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
  457. alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
  458. 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()]