iCode-TUI 0.29.1__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 (1138) hide show
  1. chrys/__init__.py +16 -0
  2. chrys/app/__init__.py +3 -0
  3. chrys/app/acp/__init__.py +3 -0
  4. chrys/app/acp/bridge.py +664 -0
  5. chrys/app/acp/content.py +132 -0
  6. chrys/app/acp/doc/frontend-api.md +425 -0
  7. chrys/app/acp/history.py +585 -0
  8. chrys/app/acp/server.py +1796 -0
  9. chrys/app/acp/session_manager.py +1815 -0
  10. chrys/app/acp/tool_status.py +45 -0
  11. chrys/app/cli/__init__.py +3 -0
  12. chrys/app/cli/acp.py +178 -0
  13. chrys/app/cli/app.py +165 -0
  14. chrys/app/cli/approvals.py +101 -0
  15. chrys/app/cli/headless.py +223 -0
  16. chrys/app/cli/launch_cwd.py +42 -0
  17. chrys/app/cli/profiles.py +298 -0
  18. chrys/app/cli/progress.py +857 -0
  19. chrys/app/cli/run.py +425 -0
  20. chrys/app/cli/serve.py +1593 -0
  21. chrys/app/cli/tool_summary.py +225 -0
  22. chrys/app/cli/trajectory.py +201 -0
  23. chrys/app/cli/workflow.py +601 -0
  24. chrys/app/features/__init__.py +3 -0
  25. chrys/app/features/buddy/__init__.py +3 -0
  26. chrys/app/features/buddy/actions.py +68 -0
  27. chrys/app/features/buddy/animation.py +76 -0
  28. chrys/app/features/buddy/commands.py +196 -0
  29. chrys/app/features/buddy/docs/design.md +187 -0
  30. chrys/app/features/buddy/hatchery.py +80 -0
  31. chrys/app/features/buddy/lifecycle.py +41 -0
  32. chrys/app/features/buddy/model.py +265 -0
  33. chrys/app/features/buddy/pixel_renderer.py +113 -0
  34. chrys/app/features/buddy/pixel_sprites.py +279 -0
  35. chrys/app/features/buddy/portrait.py +76 -0
  36. chrys/app/features/buddy/progression.py +58 -0
  37. chrys/app/features/buddy/replies.py +191 -0
  38. chrys/app/features/buddy/sprites/alien.toml +70 -0
  39. chrys/app/features/buddy/sprites/axolotl.toml +71 -0
  40. chrys/app/features/buddy/sprites/bat.toml +69 -0
  41. chrys/app/features/buddy/sprites/bee.toml +69 -0
  42. chrys/app/features/buddy/sprites/cactus.toml +73 -0
  43. chrys/app/features/buddy/sprites/capybara.toml +70 -0
  44. chrys/app/features/buddy/sprites/cat.toml +69 -0
  45. chrys/app/features/buddy/sprites/crab.toml +70 -0
  46. chrys/app/features/buddy/sprites/dragon.toml +71 -0
  47. chrys/app/features/buddy/sprites/duck.toml +69 -0
  48. chrys/app/features/buddy/sprites/fox.toml +69 -0
  49. chrys/app/features/buddy/sprites/frog.toml +70 -0
  50. chrys/app/features/buddy/sprites/ghost.toml +70 -0
  51. chrys/app/features/buddy/sprites/goose.toml +69 -0
  52. chrys/app/features/buddy/sprites/jelly.toml +70 -0
  53. chrys/app/features/buddy/sprites/llama.toml +70 -0
  54. chrys/app/features/buddy/sprites/mushroom.toml +69 -0
  55. chrys/app/features/buddy/sprites/octopus.toml +70 -0
  56. chrys/app/features/buddy/sprites/owl.toml +70 -0
  57. chrys/app/features/buddy/sprites/panda.toml +71 -0
  58. chrys/app/features/buddy/sprites/penguin.toml +69 -0
  59. chrys/app/features/buddy/sprites/rabbit.toml +70 -0
  60. chrys/app/features/buddy/sprites/robot.toml +72 -0
  61. chrys/app/features/buddy/sprites/shark.toml +70 -0
  62. chrys/app/features/buddy/sprites/snail.toml +69 -0
  63. chrys/app/features/buddy/sprites/snake.toml +69 -0
  64. chrys/app/features/buddy/sprites/tabby.toml +69 -0
  65. chrys/app/features/buddy/sprites/turtle.toml +69 -0
  66. chrys/app/features/buddy/store.py +143 -0
  67. chrys/app/features/session_title/__init__.py +7 -0
  68. chrys/app/features/session_title/updater.py +582 -0
  69. chrys/app/installer.py +651 -0
  70. chrys/app/parsing.py +17 -0
  71. chrys/app/tui/__init__.py +3 -0
  72. chrys/app/tui/app.py +1287 -0
  73. chrys/app/tui/behaviors/__init__.py +3 -0
  74. chrys/app/tui/behaviors/click_outside_dismiss.py +90 -0
  75. chrys/app/tui/behaviors/insert_clipboard.py +73 -0
  76. chrys/app/tui/behaviors/right_click_copy.py +146 -0
  77. chrys/app/tui/binding_display.py +139 -0
  78. chrys/app/tui/buddy_messages.py +17 -0
  79. chrys/app/tui/buddy_reply.py +86 -0
  80. chrys/app/tui/chrys.tcss +549 -0
  81. chrys/app/tui/clipboard.py +109 -0
  82. chrys/app/tui/copy_messages.py +10 -0
  83. chrys/app/tui/i18n.py +230 -0
  84. chrys/app/tui/language.py +58 -0
  85. chrys/app/tui/notifications/__init__.py +12 -0
  86. chrys/app/tui/notifications/drivers.py +180 -0
  87. chrys/app/tui/notifications/service.py +126 -0
  88. chrys/app/tui/notifications/settings.py +141 -0
  89. chrys/app/tui/screens/__init__.py +3 -0
  90. chrys/app/tui/screens/agents/__init__.py +8 -0
  91. chrys/app/tui/screens/agents/agent_draft_store.py +937 -0
  92. chrys/app/tui/screens/agents/config.py +2142 -0
  93. chrys/app/tui/screens/agents/config.tcss +169 -0
  94. chrys/app/tui/screens/agents/panels/__init__.py +3 -0
  95. chrys/app/tui/screens/agents/panels/acp.py +1176 -0
  96. chrys/app/tui/screens/agents/panels/basic.py +497 -0
  97. chrys/app/tui/screens/agents/panels/compaction.py +231 -0
  98. chrys/app/tui/screens/agents/panels/config_card.py +218 -0
  99. chrys/app/tui/screens/agents/panels/instructions.py +91 -0
  100. chrys/app/tui/screens/agents/panels/markdown_report.py +76 -0
  101. chrys/app/tui/screens/agents/panels/mcp.py +2262 -0
  102. chrys/app/tui/screens/agents/panels/memory.py +684 -0
  103. chrys/app/tui/screens/agents/panels/path_entry.py +227 -0
  104. chrys/app/tui/screens/agents/panels/skills.py +806 -0
  105. chrys/app/tui/screens/agents/panels/subagents.py +652 -0
  106. chrys/app/tui/screens/agents/panels/tools.py +205 -0
  107. chrys/app/tui/screens/agents/picker.py +103 -0
  108. chrys/app/tui/screens/agents/skill_paths.py +38 -0
  109. chrys/app/tui/screens/agents/validation_messages.py +452 -0
  110. chrys/app/tui/screens/dialogs/__init__.py +35 -0
  111. chrys/app/tui/screens/dialogs/agent_load.py +834 -0
  112. chrys/app/tui/screens/dialogs/agent_load.tcss +78 -0
  113. chrys/app/tui/screens/dialogs/app_mode.py +55 -0
  114. chrys/app/tui/screens/dialogs/approval/__init__.py +15 -0
  115. chrys/app/tui/screens/dialogs/approval/approval.tcss +311 -0
  116. chrys/app/tui/screens/dialogs/approval/bodies/__init__.py +3 -0
  117. chrys/app/tui/screens/dialogs/approval/bodies/file_edit.py +210 -0
  118. chrys/app/tui/screens/dialogs/approval/bodies/sub_agent.py +82 -0
  119. chrys/app/tui/screens/dialogs/approval/body.py +110 -0
  120. chrys/app/tui/screens/dialogs/approval/dialog.py +565 -0
  121. chrys/app/tui/screens/dialogs/approval/mode.py +105 -0
  122. chrys/app/tui/screens/dialogs/ask_user.py +148 -0
  123. chrys/app/tui/screens/dialogs/ask_user.tcss +62 -0
  124. chrys/app/tui/screens/dialogs/base.py +84 -0
  125. chrys/app/tui/screens/dialogs/confirm.py +184 -0
  126. chrys/app/tui/screens/dialogs/confirm.tcss +46 -0
  127. chrys/app/tui/screens/dialogs/connection_test.py +183 -0
  128. chrys/app/tui/screens/dialogs/connection_test.tcss +92 -0
  129. chrys/app/tui/screens/dialogs/diagram.py +102 -0
  130. chrys/app/tui/screens/dialogs/diagram.tcss +24 -0
  131. chrys/app/tui/screens/dialogs/editor.py +378 -0
  132. chrys/app/tui/screens/dialogs/editor.tcss +132 -0
  133. chrys/app/tui/screens/dialogs/file_picker.py +582 -0
  134. chrys/app/tui/screens/dialogs/file_picker.tcss +78 -0
  135. chrys/app/tui/screens/dialogs/fork_session.py +219 -0
  136. chrys/app/tui/screens/dialogs/fork_session.tcss +75 -0
  137. chrys/app/tui/screens/dialogs/image_compression.py +102 -0
  138. chrys/app/tui/screens/dialogs/image_compression.tcss +28 -0
  139. chrys/app/tui/screens/dialogs/man_page.py +172 -0
  140. chrys/app/tui/screens/dialogs/man_page.tcss +39 -0
  141. chrys/app/tui/screens/dialogs/new_folder.py +277 -0
  142. chrys/app/tui/screens/dialogs/new_folder.tcss +51 -0
  143. chrys/app/tui/screens/dialogs/notice.tcss +14 -0
  144. chrys/app/tui/screens/dialogs/prompt_cache.py +144 -0
  145. chrys/app/tui/screens/dialogs/prompt_cache.tcss +38 -0
  146. chrys/app/tui/screens/dialogs/runtime_details.py +371 -0
  147. chrys/app/tui/screens/dialogs/runtime_details.tcss +49 -0
  148. chrys/app/tui/screens/dialogs/session_title.py +110 -0
  149. chrys/app/tui/screens/dialogs/session_title.tcss +43 -0
  150. chrys/app/tui/screens/dialogs/tool_view.py +207 -0
  151. chrys/app/tui/screens/dialogs/tool_view.tcss +43 -0
  152. chrys/app/tui/screens/dialogs/vision_unsupported.py +122 -0
  153. chrys/app/tui/screens/dialogs/vision_unsupported.tcss +35 -0
  154. chrys/app/tui/screens/dialogs/workflow_confirm.py +91 -0
  155. chrys/app/tui/screens/dialogs/workflow_confirm.tcss +17 -0
  156. chrys/app/tui/screens/dialogs/workflow_input.py +258 -0
  157. chrys/app/tui/screens/dialogs/workflow_input.tcss +73 -0
  158. chrys/app/tui/screens/dialogs/workflow_node.py +462 -0
  159. chrys/app/tui/screens/dialogs/workflow_node.tcss +41 -0
  160. chrys/app/tui/screens/dialogs/workflow_picker.py +46 -0
  161. chrys/app/tui/screens/dialogs/workflow_result.py +136 -0
  162. chrys/app/tui/screens/dialogs/workflow_result.tcss +14 -0
  163. chrys/app/tui/screens/diff/__init__.py +22 -0
  164. chrys/app/tui/screens/diff/rollback_modal.py +1381 -0
  165. chrys/app/tui/screens/diff/rollback_modal.tcss +248 -0
  166. chrys/app/tui/screens/diff/screen.py +1532 -0
  167. chrys/app/tui/screens/diff/screen.tcss +104 -0
  168. chrys/app/tui/screens/guides/__init__.py +7 -0
  169. chrys/app/tui/screens/guides/_docs/en/guides/configuration/agents.md +178 -0
  170. chrys/app/tui/screens/guides/_docs/en/guides/configuration/approval.md +117 -0
  171. chrys/app/tui/screens/guides/_docs/en/guides/configuration/chat-options.md +276 -0
  172. chrys/app/tui/screens/guides/_docs/en/guides/configuration/compaction.md +42 -0
  173. chrys/app/tui/screens/guides/_docs/en/guides/configuration/memory.md +48 -0
  174. chrys/app/tui/screens/guides/_docs/en/guides/configuration/models.md +158 -0
  175. chrys/app/tui/screens/guides/_docs/en/guides/configuration/settings.md +210 -0
  176. chrys/app/tui/screens/guides/_docs/en/guides/configuration/themes.md +61 -0
  177. chrys/app/tui/screens/guides/_docs/en/guides/configuration/tools.md +60 -0
  178. chrys/app/tui/screens/guides/_docs/en/guides/configuration/web-tools.md +286 -0
  179. chrys/app/tui/screens/guides/_docs/en/guides/daily-use/sessions.md +77 -0
  180. chrys/app/tui/screens/guides/_docs/en/guides/daily-use/workspaces.md +51 -0
  181. chrys/app/tui/screens/guides/_docs/en/guides/extensions/external-acp-agents.md +126 -0
  182. chrys/app/tui/screens/guides/_docs/en/guides/extensions/hooks.md +167 -0
  183. chrys/app/tui/screens/guides/_docs/en/guides/extensions/mcp.md +95 -0
  184. chrys/app/tui/screens/guides/_docs/en/guides/extensions/skills.md +175 -0
  185. chrys/app/tui/screens/guides/_docs/en/guides/running/icode-acp.md +61 -0
  186. chrys/app/tui/screens/guides/_docs/en/guides/running/icode-run.md +190 -0
  187. chrys/app/tui/screens/guides/_docs/en/guides/running/icode-serve.md +239 -0
  188. chrys/app/tui/screens/guides/_docs/en/guides/running/workflows.md +664 -0
  189. chrys/app/tui/screens/guides/_docs/en/reference/agent-profile.md +548 -0
  190. chrys/app/tui/screens/guides/_docs/en/reference/hooks.md +602 -0
  191. chrys/app/tui/screens/guides/_docs/en/reference/opentelemetry.md +177 -0
  192. chrys/app/tui/screens/guides/_docs/en/reference/settings.md +245 -0
  193. chrys/app/tui/screens/guides/_docs/en/reference/themes.md +149 -0
  194. chrys/app/tui/screens/guides/_docs/en/reference/tool-kinds-and-names.md +58 -0
  195. chrys/app/tui/screens/guides/_docs/en/reference/tui-slash-commands.md +76 -0
  196. chrys/app/tui/screens/guides/_docs/en/reference/workflows.md +1006 -0
  197. chrys/app/tui/screens/guides/_docs/en/start/getting-started.md +170 -0
  198. chrys/app/tui/screens/guides/_docs/en/start/what-is-icode.md +34 -0
  199. chrys/app/tui/screens/guides/_docs/index.yaml +100 -0
  200. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/agents.md +178 -0
  201. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/approval.md +117 -0
  202. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/chat-options.md +276 -0
  203. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/compaction.md +42 -0
  204. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/memory.md +48 -0
  205. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/models.md +158 -0
  206. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/settings.md +210 -0
  207. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/themes.md +61 -0
  208. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/tools.md +60 -0
  209. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/web-tools.md +286 -0
  210. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/sessions.md +77 -0
  211. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/workspaces.md +51 -0
  212. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/external-acp-agents.md +126 -0
  213. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/hooks.md +167 -0
  214. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/mcp.md +95 -0
  215. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/skills.md +175 -0
  216. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-acp.md +61 -0
  217. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-run.md +190 -0
  218. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-serve.md +239 -0
  219. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/workflows.md +664 -0
  220. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/agent-profile.md +548 -0
  221. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/hooks.md +602 -0
  222. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/opentelemetry.md +177 -0
  223. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/settings.md +245 -0
  224. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/themes.md +149 -0
  225. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tool-kinds-and-names.md +58 -0
  226. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tui-slash-commands.md +76 -0
  227. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/workflows.md +1005 -0
  228. chrys/app/tui/screens/guides/_docs/zh-Hans/start/getting-started.md +170 -0
  229. chrys/app/tui/screens/guides/_docs/zh-Hans/start/what-is-icode.md +34 -0
  230. chrys/app/tui/screens/guides/index.py +215 -0
  231. chrys/app/tui/screens/guides/reader.py +173 -0
  232. chrys/app/tui/screens/guides/screen.py +442 -0
  233. chrys/app/tui/screens/guides/screen.tcss +52 -0
  234. chrys/app/tui/screens/logs/__init__.py +7 -0
  235. chrys/app/tui/screens/logs/screen.py +504 -0
  236. chrys/app/tui/screens/logs/screen.tcss +45 -0
  237. chrys/app/tui/screens/main/__init__.py +7 -0
  238. chrys/app/tui/screens/main/action_availability.py +30 -0
  239. chrys/app/tui/screens/main/buddy_command.py +143 -0
  240. chrys/app/tui/screens/main/commands.py +1073 -0
  241. chrys/app/tui/screens/main/config_actions.py +464 -0
  242. chrys/app/tui/screens/main/copy_actions.py +117 -0
  243. chrys/app/tui/screens/main/dialog_controllers.py +826 -0
  244. chrys/app/tui/screens/main/dialog_gateway.py +166 -0
  245. chrys/app/tui/screens/main/diff_controller.py +478 -0
  246. chrys/app/tui/screens/main/engine_read_model.py +184 -0
  247. chrys/app/tui/screens/main/event_handlers.py +2014 -0
  248. chrys/app/tui/screens/main/input_flow.py +241 -0
  249. chrys/app/tui/screens/main/live_diff.py +244 -0
  250. chrys/app/tui/screens/main/main_screen_presenter.py +295 -0
  251. chrys/app/tui/screens/main/model_indicator.py +217 -0
  252. chrys/app/tui/screens/main/navigation.py +343 -0
  253. chrys/app/tui/screens/main/ports.py +571 -0
  254. chrys/app/tui/screens/main/recent_dirs.py +123 -0
  255. chrys/app/tui/screens/main/rollback_controller.py +586 -0
  256. chrys/app/tui/screens/main/runtime_info.py +71 -0
  257. chrys/app/tui/screens/main/screen.py +2265 -0
  258. chrys/app/tui/screens/main/screen.tcss +21 -0
  259. chrys/app/tui/screens/main/session_handlers.py +1127 -0
  260. chrys/app/tui/screens/main/session_title.py +239 -0
  261. chrys/app/tui/screens/main/settings_coordinator.py +365 -0
  262. chrys/app/tui/screens/main/settings_persistence.py +184 -0
  263. chrys/app/tui/screens/main/shell_mode.py +112 -0
  264. chrys/app/tui/screens/main/state.py +289 -0
  265. chrys/app/tui/screens/main/submit_state.py +11 -0
  266. chrys/app/tui/screens/main/subscriptions.py +177 -0
  267. chrys/app/tui/screens/main/suggestions.py +982 -0
  268. chrys/app/tui/screens/main/tool_action_bridge.py +75 -0
  269. chrys/app/tui/screens/main/view_adapter.py +1567 -0
  270. chrys/app/tui/screens/main/workflow_browser.py +747 -0
  271. chrys/app/tui/screens/main/workflow_content.py +248 -0
  272. chrys/app/tui/screens/main/workflow_controller.py +240 -0
  273. chrys/app/tui/screens/main/workflow_feedback.py +137 -0
  274. chrys/app/tui/screens/main/workflow_flow.py +49 -0
  275. chrys/app/tui/screens/main/workflow_run_control.py +456 -0
  276. chrys/app/tui/screens/main/workflow_session_view.py +316 -0
  277. chrys/app/tui/screens/main/workspace_actions.py +272 -0
  278. chrys/app/tui/screens/main/workspace_branch.py +194 -0
  279. chrys/app/tui/screens/menus/__init__.py +8 -0
  280. chrys/app/tui/screens/menus/languages.py +114 -0
  281. chrys/app/tui/screens/models/__init__.py +7 -0
  282. chrys/app/tui/screens/models/picker.py +147 -0
  283. chrys/app/tui/screens/models/screen.py +2043 -0
  284. chrys/app/tui/screens/models/screen.tcss +255 -0
  285. chrys/app/tui/screens/sessions/__init__.py +8 -0
  286. chrys/app/tui/screens/sessions/presenter.py +586 -0
  287. chrys/app/tui/screens/sessions/screen.py +779 -0
  288. chrys/app/tui/screens/sessions/screen.tcss +127 -0
  289. chrys/app/tui/screens/settings/__init__.py +29 -0
  290. chrys/app/tui/screens/settings/danger.py +26 -0
  291. chrys/app/tui/screens/settings/dialog.py +281 -0
  292. chrys/app/tui/screens/settings/layout.py +519 -0
  293. chrys/app/tui/screens/settings/migrate_dialog.py +253 -0
  294. chrys/app/tui/screens/settings/panes/__init__.py +3 -0
  295. chrys/app/tui/screens/settings/panes/notifications.py +248 -0
  296. chrys/app/tui/screens/settings/panes/sessions.py +148 -0
  297. chrys/app/tui/screens/settings/ports.py +55 -0
  298. chrys/app/tui/screens/settings/provenance.py +102 -0
  299. chrys/app/tui/screens/settings/rows.py +546 -0
  300. chrys/app/tui/screens/settings/settings.tcss +406 -0
  301. chrys/app/tui/screens/themes/__init__.py +8 -0
  302. chrys/app/tui/screens/themes/dialogs.py +330 -0
  303. chrys/app/tui/screens/themes/editor.py +632 -0
  304. chrys/app/tui/screens/themes/editor.tcss +268 -0
  305. chrys/app/tui/screens/themes/messages.py +95 -0
  306. chrys/app/tui/screens/themes/palette.py +694 -0
  307. chrys/app/tui/screens/themes/panel.py +113 -0
  308. chrys/app/tui/screens/themes/picker.py +203 -0
  309. chrys/app/tui/screens/themes/save.py +191 -0
  310. chrys/app/tui/support/__init__.py +3 -0
  311. chrys/app/tui/support/file_manager.py +64 -0
  312. chrys/app/tui/support/gc_freeze.py +1114 -0
  313. chrys/app/tui/support/path_shortcuts.py +50 -0
  314. chrys/app/tui/support/prompt_history.py +298 -0
  315. chrys/app/tui/support/workspace_mru.py +476 -0
  316. chrys/app/tui/terminal/__init__.py +6 -0
  317. chrys/app/tui/terminal/_pty_backend.py +114 -0
  318. chrys/app/tui/terminal/emulator/__init__.py +24 -0
  319. chrys/app/tui/terminal/emulator/buffer.py +456 -0
  320. chrys/app/tui/terminal/emulator/charsets.py +113 -0
  321. chrys/app/tui/terminal/emulator/core.py +963 -0
  322. chrys/app/tui/terminal/emulator/input.py +342 -0
  323. chrys/app/tui/terminal/emulator/parser.py +227 -0
  324. chrys/app/tui/terminal/emulator/pen.py +158 -0
  325. chrys/app/tui/terminal/launcher.py +455 -0
  326. chrys/app/tui/terminal/leaked_reports.py +44 -0
  327. chrys/app/tui/terminal/panel.py +323 -0
  328. chrys/app/tui/terminal/pty_process.py +405 -0
  329. chrys/app/tui/terminal/rendering.py +151 -0
  330. chrys/app/tui/terminal/shell.py +273 -0
  331. chrys/app/tui/terminal/shell_integration.py +199 -0
  332. chrys/app/tui/terminal/title.py +224 -0
  333. chrys/app/tui/terminal/widget.py +654 -0
  334. chrys/app/tui/theme.py +422 -0
  335. chrys/app/tui/theme_loader.py +201 -0
  336. chrys/app/tui/theme_stylesheet.py +49 -0
  337. chrys/app/tui/themes/__init__.py +3 -0
  338. chrys/app/tui/themes/document.py +120 -0
  339. chrys/app/tui/themes/preview.py +170 -0
  340. chrys/app/tui/themes/store.py +176 -0
  341. chrys/app/tui/toasts.py +69 -0
  342. chrys/app/tui/util/__init__.py +3 -0
  343. chrys/app/tui/util/context_pressure.py +146 -0
  344. chrys/app/tui/util/debug_log.py +292 -0
  345. chrys/app/tui/util/diff_entries.py +64 -0
  346. chrys/app/tui/util/formatting.py +53 -0
  347. chrys/app/tui/util/git_branch.py +326 -0
  348. chrys/app/tui/util/invocation_progress.py +57 -0
  349. chrys/app/tui/util/logo.py +32 -0
  350. chrys/app/tui/util/message_gate.py +35 -0
  351. chrys/app/tui/util/mutation_sources.py +14 -0
  352. chrys/app/tui/util/picker_text.py +27 -0
  353. chrys/app/tui/util/removal.py +47 -0
  354. chrys/app/tui/util/rich_style.py +36 -0
  355. chrys/app/tui/util/source_text.py +12 -0
  356. chrys/app/tui/util/static_update.py +88 -0
  357. chrys/app/tui/util/todo_format.py +51 -0
  358. chrys/app/tui/util/visibility.py +214 -0
  359. chrys/app/tui/widgets/__init__.py +60 -0
  360. chrys/app/tui/widgets/ask_user_controls.py +652 -0
  361. chrys/app/tui/widgets/ask_user_markdown.py +17 -0
  362. chrys/app/tui/widgets/ask_user_prompt.py +606 -0
  363. chrys/app/tui/widgets/buttons.py +52 -0
  364. chrys/app/tui/widgets/chat/__init__.py +3 -0
  365. chrys/app/tui/widgets/chat/agent_transcript_surface.py +818 -0
  366. chrys/app/tui/widgets/chat/chrome.py +296 -0
  367. chrys/app/tui/widgets/chat/compaction_card.py +353 -0
  368. chrys/app/tui/widgets/chat/context_fold.py +77 -0
  369. chrys/app/tui/widgets/chat/file_snapshot.py +91 -0
  370. chrys/app/tui/widgets/chat/image_preview.py +214 -0
  371. chrys/app/tui/widgets/chat/live_renderer.py +383 -0
  372. chrys/app/tui/widgets/chat/messages.py +1039 -0
  373. chrys/app/tui/widgets/chat/panel.py +1769 -0
  374. chrys/app/tui/widgets/chat/ports.py +245 -0
  375. chrys/app/tui/widgets/chat/renderers/__init__.py +7 -0
  376. chrys/app/tui/widgets/chat/renderers/ask_user.py +747 -0
  377. chrys/app/tui/widgets/chat/renderers/execute.py +431 -0
  378. chrys/app/tui/widgets/chat/renderers/file_edit.py +925 -0
  379. chrys/app/tui/widgets/chat/renderers/hosted_code.py +83 -0
  380. chrys/app/tui/widgets/chat/renderers/hosted_generic.py +330 -0
  381. chrys/app/tui/widgets/chat/renderers/hosted_image.py +107 -0
  382. chrys/app/tui/widgets/chat/renderers/hosted_mcp.py +43 -0
  383. chrys/app/tui/widgets/chat/renderers/hosted_search.py +167 -0
  384. chrys/app/tui/widgets/chat/renderers/hosted_shell.py +132 -0
  385. chrys/app/tui/widgets/chat/renderers/read_file.py +399 -0
  386. chrys/app/tui/widgets/chat/renderers/search.py +348 -0
  387. chrys/app/tui/widgets/chat/renderers/skill.py +564 -0
  388. chrys/app/tui/widgets/chat/renderers/sleep.py +287 -0
  389. chrys/app/tui/widgets/chat/renderers/sub_agent.py +1385 -0
  390. chrys/app/tui/widgets/chat/renderers/todo.py +114 -0
  391. chrys/app/tui/widgets/chat/renderers/web.py +69 -0
  392. chrys/app/tui/widgets/chat/replay.py +1605 -0
  393. chrys/app/tui/widgets/chat/replay_mount.py +322 -0
  394. chrys/app/tui/widgets/chat/scroll_controller.py +579 -0
  395. chrys/app/tui/widgets/chat/selection_controller.py +435 -0
  396. chrys/app/tui/widgets/chat/session_json.py +508 -0
  397. chrys/app/tui/widgets/chat/toc_model.py +156 -0
  398. chrys/app/tui/widgets/chat/tool_call.py +2478 -0
  399. chrys/app/tui/widgets/chat/tool_registry.py +753 -0
  400. chrys/app/tui/widgets/chat/tool_renderers.py +247 -0
  401. chrys/app/tui/widgets/chat/tool_view_builders.py +260 -0
  402. chrys/app/tui/widgets/chat/transcript_reader.py +57 -0
  403. chrys/app/tui/widgets/checkbox.py +24 -0
  404. chrys/app/tui/widgets/chrome/__init__.py +3 -0
  405. chrys/app/tui/widgets/chrome/app_header.py +283 -0
  406. chrys/app/tui/widgets/chrome/commands.py +95 -0
  407. chrys/app/tui/widgets/chrome/file_index.py +68 -0
  408. chrys/app/tui/widgets/chrome/file_scanner.py +670 -0
  409. chrys/app/tui/widgets/chrome/footer.py +284 -0
  410. chrys/app/tui/widgets/chrome/image_paste.py +351 -0
  411. chrys/app/tui/widgets/chrome/input_bar.py +1299 -0
  412. chrys/app/tui/widgets/chrome/status_bar.py +911 -0
  413. chrys/app/tui/widgets/chrome/suggestion_list.py +652 -0
  414. chrys/app/tui/widgets/click_affordance.py +24 -0
  415. chrys/app/tui/widgets/color_picker/__init__.py +254 -0
  416. chrys/app/tui/widgets/color_picker/controls.py +171 -0
  417. chrys/app/tui/widgets/color_picker/model.py +89 -0
  418. chrys/app/tui/widgets/dialog_buttons.py +58 -0
  419. chrys/app/tui/widgets/dialog_scroll.py +24 -0
  420. chrys/app/tui/widgets/diff_view/__init__.py +20 -0
  421. chrys/app/tui/widgets/diff_view/cells.py +59 -0
  422. chrys/app/tui/widgets/diff_view/code.py +280 -0
  423. chrys/app/tui/widgets/diff_view/compute.py +286 -0
  424. chrys/app/tui/widgets/diff_view/gutter.py +161 -0
  425. chrys/app/tui/widgets/diff_view/inline.py +77 -0
  426. chrys/app/tui/widgets/diff_view/palette.py +87 -0
  427. chrys/app/tui/widgets/diff_view/protocols.py +24 -0
  428. chrys/app/tui/widgets/diff_view/rows.py +138 -0
  429. chrys/app/tui/widgets/diff_view/unified.py +267 -0
  430. chrys/app/tui/widgets/diff_view/widget.py +209 -0
  431. chrys/app/tui/widgets/editor/__init__.py +79 -0
  432. chrys/app/tui/widgets/editor/coordinates.py +30 -0
  433. chrys/app/tui/widgets/editor/highlighter.py +139 -0
  434. chrys/app/tui/widgets/editor/keymaps.py +357 -0
  435. chrys/app/tui/widgets/editor/types.py +216 -0
  436. chrys/app/tui/widgets/editor/vim.py +1195 -0
  437. chrys/app/tui/widgets/editor/widget.py +401 -0
  438. chrys/app/tui/widgets/file_preview.py +58 -0
  439. chrys/app/tui/widgets/hatch.py +138 -0
  440. chrys/app/tui/widgets/input.py +66 -0
  441. chrys/app/tui/widgets/loading.py +125 -0
  442. chrys/app/tui/widgets/markdown/__init__.py +25 -0
  443. chrys/app/tui/widgets/markdown/_utils.py +21 -0
  444. chrys/app/tui/widgets/markdown/anchors.py +71 -0
  445. chrys/app/tui/widgets/markdown/blocks.py +115 -0
  446. chrys/app/tui/widgets/markdown/copyable.py +163 -0
  447. chrys/app/tui/widgets/markdown/diagram/__init__.py +35 -0
  448. chrys/app/tui/widgets/markdown/diagram/canvas.py +409 -0
  449. chrys/app/tui/widgets/markdown/diagram/charts.py +52 -0
  450. chrys/app/tui/widgets/markdown/diagram/diagnostics.py +101 -0
  451. chrys/app/tui/widgets/markdown/diagram/geometry.py +46 -0
  452. chrys/app/tui/widgets/markdown/diagram/layout.py +494 -0
  453. chrys/app/tui/widgets/markdown/diagram/messages.py +18 -0
  454. chrys/app/tui/widgets/markdown/diagram/model.py +275 -0
  455. chrys/app/tui/widgets/markdown/diagram/parser.py +173 -0
  456. chrys/app/tui/widgets/markdown/diagram/parsers/__init__.py +3 -0
  457. chrys/app/tui/widgets/markdown/diagram/parsers/charts.py +349 -0
  458. chrys/app/tui/widgets/markdown/diagram/parsers/common.py +403 -0
  459. chrys/app/tui/widgets/markdown/diagram/parsers/graphs.py +1072 -0
  460. chrys/app/tui/widgets/markdown/diagram/parsers/planning.py +317 -0
  461. chrys/app/tui/widgets/markdown/diagram/parsers/schedule.py +298 -0
  462. chrys/app/tui/widgets/markdown/diagram/parsers/schemas.py +241 -0
  463. chrys/app/tui/widgets/markdown/diagram/parsers/structure.py +221 -0
  464. chrys/app/tui/widgets/markdown/diagram/presentation.py +32 -0
  465. chrys/app/tui/widgets/markdown/diagram/renderers/__init__.py +3 -0
  466. chrys/app/tui/widgets/markdown/diagram/renderers/charts.py +275 -0
  467. chrys/app/tui/widgets/markdown/diagram/renderers/common.py +73 -0
  468. chrys/app/tui/widgets/markdown/diagram/renderers/planning.py +157 -0
  469. chrys/app/tui/widgets/markdown/diagram/renderers/schedule.py +202 -0
  470. chrys/app/tui/widgets/markdown/diagram/renderers/structure.py +77 -0
  471. chrys/app/tui/widgets/markdown/diagram/renderers/xy.py +385 -0
  472. chrys/app/tui/widgets/markdown/diagram/router.py +651 -0
  473. chrys/app/tui/widgets/markdown/diagram/specs/__init__.py +3 -0
  474. chrys/app/tui/widgets/markdown/diagram/specs/charts.py +94 -0
  475. chrys/app/tui/widgets/markdown/diagram/specs/planning.py +68 -0
  476. chrys/app/tui/widgets/markdown/diagram/specs/schedule.py +65 -0
  477. chrys/app/tui/widgets/markdown/diagram/specs/structure.py +23 -0
  478. chrys/app/tui/widgets/markdown/diagram/viewer.py +92 -0
  479. chrys/app/tui/widgets/markdown/links.py +47 -0
  480. chrys/app/tui/widgets/markdown/math/__init__.py +36 -0
  481. chrys/app/tui/widgets/markdown/math/layout.py +739 -0
  482. chrys/app/tui/widgets/markdown/math/markdown.py +291 -0
  483. chrys/app/tui/widgets/markdown/math/parser.py +325 -0
  484. chrys/app/tui/widgets/markdown/math/symbols.py +325 -0
  485. chrys/app/tui/widgets/markdown/navigator.py +80 -0
  486. chrys/app/tui/widgets/markdown/parser.py +1057 -0
  487. chrys/app/tui/widgets/markdown/stream.py +73 -0
  488. chrys/app/tui/widgets/markdown/toc.py +98 -0
  489. chrys/app/tui/widgets/markdown/viewer.py +146 -0
  490. chrys/app/tui/widgets/markdown/widget.py +2046 -0
  491. chrys/app/tui/widgets/marquee.py +101 -0
  492. chrys/app/tui/widgets/option_menu.py +82 -0
  493. chrys/app/tui/widgets/page_navigator.py +118 -0
  494. chrys/app/tui/widgets/select.py +196 -0
  495. chrys/app/tui/widgets/selection.py +59 -0
  496. chrys/app/tui/widgets/sidebar/__init__.py +3 -0
  497. chrys/app/tui/widgets/sidebar/buddy.py +367 -0
  498. chrys/app/tui/widgets/sidebar/context.py +482 -0
  499. chrys/app/tui/widgets/sidebar/debug.py +195 -0
  500. chrys/app/tui/widgets/sidebar/empty_state.py +11 -0
  501. chrys/app/tui/widgets/sidebar/panel.py +198 -0
  502. chrys/app/tui/widgets/sidebar/tasks.py +198 -0
  503. chrys/app/tui/widgets/sidebar/toc.py +166 -0
  504. chrys/app/tui/widgets/sidebar/toc_types.py +30 -0
  505. chrys/app/tui/widgets/syntax_theme.py +85 -0
  506. chrys/app/tui/widgets/text_area.py +258 -0
  507. chrys/app/tui/widgets/trajectory/__init__.py +8 -0
  508. chrys/app/tui/widgets/trajectory/chartkit.py +264 -0
  509. chrys/app/tui/widgets/trajectory/insights.py +1266 -0
  510. chrys/app/tui/widgets/trajectory/overview.py +1101 -0
  511. chrys/app/tui/widgets/trajectory/panel.py +983 -0
  512. chrys/app/tui/widgets/trajectory/presentation.py +311 -0
  513. chrys/app/tui/widgets/trajectory/session_info.py +124 -0
  514. chrys/app/tui/widgets/trajectory/text_view.py +124 -0
  515. chrys/app/tui/widgets/trajectory/timeline.py +380 -0
  516. chrys/app/tui/widgets/welcome.py +126 -0
  517. chrys/app/tui/widgets/workflow/__init__.py +5 -0
  518. chrys/app/tui/widgets/workflow/edges.py +89 -0
  519. chrys/app/tui/widgets/workflow/graph.py +869 -0
  520. chrys/app/tui/widgets/workflow/info.py +310 -0
  521. chrys/app/tui/widgets/workflow/node_view.py +27 -0
  522. chrys/app/tui/widgets/workflow/output.py +225 -0
  523. chrys/app/tui/widgets/workflow/panel.py +658 -0
  524. chrys/app/tui/widgets/workflow/projector.py +324 -0
  525. chrys/app/tui/widgets/workflow/records.py +69 -0
  526. chrys/app/tui/widgets/workflow/scrollbar.py +82 -0
  527. chrys/app/tui/widgets/workflow/selection.py +151 -0
  528. chrys/app/tui/widgets/workflow/source.py +65 -0
  529. chrys/app/tui/widgets/workflow/text.py +257 -0
  530. chrys/app/tui/widgets/workflow/values.py +317 -0
  531. chrys/foundation/__init__.py +0 -0
  532. chrys/foundation/branding.py +15 -0
  533. chrys/foundation/config/__init__.py +7 -0
  534. chrys/foundation/config/coercion.py +374 -0
  535. chrys/foundation/config/context.py +34 -0
  536. chrys/foundation/config/env_file.py +141 -0
  537. chrys/foundation/config/env_layers.py +370 -0
  538. chrys/foundation/config/migrations.py +521 -0
  539. chrys/foundation/config/notification_events.py +22 -0
  540. chrys/foundation/config/process_settings.py +332 -0
  541. chrys/foundation/config/runtime_pointer.py +133 -0
  542. chrys/foundation/config/settings.py +1952 -0
  543. chrys/foundation/config/settings_store.py +996 -0
  544. chrys/foundation/config/spec.py +396 -0
  545. chrys/foundation/config/user_settings.py +140 -0
  546. chrys/foundation/config/warnings.py +262 -0
  547. chrys/foundation/config/web_values.py +135 -0
  548. chrys/foundation/config/yaml_store.py +487 -0
  549. chrys/foundation/errors/__init__.py +68 -0
  550. chrys/foundation/errors/_legacy.py +117 -0
  551. chrys/foundation/errors/_text.py +13 -0
  552. chrys/foundation/errors/_walk.py +85 -0
  553. chrys/foundation/errors/classify.py +278 -0
  554. chrys/foundation/errors/display.py +274 -0
  555. chrys/foundation/errors/formatting.py +283 -0
  556. chrys/foundation/errors/kinds.py +67 -0
  557. chrys/foundation/errors/network.py +497 -0
  558. chrys/foundation/errors/provider.py +445 -0
  559. chrys/foundation/errors/route.py +85 -0
  560. chrys/foundation/events/__init__.py +8 -0
  561. chrys/foundation/events/bus.py +143 -0
  562. chrys/foundation/events/types.py +1629 -0
  563. chrys/foundation/events/workflow.py +38 -0
  564. chrys/foundation/hosted_tools.py +142 -0
  565. chrys/foundation/i18n/__init__.py +32 -0
  566. chrys/foundation/i18n/_catalogs/zh-Hans/LC_MESSAGES/chrys.mo +0 -0
  567. chrys/foundation/i18n/catalogs.py +42 -0
  568. chrys/foundation/i18n/formatting.py +202 -0
  569. chrys/foundation/i18n/locale.py +92 -0
  570. chrys/foundation/i18n/localizer.py +187 -0
  571. chrys/foundation/i18n/messages.py +268 -0
  572. chrys/foundation/io/__init__.py +3 -0
  573. chrys/foundation/io/result_content.py +237 -0
  574. chrys/foundation/models/__init__.py +3 -0
  575. chrys/foundation/models/approval_reuse.py +23 -0
  576. chrys/foundation/models/ask_user.py +468 -0
  577. chrys/foundation/models/execution.py +18 -0
  578. chrys/foundation/models/history_markers.py +115 -0
  579. chrys/foundation/models/invocations.py +63 -0
  580. chrys/foundation/models/mutation_scope.py +28 -0
  581. chrys/foundation/models/session_env.py +87 -0
  582. chrys/foundation/models/session_surface.py +29 -0
  583. chrys/foundation/models/todos.py +74 -0
  584. chrys/foundation/models/turns.py +125 -0
  585. chrys/foundation/models/workflow_session.py +89 -0
  586. chrys/foundation/models/workspace.py +59 -0
  587. chrys/foundation/net/__init__.py +3 -0
  588. chrys/foundation/net/address_class.py +57 -0
  589. chrys/foundation/net/route_probe.py +94 -0
  590. chrys/foundation/net/url.py +107 -0
  591. chrys/foundation/observability/__init__.py +3 -0
  592. chrys/foundation/observability/exporters.py +174 -0
  593. chrys/foundation/observability/gate.py +31 -0
  594. chrys/foundation/observability/setup.py +497 -0
  595. chrys/foundation/observability/sink.py +228 -0
  596. chrys/foundation/patches/__init__.py +19 -0
  597. chrys/foundation/patches/mdurl_cache.py +59 -0
  598. chrys/foundation/patches/patcher.py +287 -0
  599. chrys/foundation/patches/staged_members.py +205 -0
  600. chrys/foundation/patches/textual_block_border.py +105 -0
  601. chrys/foundation/patches/textual_button_ansi.py +65 -0
  602. chrys/foundation/patches/textual_button_ansi_old.txt +68 -0
  603. chrys/foundation/patches/textual_callback_cache.py +248 -0
  604. chrys/foundation/patches/textual_callback_dispatch.py +109 -0
  605. chrys/foundation/patches/textual_compositor_cjk.py +566 -0
  606. chrys/foundation/patches/textual_dispatch_cache.py +150 -0
  607. chrys/foundation/patches/textual_ime_cursor_anchor.py +92 -0
  608. chrys/foundation/patches/textual_kitty_keyboard.py +268 -0
  609. chrys/foundation/patches/textual_lru_acyclic.py +241 -0
  610. chrys/foundation/patches/textual_message_pump.py +136 -0
  611. chrys/foundation/patches/textual_node_diet.py +875 -0
  612. chrys/foundation/patches/textual_one_shot_timer.py +48 -0
  613. chrys/foundation/patches/textual_option_list.py +90 -0
  614. chrys/foundation/patches/textual_precompose.py +89 -0
  615. chrys/foundation/patches/textual_pruned_tabs.py +69 -0
  616. chrys/foundation/patches/textual_reflow_reuse.py +1753 -0
  617. chrys/foundation/patches/textual_removed_node_caches.py +86 -0
  618. chrys/foundation/patches/textual_removed_screen_callbacks.py +55 -0
  619. chrys/foundation/patches/textual_selection_extract.py +48 -0
  620. chrys/foundation/patches/textual_strip_cycles.py +119 -0
  621. chrys/foundation/patches/textual_tab_selection.py +1611 -0
  622. chrys/foundation/patches/textual_timer_skip.py +70 -0
  623. chrys/foundation/patches/textual_utf8_decoder.py +106 -0
  624. chrys/foundation/patches/textual_win_sleep.py +167 -0
  625. chrys/foundation/patches/watchdog_fsevents.py +70 -0
  626. chrys/foundation/patches/watchdog_windows.py +52 -0
  627. chrys/foundation/platform/__init__.py +556 -0
  628. chrys/foundation/platform/c_api.py +32 -0
  629. chrys/foundation/platform/child_reap.py +107 -0
  630. chrys/foundation/platform/command_line.py +95 -0
  631. chrys/foundation/platform/files.py +1470 -0
  632. chrys/foundation/platform/output_capture.py +113 -0
  633. chrys/foundation/platform/paths.py +193 -0
  634. chrys/foundation/platform/process.py +1468 -0
  635. chrys/foundation/platform/pty_output.py +19 -0
  636. chrys/foundation/platform/runtime_paths.py +281 -0
  637. chrys/foundation/platform/win_console.py +129 -0
  638. chrys/foundation/reasoning_origin.py +67 -0
  639. chrys/foundation/recovery.py +16 -0
  640. chrys/foundation/retry.py +278 -0
  641. chrys/foundation/text/__init__.py +3 -0
  642. chrys/foundation/text/encoding.py +603 -0
  643. chrys/foundation/text/images.py +304 -0
  644. chrys/foundation/text/lines.py +24 -0
  645. chrys/foundation/text/mentions.py +90 -0
  646. chrys/foundation/text/model_json.py +25 -0
  647. chrys/foundation/text/reading_order.py +20 -0
  648. chrys/foundation/text/tokenizer.py +25 -0
  649. chrys/foundation/text/tool_output.py +287 -0
  650. chrys/foundation/text/yaml_io.py +62 -0
  651. chrys/foundation/tool_call_context.py +202 -0
  652. chrys/foundation/tool_execution_stamp.py +184 -0
  653. chrys/foundation/tool_invocation_order.py +48 -0
  654. chrys/foundation/tool_kinds.py +102 -0
  655. chrys/foundation/tool_result_metadata.py +287 -0
  656. chrys/foundation/trajectory/__init__.py +10 -0
  657. chrys/foundation/trajectory/context.py +481 -0
  658. chrys/foundation/trajectory/envelope.py +571 -0
  659. chrys/foundation/trajectory/event_types.py +294 -0
  660. chrys/foundation/trajectory/finalizers.py +46 -0
  661. chrys/foundation/trajectory/fingerprint.py +62 -0
  662. chrys/foundation/trajectory/ids.py +155 -0
  663. chrys/foundation/trajectory/keys.py +99 -0
  664. chrys/foundation/trajectory/lease.py +173 -0
  665. chrys/foundation/trajectory/metadata.py +72 -0
  666. chrys/foundation/trajectory/reader.py +271 -0
  667. chrys/foundation/trajectory/recovery.py +225 -0
  668. chrys/foundation/trajectory/revisions.py +152 -0
  669. chrys/foundation/trajectory/segments.py +225 -0
  670. chrys/foundation/trajectory/tools.py +117 -0
  671. chrys/foundation/trajectory/usage.py +151 -0
  672. chrys/foundation/trajectory/writer.py +972 -0
  673. chrys/foundation/trajectory_timing.py +97 -0
  674. chrys/foundation/util/__init__.py +3 -0
  675. chrys/foundation/util/async_tasks.py +29 -0
  676. chrys/foundation/util/chrys_headers.py +23 -0
  677. chrys/foundation/util/env_templates.py +104 -0
  678. chrys/foundation/util/filenames.py +96 -0
  679. chrys/foundation/util/header_charset.py +138 -0
  680. chrys/foundation/util/httpx_helpers.py +12 -0
  681. chrys/foundation/util/lock.py +236 -0
  682. chrys/foundation/util/once_close.py +46 -0
  683. chrys/foundation/util/session_ids.py +29 -0
  684. chrys/foundation/util/shell_tokens.py +88 -0
  685. chrys/foundation/util/sub_agent_context.py +38 -0
  686. chrys/foundation/util/time.py +35 -0
  687. chrys/foundation/util/unicode_scalars.py +36 -0
  688. chrys/foundation/vendor/__init__.py +43 -0
  689. chrys/foundation/vendor/ripgrep/rg-aarch64-apple-darwin +0 -0
  690. chrys/foundation/vendor/ripgrep/rg-aarch64-pc-windows-msvc.exe +0 -0
  691. chrys/foundation/vendor/ripgrep/rg-aarch64-unknown-linux-gnu +0 -0
  692. chrys/foundation/vendor/ripgrep/rg-x86_64-apple-darwin +0 -0
  693. chrys/foundation/vendor/ripgrep/rg-x86_64-pc-windows-msvc.exe +0 -0
  694. chrys/foundation/vendor/ripgrep/rg-x86_64-unknown-linux-musl +0 -0
  695. chrys/kernel/__init__.py +256 -0
  696. chrys/kernel/_content.py +1866 -0
  697. chrys/kernel/_loop_recorder.py +631 -0
  698. chrys/kernel/_null_overlay.py +1026 -0
  699. chrys/kernel/_result_ceiling.py +216 -0
  700. chrys/kernel/_serialization.py +431 -0
  701. chrys/kernel/_tool_arg_errors.py +1629 -0
  702. chrys/kernel/_tool_execution.py +1172 -0
  703. chrys/kernel/_tool_expansion.py +26 -0
  704. chrys/kernel/_types.py +2598 -0
  705. chrys/kernel/agent.py +994 -0
  706. chrys/kernel/client.py +1084 -0
  707. chrys/kernel/compaction.py +1315 -0
  708. chrys/kernel/exceptions.py +211 -0
  709. chrys/kernel/exchanges.py +683 -0
  710. chrys/kernel/identity.py +95 -0
  711. chrys/kernel/images.py +36 -0
  712. chrys/kernel/instrumentation.py +1618 -0
  713. chrys/kernel/loop.py +2308 -0
  714. chrys/kernel/middleware.py +616 -0
  715. chrys/kernel/sessions.py +604 -0
  716. chrys/kernel/tools.py +1061 -0
  717. chrys/kernel/types.py +126 -0
  718. chrys/orchestration/__init__.py +3 -0
  719. chrys/orchestration/engine/__init__.py +3 -0
  720. chrys/orchestration/engine/assembly.py +236 -0
  721. chrys/orchestration/engine/build/__init__.py +3 -0
  722. chrys/orchestration/engine/build/builder.py +1061 -0
  723. chrys/orchestration/engine/build/construction.py +408 -0
  724. chrys/orchestration/engine/build/loaded.py +139 -0
  725. chrys/orchestration/engine/engine.py +789 -0
  726. chrys/orchestration/engine/execution.py +771 -0
  727. chrys/orchestration/engine/loader.py +865 -0
  728. chrys/orchestration/engine/rollback.py +918 -0
  729. chrys/orchestration/engine/run/__init__.py +3 -0
  730. chrys/orchestration/engine/run/active_injection.py +419 -0
  731. chrys/orchestration/engine/run/attachments.py +519 -0
  732. chrys/orchestration/engine/run/bindings.py +1095 -0
  733. chrys/orchestration/engine/run/coordinator.py +1062 -0
  734. chrys/orchestration/engine/run/finalizer.py +417 -0
  735. chrys/orchestration/engine/run/input_refs.py +60 -0
  736. chrys/orchestration/engine/run/prompt_content.py +457 -0
  737. chrys/orchestration/engine/run/resume.py +299 -0
  738. chrys/orchestration/engine/run/retry.py +948 -0
  739. chrys/orchestration/engine/run/runner.py +625 -0
  740. chrys/orchestration/engine/run/runtime_skills.py +146 -0
  741. chrys/orchestration/engine/run/sub_agent_coordination.py +126 -0
  742. chrys/orchestration/engine/run/turn_hooks.py +258 -0
  743. chrys/orchestration/engine/run/turn_state.py +149 -0
  744. chrys/orchestration/engine/run/working_dir.py +50 -0
  745. chrys/orchestration/engine/session_lifecycle.py +2013 -0
  746. chrys/orchestration/engine/state/__init__.py +3 -0
  747. chrys/orchestration/engine/state/active_session.py +183 -0
  748. chrys/orchestration/engine/state/controls.py +639 -0
  749. chrys/orchestration/engine/state/current_agent.py +26 -0
  750. chrys/orchestration/engine/state/lifecycle_permits.py +395 -0
  751. chrys/orchestration/engine/state/machine.py +193 -0
  752. chrys/orchestration/engine/state/session_writer.py +434 -0
  753. chrys/orchestration/engine/trajectory.py +714 -0
  754. chrys/orchestration/engine/usage.py +118 -0
  755. chrys/orchestration/invoker/__init__.py +3 -0
  756. chrys/orchestration/invoker/acp.py +727 -0
  757. chrys/orchestration/invoker/acp_protocol.py +1115 -0
  758. chrys/orchestration/invoker/acp_spec.py +79 -0
  759. chrys/orchestration/invoker/attempts.py +994 -0
  760. chrys/orchestration/invoker/child_compaction.py +131 -0
  761. chrys/orchestration/invoker/child_history.py +99 -0
  762. chrys/orchestration/invoker/contracts.py +253 -0
  763. chrys/orchestration/invoker/evidence.py +94 -0
  764. chrys/orchestration/invoker/kernel.py +343 -0
  765. chrys/orchestration/invoker/origin.py +79 -0
  766. chrys/orchestration/invoker/resources.py +343 -0
  767. chrys/orchestration/invoker/runtime.py +299 -0
  768. chrys/orchestration/invoker/usage.py +80 -0
  769. chrys/orchestration/session_hooks.py +117 -0
  770. chrys/orchestration/session_host.py +1023 -0
  771. chrys/orchestration/session_resources.py +95 -0
  772. chrys/orchestration/session_usage.py +219 -0
  773. chrys/orchestration/startup.py +255 -0
  774. chrys/orchestration/sub_agents/__init__.py +3 -0
  775. chrys/orchestration/sub_agents/acp_policy.py +269 -0
  776. chrys/orchestration/sub_agents/kernel_policy.py +848 -0
  777. chrys/orchestration/sub_agents/registration.py +44 -0
  778. chrys/orchestration/sub_agents/shell.py +337 -0
  779. chrys/orchestration/sub_agents/tools.py +2228 -0
  780. chrys/orchestration/workflows/__init__.py +3 -0
  781. chrys/orchestration/workflows/agent_archive.py +156 -0
  782. chrys/orchestration/workflows/agent_node.py +894 -0
  783. chrys/orchestration/workflows/agent_node_build.py +687 -0
  784. chrys/orchestration/workflows/catalog.py +332 -0
  785. chrys/orchestration/workflows/coordinator.py +771 -0
  786. chrys/orchestration/workflows/hooks.py +176 -0
  787. chrys/orchestration/workflows/preview.py +322 -0
  788. chrys/orchestration/workflows/rollback.py +151 -0
  789. chrys/orchestration/workflows/runner.py +986 -0
  790. chrys/orchestration/workflows/session.py +390 -0
  791. chrys/orchestration/workflows/settings.py +97 -0
  792. chrys/orchestration/workflows/validation.py +674 -0
  793. chrys/orchestration/workflows/worker_client.py +1002 -0
  794. chrys/service/__init__.py +3 -0
  795. chrys/service/acp_client/__init__.py +65 -0
  796. chrys/service/acp_client/_transport.py +922 -0
  797. chrys/service/acp_client/client.py +1311 -0
  798. chrys/service/acp_client/errors.py +224 -0
  799. chrys/service/acp_client/protocol.py +220 -0
  800. chrys/service/acp_client/spawn.py +239 -0
  801. chrys/service/acp_client/spec.py +100 -0
  802. chrys/service/agent_middleware/__init__.py +60 -0
  803. chrys/service/agent_middleware/_metadata_keys.py +52 -0
  804. chrys/service/agent_middleware/control/__init__.py +3 -0
  805. chrys/service/agent_middleware/control/approval.py +769 -0
  806. chrys/service/agent_middleware/control/approval_hooks.py +62 -0
  807. chrys/service/agent_middleware/control/ask_user.py +207 -0
  808. chrys/service/agent_middleware/control/interrupt.py +50 -0
  809. chrys/service/agent_middleware/control/sleep.py +118 -0
  810. chrys/service/agent_middleware/control/todo.py +117 -0
  811. chrys/service/agent_middleware/events/__init__.py +3 -0
  812. chrys/service/agent_middleware/events/hook_dispatch.py +250 -0
  813. chrys/service/agent_middleware/events/hosted_tools.py +1762 -0
  814. chrys/service/agent_middleware/events/intermediate_text.py +128 -0
  815. chrys/service/agent_middleware/events/rejection_metadata.py +50 -0
  816. chrys/service/agent_middleware/events/result_persistence.py +98 -0
  817. chrys/service/agent_middleware/events/sub_agent_events.py +951 -0
  818. chrys/service/agent_middleware/events/tool_error_text.py +22 -0
  819. chrys/service/agent_middleware/events/tool_events.py +779 -0
  820. chrys/service/agent_middleware/injection.py +510 -0
  821. chrys/service/agent_middleware/reminders/__init__.py +91 -0
  822. chrys/service/agent_middleware/reminders/archive_pointer.py +154 -0
  823. chrys/service/agent_middleware/reminders/base.py +25 -0
  824. chrys/service/agent_middleware/reminders/context_usage.py +74 -0
  825. chrys/service/agent_middleware/reminders/file_change.py +63 -0
  826. chrys/service/agent_middleware/reminders/mcp.py +39 -0
  827. chrys/service/agent_middleware/reminders/profile_switch.py +106 -0
  828. chrys/service/agent_middleware/reminders/runtime_env.py +190 -0
  829. chrys/service/agent_middleware/reminders/skills.py +44 -0
  830. chrys/service/agent_middleware/reminders/sub_agents.py +35 -0
  831. chrys/service/agent_middleware/reminders/todo.py +35 -0
  832. chrys/service/agent_middleware/reminders/turn_line.py +48 -0
  833. chrys/service/agent_middleware/response_validation.py +1380 -0
  834. chrys/service/agent_middleware/system_reminder.py +1107 -0
  835. chrys/service/agent_middleware/validators.py +396 -0
  836. chrys/service/analytics/__init__.py +124 -0
  837. chrys/service/analytics/_actions.py +896 -0
  838. chrys/service/analytics/_context_evidence.py +357 -0
  839. chrys/service/analytics/_critical_path.py +179 -0
  840. chrys/service/analytics/_facts.py +919 -0
  841. chrys/service/analytics/_insights.py +769 -0
  842. chrys/service/analytics/_metric_ops.py +83 -0
  843. chrys/service/analytics/_session_projection.py +460 -0
  844. chrys/service/analytics/_timeline.py +434 -0
  845. chrys/service/analytics/_turn_graph.py +1237 -0
  846. chrys/service/analytics/_turns.py +1377 -0
  847. chrys/service/analytics/_workflows.py +171 -0
  848. chrys/service/analytics/aggregation.py +1474 -0
  849. chrys/service/analytics/classification.py +128 -0
  850. chrys/service/analytics/export.py +803 -0
  851. chrys/service/analytics/findings.py +282 -0
  852. chrys/service/analytics/math.py +79 -0
  853. chrys/service/analytics/model.py +886 -0
  854. chrys/service/analytics/reader.py +321 -0
  855. chrys/service/approval/__init__.py +3 -0
  856. chrys/service/approval/arbitration.py +102 -0
  857. chrys/service/approval/command_identity.py +109 -0
  858. chrys/service/approval/correlation.py +57 -0
  859. chrys/service/approval/grant_store.py +108 -0
  860. chrys/service/approval/judge.py +589 -0
  861. chrys/service/approval/level.py +15 -0
  862. chrys/service/approval/policy.py +209 -0
  863. chrys/service/approval/reuse.py +223 -0
  864. chrys/service/approval/reuse_binding.py +133 -0
  865. chrys/service/approval/safety_classifier.py +536 -0
  866. chrys/service/approval/turn_context.py +49 -0
  867. chrys/service/context/__init__.py +3 -0
  868. chrys/service/context/compaction/__init__.py +38 -0
  869. chrys/service/context/compaction/breaker.py +156 -0
  870. chrys/service/context/compaction/budgets.py +93 -0
  871. chrys/service/context/compaction/calibration.py +77 -0
  872. chrys/service/context/compaction/compression.py +690 -0
  873. chrys/service/context/compaction/current_turn_drop.py +458 -0
  874. chrys/service/context/compaction/events.py +201 -0
  875. chrys/service/context/compaction/exclusions.py +261 -0
  876. chrys/service/context/compaction/groups.py +361 -0
  877. chrys/service/context/compaction/last_words.py +1996 -0
  878. chrys/service/context/compaction/last_words_state.py +752 -0
  879. chrys/service/context/compaction/scoped.py +472 -0
  880. chrys/service/context/compaction/spill.py +1947 -0
  881. chrys/service/context/compaction/strategy.py +1116 -0
  882. chrys/service/context/compaction/summaries.py +319 -0
  883. chrys/service/context/compaction/turns.py +375 -0
  884. chrys/service/context/manager.py +174 -0
  885. chrys/service/context/memory_loader.py +792 -0
  886. chrys/service/context/middleware/__init__.py +3 -0
  887. chrys/service/context/middleware/usage.py +332 -0
  888. chrys/service/context/providers/__init__.py +3 -0
  889. chrys/service/context/providers/context_management.py +432 -0
  890. chrys/service/context/providers/history.py +647 -0
  891. chrys/service/context/providers/memory.py +50 -0
  892. chrys/service/context/tool_content.py +22 -0
  893. chrys/service/hooks/__init__.py +37 -0
  894. chrys/service/hooks/detached_worker.py +118 -0
  895. chrys/service/hooks/docs/README.md +14 -0
  896. chrys/service/hooks/docs/authoring.md +684 -0
  897. chrys/service/hooks/docs/configuration.md +499 -0
  898. chrys/service/hooks/docs/design.md +359 -0
  899. chrys/service/hooks/docs/project-hooks.md +348 -0
  900. chrys/service/hooks/events.py +157 -0
  901. chrys/service/hooks/loader.py +665 -0
  902. chrys/service/hooks/manager.py +807 -0
  903. chrys/service/hooks/matcher.py +111 -0
  904. chrys/service/hooks/outbox.py +294 -0
  905. chrys/service/hooks/runner.py +572 -0
  906. chrys/service/hooks/schema.py +407 -0
  907. chrys/service/llm/__init__.py +7 -0
  908. chrys/service/llm/_structured_outputs.py +192 -0
  909. chrys/service/llm/anthropic_messages/__init__.py +9 -0
  910. chrys/service/llm/anthropic_messages/client.py +254 -0
  911. chrys/service/llm/anthropic_messages/decode.py +331 -0
  912. chrys/service/llm/anthropic_messages/history.py +279 -0
  913. chrys/service/llm/anthropic_messages/request.py +323 -0
  914. chrys/service/llm/anthropic_messages/server_tools.py +319 -0
  915. chrys/service/llm/anthropic_messages/stream.py +297 -0
  916. chrys/service/llm/anthropic_messages/thinking_binding.py +156 -0
  917. chrys/service/llm/chat_completions/__init__.py +9 -0
  918. chrys/service/llm/chat_completions/client.py +369 -0
  919. chrys/service/llm/chat_completions/decode.py +338 -0
  920. chrys/service/llm/chat_completions/history.py +657 -0
  921. chrys/service/llm/chat_completions/reasoning.py +244 -0
  922. chrys/service/llm/chat_completions/request.py +192 -0
  923. chrys/service/llm/chat_completions/stream.py +546 -0
  924. chrys/service/llm/chat_completions/validation.py +308 -0
  925. chrys/service/llm/clients.py +613 -0
  926. chrys/service/llm/images.py +65 -0
  927. chrys/service/llm/mock.py +413 -0
  928. chrys/service/llm/observer.py +446 -0
  929. chrys/service/llm/one_shot.py +106 -0
  930. chrys/service/llm/openai_exceptions.py +93 -0
  931. chrys/service/llm/openai_responses/__init__.py +10 -0
  932. chrys/service/llm/openai_responses/client.py +323 -0
  933. chrys/service/llm/openai_responses/decode.py +432 -0
  934. chrys/service/llm/openai_responses/history.py +519 -0
  935. chrys/service/llm/openai_responses/hosted.py +625 -0
  936. chrys/service/llm/openai_responses/replay.py +629 -0
  937. chrys/service/llm/openai_responses/request.py +342 -0
  938. chrys/service/llm/openai_responses/stream.py +847 -0
  939. chrys/service/llm/openai_timestamps.py +57 -0
  940. chrys/service/llm/providers.py +107 -0
  941. chrys/service/llm/proxy_route.py +54 -0
  942. chrys/service/llm/raw_http_log.py +309 -0
  943. chrys/service/llm/route_facts.py +63 -0
  944. chrys/service/llm/route_sessions.py +49 -0
  945. chrys/service/llm/sdk_anthropic.py +18 -0
  946. chrys/service/llm/sdk_headers.py +47 -0
  947. chrys/service/llm/sdk_openai.py +18 -0
  948. chrys/service/llm/sdk_retry.py +59 -0
  949. chrys/service/llm/wire_client.py +246 -0
  950. chrys/service/mcp/__init__.py +3 -0
  951. chrys/service/mcp/_connection.py +235 -0
  952. chrys/service/mcp/_http_transport.py +350 -0
  953. chrys/service/mcp/_progressive.py +459 -0
  954. chrys/service/mcp/_stdio_transport.py +614 -0
  955. chrys/service/mcp/_tool_mixins.py +96 -0
  956. chrys/service/mcp/adapter.py +1031 -0
  957. chrys/service/mcp/cache.py +476 -0
  958. chrys/service/mcp/content.py +32 -0
  959. chrys/service/mcp/errors.py +169 -0
  960. chrys/service/mcp/owned.py +1330 -0
  961. chrys/service/mcp/result_limits.py +168 -0
  962. chrys/service/mcp/thinking_warning.py +95 -0
  963. chrys/service/mcp/validation.py +74 -0
  964. chrys/service/mutations/__init__.py +84 -0
  965. chrys/service/mutations/coordination.py +894 -0
  966. chrys/service/mutations/detector.py +458 -0
  967. chrys/service/mutations/git_calibrator.py +397 -0
  968. chrys/service/mutations/git_state.py +647 -0
  969. chrys/service/mutations/pipeline.py +779 -0
  970. chrys/service/mutations/scanner.py +466 -0
  971. chrys/service/mutations/snapshot_files.py +233 -0
  972. chrys/service/mutations/store.py +625 -0
  973. chrys/service/mutations/tool_names.py +15 -0
  974. chrys/service/mutations/trace.py +246 -0
  975. chrys/service/mutations/tracker.py +1289 -0
  976. chrys/service/mutations/types.py +559 -0
  977. chrys/service/mutations/workspace_changes.py +1583 -0
  978. chrys/service/profiles/__init__.py +2 -0
  979. chrys/service/profiles/agents/__init__.py +3 -0
  980. chrys/service/profiles/agents/builtins/Code.yaml +199 -0
  981. chrys/service/profiles/agents/builtins/Explore.yaml +62 -0
  982. chrys/service/profiles/agents/builtins/General.yaml +114 -0
  983. chrys/service/profiles/agents/builtins/QA.yaml +128 -0
  984. chrys/service/profiles/agents/loader.py +808 -0
  985. chrys/service/profiles/agents/registry.py +727 -0
  986. chrys/service/profiles/agents/schema.py +371 -0
  987. chrys/service/profiles/agents/serializer.py +196 -0
  988. chrys/service/profiles/coercion.py +22 -0
  989. chrys/service/profiles/models/__init__.py +3 -0
  990. chrys/service/profiles/models/env_bridge.py +157 -0
  991. chrys/service/profiles/models/loader.py +282 -0
  992. chrys/service/profiles/models/options.py +438 -0
  993. chrys/service/profiles/models/registry.py +104 -0
  994. chrys/service/profiles/models/resolver.py +286 -0
  995. chrys/service/profiles/models/schema.py +91 -0
  996. chrys/service/profiles/models/serializer.py +76 -0
  997. chrys/service/session/__init__.py +3 -0
  998. chrys/service/session/checkpoint.py +156 -0
  999. chrys/service/session/history.py +1616 -0
  1000. chrys/service/session/message_metadata.py +151 -0
  1001. chrys/service/session/persistence.py +435 -0
  1002. chrys/service/session/runtime_metadata.py +210 -0
  1003. chrys/service/session/sub_agent_logs.py +1069 -0
  1004. chrys/service/session/sub_agent_transcript.py +654 -0
  1005. chrys/service/skills/__init__.py +3 -0
  1006. chrys/service/skills/adapter.py +408 -0
  1007. chrys/service/skills/constants.py +8 -0
  1008. chrys/service/skills/loader.py +575 -0
  1009. chrys/service/skills/model.py +130 -0
  1010. chrys/service/skills/provider.py +841 -0
  1011. chrys/service/skills/runner.py +390 -0
  1012. chrys/service/state/__init__.py +3 -0
  1013. chrys/service/state/_fork.py +710 -0
  1014. chrys/service/state/_session_files.py +175 -0
  1015. chrys/service/state/_session_meta.py +552 -0
  1016. chrys/service/state/locks.py +154 -0
  1017. chrys/service/state/serializers.py +156 -0
  1018. chrys/service/state/session_catalog.py +361 -0
  1019. chrys/service/state/session_listing.py +84 -0
  1020. chrys/service/state/session_migration.py +446 -0
  1021. chrys/service/state/session_mru.py +315 -0
  1022. chrys/service/state/store.py +2132 -0
  1023. chrys/service/state/workflow.py +158 -0
  1024. chrys/service/text_blocks.py +47 -0
  1025. chrys/service/todos/__init__.py +3 -0
  1026. chrys/service/todos/reminder.py +31 -0
  1027. chrys/service/todos/tracker.py +58 -0
  1028. chrys/service/tools/__init__.py +3 -0
  1029. chrys/service/tools/approval_targets.py +92 -0
  1030. chrys/service/tools/builtins/__init__.py +3 -0
  1031. chrys/service/tools/builtins/ask_user.py +326 -0
  1032. chrys/service/tools/builtins/doc_converter/__init__.py +494 -0
  1033. chrys/service/tools/builtins/doc_converter/artifacts.py +392 -0
  1034. chrys/service/tools/builtins/doc_converter/parsers/__init__.py +3 -0
  1035. chrys/service/tools/builtins/doc_converter/parsers/_table.py +34 -0
  1036. chrys/service/tools/builtins/doc_converter/parsers/base.py +101 -0
  1037. chrys/service/tools/builtins/doc_converter/parsers/docx.py +88 -0
  1038. chrys/service/tools/builtins/doc_converter/parsers/pdf.py +144 -0
  1039. chrys/service/tools/builtins/doc_converter/parsers/pptx.py +103 -0
  1040. chrys/service/tools/builtins/doc_converter/parsers/xls.py +32 -0
  1041. chrys/service/tools/builtins/doc_converter/parsers/xlsx.py +51 -0
  1042. chrys/service/tools/builtins/doc_converter/registry.py +55 -0
  1043. chrys/service/tools/builtins/filesystem.py +1071 -0
  1044. chrys/service/tools/builtins/search.py +1265 -0
  1045. chrys/service/tools/builtins/shell.py +967 -0
  1046. chrys/service/tools/builtins/shell_command_filtering/__init__.py +5 -0
  1047. chrys/service/tools/builtins/shell_command_filtering/policy.py +60 -0
  1048. chrys/service/tools/builtins/shell_command_filtering/presets.py +152 -0
  1049. chrys/service/tools/builtins/shell_command_filtering/readonly/__init__.py +358 -0
  1050. chrys/service/tools/builtins/shell_command_filtering/readonly/commands.py +416 -0
  1051. chrys/service/tools/builtins/shell_command_filtering/readonly/git.py +408 -0
  1052. chrys/service/tools/builtins/shell_command_filtering/readonly/syntax.py +87 -0
  1053. chrys/service/tools/builtins/shell_command_filtering/tokenizer.py +214 -0
  1054. chrys/service/tools/builtins/shell_filter.py +24 -0
  1055. chrys/service/tools/builtins/sleep.py +91 -0
  1056. chrys/service/tools/builtins/todo.py +64 -0
  1057. chrys/service/tools/builtins/web/__init__.py +6 -0
  1058. chrys/service/tools/builtins/web/build.py +268 -0
  1059. chrys/service/tools/builtins/web/config.py +338 -0
  1060. chrys/service/tools/builtins/web/credentials.py +37 -0
  1061. chrys/service/tools/builtins/web/fetch/__init__.py +3 -0
  1062. chrys/service/tools/builtins/web/fetch/cache.py +68 -0
  1063. chrys/service/tools/builtins/web/fetch/convert.py +56 -0
  1064. chrys/service/tools/builtins/web/fetch/tool.py +223 -0
  1065. chrys/service/tools/builtins/web/hosted.py +61 -0
  1066. chrys/service/tools/builtins/web/http.py +330 -0
  1067. chrys/service/tools/builtins/web/progress.py +18 -0
  1068. chrys/service/tools/builtins/web/search/__init__.py +3 -0
  1069. chrys/service/tools/builtins/web/search/exa_mcp.py +146 -0
  1070. chrys/service/tools/builtins/web/search/html.py +152 -0
  1071. chrys/service/tools/builtins/web/search/json_api.py +227 -0
  1072. chrys/service/tools/builtins/web/search/providers.py +77 -0
  1073. chrys/service/tools/builtins/web/search/tool.py +218 -0
  1074. chrys/service/tools/builtins/web/search/types.py +28 -0
  1075. chrys/service/tools/builtins/web/tls_identity.py +66 -0
  1076. chrys/service/tools/kinds.py +115 -0
  1077. chrys/service/tools/names.py +47 -0
  1078. chrys/service/tools/registry.py +242 -0
  1079. chrys/service/tools/result_metadata.py +103 -0
  1080. chrys/service/tools/session_artifacts.py +196 -0
  1081. chrys/service/tools/spill.py +218 -0
  1082. chrys/service/tools/workspace_paths.py +81 -0
  1083. chrys/service/trajectory/__init__.py +11 -0
  1084. chrys/service/trajectory/approvals.py +201 -0
  1085. chrys/service/trajectory/compaction.py +420 -0
  1086. chrys/service/trajectory/fork.py +75 -0
  1087. chrys/service/trajectory/hooks.py +217 -0
  1088. chrys/service/trajectory/items.py +41 -0
  1089. chrys/service/trajectory/preparation.py +374 -0
  1090. chrys/service/trajectory/retries.py +95 -0
  1091. chrys/service/trajectory/revisions.py +129 -0
  1092. chrys/service/trajectory/segmented.py +163 -0
  1093. chrys/service/trajectory/session.py +762 -0
  1094. chrys/service/trajectory/state.py +83 -0
  1095. chrys/service/trajectory/sub_agents.py +169 -0
  1096. chrys/service/trajectory/tombstone.py +262 -0
  1097. chrys/service/trajectory/tools.py +323 -0
  1098. chrys/service/trajectory/validation.py +130 -0
  1099. chrys/service/trajectory/waits.py +161 -0
  1100. chrys/service/trajectory/workflow.py +148 -0
  1101. chrys/service/vision.py +128 -0
  1102. chrys/service/workflows/__init__.py +3 -0
  1103. chrys/service/workflows/admission.py +151 -0
  1104. chrys/service/workflows/artifacts.py +109 -0
  1105. chrys/service/workflows/asks.py +130 -0
  1106. chrys/service/workflows/builtins/__init__.py +12 -0
  1107. chrys/service/workflows/builtins/demo-workflow.manifest.json +635 -0
  1108. chrys/service/workflows/builtins/demo-workflow.py +756 -0
  1109. chrys/service/workflows/discovery.py +554 -0
  1110. chrys/service/workflows/environment.py +284 -0
  1111. chrys/service/workflows/graph.py +430 -0
  1112. chrys/service/workflows/history.py +82 -0
  1113. chrys/service/workflows/interpreter.py +107 -0
  1114. chrys/service/workflows/journal.py +334 -0
  1115. chrys/service/workflows/layout.py +56 -0
  1116. chrys/service/workflows/ledger.py +149 -0
  1117. chrys/service/workflows/model_selection.py +18 -0
  1118. chrys/service/workflows/orphans.py +209 -0
  1119. chrys/service/workflows/outcomes.py +46 -0
  1120. chrys/service/workflows/protocol.py +210 -0
  1121. chrys/service/workflows/records.py +142 -0
  1122. chrys/service/workflows/scheduler.py +770 -0
  1123. chrys/service/workflows/sdk/__init__.py +51 -0
  1124. chrys/service/workflows/sdk/_ask.py +167 -0
  1125. chrys/service/workflows/sdk/_builder.py +781 -0
  1126. chrys/service/workflows/sdk/_values.py +92 -0
  1127. chrys/service/workflows/sdk_artifact.py +74 -0
  1128. chrys/service/workflows/store.py +494 -0
  1129. chrys/service/workflows/transcript.py +102 -0
  1130. chrys/service/workflows/values.py +73 -0
  1131. chrys/service/workflows/worker_host.py +1820 -0
  1132. chrys/workflows.py +38 -0
  1133. icode_tui-0.29.1.dist-info/METADATA +104 -0
  1134. icode_tui-0.29.1.dist-info/RECORD +1138 -0
  1135. icode_tui-0.29.1.dist-info/WHEEL +4 -0
  1136. icode_tui-0.29.1.dist-info/entry_points.txt +2 -0
  1137. icode_tui-0.29.1.dist-info/licenses/LICENSE +202 -0
  1138. icode_tui-0.29.1.dist-info/licenses/NOTICE +1955 -0
