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,1952 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """Centralized configuration for Chrys."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ import math
9
+ import os
10
+ import tempfile
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+ from typing import Any, Final, Literal
14
+
15
+ from chrys.foundation.config.coercion import (
16
+ MISSING,
17
+ Coerced,
18
+ Coercer,
19
+ CoerceReason,
20
+ CoerceStatus,
21
+ bool_coercer,
22
+ choice_coercer,
23
+ float_coercer,
24
+ int_coercer,
25
+ invalid,
26
+ optional_int_coercer,
27
+ text_coercer,
28
+ )
29
+ from chrys.foundation.config.context import EvalContext
30
+ from chrys.foundation.config.spec import (
31
+ Apply,
32
+ ChoiceProvider,
33
+ InvalidPolicy,
34
+ Kind,
35
+ ProjectMerge,
36
+ Risk,
37
+ spec,
38
+ )
39
+ from chrys.foundation.config.web_values import (
40
+ web_custom_endpoints_coercer,
41
+ web_mode_coercer,
42
+ web_origins_coercer,
43
+ web_proxy_url_coercer,
44
+ )
45
+ from chrys.foundation.i18n import msg
46
+ from chrys.foundation.i18n.locale import SUPPORTED_LOCALES
47
+
48
+ logger = logging.getLogger(__name__)
49
+
50
+ DEFAULT_THEME = "chrys"
51
+ """Fallback theme name when ``CHRYS_THEME`` is empty, missing, or invalid."""
52
+
53
+ RENAMED_THEMES: Final[dict[str, str]] = {"chrys-dark": "chrys"}
54
+ """Built-in theme names that are gone, and the name each of them goes by now.
55
+
56
+ The ``ui.theme`` coercer follows this at every layer, so a retired name in an
57
+ environment variable or a project file still selects the theme it meant. A
58
+ retired name saved in the user document is also rewritten there, by
59
+ :func:`chrys.foundation.config.migrations.migrate_renamed_theme`. The old names
60
+ stay reserved: a user theme taking one would be renamed away on the next start.
61
+ """
62
+
63
+ DEFAULT_LOCALE = "system"
64
+ """Fallback locale selector when ``CHRYS_LOCALE`` is empty or missing."""
65
+
66
+ DEFAULT_APPROVAL_MODE = "manual"
67
+ """Fallback approval mode when ``CHRYS_DEFAULT_APPROVAL_MODE`` is empty, missing, or invalid.
68
+
69
+ Valid values are ``manual``, ``auto``, ``bypass``. The persistence helper
70
+ (:func:`persist_approval_mode`) deliberately downgrades ``bypass`` to ``auto``
71
+ so that Chrys never launches in BYPASS mode — that mode is opt-in per launch:
72
+ once chosen it stays in force for every session until the app exits.
73
+ """
74
+
75
+ DEFAULT_AGENT_PROFILE = "Code"
76
+ """Fallback TUI launch agent profile when ``CHRYS_DEFAULT_AGENT`` is empty or unusable."""
77
+
78
+ DEFAULT_EDITOR_KEYMAP = "standard"
79
+ """Fallback message-editor keymap when its persisted value is missing or invalid."""
80
+
81
+ SESSION_ROOT_DIR_ENV_VAR: Final[str] = "CHRYS_SESSION_ROOT_DIR"
82
+ """Base directory for session storage. The actual session store lives under ``sessions/`` within it."""
83
+
84
+ _VALID_APPROVAL_MODES = ("manual", "auto", "bypass")
85
+ _VALID_EDITOR_KEYMAPS = ("standard", "emacs", "vim")
86
+
87
+ _TRUTHY_ENV = {"1", "true", "yes", "on"}
88
+
89
+ _RESOLVED_SESSIONS_DIR_CACHE: dict[str, Path] = {}
90
+
91
+
92
+ def _load_default_approval_mode() -> str:
93
+ """Read ``CHRYS_DEFAULT_APPROVAL_MODE`` from env, fall back on invalid/missing."""
94
+ raw = os.environ.get("CHRYS_DEFAULT_APPROVAL_MODE", "").strip().lower()
95
+ return raw if raw in _VALID_APPROVAL_MODES else DEFAULT_APPROVAL_MODE
96
+
97
+
98
+ def _load_default_agent() -> str:
99
+ """Read ``CHRYS_DEFAULT_AGENT`` from env, falling back to the built-in Code profile."""
100
+ return os.environ.get("CHRYS_DEFAULT_AGENT", "").strip() or DEFAULT_AGENT_PROFILE
101
+
102
+
103
+ def _load_editor_keymap() -> str:
104
+ """Read and normalize ``CHRYS_EDITOR_KEYMAP`` with a Standard fallback."""
105
+ raw = os.environ.get("CHRYS_EDITOR_KEYMAP", "").strip().lower()
106
+ return raw if raw in _VALID_EDITOR_KEYMAPS else DEFAULT_EDITOR_KEYMAP
107
+
108
+
109
+ def parse_bool_env_value(raw: str | None, *, default: bool = False) -> bool:
110
+ """Parse one environment-style boolean with Chrys's shared semantics."""
111
+ normalized = (raw or "").strip().lower()
112
+ if not normalized:
113
+ return default
114
+ return normalized in _TRUTHY_ENV
115
+
116
+
117
+ def _load_bool_env(name: str, *, default: bool = False) -> bool:
118
+ """Parse ``name`` as a boolean (1/true/yes/on, case-insensitive)."""
119
+ return parse_bool_env_value(os.environ.get(name), default=default)
120
+
121
+
122
+ DEFAULT_ASK_USER_TIMEOUT_SECONDS = 900
123
+ """Default ``ask_user`` reply wait (15 minutes) when ``CHRYS_ASK_USER_TIMEOUT_SECONDS`` is unset."""
124
+
125
+ DEFAULT_MAX_TRANSIENT_RETRIES = 10
126
+ """Default application-layer transient retry budget for interactive frontends."""
127
+
128
+ HEADLESS_DEFAULT_MAX_TRANSIENT_RETRIES = 18
129
+ """Default application-layer transient retry budget for ``chrys run``."""
130
+
131
+ MAX_TRANSIENT_RETRIES_LIMIT = 50
132
+ """Upper bound for ``CHRYS_MAX_TRANSIENT_RETRIES``."""
133
+
134
+
135
+ @dataclass(frozen=True, slots=True)
136
+ class MaxTransientRetriesWarning:
137
+ """Structured warning emitted while parsing the transient retry budget."""
138
+
139
+ variant: Literal["invalid", "clamped"]
140
+ raw: str
141
+ value: int | None
142
+ limit: int
143
+
144
+
145
+ DEFAULT_TOOL_RESULT_CEILING_TOKENS = 64_000
146
+ """Default kernel backstop for one local tool result."""
147
+
148
+ TOOL_RESULT_CEILING_FLOOR = 2_000
149
+ """Minimum positive ``CHRYS_TOOL_RESULT_CEILING_TOKENS`` value."""
150
+
151
+ DEFAULT_WORKSPACE_MRU_MAX_ENTRIES = 20
152
+ """Default number of recently used workspace directories kept in the MRU index."""
153
+
154
+ WORKSPACE_MRU_MAX_ENTRIES_LIMIT = 100
155
+ """Upper clamp for ``CHRYS_WORKSPACE_MRU_MAX_ENTRIES`` so quick-access lists stay bounded."""
156
+
157
+
158
+ def _load_workspace_mru_max_entries() -> int:
159
+ """Read ``CHRYS_WORKSPACE_MRU_MAX_ENTRIES``; garbage → default, non-positive → 0 (disabled)."""
160
+ raw = os.environ.get("CHRYS_WORKSPACE_MRU_MAX_ENTRIES", "").strip()
161
+ if not raw:
162
+ return DEFAULT_WORKSPACE_MRU_MAX_ENTRIES
163
+ try:
164
+ value = int(raw)
165
+ except ValueError:
166
+ return DEFAULT_WORKSPACE_MRU_MAX_ENTRIES
167
+ if value <= 0:
168
+ return 0
169
+ return min(value, WORKSPACE_MRU_MAX_ENTRIES_LIMIT)
170
+
171
+
172
+ DEFAULT_WORKSPACE_CHANGE_NOTICE_MAX_ENTRIES = 50
173
+ WORKSPACE_CHANGE_NOTICE_MAX_ENTRIES_LIMIT = 100
174
+
175
+ DEFAULT_TRAJECTORY_VERIFY_COMMANDS = (
176
+ "pytest,ruff,ty,mypy,pyright,flake8,pylint,tox,nox,unittest,"
177
+ "npm test,npm run test,pnpm test,yarn test,bun test,deno test,node --test,"
178
+ "vitest,jest,mocha,playwright test,cypress run,eslint,biome check,tsc,"
179
+ "cargo test,cargo clippy,cargo check,cargo nextest,"
180
+ "go test,go vet,golangci-lint,staticcheck,"
181
+ "mvn test,mvn verify,gradle test,gradlew test,gradle check,gradlew check,"
182
+ "rspec,rake test,rubocop,phpunit,phpstan,dotnet test,mix test,swift test,"
183
+ "ctest,shellcheck,make"
184
+ )
185
+ """Comma-separated command words and phrases classified as verification work."""
186
+
187
+
188
+ def _load_workspace_change_notice_max_entries() -> int:
189
+ """Read and clamp the workspace-change notice path limit to ``1..100``."""
190
+ raw = os.environ.get("CHRYS_WORKSPACE_CHANGE_NOTICE_MAX_ENTRIES", "").strip()
191
+ if not raw:
192
+ return DEFAULT_WORKSPACE_CHANGE_NOTICE_MAX_ENTRIES
193
+ try:
194
+ value = int(raw)
195
+ except ValueError:
196
+ return DEFAULT_WORKSPACE_CHANGE_NOTICE_MAX_ENTRIES
197
+ return min(max(value, 1), WORKSPACE_CHANGE_NOTICE_MAX_ENTRIES_LIMIT)
198
+
199
+
200
+ DEFAULT_MUTATION_SNAPSHOT_MAX_FILE_MB = 50
201
+ """Default per-file cap (in MiB) on mutation backup blobs stored under the session dir."""
202
+
203
+
204
+ def _load_mutation_snapshot_max_file_mb() -> int:
205
+ """Read ``CHRYS_MUTATION_SNAPSHOT_MAX_FILE_MB``; garbage → default, non-positive → 0 (no cap)."""
206
+ raw = os.environ.get("CHRYS_MUTATION_SNAPSHOT_MAX_FILE_MB", "").strip()
207
+ if not raw:
208
+ return DEFAULT_MUTATION_SNAPSHOT_MAX_FILE_MB
209
+ try:
210
+ value = int(raw)
211
+ except ValueError:
212
+ return DEFAULT_MUTATION_SNAPSHOT_MAX_FILE_MB
213
+ return max(0, value)
214
+
215
+
216
+ def _load_ask_user_timeout() -> int | None:
217
+ """Read ``CHRYS_ASK_USER_TIMEOUT_SECONDS``; empty → default, non-positive → no timeout."""
218
+ raw = os.environ.get("CHRYS_ASK_USER_TIMEOUT_SECONDS", "").strip()
219
+ if not raw:
220
+ return DEFAULT_ASK_USER_TIMEOUT_SECONDS
221
+ try:
222
+ value = int(raw)
223
+ except ValueError:
224
+ return DEFAULT_ASK_USER_TIMEOUT_SECONDS
225
+ return value if value > 0 else None
226
+
227
+
228
+ def parse_max_transient_retries_structured(
229
+ raw: str | None,
230
+ ) -> tuple[int | None, MaxTransientRetriesWarning | None]:
231
+ """Parse ``CHRYS_MAX_TRANSIENT_RETRIES`` and retain warning arguments."""
232
+ normalized = (raw or "").strip()
233
+ if not normalized:
234
+ return None, None
235
+ try:
236
+ value = int(normalized)
237
+ except ValueError:
238
+ return None, MaxTransientRetriesWarning(
239
+ variant="invalid",
240
+ raw=str(raw),
241
+ value=None,
242
+ limit=MAX_TRANSIENT_RETRIES_LIMIT,
243
+ )
244
+ if value < 0:
245
+ return None, MaxTransientRetriesWarning(
246
+ variant="invalid",
247
+ raw=str(raw),
248
+ value=None,
249
+ limit=MAX_TRANSIENT_RETRIES_LIMIT,
250
+ )
251
+ if value > MAX_TRANSIENT_RETRIES_LIMIT:
252
+ return MAX_TRANSIENT_RETRIES_LIMIT, MaxTransientRetriesWarning(
253
+ variant="clamped",
254
+ raw=str(raw),
255
+ value=value,
256
+ limit=MAX_TRANSIENT_RETRIES_LIMIT,
257
+ )
258
+ return value, None
259
+
260
+
261
+ def parse_max_transient_retries(raw: str | None) -> tuple[int | None, str | None]:
262
+ """Parse ``CHRYS_MAX_TRANSIENT_RETRIES`` without raising."""
263
+ value, warning = parse_max_transient_retries_structured(raw)
264
+ if warning is None:
265
+ return value, None
266
+ if warning.variant == "invalid":
267
+ message = (
268
+ f"Ignoring invalid CHRYS_MAX_TRANSIENT_RETRIES={raw!r}; "
269
+ "expected a non-negative integer and will use the frontend default."
270
+ )
271
+ else:
272
+ message = (
273
+ f"CHRYS_MAX_TRANSIENT_RETRIES={warning.value} exceeds the limit of {warning.limit}; "
274
+ f"clamping to {warning.limit}."
275
+ )
276
+ return value, message
277
+
278
+
279
+ def _load_max_transient_retries() -> int | None:
280
+ """Read the transient retry budget from the environment without raising."""
281
+ value, warning = parse_max_transient_retries(os.environ.get("CHRYS_MAX_TRANSIENT_RETRIES"))
282
+ if warning is not None:
283
+ logger.warning(warning)
284
+ return value
285
+
286
+
287
+ def parse_tool_result_ceiling_tokens(raw: str | None) -> tuple[int, str | None]:
288
+ """Parse ``CHRYS_TOOL_RESULT_CEILING_TOKENS`` without raising."""
289
+ normalized = (raw or "").strip()
290
+ if not normalized:
291
+ return DEFAULT_TOOL_RESULT_CEILING_TOKENS, None
292
+ try:
293
+ value = int(normalized)
294
+ except ValueError:
295
+ return DEFAULT_TOOL_RESULT_CEILING_TOKENS, (
296
+ f"Ignoring invalid CHRYS_TOOL_RESULT_CEILING_TOKENS={raw!r}; "
297
+ f"using the default of {DEFAULT_TOOL_RESULT_CEILING_TOKENS}."
298
+ )
299
+ if value == 0:
300
+ return 0, None
301
+ if value < 0:
302
+ return DEFAULT_TOOL_RESULT_CEILING_TOKENS, (
303
+ f"Ignoring invalid CHRYS_TOOL_RESULT_CEILING_TOKENS={raw!r}; "
304
+ f"using the default of {DEFAULT_TOOL_RESULT_CEILING_TOKENS}."
305
+ )
306
+ if value < TOOL_RESULT_CEILING_FLOOR:
307
+ return TOOL_RESULT_CEILING_FLOOR, (
308
+ f"CHRYS_TOOL_RESULT_CEILING_TOKENS={value} is below the minimum of "
309
+ f"{TOOL_RESULT_CEILING_FLOOR}; clamping to {TOOL_RESULT_CEILING_FLOOR}."
310
+ )
311
+ return value, None
312
+
313
+
314
+ def _load_tool_result_ceiling_tokens() -> int:
315
+ value, warning = parse_tool_result_ceiling_tokens(os.environ.get("CHRYS_TOOL_RESULT_CEILING_TOKENS"))
316
+ if warning is not None:
317
+ logger.warning(warning)
318
+ return value
319
+
320
+
321
+ def default_session_root_dir(config_dir: str | Path | None = None) -> Path:
322
+ """Return the default root under which session storage lives."""
323
+ if config_dir is None:
324
+ from chrys.foundation.platform import get_platform
325
+
326
+ config_dir = get_platform().config_dir
327
+ return Path(config_dir)
328
+
329
+
330
+ def resolve_session_root_dir(config_dir: str | Path | None = None, *, create: bool = True) -> Path:
331
+ """Return the active base root for session storage.
332
+
333
+ Reads the process snapshot rather than the environment: the twelve callers
334
+ that resolve a sessions directory hold no ``Settings`` between them, and
335
+ threading one through all of them would only move the problem — the value
336
+ is ``Apply.RESTART``, so a single per-process answer is the honest one.
337
+ """
338
+ # Local import: ``process_settings`` falls back to a fresh ``load_settings``
339
+ # when bootstrap has not installed a snapshot, which needs this module.
340
+ from chrys.foundation.config.process_settings import process_settings
341
+
342
+ fallback = default_session_root_dir(config_dir)
343
+ raw = process_settings().session_root_dir.strip()
344
+ if raw:
345
+ candidate = _session_root_candidate(raw)
346
+ if candidate is not None and _session_root_dir_is_valid(candidate, create=create):
347
+ return candidate
348
+ logger.warning("Ignoring invalid %s=%r; using %s", SESSION_ROOT_DIR_ENV_VAR, raw, fallback)
349
+
350
+ if create:
351
+ fallback.mkdir(parents=True, exist_ok=True)
352
+ return fallback
353
+
354
+
355
+ def resolve_sessions_dir(config_dir: str | Path | None = None, *, create: bool = True) -> Path:
356
+ """Return the active directory containing saved session folders."""
357
+ root = resolve_session_root_dir(config_dir, create=create)
358
+ sessions_dir = root / "sessions"
359
+ if create:
360
+ cache_key = str(sessions_dir)
361
+ cached = _RESOLVED_SESSIONS_DIR_CACHE.get(cache_key)
362
+ if cached is not None:
363
+ return cached
364
+ if _session_storage_dir_is_valid(sessions_dir, create=create):
365
+ if create:
366
+ _RESOLVED_SESSIONS_DIR_CACHE[str(sessions_dir)] = sessions_dir
367
+ return sessions_dir
368
+
369
+ fallback = default_session_root_dir(config_dir) / "sessions"
370
+ if sessions_dir != fallback:
371
+ logger.warning("Ignoring invalid session storage directory %s; using %s", sessions_dir, fallback)
372
+ if _session_storage_dir_is_valid(fallback, create=create) and create:
373
+ _RESOLVED_SESSIONS_DIR_CACHE[str(fallback)] = fallback
374
+ return fallback
375
+
376
+
377
+ def session_root_is_ruled_out(raw: str) -> bool:
378
+ """Whether *raw* can be rejected as a session root without creating it.
379
+
380
+ Answers only what can be settled by looking: that the system takes the path
381
+ at all, and that nothing already sitting on the way down to
382
+ ``<root>/sessions`` rules out a directory ever being there. A root that gets
383
+ past this is one the settings may go on naming.
384
+
385
+ It deliberately does not predict whether the write will be *permitted* —
386
+ whether a directory that exists will accept a new one, whether one that does
387
+ not exist can be created. Those cannot be answered without performing the
388
+ operation being predicted, on the real names, which is the creation this is
389
+ supposed to avoid; every approximation of it (a file probe standing in for a
390
+ ``mkdir``, one level standing in for the descent, a scratch name standing in
391
+ for the real one) is wrong in both directions for some real permission
392
+ scheme. So permission is left to the moment it is exercised, where the
393
+ answer is observed rather than guessed: :func:`resolve_sessions_dir` falls
394
+ back to the default location and logs that it did. The cost of that choice
395
+ is bounded and known — until first use the settings can name a root that
396
+ turns out to be unwritable — and it is the only part of this that a
397
+ check running here cannot get right.
398
+ """
399
+ candidate = _session_root_candidate(raw)
400
+ if candidate is None:
401
+ return True
402
+ # The root is only ever used through this child, and the walk to the child
403
+ # passes through the root, so asking about the child asks about both.
404
+ sessions = candidate / "sessions"
405
+ start = _deepest_existing_directory(sessions)
406
+ if start is None:
407
+ return True
408
+ return any(_name_is_refused(start, part) for part in sessions.relative_to(start).parts)
409
+
410
+
411
+ def _name_is_refused(parent: Path, name: str) -> bool:
412
+ """Whether *name* is one this filesystem will not take as a child of *parent*.
413
+
414
+ Asking about the whole path only gets as far as the first component that is
415
+ not there: resolution stops at that one with ``ENOENT``, so a name below it
416
+ that no filesystem would accept is never looked at, and the ``mkdir`` that
417
+ creates the level above then fails on it. Each of them therefore gets asked
418
+ about separately, hung off the deepest directory that does exist.
419
+
420
+ Which is the right place to ask, not an approximation of it: nothing can be
421
+ mounted under a directory that is not there, so every level still to be
422
+ created lands in that one's filesystem and is held to its limits. And it
423
+ stays a question — ``lstat`` on a name that is merely free answers ``ENOENT``
424
+ and creates nothing. Some FUSE filesystems also answer ``ENOENT`` for a
425
+ name they refuse as too long, so that ambiguous answer is checked against
426
+ the filesystem's advertised name limit.
427
+ """
428
+ try:
429
+ (parent / name).lstat()
430
+ except FileNotFoundError:
431
+ if not hasattr(os, "statvfs"):
432
+ return False
433
+ try:
434
+ name_max = os.statvfs(parent).f_namemax
435
+ except OSError, ValueError:
436
+ return False
437
+ return name_max > 0 and len(os.fsencode(name)) > name_max
438
+ except OSError, ValueError:
439
+ return True
440
+ return False
441
+
442
+
443
+ def _deepest_existing_directory(path: Path) -> Path | None:
444
+ """The directory ``mkdir(parents=True)`` would build *path* down from.
445
+
446
+ ``None`` when there is none, which is a rejection: everything below it the
447
+ ``mkdir`` creates, and it creates none of it if what it finds is not a
448
+ directory. A dangling link two levels up leaves the root reading as absent
449
+ while the entry holding its name is very much present.
450
+
451
+ ``lstat`` rather than ``exists`` or ``lexists``, both of which answer two
452
+ questions with one ``False``: "nothing is there" and "the system would not
453
+ even take the question". Only the first keeps the walk going. The second
454
+ covers a component too long for the filesystem, an embedded NUL, a path
455
+ Windows spells illegally, a symlink loop, a parent that is a regular file —
456
+ every one of which reads as free to create, and then fails the ``mkdir``,
457
+ leaving storage on the default while the settings go on naming this root.
458
+ Asking the system beats keeping a list of what it rejects, which would grow
459
+ a case per platform and still be a case behind.
460
+
461
+ ``exists`` misreports one more: it follows the link, so a dangling one reads
462
+ as absent while its entry still occupies the name. ``lstat`` does not follow
463
+ the last component and ``is_dir`` does, which is what keeps a link resolving
464
+ to a real directory usable while a dangling one is not.
465
+ """
466
+ for existing in (path, *path.parents):
467
+ try:
468
+ existing.lstat()
469
+ except FileNotFoundError:
470
+ continue
471
+ except OSError, ValueError:
472
+ return None
473
+ return existing if existing.is_dir() else None
474
+ # Nothing on the way up is there — not even the anchor the walk ends at.
475
+ # POSIX bottoms out at ``/``, which is always there, but a Windows path ends
476
+ # at its drive or share, and an unmapped ``Z:\`` is as missing as its
477
+ # children.
478
+ return None
479
+
480
+
481
+ def _session_root_candidate(raw: str) -> Path | None:
482
+ """Parse a session root env value, returning ``None`` on unusable path syntax."""
483
+ try:
484
+ return Path(raw).expanduser()
485
+ except OSError, RuntimeError, ValueError:
486
+ return None
487
+
488
+
489
+ def _session_root_dir_is_valid(path: Path, *, create: bool) -> bool:
490
+ """Return whether *path* can be used as a session storage root."""
491
+ try:
492
+ if path.exists():
493
+ return path.is_dir()
494
+ if not create:
495
+ return True
496
+ path.mkdir(parents=True, exist_ok=True)
497
+ return path.is_dir()
498
+ except OSError, ValueError:
499
+ return False
500
+
501
+
502
+ DEFAULT_ROLLBACK_SNAPSHOTS_KEEP = 20
503
+ """Per-turn ``session.json`` snapshots retained for ``/rollback``."""
504
+
505
+ DEFAULT_FILE_SNAPSHOT_INLINE_CHARS = 128 * 1024
506
+ """Combined before/after snapshot bytes chat tool records keep inline."""
507
+
508
+ DEFAULT_MUTATION_TRACE_MODE = "auto"
509
+ MUTATION_TRACE_MODES = ("auto", "off")
510
+ """``auto`` probes for fsatrace once per process; ``off`` skips tracing."""
511
+
512
+ _TRACE_MODE_OFF_SPELLINGS = frozenset({"off", "0", "false", "no"})
513
+
514
+
515
+ def _trace_mode_env_coercer() -> Coercer:
516
+ """Coerce ``CHRYS_MUTATION_TRACE``, preserving its historical spellings.
517
+
518
+ The consumer only ever asked "is it off", so anything that is not one of
519
+ the off spellings has always meant ``auto`` — including typos. Keep that
520
+ for the variable rather than start rejecting values a working setup may
521
+ rely on. The dotted key is a new surface with no such history, so it gets
522
+ the ordinary closed-choice grammar instead of inheriting this one.
523
+ """
524
+
525
+ def coerce(raw: object) -> Coerced:
526
+ if raw is None:
527
+ return MISSING
528
+ if not isinstance(raw, str):
529
+ return invalid(raw, CoerceReason.EXPECTED_TEXT)
530
+ if not raw.strip():
531
+ return MISSING
532
+ off = raw.strip().lower() in _TRACE_MODE_OFF_SPELLINGS
533
+ return Coerced(status=CoerceStatus.VALID, value="off" if off else DEFAULT_MUTATION_TRACE_MODE)
534
+
535
+ return coerce
536
+
537
+
538
+ def _theme_coercer() -> Coercer:
539
+ """Coerce a theme name, following a retired built-in name to the one it goes by now.
540
+
541
+ Whether the name is a theme at all is still the app's call — themes are
542
+ registered in a tier this one cannot import. A rename is different: it is
543
+ a fact about the value, the same whichever layer wrote it.
544
+ """
545
+ text = text_coercer()
546
+
547
+ def coerce(raw: object) -> Coerced:
548
+ outcome = text(raw)
549
+ if outcome.status is not CoerceStatus.VALID:
550
+ return outcome
551
+ return Coerced(status=CoerceStatus.VALID, value=RENAMED_THEMES.get(outcome.value, outcome.value))
552
+
553
+ return coerce
554
+
555
+
556
+ def _history_disable_coercer() -> Coercer:
557
+ """Coerce ``CHRYS_HISTORY_DISABLE`` into the positive key it feeds.
558
+
559
+ Exactly ``"1"`` disables history; every other value, including ``"0"`` and
560
+ the empty string, leaves it on. Narrower than the general boolean grammar
561
+ on purpose — that is what the variable has always meant.
562
+ """
563
+
564
+ def coerce(raw: object) -> Coerced:
565
+ if raw is None:
566
+ return MISSING
567
+ if not isinstance(raw, str):
568
+ return invalid(raw, CoerceReason.EXPECTED_TEXT)
569
+ return Coerced(status=CoerceStatus.VALID, value=raw != "1")
570
+
571
+ return coerce
572
+
573
+
574
+ def _semantic_max_transient_retries(settings: Settings, ctx: EvalContext) -> int:
575
+ """Comparable retry budget: ``None`` means "whatever this frontend uses".
576
+
577
+ The stored value cannot be compared directly — a project value of 10 is a
578
+ tightening against headless (15) and a loosening against the TUI (7), and
579
+ ``None`` is neither until the frontend is known.
580
+ """
581
+ if settings.max_transient_retries is not None:
582
+ return settings.max_transient_retries
583
+ return ctx.frontend_default_max_transient_retries
584
+
585
+
586
+ def _semantic_tool_result_ceiling(settings: Settings, _ctx: EvalContext) -> float:
587
+ """Comparable ceiling: ``0`` disables the backstop, so it is the largest."""
588
+ ceiling = settings.tool_result_ceiling_tokens
589
+ return math.inf if ceiling <= 0 else float(ceiling)
590
+
591
+
592
+ def _session_storage_dir_is_valid(path: Path, *, create: bool) -> bool:
593
+ """Return whether the concrete sessions directory is usable."""
594
+ try:
595
+ if path.exists():
596
+ if not path.is_dir():
597
+ return False
598
+ elif not create:
599
+ return True
600
+ else:
601
+ path.mkdir(parents=True, exist_ok=True)
602
+ if create:
603
+ with tempfile.TemporaryFile(dir=path):
604
+ pass
605
+ return True
606
+ except OSError, ValueError:
607
+ return False
608
+
609
+
610
+ def probe_session_root(raw: str) -> Path | None:
611
+ """Try the sessions directory a session-root value would resolve to.
612
+
613
+ Where :func:`session_root_is_ruled_out` only looks, this creates: it makes
614
+ ``<root>/sessions`` (or the default location's, for a blank *raw*) and
615
+ checks that it takes a write, exactly as :func:`resolve_sessions_dir` will
616
+ on first use. The returned path is for validation and display only — the
617
+ persisted value stays the string, and ``None`` means the value would send
618
+ storage to the fallback.
619
+
620
+ A blank value is probed at the default location directly rather than
621
+ through the ruled-out check, whose empty-string candidate is the current
622
+ directory: a file named ``sessions`` sitting there would otherwise reject
623
+ the one value that never touches it.
624
+ """
625
+ # The persisted value is the trimmed string, so that is what gets probed.
626
+ raw = raw.strip()
627
+ if not raw:
628
+ default_sessions = default_session_root_dir() / "sessions"
629
+ return default_sessions if _session_storage_dir_is_valid(default_sessions, create=True) else None
630
+ if session_root_is_ruled_out(raw):
631
+ return None
632
+ candidate = _session_root_candidate(raw)
633
+ if candidate is None:
634
+ return None
635
+ sessions = candidate / "sessions"
636
+ return sessions if _session_storage_dir_is_valid(sessions, create=True) else None
637
+
638
+
639
+ # ── Panel labels ─────────────────────────────────────────────────────
640
+ # One per persisted field, keyed ``settings.<dotted key>.label``. Declared at
641
+ # module level so extraction sees them; ``SettingSpec`` refuses a persisted
642
+ # field without one.
643
+
644
+ _LABEL_MODEL_PROFILE_ACTIVE = msg("settings.model.profile.active.label", fallback="Active model profile")
645
+ _LABEL_AGENT_DEFAULT_PROFILE = msg("settings.agent.default_profile.label", fallback="Default agent")
646
+ _LABEL_MODEL_ROLE_APPROVAL_JUDGE = msg("settings.model.role.approval_judge.label", fallback="Approval judge model")
647
+ _LABEL_MODEL_ROLE_SESSION_TITLE = msg("settings.model.role.session_title.label", fallback="Session title model")
648
+ _LABEL_SESSION_TITLE_AUTO = msg("settings.session.title.auto.label", fallback="Auto-generate session titles")
649
+ _LABEL_CONTEXT_WARN_THRESHOLD_PCT = msg(
650
+ "settings.context.warn_threshold_pct.label", fallback="Context warning threshold"
651
+ )
652
+ _LABEL_TRAJECTORY_VERIFY_COMMANDS = msg(
653
+ "settings.trajectory.verify_commands.label", fallback="Trajectory verification commands"
654
+ )
655
+ _LABEL_LLM_RETRY_MAX_TRANSIENT = msg("settings.llm.retry.max_transient.label", fallback="Max transient retries")
656
+ _LABEL_TOOLS_RESULT_CEILING_TOKENS = msg(
657
+ "settings.tools.result.ceiling_tokens.label", fallback="Tool result ceiling (tokens)"
658
+ )
659
+ _LABEL_SEARCH_RESPECT_GITIGNORE = msg("settings.tools.search.respect_gitignore.label", fallback="Respect .gitignore")
660
+ _LABEL_UI_THEME = msg("settings.ui.theme.label", fallback="Theme")
661
+ _LABEL_UI_LOCALE = msg("settings.ui.locale.label", fallback="UI Language")
662
+ _LABEL_UI_CHAT_TOOL_GROUPS_EXPANDED = msg(
663
+ "settings.ui.chat.tool_groups_expanded.label",
664
+ fallback="Expand tool details by default",
665
+ )
666
+ _LABEL_UI_EDITOR_KEYMAP = msg("settings.ui.editor.keymap.label", fallback="Editor keymap")
667
+ _LABEL_WORKSPACE_MRU_MAX_ENTRIES = msg("settings.workspace.mru_max_entries.label", fallback="Recent workspaces to keep")
668
+ _LABEL_APPROVAL_DEFAULT_MODE = msg("settings.approval.default_mode.label", fallback="Default approval mode")
669
+ _LABEL_UI_APPROVAL_DEFER_WHILE_JUDGING = msg(
670
+ "settings.ui.approval.defer_while_judging.label",
671
+ fallback="Show the approval dialog only when Auto-Review flags a call",
672
+ )
673
+ _LABEL_APP_DEV_MODE = msg("settings.app.dev_mode.label", fallback="Developer mode")
674
+ _LABEL_MUTATIONS_PARALLEL_IMPLICIT_TOOLS = msg(
675
+ "settings.mutations.parallel_implicit_tools.label", fallback="Parallel implicit tools"
676
+ )
677
+ _LABEL_MUTATIONS_COORDINATION_ENABLED = msg(
678
+ "settings.mutations.coordination.enabled.label", fallback="Mutation coordination"
679
+ )
680
+ _LABEL_MUTATIONS_SNAPSHOT_MAX_FILE_MB = msg(
681
+ "settings.mutations.snapshot.max_file_mb.label", fallback="Snapshot max file size (MB)"
682
+ )
683
+ _LABEL_MUTATIONS_SNAPSHOT_SKIP_BINARY = msg(
684
+ "settings.mutations.snapshot.skip_binary.label", fallback="Skip binary files in snapshots"
685
+ )
686
+ _LABEL_WORKSPACE_CHANGE_NOTICE_ENABLED = msg(
687
+ "settings.workspace.change_notice.enabled.label", fallback="Workspace change notices"
688
+ )
689
+ _LABEL_WORKSPACE_CHANGE_NOTICE_MAX_ENTRIES = msg(
690
+ "settings.workspace.change_notice.max_entries.label", fallback="Workspace change notice entries"
691
+ )
692
+ _LABEL_ROLLBACK_SNAPSHOTS_KEEP = msg("settings.rollback.snapshots_keep.label", fallback="Rollback snapshots to keep")
693
+ _LABEL_OTEL_ENABLED = msg("settings.otel.enabled.label", fallback="OpenTelemetry export")
694
+ _LABEL_OTEL_ENDPOINT = msg("settings.otel.endpoint.label", fallback="Telemetry endpoint")
695
+ _LABEL_OTEL_SENSITIVE_DATA = msg("settings.otel.sensitive_data.label", fallback="Include sensitive data in telemetry")
696
+ _LABEL_TOOLS_ASK_USER_TIMEOUT_SECONDS = msg(
697
+ "settings.tools.ask_user.timeout_seconds.label", fallback="Ask-user timeout (seconds)"
698
+ )
699
+ _LABEL_TOOLS_ASK_USER_INLINE = msg("settings.tools.ask_user.inline.label", fallback="Show questions in the chat")
700
+ _LABEL_STORAGE_SESSION_ROOT_DIR = msg("settings.storage.session_root_dir.label", fallback="Session storage root")
701
+ _LABEL_HISTORY_PROMPT_ENABLED = msg("settings.history.prompt.enabled.label", fallback="Save prompt history")
702
+ _LABEL_UI_CHAT_FILE_SNAPSHOT_INLINE_CHARS = msg(
703
+ "settings.ui.chat.file_snapshot_inline_chars.label", fallback="Inline file snapshot limit (chars)"
704
+ )
705
+ _LABEL_MODEL_ROLE_BUDDY_MODEL_ID = msg("settings.model.role.buddy_model_id.label", fallback="Buddy model")
706
+ _LABEL_LOG_RAW_HTTP_CAPTURE = msg("settings.log.raw_http_capture.label", fallback="Capture raw HTTP traffic")
707
+ _LABEL_MUTATIONS_TRACE_MODE = msg("settings.mutations.trace.mode.label", fallback="Mutation trace mode")
708
+ _LABEL_MUTATIONS_TRACE_FSATRACE_PATH = msg("settings.mutations.trace.fsatrace_path.label", fallback="fsatrace path")
709
+ _LABEL_PROJECT_CONFIG_ENABLED = msg("settings.project.config_enabled.label", fallback="Load project settings")
710
+ _LABEL_PROJECT_HOOKS_ENABLED = msg("settings.project.hooks_enabled.label", fallback="Load project hooks")
711
+ _LABEL_PROJECT_SKILLS_ENABLED = msg("settings.project.skills_enabled.label", fallback="Load project skills")
712
+ _LABEL_NOTIFICATIONS_ENABLED = msg("settings.notifications.enabled.label", fallback="Enable notifications")
713
+ _LABEL_NOTIFICATIONS_DELIVERY_DESKTOP = msg("settings.notifications.delivery.desktop.label", fallback="Desktop popup")
714
+ _LABEL_NOTIFICATIONS_DELIVERY_SOUND = msg("settings.notifications.delivery.sound.label", fallback="Sound")
715
+ _LABEL_NOTIFICATIONS_SUPPRESS_WHEN_FOCUSED = msg(
716
+ "settings.notifications.suppress_when_focused.label", fallback="Suppress while focused"
717
+ )
718
+ _LABEL_NOTIFICATIONS_EVENTS_APPROVAL_REQUIRED = msg(
719
+ "settings.notifications.events.approval_required.label", fallback="Approval required"
720
+ )
721
+ _LABEL_NOTIFICATIONS_EVENTS_ASK_USER = msg("settings.notifications.events.ask_user.label", fallback="Question asked")
722
+ _LABEL_NOTIFICATIONS_EVENTS_TURN_COMPLETE = msg(
723
+ "settings.notifications.events.turn_complete.label", fallback="Turn complete"
724
+ )
725
+ _LABEL_NOTIFICATIONS_EVENTS_TURN_ERROR = msg("settings.notifications.events.turn_error.label", fallback="Turn error")
726
+
727
+
728
+ _WEB_LABEL_WEB_SEARCH_MODE = msg("settings.tools.web_search.mode.label", fallback="Web search mode")
729
+ _WEB_LABEL_WEB_SEARCH_NUM_RESULTS = msg(
730
+ "settings.tools.web_search.num_results.label", fallback="Results per web search"
731
+ )
732
+ _WEB_LABEL_WEB_SEARCH_TIMEOUT_SECONDS = msg(
733
+ "settings.tools.web_search.timeout_seconds.label", fallback="Web search timeout (seconds)"
734
+ )
735
+ _WEB_LABEL_WEB_FETCH_MODE = msg("settings.tools.web_fetch.mode.label", fallback="Web fetch mode")
736
+ _WEB_LABEL_WEB_FETCH_MAX_TOKENS = msg("settings.tools.web_fetch.max_tokens.label", fallback="Web fetch token budget")
737
+ _WEB_LABEL_WEB_FETCH_TIMEOUT_SECONDS = msg(
738
+ "settings.tools.web_fetch.timeout_seconds.label", fallback="Web fetch timeout (seconds)"
739
+ )
740
+ _WEB_LABEL_WEB_EGRESS_PROXY_URL = msg("settings.tools.web_egress.proxy_url.label", fallback="Web proxy URL")
741
+ _WEB_LABEL_WEB_EGRESS_PROXY_DNS = msg("settings.tools.web_egress.proxy_dns.label", fallback="Web proxy DNS")
742
+ _WEB_LABEL_WEB_SEARCH_CUSTOM_ENDPOINTS = msg(
743
+ "settings.tools.web_search.custom_endpoints.label", fallback="Custom search endpoints"
744
+ )
745
+ _WEB_LABEL_WEB_SEARCH_PRIVATE_ORIGINS = msg(
746
+ "settings.tools.web_search.private_origins.label", fallback="Web search private-network origins"
747
+ )
748
+ _WEB_LABEL_WEB_SEARCH_HTTP_ORIGINS = msg(
749
+ "settings.tools.web_search.http_origins.label", fallback="Web search plain-HTTP origins"
750
+ )
751
+ _WEB_LABEL_WEB_FETCH_PRIVATE_ORIGINS = msg(
752
+ "settings.tools.web_fetch.private_origins.label", fallback="Web fetch private-network origins"
753
+ )
754
+ _WEB_LABEL_WEB_FETCH_HTTP_ORIGINS = msg(
755
+ "settings.tools.web_fetch.http_origins.label", fallback="Web fetch plain-HTTP origins"
756
+ )
757
+ _WEB_LABEL_WEB_FETCH_ALLOWED_ORIGINS = msg(
758
+ "settings.tools.web_fetch.allowed_origins.label", fallback="Web fetch allowed origins"
759
+ )
760
+ _WEB_LABEL_WEB_FETCH_DENIED_ORIGINS = msg(
761
+ "settings.tools.web_fetch.denied_origins.label", fallback="Web fetch denied origins"
762
+ )
763
+
764
+
765
+ @dataclass(frozen=True)
766
+ class Settings:
767
+ """Application settings.
768
+
769
+ Field defaults are plain literals: every source (built-in default, user
770
+ YAML, project layer, dotenv, real process env, CLI, session pin) is merged
771
+ by :func:`chrys.foundation.config.settings_store.load_settings`, which is
772
+ the only thing that reads the environment. A bare ``Settings()`` is
773
+ therefore a pure value with no hidden process dependency.
774
+
775
+ Each field carries its own :class:`SettingSpec` in ``field(metadata=...)``:
776
+ the dotted key, the coercer both YAML and env values go through, when a
777
+ change takes effect, and whether a repository may set it.
778
+
779
+ **Frozen on purpose.** A value only means something alongside the layer it
780
+ came from and whether that layer was sealed, and those three live in
781
+ :class:`~chrys.foundation.config.settings_store.LoadedSettings`. Writing a
782
+ field in place changes one of the three and silently leaves the other two
783
+ describing the value it replaced — which is how a key sealed at its
784
+ built-in default keeps its seal while holding a value nobody defaulted to.
785
+ Go through ``LoadedSettings.overlay()`` and install the result; use
786
+ ``dataclasses.replace`` only where no provenance exists to keep (tests
787
+ building a fixture, a caller composing a fresh value).
788
+ """
789
+
790
+ # ── Active model profile pointers ─────────────────────────────
791
+ # Active model profile selector for the main agent (id or unique name).
792
+ # Looked up against ``ModelProfileRegistry`` by ``resolve_active_profile``.
793
+ model_profile: str = field(
794
+ default="",
795
+ metadata=spec(
796
+ key="model.profile.active",
797
+ label=_LABEL_MODEL_PROFILE_ACTIVE,
798
+ env="CHRYS_MODEL_PROFILE",
799
+ coerce=text_coercer(),
800
+ apply=Apply.RELOAD,
801
+ group="model",
802
+ kind=Kind.TEXT,
803
+ ),
804
+ )
805
+
806
+ # Explicit per-session model profile selector override (id or unique name).
807
+ # This is not loaded from env; frontends set it when the user provides an
808
+ # out-of-band model selection that must win over agent-level
809
+ # ``model.profile_id`` bindings.
810
+ model_profile_override: str = field(
811
+ default="",
812
+ metadata=spec(
813
+ key="model.profile.override",
814
+ coerce=text_coercer(),
815
+ apply=Apply.RELOAD,
816
+ group="model",
817
+ kind=Kind.TEXT,
818
+ persist=False,
819
+ ),
820
+ )
821
+ model_profile_override_sub_agents: bool = field(
822
+ default=False,
823
+ metadata=spec(
824
+ key="model.profile.override_sub_agents",
825
+ coerce=bool_coercer(),
826
+ apply=Apply.RELOAD,
827
+ group="model",
828
+ kind=Kind.BOOL,
829
+ persist=False,
830
+ ),
831
+ )
832
+
833
+ # Agent profile used by the TUI for fresh sessions. Invalid/missing
834
+ # profile names are resolved by the TUI after the agent registry is loaded.
835
+ default_agent: str = field(
836
+ default=DEFAULT_AGENT_PROFILE,
837
+ metadata=spec(
838
+ key="agent.default_profile",
839
+ label=_LABEL_AGENT_DEFAULT_PROFILE,
840
+ env="CHRYS_DEFAULT_AGENT",
841
+ coerce=text_coercer(),
842
+ apply=Apply.RELOAD,
843
+ group="agent",
844
+ kind=Kind.TEXT,
845
+ # A profile carries its own approval policy, which ``builder.py``
846
+ # adopts wholesale — a repository must not get to pick it.
847
+ project_merge=ProjectMerge.DENY,
848
+ risk=Risk.CAUTION,
849
+ ),
850
+ )
851
+
852
+ # Approval judge model profile ID. Empty = use the main agent's
853
+ # active profile. Looked up by ``resolve_judge_profile``.
854
+ approval_judge_model_profile: str = field(
855
+ default="",
856
+ metadata=spec(
857
+ key="model.role.approval_judge",
858
+ label=_LABEL_MODEL_ROLE_APPROVAL_JUDGE,
859
+ env="CHRYS_MODEL_PROFILE_APPROVAL_JUDGE",
860
+ coerce=text_coercer(),
861
+ apply=Apply.RELOAD,
862
+ group="model",
863
+ kind=Kind.TEXT,
864
+ ),
865
+ )
866
+
867
+ # ── Session titles ────────────────────────────────────────────
868
+ # Session-title summarizer model profile selector (id or unique name).
869
+ # Empty = use the main agent's active profile. Looked up by
870
+ # ``resolve_session_title_profile``.
871
+ session_title_model_profile: str = field(
872
+ default="",
873
+ metadata=spec(
874
+ key="model.role.session_title",
875
+ label=_LABEL_MODEL_ROLE_SESSION_TITLE,
876
+ env="CHRYS_MODEL_PROFILE_SESSION_TITLE",
877
+ coerce=text_coercer(),
878
+ apply=Apply.RELOAD,
879
+ group="model",
880
+ kind=Kind.TEXT,
881
+ ),
882
+ )
883
+
884
+ # Whether each successful turn schedules an async LLM call that
885
+ # summarizes the conversation into a short session title (skipped
886
+ # forever once the user sets a custom title). Set
887
+ # ``CHRYS_SESSION_TITLE_AUTO=0`` to disable the extra LLM traffic.
888
+ session_title_auto: bool = field(
889
+ default=True,
890
+ metadata=spec(
891
+ key="session.title.auto",
892
+ label=_LABEL_SESSION_TITLE_AUTO,
893
+ env="CHRYS_SESSION_TITLE_AUTO",
894
+ coerce=bool_coercer(),
895
+ apply=Apply.RELOAD,
896
+ group="session",
897
+ kind=Kind.BOOL,
898
+ # A repository may switch the extra LLM traffic off, never back on.
899
+ project_merge=ProjectMerge.DISABLE_ONLY,
900
+ ),
901
+ )
902
+
903
+ # ── Compaction / context-display thresholds ───────────────────
904
+ # Threshold (fraction of context window) at which the TUI starts
905
+ # warning the user about high context usage. App-level because it
906
+ # tunes UI behaviour, not the LLM call itself.
907
+ warn_threshold_pct: float = field(
908
+ default=0.50,
909
+ metadata=spec(
910
+ key="context.warn_threshold_pct",
911
+ label=_LABEL_CONTEXT_WARN_THRESHOLD_PCT,
912
+ coerce=float_coercer(minimum=0.0, maximum=1.0),
913
+ apply=Apply.RELOAD,
914
+ group="context",
915
+ kind=Kind.FLOAT,
916
+ project_merge=ProjectMerge.FREE,
917
+ ),
918
+ )
919
+
920
+ # Comma-separated rather than a list so Settings remains an all-scalar
921
+ # value object and project-layer FREE replacement has unambiguous
922
+ # override semantics.
923
+ trajectory_verify_commands: str = field(
924
+ default=DEFAULT_TRAJECTORY_VERIFY_COMMANDS,
925
+ metadata=spec(
926
+ key="trajectory.verify_commands",
927
+ label=_LABEL_TRAJECTORY_VERIFY_COMMANDS,
928
+ coerce=text_coercer(),
929
+ apply=Apply.LIVE,
930
+ group="trajectory",
931
+ kind=Kind.TEXT,
932
+ project_merge=ProjectMerge.FREE,
933
+ ),
934
+ )
935
+
936
+ # ── Application-layer transient retries ──────────────────────
937
+ # The environment-derived override stays separate from the frontend
938
+ # policy default so a settings reload can re-read the former while
939
+ # retaining the launch mode's default.
940
+ max_transient_retries: int | None = field(
941
+ default=None,
942
+ metadata=spec(
943
+ key="llm.retry.max_transient",
944
+ label=_LABEL_LLM_RETRY_MAX_TRANSIENT,
945
+ env="CHRYS_MAX_TRANSIENT_RETRIES",
946
+ coerce=int_coercer(reject_negative=True, maximum=MAX_TRANSIENT_RETRIES_LIMIT),
947
+ apply=Apply.RELOAD,
948
+ group="llm",
949
+ kind=Kind.OPTIONAL_INT,
950
+ # Raising it multiplies cost and wait against the backoff (up to
951
+ # 600 s a retry), so a repository may only lower it.
952
+ project_merge=ProjectMerge.TIGHTEN_ONLY,
953
+ semantic_value=_semantic_max_transient_retries,
954
+ ),
955
+ )
956
+ frontend_default_max_transient_retries: int = field(
957
+ default=DEFAULT_MAX_TRANSIENT_RETRIES,
958
+ metadata=spec(
959
+ key="llm.retry.frontend_default",
960
+ coerce=int_coercer(minimum=0),
961
+ apply=Apply.RESTART,
962
+ group="llm",
963
+ kind=Kind.INT,
964
+ persist=False,
965
+ ),
966
+ )
967
+
968
+ # Static sanity bound for every result on the local tool-invocation path.
969
+ # Zero disables the backstop; producer-specific caps remain independent.
970
+ tool_result_ceiling_tokens: int = field(
971
+ default=DEFAULT_TOOL_RESULT_CEILING_TOKENS,
972
+ metadata=spec(
973
+ key="tools.result.ceiling_tokens",
974
+ label=_LABEL_TOOLS_RESULT_CEILING_TOKENS,
975
+ env="CHRYS_TOOL_RESULT_CEILING_TOKENS",
976
+ coerce=int_coercer(zero=0, reject_negative=True, minimum=TOOL_RESULT_CEILING_FLOOR),
977
+ apply=Apply.RELOAD,
978
+ group="tools",
979
+ kind=Kind.INT,
980
+ # ``0`` means "no ceiling", so it is the loosest value, not the
981
+ # strictest — see the comparator. That also makes falling through
982
+ # to a lower layer a posture reversal rather than a preference
983
+ # question: a rejected value must not uncap the backstop.
984
+ invalid_policy=InvalidPolicy.SAFE_DEFAULT,
985
+ project_merge=ProjectMerge.TIGHTEN_ONLY,
986
+ semantic_value=_semantic_tool_result_ceiling,
987
+ ),
988
+ )
989
+
990
+ search_respect_gitignore: bool = field(
991
+ default=True,
992
+ metadata=spec(
993
+ key="tools.search.respect_gitignore",
994
+ label=_LABEL_SEARCH_RESPECT_GITIGNORE,
995
+ coerce=bool_coercer(),
996
+ apply=Apply.RELOAD,
997
+ group="tools",
998
+ kind=Kind.BOOL,
999
+ ),
1000
+ )
1001
+
1002
+ # ── TUI theme ─────────────────────────────────────────────────
1003
+ # Persisted theme name. Validated against registered themes at app
1004
+ # startup; invalid/unknown values fall back to :data:`DEFAULT_THEME`.
1005
+ theme: str = field(
1006
+ default=DEFAULT_THEME,
1007
+ metadata=spec(
1008
+ key="ui.theme",
1009
+ label=_LABEL_UI_THEME,
1010
+ env="CHRYS_THEME",
1011
+ # Themes are registered in the app layer, which foundation cannot
1012
+ # import, so validation stays where it is today: at app startup.
1013
+ coerce=_theme_coercer(),
1014
+ apply=Apply.LIVE,
1015
+ group="ui",
1016
+ kind=Kind.ENUM,
1017
+ choices=ChoiceProvider.THEMES,
1018
+ ),
1019
+ )
1020
+
1021
+ # ── Display locale ───────────────────────────────────────────────
1022
+ # Persisted locale selector. Resolution and fallback are owned by
1023
+ # ``Localizer``; Settings only strips surrounding whitespace.
1024
+ locale: str = field(
1025
+ default=DEFAULT_LOCALE,
1026
+ metadata=spec(
1027
+ key="ui.locale",
1028
+ label=_LABEL_UI_LOCALE,
1029
+ env="CHRYS_LOCALE",
1030
+ # Deliberately not a ``choice_coercer``: ``normalize_locale``
1031
+ # accepts far more spellings than the picker offers (``zh_CN``,
1032
+ # ``en_US.UTF-8``, ``C``, …) and maps them itself. ``choices``
1033
+ # here describes the picker, not the accepted set.
1034
+ coerce=text_coercer(),
1035
+ apply=Apply.LIVE,
1036
+ group="ui",
1037
+ kind=Kind.ENUM,
1038
+ choices=(DEFAULT_LOCALE, *SUPPORTED_LOCALES),
1039
+ ),
1040
+ )
1041
+
1042
+ # ── Message editor ────────────────────────────────────────────
1043
+ # Persisted keymap selected in the message editor.
1044
+ editor_keymap: str = field(
1045
+ default=DEFAULT_EDITOR_KEYMAP,
1046
+ metadata=spec(
1047
+ key="ui.editor.keymap",
1048
+ label=_LABEL_UI_EDITOR_KEYMAP,
1049
+ env="CHRYS_EDITOR_KEYMAP",
1050
+ coerce=choice_coercer(choices=_VALID_EDITOR_KEYMAPS),
1051
+ apply=Apply.LIVE,
1052
+ group="ui",
1053
+ kind=Kind.ENUM,
1054
+ choices=_VALID_EDITOR_KEYMAPS,
1055
+ ),
1056
+ )
1057
+
1058
+ # ── Workspace MRU ─────────────────────────────────────────────
1059
+ # Maximum entries kept in the persisted workspace MRU index that feeds
1060
+ # quick-access surfaces such as the Change Directory picker's recent
1061
+ # paths. ``0`` disables the MRU entirely (no index file is created,
1062
+ # loaded, or updated). Workspace-level UX config, not bound to any one
1063
+ # consumer; read once at TUI startup (no live reload).
1064
+ workspace_mru_max_entries: int = field(
1065
+ default=DEFAULT_WORKSPACE_MRU_MAX_ENTRIES,
1066
+ metadata=spec(
1067
+ key="workspace.mru_max_entries",
1068
+ label=_LABEL_WORKSPACE_MRU_MAX_ENTRIES,
1069
+ env="CHRYS_WORKSPACE_MRU_MAX_ENTRIES",
1070
+ coerce=int_coercer(non_positive=0, maximum=WORKSPACE_MRU_MAX_ENTRIES_LIMIT),
1071
+ apply=Apply.RESTART,
1072
+ group="workspace",
1073
+ kind=Kind.INT,
1074
+ ),
1075
+ )
1076
+
1077
+ # ── Approval ──────────────────────────────────────────────────
1078
+ # Persisted default approval mode applied at engine startup. One of
1079
+ # ``manual`` / ``auto`` / ``bypass``. Invalid or missing values fall
1080
+ # back to :data:`DEFAULT_APPROVAL_MODE`. :func:`persist_approval_mode`
1081
+ # maps ``bypass`` → ``auto`` on write so Chrys never boots directly
1082
+ # into BYPASS mode.
1083
+ default_approval_mode: str = field(
1084
+ default=DEFAULT_APPROVAL_MODE,
1085
+ metadata=spec(
1086
+ key="approval.default_mode",
1087
+ label=_LABEL_APPROVAL_DEFAULT_MODE,
1088
+ env="CHRYS_DEFAULT_APPROVAL_MODE",
1089
+ coerce=choice_coercer(choices=_VALID_APPROVAL_MODES),
1090
+ apply=Apply.RELOAD,
1091
+ group="approval",
1092
+ kind=Kind.ENUM,
1093
+ choices=_VALID_APPROVAL_MODES,
1094
+ risk=Risk.DANGEROUS,
1095
+ # Falling through could land on a persisted ``bypass``.
1096
+ invalid_policy=InvalidPolicy.SAFE_DEFAULT,
1097
+ ),
1098
+ )
1099
+
1100
+ # TUI presentation only: in AUTO mode, keep a call the judge is still
1101
+ # reviewing out of sight and open its dialog only once it is flagged.
1102
+ # The backend sends the same events either way; ACP and headless ignore it.
1103
+ approval_defer_while_judging: bool = field(
1104
+ default=True,
1105
+ metadata=spec(
1106
+ key="ui.approval.defer_while_judging",
1107
+ label=_LABEL_UI_APPROVAL_DEFER_WHILE_JUDGING,
1108
+ coerce=bool_coercer(),
1109
+ apply=Apply.LIVE,
1110
+ group="ui",
1111
+ kind=Kind.BOOL,
1112
+ ),
1113
+ )
1114
+
1115
+ # ── Developer mode ─────────────────────────────────────────────
1116
+ # Enables developer-only behaviors (debugging hooks, review surfaces, etc).
1117
+ dev_mode: bool = field(
1118
+ default=False,
1119
+ metadata=spec(
1120
+ key="app.dev_mode",
1121
+ label=_LABEL_APP_DEV_MODE,
1122
+ env="CHRYS_DEV_MODE",
1123
+ coerce=bool_coercer(),
1124
+ # Its one consumer takes it from ``Settings`` during the rebuild a
1125
+ # reload performs, so a reload genuinely applies it — RESTART would
1126
+ # promise a stability this process never had.
1127
+ apply=Apply.RELOAD,
1128
+ group="app",
1129
+ kind=Kind.BOOL,
1130
+ ),
1131
+ )
1132
+
1133
+ # ── Mutation tracking ──────────────────────────────────────────
1134
+ # Whether shell-like tools that can mutate files implicitly may run
1135
+ # concurrently inside one session. True preserves the historical
1136
+ # parallel behaviour. Set ``CHRYS_PARALLEL_IMPLICIT_TOOLS=0`` to
1137
+ # serialize shell/run_skill_script mutation-observation windows for
1138
+ # stronger in-session /diff attribution.
1139
+ parallel_implicit_tools: bool = field(
1140
+ default=True,
1141
+ metadata=spec(
1142
+ key="mutations.parallel_implicit_tools",
1143
+ label=_LABEL_MUTATIONS_PARALLEL_IMPLICIT_TOOLS,
1144
+ env="CHRYS_PARALLEL_IMPLICIT_TOOLS",
1145
+ coerce=bool_coercer(),
1146
+ apply=Apply.RELOAD,
1147
+ group="mutations",
1148
+ kind=Kind.BOOL,
1149
+ # Setting it false buys stronger /diff attribution; a repository
1150
+ # must not undo a user who chose that.
1151
+ project_merge=ProjectMerge.DISABLE_ONLY,
1152
+ ),
1153
+ )
1154
+
1155
+ # Cross-session mutation coordination: sessions sharing a working
1156
+ # tree publish proven writes to a registry and reclassify their
1157
+ # window inferences against peers' claims. Escape hatch only — the feature is inert (one
1158
+ # listdir) when no peer sessions exist. Set
1159
+ # ``CHRYS_MUTATION_COORDINATION=0`` to disable.
1160
+ mutation_coordination: bool = field(
1161
+ default=True,
1162
+ metadata=spec(
1163
+ key="mutations.coordination.enabled",
1164
+ label=_LABEL_MUTATIONS_COORDINATION_ENABLED,
1165
+ env="CHRYS_MUTATION_COORDINATION",
1166
+ coerce=bool_coercer(),
1167
+ apply=Apply.RELOAD,
1168
+ group="mutations",
1169
+ kind=Kind.BOOL,
1170
+ # Turning it off loses cross-session attribution.
1171
+ project_merge=ProjectMerge.ENABLE_ONLY,
1172
+ ),
1173
+ )
1174
+
1175
+ # Per-file cap (MiB) on mutation backup blobs written under
1176
+ # ``{session_dir}/mutations/``. Files larger than this are still
1177
+ # *recorded* as mutated, but their content is not backed up — no
1178
+ # diff rendering and no rollback restore for them. ``0`` (or any
1179
+ # non-positive env value) disables the size cap. Consumed via
1180
+ # ``SnapshotPolicy.from_settings``.
1181
+ mutation_snapshot_max_file_mb: int = field(
1182
+ default=DEFAULT_MUTATION_SNAPSHOT_MAX_FILE_MB,
1183
+ metadata=spec(
1184
+ key="mutations.snapshot.max_file_mb",
1185
+ label=_LABEL_MUTATIONS_SNAPSHOT_MAX_FILE_MB,
1186
+ env="CHRYS_MUTATION_SNAPSHOT_MAX_FILE_MB",
1187
+ coerce=int_coercer(non_positive=0),
1188
+ # ``SnapshotPolicy`` is captured when ``SnapshotStore`` is first
1189
+ # built and never rebuilt on reload, so RELOAD would be a promise
1190
+ # nothing keeps.
1191
+ apply=Apply.RESTART,
1192
+ group="mutations",
1193
+ kind=Kind.INT,
1194
+ ),
1195
+ )
1196
+
1197
+ # Whether binary (non-text) file contents are excluded from mutation
1198
+ # backups, regardless of size. Detection uses
1199
+ # ``EncodingDetector.looks_binary`` — the same heuristic diff
1200
+ # surfaces use for rendering. Set
1201
+ # ``CHRYS_MUTATION_SNAPSHOT_SKIP_BINARY=0`` to back up binary files.
1202
+ mutation_snapshot_skip_binary: bool = field(
1203
+ default=True,
1204
+ metadata=spec(
1205
+ key="mutations.snapshot.skip_binary",
1206
+ label=_LABEL_MUTATIONS_SNAPSHOT_SKIP_BINARY,
1207
+ env="CHRYS_MUTATION_SNAPSHOT_SKIP_BINARY",
1208
+ coerce=bool_coercer(),
1209
+ apply=Apply.RESTART, # Same captured policy as max_file_mb.
1210
+ group="mutations",
1211
+ kind=Kind.BOOL,
1212
+ ),
1213
+ )
1214
+
1215
+ # One-shot reminders describing agent and external workspace changes at
1216
+ # real turn boundaries.
1217
+ workspace_change_notice: bool = field(
1218
+ default=True,
1219
+ metadata=spec(
1220
+ key="workspace.change_notice.enabled",
1221
+ label=_LABEL_WORKSPACE_CHANGE_NOTICE_ENABLED,
1222
+ env="CHRYS_WORKSPACE_CHANGE_NOTICE",
1223
+ coerce=bool_coercer(),
1224
+ apply=Apply.RELOAD,
1225
+ group="workspace",
1226
+ kind=Kind.BOOL,
1227
+ # A repository must not hide file changes from the user.
1228
+ project_merge=ProjectMerge.ENABLE_ONLY,
1229
+ ),
1230
+ )
1231
+ workspace_change_notice_max_entries: int = field(
1232
+ default=DEFAULT_WORKSPACE_CHANGE_NOTICE_MAX_ENTRIES,
1233
+ metadata=spec(
1234
+ key="workspace.change_notice.max_entries",
1235
+ label=_LABEL_WORKSPACE_CHANGE_NOTICE_MAX_ENTRIES,
1236
+ env="CHRYS_WORKSPACE_CHANGE_NOTICE_MAX_ENTRIES",
1237
+ coerce=int_coercer(minimum=1, maximum=WORKSPACE_CHANGE_NOTICE_MAX_ENTRIES_LIMIT),
1238
+ apply=Apply.RELOAD,
1239
+ group="workspace",
1240
+ kind=Kind.INT,
1241
+ # Already clamped to 1..100, so there is no room to hide in.
1242
+ project_merge=ProjectMerge.FREE,
1243
+ ),
1244
+ )
1245
+
1246
+ # ── Rollback ──────────────────────────────────────────────────
1247
+ # How many per-turn ``session.json`` snapshots to retain under
1248
+ # ``{session_dir}/snapshots/``. Each snapshot is a full copy of
1249
+ # ``session.json`` taken just before a new user turn begins, enabling
1250
+ # ``/rollback`` to restore the session (and optionally file-system
1251
+ # changes) to any of the last N turn boundaries. Older snapshots
1252
+ # are pruned on every new turn.
1253
+ rollback_snapshots_keep: int = field(
1254
+ default=DEFAULT_ROLLBACK_SNAPSHOTS_KEEP,
1255
+ metadata=spec(
1256
+ key="rollback.snapshots_keep",
1257
+ label=_LABEL_ROLLBACK_SNAPSHOTS_KEEP,
1258
+ env="CHRYS_ROLLBACK_SNAPSHOTS_KEEP",
1259
+ # Behaviour change: a non-numeric value used to raise ``ValueError``
1260
+ # out of ``Settings()`` and take the process down at startup. It is
1261
+ # now rejected like any other invalid value.
1262
+ coerce=int_coercer(minimum=1),
1263
+ apply=Apply.RELOAD,
1264
+ group="rollback",
1265
+ kind=Kind.INT,
1266
+ ),
1267
+ )
1268
+
1269
+ # ── Observability ─────────────────────────────────────────────
1270
+ # When True, :func:`chrys.foundation.observability.setup.setup_otel` builds the OTel
1271
+ # providers/exporters and opens TELEMETRY_GATE. Kernel chat, agent and
1272
+ # function spans and the MCP spans from service/mcp/owned.py share this gate.
1273
+ #
1274
+ # Exporter destination priority:
1275
+ # 1. If ``OTEL_EXPORTER_OTLP_ENDPOINT`` (or a signal-specific
1276
+ # variant) is set, or ``otel_endpoint`` below is non-blank, Chrys
1277
+ # builds OTLP exporters from the OTel-spec'd env vars
1278
+ # through _get_exporters_from_env; the setting supplies a fallback
1279
+ # base endpoint.
1280
+ # 2. Otherwise, Chrys installs a local JSONL file exporter that
1281
+ # writes to ``{session_dir}/otel/{traces,logs}.jsonl`` once
1282
+ # a session is live. Zero-config default; safe in the TUI
1283
+ # (no stdout contention).
1284
+ #
1285
+ # ``otel_sensitive_data`` (``CHRYS_OTEL_SENSITIVE_DATA``) feeds TELEMETRY_GATE.
1286
+ # When enabled, prompt, response and tool-argument content is captured;
1287
+ # SessionJsonlLogExporter writes message events alongside span traces.
1288
+ otel_enabled: bool = field(
1289
+ default=False,
1290
+ metadata=spec(
1291
+ key="otel.enabled",
1292
+ label=_LABEL_OTEL_ENABLED,
1293
+ env="CHRYS_OTEL",
1294
+ coerce=bool_coercer(),
1295
+ apply=Apply.RESTART, # Providers are installed once per process.
1296
+ group="otel",
1297
+ kind=Kind.BOOL,
1298
+ ),
1299
+ )
1300
+ otel_sensitive_data: bool = field(
1301
+ default=False,
1302
+ metadata=spec(
1303
+ key="otel.sensitive_data",
1304
+ label=_LABEL_OTEL_SENSITIVE_DATA,
1305
+ env="CHRYS_OTEL_SENSITIVE_DATA",
1306
+ coerce=bool_coercer(),
1307
+ apply=Apply.RESTART,
1308
+ group="otel",
1309
+ kind=Kind.BOOL,
1310
+ # Full prompt / response / tool-arg content is exported.
1311
+ risk=Risk.DANGEROUS,
1312
+ invalid_policy=InvalidPolicy.SAFE_DEFAULT,
1313
+ ),
1314
+ )
1315
+ # Base OTLP endpoint for the exporters when the standard
1316
+ # ``OTEL_EXPORTER_OTLP_ENDPOINT`` is not set; the standard variables keep
1317
+ # winning so an OpenTelemetry-configured environment behaves as documented.
1318
+ # Blank keeps the session-folder JSONL fallback.
1319
+ otel_endpoint: str = field(
1320
+ default="",
1321
+ metadata=spec(
1322
+ key="otel.endpoint",
1323
+ label=_LABEL_OTEL_ENDPOINT,
1324
+ env="CHRYS_OTEL_ENDPOINT",
1325
+ coerce=text_coercer(),
1326
+ apply=Apply.RESTART, # Read once by setup_otel with the other otel keys.
1327
+ group="otel",
1328
+ kind=Kind.TEXT,
1329
+ ),
1330
+ )
1331
+
1332
+ # ── Ask-user ──────────────────────────────────────────────────
1333
+ # Seconds to wait for a user reply to an ``ask_user`` tool call before
1334
+ # the middleware gives up and reports a timeout to the model. Read from
1335
+ # ``CHRYS_ASK_USER_TIMEOUT_SECONDS`` (non-positive/empty → no timeout via
1336
+ # the loader). Per-session: each engine carries its own ``Settings``, so
1337
+ # frontends pick their own policy — the TUI/CLI keep the bounded default,
1338
+ # while ``chrys acp`` defaults to ``None`` so the ACP client owns timing.
1339
+ ask_user_timeout_seconds: int | None = field(
1340
+ default=DEFAULT_ASK_USER_TIMEOUT_SECONDS,
1341
+ metadata=spec(
1342
+ key="tools.ask_user.timeout_seconds",
1343
+ label=_LABEL_TOOLS_ASK_USER_TIMEOUT_SECONDS,
1344
+ env="CHRYS_ASK_USER_TIMEOUT_SECONDS",
1345
+ coerce=optional_int_coercer(),
1346
+ apply=Apply.RELOAD,
1347
+ group="tools",
1348
+ kind=Kind.OPTIONAL_INT,
1349
+ ),
1350
+ )
1351
+ # Where an ``ask_user`` question first appears in the TUI: inside the
1352
+ # tool card in the transcript (True) or as a modal dialog (False, from
1353
+ # which "Answer Inline" still hands the question over to the transcript).
1354
+ ask_user_inline: bool = field(
1355
+ default=False,
1356
+ metadata=spec(
1357
+ key="tools.ask_user.inline",
1358
+ label=_LABEL_TOOLS_ASK_USER_INLINE,
1359
+ env="CHRYS_ASK_USER_INLINE",
1360
+ coerce=bool_coercer(),
1361
+ apply=Apply.RELOAD,
1362
+ group="tools",
1363
+ kind=Kind.BOOL,
1364
+ ),
1365
+ )
1366
+
1367
+ # ── Storage ───────────────────────────────────────────────────
1368
+ # Base directory under which session storage lives; the session store
1369
+ # itself is ``<root>/sessions``. Empty selects the platform config dir.
1370
+ # Resolution and validation stay in ``resolve_session_root_dir``.
1371
+ session_root_dir: str = field(
1372
+ default="",
1373
+ metadata=spec(
1374
+ key="storage.session_root_dir",
1375
+ label=_LABEL_STORAGE_SESSION_ROOT_DIR,
1376
+ env=SESSION_ROOT_DIR_ENV_VAR,
1377
+ coerce=text_coercer(),
1378
+ apply=Apply.RESTART,
1379
+ group="storage",
1380
+ kind=Kind.PATH,
1381
+ # Where a user's sessions live is not a repository's business.
1382
+ project_merge=ProjectMerge.DENY,
1383
+ risk=Risk.CAUTION,
1384
+ ),
1385
+ )
1386
+
1387
+ # ── Prompt history ────────────────────────────────────────────
1388
+ # Whether the TUI keeps a persistent prompt history. The key is
1389
+ # positive; the legacy variable is negative, hence the separate alias
1390
+ # coercer.
1391
+ prompt_history_enabled: bool = field(
1392
+ default=True,
1393
+ metadata=spec(
1394
+ key="history.prompt.enabled",
1395
+ label=_LABEL_HISTORY_PROMPT_ENABLED,
1396
+ env="CHRYS_HISTORY_DISABLE",
1397
+ coerce=bool_coercer(),
1398
+ env_coerce=_history_disable_coercer(),
1399
+ # The consumer is a module-level function on the TUI side with no
1400
+ # engine in reach, and ``ChrysApp._settings`` is never refreshed on
1401
+ # reload, so RELOAD would be a promise nothing keeps. This is not a
1402
+ # regression: today the variable can only be set before launch.
1403
+ apply=Apply.RESTART,
1404
+ group="history",
1405
+ kind=Kind.BOOL,
1406
+ ),
1407
+ )
1408
+
1409
+ # ── Chat rendering ────────────────────────────────────────────
1410
+ tool_groups_expanded: bool = field(
1411
+ default=False,
1412
+ metadata=spec(
1413
+ key="ui.chat.tool_groups_expanded",
1414
+ label=_LABEL_UI_CHAT_TOOL_GROUPS_EXPANDED,
1415
+ coerce=bool_coercer(),
1416
+ apply=Apply.LIVE,
1417
+ group="ui",
1418
+ kind=Kind.BOOL,
1419
+ project_merge=ProjectMerge.DENY,
1420
+ ),
1421
+ )
1422
+
1423
+ # Maximum combined before/after snapshot bytes chat tool records keep
1424
+ # inline. ``0`` keeps nothing inline.
1425
+ chat_file_snapshot_inline_chars: int = field(
1426
+ default=DEFAULT_FILE_SNAPSHOT_INLINE_CHARS,
1427
+ metadata=spec(
1428
+ key="ui.chat.file_snapshot_inline_chars",
1429
+ label=_LABEL_UI_CHAT_FILE_SNAPSHOT_INLINE_CHARS,
1430
+ env="CHRYS_TUI_FILE_SNAPSHOT_INLINE_CHARS",
1431
+ coerce=int_coercer(non_positive=0),
1432
+ apply=Apply.RESTART, # Read once, from the process snapshot.
1433
+ group="ui",
1434
+ kind=Kind.INT,
1435
+ ),
1436
+ )
1437
+
1438
+ # ── Buddy ─────────────────────────────────────────────────────
1439
+ # Model id used for what the buddy says when petted. Empty = the active
1440
+ # profile's model. Sits under ``model.role`` with the other per-role
1441
+ # selectors, but holds a *model id* rather than a profile selector —
1442
+ # hence the ``_model_id`` leaf, which is the only thing telling a reader
1443
+ # of the YAML that a profile name will not work here.
1444
+ buddy_model: str = field(
1445
+ default="",
1446
+ metadata=spec(
1447
+ key="model.role.buddy_model_id",
1448
+ label=_LABEL_MODEL_ROLE_BUDDY_MODEL_ID,
1449
+ env="CHRYS_PET_MODEL",
1450
+ coerce=text_coercer(),
1451
+ apply=Apply.RELOAD,
1452
+ group="model",
1453
+ kind=Kind.TEXT,
1454
+ ),
1455
+ )
1456
+
1457
+ # ── Diagnostics ───────────────────────────────────────────────
1458
+ # Full LLM HTTP exchange capture, written under the session dir.
1459
+ raw_http_capture: bool = field(
1460
+ default=False,
1461
+ metadata=spec(
1462
+ key="log.raw_http_capture",
1463
+ label=_LABEL_LOG_RAW_HTTP_CAPTURE,
1464
+ env="CHRYS_DEBUG_LLM_RAW_HTTP_LOG",
1465
+ coerce=bool_coercer(),
1466
+ apply=Apply.RESTART,
1467
+ group="log",
1468
+ kind=Kind.BOOL,
1469
+ # Captured exchanges contain API keys and full message content.
1470
+ risk=Risk.DANGEROUS,
1471
+ invalid_policy=InvalidPolicy.SAFE_DEFAULT,
1472
+ ),
1473
+ )
1474
+
1475
+ # ── Mutation tracing ──────────────────────────────────────────
1476
+ # ``auto`` probes for fsatrace once per process; ``off`` skips tracing.
1477
+ mutation_trace_mode: str = field(
1478
+ default=DEFAULT_MUTATION_TRACE_MODE,
1479
+ metadata=spec(
1480
+ key="mutations.trace.mode",
1481
+ label=_LABEL_MUTATIONS_TRACE_MODE,
1482
+ env="CHRYS_MUTATION_TRACE",
1483
+ coerce=choice_coercer(choices=MUTATION_TRACE_MODES),
1484
+ env_coerce=_trace_mode_env_coercer(),
1485
+ apply=Apply.RESTART, # The probe result is cached process-wide.
1486
+ group="mutations",
1487
+ kind=Kind.ENUM,
1488
+ choices=MUTATION_TRACE_MODES,
1489
+ ),
1490
+ )
1491
+
1492
+ # Explicit fsatrace binary, overriding the ``PATH`` lookup.
1493
+ mutation_trace_fsatrace_path: str = field(
1494
+ default="",
1495
+ metadata=spec(
1496
+ key="mutations.trace.fsatrace_path",
1497
+ label=_LABEL_MUTATIONS_TRACE_FSATRACE_PATH,
1498
+ env="CHRYS_FSATRACE_PATH",
1499
+ coerce=text_coercer(),
1500
+ apply=Apply.RESTART,
1501
+ group="mutations",
1502
+ kind=Kind.PATH,
1503
+ # This value becomes a subprocess argv[0]. A cloned repository
1504
+ # naming its own binary here would have Chrys execute it.
1505
+ project_merge=ProjectMerge.DENY,
1506
+ risk=Risk.DANGEROUS,
1507
+ invalid_policy=InvalidPolicy.SAFE_DEFAULT,
1508
+ ),
1509
+ )
1510
+
1511
+ # ── Project layer ─────────────────────────────────────────────
1512
+ # Master switch for the project trust domain: whether
1513
+ # ``<root>/.chrys/settings.yaml`` is consulted at all. Off by
1514
+ # default — cloning a repository must not hand it configuration
1515
+ # authority.
1516
+ project_config_enabled: bool = field(
1517
+ default=False,
1518
+ metadata=spec(
1519
+ key="project.config_enabled",
1520
+ label=_LABEL_PROJECT_CONFIG_ENABLED,
1521
+ coerce=bool_coercer(),
1522
+ apply=Apply.RELOAD,
1523
+ group="project",
1524
+ kind=Kind.BOOL,
1525
+ # Read only from DEFAULT/USER: letting a project enable the
1526
+ # project layer would be the layer authorising itself.
1527
+ project_merge=ProjectMerge.DENY,
1528
+ risk=Risk.CAUTION,
1529
+ ),
1530
+ )
1531
+
1532
+ # Whether ``<root>/.chrys/hooks/hooks.{yaml,yml,json}`` is loaded. Off
1533
+ # by default: a hook is a command that runs on its own, outside tool
1534
+ # approval, so a cloned repository must not get to run one unasked.
1535
+ project_hooks_enabled: bool = field(
1536
+ default=False,
1537
+ metadata=spec(
1538
+ key="project.hooks_enabled",
1539
+ label=_LABEL_PROJECT_HOOKS_ENABLED,
1540
+ coerce=bool_coercer(),
1541
+ apply=Apply.RELOAD,
1542
+ group="project",
1543
+ kind=Kind.BOOL,
1544
+ # A project must not be able to switch its own hooks back on.
1545
+ project_merge=ProjectMerge.DENY,
1546
+ risk=Risk.CAUTION,
1547
+ ),
1548
+ )
1549
+
1550
+ # Whether ``<root>/.agents/skills`` is loaded. Off by default for the
1551
+ # same reason: a skill's instructions and scripts come from whoever
1552
+ # wrote the repository. The agent profile's own working-folder switch
1553
+ # still applies on top of this one.
1554
+ project_skills_enabled: bool = field(
1555
+ default=False,
1556
+ metadata=spec(
1557
+ key="project.skills_enabled",
1558
+ label=_LABEL_PROJECT_SKILLS_ENABLED,
1559
+ coerce=bool_coercer(),
1560
+ apply=Apply.RELOAD,
1561
+ group="project",
1562
+ kind=Kind.BOOL,
1563
+ # A project must not be able to switch its own skills back on.
1564
+ project_merge=ProjectMerge.DENY,
1565
+ risk=Risk.CAUTION,
1566
+ ),
1567
+ )
1568
+
1569
+ # ── Desktop notifications ─────────────────────────────────────
1570
+ # Flattened from the former ``notifications.yaml`` document. The TUI
1571
+ # consumes these through the ``NotificationSettings`` view object
1572
+ # (``app/tui/notifications``); delivery behaviour stays there.
1573
+ notifications_enabled: bool = field(
1574
+ default=True,
1575
+ metadata=spec(
1576
+ key="notifications.enabled",
1577
+ label=_LABEL_NOTIFICATIONS_ENABLED,
1578
+ coerce=bool_coercer(),
1579
+ apply=Apply.LIVE,
1580
+ group="notifications",
1581
+ kind=Kind.BOOL,
1582
+ ),
1583
+ )
1584
+
1585
+ notifications_desktop: bool = field(
1586
+ default=True,
1587
+ metadata=spec(
1588
+ key="notifications.delivery.desktop",
1589
+ label=_LABEL_NOTIFICATIONS_DELIVERY_DESKTOP,
1590
+ coerce=bool_coercer(),
1591
+ apply=Apply.LIVE,
1592
+ group="notifications",
1593
+ kind=Kind.BOOL,
1594
+ ),
1595
+ )
1596
+
1597
+ notifications_sound: bool = field(
1598
+ default=True,
1599
+ metadata=spec(
1600
+ key="notifications.delivery.sound",
1601
+ label=_LABEL_NOTIFICATIONS_DELIVERY_SOUND,
1602
+ coerce=bool_coercer(),
1603
+ apply=Apply.LIVE,
1604
+ group="notifications",
1605
+ kind=Kind.BOOL,
1606
+ ),
1607
+ )
1608
+
1609
+ notifications_suppress_when_focused: bool = field(
1610
+ default=True,
1611
+ metadata=spec(
1612
+ key="notifications.suppress_when_focused",
1613
+ label=_LABEL_NOTIFICATIONS_SUPPRESS_WHEN_FOCUSED,
1614
+ coerce=bool_coercer(),
1615
+ apply=Apply.LIVE,
1616
+ group="notifications",
1617
+ kind=Kind.BOOL,
1618
+ ),
1619
+ )
1620
+
1621
+ notifications_event_approval_required: bool = field(
1622
+ default=True,
1623
+ metadata=spec(
1624
+ key="notifications.events.approval_required",
1625
+ label=_LABEL_NOTIFICATIONS_EVENTS_APPROVAL_REQUIRED,
1626
+ coerce=bool_coercer(),
1627
+ apply=Apply.LIVE,
1628
+ group="notifications",
1629
+ kind=Kind.BOOL,
1630
+ ),
1631
+ )
1632
+
1633
+ notifications_event_ask_user: bool = field(
1634
+ default=True,
1635
+ metadata=spec(
1636
+ key="notifications.events.ask_user",
1637
+ label=_LABEL_NOTIFICATIONS_EVENTS_ASK_USER,
1638
+ coerce=bool_coercer(),
1639
+ apply=Apply.LIVE,
1640
+ group="notifications",
1641
+ kind=Kind.BOOL,
1642
+ ),
1643
+ )
1644
+
1645
+ notifications_event_turn_complete: bool = field(
1646
+ default=True,
1647
+ metadata=spec(
1648
+ key="notifications.events.turn_complete",
1649
+ label=_LABEL_NOTIFICATIONS_EVENTS_TURN_COMPLETE,
1650
+ coerce=bool_coercer(),
1651
+ apply=Apply.LIVE,
1652
+ group="notifications",
1653
+ kind=Kind.BOOL,
1654
+ ),
1655
+ )
1656
+
1657
+ notifications_event_turn_error: bool = field(
1658
+ default=True,
1659
+ metadata=spec(
1660
+ key="notifications.events.turn_error",
1661
+ label=_LABEL_NOTIFICATIONS_EVENTS_TURN_ERROR,
1662
+ coerce=bool_coercer(),
1663
+ apply=Apply.LIVE,
1664
+ group="notifications",
1665
+ kind=Kind.BOOL,
1666
+ ),
1667
+ )
1668
+
1669
+ web_search_mode: str = field(
1670
+ default="auto",
1671
+ metadata=spec(
1672
+ key="tools.web_search.mode",
1673
+ label=_WEB_LABEL_WEB_SEARCH_MODE,
1674
+ # "provider" needs a profile-level provider map, so it cannot build from
1675
+ # global settings and the panel must not offer it.
1676
+ coerce=web_mode_coercer(on="auto"),
1677
+ apply=Apply.RELOAD,
1678
+ group="tools",
1679
+ kind=Kind.ENUM,
1680
+ project_merge=ProjectMerge.DENY,
1681
+ choices=("off", "auto"),
1682
+ ),
1683
+ )
1684
+
1685
+ web_search_num_results: int = field(
1686
+ default=8,
1687
+ metadata=spec(
1688
+ key="tools.web_search.num_results",
1689
+ label=_WEB_LABEL_WEB_SEARCH_NUM_RESULTS,
1690
+ coerce=int_coercer(minimum=1, maximum=20),
1691
+ apply=Apply.RELOAD,
1692
+ group="tools",
1693
+ kind=Kind.INT,
1694
+ project_merge=ProjectMerge.DENY,
1695
+ ),
1696
+ )
1697
+
1698
+ web_search_timeout_seconds: int = field(
1699
+ default=30,
1700
+ metadata=spec(
1701
+ key="tools.web_search.timeout_seconds",
1702
+ label=_WEB_LABEL_WEB_SEARCH_TIMEOUT_SECONDS,
1703
+ coerce=int_coercer(minimum=1, maximum=120),
1704
+ apply=Apply.RELOAD,
1705
+ group="tools",
1706
+ kind=Kind.INT,
1707
+ project_merge=ProjectMerge.DENY,
1708
+ ),
1709
+ )
1710
+
1711
+ web_fetch_mode: str = field(
1712
+ default="on",
1713
+ metadata=spec(
1714
+ key="tools.web_fetch.mode",
1715
+ label=_WEB_LABEL_WEB_FETCH_MODE,
1716
+ coerce=web_mode_coercer(on="on"),
1717
+ apply=Apply.RELOAD,
1718
+ group="tools",
1719
+ kind=Kind.ENUM,
1720
+ project_merge=ProjectMerge.DENY,
1721
+ choices=("off", "on"),
1722
+ ),
1723
+ )
1724
+
1725
+ web_fetch_max_tokens: int = field(
1726
+ default=16000,
1727
+ metadata=spec(
1728
+ key="tools.web_fetch.max_tokens",
1729
+ label=_WEB_LABEL_WEB_FETCH_MAX_TOKENS,
1730
+ coerce=int_coercer(minimum=1, maximum=64000),
1731
+ apply=Apply.RELOAD,
1732
+ group="tools",
1733
+ kind=Kind.INT,
1734
+ project_merge=ProjectMerge.DENY,
1735
+ ),
1736
+ )
1737
+
1738
+ web_fetch_timeout_seconds: int = field(
1739
+ default=60,
1740
+ metadata=spec(
1741
+ key="tools.web_fetch.timeout_seconds",
1742
+ label=_WEB_LABEL_WEB_FETCH_TIMEOUT_SECONDS,
1743
+ coerce=int_coercer(minimum=1, maximum=120),
1744
+ apply=Apply.RELOAD,
1745
+ group="tools",
1746
+ kind=Kind.INT,
1747
+ project_merge=ProjectMerge.DENY,
1748
+ ),
1749
+ )
1750
+
1751
+ web_egress_proxy_url: str = field(
1752
+ default="",
1753
+ metadata=spec(
1754
+ key="tools.web_egress.proxy_url",
1755
+ label=_WEB_LABEL_WEB_EGRESS_PROXY_URL,
1756
+ coerce=web_proxy_url_coercer,
1757
+ apply=Apply.RELOAD,
1758
+ group="tools",
1759
+ kind=Kind.TEXT,
1760
+ project_merge=ProjectMerge.DENY,
1761
+ ),
1762
+ )
1763
+
1764
+ web_egress_proxy_dns: str = field(
1765
+ default="local",
1766
+ metadata=spec(
1767
+ key="tools.web_egress.proxy_dns",
1768
+ label=_WEB_LABEL_WEB_EGRESS_PROXY_DNS,
1769
+ # remote hands the final-address check to the proxy, so only the user may choose it.
1770
+ coerce=choice_coercer(choices=("local", "remote")),
1771
+ apply=Apply.RELOAD,
1772
+ group="tools",
1773
+ kind=Kind.ENUM,
1774
+ project_merge=ProjectMerge.DENY,
1775
+ choices=("local", "remote"),
1776
+ ),
1777
+ )
1778
+
1779
+ web_search_custom_endpoints: str = field(
1780
+ default="[]",
1781
+ metadata=spec(
1782
+ key="tools.web_search.custom_endpoints",
1783
+ label=_WEB_LABEL_WEB_SEARCH_CUSTOM_ENDPOINTS,
1784
+ coerce=web_custom_endpoints_coercer,
1785
+ apply=Apply.RELOAD,
1786
+ group="tools",
1787
+ kind=Kind.TEXT,
1788
+ project_merge=ProjectMerge.DENY,
1789
+ ),
1790
+ )
1791
+
1792
+ web_search_private_origins: str = field(
1793
+ default="[]",
1794
+ metadata=spec(
1795
+ key="tools.web_search.private_origins",
1796
+ label=_WEB_LABEL_WEB_SEARCH_PRIVATE_ORIGINS,
1797
+ coerce=web_origins_coercer,
1798
+ apply=Apply.RELOAD,
1799
+ group="tools",
1800
+ kind=Kind.TEXT,
1801
+ project_merge=ProjectMerge.DENY,
1802
+ ),
1803
+ )
1804
+
1805
+ web_search_http_origins: str = field(
1806
+ default="[]",
1807
+ metadata=spec(
1808
+ key="tools.web_search.http_origins",
1809
+ label=_WEB_LABEL_WEB_SEARCH_HTTP_ORIGINS,
1810
+ coerce=web_origins_coercer,
1811
+ apply=Apply.RELOAD,
1812
+ group="tools",
1813
+ kind=Kind.TEXT,
1814
+ project_merge=ProjectMerge.DENY,
1815
+ ),
1816
+ )
1817
+
1818
+ web_fetch_private_origins: str = field(
1819
+ default="[]",
1820
+ metadata=spec(
1821
+ key="tools.web_fetch.private_origins",
1822
+ label=_WEB_LABEL_WEB_FETCH_PRIVATE_ORIGINS,
1823
+ coerce=web_origins_coercer,
1824
+ apply=Apply.RELOAD,
1825
+ group="tools",
1826
+ kind=Kind.TEXT,
1827
+ project_merge=ProjectMerge.DENY,
1828
+ ),
1829
+ )
1830
+
1831
+ web_fetch_http_origins: str = field(
1832
+ default="[]",
1833
+ metadata=spec(
1834
+ key="tools.web_fetch.http_origins",
1835
+ label=_WEB_LABEL_WEB_FETCH_HTTP_ORIGINS,
1836
+ coerce=web_origins_coercer,
1837
+ apply=Apply.RELOAD,
1838
+ group="tools",
1839
+ kind=Kind.TEXT,
1840
+ project_merge=ProjectMerge.DENY,
1841
+ ),
1842
+ )
1843
+
1844
+ web_fetch_allowed_origins: str = field(
1845
+ default="[]",
1846
+ metadata=spec(
1847
+ key="tools.web_fetch.allowed_origins",
1848
+ label=_WEB_LABEL_WEB_FETCH_ALLOWED_ORIGINS,
1849
+ coerce=web_origins_coercer,
1850
+ apply=Apply.RELOAD,
1851
+ group="tools",
1852
+ kind=Kind.TEXT,
1853
+ project_merge=ProjectMerge.DENY,
1854
+ ),
1855
+ )
1856
+
1857
+ web_fetch_denied_origins: str = field(
1858
+ default="[]",
1859
+ metadata=spec(
1860
+ key="tools.web_fetch.denied_origins",
1861
+ label=_WEB_LABEL_WEB_FETCH_DENIED_ORIGINS,
1862
+ coerce=web_origins_coercer,
1863
+ apply=Apply.RELOAD,
1864
+ group="tools",
1865
+ kind=Kind.TEXT,
1866
+ project_merge=ProjectMerge.DENY,
1867
+ ),
1868
+ )
1869
+
1870
+ @classmethod
1871
+ def from_env(cls, **overrides: Any) -> Settings:
1872
+ """Create settings from every source, dropping provenance and warnings.
1873
+
1874
+ Compatibility shim. Production paths must call ``load_settings`` and
1875
+ keep all three parts of its result — a caller that throws provenance
1876
+ away cannot tell the user why the value they typed has no effect, and
1877
+ one that throws warnings away silently swallows rejected values.
1878
+
1879
+ ``overrides`` are pins, so each one must already be the field's
1880
+ canonical value: they outrank every layer and never meet a coercer, and
1881
+ a ``TypeError`` here is better than a value that is stored as one thing
1882
+ and shown to the panel as another. ``ask_user_timeout_seconds=0`` is
1883
+ rejected in favour of ``None``, for instance, rather than quietly
1884
+ becoming a zero-second timeout.
1885
+ """
1886
+ from chrys.foundation.config.settings_store import load_settings
1887
+
1888
+ return load_settings(**overrides).settings
1889
+
1890
+ def effective_max_transient_retries(self) -> int:
1891
+ """Return the environment override or this frontend's policy default."""
1892
+ if self.max_transient_retries is not None:
1893
+ return self.max_transient_retries
1894
+ return self.frontend_default_max_transient_retries
1895
+
1896
+
1897
+ def _persist_one(key: str, value: Any, *, log_value: bool = True) -> None:
1898
+ """Write one dotted key to the user settings document, without raising.
1899
+
1900
+ The thin shared body of the ``persist_*`` helpers: failures are logged and
1901
+ swallowed so a disk/permission issue doesn't crash the TUI, and a value the
1902
+ store refuses is logged rather than written. No ``os.environ`` mirror —
1903
+ the mirror would turn the panel's own write into an ``ENV``-layer value,
1904
+ and provenance would then call it "overridden, editing has no effect".
1905
+ """
1906
+ from chrys.foundation.config.settings_store import persist
1907
+
1908
+ try:
1909
+ result = persist({key: value})
1910
+ except Exception:
1911
+ if log_value:
1912
+ logger.warning("Failed to persist %s=%r", key, value, exc_info=True)
1913
+ else:
1914
+ logger.warning("Failed to persist %s", key, exc_info=True)
1915
+ return
1916
+ if not result.ok:
1917
+ if log_value:
1918
+ logger.warning("Refusing to persist %s: %r", key, value)
1919
+ else:
1920
+ logger.warning("Refusing to persist %s: value rejected", key)
1921
+
1922
+
1923
+ def persist_theme(theme: str) -> None:
1924
+ """Write ``ui.theme`` to the user settings document so it survives restart."""
1925
+ _persist_one("ui.theme", theme)
1926
+
1927
+
1928
+ def persist_locale(locale: str) -> None:
1929
+ """Write ``ui.locale`` to the user settings document without raising."""
1930
+ # Content-free logging by design: the requested value must not reach the log.
1931
+ _persist_one("ui.locale", locale, log_value=False)
1932
+
1933
+
1934
+ def persist_tool_groups_expanded(value: bool) -> None:
1935
+ """Persist whether newly created live tool groups start expanded."""
1936
+ _persist_one("ui.chat.tool_groups_expanded", value)
1937
+
1938
+
1939
+ def persist_approval_mode(mode: str) -> None:
1940
+ """Write ``approval.default_mode`` to the user settings document.
1941
+
1942
+ ``bypass`` never persists — :func:`chrys.foundation.config.settings_store.persist`
1943
+ downgrades it to ``auto`` so the *next* app launch cannot start in BYPASS
1944
+ mode; that mode must always be an explicit opt-in within a launch. Unknown
1945
+ values are ignored (logged, not written).
1946
+ """
1947
+ _persist_one("approval.default_mode", mode)
1948
+
1949
+
1950
+ def persist_editor_keymap(mode: str) -> None:
1951
+ """Persist the selected message-editor keymap."""
1952
+ _persist_one("ui.editor.keymap", mode)