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,894 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """Cross-session mutation coordination registry.
4
+
5
+ Resolves the chrys-vs-chrys attribution case: N chrys sessions working in
6
+ the same working tree, each publishing what it *provably* wrote, each
7
+ subtracting peers' proven writes from its own window inferences.
8
+
9
+ Storage layout::
10
+
11
+ {session_root}/attribution/{repo_key}/{session_id}.json
12
+
13
+ - ``repo_key`` = first 16 hex chars of sha256 over the canonicalized
14
+ working-tree root (``.git`` directory *or* file — linked worktrees and
15
+ submodules resolve to their own tree). Outside any repo, a path keys
16
+ by its own directory — a pure function of the path, so independent
17
+ sessions land in the same tree. Keying by tree root is for
18
+ *discovery* only; matching itself is path-exact.
19
+ - One file per (tree, session), **single writer** (the owning session),
20
+ replaced atomically. Readers tolerate missing/partial/undecodable
21
+ files by skipping them — no cross-process locks. Within the session
22
+ the registry is in-memory state behind one lock and the file is its
23
+ serialization (no read-modify-write of the file itself).
24
+ - Files outlive their session: clean shutdown stamps ``closed_at`` but
25
+ does not delete (late rollbacks in other sessions still need the
26
+ claims). Removal happens only via opportunistic GC — on write,
27
+ peer files whose mtime is older than :data:`REGISTRY_GC_AGE_SECONDS`
28
+ are deleted.
29
+
30
+ Reclassification (read side) is **retroactive, idempotent, and runs at
31
+ every opportunity** — own finalize, pre-summary display refresh, and
32
+ rollback-plan construction. Matching uses write *intervals* (never
33
+ point timestamps) on *canonical* path keys (``realpath`` +
34
+ ``normcase``). Conflict rules per overlapping path:
35
+
36
+ ========== ============================== ===========================
37
+ local row evidence result
38
+ ========== ============================== ===========================
39
+ ASSUMED content identity demote to FOREIGN
40
+ ASSUMED overlap without identity stays ASSUMED, contested
41
+ PROVEN any overlap stays PROVEN, contested
42
+ ========== ============================== ===========================
43
+
44
+ Content identity = hashes present and equal, or both sides gone.
45
+ Skip-marked sides and MOVEs never demote (they fall to ``contested``).
46
+ Coordination must never break tool execution: every filesystem failure
47
+ is swallowed and logged at debug level.
48
+ """
49
+
50
+ from __future__ import annotations
51
+
52
+ import contextlib
53
+ import hashlib
54
+ import json
55
+ import logging
56
+ import os
57
+ import threading
58
+ import time
59
+ import uuid
60
+ from dataclasses import dataclass, field
61
+ from pathlib import Path
62
+ from typing import TYPE_CHECKING, Any
63
+
64
+ from chrys.foundation.branding import APP_DISPLAY_NAME
65
+ from chrys.foundation.platform import get_platform
66
+ from chrys.foundation.platform.files import atomic_write_text
67
+ from chrys.service.mutations.types import (
68
+ FileMutation,
69
+ MutationProvenance,
70
+ RollbackExclusionReason,
71
+ RollbackPlan,
72
+ RollbackWarning,
73
+ )
74
+
75
+ if TYPE_CHECKING:
76
+ from collections.abc import Callable, Iterable
77
+
78
+ from chrys.service.mutations.tracker import MutationTracker
79
+ from chrys.service.mutations.types import MutationLog
80
+
81
+ logger = logging.getLogger(__name__)
82
+
83
+ REGISTRY_SCHEMA = 1
84
+ MAX_CLAIMS = 1000
85
+ REGISTRY_GC_AGE_SECONDS = 7 * 24 * 60 * 60
86
+ ATTRIBUTION_DIR_NAME = "attribution"
87
+
88
+ # In-flight peer warning (RollbackWarning.code).
89
+ PEER_ACTIVE_WINDOW_WARNING = "peer_active_window"
90
+
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # Path canonicalization and working-tree discovery
94
+ # ---------------------------------------------------------------------------
95
+
96
+
97
+ def fold_case(text: str) -> str:
98
+ """The case fold ``canonical_key`` applies — usable on path fragments.
99
+
100
+ ``normcase`` folds on Windows ONLY — darwin needs the explicit fold,
101
+ since default APFS/HFS+ volumes are case-insensitive too. (On a
102
+ case-SENSITIVE mac volume case twins then merely share a registry
103
+ namespace; matching still demands interval overlap plus content
104
+ identity — the same trade Windows makes for case-sensitive NTFS
105
+ directories.)
106
+ """
107
+ folded = os.path.normcase(text)
108
+ if get_platform().is_macos:
109
+ folded = folded.lower()
110
+ return folded
111
+
112
+
113
+ def canonical_key(path: str) -> str:
114
+ """Canonical matching key: ``realpath`` + case folding.
115
+
116
+ The codebase's usual ``abspath``+``normpath`` misses case-variant
117
+ launches (Windows, default macOS volumes) and symlinked routes; a
118
+ miss only costs a demotion, but it is cheap to avoid.
119
+ """
120
+ try:
121
+ real = os.path.realpath(path)
122
+ except OSError:
123
+ real = os.path.abspath(path)
124
+ return fold_case(real)
125
+
126
+
127
+ def resolve_tree_root(path: str) -> str | None:
128
+ """Nearest ancestor whose ``.git`` exists — directory *or* file.
129
+
130
+ File paths are normalized to their containing directory before the
131
+ ancestor walk. Linked worktrees/submodules pair up by their own
132
+ ``.git`` file, so they stay separate from their parent repository.
133
+ """
134
+ current = os.path.abspath(path)
135
+ if not os.path.isdir(current):
136
+ current = os.path.dirname(current)
137
+ while True:
138
+ if os.path.exists(os.path.join(current, ".git")):
139
+ return current
140
+ parent = os.path.dirname(current)
141
+ if parent == current:
142
+ return None
143
+ current = parent
144
+
145
+
146
+ def repo_key_for_root(tree_root: str) -> str:
147
+ """First 16 hex chars of sha256 over the canonicalized tree root."""
148
+ return hashlib.sha256(canonical_key(tree_root).encode("utf-8")).hexdigest()[:16]
149
+
150
+
151
+ def _is_pid_alive(pid: int) -> bool:
152
+ """Cross-platform liveness probe.
153
+
154
+ A peer whose recorded pid is no longer alive counts as closed —
155
+ crash leftovers must not warn until GC.
156
+ """
157
+ if pid <= 0:
158
+ return False
159
+ import psutil
160
+
161
+ try:
162
+ return psutil.pid_exists(pid)
163
+ except Exception: # pragma: no cover - psutil failure → assume alive (conservative)
164
+ return True
165
+
166
+
167
+ def _strict_overlap(a0: float, a1: float, b0: float, b1: float) -> bool:
168
+ """Strict interval overlap — no slack, touching endpoints don't pair.
169
+
170
+ Both intervals are conservative supersets of the true write instant
171
+ taken from the same machine's wall clock, so a genuinely in-window
172
+ peer write always overlaps strictly; slack could only manufacture
173
+ pairings that never happened.
174
+ """
175
+ return a0 < b1 and b0 < a1
176
+
177
+
178
+ # ---------------------------------------------------------------------------
179
+ # Registry records
180
+ # ---------------------------------------------------------------------------
181
+
182
+
183
+ @dataclass(frozen=True)
184
+ class MutationClaim:
185
+ """One published proven write: path identity, after-state, write interval.
186
+
187
+ ``key``/``old_key`` are canonical (:func:`canonical_key`) forms
188
+ carried alongside the display paths. ``after_exists`` is skip-aware,
189
+ mirroring ``FileMutation``: it distinguishes deletion from a
190
+ policy-skipped backup — both read ``after_hash: null``.
191
+ ``[t0, t1]`` bounds when the disk write actually occurred.
192
+ """
193
+
194
+ path: str
195
+ key: str
196
+ after_hash: str | None
197
+ after_exists: bool
198
+ t0: float
199
+ t1: float
200
+ source: str = ""
201
+ old_path: str | None = None
202
+ old_key: str | None = None
203
+
204
+ def to_dict(self) -> dict[str, Any]:
205
+ d: dict[str, Any] = {
206
+ "path": self.path,
207
+ "key": self.key,
208
+ "after_hash": self.after_hash,
209
+ "after_exists": self.after_exists,
210
+ "t0": self.t0,
211
+ "t1": self.t1,
212
+ "source": self.source,
213
+ }
214
+ if self.old_path is not None:
215
+ d["old_path"] = self.old_path
216
+ if self.old_key is not None:
217
+ d["old_key"] = self.old_key
218
+ return d
219
+
220
+ @classmethod
221
+ def from_dict(cls, d: dict[str, Any]) -> MutationClaim:
222
+ return cls(
223
+ path=str(d["path"]),
224
+ key=str(d.get("key") or canonical_key(str(d["path"]))),
225
+ after_hash=d.get("after_hash"),
226
+ after_exists=bool(d.get("after_exists", d.get("after_hash") is not None)),
227
+ t0=float(d["t0"]),
228
+ t1=float(d["t1"]),
229
+ source=str(d.get("source", "")),
230
+ old_path=d.get("old_path"),
231
+ old_key=d.get("old_key"),
232
+ )
233
+
234
+
235
+ @dataclass
236
+ class ObservationWindow:
237
+ """One advertised observation window (shell / implicit tool call).
238
+
239
+ Opened in ``prepare_mutation_tracking`` (``end: None``) *before* any
240
+ observation (git capture, pre-scan, snapshots) so the advertised
241
+ window is a superset of the observation; closed in finalize. Peers
242
+ use still-open windows for the in-flight rollback warning.
243
+ """
244
+
245
+ id: str
246
+ scope: list[str]
247
+ start: float
248
+ end: float | None = None
249
+
250
+ def to_dict(self) -> dict[str, Any]:
251
+ return {"id": self.id, "scope": list(self.scope), "start": self.start, "end": self.end}
252
+
253
+ @classmethod
254
+ def from_dict(cls, d: dict[str, Any]) -> ObservationWindow:
255
+ return cls(
256
+ id=str(d.get("id", "")),
257
+ scope=[str(s) for s in d.get("scope", [])],
258
+ start=float(d["start"]),
259
+ end=None if d.get("end") is None else float(d["end"]),
260
+ )
261
+
262
+
263
+ @dataclass
264
+ class PeerRegistry:
265
+ """Parsed peer registry file (tolerant read)."""
266
+
267
+ session_id: str
268
+ pid: int
269
+ closed_at: float | None
270
+ windows: list[ObservationWindow]
271
+ claims: list[MutationClaim]
272
+
273
+ @property
274
+ def is_active(self) -> bool:
275
+ """Open for business: not cleanly closed AND its process is alive."""
276
+ return self.closed_at is None and _is_pid_alive(self.pid)
277
+
278
+
279
+ def parse_registry_payload(payload: str) -> PeerRegistry | None:
280
+ """Parse one registry file's text; ``None`` on any structural problem.
281
+
282
+ Readers must tolerate missing/partial/undecodable files by skipping
283
+ them — a peer may be mid-replace or of a future schema.
284
+ """
285
+ try:
286
+ data = json.loads(payload)
287
+ if not isinstance(data, dict) or int(data.get("schema", 0)) != REGISTRY_SCHEMA:
288
+ return None
289
+ closed_raw = data.get("closed_at")
290
+ windows = [ObservationWindow.from_dict(w) for w in data.get("windows", [])]
291
+ claims = [MutationClaim.from_dict(c) for c in data.get("claims", [])]
292
+ return PeerRegistry(
293
+ session_id=str(data.get("session_id", "")),
294
+ pid=int(data.get("pid", 0)),
295
+ closed_at=None if closed_raw is None else float(closed_raw),
296
+ windows=windows,
297
+ claims=claims,
298
+ )
299
+ except Exception:
300
+ return None
301
+
302
+
303
+ @dataclass
304
+ class _TreeState:
305
+ """In-memory single-writer state for one (tree, session) file."""
306
+
307
+ windows: dict[str, ObservationWindow] = field(default_factory=dict)
308
+ claims: list[MutationClaim] = field(default_factory=list)
309
+ loaded_existing: bool = False
310
+
311
+
312
+ # ---------------------------------------------------------------------------
313
+ # Coordinator
314
+ # ---------------------------------------------------------------------------
315
+
316
+
317
+ class MutationCoordinator:
318
+ """Owns cross-session attribution for one session.
319
+
320
+ Built alongside the :class:`MutationTracker` at engine construction
321
+ (it needs session identity and settings the tracker never sees);
322
+ the tracker itself stays coordination-unaware. All methods are
323
+ thread-safe (pipeline calls arrive from executor threads) and all
324
+ registry I/O failures degrade to a no-op with a debug log.
325
+ """
326
+
327
+ def __init__(
328
+ self,
329
+ *,
330
+ registry_root: str | Path,
331
+ session_id: str,
332
+ enabled: bool = True,
333
+ pid: int | None = None,
334
+ now: Callable[[], float] = time.time,
335
+ max_claims: int = MAX_CLAIMS,
336
+ gc_age_seconds: float = REGISTRY_GC_AGE_SECONDS,
337
+ ) -> None:
338
+ self._root = Path(registry_root)
339
+ self._session_id = session_id
340
+ self._enabled = enabled
341
+ self._pid = os.getpid() if pid is None else pid
342
+ self._now = now
343
+ self._max_claims = max_claims
344
+ self._gc_age = gc_age_seconds
345
+ self._lock = threading.Lock()
346
+ # repo_key -> single-writer state (this session's own file).
347
+ self._states: dict[str, _TreeState] = {}
348
+ # window id -> repo_keys the window was published under.
349
+ self._window_trees: dict[str, list[str]] = {}
350
+ # Directory (canonical) -> resolved tree root; cheap memo for the
351
+ # hot path (every mutation path resolves its tree on publish and
352
+ # reclassify).
353
+ self._tree_cache: dict[str, str | None] = {}
354
+ # Short-circuit signatures for reclassify: peer-dir signature per
355
+ # repo_key + local log signature at the last completed pass.
356
+ self._last_peer_sig: dict[str, tuple] = {}
357
+ self._last_local_sig: tuple | None = None
358
+ self._unsaved_reclassification = False
359
+ self._closed_at: float | None = None
360
+
361
+ @property
362
+ def enabled(self) -> bool:
363
+ return self._enabled
364
+
365
+ # -- Tree routing -------------------------------------------------
366
+
367
+ def _tree_root_for(self, path: str, fallback_root: str | None) -> str | None:
368
+ # Window scope paths are directories; claim paths are files.
369
+ absolute = os.path.abspath(path)
370
+ directory = absolute if os.path.isdir(absolute) else (os.path.dirname(absolute) or absolute)
371
+ cache_key = os.path.normcase(directory)
372
+ if cache_key not in self._tree_cache:
373
+ self._tree_cache[cache_key] = resolve_tree_root(directory)
374
+ root = self._tree_cache[cache_key]
375
+ if root is not None:
376
+ return root
377
+ # Non-git paths key by their OWN directory, never the caller's
378
+ # fallback root: the registry key must be a pure function of
379
+ # the path — sessions with different primary workspaces touching
380
+ # the same untracked directory must land in the same tree or
381
+ # they never see each other's claims. ``fallback_root`` only
382
+ # gates WHETHER untracked paths participate (None → the caller
383
+ # has no workspace context → they don't).
384
+ if fallback_root:
385
+ return directory
386
+ return None
387
+
388
+ def _repo_keys_for_paths(self, paths: Iterable[str], fallback_root: str | None) -> dict[str, str]:
389
+ """Map repo_key -> tree root for every resolvable path."""
390
+ keys: dict[str, str] = {}
391
+ for p in paths:
392
+ root = self._tree_root_for(p, fallback_root)
393
+ if root is not None:
394
+ keys.setdefault(repo_key_for_root(root), root)
395
+ return keys
396
+
397
+ # -- Own-file serialization ----------------------------------------
398
+
399
+ def _tree_dir(self, repo_key: str) -> Path:
400
+ return self._root / repo_key
401
+
402
+ def _own_file(self, repo_key: str) -> Path:
403
+ return self._tree_dir(repo_key) / f"{self._session_id}.json"
404
+
405
+ def _state_for(self, repo_key: str) -> _TreeState:
406
+ """Get (lazily creating) the in-memory state for one tree.
407
+
408
+ On first touch, merge any existing file left by a previous
409
+ process of this session (resume): claims are kept — peers'
410
+ late rollbacks still need them — and stale open windows are
411
+ closed at the file's mtime (the last instant that process
412
+ provably lived); the dead pid would already read as closed, but
413
+ the file is about to be re-stamped with *our* live pid.
414
+ """
415
+ state = self._states.get(repo_key)
416
+ if state is not None:
417
+ return state
418
+ state = _TreeState()
419
+ own = self._own_file(repo_key)
420
+ try:
421
+ if own.exists():
422
+ stamp = own.stat().st_mtime
423
+ prior = parse_registry_payload(own.read_text(encoding="utf-8"))
424
+ if prior is not None:
425
+ state.claims = prior.claims[-self._max_claims :]
426
+ for w in prior.windows:
427
+ if w.end is None:
428
+ w.end = max(w.start, stamp)
429
+ state.windows[w.id] = w
430
+ state.loaded_existing = True
431
+ except OSError:
432
+ logger.debug("Coordination: could not merge prior registry file %s", own, exc_info=True)
433
+ self._states[repo_key] = state
434
+ return state
435
+
436
+ def _write_state(self, repo_key: str) -> None:
437
+ state = self._state_for(repo_key)
438
+ payload = {
439
+ "session_id": self._session_id,
440
+ "pid": self._pid,
441
+ "schema": REGISTRY_SCHEMA,
442
+ # Sticky close stamp: a straggler racing close() (an
443
+ # in-flight finalize's close_window / publish_claims) must
444
+ # not revive the file as open — close() itself never
445
+ # re-stamps (idempotence guard), so the stamp would be lost
446
+ # for good and peers would read a live pid as "active".
447
+ "closed_at": self._closed_at,
448
+ "windows": [w.to_dict() for w in state.windows.values()],
449
+ "claims": [c.to_dict() for c in state.claims],
450
+ }
451
+ try:
452
+ self._tree_dir(repo_key).mkdir(parents=True, exist_ok=True)
453
+ atomic_write_text(self._own_file(repo_key), json.dumps(payload))
454
+ except OSError:
455
+ logger.debug("Coordination: registry write failed for %s", repo_key, exc_info=True)
456
+ return
457
+ self._gc_tree(repo_key)
458
+
459
+ def _gc_tree(self, repo_key: str) -> None:
460
+ """Opportunistic on-write GC: drop peer files older than the age cap."""
461
+ cutoff = self._now() - self._gc_age
462
+ own_name = f"{self._session_id}.json"
463
+ try:
464
+ with os.scandir(self._tree_dir(repo_key)) as it:
465
+ for entry in it:
466
+ if entry.name == own_name or not entry.name.endswith(".json"):
467
+ continue
468
+ with contextlib.suppress(OSError):
469
+ if entry.stat().st_mtime < cutoff:
470
+ os.unlink(entry.path)
471
+ except OSError:
472
+ pass
473
+
474
+ # -- Write side ------------------------------------------------------
475
+
476
+ def open_window(self, scope_paths: list[str], *, fallback_root: str | None = None) -> str | None:
477
+ """Advertise an observation window; returns its id (None when disabled).
478
+
479
+ Must be called as the *first* operation of an observation
480
+ (before git capture, shell pre-scan, or any snapshot) so the
481
+ advertised window is a superset — a peer write in a pre-scan
482
+ gap would otherwise be attributed by the diff yet never overlap
483
+ our interval.
484
+ """
485
+ if not self._enabled or not scope_paths:
486
+ return None
487
+ window = ObservationWindow(
488
+ id=uuid.uuid4().hex, scope=[os.path.abspath(p) for p in scope_paths], start=self._now()
489
+ )
490
+ with self._lock:
491
+ keys = self._repo_keys_for_paths(scope_paths, fallback_root)
492
+ if not keys:
493
+ return None
494
+ self._window_trees[window.id] = list(keys)
495
+ for repo_key in keys:
496
+ self._state_for(repo_key).windows[window.id] = window
497
+ self._write_state(repo_key)
498
+ return window.id
499
+
500
+ def close_window(self, window_id: str | None) -> None:
501
+ """Stamp the window's end and republish the affected tree files."""
502
+ if not self._enabled or not window_id:
503
+ return
504
+ with self._lock:
505
+ end = self._now()
506
+ for repo_key in self._window_trees.pop(window_id, []):
507
+ window = self._states[repo_key].windows.get(window_id)
508
+ if window is not None:
509
+ window.end = end
510
+ self._write_state(repo_key)
511
+
512
+ def publish_claims(self, mutations: list[FileMutation], *, fallback_root: str | None = None) -> int:
513
+ """Publish claims for the eligible subset of ``mutations``.
514
+
515
+ Eligibility is decided here for the *evidence* conditions
516
+ (proven provenance, provable net change); the *authorship*
517
+ condition — did the tool actually execute and succeed for
518
+ write/edit — is the caller's to enforce by not passing such
519
+ mutations at all (the pipeline sees the tool result; we don't).
520
+ Over-publishing falsely poisons peers' rollbacks via contested
521
+ marks, while under-publishing only costs missed demotions.
522
+ """
523
+ if not self._enabled or not mutations:
524
+ return 0
525
+ eligible = [m for m in mutations if self._claim_eligible(m)]
526
+ if not eligible:
527
+ return 0
528
+ published = 0
529
+ with self._lock:
530
+ touched: set[str] = set()
531
+ for m in eligible:
532
+ t0 = m.t_start if m.t_start is not None else m.timestamp
533
+ t1 = m.t_end if m.t_end is not None else self._now()
534
+ claim = MutationClaim(
535
+ path=m.path,
536
+ key=canonical_key(m.path),
537
+ after_hash=m.after_hash,
538
+ after_exists=m.after_hash is not None or m.after_skip is not None,
539
+ t0=t0,
540
+ t1=t1,
541
+ source=m.source.value,
542
+ old_path=m.old_path,
543
+ old_key=canonical_key(m.old_path) if m.old_path else None,
544
+ )
545
+ # A MOVE names two paths, but a same-repo rename (the
546
+ # common case) resolves both to one tree — dedupe or the
547
+ # claim lands twice and burns MAX_CLAIMS double.
548
+ repo_keys: set[str] = set()
549
+ for path in (m.path, m.old_path) if m.old_path else (m.path,):
550
+ root = self._tree_root_for(path, fallback_root)
551
+ if root is not None:
552
+ repo_keys.add(repo_key_for_root(root))
553
+ for repo_key in sorted(repo_keys):
554
+ state = self._state_for(repo_key)
555
+ state.claims.append(claim)
556
+ if len(state.claims) > self._max_claims:
557
+ # Bounded by count, not age — FIFO.
558
+ del state.claims[: len(state.claims) - self._max_claims]
559
+ touched.add(repo_key)
560
+ published += 1
561
+ for repo_key in touched:
562
+ self._write_state(repo_key)
563
+ return published
564
+
565
+ @staticmethod
566
+ def _claim_eligible(m: FileMutation) -> bool:
567
+ """Evidence test: proven provenance + provable net change.
568
+
569
+ The change test is the same skip-aware rule as ``is_net_zero``:
570
+ hash inequality or existence flip. A fully-skipped MODIFY
571
+ proves nothing and publishes nothing. ``ASSUMED`` inferences
572
+ are never published.
573
+ """
574
+ if m.provenance is not MutationProvenance.PROVEN:
575
+ return False
576
+ before_exists = m.before_hash is not None or m.before_skip is not None
577
+ after_exists = m.after_hash is not None or m.after_skip is not None
578
+ return m.before_hash != m.after_hash or before_exists != after_exists
579
+
580
+ def close(self) -> None:
581
+ """Stamp ``closed_at`` on every tree file this session wrote.
582
+
583
+ Idempotent; files are never deleted here (peers' late rollbacks
584
+ still need the claims — removal is GC's job).
585
+ """
586
+ if not self._enabled or self._closed_at is not None:
587
+ return
588
+ with self._lock:
589
+ if self._closed_at is not None:
590
+ return
591
+ end = self._now()
592
+ self._closed_at = end
593
+ for repo_key, state in self._states.items():
594
+ for window in state.windows.values():
595
+ if window.end is None:
596
+ window.end = end
597
+ self._write_state(repo_key)
598
+
599
+ # -- Read side -------------------------------------------------------
600
+
601
+ def _peer_dir_signature(self, repo_key: str) -> tuple:
602
+ """Cheap change signature over peer files in one tree dir."""
603
+ own_name = f"{self._session_id}.json"
604
+ entries: list[tuple[str, int, int]] = []
605
+ try:
606
+ with os.scandir(self._tree_dir(repo_key)) as it:
607
+ for entry in it:
608
+ if entry.name == own_name or not entry.name.endswith(".json"):
609
+ continue
610
+ with contextlib.suppress(OSError):
611
+ st = entry.stat()
612
+ entries.append((entry.name, st.st_mtime_ns, st.st_size))
613
+ except OSError:
614
+ return ()
615
+ entries.sort()
616
+ return tuple(entries)
617
+
618
+ def _read_peers(self, repo_key: str) -> list[PeerRegistry]:
619
+ own_name = f"{self._session_id}.json"
620
+ peers: list[PeerRegistry] = []
621
+ try:
622
+ with os.scandir(self._tree_dir(repo_key)) as it:
623
+ names = [e.name for e in it if e.name.endswith(".json") and e.name != own_name]
624
+ except OSError:
625
+ return []
626
+ for name in names:
627
+ try:
628
+ payload = (self._tree_dir(repo_key) / name).read_text(encoding="utf-8")
629
+ except OSError:
630
+ continue
631
+ peer = parse_registry_payload(payload)
632
+ if peer is not None and peer.session_id != self._session_id:
633
+ peers.append(peer)
634
+ return peers
635
+
636
+ @staticmethod
637
+ def _local_log_signature(log: MutationLog) -> tuple:
638
+ # The completed-interval count is load-bearing: file-tool rows
639
+ # exist from record() time with an OPEN interval (t_end unset)
640
+ # and only become matchable when finalize stamps t_end. A read
641
+ # refresh (/diff, ACP) landing mid-call would otherwise cache a
642
+ # signature the t_end stamp cannot change — turn/row counts stay
643
+ # identical — and the finalize reclassify would short-circuit
644
+ # past the completed row forever.
645
+ rows = 0
646
+ complete = 0
647
+ for turn in log.periods:
648
+ rows += len(turn.mutations)
649
+ for m in turn.mutations:
650
+ if m.t_end is not None:
651
+ complete += 1
652
+ return (len(log.periods), rows, complete)
653
+
654
+ def reclassify(self, tracker: MutationTracker, *, force: bool = False, fallback_root: str | None = None) -> bool:
655
+ """Retroactively re-attribute the tracker's log against peer claims.
656
+
657
+ Idempotent; safe to run at every opportunity. Returns True when
658
+ any row changed (caller persists via the normal session-state
659
+ save). ``force=True`` bypasses the no-change short-circuit —
660
+ used at rollback-plan construction, the authoritative last check
661
+ before the only destructive consumer acts.
662
+ """
663
+ if not self._enabled:
664
+ return False
665
+
666
+ # Collect the candidate rows and their trees under the tracker
667
+ # lock; read peer files outside it; apply matches under it again.
668
+ def _collect(log: MutationLog) -> tuple[tuple, list[str]]:
669
+ paths: list[str] = []
670
+ for turn in log.periods:
671
+ for m in turn.mutations:
672
+ if m.provenance is MutationProvenance.FOREIGN:
673
+ continue
674
+ if m.t_start is None or m.t_end is None:
675
+ continue # legacy row — no interval, never matches
676
+ paths.append(m.path)
677
+ if m.old_path:
678
+ paths.append(m.old_path)
679
+ return self._local_log_signature(log), paths
680
+
681
+ local_sig, paths = tracker.with_locked_log(_collect)
682
+ if not paths:
683
+ return False
684
+
685
+ with self._lock:
686
+ repo_keys = self._repo_keys_for_paths(paths, fallback_root)
687
+ peer_sigs = {k: self._peer_dir_signature(k) for k in repo_keys}
688
+ if (
689
+ not force
690
+ and self._last_local_sig == local_sig
691
+ and all(self._last_peer_sig.get(k) == sig for k, sig in peer_sigs.items())
692
+ ):
693
+ return False
694
+
695
+ claims_by_key: dict[str, list[MutationClaim]] = {}
696
+ for repo_key in repo_keys:
697
+ for peer in self._read_peers(repo_key):
698
+ for claim in peer.claims:
699
+ claims_by_key.setdefault(claim.key, []).append(claim)
700
+ if claim.old_key:
701
+ claims_by_key.setdefault(claim.old_key, []).append(claim)
702
+
703
+ changed = False
704
+ if claims_by_key:
705
+ key_cache: dict[str, str] = {}
706
+
707
+ def _key(p: str) -> str:
708
+ if p not in key_cache:
709
+ key_cache[p] = canonical_key(p)
710
+ return key_cache[p]
711
+
712
+ def _apply(log: MutationLog) -> bool:
713
+ any_change = False
714
+ for turn in log.periods:
715
+ for m in turn.mutations:
716
+ if m.provenance is MutationProvenance.FOREIGN:
717
+ continue
718
+ if m.t_start is None or m.t_end is None:
719
+ continue
720
+ candidates = list(claims_by_key.get(_key(m.path), ()))
721
+ if m.old_path:
722
+ candidates += claims_by_key.get(_key(m.old_path), ())
723
+ for claim in candidates:
724
+ if not _strict_overlap(m.t_start, m.t_end, claim.t0, claim.t1):
725
+ continue
726
+ if (
727
+ m.provenance is MutationProvenance.ASSUMED
728
+ and m.old_path is None
729
+ # MOVEs never demote — EITHER side. A peer
730
+ # MOVE claim is indexed under both key and
731
+ # old_key, so a local simple row on the move
732
+ # destination can carry identical bytes; the
733
+ # rename still isn't proof the peer authored
734
+ # our row's content. Fall to contested.
735
+ and claim.old_path is None
736
+ and self._content_identity(m, claim)
737
+ ):
738
+ # Foreign by policy: identity proves the peer
739
+ # wrote these bytes; preferring under-claiming
740
+ # our own work over clobbering theirs.
741
+ m.provenance = MutationProvenance.FOREIGN
742
+ any_change = True
743
+ break
744
+ if not m.contested:
745
+ m.contested = True
746
+ any_change = True
747
+ return any_change
748
+
749
+ changed = tracker.with_locked_log(_apply)
750
+
751
+ with self._lock:
752
+ self._last_local_sig = tracker.with_locked_log(self._local_log_signature)
753
+ self._last_peer_sig.update(peer_sigs)
754
+ if changed:
755
+ # The signature update above makes this change invisible
756
+ # to the next non-forced call — remember that no one has
757
+ # persisted it yet (see consume_unsaved_reclassification).
758
+ self._unsaved_reclassification = True
759
+ return changed
760
+
761
+ def consume_unsaved_reclassification(self) -> bool:
762
+ """Return-and-clear the "rows changed, nobody saved yet" flag.
763
+
764
+ ``reclassify`` updates its short-circuit signatures as it
765
+ applies changes, so a later refresh that itself changes nothing
766
+ cannot re-discover that the serialized session is behind — a
767
+ finalize-time reclassification (which has no saver) would leave
768
+ detached readers of ``session.json`` stale forever. The
769
+ engine's refresh entry point (the persister) consumes this
770
+ instead of trusting only its own call's return value.
771
+ """
772
+ with self._lock:
773
+ dirty = self._unsaved_reclassification
774
+ self._unsaved_reclassification = False
775
+ return dirty
776
+
777
+ @staticmethod
778
+ def _content_identity(m: FileMutation, claim: MutationClaim) -> bool:
779
+ """Positive evidence the resulting bytes are the peer's own write.
780
+
781
+ Hashes present and equal, or both sides gone. Skip-marked sides
782
+ (no bytes to compare) never qualify.
783
+ """
784
+ if m.after_hash is not None and claim.after_hash is not None:
785
+ return m.after_hash == claim.after_hash
786
+ local_gone = m.after_hash is None and m.after_skip is None
787
+ return local_gone and not claim.after_exists
788
+
789
+ # -- Rollback augmentation --------------------------------------------
790
+
791
+ def augment_rollback_plan(
792
+ self,
793
+ tracker: MutationTracker,
794
+ plan: RollbackPlan,
795
+ *,
796
+ scope_paths: list[str] | None = None,
797
+ fallback_root: str | None = None,
798
+ ) -> RollbackPlan:
799
+ """Final peer check on a freshly built plan.
800
+
801
+ - A path carrying a peer claim strictly *newer* than our last
802
+ write on it is excluded (``PEER_MODIFIED_SINCE``): restoring
803
+ our older snapshot would clobber the peer's newer write, and
804
+ warning-only would let non-modal rollback paths proceed
805
+ straight through the data loss. (Claims *overlapping* our
806
+ writes were already handled by reclassification — the caller
807
+ runs ``reclassify(force=True)`` before building the plan.)
808
+ - An active peer (not ``closed_at``, live pid) with a still-open
809
+ window whose scope overlaps ours produces a repo-level
810
+ :class:`RollbackWarning` — covering the one case attribution
811
+ cannot: writes on disk whose claims don't exist yet.
812
+ """
813
+ if not self._enabled or (not plan.entries and not plan.exclusions):
814
+ return plan
815
+
816
+ # Last local write end per plan path (from the log, under lock).
817
+ entry_paths = [path for path, _snap in plan.entries]
818
+ wanted = set(entry_paths)
819
+
820
+ def _last_writes(log: MutationLog) -> dict[str, float]:
821
+ last: dict[str, float] = {}
822
+ for turn in log.periods:
823
+ for m in turn.mutations:
824
+ if m.provenance is MutationProvenance.FOREIGN:
825
+ continue
826
+ end = m.t_end if m.t_end is not None else m.timestamp
827
+ for p in (m.path, m.old_path) if m.old_path else (m.path,):
828
+ if p in wanted and end > last.get(p, float("-inf")):
829
+ last[p] = end
830
+ return last
831
+
832
+ last_write = tracker.with_locked_log(_last_writes)
833
+
834
+ reference_paths = list(entry_paths) + [path for path, _reason in plan.exclusions] + list(scope_paths or [])
835
+ with self._lock:
836
+ repo_keys = self._repo_keys_for_paths(reference_paths, fallback_root)
837
+ peers = [peer for repo_key in repo_keys for peer in self._read_peers(repo_key)]
838
+
839
+ claims_by_key: dict[str, list[MutationClaim]] = {}
840
+ for peer in peers:
841
+ for claim in peer.claims:
842
+ claims_by_key.setdefault(claim.key, []).append(claim)
843
+ if claim.old_key:
844
+ claims_by_key.setdefault(claim.old_key, []).append(claim)
845
+
846
+ kept: list[tuple[str, Any]] = []
847
+ added_exclusions: list[tuple[str, RollbackExclusionReason]] = []
848
+ for path, snap in plan.entries:
849
+ claims = claims_by_key.get(canonical_key(path), ())
850
+ threshold = last_write.get(path, float("-inf"))
851
+ if any(c.t0 > threshold for c in claims):
852
+ added_exclusions.append((path, RollbackExclusionReason.PEER_MODIFIED_SINCE))
853
+ else:
854
+ kept.append((path, snap))
855
+
856
+ warnings = list(plan.warnings)
857
+ our_scope = [canonical_key(p) for p in reference_paths]
858
+ warned_sessions: set[str] = set()
859
+ for peer in peers:
860
+ if not peer.is_active or peer.session_id in warned_sessions:
861
+ continue
862
+ for window in peer.windows:
863
+ if window.end is None and self._scopes_overlap(window.scope, our_scope):
864
+ warned_sessions.add(peer.session_id)
865
+ warnings.append(
866
+ RollbackWarning(
867
+ code=PEER_ACTIVE_WINDOW_WARNING,
868
+ message=(
869
+ f"Another {APP_DISPLAY_NAME} session has an active command in this "
870
+ "workspace; its changes may not be attributed yet."
871
+ ),
872
+ )
873
+ )
874
+ break
875
+
876
+ if not added_exclusions and len(warnings) == len(plan.warnings):
877
+ return plan
878
+ return RollbackPlan(
879
+ entries=kept,
880
+ exclusions=sorted([*plan.exclusions, *added_exclusions], key=lambda item: item[0]),
881
+ warnings=warnings,
882
+ )
883
+
884
+ @staticmethod
885
+ def _scopes_overlap(peer_scope: list[str], our_scope_keys: list[str]) -> bool:
886
+ """Ancestor-or-equal containment in either direction, canonically."""
887
+ for raw in peer_scope:
888
+ p = canonical_key(raw)
889
+ for q in our_scope_keys:
890
+ if p == q:
891
+ return True
892
+ if q.startswith(p.rstrip(os.sep) + os.sep) or p.startswith(q.rstrip(os.sep) + os.sep):
893
+ return True
894
+ return False