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,275 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import mimetypes
5
+ import os
6
+ import shutil
7
+ import tempfile
8
+ import uuid
9
+ from contextlib import suppress
10
+ from dataclasses import replace
11
+ from pathlib import Path
12
+ from typing import Any, Literal
13
+
14
+ from ..forge import RunPaths, now_iso
15
+ from .index import AssetIndex
16
+ from .models import AssetAlreadyExistsError, AssetError, AssetRecord
17
+ from .paths import asset_raw_path, asset_thumbnail_path, repo_rel
18
+
19
+ _TEXT_SUFFIXES = {
20
+ ".txt",
21
+ ".md",
22
+ ".markdown",
23
+ ".json",
24
+ ".yaml",
25
+ ".yml",
26
+ ".toml",
27
+ ".ini",
28
+ ".cfg",
29
+ ".csv",
30
+ ".tsv",
31
+ ".log",
32
+ ".py",
33
+ ".js",
34
+ ".jsx",
35
+ ".ts",
36
+ ".tsx",
37
+ ".html",
38
+ ".css",
39
+ ".xml",
40
+ ".sql",
41
+ ".sh",
42
+ ".ps1",
43
+ }
44
+ _IMAGE_THUMB_MAX_EDGE = 512
45
+
46
+
47
+ def ingest_asset(
48
+ path: Path,
49
+ *,
50
+ title: str,
51
+ description: str = "",
52
+ run_paths: RunPaths,
53
+ pinned: bool = False,
54
+ added_by: dict[str, Any] | None = None,
55
+ dedupe_policy: Literal["reject", "link"] = "reject",
56
+ ) -> AssetRecord:
57
+ source = path.expanduser().resolve()
58
+ clean_title = title.strip()
59
+ if not clean_title:
60
+ raise AssetError("Asset title is required.")
61
+ if not source.exists():
62
+ raise AssetError(f"Asset file does not exist: {source}")
63
+ if not source.is_file():
64
+ raise AssetError(f"Asset path is not a file: {source}")
65
+
66
+ raw = source.read_bytes()
67
+ digest = hashlib.sha256(raw).hexdigest()
68
+ index = AssetIndex(run_paths)
69
+ existing = index.find_by_sha256(digest)
70
+ if existing is not None:
71
+ if dedupe_policy == "link":
72
+ return existing
73
+ raise AssetAlreadyExistsError(existing.id)
74
+
75
+ kind, mime = _classify_asset(source, raw)
76
+ staging_dir = _staging_dir(run_paths)
77
+ staged_raw = staging_dir / source.name
78
+ staged_thumbnail = staging_dir / "thumbnail.png"
79
+ try:
80
+ _atomic_write_bytes(staged_raw, raw)
81
+ if kind == "image":
82
+ _write_thumbnail(source, staged_thumbnail)
83
+ return index.add_with_publish(
84
+ sha256=digest,
85
+ dedupe_policy=dedupe_policy,
86
+ build_record=lambda existing_ids: _build_record(
87
+ run_paths=run_paths,
88
+ existing_ids=existing_ids,
89
+ digest=digest,
90
+ title=clean_title,
91
+ description=description,
92
+ kind=kind,
93
+ mime=mime,
94
+ original_filename=source.name,
95
+ size_bytes=len(raw),
96
+ pinned=pinned,
97
+ added_by=added_by,
98
+ ),
99
+ publish=lambda record: _publish_staged_asset(
100
+ run_paths=run_paths,
101
+ record=record,
102
+ staged_raw=staged_raw,
103
+ staged_thumbnail=staged_thumbnail if kind == "image" else None,
104
+ ),
105
+ rollback=lambda record: _rollback_published_asset(run_paths, record),
106
+ )
107
+ finally:
108
+ with suppress(OSError):
109
+ shutil.rmtree(staging_dir)
110
+
111
+
112
+ def _staging_dir(run_paths: RunPaths) -> Path:
113
+ return run_paths.assets_raw_dir / ".staging" / uuid.uuid4().hex
114
+
115
+
116
+ def _build_record(
117
+ *,
118
+ run_paths: RunPaths,
119
+ existing_ids: set[str],
120
+ digest: str,
121
+ title: str,
122
+ description: str,
123
+ kind: Literal["text", "image"],
124
+ mime: str,
125
+ original_filename: str,
126
+ size_bytes: int,
127
+ pinned: bool,
128
+ added_by: dict[str, Any] | None,
129
+ ) -> AssetRecord:
130
+ asset_id = _asset_id_for_sha256(digest, existing_ids)
131
+ stored_path = asset_raw_path(run_paths, asset_id, original_filename)
132
+ record = AssetRecord(
133
+ id=asset_id,
134
+ title=title,
135
+ description=description,
136
+ kind=kind,
137
+ mime=mime,
138
+ original_filename=original_filename,
139
+ size_bytes=size_bytes,
140
+ sha256=digest,
141
+ stored_path=repo_rel(run_paths.root, stored_path),
142
+ extracted_text_path=None,
143
+ thumbnail_path=None,
144
+ pinned=bool(pinned),
145
+ added_at=now_iso(),
146
+ added_by=dict(added_by or {}),
147
+ deleted_at=None,
148
+ comprehension_status="pending",
149
+ comprehension_current_version=None,
150
+ )
151
+ if kind == "text":
152
+ return _with_text_paths(record)
153
+ return _with_thumbnail(
154
+ record, repo_rel(run_paths.root, asset_thumbnail_path(run_paths, asset_id))
155
+ )
156
+
157
+
158
+ def _publish_staged_asset(
159
+ *,
160
+ run_paths: RunPaths,
161
+ record: AssetRecord,
162
+ staged_raw: Path,
163
+ staged_thumbnail: Path | None,
164
+ ) -> None:
165
+ final_raw = run_paths.root / record.stored_path
166
+ final_raw.parent.mkdir(parents=True, exist_ok=True)
167
+ os.replace(staged_raw, final_raw)
168
+ if staged_thumbnail is not None and record.thumbnail_path is not None:
169
+ final_thumbnail = run_paths.root / record.thumbnail_path
170
+ final_thumbnail.parent.mkdir(parents=True, exist_ok=True)
171
+ os.replace(staged_thumbnail, final_thumbnail)
172
+
173
+
174
+ def _rollback_published_asset(run_paths: RunPaths, record: AssetRecord) -> None:
175
+ with suppress(OSError):
176
+ shutil.rmtree((run_paths.root / record.stored_path).parent)
177
+ if record.thumbnail_path is not None:
178
+ with suppress(FileNotFoundError):
179
+ (run_paths.root / record.thumbnail_path).unlink()
180
+
181
+
182
+ def _with_text_paths(record: AssetRecord) -> AssetRecord:
183
+ return replace(record, extracted_text_path=record.stored_path)
184
+
185
+
186
+ def _with_thumbnail(record: AssetRecord, thumbnail_path: str) -> AssetRecord:
187
+ return replace(record, thumbnail_path=thumbnail_path)
188
+
189
+
190
+ def _classify_asset(source: Path, raw: bytes) -> tuple[Literal["text", "image"], str]:
191
+ mime, _ = mimetypes.guess_type(source.name)
192
+ suffix = source.suffix.lower()
193
+ if mime and mime.startswith("image/"):
194
+ _ensure_pillow_can_open(source)
195
+ return "image", mime
196
+ if mime and mime.startswith("text/"):
197
+ _ensure_utf8_text(raw, source)
198
+ return "text", mime
199
+ if suffix in _TEXT_SUFFIXES:
200
+ _ensure_utf8_text(raw, source)
201
+ return "text", mime or "text/plain"
202
+ raise AssetError(f"Unsupported asset file type: {source.name}")
203
+
204
+
205
+ def _ensure_utf8_text(raw: bytes, source: Path) -> None:
206
+ if b"\x00" in raw:
207
+ raise AssetError(f"Unsupported binary text asset: {source.name}")
208
+ try:
209
+ raw.decode("utf-8")
210
+ except UnicodeDecodeError as e:
211
+ raise AssetError(f"Text asset must be UTF-8: {source.name}") from e
212
+
213
+
214
+ def _ensure_pillow_can_open(source: Path) -> None:
215
+ try:
216
+ from PIL import Image
217
+ except ModuleNotFoundError as e:
218
+ raise AssetError("Pillow is required for image asset ingestion.") from e
219
+ try:
220
+ with Image.open(source) as image:
221
+ image.verify()
222
+ except Exception as e: # noqa: BLE001 - Pillow raises multiple format-specific errors
223
+ raise AssetError(f"Unsupported image asset: {source.name}") from e
224
+
225
+
226
+ def _write_thumbnail(source: Path, destination: Path) -> None:
227
+ try:
228
+ from PIL import Image
229
+ except ModuleNotFoundError as e:
230
+ raise AssetError("Pillow is required for image asset thumbnails.") from e
231
+ with Image.open(source) as image:
232
+ normalized = image.convert("RGB")
233
+ normalized.thumbnail((_IMAGE_THUMB_MAX_EDGE, _IMAGE_THUMB_MAX_EDGE))
234
+ destination.parent.mkdir(parents=True, exist_ok=True)
235
+ fd, temp_name = tempfile.mkstemp(
236
+ dir=destination.parent,
237
+ prefix=f".{destination.name}.",
238
+ suffix=".tmp",
239
+ )
240
+ temp_path = Path(temp_name)
241
+ os.close(fd)
242
+ try:
243
+ normalized.save(temp_path, format="PNG")
244
+ os.replace(temp_path, destination)
245
+ finally:
246
+ with suppress(FileNotFoundError):
247
+ temp_path.unlink()
248
+
249
+
250
+ def _atomic_write_bytes(path: Path, data: bytes) -> None:
251
+ path.parent.mkdir(parents=True, exist_ok=True)
252
+ fd, temp_name = tempfile.mkstemp(
253
+ dir=path.parent,
254
+ prefix=f".{path.name}.",
255
+ suffix=".tmp",
256
+ )
257
+ temp_path = Path(temp_name)
258
+ try:
259
+ with os.fdopen(fd, "wb") as handle:
260
+ handle.write(data)
261
+ handle.flush()
262
+ os.fsync(handle.fileno())
263
+ os.replace(temp_path, path)
264
+ finally:
265
+ with suppress(FileNotFoundError):
266
+ temp_path.unlink()
267
+
268
+
269
+ def _asset_id_for_sha256(sha256: str, existing_ids: set[str]) -> str:
270
+ digest = sha256.strip().lower()
271
+ for offset in range(0, max(len(digest) - 7, 1), 8):
272
+ candidate = f"ast_{digest[offset : offset + 8]}"
273
+ if len(candidate) == 12 and candidate not in existing_ids:
274
+ return candidate
275
+ raise AssetError("Unable to allocate stable asset id without collision.")
@@ -0,0 +1,413 @@
1
+ from __future__ import annotations
2
+
3
+ import errno
4
+ import hashlib
5
+ import json
6
+ import logging
7
+ import os
8
+ import socket
9
+ import time
10
+ from contextlib import suppress
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import Any, Literal
14
+
15
+ from ..config import AppConfig
16
+ from ..forge import RunPaths, now_iso
17
+ from .models import AssetAlreadyExistsError, AssetError
18
+ from .surface import AssetSurface
19
+
20
+ LOGGER = logging.getLogger(__name__)
21
+
22
+ PLAN_SCHEMA_VERSION_V1 = 1
23
+ PLAN_SCHEMA_VERSION_V2 = 2
24
+ _LOCK_POLL_SECONDS = 0.05
25
+ _LOCK_WAIT_SECONDS = 30.0
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class MigratedLegacyAsset:
30
+ legacy_stored_path: str
31
+ legacy_text_copy_path: str | None
32
+ new_asset_id: str
33
+ title: str
34
+ description: str
35
+ comprehension_handle_running: bool
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class LegacyAssetMigrationResult:
40
+ schema_version_before: int
41
+ schema_version_after: int
42
+ migrated_assets: list[MigratedLegacyAsset]
43
+ skipped_existing: list[str]
44
+ failed: list[tuple[str, str]]
45
+ plan_assets_array_cleared: bool
46
+ plan_v2_written: bool
47
+
48
+
49
+ def migrate_legacy_assets(
50
+ *,
51
+ cfg: AppConfig,
52
+ run_paths: RunPaths,
53
+ surface: AssetSurface,
54
+ comprehend_mode: Literal["sync", "async", "skip"] = "async",
55
+ ) -> LegacyAssetMigrationResult:
56
+ plan = _read_plan(run_paths.plan_json_path)
57
+ schema_before = _schema_version(plan)
58
+ if schema_before >= PLAN_SCHEMA_VERSION_V2:
59
+ return LegacyAssetMigrationResult(
60
+ schema_version_before=schema_before,
61
+ schema_version_after=schema_before,
62
+ migrated_assets=[],
63
+ skipped_existing=[],
64
+ failed=[],
65
+ plan_assets_array_cleared=False,
66
+ plan_v2_written=False,
67
+ )
68
+
69
+ with LegacyMigrationLock(run_paths):
70
+ plan = _read_plan(run_paths.plan_json_path)
71
+ schema_before = _schema_version(plan)
72
+ if schema_before >= PLAN_SCHEMA_VERSION_V2:
73
+ return LegacyAssetMigrationResult(
74
+ schema_version_before=schema_before,
75
+ schema_version_after=schema_before,
76
+ migrated_assets=[],
77
+ skipped_existing=[],
78
+ failed=[],
79
+ plan_assets_array_cleared=False,
80
+ plan_v2_written=False,
81
+ )
82
+ result = _migrate_locked(
83
+ cfg=cfg,
84
+ run_paths=run_paths,
85
+ surface=surface,
86
+ plan=plan,
87
+ schema_before=schema_before,
88
+ comprehend_mode="skip" if not cfg.assets.enabled else comprehend_mode,
89
+ )
90
+ return result
91
+
92
+
93
+ class LegacyMigrationLock:
94
+ def __init__(self, run_paths: RunPaths) -> None:
95
+ self.path = run_paths.run_dir / "legacy_migration.lock"
96
+ self._fd: int | None = None
97
+
98
+ def __enter__(self) -> LegacyMigrationLock:
99
+ deadline = time.monotonic() + _LOCK_WAIT_SECONDS
100
+ while True:
101
+ try:
102
+ self.path.parent.mkdir(parents=True, exist_ok=True)
103
+ fd = os.open(self.path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
104
+ except FileExistsError:
105
+ if self._recover_stale_lock():
106
+ continue
107
+ if time.monotonic() >= deadline:
108
+ raise AssetError(
109
+ f"Timed out waiting for legacy asset migration lock: {self.path}"
110
+ ) from None
111
+ time.sleep(_LOCK_POLL_SECONDS)
112
+ continue
113
+ self._fd = fd
114
+ payload = {
115
+ "pid": os.getpid(),
116
+ "hostname": socket.gethostname(),
117
+ "created_at": now_iso(),
118
+ "created_epoch": time.time(),
119
+ }
120
+ os.write(fd, (json.dumps(payload, sort_keys=True) + "\n").encode("utf-8"))
121
+ os.fsync(fd)
122
+ return self
123
+
124
+ def __exit__(self, exc_type: object, exc: object, tb: object) -> None:
125
+ if self._fd is not None:
126
+ os.close(self._fd)
127
+ self._fd = None
128
+ with suppress(FileNotFoundError):
129
+ self.path.unlink()
130
+
131
+ def _recover_stale_lock(self) -> bool:
132
+ try:
133
+ payload = json.loads(self.path.read_text(encoding="utf-8"))
134
+ except (OSError, json.JSONDecodeError):
135
+ return False
136
+ if not isinstance(payload, dict):
137
+ return False
138
+ created_epoch = _safe_float(payload.get("created_epoch"))
139
+ if created_epoch is None or time.time() - created_epoch < _LOCK_WAIT_SECONDS:
140
+ return False
141
+ hostname = str(payload.get("hostname") or "").strip()
142
+ if hostname and hostname != socket.gethostname():
143
+ return False
144
+ pid = _safe_int(payload.get("pid"))
145
+ if pid is not None and _process_is_running(pid) is not False:
146
+ return False
147
+ with suppress(FileNotFoundError):
148
+ self.path.unlink()
149
+ return True
150
+
151
+
152
+ def _migrate_locked(
153
+ *,
154
+ cfg: AppConfig,
155
+ run_paths: RunPaths,
156
+ surface: AssetSurface,
157
+ plan: dict[str, Any],
158
+ schema_before: int,
159
+ comprehend_mode: Literal["sync", "async", "skip"],
160
+ ) -> LegacyAssetMigrationResult:
161
+ legacy_assets = plan.get("assets")
162
+ if not isinstance(legacy_assets, list):
163
+ raise AssetError("Invalid legacy plan: 'assets' must be an array before migration.")
164
+ title_counts: dict[str, int] = {}
165
+ migrated: list[MigratedLegacyAsset] = []
166
+ skipped: list[str] = []
167
+ failed: list[tuple[str, str]] = []
168
+
169
+ for entry in legacy_assets:
170
+ if not isinstance(entry, dict):
171
+ failed.append(("(invalid legacy asset entry)", "Legacy asset entry must be an object"))
172
+ continue
173
+ stored_path = str(entry.get("stored_path") or "").strip()
174
+ text_copy_path = _optional_string(entry.get("text_copy_path"))
175
+ if not stored_path:
176
+ failed.append(("(missing stored_path)", "Legacy asset is missing stored_path"))
177
+ continue
178
+ source = (run_paths.root / stored_path).resolve()
179
+ try:
180
+ _ensure_under_root(run_paths.root, source)
181
+ digest = hashlib.sha256(source.read_bytes()).hexdigest()
182
+ except OSError as exc:
183
+ failed.append((stored_path, str(exc)))
184
+ continue
185
+ except AssetError as exc:
186
+ failed.append((stored_path, str(exc)))
187
+ continue
188
+
189
+ existing = surface.index.find_by_sha256(digest, include_deleted=False)
190
+ if existing is not None:
191
+ skipped.append(stored_path)
192
+ LOGGER.info(
193
+ "legacy_asset_migration skipped existing legacy_stored_path=%s asset_id=%s",
194
+ stored_path,
195
+ existing.id,
196
+ )
197
+ continue
198
+
199
+ title = _deduped_title(_derive_title(source), title_counts)
200
+ try:
201
+ add_result = surface.add_asset(
202
+ source,
203
+ title=title,
204
+ description="",
205
+ pinned=False,
206
+ added_by={
207
+ "phase": "legacy_migration",
208
+ "schema_version_before": schema_before,
209
+ "legacy_stored_path": stored_path,
210
+ },
211
+ comprehend=comprehend_mode,
212
+ dedupe_policy="link",
213
+ )
214
+ except AssetAlreadyExistsError as exc:
215
+ skipped.append(stored_path)
216
+ LOGGER.info(
217
+ "legacy_asset_migration skipped dedupe legacy_stored_path=%s asset_id=%s",
218
+ stored_path,
219
+ exc.existing_id,
220
+ )
221
+ continue
222
+ except AssetError as exc:
223
+ failed.append((stored_path, str(exc)))
224
+ continue
225
+ handle = add_result.comprehension_handle
226
+ migrated.append(
227
+ MigratedLegacyAsset(
228
+ legacy_stored_path=stored_path,
229
+ legacy_text_copy_path=text_copy_path,
230
+ new_asset_id=add_result.record.id,
231
+ title=add_result.record.title,
232
+ description=add_result.record.description,
233
+ comprehension_handle_running=bool(handle is not None and not handle.is_done()),
234
+ )
235
+ )
236
+ LOGGER.info(
237
+ "legacy_asset_migration migrated legacy_stored_path=%s asset_id=%s comprehend=%s",
238
+ stored_path,
239
+ add_result.record.id,
240
+ comprehend_mode,
241
+ )
242
+
243
+ if failed:
244
+ return LegacyAssetMigrationResult(
245
+ schema_version_before=schema_before,
246
+ schema_version_after=schema_before,
247
+ migrated_assets=migrated,
248
+ skipped_existing=skipped,
249
+ failed=failed,
250
+ plan_assets_array_cleared=False,
251
+ plan_v2_written=False,
252
+ )
253
+
254
+ plan["schema_version"] = PLAN_SCHEMA_VERSION_V2
255
+ plan["assets"] = []
256
+ plan["legacy_assets_migrated_at"] = now_iso()
257
+ _bind_migrated_assets_to_matching_tasks(plan=plan, surface=surface, migrated=migrated)
258
+ from ..forge import save_plan
259
+
260
+ save_plan(run_paths, plan)
261
+ return LegacyAssetMigrationResult(
262
+ schema_version_before=schema_before,
263
+ schema_version_after=PLAN_SCHEMA_VERSION_V2,
264
+ migrated_assets=migrated,
265
+ skipped_existing=skipped,
266
+ failed=[],
267
+ plan_assets_array_cleared=True,
268
+ plan_v2_written=True,
269
+ )
270
+
271
+
272
+ def _bind_migrated_assets_to_matching_tasks(
273
+ *,
274
+ plan: dict[str, Any],
275
+ surface: AssetSurface,
276
+ migrated: list[MigratedLegacyAsset],
277
+ ) -> None:
278
+ if not migrated:
279
+ return
280
+ from .plan_binding import bind_asset_to_matching_tasks
281
+
282
+ try:
283
+ active_records = surface.index.records(include_deleted=False)
284
+ except AssetError:
285
+ return
286
+ by_id = {record.id: record for record in active_records}
287
+ for item in migrated:
288
+ record = by_id.get(item.new_asset_id)
289
+ if record is None:
290
+ continue
291
+ bind_asset_to_matching_tasks(
292
+ plan=plan,
293
+ record=record,
294
+ active_records=active_records,
295
+ )
296
+
297
+
298
+ def _read_plan(path: Path) -> dict[str, Any]:
299
+ try:
300
+ payload = json.loads(path.read_text(encoding="utf-8"))
301
+ except OSError as exc:
302
+ raise AssetError(f"Failed to read plan for legacy migration: {path}") from exc
303
+ except json.JSONDecodeError as exc:
304
+ raise AssetError(f"Invalid plan JSON for legacy migration: {path}") from exc
305
+ if not isinstance(payload, dict):
306
+ raise AssetError(f"Invalid plan structure for legacy migration: {path}")
307
+ return payload
308
+
309
+
310
+ def _schema_version(plan: dict[str, Any]) -> int:
311
+ try:
312
+ return int(plan.get("schema_version", PLAN_SCHEMA_VERSION_V1) or PLAN_SCHEMA_VERSION_V1)
313
+ except (TypeError, ValueError):
314
+ return PLAN_SCHEMA_VERSION_V1
315
+
316
+
317
+ def _derive_title(source: Path) -> str:
318
+ stem = source.stem.replace("_", " ").replace("-", " ").strip().casefold()
319
+ stem = " ".join(stem.split())
320
+ if not stem:
321
+ return "Legacy asset"
322
+ return stem[:1].upper() + stem[1:]
323
+
324
+
325
+ def _deduped_title(title: str, counts: dict[str, int]) -> str:
326
+ count = counts.get(title, 0) + 1
327
+ counts[title] = count
328
+ return title if count == 1 else f"{title} {count}"
329
+
330
+
331
+ def _optional_string(value: Any) -> str | None:
332
+ if value is None:
333
+ return None
334
+ text = str(value).strip()
335
+ return text or None
336
+
337
+
338
+ def _safe_int(value: Any) -> int | None:
339
+ try:
340
+ return int(value)
341
+ except (TypeError, ValueError):
342
+ return None
343
+
344
+
345
+ def _safe_float(value: Any) -> float | None:
346
+ try:
347
+ return float(value)
348
+ except (TypeError, ValueError):
349
+ return None
350
+
351
+
352
+ def _process_is_running(pid: int) -> bool | None:
353
+ try:
354
+ if pid <= 0:
355
+ return False
356
+ except TypeError:
357
+ return None
358
+ if os.name == "nt":
359
+ return _windows_process_is_running(pid)
360
+ try:
361
+ os.kill(pid, 0)
362
+ except ProcessLookupError:
363
+ return False
364
+ except PermissionError:
365
+ return True
366
+ except OSError as exc:
367
+ if getattr(exc, "errno", None) == errno.ESRCH:
368
+ return False
369
+ return None
370
+ return True
371
+
372
+
373
+ def _windows_process_is_running(pid: int) -> bool | None:
374
+ try:
375
+ import ctypes
376
+ from ctypes import wintypes
377
+
378
+ process_query_limited_information = 0x1000
379
+ still_active = 259
380
+ error_invalid_parameter = 87
381
+ kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
382
+ kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
383
+ kernel32.OpenProcess.restype = wintypes.HANDLE
384
+ kernel32.GetExitCodeProcess.argtypes = [
385
+ wintypes.HANDLE,
386
+ ctypes.POINTER(wintypes.DWORD),
387
+ ]
388
+ kernel32.GetExitCodeProcess.restype = wintypes.BOOL
389
+ kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
390
+ kernel32.CloseHandle.restype = wintypes.BOOL
391
+
392
+ handle = kernel32.OpenProcess(process_query_limited_information, False, pid)
393
+ if not handle:
394
+ error = ctypes.get_last_error()
395
+ if error == error_invalid_parameter:
396
+ return False
397
+ return None
398
+ exit_code = wintypes.DWORD()
399
+ try:
400
+ if not kernel32.GetExitCodeProcess(handle, ctypes.byref(exit_code)):
401
+ return None
402
+ return exit_code.value == still_active
403
+ finally:
404
+ kernel32.CloseHandle(handle)
405
+ except Exception:
406
+ return None
407
+
408
+
409
+ def _ensure_under_root(root: Path, path: Path) -> None:
410
+ try:
411
+ path.resolve().relative_to(root.resolve())
412
+ except ValueError as exc:
413
+ raise AssetError(f"Legacy asset path escapes workspace root: {path}") from exc