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,1265 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """Code search tools — grep and glob, powered by ripgrep.
4
+
5
+ Uses the ``rg`` binary for fast, correct file searching with full glob and
6
+ regex support. The binary is bundled as package data in
7
+ ``chrys/foundation/vendor/ripgrep/`` (downloaded at build time via
8
+ ``scripts/fetch_rg.sh``). A system-installed ``rg`` on PATH is also accepted
9
+ as a fallback.
10
+
11
+ When the search pattern contains non-ASCII characters, multiple encoding
12
+ passes (UTF-8, GBK, and the system ANSI codepage) are run automatically
13
+ to catch files in legacy encodings.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ import base64
20
+ import hashlib
21
+ import json
22
+ import locale
23
+ import os
24
+ import re
25
+ import shutil
26
+ import subprocess
27
+ from collections.abc import Callable, Iterator
28
+ from dataclasses import dataclass, field
29
+ from pathlib import Path, PurePath
30
+ from typing import TYPE_CHECKING, Annotated, Any, cast
31
+
32
+ from pathspec import GitIgnoreSpec
33
+
34
+ from chrys.foundation.platform import get_platform
35
+ from chrys.foundation.platform.files import surrogate_safe_text
36
+ from chrys.foundation.platform.output_capture import BoundedCapture, drain_process_pipes
37
+ from chrys.foundation.platform.paths import resolve_workspace_path
38
+ from chrys.foundation.platform.process import (
39
+ MissingWorkingDirectoryError,
40
+ decode_subprocess_output,
41
+ managed_subprocess,
42
+ )
43
+ from chrys.foundation.vendor import find_rg
44
+ from chrys.service.tools.kinds import KIND_SEARCH, tool
45
+ from chrys.service.tools.result_metadata import record_process_result, record_process_timeout, tool_error
46
+ from chrys.service.tools.workspace_paths import missing_base_cwd_error
47
+
48
+ if TYPE_CHECKING:
49
+ from chrys.foundation.models.session_env import SessionEnvironment
50
+
51
+ _MAX_RESULTS = 50
52
+ _MAX_RESULTS_HARD_LIMIT = 100
53
+ _TIMEOUT = 30
54
+ _MAX_LINE_DISPLAY_CHARS = 2048
55
+ _MAX_ERROR_DIAGNOSTIC_CHARS = 512
56
+ _WINDOWS_COMMAND_LIMIT = 32_767 # UTF-16 code units, including the terminating NUL.
57
+ _WINDOWS_COMMAND_HEADROOM = 512 # Allow PATH shims to expand the executable path when forwarding argv.
58
+ _POSIX_COMMAND_LIMIT = 128 * 1024 # Leave room for the inherited environment and OS overhead.
59
+ _MAX_LISTING_BYTES = 64 * 1024 * 1024
60
+ """File names one ``rg --files`` listing may print before the search is refused as too broad."""
61
+ _MAX_RECORD_BYTES = 1024 * 1024
62
+ """The longest ``rg --json`` record parsed whole.
63
+
64
+ Each record carries its whole source line, and a match record lists every match
65
+ on it, so one minified line can print megabytes. A longer record keeps only
66
+ ``_RECORD_HEAD_BYTES``, enough to name its file.
67
+ """
68
+ _RECORD_HEAD_BYTES = 64 * 1024
69
+ _MAX_STDERR_BYTES = 64 * 1024
70
+ _MAX_OVERSIZED_LINES_SHOWN = 10
71
+
72
+
73
+ @dataclass(slots=True)
74
+ class GrepEntry:
75
+ """A single line from ripgrep output (match or context)."""
76
+
77
+ rel_path: str
78
+ line_num: int
79
+ marker: str # ">" for match, " " for context
80
+ text: str
81
+
82
+
83
+ @dataclass(slots=True)
84
+ class LongLine:
85
+ """A line that was truncated for exceeding ``_MAX_LINE_DISPLAY_CHARS``."""
86
+
87
+ rel_path: str
88
+ line_num: int
89
+ actual_length: int
90
+
91
+
92
+ @dataclass(slots=True)
93
+ class GrepParseResult:
94
+ """Parsed output from a single ``rg --json`` invocation."""
95
+
96
+ entries: list[GrepEntry] = field(default_factory=list)
97
+ match_count: int = 0
98
+ """Matches counted toward the limit, those too long to show included."""
99
+ long_lines: list[LongLine] = field(default_factory=list)
100
+ oversized: set[tuple[str, int]] = field(default_factory=set)
101
+ """Matching lines, by file and line number, whose record was longer than ``_MAX_RECORD_BYTES``."""
102
+
103
+
104
+ @dataclass(slots=True)
105
+ class _SearchErrors:
106
+ """Bound diagnostics before they enter tool results or persisted metadata."""
107
+
108
+ first_diagnostic: str = ""
109
+ diagnostic_count: int = 0
110
+ exit_code: int | None = None
111
+ _seen_headers: set[bytes] = field(default_factory=set)
112
+
113
+ def add(self, stderr: str, exit_code: int) -> None:
114
+ if self.exit_code is None:
115
+ self.exit_code = exit_code
116
+ diagnostic = stderr.strip() or f"rg exited with code {exit_code}"
117
+ capture_first = False
118
+ for index, line in enumerate(diagnostic.splitlines()):
119
+ # Continuation lines contain regex causes, source locations and hints.
120
+ # Unprefixed stderr is one diagnostic per invocation.
121
+ if index == 0 or line.startswith("rg:"):
122
+ capture_first = self.diagnostic_count == 0
123
+ digest = hashlib.blake2b(line.encode("utf-8", errors="surrogatepass"), digest_size=16).digest()
124
+ if digest not in self._seen_headers:
125
+ self._seen_headers.add(digest)
126
+ self.diagnostic_count += 1
127
+ if capture_first:
128
+ separator = "\n" if index else ""
129
+ # One extra character records truncation without retaining the tail.
130
+ limit = _MAX_ERROR_DIAGNOSTIC_CHARS + 1
131
+ self.first_diagnostic = (self.first_diagnostic + separator + line[:limit])[:limit]
132
+
133
+ def summary(self) -> str:
134
+ message = self.first_diagnostic.rstrip()
135
+ if len(self.first_diagnostic) > _MAX_ERROR_DIAGNOSTIC_CHARS:
136
+ marker = "... [truncated]"
137
+ message = self.first_diagnostic[: _MAX_ERROR_DIAGNOSTIC_CHARS - len(marker)] + marker
138
+ if self.diagnostic_count > 1:
139
+ return f"{message} [{self.diagnostic_count - 1} additional diagnostics omitted]"
140
+ return message
141
+
142
+
143
+ _PLATFORM = get_platform()
144
+
145
+ # Map Python codec names → WHATWG encoding labels accepted by ``rg -E``.
146
+ # Only entries where the names differ need to be listed.
147
+ _CODEC_TO_WHATWG: dict[str, str] = {
148
+ "cp874": "windows-874",
149
+ "cp932": "shift_jis",
150
+ "cp949": "euc-kr",
151
+ "cp950": "big5",
152
+ "iso8859-1": "windows-1252",
153
+ "latin-1": "windows-1252",
154
+ "euc_kr": "euc-kr",
155
+ "euc_jp": "euc-jp",
156
+ "shift_jis": "shift_jis",
157
+ "gb2312": "gbk",
158
+ "gb18030": "gb18030",
159
+ }
160
+
161
+
162
+ def _find_rg() -> str:
163
+ """Locate the ``rg`` binary: bundled vendor copy → PATH → error."""
164
+ path = find_rg()
165
+ if path:
166
+ return path
167
+ raise FileNotFoundError(
168
+ "ripgrep (rg) not found — run scripts/fetch_rg.sh to bundle it, or install rg on your system PATH"
169
+ )
170
+
171
+
172
+ def _to_posix(path: str) -> str:
173
+ """Normalize a path to forward slashes for consistent output across platforms.
174
+
175
+ ``PurePath.as_posix()`` is platform-aware: on Windows it converts
176
+ ``\\`` separators; on POSIX it preserves any literal backslash a
177
+ filename may legitimately contain.
178
+ """
179
+ return PurePath(path).as_posix()
180
+
181
+
182
+ def _codec_to_whatwg(codec_name: str) -> str:
183
+ """Convert a Python codec name to a WHATWG encoding label for ``rg -E``."""
184
+ normalized = codec_name.lower().replace("-", "_").replace(" ", "_")
185
+ if normalized in _CODEC_TO_WHATWG:
186
+ return _CODEC_TO_WHATWG[normalized]
187
+ # Many names work directly (utf-8, gbk, big5, etc.)
188
+ return codec_name.lower().replace("_", "-")
189
+
190
+
191
+ def _get_system_ansi_codepage() -> str | None:
192
+ """Detect the system ANSI codepage and return its WHATWG label.
193
+
194
+ Returns ``None`` if detection fails or the codepage is already UTF-8.
195
+ """
196
+ try:
197
+ if _PLATFORM.is_windows:
198
+ import ctypes
199
+
200
+ ctypes_module = cast(Any, ctypes)
201
+ acp = ctypes_module.windll.kernel32.GetACP()
202
+ codec_name = f"cp{acp}"
203
+ else:
204
+ codec_name = locale.getpreferredencoding(False)
205
+
206
+ import codecs
207
+
208
+ info = codecs.lookup(codec_name)
209
+ normalized = info.name.lower().replace("-", "_")
210
+
211
+ # Skip if it's already UTF-8 or GBK (always included)
212
+ if normalized in ("utf_8", "gbk", "cp936", "gb2312"):
213
+ return None
214
+
215
+ return _codec_to_whatwg(info.name)
216
+ except Exception:
217
+ return None
218
+
219
+
220
+ def _get_encodings() -> list[str]:
221
+ """Return the list of WHATWG encoding labels to search with.
222
+
223
+ Always includes UTF-8 and GBK. Adds the system ANSI codepage if it
224
+ differs from both.
225
+ """
226
+ encodings = ["utf-8", "gbk"]
227
+ ansi = _get_system_ansi_codepage()
228
+ if ansi and ansi not in encodings:
229
+ encodings.append(ansi)
230
+ return encodings
231
+
232
+
233
+ def _is_ascii_only(text: str) -> bool:
234
+ """Return ``True`` if *text* contains only ASCII characters."""
235
+ try:
236
+ text.encode("ascii")
237
+ return True
238
+ except UnicodeEncodeError:
239
+ return False
240
+
241
+
242
+ class _ListingTooLarge(Exception):
243
+ """An ``rg --files`` listing printed more than ``_MAX_LISTING_BYTES``."""
244
+
245
+
246
+ class _RgStopped(Exception):
247
+ """Leaves ``managed_subprocess`` once rg's output is no longer needed.
248
+
249
+ Leaving it by an exception kills rg's whole tree; leaving it normally does so
250
+ only while the process it started runs, and a launcher rg runs under may
251
+ already have exited.
252
+ """
253
+
254
+
255
+ class _RgStdout:
256
+ """rg's stdout: kept whole up to ``_MAX_LISTING_BYTES``, or handed to *consume* as it arrives."""
257
+
258
+ def __init__(self, consume: Callable[[bytes], bool] | None) -> None:
259
+ self._consume = consume
260
+ self.data = bytearray()
261
+ self.stopped = asyncio.Event()
262
+ """Set once no more output is needed."""
263
+ self.too_large = False
264
+
265
+ def feed(self, chunk: bytes) -> None:
266
+ if self.stopped.is_set():
267
+ return
268
+ if self._consume is not None:
269
+ if not self._consume(chunk):
270
+ self.stopped.set()
271
+ elif len(self.data) + len(chunk) > _MAX_LISTING_BYTES:
272
+ self.too_large = True
273
+ self.stopped.set()
274
+ else:
275
+ self.data += chunk
276
+
277
+
278
+ async def _drain_until_stopped(proc: asyncio.subprocess.Process, stdout: _RgStdout, stderr: BoundedCapture) -> None:
279
+ """Read rg's output until it exits, or until *stdout* needs no more of it.
280
+
281
+ A stopped rg is left running for ``managed_subprocess`` to kill with its whole
282
+ tree, which on Windows must happen while a launcher rg runs under lives.
283
+ """
284
+ drain = asyncio.ensure_future(drain_process_pipes(proc, stdout, stderr))
285
+ stopped = asyncio.ensure_future(stdout.stopped.wait())
286
+ try:
287
+ await asyncio.wait((drain, stopped), return_when=asyncio.FIRST_COMPLETED)
288
+ finally:
289
+ drain.cancel()
290
+ stopped.cancel()
291
+ await asyncio.gather(drain, stopped, return_exceptions=True)
292
+ error = None if drain.cancelled() else drain.exception()
293
+ if error is not None:
294
+ raise error
295
+
296
+
297
+ async def _run_rg(
298
+ args: list[str],
299
+ *,
300
+ timeout: int = _TIMEOUT,
301
+ cwd: str | None = None,
302
+ consume: Callable[[bytes], bool] | None = None,
303
+ ) -> tuple[str, str, int]:
304
+ """Run ``rg`` asynchronously and return ``(stdout, stderr, returncode)``.
305
+
306
+ With *consume*, stdout goes to it as rg prints it and comes back empty;
307
+ once *consume* returns False, rg is stopped, and its exit code then says
308
+ nothing about the search. Without, stdout comes back whole, and a listing
309
+ longer than ``_MAX_LISTING_BYTES`` stops rg and raises ``_ListingTooLarge``.
310
+ """
311
+ rg = _find_rg()
312
+ stdout = _RgStdout(consume)
313
+ stderr = BoundedCapture(_MAX_STDERR_BYTES)
314
+ returncode = 0
315
+ try:
316
+ async with managed_subprocess(
317
+ rg,
318
+ *args,
319
+ cwd=cwd,
320
+ stdin=asyncio.subprocess.DEVNULL,
321
+ stdout=asyncio.subprocess.PIPE,
322
+ stderr=asyncio.subprocess.PIPE,
323
+ ) as proc:
324
+ await asyncio.wait_for(_drain_until_stopped(proc, stdout, stderr), timeout=timeout)
325
+ if stdout.stopped.is_set():
326
+ raise _RgStopped
327
+ returncode = proc.returncode or 0
328
+ except _RgStopped:
329
+ pass
330
+ except (FileNotFoundError, NotADirectoryError, MissingWorkingDirectoryError) as exc:
331
+ # Process creation can fail because cwd disappeared, even while rg exists.
332
+ if cwd is not None and not os.path.isdir(cwd):
333
+ raise NotADirectoryError(f"search directory unavailable — {cwd}") from exc
334
+ raise
335
+ if stdout.too_large:
336
+ raise _ListingTooLarge
337
+ output = bytes(stdout.data)
338
+ return (
339
+ os.fsdecode(output) if "--null" in args else decode_subprocess_output(output),
340
+ stderr.snapshot().text(),
341
+ returncode,
342
+ )
343
+
344
+
345
+ def _ignore_args(respect_gitignore: bool) -> list[str]:
346
+ # User ripgrep configuration must not silently override this tool's policy.
347
+ return ["--no-config", "--glob", "!.git/", *([] if respect_gitignore else ["--no-ignore-vcs"])]
348
+
349
+
350
+ def _search_cwd(root: str) -> str:
351
+ """Keep CreateProcess's working directory below its Windows path limit."""
352
+ if _PLATFORM.is_windows:
353
+ while len(root.encode("utf-16-le", errors="surrogatepass")) // 2 >= 258:
354
+ parent = os.path.dirname(root)
355
+ if parent == root:
356
+ break
357
+ root = parent
358
+ return root
359
+
360
+
361
+ def _literal_glob(pattern: str) -> str | None:
362
+ """Decode a literal glob component without implementing glob matching."""
363
+ literal: list[str] = []
364
+ escaped = False
365
+ for char in pattern:
366
+ if escaped:
367
+ literal.append(char)
368
+ escaped = False
369
+ elif char == "\\":
370
+ escaped = True
371
+ elif char in "*?[]{}":
372
+ return None
373
+ else:
374
+ literal.append(char)
375
+ return None if escaped else "".join(literal)
376
+
377
+
378
+ def _glob_scope(root: str, pattern: str | None) -> tuple[str, str | None]:
379
+ """Pass explicitly named directory prefixes to rg as search operands.
380
+
381
+ Keep the remaining glob anchored so src/*.py does not become a recursive
382
+ *.py search. The caller chooses the ignore policy for the explicit target.
383
+ """
384
+ if not pattern or pattern.startswith(("!", "#")) or not os.path.isdir(root):
385
+ return root, pattern
386
+ parts = pattern.removeprefix("/").split("/")
387
+ prefix: list[str] = []
388
+ scoped = root
389
+ for part in parts[:-1]:
390
+ literal = _literal_glob(part)
391
+ if not literal or literal in (".", "..") or os.path.splitdrive(literal)[0]:
392
+ break
393
+ # Filesystem lookup may accept SRC for src (or .git. for .git).
394
+ # A glob is case-sensitive: only promote an exact directory entry.
395
+ try:
396
+ with os.scandir(scoped) as entries:
397
+ if not any(entry.name == literal and entry.is_dir() for entry in entries):
398
+ break
399
+ except OSError:
400
+ break # Let rg report an unavailable/unreadable search directory.
401
+ if literal == ".git":
402
+ break
403
+ prefix.append(literal)
404
+ scoped = os.path.join(scoped, literal)
405
+ if not prefix:
406
+ return root, pattern
407
+ return scoped, "/" + ("/".join(parts[len(prefix) :]) or "**")
408
+
409
+
410
+ def _read_ignore_probe_file(path: Path) -> str | None:
411
+ """Read small rule/config files; defer unreadable or large inputs to Git."""
412
+ try:
413
+ with path.open("rb") as stream:
414
+ data = stream.read(65_537)
415
+ # Rule/config contents can contain legacy-encoded comments on any OS.
416
+ return data.decode("utf-8", errors="surrogateescape") if len(data) <= 65_536 else None
417
+ except FileNotFoundError:
418
+ return ""
419
+ except OSError:
420
+ return None
421
+
422
+
423
+ def _ignore_rule_names_directory(rule: str, target: str) -> bool:
424
+ """Recognize rules such as /log/* and **/dist/** without broadening '*' rules."""
425
+ named = rule.removeprefix("/")
426
+ recursive = named.startswith("**/")
427
+ named = named.removeprefix("**/")
428
+ if not named.endswith(("/*", "/**")):
429
+ return False
430
+ named = _literal_glob(named.removesuffix("/**").removesuffix("/*"))
431
+ return bool(
432
+ named and (target.lower() == named.lower() or (recursive and target.lower().endswith("/" + named.lower())))
433
+ )
434
+
435
+
436
+ def _gitignore_probe_needed(root: str, repo: str, git: str) -> bool:
437
+ """Skip Git only when local rules cannot affect the explicitly selected directory.
438
+
439
+ This is a conservative prefilter, not the authoritative ignore decision.
440
+ Negations never suppress a probe; Git resolves their precedence. Unusual
441
+ configuration and linked worktrees retain the original Git-based path.
442
+ """
443
+ repository = Path(repo)
444
+ if not (repository / ".git").is_dir() or any(
445
+ key in os.environ
446
+ for key in ("GIT_DIR", "GIT_COMMON_DIR", "GIT_WORK_TREE", "GIT_CONFIG_PARAMETERS", "GIT_CONFIG_COUNT")
447
+ ):
448
+ return True
449
+ git_home = Path(os.environ.get("HOME") or Path.home())
450
+ xdg_git = Path(os.environ.get("XDG_CONFIG_HOME") or git_home / ".config") / "git"
451
+ configs = [repository / ".git/config"]
452
+ configs.extend(
453
+ [Path(os.environ["GIT_CONFIG_GLOBAL"])]
454
+ if "GIT_CONFIG_GLOBAL" in os.environ
455
+ else [git_home / ".gitconfig", xdg_git / "config"]
456
+ )
457
+ configs.extend(
458
+ [Path(os.environ["GIT_CONFIG_SYSTEM"])]
459
+ if "GIT_CONFIG_SYSTEM" in os.environ
460
+ else [
461
+ *([] if _PLATFORM.is_windows else [Path("/etc/gitconfig")]),
462
+ Path(git).parent.parent / "etc/gitconfig",
463
+ Path(git).resolve().parent.parent / "etc/gitconfig",
464
+ *([Path(git).parent.parent.parent / "etc/gitconfig"] if _PLATFORM.is_windows else []),
465
+ ]
466
+ )
467
+ for config in configs:
468
+ if not config.is_absolute() and os.fspath(config) != os.devnull:
469
+ return True # Relative config paths may resolve against Git's -C directory.
470
+ contents = _read_ignore_probe_file(config)
471
+ if contents is None or re.search(r"(?i)\bexcludesfile\b|\[\s*include|\bworktreeconfig\b", contents):
472
+ return True
473
+
474
+ relative = _to_posix(os.path.relpath(root, repo))
475
+ sources = [(repository / ".git/info/exclude", relative), (xdg_git / "ignore", relative)]
476
+ directory = Path(root)
477
+ while True:
478
+ sources.append((directory / ".gitignore", _to_posix(os.path.relpath(root, directory))))
479
+ if directory == repository:
480
+ break
481
+ directory = directory.parent
482
+ for source, target in sources:
483
+ contents = _read_ignore_probe_file(source)
484
+ if contents is None:
485
+ return True
486
+ lines = contents.removeprefix("\ufeff").splitlines()
487
+ # Git drops the initial UTF-8 BOM and unescaped trailing spaces. Keep
488
+ # raw variants for escaped whitespace; extra matches only request a Git probe.
489
+ lines += [line.rstrip() for line in lines if line.rstrip() != line]
490
+ if target == "." and any(line in ("*", "**", "/*", "/**") for line in lines):
491
+ return True
492
+ if any(_ignore_rule_names_directory(line, target) for line in lines if not line.startswith("!")):
493
+ return True
494
+ try:
495
+ # Case-folding can overestimate matches on a case-sensitive repo;
496
+ # it must not miss one when Git has core.ignoreCase enabled.
497
+ spec = GitIgnoreSpec.from_lines(line.lower() for line in lines if not line.startswith("!"))
498
+ if spec.match_file(target.lower()) or spec.match_file(target.lower() + "/"):
499
+ return True
500
+ except ValueError, re.error:
501
+ return True
502
+ return False
503
+
504
+
505
+ async def _directory_is_gitignored(root: str) -> bool:
506
+ """Recognize explicit ignored targets, including a directory's own '*' rule."""
507
+ repo = root
508
+ while not os.path.exists(os.path.join(repo, ".git")):
509
+ parent = os.path.dirname(repo)
510
+ if parent == repo:
511
+ return False
512
+ repo = parent
513
+ # A worktree root's rules still govern ordinary project-wide discovery.
514
+ git = shutil.which("git")
515
+ if repo == root or git is None:
516
+ return False
517
+ if not await asyncio.to_thread(_gitignore_probe_needed, root, repo, git):
518
+ return False
519
+ relative = _to_posix(os.path.relpath(root, repo))
520
+ operands = os.fsencode(relative + "\0" + relative + "/\0")
521
+ try:
522
+ async with managed_subprocess(
523
+ git,
524
+ "-C",
525
+ repo,
526
+ "check-ignore",
527
+ "--no-index",
528
+ "--verbose",
529
+ "-z",
530
+ "--stdin",
531
+ cwd=_search_cwd(repo),
532
+ stdin=asyncio.subprocess.PIPE,
533
+ stdout=asyncio.subprocess.PIPE,
534
+ stderr=asyncio.subprocess.DEVNULL,
535
+ ) as proc:
536
+ stdout, _ = await asyncio.wait_for(proc.communicate(operands), timeout=5)
537
+ if proc.returncode not in (0, 1):
538
+ return False
539
+ except OSError, TimeoutError:
540
+ return False
541
+ try:
542
+ text = os.fsdecode(stdout)
543
+ except UnicodeError:
544
+ # Git can report raw non-UTF-8 source paths or matching patterns.
545
+ text = stdout.decode("utf-8", errors="surrogateescape")
546
+ fields = text.split("\0")
547
+ for index in range(0, len(fields) - 3, 4):
548
+ source, _, rule, operand = fields[index : index + 4]
549
+ if rule.startswith("!"):
550
+ continue
551
+ if operand == relative:
552
+ return True
553
+ # A trailing slash also probes rules for the target's contents. An
554
+ # inherited blanket '*' may coexist with !directory/, so only accept
555
+ # a local blanket rule or an inherited rule naming this directory.
556
+ if source == relative + "/.gitignore" and rule in ("*", "**", "/*", "/**"):
557
+ return True
558
+ origin = (
559
+ os.path.dirname(os.path.join(repo, source))
560
+ if not os.path.isabs(source) and os.path.basename(source) == ".gitignore"
561
+ else repo
562
+ )
563
+ target = _to_posix(os.path.relpath(root, origin))
564
+ if _ignore_rule_names_directory(rule, target):
565
+ return True
566
+ return False
567
+
568
+
569
+ async def _search_files(root: str, pattern: str | None, respect_gitignore: bool) -> list[str] | str:
570
+ """Intersect ignore-aware discovery with rg's native glob matching.
571
+
572
+ Wildcards intersect an ignore-aware listing with rg's native glob matcher.
573
+ Literal names are explicit requests and use rg's override semantics; named
574
+ directory prefixes similarly become explicit operands. NUL delimiters
575
+ preserve whitespace and newlines in filenames.
576
+ Explicit ignored directories bypass Git rules for their contents.
577
+ """
578
+ files: list[str] = []
579
+ root, pattern = _glob_scope(root, pattern)
580
+ if respect_gitignore and os.path.isdir(root) and await _directory_is_gitignored(root):
581
+ respect_gitignore = False
582
+ explicit_name = bool(pattern and not pattern.startswith(("!", "#")) and _literal_glob(pattern) is not None)
583
+ # Recognize explicit dots at component/alternative starts and in positive
584
+ # character classes. The extension dot in *.py must not enable hidden files.
585
+ hidden = bool(
586
+ pattern
587
+ and not pattern.startswith(("!", "#"))
588
+ and re.search(r"(?:^|[/,{])(?:\\?\.(?!\.?(?:/|$))|\[(?![!^])[^.\]]*\.[^\]]*\])", pattern)
589
+ )
590
+ cwd, target = (root, ".") if os.path.isdir(root) else os.path.split(root)
591
+ safe_cwd = _search_cwd(cwd)
592
+ if safe_cwd != cwd:
593
+ prefix = _to_posix(os.path.relpath(cwd, safe_cwd))
594
+ # Slash-containing globs are rooted at cwd. Move their anchor with the
595
+ # operand; basename-only globs already match at any depth.
596
+ if pattern:
597
+ negation = "!" if pattern.startswith("!") else ""
598
+ body = pattern.removeprefix("!")
599
+ if body.startswith("/") or "/" in body.removesuffix("/"):
600
+ escaped = "".join("\\" + char if char in "\\*?[]{}!#" else char for char in prefix)
601
+ pattern = f"{negation}{escaped}/{body.removeprefix('/')}"
602
+ target = os.path.relpath(root, safe_cwd)
603
+ cwd = safe_cwd
604
+ if target == "-":
605
+ target = os.path.join(".", target)
606
+ patterns = (pattern,) if explicit_name else ((None,) if pattern is None or pattern == "*" else (None, pattern))
607
+ for index, filename_pattern in enumerate(patterns):
608
+ filters = [] if filename_pattern is None else ["--glob", filename_pattern]
609
+ try:
610
+ stdout, stderr, code = await _run_rg(
611
+ [
612
+ "--files",
613
+ "--null",
614
+ *(["--hidden"] if hidden else []),
615
+ *filters,
616
+ *_ignore_args(respect_gitignore),
617
+ "--",
618
+ target,
619
+ ],
620
+ cwd=cwd,
621
+ )
622
+ except _ListingTooLarge:
623
+ # Searching only part of the listing would hide matches without saying so.
624
+ return tool_error(
625
+ "search_too_broad",
626
+ f"too many files under {root} to list (over {_MAX_LISTING_BYTES // (1024 * 1024)} MiB of names)"
627
+ " — search a narrower path",
628
+ )
629
+ if code not in (0, 1):
630
+ errors = _SearchErrors()
631
+ errors.add(stderr, code)
632
+ record_process_result(code)
633
+ return tool_error("search_process_failed", errors.summary())
634
+ found = stdout.split("\0")
635
+ if index == 0:
636
+ files = [name for name in found if name]
637
+ else:
638
+ matches = set(found)
639
+ files = [name for name in files if name in matches]
640
+ return [os.path.normpath(os.path.join(cwd, name)) for name in files]
641
+
642
+
643
+ def _argument_size(argument: str) -> int:
644
+ """Measure one appended argument, including its separator or terminator."""
645
+ if _PLATFORM.is_windows:
646
+ quoted = subprocess.list2cmdline([argument])
647
+ return len(quoted.encode("utf-16-le", errors="surrogatepass")) // 2 + 1
648
+ return len(os.fsencode(argument)) + 1 + (8 if _PLATFORM.is_linux else 0)
649
+
650
+
651
+ def _command_size(command: list[str]) -> int:
652
+ """Measure the executable and fixed options in the platform's budget units."""
653
+ if _PLATFORM.is_windows:
654
+ return len(subprocess.list2cmdline(command).encode("utf-16-le", errors="surrogatepass")) // 2 + 1
655
+ return sum(_argument_size(argument) for argument in command) + (8 if _PLATFORM.is_linux else 0)
656
+
657
+
658
+ def _file_batches(files: list[str], *, command: list[str]) -> Iterator[list[str]]:
659
+ """Fill the remaining command budget after the executable and fixed options."""
660
+ limit = _WINDOWS_COMMAND_LIMIT - _WINDOWS_COMMAND_HEADROOM if _PLATFORM.is_windows else _POSIX_COMMAND_LIMIT
661
+ fixed_size = _command_size(command)
662
+ batch: list[str] = []
663
+ size = fixed_size
664
+ for path in files:
665
+ cost = _argument_size(path)
666
+ if fixed_size + cost > limit:
667
+ raise ValueError(f"search command and file path exceed the platform command budget ({limit})")
668
+ if batch and size + cost > limit:
669
+ yield batch
670
+ batch = []
671
+ size = fixed_size
672
+ batch.append(path)
673
+ size += cost
674
+ if batch:
675
+ yield batch
676
+
677
+
678
+ # ---------------------------------------------------------------------------
679
+ # grep
680
+ # ---------------------------------------------------------------------------
681
+
682
+
683
+ _CUT_RECORD_HEAD = re.compile(rb'\{"type":"(match|context)","data":\{"path":')
684
+ """How rg starts a match or context record: its file comes first, before the line."""
685
+ _CUT_RECORD_LINE = re.compile(rb',"line_number":(\d+)[,}]')
686
+ """How rg gives a record's line number, after the line: a JSON string holds no unescaped quote."""
687
+ _CUT_RECORD_LINE_WINDOW = 64
688
+ """Bytes kept from one chunk to the next, so a line number split between them is still found."""
689
+
690
+
691
+ def _whole_record(record: bytes | bytearray) -> tuple[str, dict[str, Any]]:
692
+ entry = json.loads(record)
693
+ return entry.get("type", ""), entry.get("data", {})
694
+
695
+
696
+ def _cut_record(head: bytes | bytearray) -> tuple[str, dict[str, Any]]:
697
+ """Read the type and file of a record from the head kept of it."""
698
+ match = _CUT_RECORD_HEAD.match(head)
699
+ if match is None:
700
+ raise ValueError("not a match or context record")
701
+ path, _ = json.JSONDecoder().raw_decode(head[match.end() :].decode("utf-8", errors="replace"))
702
+ return match.group(1).decode("ascii"), {"path": path}
703
+
704
+
705
+ @dataclass(frozen=True, slots=True)
706
+ class _OversizedMatch:
707
+ """A match whose record was too long to keep: its file and line number, without the line."""
708
+
709
+ rel_path: str
710
+ line_num: int
711
+
712
+
713
+ type _GrepItem = tuple[GrepEntry, LongLine | None] | _OversizedMatch
714
+ """A parsed line and its long-line note, or a match too long to keep."""
715
+
716
+
717
+ class _GrepStream:
718
+ """Parse ``rg --json`` output as it arrives, keeping only what the result can show.
719
+
720
+ Output stops being needed once one match more than *max_results* has
721
+ arrived; its caller stops rg there. Matches in *seen_matches* (found by an
722
+ earlier pass) are kept without being counted. A match too long to keep is
723
+ named in ``oversized`` by its file and line, and counts like any other.
724
+ With *skip_binary*, a file's entries wait
725
+ for its ``end`` record, which says whether rg found binary content, and
726
+ only as many wait as could still fill the result.
727
+ """
728
+
729
+ def __init__(
730
+ self,
731
+ root: str,
732
+ max_results: int,
733
+ *,
734
+ seen_matches: set[tuple[str, int]] | None = None,
735
+ skip_binary: bool = False,
736
+ ) -> None:
737
+ self.result = GrepParseResult()
738
+ self.full = False
739
+ self.saw_output = False
740
+ self._root = root
741
+ self._max_results = max_results
742
+ self._seen_matches = seen_matches
743
+ self._skip_binary = skip_binary
744
+ self._record = bytearray()
745
+ self._record_cut = False
746
+ self._cut_tail = bytearray()
747
+ self._cut_line: int | None = None
748
+ self._pending: dict[tuple[str | None, str | None], list[_GrepItem]] = {}
749
+ self._pending_matches: dict[tuple[str | None, str | None], int] = {}
750
+
751
+ def feed(self, chunk: bytes) -> bool:
752
+ """Take the next chunk of output; False once the result needs no more."""
753
+ self.saw_output = self.saw_output or bool(chunk)
754
+ start = 0
755
+ while not self.full:
756
+ end = chunk.find(b"\n", start)
757
+ piece = chunk[start : len(chunk) if end < 0 else end]
758
+ if not self._record_cut:
759
+ self._record += piece
760
+ if len(self._record) > _MAX_RECORD_BYTES:
761
+ self._find_cut_line(self._record)
762
+ del self._record[_RECORD_HEAD_BYTES:]
763
+ self._record_cut = True
764
+ elif self._cut_line is None:
765
+ self._find_cut_line(piece)
766
+ if end < 0:
767
+ break
768
+ self._take(self._record, cut=self._record_cut)
769
+ self._record.clear()
770
+ self._record_cut = False
771
+ self._cut_tail.clear()
772
+ self._cut_line = None
773
+ start = end + 1
774
+ return not self.full
775
+
776
+ def _find_cut_line(self, data: bytes | bytearray) -> None:
777
+ self._cut_tail += data
778
+ found = _CUT_RECORD_LINE.search(self._cut_tail)
779
+ if found is not None:
780
+ self._cut_line = int(found.group(1))
781
+ del self._cut_tail[:-_CUT_RECORD_LINE_WINDOW]
782
+
783
+ def _take(self, record: bytearray, *, cut: bool) -> None:
784
+ try:
785
+ entry_type, data = _cut_record(record) if cut else _whole_record(record)
786
+ except ValueError:
787
+ return
788
+ path_data = data.get("path", {})
789
+ key = (path_data.get("text"), path_data.get("bytes"))
790
+ if entry_type == "end":
791
+ items = self._pending.pop(key, [])
792
+ self._pending_matches.pop(key, None)
793
+ if data.get("binary_offset") is None:
794
+ for item in items:
795
+ self._keep(item)
796
+ if self.full:
797
+ return
798
+ return
799
+ if entry_type not in ("match", "context"):
800
+ return
801
+ waiting = self._pending_matches.get(key, 0)
802
+ if self._skip_binary and waiting > self._max_results - self.result.match_count:
803
+ return # Enough of this file waits to fill the result; a dense file sends many more.
804
+ rel_path = self._rel_path(path_data)
805
+ if cut:
806
+ # The line itself was not kept: a match is named by its file and line, its context dropped.
807
+ if entry_type != "match" or self._cut_line is None:
808
+ return
809
+ item: _GrepItem = _OversizedMatch(rel_path, self._cut_line)
810
+ else:
811
+ item = self._entry(entry_type, data, rel_path)
812
+ if not self._skip_binary:
813
+ self._keep(item)
814
+ return
815
+ if self._counts(item):
816
+ self._pending_matches[key] = waiting + 1
817
+ self._pending.setdefault(key, []).append(item)
818
+
819
+ def _rel_path(self, path_data: dict[str, Any]) -> str:
820
+ file_path = (
821
+ os.fsdecode(base64.b64decode(path_data["bytes"])) if "bytes" in path_data else path_data.get("text", "")
822
+ )
823
+ if file_path and not os.path.isabs(file_path):
824
+ file_path = os.path.join(self._root, file_path)
825
+ return _to_posix(os.path.relpath(file_path, self._root)) if file_path else ""
826
+
827
+ def _entry(self, entry_type: str, data: dict[str, Any], rel_path: str) -> tuple[GrepEntry, LongLine | None]:
828
+ line_num = data.get("line_number", 0)
829
+ line_text = data.get("lines", {}).get("text", "").rstrip("\r\n")
830
+ long_line = None
831
+ if len(line_text) > _MAX_LINE_DISPLAY_CHARS:
832
+ long_line = LongLine(rel_path, line_num, len(line_text))
833
+ line_text = line_text[:_MAX_LINE_DISPLAY_CHARS] + "... [truncated]"
834
+ return GrepEntry(rel_path, line_num, ">" if entry_type == "match" else " ", line_text), long_line
835
+
836
+ def _counts(self, item: _GrepItem) -> bool:
837
+ if isinstance(item, _OversizedMatch):
838
+ key = (item.rel_path, item.line_num)
839
+ elif item[0].marker == ">":
840
+ key = (item[0].rel_path, item[0].line_num)
841
+ else:
842
+ return False
843
+ return self._seen_matches is None or key not in self._seen_matches
844
+
845
+ def _keep(self, item: _GrepItem) -> None:
846
+ counts = self._counts(item)
847
+ if counts:
848
+ self.result.match_count += 1
849
+ if self.result.match_count > self._max_results:
850
+ self.full = True
851
+ return
852
+ if isinstance(item, _OversizedMatch):
853
+ if counts: # Otherwise an earlier pass showed or named it.
854
+ self.result.oversized.add((item.rel_path, item.line_num))
855
+ return
856
+ entry, long_line = item
857
+ self.result.entries.append(entry)
858
+ if long_line is not None:
859
+ self.result.long_lines.append(long_line)
860
+
861
+
862
+ def _oversized_note(lines: set[tuple[str, int]]) -> str:
863
+ if not lines:
864
+ return ""
865
+ ordered = sorted(lines)
866
+ shown = ", ".join(f"{rel_path}:{line_num}" for rel_path, line_num in ordered[:_MAX_OVERSIZED_LINES_SHOWN])
867
+ more = len(ordered) - _MAX_OVERSIZED_LINES_SHOWN
868
+ if more > 0:
869
+ shown += f" and {more} more line(s)"
870
+ limit = _MAX_RECORD_BYTES // (1024 * 1024)
871
+ return f"\n\n[Matching lines too long to show (over {limit} MiB of search output each): {shown}]"
872
+
873
+
874
+ def _format_matches(entries: list[GrepEntry]) -> list[str]:
875
+ """Group entries into context blocks and format for display."""
876
+ matches: list[str] = []
877
+ block: list[GrepEntry] = []
878
+
879
+ def _flush_block() -> None:
880
+ if not block:
881
+ return
882
+ first_match = next((b for b in block if b.marker == ">"), block[0])
883
+ header = f"{first_match.rel_path}:{first_match.line_num}"
884
+ lines = [f" {e.marker} {e.line_num:>4} | {e.text}" for e in block]
885
+ matches.append(header + "\n" + "\n".join(lines))
886
+
887
+ for entry in entries:
888
+ if block:
889
+ prev = block[-1]
890
+ if entry.rel_path != prev.rel_path or entry.line_num != prev.line_num + 1:
891
+ _flush_block()
892
+ block = []
893
+ block.append(entry)
894
+
895
+ _flush_block()
896
+ return matches
897
+
898
+
899
+ async def _grep_impl(
900
+ pattern: str,
901
+ path: str = ".",
902
+ glob: str | None = None,
903
+ context_lines: int = 2,
904
+ max_results: int = _MAX_RESULTS,
905
+ *,
906
+ base_cwd: str | None = None,
907
+ respect_gitignore: bool = True,
908
+ ) -> str:
909
+ max_results = min(max_results, _MAX_RESULTS_HARD_LIMIT)
910
+ missing_base = missing_base_cwd_error(path, base_cwd)
911
+ if missing_base is not None:
912
+ return missing_base
913
+ root = resolve_workspace_path(path, base_cwd=base_cwd)
914
+ if not os.path.exists(root):
915
+ return tool_error("path_not_found", f"path not found — {root}", details={"path": path, "resolved_path": root})
916
+
917
+ base_args = [
918
+ "--json",
919
+ "-e",
920
+ pattern,
921
+ "-C",
922
+ str(context_lines),
923
+ *_ignore_args(respect_gitignore),
924
+ ]
925
+
926
+ # Determine which encodings to search. For pure-ASCII patterns a single
927
+ # pass suffices since ASCII bytes are identical across all encodings.
928
+ encodings = ["utf-8"] if _is_ascii_only(pattern) else _get_encodings()
929
+
930
+ all_entries: dict[tuple[str, int], GrepEntry] = {}
931
+ all_long_lines: list[LongLine] = []
932
+ seen_long: set[tuple[str, int]] = set()
933
+ oversized: set[tuple[str, int]] = set()
934
+ total_matches = 0
935
+ # Matches too long to show count toward the limit too, so a full search can show fewer.
936
+ limited = False
937
+ errors = _SearchErrors()
938
+
939
+ try:
940
+ async with asyncio.timeout(_TIMEOUT):
941
+ files = [root]
942
+ globbed_directory = bool(glob and glob != "*" and os.path.isdir(root))
943
+ if (
944
+ not globbed_directory
945
+ and respect_gitignore
946
+ and os.path.isdir(root)
947
+ and await _directory_is_gitignored(root)
948
+ ):
949
+ base_args.append("--no-ignore-vcs")
950
+ search_cwd = root if globbed_directory and _search_cwd(root) == root else None
951
+ if globbed_directory:
952
+ selected = await _search_files(root, glob, respect_gitignore)
953
+ if isinstance(selected, str):
954
+ return selected
955
+ files = []
956
+ for name in selected:
957
+ if search_cwd is None:
958
+ files.append(name)
959
+ continue
960
+ relative = os.path.relpath(name, root)
961
+ # Even after --, bare '-' means stdin to rg.
962
+ files.append(os.path.join(".", relative) if relative == "-" else relative)
963
+ encoding_args = {encoding: [*base_args, "-E", encoding, "--"] for encoding in encodings}
964
+ if not files:
965
+ # Validate with rg's own regex/flag parser against explicitly empty
966
+ # stdin. Never let an empty candidate set fall back to cwd traversal.
967
+ _, stderr, returncode = await _run_rg([*encoding_args["utf-8"], "-"])
968
+ if returncode not in (0, 1):
969
+ errors.add(stderr, returncode)
970
+ record_process_result(returncode)
971
+ return tool_error(
972
+ "search_process_failed", errors.summary(), details={"path": path, "pattern": pattern}
973
+ )
974
+ return surrogate_safe_text(f"No matches found for /{pattern}/ in {root}")
975
+ rg = _find_rg()
976
+ command = max(([rg, *args] for args in encoding_args.values()), key=_command_size)
977
+ fatal_error = False
978
+ # Never invoke rg with an empty argv file list: it would search cwd/stdin.
979
+ for batch in _file_batches(files, command=command):
980
+ utf8_searched = False
981
+ for encoding, args in tuple(encoding_args.items()):
982
+ parser = _GrepStream(
983
+ root,
984
+ max_results - total_matches - len(oversized),
985
+ seen_matches={key for key, entry in all_entries.items() if entry.marker == ">"} | oversized,
986
+ # Enumerated paths are explicit to rg, which enables binary searching.
987
+ # Filter before counting matches; user-supplied files retain rg's defaults.
988
+ skip_binary=globbed_directory,
989
+ )
990
+ _, stderr, returncode = await _run_rg([*args, *batch], cwd=search_cwd, consume=parser.feed)
991
+ # A full result already says it is limited. The rg stopped for it was killed: its exit code
992
+ # is the kill's, and its stderr names only the files it reached before then.
993
+ if not parser.full and returncode not in (0, 1):
994
+ errors.add(stderr, returncode)
995
+ if returncode == 2 and not parser.saw_output:
996
+ # File-read errors still emit a JSON summary. No stdout
997
+ # means rg failed before searching; repeating cannot help.
998
+ # UTF-8 runs first. Its completed search validates the
999
+ # shared args; only -E changes in subsequent passes.
1000
+ if utf8_searched:
1001
+ del encoding_args[encoding]
1002
+ continue
1003
+ fatal_error = True
1004
+ break
1005
+ if returncode != 2:
1006
+ continue
1007
+ if encoding == "utf-8":
1008
+ utf8_searched = True
1009
+
1010
+ parsed = parser.result
1011
+ limited = limited or parser.full
1012
+ oversized |= parsed.oversized
1013
+ for entry in parsed.entries:
1014
+ key = (entry.rel_path, entry.line_num)
1015
+ previous = all_entries.get(key)
1016
+ if previous is None or (previous.marker != ">" and entry.marker == ">"):
1017
+ all_entries[key] = entry
1018
+ if entry.marker == ">":
1019
+ total_matches += 1
1020
+ oversized.discard(key) # Too long to show under another encoding only.
1021
+ for ll in parsed.long_lines:
1022
+ key = (ll.rel_path, ll.line_num)
1023
+ if key not in seen_long:
1024
+ seen_long.add(key)
1025
+ all_long_lines.append(ll)
1026
+ if limited or total_matches + len(oversized) >= max_results:
1027
+ break
1028
+ if fatal_error or not encoding_args or limited or total_matches + len(oversized) >= max_results:
1029
+ break
1030
+ except NotADirectoryError as exc:
1031
+ # The session directory can vanish between the check above and rg's spawn.
1032
+ missing_base = missing_base_cwd_error(path, base_cwd)
1033
+ if missing_base is not None:
1034
+ return missing_base
1035
+ return tool_error("path_not_found", str(exc), details={"path": path, "resolved_path": root})
1036
+ except FileNotFoundError:
1037
+ return tool_error("ripgrep_not_found", "ripgrep (rg) not found")
1038
+ except TimeoutError:
1039
+ record_process_timeout(_TIMEOUT)
1040
+ return tool_error("search_timeout", f"search timed out after {_TIMEOUT}s", retryable=True)
1041
+ except Exception as e:
1042
+ return tool_error("search_failed", f"search failed — {e}", details={"path": path, "pattern": pattern})
1043
+
1044
+ if not total_matches and not oversized:
1045
+ if errors.exit_code is not None:
1046
+ record_process_result(errors.exit_code)
1047
+ # rg reports a session directory deleted after the check above as its own IO error.
1048
+ missing_base = missing_base_cwd_error(path, base_cwd)
1049
+ if missing_base is not None:
1050
+ return missing_base
1051
+ return tool_error("search_process_failed", errors.summary(), details={"path": path, "pattern": pattern})
1052
+ return surrogate_safe_text(f"No matches found for /{pattern}/ in {root}")
1053
+
1054
+ # Sort by (file, line_number) for consistent display
1055
+ matches = _format_matches(sorted(all_entries.values(), key=lambda e: (e.rel_path, e.line_num)))
1056
+ header = (
1057
+ f"Found {total_matches} match(es) in {root}"
1058
+ if total_matches
1059
+ else f"Found matches for /{pattern}/ in {root}, but every matching line is too long to show"
1060
+ )
1061
+ if limited or total_matches + len(oversized) >= max_results:
1062
+ header += f" (limited to {max_results})"
1063
+ result = "\n\n".join([header, *matches])
1064
+ if all_long_lines:
1065
+ details = ", ".join(f"{ll.rel_path}:{ll.line_num} ({ll.actual_length} chars)" for ll in all_long_lines)
1066
+ result += f"\n\n[Long lines truncated to {_MAX_LINE_DISPLAY_CHARS} chars: {details}]"
1067
+ result += _oversized_note(oversized)
1068
+ if errors.exit_code is not None:
1069
+ record_process_result(errors.exit_code)
1070
+ result += "\n\n" + tool_error(
1071
+ "search_incomplete",
1072
+ f"search results may be incomplete — {errors.summary()}",
1073
+ details={"path": path, "pattern": pattern},
1074
+ )
1075
+ # Keep raw path identities through filtering/deduplication; escape only display text.
1076
+ return surrogate_safe_text(result)
1077
+
1078
+
1079
+ @tool(kind=KIND_SEARCH)
1080
+ async def grep(
1081
+ pattern: Annotated[str, "Regex pattern to search for in file contents."],
1082
+ path: Annotated[str, "Directory or file path to search in."] = ".",
1083
+ glob: Annotated[
1084
+ str | None, "Glob pattern to filter files (e.g. '*.py', '**/*.ts'). Omit to search all files."
1085
+ ] = None,
1086
+ context_lines: Annotated[int, "Number of context lines before and after each match."] = 2,
1087
+ max_results: Annotated[int, "Maximum number of matches to return."] = _MAX_RESULTS,
1088
+ ) -> str:
1089
+ """Search file contents using a regex pattern. Returns matching lines with context.
1090
+
1091
+ Powered by ripgrep (rg --json -e PATTERN -C CONTEXT_LINES -E ENCODING PATH).
1092
+ The pattern is a Rust-flavor regex (PCRE-like, not Python re). Glob filtering
1093
+ uses gitignore-style rules: patterns without '/' match the filename at any
1094
+ depth (e.g. '*.py' matches 'src/foo.py'), patterns with '/' match against
1095
+ the relative path. max_results is capped at 100 regardless of the value provided.
1096
+ Directory searches respect Git ignore rules and ripgrep hidden-file rules.
1097
+ Explicit file names and directory prefixes can select hidden or ignored paths.
1098
+ Explicit ignored directories bypass Git ignore rules within that directory.
1099
+ """
1100
+ return await _grep_impl(
1101
+ pattern,
1102
+ path=path,
1103
+ glob=glob,
1104
+ context_lines=context_lines,
1105
+ max_results=max_results,
1106
+ )
1107
+
1108
+
1109
+ # ---------------------------------------------------------------------------
1110
+ # glob
1111
+ # ---------------------------------------------------------------------------
1112
+
1113
+
1114
+ async def _glob_impl(
1115
+ pattern: str,
1116
+ path: str = ".",
1117
+ max_results: int = _MAX_RESULTS,
1118
+ *,
1119
+ base_cwd: str | None = None,
1120
+ respect_gitignore: bool = True,
1121
+ ) -> str:
1122
+ max_results = min(max_results, _MAX_RESULTS_HARD_LIMIT)
1123
+ missing_base = missing_base_cwd_error(path, base_cwd)
1124
+ if missing_base is not None:
1125
+ return missing_base
1126
+ root = resolve_workspace_path(path, base_cwd=base_cwd)
1127
+ if not os.path.exists(root):
1128
+ return tool_error("path_not_found", f"path not found — {root}", details={"path": path, "resolved_path": root})
1129
+
1130
+ try:
1131
+ async with asyncio.timeout(_TIMEOUT):
1132
+ files = await _search_files(root, pattern, respect_gitignore)
1133
+ except NotADirectoryError as exc:
1134
+ # The session directory can vanish between the check above and rg's spawn.
1135
+ missing_base = missing_base_cwd_error(path, base_cwd)
1136
+ if missing_base is not None:
1137
+ return missing_base
1138
+ return tool_error("path_not_found", str(exc), details={"path": path, "resolved_path": root})
1139
+ except FileNotFoundError:
1140
+ return tool_error("ripgrep_not_found", "ripgrep (rg) not found")
1141
+ except TimeoutError:
1142
+ record_process_timeout(_TIMEOUT)
1143
+ return tool_error("search_timeout", f"search timed out after {_TIMEOUT}s", retryable=True)
1144
+ except Exception as e:
1145
+ return tool_error("glob_failed", f"glob failed — {e}", details={"path": path, "pattern": pattern})
1146
+
1147
+ if isinstance(files, str):
1148
+ return files
1149
+ if not files:
1150
+ return surrogate_safe_text(f"No files matching '{pattern}' found in {root}")
1151
+
1152
+ # Convert to relative paths and limit results
1153
+ results = []
1154
+ for name in files:
1155
+ rel = _to_posix(os.path.relpath(name, root))
1156
+ results.append(rel)
1157
+ if len(results) >= max_results:
1158
+ break
1159
+
1160
+ header = f"Found {len(results)} file(s) matching '{pattern}' in {root}"
1161
+ if len(results) >= max_results:
1162
+ header += f" (limited to {max_results})"
1163
+ return surrogate_safe_text(header + "\n" + "\n".join(f" {r}" for r in results))
1164
+
1165
+
1166
+ @tool(kind=KIND_SEARCH)
1167
+ async def glob(
1168
+ pattern: Annotated[str, "Glob pattern to match file names (e.g. '*.py', 'test_*.py', '**/*.ts')."],
1169
+ path: Annotated[str, "Directory to search in."] = ".",
1170
+ max_results: Annotated[int, "Maximum number of results to return."] = _MAX_RESULTS,
1171
+ ) -> str:
1172
+ """Find files by name pattern. Returns relative paths of matching files.
1173
+
1174
+ Powered by ripgrep (rg --files --glob PATTERN PATH). The pattern uses
1175
+ gitignore-style glob rules: patterns without '/' match the filename at any
1176
+ depth (e.g. '*.py' matches 'src/foo.py'), patterns with '/' match against
1177
+ the relative path. Searches recursively through all subdirectories.
1178
+ max_results is capped at 100 regardless of the value provided.
1179
+ Discovery respects Git ignore rules and ripgrep hidden-file rules.
1180
+ Explicit file names and directory prefixes can select hidden or ignored paths.
1181
+ Explicit ignored directories bypass Git ignore rules within that directory.
1182
+ """
1183
+ return await _glob_impl(pattern, path=path, max_results=max_results)
1184
+
1185
+
1186
+ class SearchTools:
1187
+ """Runtime-bound search tools that resolve relative paths per session."""
1188
+
1189
+ def __init__(self, runtime: SessionEnvironment, *, respect_gitignore: bool = True) -> None:
1190
+ self._runtime = runtime
1191
+ self._respect_gitignore = respect_gitignore
1192
+
1193
+ def tools(self) -> list:
1194
+ """Return search tools with descriptions reflecting this runtime's ignore policy."""
1195
+ if self._respect_gitignore:
1196
+ policy = (
1197
+ "Current setting: Respect .gitignore is enabled. Directory searches respect Git ignore rules.\n"
1198
+ "Explicit ignored directories bypass Git ignore rules within that directory."
1199
+ )
1200
+ else:
1201
+ policy = (
1202
+ "Current setting: Respect .gitignore is disabled. Directory searches do not respect Git ignore rules "
1203
+ "and can include Git-ignored files and directories."
1204
+ )
1205
+ guidance = (
1206
+ f"{policy}\n"
1207
+ "Ripgrep hidden-file rules and .ignore/.rgignore rules still apply.\n"
1208
+ "Explicit file names and directory prefixes can select hidden or ignored paths."
1209
+ )
1210
+ tools = [self.grep, self.glob]
1211
+ for search_tool in tools:
1212
+ search_tool.description = f"{search_tool.description.rstrip()}\n\n{guidance}"
1213
+ return tools
1214
+
1215
+ @tool(kind=KIND_SEARCH)
1216
+ async def grep(
1217
+ self,
1218
+ pattern: Annotated[str, "Regex pattern to search for in file contents."],
1219
+ path: Annotated[str, "Directory or file path to search in."] = ".",
1220
+ glob: Annotated[
1221
+ str | None, "Glob pattern to filter files (e.g. '*.py', '**/*.ts'). Omit to search all files."
1222
+ ] = None,
1223
+ context_lines: Annotated[int, "Number of context lines before and after each match."] = 2,
1224
+ max_results: Annotated[int, "Maximum number of matches to return."] = _MAX_RESULTS,
1225
+ ) -> str:
1226
+ """Search file contents using a regex pattern. Returns matching lines with context.
1227
+
1228
+ Powered by ripgrep (rg --json -e PATTERN -C CONTEXT_LINES -E ENCODING PATH).
1229
+ The pattern is a Rust-flavor regex (PCRE-like, not Python re). Glob filtering
1230
+ uses gitignore-style rules: patterns without '/' match the filename at any
1231
+ depth (e.g. '*.py' matches 'src/foo.py'), patterns with '/' match against
1232
+ the relative path. max_results is capped at 100 regardless of the value provided.
1233
+ """
1234
+ return await _grep_impl(
1235
+ pattern,
1236
+ path=path,
1237
+ glob=glob,
1238
+ context_lines=context_lines,
1239
+ max_results=max_results,
1240
+ base_cwd=self._runtime.cwd,
1241
+ respect_gitignore=self._respect_gitignore,
1242
+ )
1243
+
1244
+ @tool(kind=KIND_SEARCH)
1245
+ async def glob(
1246
+ self,
1247
+ pattern: Annotated[str, "Glob pattern to match file names (e.g. '*.py', 'test_*.py', '**/*.ts')."],
1248
+ path: Annotated[str, "Directory to search in."] = ".",
1249
+ max_results: Annotated[int, "Maximum number of results to return."] = _MAX_RESULTS,
1250
+ ) -> str:
1251
+ """Find files by name pattern. Returns relative paths of matching files.
1252
+
1253
+ Powered by ripgrep (rg --files --glob PATTERN PATH). The pattern uses
1254
+ gitignore-style glob rules: patterns without '/' match the filename at any
1255
+ depth (e.g. '*.py' matches 'src/foo.py'), patterns with '/' match against
1256
+ the relative path. Searches recursively through all subdirectories.
1257
+ max_results is capped at 100 regardless of the value provided.
1258
+ """
1259
+ return await _glob_impl(
1260
+ pattern,
1261
+ path=path,
1262
+ max_results=max_results,
1263
+ base_cwd=self._runtime.cwd,
1264
+ respect_gitignore=self._respect_gitignore,
1265
+ )