@@ -0,0 +1,1468 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """Async subprocess utilities.
4
+
5
+ Provides ``managed_subprocess`` for proper transport cleanup on Windows
6
+ (CPython issue #43884) and ``decode_subprocess_output`` for robust
7
+ decoding of subprocess byte output with a strong UTF-8 preference.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import codecs
14
+ import contextlib
15
+ import ctypes
16
+ import errno
17
+ import functools
18
+ import ntpath
19
+ import os
20
+ import shutil
21
+ import signal
22
+ import subprocess
23
+ import sys
24
+ from collections.abc import AsyncIterator, Awaitable, Iterable, Iterator
25
+ from ctypes import wintypes
26
+ from dataclasses import dataclass
27
+ from typing import Any, Never, cast
28
+
29
+ from chrys.foundation.platform.c_api import declare_functions, struct_fields
30
+ from chrys.foundation.platform.child_reap import install_stopped_child_reap_fix
31
+ from chrys.foundation.text.encoding import decode_bytes, is_mostly_text
32
+
33
+ # Every subprocess in the app is spawned through this module, and the shim has to
34
+ # be in place before the first one starts, so install it on import. See
35
+ # chrys.foundation.platform.child_reap for what it works around.
36
+ install_stopped_child_reap_fix()
37
+
38
+ _CP_UTF8 = 65001
39
+ _STOPPED_PROCESS_POLL_INTERVAL = 1.0
40
+ _PROCESS_WAIT_CLEANUP_TIMEOUT = 2.0
41
+ _WINDOWS_TREE_KILL_TIMEOUT = 2.0
42
+
43
+
44
+ class SubprocessStoppedError(RuntimeError):
45
+ """Raised when a POSIX subprocess group/session enters job-control stopped state."""
46
+
47
+
48
+ class MissingWorkingDirectoryError(OSError):
49
+ """A child could not start because its working directory no longer exists.
50
+
51
+ A spawn into a deleted cwd fails with the same ``FileNotFoundError`` as a
52
+ missing executable on POSIX (``NotADirectoryError``/``WinError 267`` on
53
+ Windows). This is deliberately NOT a ``FileNotFoundError`` subclass, so
54
+ "executable not found" handlers do not swallow it.
55
+ """
56
+
57
+ def __init__(self, path: str) -> None:
58
+ super().__init__(errno.ENOENT, "working directory no longer exists", path)
59
+ self.path = path
60
+
61
+ def __str__(self) -> str:
62
+ return f"working directory no longer exists: {self.path}"
63
+
64
+
65
+ def raise_if_missing_cwd(cwd: object, error: OSError) -> None:
66
+ """Re-raise a failed spawn as :class:`MissingWorkingDirectoryError` when *cwd* is gone.
67
+
68
+ Called from an ``except OSError`` around a spawn. Checking the directory
69
+ after the failure covers both platforms without parsing errno, winerror
70
+ or ``filename``; when *cwd* still exists this returns and the caller
71
+ re-raises the original error.
72
+ """
73
+ if isinstance(error, MissingWorkingDirectoryError) or not isinstance(cwd, str | os.PathLike):
74
+ return
75
+ path = os.fspath(cwd)
76
+ if isinstance(path, str) and path and not os.path.isdir(path):
77
+ raise MissingWorkingDirectoryError(path) from error
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class _ProcessStatus:
82
+ pid: int
83
+ state: str
84
+ pgid: int
85
+ session_id: int
86
+
87
+
88
+ @dataclass(frozen=True)
89
+ class _WindowsProcessAPI:
90
+ """Typed handle for the lazily loaded Win32 process API."""
91
+
92
+ kernel32: ctypes.CDLL
93
+
94
+
95
+ class _StartupInfoW(ctypes.Structure):
96
+ """``STARTUPINFOW``."""
97
+
98
+ _fields_ = struct_fields(
99
+ (wintypes.DWORD, "cb"),
100
+ (wintypes.LPWSTR, "lpReserved lpDesktop lpTitle"),
101
+ (wintypes.DWORD, "dwX dwY dwXSize dwYSize dwXCountChars dwYCountChars dwFillAttribute dwFlags"),
102
+ (wintypes.WORD, "wShowWindow cbReserved2"),
103
+ (ctypes.POINTER(ctypes.c_ubyte), "lpReserved2"),
104
+ (wintypes.HANDLE, "hStdInput hStdOutput hStdError"),
105
+ )
106
+
107
+
108
+ class _StartupInfoExW(ctypes.Structure):
109
+ """``STARTUPINFOEXW``: the startup info plus a process/thread attribute list."""
110
+
111
+ _fields_ = struct_fields((_StartupInfoW, "StartupInfo"), (wintypes.LPVOID, "lpAttributeList"))
112
+
113
+
114
+ class _ProcessInformation(ctypes.Structure):
115
+ """``PROCESS_INFORMATION``, filled in by ``CreateProcessW``."""
116
+
117
+ _fields_ = struct_fields((wintypes.HANDLE, "hProcess hThread"), (wintypes.DWORD, "dwProcessId dwThreadId"))
118
+
119
+
120
+ class _IoCounters(ctypes.Structure):
121
+ """``IO_COUNTERS``."""
122
+
123
+ _fields_ = struct_fields(
124
+ (ctypes.c_ulonglong, "ReadOperationCount WriteOperationCount OtherOperationCount"),
125
+ (ctypes.c_ulonglong, "ReadTransferCount WriteTransferCount OtherTransferCount"),
126
+ )
127
+
128
+
129
+ class _BasicLimitInformation(ctypes.Structure):
130
+ """``JOBOBJECT_BASIC_LIMIT_INFORMATION``."""
131
+
132
+ _fields_ = struct_fields(
133
+ (ctypes.c_longlong, "PerProcessUserTimeLimit PerJobUserTimeLimit"),
134
+ (wintypes.DWORD, "LimitFlags"),
135
+ (ctypes.c_size_t, "MinimumWorkingSetSize MaximumWorkingSetSize"),
136
+ (wintypes.DWORD, "ActiveProcessLimit"),
137
+ (ctypes.c_size_t, "Affinity"),
138
+ (wintypes.DWORD, "PriorityClass SchedulingClass"),
139
+ )
140
+
141
+
142
+ class _ExtendedLimitInformation(ctypes.Structure):
143
+ """``JOBOBJECT_EXTENDED_LIMIT_INFORMATION``, the job's kill-on-close setting among them."""
144
+
145
+ _fields_ = struct_fields(
146
+ (_BasicLimitInformation, "BasicLimitInformation"),
147
+ (_IoCounters, "IoInfo"),
148
+ (ctypes.c_size_t, "ProcessMemoryLimit JobMemoryLimit PeakProcessMemoryUsed PeakJobMemoryUsed"),
149
+ )
150
+
151
+
152
+ @functools.cache
153
+ def _windows_uses_utf8() -> bool:
154
+ """Return True if the Windows console output code page is UTF-8.
155
+
156
+ Uses ``GetConsoleOutputCP`` (the OEM output code page) rather than
157
+ ``locale.getencoding`` (the ANSI code page) because subprocess
158
+ output follows the console code page, not the ANSI one.
159
+ """
160
+ if sys.platform != "win32":
161
+ return True
162
+ try:
163
+ return ctypes.windll.kernel32.GetConsoleOutputCP() == _CP_UTF8
164
+ except AttributeError, OSError:
165
+ return False
166
+
167
+
168
+ @functools.cache
169
+ def _windows_console_encoding() -> str | None:
170
+ """Return the Python codec name for the Windows console output code page.
171
+
172
+ E.g. ``"cp936"`` for GBK (Simplified Chinese), ``"cp932"`` for
173
+ Shift-JIS (Japanese), ``"cp949"`` for EUC-KR (Korean).
174
+
175
+ Returns ``None`` on non-Windows platforms, when the code page is
176
+ UTF-8, or when the code page cannot be determined.
177
+ """
178
+ if sys.platform != "win32":
179
+ return None
180
+ try:
181
+ cp = ctypes.windll.kernel32.GetConsoleOutputCP()
182
+ if cp == _CP_UTF8 or cp == 0:
183
+ return None
184
+ return f"cp{cp}"
185
+ except AttributeError, OSError:
186
+ return None
187
+
188
+
189
+ _CREATE_NEW_CONSOLE = 0x00000010
190
+ _WINDOWS_BATCH_SUFFIXES = {".bat", ".cmd"}
191
+ _WINDOWS_BATCH_FORBIDDEN = {'"', "%", "!", "^", "\r", "\n"}
192
+
193
+
194
+ def windows_hidden_subprocess_kwargs() -> dict[str, Any]:
195
+ """Return subprocess kwargs that spawn a hidden child with its *own* console.
196
+
197
+ Intended for synchronous ``subprocess.run`` callers — unpack with
198
+ ``**windows_hidden_subprocess_kwargs()``. Returns ``{}`` off Windows.
199
+
200
+ On Windows we pair:
201
+
202
+ - ``creationflags=CREATE_NEW_CONSOLE`` — the child gets its own
203
+ console, so it cannot call ``SetConsoleMode()`` on the parent's
204
+ console (which would reset mouse / keyboard flags and break the
205
+ TUI). ``CREATE_NO_WINDOW`` is **not** safe here: it makes the
206
+ child share the parent's console.
207
+ - ``startupinfo`` with ``STARTF_USESHOWWINDOW | SW_HIDE`` — suppress
208
+ the new console window so it doesn't flash on screen.
209
+ """
210
+ if sys.platform != "win32":
211
+ return {}
212
+ import subprocess as _sp
213
+
214
+ si = _sp.STARTUPINFO()
215
+ si.dwFlags |= _sp.STARTF_USESHOWWINDOW
216
+ si.wShowWindow = _sp.SW_HIDE
217
+ return {"creationflags": _CREATE_NEW_CONSOLE, "startupinfo": si}
218
+
219
+
220
+ _windows_hidden_subprocess_kwargs = windows_hidden_subprocess_kwargs
221
+ """Backward-compatible alias for callers predating the public helper."""
222
+
223
+
224
+ def finalize_subprocess_transport(proc: asyncio.subprocess.Process) -> None:
225
+ """Close an asyncio subprocess transport, including Proactor pipe transports."""
226
+ transport = getattr(proc, "_transport", None)
227
+ if transport is not None:
228
+ with contextlib.suppress(OSError):
229
+ transport.close()
230
+
231
+
232
+ def resolve_windows_comspec(parent_env: dict[str, str] | None = None) -> str:
233
+ """Resolve a trusted absolute ``cmd.exe`` from the parent environment."""
234
+ source = os.environ if parent_env is None else parent_env
235
+ casefolded = {key.casefold(): value for key, value in source.items()}
236
+ candidate = casefolded.get("comspec", "")
237
+ if (
238
+ type(candidate) is str
239
+ and ntpath.isabs(candidate)
240
+ and not any(character in candidate for character in _WINDOWS_BATCH_FORBIDDEN | {"\x00"})
241
+ ):
242
+ return ntpath.normpath(candidate)
243
+ system_root = casefolded.get("systemroot", r"C:\Windows")
244
+ if (
245
+ type(system_root) is not str
246
+ or not ntpath.isabs(system_root)
247
+ or any(character in system_root for character in _WINDOWS_BATCH_FORBIDDEN | {"\x00"})
248
+ ):
249
+ system_root = r"C:\Windows"
250
+ return ntpath.normpath(ntpath.join(system_root, "System32", "cmd.exe"))
251
+
252
+
253
+ def serialize_windows_batch_command(shim_path: str, args: Iterable[str]) -> str:
254
+ """Serialize the representable subset accepted by the ACP batch shim boundary."""
255
+ values = [shim_path, *args]
256
+ for value in values:
257
+ if type(value) is not str:
258
+ raise TypeError("Windows batch command values must be exact strings.")
259
+ if "\x00" in value:
260
+ raise ValueError("Windows batch command values cannot contain NUL.")
261
+ if any(character in value for character in _WINDOWS_BATCH_FORBIDDEN):
262
+ raise ValueError("Windows batch command contains characters that cannot be represented safely.")
263
+ if value.endswith("\\"):
264
+ raise ValueError("Windows batch command cannot safely represent a trailing backslash.")
265
+ if not ntpath.isabs(shim_path):
266
+ raise ValueError("Windows batch shim path must be absolute.")
267
+ return " ".join(f'"{value}"' for value in values)
268
+
269
+
270
+ @dataclass
271
+ class ManagedStdioProcess:
272
+ """Owned subprocess with real asyncio streams and process-tree termination."""
273
+
274
+ stdin: asyncio.StreamWriter
275
+ stdout: asyncio.StreamReader
276
+ stderr: asyncio.StreamReader
277
+ pid: int
278
+ process_group_id: int | None = None
279
+ _process: asyncio.subprocess.Process | None = None
280
+ _windows_process_handle: int | None = None
281
+ _windows_job_handle: int | None = None
282
+ _windows_returncode: int | None = None
283
+ _windows_stdout_transport: asyncio.BaseTransport | None = None
284
+ _windows_stderr_transport: asyncio.BaseTransport | None = None
285
+ _windows_wait_task: asyncio.Task[int] | None = None
286
+
287
+ @property
288
+ def returncode(self) -> int | None:
289
+ if self._process is not None:
290
+ return self._process.returncode
291
+ return self._windows_returncode
292
+
293
+ async def wait(self) -> int:
294
+ """Wait for the retained process and return its exit code."""
295
+ if self._process is not None:
296
+ return await self._process.wait()
297
+ if self._windows_returncode is not None:
298
+ return self._windows_returncode
299
+ waiter = self._windows_wait_task
300
+ if waiter is None:
301
+ if self._windows_process_handle is None:
302
+ raise RuntimeError("Windows process handle has already been released.")
303
+ # One registered wait per process: the loop's completion port completes
304
+ # it when the process exits, with no thread blocked in WaitForSingleObject,
305
+ # and every wait() call awaits that same task.
306
+ waiter = asyncio.create_task(
307
+ _windows_process_exit(self._windows_process_handle),
308
+ name="chrys.platform.windows-process-wait",
309
+ )
310
+ waiter.add_done_callback(_consume_task_exception)
311
+ self._windows_wait_task = waiter
312
+ # asyncio.wait never cancels the watched task: a caller-scoped timeout
313
+ # interrupts this wait() while the registered wait stays put for the
314
+ # next caller to resume awaiting.
315
+ await asyncio.wait({waiter})
316
+ self._windows_returncode = waiter.result()
317
+ return self._windows_returncode
318
+
319
+ def close_stdin(self) -> None:
320
+ """Send EOF to the child without waiting for its process exit."""
321
+ try:
322
+ self.stdin.write_eof()
323
+ except AttributeError, NotImplementedError, OSError, RuntimeError:
324
+ self.stdin.close()
325
+
326
+ def terminate_tree(self) -> None:
327
+ """Terminate the owned process tree using retained identities only."""
328
+ if self._process is not None:
329
+ if self.process_group_id is not None:
330
+ terminate_process_group(self.process_group_id)
331
+ elif self._process.returncode is None:
332
+ with contextlib.suppress(ProcessLookupError, OSError):
333
+ self._process.terminate()
334
+ return
335
+ _terminate_windows_retained_process(self._windows_job_handle, self._windows_process_handle, exit_code=1)
336
+
337
+ def kill_tree(self) -> None:
338
+ """Kill the owned process tree using retained identities only."""
339
+ if self._process is not None:
340
+ if self.process_group_id is not None:
341
+ kill_process_group(self.process_group_id)
342
+ elif self._process.returncode is None:
343
+ with contextlib.suppress(ProcessLookupError, OSError):
344
+ self._process.kill()
345
+ return
346
+ _terminate_windows_retained_process(self._windows_job_handle, self._windows_process_handle, exit_code=9)
347
+
348
+ def close_transports(self) -> None:
349
+ """Close owned stream transports and release retained process handles."""
350
+ self.stdin.close()
351
+ if self._process is not None:
352
+ finalize_subprocess_transport(self._process)
353
+ return
354
+ if self._windows_stdout_transport is not None:
355
+ self._windows_stdout_transport.close()
356
+ self._windows_stdout_transport = None
357
+ if self._windows_stderr_transport is not None:
358
+ self._windows_stderr_transport.close()
359
+ self._windows_stderr_transport = None
360
+ process_handle = self._windows_process_handle
361
+ job_handle = self._windows_job_handle
362
+ self._windows_process_handle = None
363
+ self._windows_job_handle = None
364
+ # The job handle is never waited on and KILL_ON_JOB_CLOSE makes closing
365
+ # it the final kill switch, so it always closes inline. The process
366
+ # handle may still be registered with the completion port — closing a
367
+ # handle under a registered wait is undefined — so its close waits for
368
+ # the wait task to finish: the proactor unregisters the wait before the
369
+ # task can complete, whether the process exited or loop shutdown
370
+ # cancelled the task. A wait the loop never resumes (loop closed) leaks
371
+ # the handle until process exit; leak beats undefined.
372
+ _close_windows_handle(job_handle)
373
+ waiter = self._windows_wait_task
374
+ if waiter is None or waiter.done():
375
+ _close_windows_handle(process_handle)
376
+ return
377
+ waiter.add_done_callback(lambda _task: _close_windows_handle(process_handle))
378
+
379
+
380
+ async def spawn_managed_stdio_process(
381
+ command: str,
382
+ *args: str,
383
+ cwd: str,
384
+ env: dict[str, str],
385
+ limit: int,
386
+ parent_env: dict[str, str] | None = None,
387
+ ) -> ManagedStdioProcess:
388
+ """Spawn an owned stdio child with POSIX-session or Windows-Job semantics."""
389
+ if type(command) is not str or type(cwd) is not str:
390
+ raise TypeError("Subprocess command and cwd must be exact strings.")
391
+ if "\x00" in command or "\x00" in cwd:
392
+ raise ValueError("Subprocess command and cwd cannot contain NUL.")
393
+ if any(type(arg) is not str for arg in args):
394
+ raise TypeError("Subprocess arguments must be exact strings.")
395
+ if any("\x00" in arg for arg in args):
396
+ raise ValueError("Subprocess arguments cannot contain NUL.")
397
+
398
+ from chrys.foundation.platform import get_platform
399
+
400
+ if get_platform().is_windows:
401
+ try:
402
+ return await _spawn_windows_managed_stdio_process(
403
+ command,
404
+ args,
405
+ cwd=cwd,
406
+ env=env,
407
+ limit=limit,
408
+ parent_env=parent_env,
409
+ )
410
+ except OSError as exc:
411
+ raise_if_missing_cwd(cwd, exc)
412
+ raise
413
+
414
+ try:
415
+ process = await asyncio.create_subprocess_exec(
416
+ command,
417
+ *args,
418
+ stdin=asyncio.subprocess.PIPE,
419
+ stdout=asyncio.subprocess.PIPE,
420
+ stderr=asyncio.subprocess.PIPE,
421
+ cwd=cwd,
422
+ env=env,
423
+ limit=limit,
424
+ start_new_session=True,
425
+ )
426
+ except OSError as exc:
427
+ raise_if_missing_cwd(cwd, exc)
428
+ raise
429
+ if process.stdin is None or process.stdout is None or process.stderr is None or process.pid is None:
430
+ with contextlib.suppress(ProcessLookupError):
431
+ process.kill()
432
+ await process.wait()
433
+ raise RuntimeError("Managed subprocess did not expose complete stdio pipes.")
434
+ return ManagedStdioProcess(
435
+ stdin=process.stdin,
436
+ stdout=process.stdout,
437
+ stderr=process.stderr,
438
+ pid=process.pid,
439
+ process_group_id=process.pid,
440
+ _process=process,
441
+ )
442
+
443
+
444
+ def terminate_process_group(process_group_id: int) -> bool:
445
+ """Send SIGTERM to a POSIX process group without signaling Chrys's own group."""
446
+ from chrys.foundation.platform import get_platform
447
+
448
+ if get_platform().is_windows or process_group_id <= 0:
449
+ return False
450
+ posix_os = cast(Any, os)
451
+ with contextlib.suppress(OSError):
452
+ if process_group_id == posix_os.getpgrp():
453
+ return False
454
+ try:
455
+ posix_os.killpg(process_group_id, signal.SIGTERM)
456
+ except ProcessLookupError, PermissionError, OSError:
457
+ return False
458
+ return True
459
+
460
+
461
+ @functools.cache
462
+ def _windows_process_api() -> _WindowsProcessAPI:
463
+ """Load the Win32 process, Job Object, and attribute-list functions once."""
464
+ kernel32 = cast(Any, ctypes).WinDLL("kernel32", use_last_error=True)
465
+ declare_functions(
466
+ kernel32,
467
+ {
468
+ "CreateJobObjectW": (wintypes.HANDLE, [wintypes.LPVOID, wintypes.LPCWSTR]),
469
+ "SetInformationJobObject": (
470
+ wintypes.BOOL,
471
+ [wintypes.HANDLE, ctypes.c_int, wintypes.LPVOID, wintypes.DWORD],
472
+ ),
473
+ "InitializeProcThreadAttributeList": (
474
+ wintypes.BOOL,
475
+ [wintypes.LPVOID, wintypes.DWORD, wintypes.DWORD, ctypes.POINTER(ctypes.c_size_t)],
476
+ ),
477
+ # List, flags, attribute, value and its size, then the reserved previous-value and return-size slots.
478
+ "UpdateProcThreadAttribute": (
479
+ wintypes.BOOL,
480
+ [
481
+ wintypes.LPVOID,
482
+ wintypes.DWORD,
483
+ ctypes.c_size_t,
484
+ wintypes.LPVOID,
485
+ ctypes.c_size_t,
486
+ wintypes.LPVOID,
487
+ wintypes.LPVOID,
488
+ ],
489
+ ),
490
+ "DeleteProcThreadAttributeList": (None, [wintypes.LPVOID]),
491
+ "CreateProcessW": (
492
+ wintypes.BOOL,
493
+ [
494
+ wintypes.LPCWSTR,
495
+ wintypes.LPWSTR,
496
+ wintypes.LPVOID,
497
+ wintypes.LPVOID,
498
+ wintypes.BOOL,
499
+ wintypes.DWORD,
500
+ wintypes.LPVOID,
501
+ wintypes.LPCWSTR,
502
+ ctypes.POINTER(_StartupInfoExW),
503
+ ctypes.POINTER(_ProcessInformation),
504
+ ],
505
+ ),
506
+ "SetHandleInformation": (wintypes.BOOL, [wintypes.HANDLE, wintypes.DWORD, wintypes.DWORD]),
507
+ "TerminateJobObject": (wintypes.BOOL, [wintypes.HANDLE, wintypes.UINT]),
508
+ "TerminateProcess": (wintypes.BOOL, [wintypes.HANDLE, wintypes.UINT]),
509
+ "GetExitCodeProcess": (wintypes.BOOL, [wintypes.HANDLE, ctypes.POINTER(wintypes.DWORD)]),
510
+ "CloseHandle": (wintypes.BOOL, [wintypes.HANDLE]),
511
+ },
512
+ )
513
+ return _WindowsProcessAPI(kernel32=kernel32)
514
+
515
+
516
+ def _raise_windows_process_error(api: _WindowsProcessAPI, message: str) -> None:
517
+ error = cast(Any, ctypes).get_last_error()
518
+ raise OSError(error, message, None, error)
519
+
520
+
521
+ def _windows_create_job(api: _WindowsProcessAPI) -> int:
522
+ job = api.kernel32.CreateJobObjectW(None, None)
523
+ if not job:
524
+ _raise_windows_process_error(api, "Unable to create a Windows Job Object.")
525
+ info = _ExtendedLimitInformation()
526
+ info.BasicLimitInformation.LimitFlags = 0x00002000
527
+ if not api.kernel32.SetInformationJobObject(
528
+ job,
529
+ 9,
530
+ ctypes.byref(info),
531
+ ctypes.sizeof(info),
532
+ ):
533
+ api.kernel32.CloseHandle(job)
534
+ _raise_windows_process_error(api, "Unable to configure a Windows Job Object.")
535
+ return int(job)
536
+
537
+
538
+ def _windows_environment_block(env: dict[str, str]) -> object:
539
+ values = [f"{key}={value}" for key, value in sorted(env.items(), key=lambda item: item[0].casefold())]
540
+ return ctypes.create_unicode_buffer("\x00".join(values) + "\x00\x00")
541
+
542
+
543
+ _WINDOWS_DEFAULT_PATHEXT = (".COM", ".EXE", ".BAT", ".CMD")
544
+
545
+
546
+ def _windows_resolve_application(command: str, env: dict[str, str], cwd: str) -> str:
547
+ """Resolve a Windows command against the child environment only.
548
+
549
+ ``shutil.which`` is deliberately avoided: on Windows it prepends the
550
+ *parent* process's current directory and reads ``PATHEXT`` from the parent
551
+ environment, either of which can resolve a different program than the
552
+ child's configured ``PATH`` names.
553
+ """
554
+ if ntpath.dirname(command):
555
+ # Never ntpath.abspath here: on Windows it resolves drive-relative
556
+ # paths ("C:tools\\agent.exe") through the PARENT process's per-drive
557
+ # cwd (_getfullpathname). ntpath.join anchors same-drive and
558
+ # driveless paths at the child cwd; a cross-drive drive-relative
559
+ # result stays non-absolute and the spawn path rejects it.
560
+ candidate = command if ntpath.isabs(command) else ntpath.join(cwd, command)
561
+ return ntpath.normpath(candidate)
562
+ # The child env is casefold-deduplicated but keeps the base env's original
563
+ # key casing, so PATH/PATHEXT must be looked up case-insensitively.
564
+ lookup = {key.casefold(): value for key, value in env.items()}
565
+ raw_pathext = lookup.get("pathext", "")
566
+ extensions = (
567
+ tuple(
568
+ extension if extension.startswith(".") else f".{extension}"
569
+ for extension in (part.strip() for part in raw_pathext.split(";"))
570
+ if extension
571
+ )
572
+ or _WINDOWS_DEFAULT_PATHEXT
573
+ )
574
+ has_extension = bool(ntpath.splitext(command)[1])
575
+ for raw_directory in (lookup.get("path") or "").split(";"):
576
+ directory = raw_directory.strip().strip('"')
577
+ if not directory:
578
+ continue
579
+ # Relative PATH entries belong to the child: anchor them at the child
580
+ # cwd, never at the Chrys process's own current directory.
581
+ directory = ntpath.normpath(ntpath.join(cwd, directory))
582
+ if not ntpath.isabs(directory):
583
+ # A cross-drive drive-relative entry ("C:bin") is only resolvable
584
+ # through the parent's per-drive cwd state; skip it entirely so
585
+ # not even the isfile probe consults parent state.
586
+ continue
587
+ base = ntpath.join(directory, command)
588
+ candidates = (base,) if has_extension else tuple(base + extension for extension in extensions)
589
+ for candidate in candidates:
590
+ if os.path.isfile(candidate):
591
+ return ntpath.normpath(candidate)
592
+ # Unresolved bare names are returned as-is so the spawn path fails closed
593
+ # with a deterministic not-found error instead of searching implicit
594
+ # locations such as the current directory.
595
+ return ntpath.normpath(command)
596
+
597
+
598
+ def _make_windows_pipe_pairs() -> tuple[tuple[int, int], tuple[int, int], tuple[int, int]]:
599
+ from asyncio import windows_utils
600
+
601
+ pairs: list[tuple[int, int]] = []
602
+ try:
603
+ while len(pairs) < 3:
604
+ pairs.append(cast(Any, windows_utils).pipe(duplex=True, overlapped=(True, False)))
605
+ except BaseException:
606
+ for pair in pairs:
607
+ for handle in pair:
608
+ _close_windows_handle(handle)
609
+ raise
610
+ return pairs[0], pairs[1], pairs[2]
611
+
612
+
613
+ async def _wrap_windows_parent_pipes(
614
+ stdin_parent: int,
615
+ stdout_parent: int,
616
+ stderr_parent: int,
617
+ *,
618
+ limit: int,
619
+ ) -> tuple[
620
+ asyncio.StreamWriter,
621
+ asyncio.StreamReader,
622
+ asyncio.StreamReader,
623
+ asyncio.BaseTransport,
624
+ asyncio.BaseTransport,
625
+ ]:
626
+ from asyncio import windows_utils
627
+
628
+ loop = asyncio.get_running_loop()
629
+ stdin_pipe = cast(Any, windows_utils).PipeHandle(stdin_parent)
630
+ stdout_pipe = cast(Any, windows_utils).PipeHandle(stdout_parent)
631
+ stderr_pipe = cast(Any, windows_utils).PipeHandle(stderr_parent)
632
+ stdin_transport = None
633
+ stdout_transport = None
634
+ stderr_transport = None
635
+ try:
636
+ stdout = asyncio.StreamReader(limit=limit)
637
+ stdout_protocol = asyncio.StreamReaderProtocol(stdout)
638
+ stdout_transport, _ = await loop.connect_read_pipe(lambda: stdout_protocol, stdout_pipe)
639
+
640
+ stderr = asyncio.StreamReader(limit=limit)
641
+ stderr_protocol = asyncio.StreamReaderProtocol(stderr)
642
+ stderr_transport, _ = await loop.connect_read_pipe(lambda: stderr_protocol, stderr_pipe)
643
+
644
+ stdin_protocol = asyncio.streams.FlowControlMixin(loop=loop)
645
+ stdin_transport, _ = await loop.connect_write_pipe(
646
+ lambda: stdin_protocol,
647
+ stdin_pipe,
648
+ )
649
+ stdin = asyncio.StreamWriter(stdin_transport, stdin_protocol, None, loop)
650
+ return stdin, stdout, stderr, stdout_transport, stderr_transport
651
+ except BaseException:
652
+ for transport in (stdin_transport, stdout_transport, stderr_transport):
653
+ if transport is not None:
654
+ transport.close()
655
+ for pipe, transport in (
656
+ (stdin_pipe, stdin_transport),
657
+ (stdout_pipe, stdout_transport),
658
+ (stderr_pipe, stderr_transport),
659
+ ):
660
+ if transport is None:
661
+ pipe.close()
662
+ raise
663
+
664
+
665
+ async def _spawn_windows_managed_stdio_process(
666
+ command: str,
667
+ args: tuple[str, ...],
668
+ *,
669
+ cwd: str,
670
+ env: dict[str, str],
671
+ limit: int,
672
+ parent_env: dict[str, str] | None,
673
+ ) -> ManagedStdioProcess:
674
+ api = _windows_process_api()
675
+ job_handle = _windows_create_job(api)
676
+ pipe_pairs: tuple[tuple[int, int], tuple[int, int], tuple[int, int]] | None = None
677
+ attribute_buffer = None
678
+ attribute_list = None
679
+ process_handle: int | None = None
680
+ try:
681
+ pipe_pairs = _make_windows_pipe_pairs()
682
+ (stdin_parent, stdin_child), (stdout_parent, stdout_child), (stderr_parent, stderr_child) = pipe_pairs
683
+ HANDLE_FLAG_INHERIT = 0x00000001
684
+ for child_handle in (stdin_child, stdout_child, stderr_child):
685
+ if not api.kernel32.SetHandleInformation(child_handle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT):
686
+ _raise_windows_process_error(api, "Unable to make a child stdio handle inheritable.")
687
+ for parent_handle in (stdin_parent, stdout_parent, stderr_parent):
688
+ if not api.kernel32.SetHandleInformation(parent_handle, HANDLE_FLAG_INHERIT, 0):
689
+ _raise_windows_process_error(api, "Unable to protect a parent stdio handle.")
690
+
691
+ # The child joins the job as it is created and inherits only its three stdio handles.
692
+ # The attribute list points into these arrays, so they stay referenced until
693
+ # DeleteProcThreadAttributeList in the finally below.
694
+ PROC_THREAD_ATTRIBUTE_JOB_LIST = 0x0002000D
695
+ PROC_THREAD_ATTRIBUTE_HANDLE_LIST = 0x00020002
696
+ job_array = (wintypes.HANDLE * 1)(job_handle)
697
+ handle_array = (wintypes.HANDLE * 3)(stdin_child, stdout_child, stderr_child)
698
+ attributes = (
699
+ (PROC_THREAD_ATTRIBUTE_JOB_LIST, job_array, "Unable to attach the Windows Job Object atomically."),
700
+ (PROC_THREAD_ATTRIBUTE_HANDLE_LIST, handle_array, "Unable to restrict inherited Windows handles."),
701
+ )
702
+ size = ctypes.c_size_t()
703
+ api.kernel32.InitializeProcThreadAttributeList(None, len(attributes), 0, ctypes.byref(size))
704
+ attribute_buffer = ctypes.create_string_buffer(size.value)
705
+ candidate = ctypes.cast(attribute_buffer, wintypes.LPVOID)
706
+ if not api.kernel32.InitializeProcThreadAttributeList(candidate, len(attributes), 0, ctypes.byref(size)):
707
+ _raise_windows_process_error(api, "Unable to initialize Windows process attributes.")
708
+ # Named only once initialized: the finally below deletes whatever attribute_list names.
709
+ attribute_list = candidate
710
+ for attribute, value, failure in attributes:
711
+ if not api.kernel32.UpdateProcThreadAttribute(
712
+ attribute_list,
713
+ 0,
714
+ attribute,
715
+ ctypes.cast(value, wintypes.LPVOID),
716
+ ctypes.sizeof(value),
717
+ None,
718
+ None,
719
+ ):
720
+ _raise_windows_process_error(api, failure)
721
+
722
+ application = _windows_resolve_application(command, env, cwd)
723
+ if not ntpath.isabs(application):
724
+ # A relative name would make the batch-shim existence check and
725
+ # CreateProcessW itself resolve against the parent's current
726
+ # directory instead of the child's configured environment.
727
+ raise FileNotFoundError(errno.ENOENT, "Windows command was not found on the configured PATH.")
728
+ if ntpath.splitext(application)[1].casefold() in _WINDOWS_BATCH_SUFFIXES:
729
+ if not os.path.exists(application):
730
+ raise FileNotFoundError(errno.ENOENT, "Windows batch shim does not exist.")
731
+ if not os.path.isfile(application):
732
+ raise IsADirectoryError(errno.EISDIR, "Windows batch shim is not a regular file.")
733
+ shim = ntpath.abspath(application)
734
+ batch_command = serialize_windows_batch_command(shim, args)
735
+ application = resolve_windows_comspec(parent_env)
736
+ command_line_text = f'"{application}" /d /s /v:off /c "{batch_command}"'
737
+ else:
738
+ argv = [application, *args]
739
+ command_line_text = subprocess.list2cmdline(argv)
740
+ command_line = ctypes.create_unicode_buffer(command_line_text)
741
+ environment = _windows_environment_block(env)
742
+
743
+ startup = _StartupInfoExW(
744
+ _StartupInfoW(
745
+ cb=ctypes.sizeof(_StartupInfoExW),
746
+ dwFlags=0x00000101, # STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES
747
+ wShowWindow=0, # SW_HIDE
748
+ hStdInput=stdin_child,
749
+ hStdOutput=stdout_child,
750
+ hStdError=stderr_child,
751
+ ),
752
+ attribute_list,
753
+ )
754
+ process_info = _ProcessInformation()
755
+ creation_flags = _CREATE_NEW_CONSOLE | 0x00000400 | 0x00080000
756
+ if not api.kernel32.CreateProcessW(
757
+ application,
758
+ command_line,
759
+ None,
760
+ None,
761
+ True,
762
+ creation_flags,
763
+ environment,
764
+ cwd,
765
+ ctypes.byref(startup),
766
+ ctypes.byref(process_info),
767
+ ):
768
+ _raise_windows_process_error(api, "Unable to create the Windows subprocess.")
769
+ process_handle = int(process_info.hProcess)
770
+ api.kernel32.CloseHandle(process_info.hThread)
771
+ for child_handle in (stdin_child, stdout_child, stderr_child):
772
+ api.kernel32.CloseHandle(child_handle)
773
+ # _wrap_windows_parent_pipes owns the parent handles from here, including on failure.
774
+ pipe_pairs = None
775
+ stdin, stdout, stderr, stdout_transport, stderr_transport = await _wrap_windows_parent_pipes(
776
+ stdin_parent,
777
+ stdout_parent,
778
+ stderr_parent,
779
+ limit=limit,
780
+ )
781
+ return ManagedStdioProcess(
782
+ stdin=stdin,
783
+ stdout=stdout,
784
+ stderr=stderr,
785
+ pid=int(process_info.dwProcessId),
786
+ _windows_process_handle=process_handle,
787
+ _windows_job_handle=job_handle,
788
+ _windows_stdout_transport=stdout_transport,
789
+ _windows_stderr_transport=stderr_transport,
790
+ )
791
+ except BaseException:
792
+ _terminate_windows_retained_process(job_handle, process_handle, exit_code=1)
793
+ _close_windows_handle(process_handle)
794
+ _close_windows_handle(job_handle)
795
+ if pipe_pairs is not None:
796
+ for pair in pipe_pairs:
797
+ for handle in pair:
798
+ _close_windows_handle(handle)
799
+ raise
800
+ finally:
801
+ if attribute_list is not None:
802
+ api.kernel32.DeleteProcThreadAttributeList(attribute_list)
803
+
804
+
805
+ async def _windows_process_exit(process_handle: int) -> int:
806
+ """Complete when the process handle is signalled, then read its exit code."""
807
+ await _register_windows_process_wait(process_handle)
808
+ return _windows_process_exit_code(process_handle)
809
+
810
+
811
+ def _register_windows_process_wait(process_handle: int) -> asyncio.Future[bool]:
812
+ """Register the process handle with the running loop's completion port.
813
+
814
+ The Windows spawn path connects its pipes through ``ProactorEventLoop``, whose
815
+ ``IocpProactor.wait_for_handle`` is the wait asyncio uses for its own subprocess
816
+ exits: a kernel wait thread posts to the port when the process ends and the loop
817
+ completes the future itself. Cancelling the future unregisters the wait.
818
+ """
819
+ loop = asyncio.get_running_loop()
820
+ proactor = getattr(loop, "_proactor", None)
821
+ if proactor is None:
822
+ raise RuntimeError("Windows subprocess waits need the proactor event loop that connected the pipes.")
823
+ return proactor.wait_for_handle(process_handle)
824
+
825
+
826
+ def _windows_process_exit_code(process_handle: int) -> int:
827
+ api = _windows_process_api()
828
+ exit_code = wintypes.DWORD()
829
+ if not api.kernel32.GetExitCodeProcess(process_handle, ctypes.byref(exit_code)):
830
+ _raise_windows_process_error(api, "Unable to read the Windows subprocess exit code.")
831
+ return int(exit_code.value)
832
+
833
+
834
+ def _terminate_windows_retained_process(
835
+ job_handle: int | None,
836
+ process_handle: int | None,
837
+ *,
838
+ exit_code: int,
839
+ ) -> None:
840
+ if not job_handle and not process_handle:
841
+ return
842
+ api = _windows_process_api()
843
+ terminated = bool(job_handle and api.kernel32.TerminateJobObject(job_handle, exit_code))
844
+ if not terminated and process_handle:
845
+ api.kernel32.TerminateProcess(process_handle, exit_code)
846
+
847
+
848
+ def _consume_task_exception(task: asyncio.Task[Any]) -> None:
849
+ """Mark abandoned waiter failures retrieved without changing await semantics."""
850
+ if not task.cancelled():
851
+ task.exception()
852
+
853
+
854
+ def _close_windows_handle(handle: int | None) -> None:
855
+ if not handle:
856
+ return
857
+ api = _windows_process_api()
858
+ api.kernel32.CloseHandle(handle)
859
+
860
+
861
+ async def _managed_subprocess_gen(*args: Any, **kwargs: Any) -> AsyncIterator[asyncio.subprocess.Process]:
862
+ """Generator backing :func:`managed_subprocess`."""
863
+ if (
864
+ sys.platform != "win32"
865
+ and "start_new_session" not in kwargs
866
+ and "process_group" not in kwargs
867
+ and "preexec_fn" not in kwargs
868
+ ):
869
+ # Own a POSIX process group so cleanup reaches grandchildren that
870
+ # inherit pipes and outlive the wrapper process.
871
+ kwargs["start_new_session"] = True
872
+
873
+ # On Windows, give the child its own console so it cannot call
874
+ # SetConsoleMode() on the *parent's* console (which would reset
875
+ # mouse-input flags and break TUI interactions).
876
+ #
877
+ # CREATE_NEW_CONSOLE — child gets a separate console, so cmd.exe,
878
+ # pwsh, and powershell still work (they need a console for internal
879
+ # commands like echo, dir, etc.) but cannot touch the parent's.
880
+ # STARTF_USESHOWWINDOW + SW_HIDE — suppress the new console window
881
+ # so it doesn't flash on screen.
882
+ #
883
+ # Why not the alternatives?
884
+ # - CREATE_NO_WINDOW: child shares the parent's console → can modify it.
885
+ # - DETACHED_PROCESS: child has NO console → cmd.exe / pwsh / powershell fail.
886
+ if sys.platform == "win32" and "creationflags" not in kwargs:
887
+ for k, v in windows_hidden_subprocess_kwargs().items():
888
+ kwargs.setdefault(k, v)
889
+
890
+ # Detach from the parent's stdin unless a caller asked for something else.
891
+ # Under ACP that descriptor carries the JSON-RPC stream, and a child that
892
+ # inherits it can consume protocol frames. Defaulting here rather than at
893
+ # each call site means a new caller is safe by omission, which is also what
894
+ # lets the static sweep accept the ``**kwargs`` splat below as proof.
895
+ kwargs.setdefault("stdin", subprocess.DEVNULL)
896
+
897
+ try:
898
+ proc = await asyncio.create_subprocess_exec(*args, **kwargs)
899
+ except OSError as exc:
900
+ raise_if_missing_cwd(kwargs.get("cwd"), exc)
901
+ raise
902
+ process_group_id = _infer_process_group_id(proc, kwargs)
903
+ body_raised = False
904
+ try:
905
+ yield proc
906
+ except BaseException:
907
+ body_raised = True
908
+ raise
909
+ finally:
910
+ should_kill_owned_tree = body_raised or proc.returncode is None
911
+ if process_group_id is not None and should_kill_owned_tree:
912
+ kill_process_group(process_group_id)
913
+ try:
914
+ if proc.pid is not None and should_kill_owned_tree:
915
+ # Returns at once off Windows, where the group kill above reached the tree.
916
+ await kill_windows_process_tree(proc.pid)
917
+ finally:
918
+ if proc.returncode is None:
919
+ # Keep the direct child moving if process-group cleanup was
920
+ # unavailable or a custom setup kept the child outside the group.
921
+ with contextlib.suppress(ProcessLookupError, OSError):
922
+ proc.kill()
923
+ # Close the subprocess transport BEFORE wait() so pipe
924
+ # transports are released immediately. Without this,
925
+ # wait() blocks until all inherited pipe handles close —
926
+ # which hangs when a wrapper process (e.g. `uv run python`)
927
+ # spawns a child that inherits the pipes and outlives the
928
+ # killed parent.
929
+ finalize_subprocess_transport(proc)
930
+ with contextlib.suppress(ProcessLookupError, OSError, asyncio.CancelledError, TimeoutError):
931
+ await asyncio.wait_for(proc.wait(), timeout=_PROCESS_WAIT_CLEANUP_TIMEOUT)
932
+ else:
933
+ # Process already exited — just release the transport.
934
+ finalize_subprocess_transport(proc)
935
+
936
+
937
+ managed_subprocess = contextlib.asynccontextmanager(_managed_subprocess_gen)
938
+ """Create a subprocess with guaranteed transport cleanup.
939
+
940
+ Wraps ``asyncio.create_subprocess_exec`` and ensures that on exit:
941
+
942
+ 1. The POSIX process group is killed on abnormal exit or unfinished child
943
+ 2. The direct child is killed if it hasn't exited yet
944
+ 3. The direct child is waited/reaped (prevents zombie processes)
945
+ 4. The subprocess transport is closed (prevents Windows Proactor
946
+ ``ResourceWarning`` during interpreter shutdown)
947
+
948
+ Usage::
949
+
950
+ async with managed_subprocess("rg", "--files", stdout=PIPE) as proc:
951
+ stdout, stderr = await proc.communicate()
952
+ """
953
+
954
+
955
+ async def wait_for_subprocess[T](
956
+ awaitable: Awaitable[T],
957
+ *,
958
+ timeout: float | None,
959
+ process_group_id: int | None = None,
960
+ process_session_id: int | None = None,
961
+ ) -> T:
962
+ """Await subprocess work with timeout and stopped-process detection.
963
+
964
+ ``asyncio`` only completes ``Process.wait()`` when the direct child exits;
965
+ a POSIX child or grandchild can enter job-control stopped state and keep
966
+ pipes or a PTY slave open forever. When a process group or session id is
967
+ supplied, this helper polls for stopped members and raises
968
+ :class:`SubprocessStoppedError` as soon as one is found so callers can kill
969
+ the owned process set.
970
+ """
971
+ if process_group_id is None and process_session_id is None:
972
+ return await asyncio.wait_for(awaitable, timeout=timeout)
973
+ if sys.platform == "win32":
974
+ return await asyncio.wait_for(awaitable, timeout=timeout)
975
+
976
+ operation = asyncio.ensure_future(awaitable)
977
+ if process_session_id is None:
978
+ monitor = asyncio.create_task(_monitor_process_group_stopped(cast(int, process_group_id)))
979
+ else:
980
+ monitor = asyncio.create_task(_monitor_processes_stopped(process_group_id, process_session_id))
981
+ try:
982
+ done, _pending = await asyncio.wait(
983
+ {operation, monitor},
984
+ timeout=timeout,
985
+ return_when=asyncio.FIRST_COMPLETED,
986
+ )
987
+ if not done:
988
+ await _cancel_subprocess_wait(operation)
989
+ raise TimeoutError
990
+ if operation in done:
991
+ return await operation
992
+
993
+ await _cancel_subprocess_wait(operation)
994
+ return await monitor
995
+ except BaseException:
996
+ if not operation.done():
997
+ await _cancel_subprocess_wait(operation)
998
+ raise
999
+ finally:
1000
+ monitor.cancel()
1001
+ with contextlib.suppress(asyncio.CancelledError, SubprocessStoppedError):
1002
+ await monitor
1003
+
1004
+
1005
+ async def _cancel_subprocess_wait(task: asyncio.Future[Any]) -> None:
1006
+ task.cancel()
1007
+ with contextlib.suppress(asyncio.CancelledError, TimeoutError):
1008
+ await asyncio.wait_for(task, timeout=_PROCESS_WAIT_CLEANUP_TIMEOUT)
1009
+
1010
+
1011
+ def kill_process_group(process_group_id: int) -> bool:
1012
+ """Kill a POSIX process group without signaling Chrys's own group."""
1013
+ if sys.platform == "win32" or process_group_id <= 0:
1014
+ return False
1015
+ with contextlib.suppress(OSError):
1016
+ if process_group_id == os.getpgrp():
1017
+ return False
1018
+ try:
1019
+ os.killpg(process_group_id, signal.SIGKILL)
1020
+ except ProcessLookupError:
1021
+ return False
1022
+ except PermissionError:
1023
+ return False
1024
+ except OSError:
1025
+ return False
1026
+ return True
1027
+
1028
+
1029
+ def kill_process_session(process_session_id: int) -> bool:
1030
+ """Kill all known POSIX process groups in a session owned by Chrys."""
1031
+ if sys.platform == "win32" or process_session_id <= 0:
1032
+ return False
1033
+ with contextlib.suppress(OSError):
1034
+ if process_session_id == os.getsid(0):
1035
+ return False
1036
+
1037
+ killed = False
1038
+ for process_group_id in sorted(_process_groups_in_session(process_session_id)):
1039
+ killed = kill_process_group(process_group_id) or killed
1040
+ return killed
1041
+
1042
+
1043
+ async def kill_windows_process_tree(pid: int) -> bool:
1044
+ """Best-effort Windows subtree kill using the platform ``taskkill`` utility.
1045
+
1046
+ ``taskkill`` runs as an asyncio subprocess, whose exit the loop awaits on its
1047
+ completion port (``IocpProactor.wait_for_handle``, as for every asyncio
1048
+ subprocess), so the loop keeps serving other work meanwhile. A blocking run
1049
+ stalled the whole TUI on every hook timeout, and concurrent timeouts queued
1050
+ behind one another.
1051
+
1052
+ A cancelled caller waits for the bounded kill to end before its cancellation
1053
+ goes on. Stopping ``taskkill`` part-way would leave the caller to kill only the
1054
+ direct child, and the descendants ``taskkill`` had yet to reach would outlive it.
1055
+ """
1056
+ argv = _windows_tree_kill_argv(pid)
1057
+ if argv is None:
1058
+ return False
1059
+ kill = asyncio.ensure_future(_run_windows_tree_kill(argv))
1060
+ try:
1061
+ return await asyncio.shield(kill)
1062
+ except asyncio.CancelledError:
1063
+ while not kill.done():
1064
+ with contextlib.suppress(asyncio.CancelledError, Exception):
1065
+ await asyncio.shield(kill)
1066
+ raise
1067
+
1068
+
1069
+ async def _run_windows_tree_kill(argv: list[str]) -> bool:
1070
+ try:
1071
+ killer = await asyncio.create_subprocess_exec(
1072
+ *argv,
1073
+ stdin=asyncio.subprocess.DEVNULL,
1074
+ stdout=asyncio.subprocess.DEVNULL,
1075
+ stderr=asyncio.subprocess.DEVNULL,
1076
+ **windows_hidden_subprocess_kwargs(),
1077
+ )
1078
+ except OSError:
1079
+ return False
1080
+ try:
1081
+ return await asyncio.wait_for(killer.wait(), timeout=_WINDOWS_TREE_KILL_TIMEOUT) == 0
1082
+ except TimeoutError:
1083
+ return False
1084
+ finally:
1085
+ if killer.returncode is None:
1086
+ with contextlib.suppress(ProcessLookupError, OSError):
1087
+ killer.kill()
1088
+ finalize_subprocess_transport(killer)
1089
+
1090
+
1091
+ def _windows_tree_kill_argv(pid: int) -> list[str] | None:
1092
+ if sys.platform != "win32" or pid <= 0:
1093
+ return None
1094
+ taskkill = shutil.which("taskkill")
1095
+ if taskkill is None:
1096
+ return None
1097
+ return [taskkill, "/PID", str(pid), "/T", "/F"]
1098
+
1099
+
1100
+ def _infer_process_group_id(proc: asyncio.subprocess.Process, kwargs: dict[str, Any]) -> int | None:
1101
+ if sys.platform == "win32" or proc.pid is None:
1102
+ return None
1103
+ if kwargs.get("start_new_session") is True:
1104
+ return proc.pid
1105
+ process_group = kwargs.get("process_group")
1106
+ if process_group == 0:
1107
+ return proc.pid
1108
+ if isinstance(process_group, int) and process_group > 0:
1109
+ return process_group
1110
+ return None
1111
+
1112
+
1113
+ async def _monitor_process_group_stopped(process_group_id: int) -> Never:
1114
+ while True:
1115
+ await asyncio.sleep(_STOPPED_PROCESS_POLL_INTERVAL)
1116
+ if await asyncio.to_thread(_process_group_has_stopped_member, process_group_id):
1117
+ msg = f"Subprocess process group {process_group_id} entered stopped state."
1118
+ raise SubprocessStoppedError(msg)
1119
+
1120
+
1121
+ async def _monitor_processes_stopped(process_group_id: int | None, process_session_id: int) -> Never:
1122
+ while True:
1123
+ await asyncio.sleep(_STOPPED_PROCESS_POLL_INTERVAL)
1124
+ if await asyncio.to_thread(_processes_have_stopped_member, process_group_id, process_session_id):
1125
+ msg = f"Subprocess process session {process_session_id} entered stopped state."
1126
+ raise SubprocessStoppedError(msg)
1127
+
1128
+
1129
+ def _processes_have_stopped_member(process_group_id: int | None, process_session_id: int) -> bool:
1130
+ if _process_session_has_stopped_member(process_session_id):
1131
+ return True
1132
+ return process_group_id is not None and _process_group_has_stopped_member(process_group_id)
1133
+
1134
+
1135
+ def _process_group_has_stopped_member(process_group_id: int) -> bool:
1136
+ if sys.platform == "win32" or process_group_id <= 0:
1137
+ return False
1138
+ ps = shutil.which("ps")
1139
+ if ps is None:
1140
+ return False
1141
+ try:
1142
+ result = subprocess.run( # noqa: S603
1143
+ [ps, "-e", "-o", "state=", "-o", "pgid="],
1144
+ stdin=subprocess.DEVNULL,
1145
+ capture_output=True,
1146
+ check=False,
1147
+ timeout=1,
1148
+ )
1149
+ except OSError, subprocess.SubprocessError:
1150
+ return False
1151
+ return _process_listing_has_stopped_group_member(result.stdout, process_group_id)
1152
+
1153
+
1154
+ def _process_listing_has_stopped_group_member(raw: bytes, process_group_id: int) -> bool:
1155
+ for line in raw.decode("ascii", errors="ignore").splitlines():
1156
+ stripped = line.strip()
1157
+ if not stripped:
1158
+ continue
1159
+ parts = stripped.rsplit(None, 1)
1160
+ if len(parts) != 2:
1161
+ continue
1162
+ state, pgid_text = parts
1163
+ try:
1164
+ pgid = int(pgid_text)
1165
+ except ValueError:
1166
+ continue
1167
+ if pgid == process_group_id and state[:1] in {"T", "t"}:
1168
+ return True
1169
+ return False
1170
+
1171
+
1172
+ def _process_session_has_stopped_member(process_session_id: int) -> bool:
1173
+ if sys.platform == "win32" or process_session_id <= 0:
1174
+ return False
1175
+ return _process_statuses_have_stopped_session_member(_iter_posix_process_statuses(), process_session_id)
1176
+
1177
+
1178
+ def _process_groups_in_session(process_session_id: int) -> set[int]:
1179
+ if sys.platform == "win32" or process_session_id <= 0:
1180
+ return set()
1181
+ return _process_statuses_session_groups(_iter_posix_process_statuses(), process_session_id)
1182
+
1183
+
1184
+ def _process_statuses_have_stopped_session_member(statuses: Iterable[_ProcessStatus], process_session_id: int) -> bool:
1185
+ return any(status.session_id == process_session_id and status.state[:1] in {"T", "t"} for status in statuses)
1186
+
1187
+
1188
+ def _process_statuses_session_groups(statuses: Iterable[_ProcessStatus], process_session_id: int) -> set[int]:
1189
+ return {status.pgid for status in statuses if status.session_id == process_session_id and status.pgid > 0}
1190
+
1191
+
1192
+ def _iter_posix_process_statuses() -> Iterator[_ProcessStatus]:
1193
+ if sys.platform == "win32":
1194
+ return
1195
+ if sys.platform == "linux":
1196
+ yielded = False
1197
+ for status in _iter_linux_process_statuses():
1198
+ yielded = True
1199
+ yield status
1200
+ if yielded:
1201
+ return
1202
+ yield from _iter_ps_process_statuses()
1203
+
1204
+
1205
+ def _iter_linux_process_statuses() -> Iterator[_ProcessStatus]:
1206
+ if sys.platform != "linux":
1207
+ return
1208
+ with contextlib.suppress(OSError), os.scandir("/proc") as entries:
1209
+ for entry in entries:
1210
+ if not entry.name.isdecimal():
1211
+ continue
1212
+ with contextlib.suppress(OSError, UnicodeDecodeError):
1213
+ with open(f"/proc/{entry.name}/stat", encoding="utf-8") as stat_file:
1214
+ status = _parse_linux_proc_stat(stat_file.read())
1215
+ if status is not None:
1216
+ yield status
1217
+
1218
+
1219
+ def _parse_linux_proc_stat(raw: str) -> _ProcessStatus | None:
1220
+ close_paren = raw.rfind(")")
1221
+ if close_paren < 0:
1222
+ return None
1223
+ pid_text = raw[:close_paren].partition("(")[0]
1224
+ values = raw[close_paren + 1 :].split()
1225
+ if len(values) < 4:
1226
+ return None
1227
+ try:
1228
+ return _ProcessStatus(
1229
+ pid=int(pid_text.strip()),
1230
+ state=values[0],
1231
+ pgid=int(values[2]),
1232
+ session_id=int(values[3]),
1233
+ )
1234
+ except ValueError:
1235
+ return None
1236
+
1237
+
1238
+ def _iter_ps_process_statuses() -> Iterator[_ProcessStatus]:
1239
+ ps = shutil.which("ps")
1240
+ if ps is None:
1241
+ return
1242
+ try:
1243
+ result = subprocess.run( # noqa: S603
1244
+ [ps, "-e", "-o", "pid=", "-o", "state="],
1245
+ stdin=subprocess.DEVNULL,
1246
+ capture_output=True,
1247
+ check=False,
1248
+ timeout=1,
1249
+ )
1250
+ except OSError, subprocess.SubprocessError:
1251
+ return
1252
+ if result.returncode != 0:
1253
+ return
1254
+ for pid, state in _parse_ps_pid_state_listing(result.stdout):
1255
+ with contextlib.suppress(ProcessLookupError, PermissionError, OSError):
1256
+ posix_os = cast(Any, os)
1257
+ pgid = posix_os.getpgid(pid)
1258
+ session_id = posix_os.getsid(pid)
1259
+ yield _ProcessStatus(pid=pid, state=state, pgid=pgid, session_id=session_id)
1260
+
1261
+
1262
+ def _parse_ps_pid_state_listing(raw: bytes) -> Iterator[tuple[int, str]]:
1263
+ for line in raw.decode("ascii", errors="ignore").splitlines():
1264
+ stripped = line.strip()
1265
+ if not stripped:
1266
+ continue
1267
+ parts = stripped.split(None, 1)
1268
+ if len(parts) != 2:
1269
+ continue
1270
+ try:
1271
+ yield int(parts[0]), parts[1]
1272
+ except ValueError:
1273
+ continue
1274
+
1275
+
1276
+ def decode_subprocess_output(raw: bytes | bytearray) -> str:
1277
+ """Decode subprocess output bytes to str, strongly preferring UTF-8.
1278
+
1279
+ The generic ``decode_bytes`` runs a multi-encoding detection pipeline
1280
+ that can misidentify ANSI-heavy terminal output as UTF-16: ESC bytes
1281
+ (0x1b) inflate the control-character ratio past the text-quality
1282
+ threshold, causing the UTF-8 fast-path to be rejected, while ASCII
1283
+ byte pairs happen to decode as valid CJK under UTF-16.
1284
+
1285
+ Strategy (aligned with Ansible / CPython / xterm.js best practice):
1286
+
1287
+ 1. **UTF-16 probe on Windows** -- catches PowerShell / ``cmd /u``
1288
+ output that otherwise looks like valid UTF-8 with embedded NULs.
1289
+ 2. **Strict UTF-8** -- fast path for the overwhelming common case.
1290
+ 3. **Tolerant UTF-8** -- handles PTY reads that split multi-byte
1291
+ characters at buffer boundaries. Uses ``surrogateescape`` to
1292
+ count bad bytes accurately (each maps 1:1 to a surrogate), then
1293
+ re-encodes and decodes with ``replace`` so the returned string
1294
+ is safe for JSON serialization and terminal rendering.
1295
+ 4. **Full detection (Windows only)** -- falls back to ``decode_bytes``
1296
+ for legacy ANSI code page output (GBK, Shift-JIS, etc.). On
1297
+ Unix/macOS the locale is always UTF-8, so detection adds risk
1298
+ of misidentification with no benefit.
1299
+ """
1300
+ if not raw:
1301
+ return ""
1302
+ if sys.platform == "win32":
1303
+ utf16_text = _decode_utf16_output(raw)
1304
+ if utf16_text is not None:
1305
+ return utf16_text
1306
+ try:
1307
+ return raw.decode("utf-8")
1308
+ except UnicodeDecodeError:
1309
+ # On Windows with a legacy code page (GBK, Shift-JIS, cp1252, …),
1310
+ # non-UTF-8 bytes are likely valid in the system encoding. Try
1311
+ # the known console code page first (fast, accurate) before
1312
+ # falling back to statistical detection (charset_normalizer).
1313
+ # Without this, mostly-ASCII output with a few CJK characters
1314
+ # falls below the surrogate threshold and gets garbled by
1315
+ # ``errors="replace"`` before the code-page fallback is reached.
1316
+ if sys.platform == "win32" and not _windows_uses_utf8():
1317
+ cp_enc = _windows_console_encoding()
1318
+ if cp_enc:
1319
+ try:
1320
+ decoded = raw.decode(cp_enc)
1321
+ if _is_subprocess_text(decoded):
1322
+ return decoded
1323
+ except UnicodeDecodeError, LookupError:
1324
+ pass
1325
+ return decode_bytes(raw)
1326
+ # Unix/macOS: locale is always UTF-8, so non-UTF-8 bytes are
1327
+ # buffer-boundary splits, not a different encoding. Use
1328
+ # surrogateescape to count bad bytes; replace if few enough.
1329
+ probed = raw.decode("utf-8", errors="surrogateescape")
1330
+ surrogates = sum(1 for ch in probed if "\udc80" <= ch <= "\udcff")
1331
+ if surrogates / max(len(probed), 1) < 0.1:
1332
+ return raw.decode("utf-8", errors="replace")
1333
+ return raw.decode("utf-8", errors="replace")
1334
+
1335
+
1336
+ def _is_subprocess_text(decoded: str) -> bool:
1337
+ """Return True for regular text or NUL-delimited text records."""
1338
+ if is_mostly_text(decoded):
1339
+ return True
1340
+ if "\x00" not in decoded:
1341
+ return False
1342
+
1343
+ parts = decoded.split("\x00")
1344
+ if parts and parts[-1] == "":
1345
+ parts.pop()
1346
+ if not parts or any(part == "" for part in parts):
1347
+ return False
1348
+
1349
+ return is_mostly_text("".join(parts))
1350
+
1351
+
1352
+ def _decode_utf16_output(raw: bytes | bytearray) -> str | None:
1353
+ """Decode subprocess output that is clearly UTF-16 text."""
1354
+ data = bytes(raw)
1355
+ layout = _utf16_layout(data)
1356
+ if layout is None:
1357
+ return None
1358
+ codec, bom_length = layout
1359
+ try:
1360
+ decoded = data[bom_length:].decode(codec)
1361
+ except UnicodeDecodeError:
1362
+ return None
1363
+ # ASCII-only UTF-16 without a BOM is indistinguishable from NUL-delimited
1364
+ # UTF-8/ASCII output. Preserve the bytes in that ambiguous case.
1365
+ if not bom_length and decoded.isascii():
1366
+ return None
1367
+ return decoded if is_mostly_text(decoded) else None
1368
+
1369
+
1370
+ def _utf16_layout(data: bytes) -> tuple[str, int] | None:
1371
+ """Return the UTF-16 codec *data*'s BOM or NUL layout shows, and the BOM's length."""
1372
+ if data.startswith(codecs.BOM_UTF16_LE):
1373
+ return "utf-16-le", len(codecs.BOM_UTF16_LE)
1374
+ if data.startswith(codecs.BOM_UTF16_BE):
1375
+ return "utf-16-be", len(codecs.BOM_UTF16_BE)
1376
+ if len(data) < 4 or b"\x00" not in data:
1377
+ return None
1378
+
1379
+ even = data[0::2]
1380
+ odd = data[1::2]
1381
+ even_nul_ratio = even.count(0) / len(even)
1382
+ odd_nul_ratio = odd.count(0) / len(odd)
1383
+ if odd_nul_ratio >= 0.20 and odd_nul_ratio >= max(even_nul_ratio * 4, 0.20):
1384
+ return "utf-16-le", 0
1385
+ if even_nul_ratio >= 0.20 and even_nul_ratio >= max(odd_nul_ratio * 4, 0.20):
1386
+ return "utf-16-be", 0
1387
+ return None
1388
+
1389
+
1390
+ def decode_split_output(head: bytes, tail: bytes, tail_offset: int) -> tuple[str, str]:
1391
+ """Decode the kept head and tail of an output stream whose middle was dropped.
1392
+
1393
+ *tail_offset* is where *tail* started in the stream. One codec, chosen
1394
+ from both parts so the cut cannot flip the choice, decodes both, and only
1395
+ the edges at the cut are repaired: a character the cut left incomplete at
1396
+ the end of *head* is dropped, and *tail* starts at its first whole
1397
+ character. When no one codec fits both parts, each is decoded on its own
1398
+ with :func:`decode_subprocess_output`.
1399
+ """
1400
+ if sys.platform == "win32":
1401
+ utf16 = _decode_utf16_split(head, tail, tail_offset)
1402
+ if utf16 is not None:
1403
+ return utf16
1404
+ skipped = _utf8_continuation_prefix_length(tail)
1405
+ utf8_tail = tail[skipped:]
1406
+ if sys.platform != "win32":
1407
+ return _decode_split_with("utf-8", head, utf8_tail)
1408
+
1409
+ code_page = None if _windows_uses_utf8() else _windows_console_encoding()
1410
+ utf8 = _strict_split("utf-8", head, utf8_tail)
1411
+ if utf8 is not None:
1412
+ head_text, tail_text = utf8
1413
+ repaired = skipped > 0 or len(head_text.encode("utf-8")) < len(head)
1414
+ # Bytes dropped as the pieces of a character the cut split are no
1415
+ # evidence for UTF-8 when every character kept is ASCII: the console
1416
+ # code page may read those bytes as text.
1417
+ if not (code_page and repaired and head_text.isascii() and tail_text.isascii()):
1418
+ return utf8
1419
+ if code_page:
1420
+ with contextlib.suppress(LookupError):
1421
+ # A cut inside a double-byte character leaves its trail byte first.
1422
+ parts = next(
1423
+ (parts for k in range(4) if (parts := _strict_split(code_page, head, tail[k:])) is not None), None
1424
+ )
1425
+ if parts is not None and all(_is_subprocess_text(part) for part in parts):
1426
+ return parts
1427
+ if utf8 is not None:
1428
+ return utf8
1429
+ return decode_subprocess_output(head), decode_subprocess_output(tail)
1430
+
1431
+
1432
+ def _utf8_continuation_prefix_length(data: bytes) -> int:
1433
+ """Count the UTF-8 continuation bytes, at most three, that start *data*."""
1434
+ length = 0
1435
+ while length < min(3, len(data)) and data[length] & 0xC0 == 0x80:
1436
+ length += 1
1437
+ return length
1438
+
1439
+
1440
+ def _strict_split(codec: str, head: bytes, tail: bytes) -> tuple[str, str] | None:
1441
+ """Decode both parts with *codec*, dropping a cut character at the end of *head*; None when either fails."""
1442
+ try:
1443
+ head_text = codecs.getincrementaldecoder(codec)("strict").decode(head, final=False)
1444
+ return head_text, codecs.getincrementaldecoder(codec)("strict").decode(tail, final=True)
1445
+ except UnicodeDecodeError:
1446
+ return None
1447
+
1448
+
1449
+ def _decode_split_with(codec: str, head: bytes, tail: bytes) -> tuple[str, str]:
1450
+ head_text = codecs.getincrementaldecoder(codec)("replace").decode(head, final=False)
1451
+ return head_text, codecs.getincrementaldecoder(codec)("replace").decode(tail, final=True)
1452
+
1453
+
1454
+ def _decode_utf16_split(head: bytes, tail: bytes, tail_offset: int) -> tuple[str, str] | None:
1455
+ """Decode both parts as UTF-16 when *head* clearly is UTF-16 text."""
1456
+ layout = _utf16_layout(head[: len(head) - len(head) % 2])
1457
+ if layout is None:
1458
+ return None
1459
+ codec, bom_length = layout
1460
+ tail = tail[tail_offset % 2 :]
1461
+ # The cut may split a surrogate pair; its low half cannot start a character.
1462
+ high_byte = 1 if codec == "utf-16-le" else 0
1463
+ if len(tail) >= 2 and 0xDC <= tail[high_byte] <= 0xDF:
1464
+ tail = tail[2:]
1465
+ head_text, tail_text = _decode_split_with(codec, head[bom_length:], tail)
1466
+ if not bom_length and head_text.isascii() and tail_text.isascii():
1467
+ return None
1468
+ return (head_text, tail_text) if is_mostly_text(head_text + tail_text) else None