@clawpump/claw-agent 0.1.5 → 0.1.7

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 (1212) hide show
  1. package/agent/.dockerignore +67 -0
  2. package/agent/.envrc +1 -1
  3. package/agent/.gitattributes +8 -0
  4. package/agent/AGENTS.md +216 -4
  5. package/agent/CONTRIBUTING.md +46 -8
  6. package/agent/Dockerfile +78 -35
  7. package/agent/MANIFEST.in +2 -0
  8. package/agent/README.md +12 -5
  9. package/agent/README.ur-pk.md +261 -0
  10. package/agent/README.zh-CN.md +11 -8
  11. package/agent/SECURITY.md +5 -4
  12. package/agent/acp_adapter/provenance.py +127 -0
  13. package/agent/acp_adapter/server.py +112 -5
  14. package/agent/acp_adapter/session.py +1 -6
  15. package/agent/acp_registry/agent.json +2 -2
  16. package/agent/agent/account_usage.py +313 -1
  17. package/agent/agent/agent_init.py +140 -37
  18. package/agent/agent/agent_runtime_helpers.py +342 -83
  19. package/agent/agent/anthropic_adapter.py +320 -33
  20. package/agent/agent/auxiliary_client.py +525 -105
  21. package/agent/agent/background_review.py +157 -19
  22. package/agent/agent/bedrock_adapter.py +71 -6
  23. package/agent/agent/billing_view.py +295 -0
  24. package/agent/agent/chat_completion_helpers.py +229 -4
  25. package/agent/agent/codex_responses_adapter.py +86 -10
  26. package/agent/agent/codex_runtime.py +153 -1
  27. package/agent/agent/coding_context.py +738 -0
  28. package/agent/agent/context_compressor.py +392 -44
  29. package/agent/agent/context_references.py +34 -1
  30. package/agent/agent/conversation_compression.py +159 -22
  31. package/agent/agent/conversation_loop.py +643 -908
  32. package/agent/agent/copilot_acp_client.py +4 -11
  33. package/agent/agent/credential_pool.py +5 -3
  34. package/agent/agent/credits_tracker.py +794 -0
  35. package/agent/agent/curator.py +91 -18
  36. package/agent/agent/curator_backup.py +26 -10
  37. package/agent/agent/display.py +42 -1
  38. package/agent/agent/error_classifier.py +52 -3
  39. package/agent/agent/errors.py +3 -0
  40. package/agent/agent/file_safety.py +0 -17
  41. package/agent/agent/gemini_native_adapter.py +31 -1
  42. package/agent/agent/i18n.py +48 -4
  43. package/agent/agent/image_gen_provider.py +74 -5
  44. package/agent/agent/image_routing.py +29 -0
  45. package/agent/agent/insights.py +8 -17
  46. package/agent/agent/lsp/install.py +3 -0
  47. package/agent/agent/memory_manager.py +326 -31
  48. package/agent/agent/message_content.py +50 -0
  49. package/agent/agent/model_metadata.py +214 -3
  50. package/agent/agent/moonshot_schema.py +8 -1
  51. package/agent/agent/onboarding.py +60 -0
  52. package/agent/agent/prompt_builder.py +327 -37
  53. package/agent/agent/redact.py +1 -0
  54. package/agent/agent/runtime_cwd.py +34 -5
  55. package/agent/agent/secret_scope.py +205 -0
  56. package/agent/agent/secret_sources/bitwarden.py +34 -2
  57. package/agent/agent/skill_commands.py +90 -1
  58. package/agent/agent/skill_preprocessing.py +1 -0
  59. package/agent/agent/skill_utils.py +209 -36
  60. package/agent/agent/ssl_guard.py +94 -0
  61. package/agent/agent/system_prompt.py +133 -5
  62. package/agent/agent/tool_executor.py +496 -70
  63. package/agent/agent/transports/anthropic.py +83 -21
  64. package/agent/agent/transports/chat_completions.py +94 -5
  65. package/agent/agent/transports/codex.py +67 -2
  66. package/agent/agent/transports/codex_app_server.py +1 -0
  67. package/agent/agent/transports/codex_app_server_session.py +30 -0
  68. package/agent/agent/transports/types.py +12 -0
  69. package/agent/agent/turn_context.py +408 -0
  70. package/agent/agent/turn_finalizer.py +428 -0
  71. package/agent/agent/turn_retry_state.py +68 -0
  72. package/agent/agent/usage_pricing.py +3 -0
  73. package/agent/apps/bootstrap-installer/package.json +6 -5
  74. package/agent/apps/bootstrap-installer/src/routes/failure.tsx +12 -5
  75. package/agent/apps/bootstrap-installer/src/routes/progress.tsx +1 -3
  76. package/agent/apps/bootstrap-installer/src/store.ts +3 -2
  77. package/agent/apps/bootstrap-installer/src-tauri/src/bootstrap.rs +172 -7
  78. package/agent/apps/bootstrap-installer/src-tauri/src/events.rs +14 -1
  79. package/agent/apps/bootstrap-installer/src-tauri/src/paths.rs +29 -0
  80. package/agent/apps/bootstrap-installer/src-tauri/src/powershell.rs +93 -3
  81. package/agent/apps/bootstrap-installer/src-tauri/src/update.rs +695 -39
  82. package/agent/apps/bootstrap-installer/tsconfig.json +3 -4
  83. package/agent/apps/desktop/DESIGN.md +167 -0
  84. package/agent/apps/desktop/README.md +20 -16
  85. package/agent/apps/desktop/assets/icon.icns +0 -0
  86. package/agent/apps/desktop/assets/icon.ico +0 -0
  87. package/agent/apps/desktop/assets/icon.png +0 -0
  88. package/agent/apps/desktop/electron/backend-env.cjs +112 -0
  89. package/agent/apps/desktop/electron/backend-env.test.cjs +111 -0
  90. package/agent/apps/desktop/electron/backend-probes.test.cjs +3 -1
  91. package/agent/apps/desktop/electron/backend-ready.cjs +66 -0
  92. package/agent/apps/desktop/electron/bootstrap-platform.cjs +52 -0
  93. package/agent/apps/desktop/electron/bootstrap-platform.test.cjs +59 -1
  94. package/agent/apps/desktop/electron/bootstrap-runner.cjs +176 -38
  95. package/agent/apps/desktop/electron/bootstrap-runner.test.cjs +112 -1
  96. package/agent/apps/desktop/electron/connection-config.cjs +288 -0
  97. package/agent/apps/desktop/electron/connection-config.test.cjs +396 -0
  98. package/agent/apps/desktop/electron/dashboard-token.cjs +99 -0
  99. package/agent/apps/desktop/electron/dashboard-token.test.cjs +142 -0
  100. package/agent/apps/desktop/electron/desktop-uninstall.cjs +232 -0
  101. package/agent/apps/desktop/electron/desktop-uninstall.test.cjs +246 -0
  102. package/agent/apps/desktop/electron/entitlements.mac.inherit.plist +2 -0
  103. package/agent/apps/desktop/electron/fs-read-dir.cjs +109 -0
  104. package/agent/apps/desktop/electron/fs-read-dir.test.cjs +364 -0
  105. package/agent/apps/desktop/electron/gateway-ws-probe.cjs +188 -0
  106. package/agent/apps/desktop/electron/gateway-ws-probe.test.cjs +122 -0
  107. package/agent/apps/desktop/electron/git-root.cjs +54 -0
  108. package/agent/apps/desktop/electron/git-root.test.cjs +40 -0
  109. package/agent/apps/desktop/electron/git-worktrees.cjs +174 -0
  110. package/agent/apps/desktop/electron/hardening.cjs +123 -28
  111. package/agent/apps/desktop/electron/hardening.test.cjs +163 -0
  112. package/agent/apps/desktop/electron/main.cjs +3121 -331
  113. package/agent/apps/desktop/electron/oauth-net-request.cjs +20 -0
  114. package/agent/apps/desktop/electron/oauth-net-request.test.cjs +34 -0
  115. package/agent/apps/desktop/electron/preload.cjs +52 -2
  116. package/agent/apps/desktop/electron/session-windows.cjs +124 -0
  117. package/agent/apps/desktop/electron/session-windows.test.cjs +199 -0
  118. package/agent/apps/desktop/electron/update-rebuild.cjs +29 -0
  119. package/agent/apps/desktop/electron/update-rebuild.test.cjs +55 -0
  120. package/agent/apps/desktop/electron/update-remote.cjs +56 -0
  121. package/agent/apps/desktop/electron/update-remote.test.cjs +78 -0
  122. package/agent/apps/desktop/electron/vscode-marketplace.cjs +331 -0
  123. package/agent/apps/desktop/electron/vscode-marketplace.test.cjs +113 -0
  124. package/agent/apps/desktop/electron/windows-child-process.test.cjs +57 -0
  125. package/agent/apps/desktop/electron/windows-user-env.cjs +76 -0
  126. package/agent/apps/desktop/electron/windows-user-env.test.cjs +90 -0
  127. package/agent/apps/desktop/electron/workspace-cwd.cjs +38 -0
  128. package/agent/apps/desktop/electron/workspace-cwd.test.cjs +45 -0
  129. package/agent/apps/desktop/eslint.config.mjs +0 -3
  130. package/agent/apps/desktop/index.html +27 -2
  131. package/agent/apps/desktop/package.json +31 -11
  132. package/agent/apps/desktop/pr-assets/session-source-folders.png +0 -0
  133. package/agent/apps/desktop/public/apple-touch-icon.png +0 -0
  134. package/agent/apps/desktop/public/nous-girl.jpg +0 -0
  135. package/agent/apps/desktop/scripts/assert-dist-built.cjs +70 -0
  136. package/agent/apps/desktop/scripts/assert-dist-built.test.cjs +84 -0
  137. package/agent/apps/desktop/scripts/before-pack.cjs +78 -0
  138. package/agent/apps/desktop/scripts/before-pack.test.cjs +53 -0
  139. package/agent/apps/desktop/scripts/diag-scroll-reset.mjs +229 -0
  140. package/agent/apps/desktop/scripts/patch-electron-builder-mac-binary.cjs +64 -0
  141. package/agent/apps/desktop/scripts/run-electron-builder.cjs +57 -0
  142. package/agent/apps/desktop/src/app/agents/index.tsx +53 -45
  143. package/agent/apps/desktop/src/app/artifacts/index.tsx +102 -83
  144. package/agent/apps/desktop/src/app/chat/chat-drop-overlay.tsx +29 -8
  145. package/agent/apps/desktop/src/app/chat/chat-swap-overlay.tsx +47 -0
  146. package/agent/apps/desktop/src/app/chat/composer/attachments.tsx +81 -45
  147. package/agent/apps/desktop/src/app/chat/composer/completion-drawer.tsx +13 -24
  148. package/agent/apps/desktop/src/app/chat/composer/context-menu.tsx +138 -88
  149. package/agent/apps/desktop/src/app/chat/composer/controls.tsx +138 -90
  150. package/agent/apps/desktop/src/app/chat/composer/enter-submit-dom-race.test.tsx +218 -0
  151. package/agent/apps/desktop/src/app/chat/composer/focus.ts +32 -0
  152. package/agent/apps/desktop/src/app/chat/composer/help-hint.tsx +38 -25
  153. package/agent/apps/desktop/src/app/chat/composer/hooks/use-live-completion-adapter.ts +7 -0
  154. package/agent/apps/desktop/src/app/chat/composer/hooks/use-mic-recorder.ts +22 -12
  155. package/agent/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.ts +142 -14
  156. package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-conversation.ts +14 -11
  157. package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-recorder.ts +9 -6
  158. package/agent/apps/desktop/src/app/chat/composer/ime-composition-dom-repro.test.tsx +108 -0
  159. package/agent/apps/desktop/src/app/chat/composer/index.tsx +930 -180
  160. package/agent/apps/desktop/src/app/chat/composer/inline-refs.ts +136 -32
  161. package/agent/apps/desktop/src/app/chat/composer/model-pill.tsx +86 -0
  162. package/agent/apps/desktop/src/app/chat/composer/queue-panel.tsx +54 -75
  163. package/agent/apps/desktop/src/app/chat/composer/rich-editor.test.ts +117 -1
  164. package/agent/apps/desktop/src/app/chat/composer/rich-editor.ts +117 -6
  165. package/agent/apps/desktop/src/app/chat/composer/slash-nav-dom-repro.test.tsx +186 -0
  166. package/agent/apps/desktop/src/app/chat/composer/status-stack/index.tsx +202 -0
  167. package/agent/apps/desktop/src/app/chat/composer/status-stack/status-row.tsx +155 -0
  168. package/agent/apps/desktop/src/app/chat/composer/text-utils.test.ts +104 -0
  169. package/agent/apps/desktop/src/app/chat/composer/text-utils.ts +37 -9
  170. package/agent/apps/desktop/src/app/chat/composer/trigger-popover.test.tsx +50 -0
  171. package/agent/apps/desktop/src/app/chat/composer/trigger-popover.tsx +105 -40
  172. package/agent/apps/desktop/src/app/chat/composer/types.ts +5 -0
  173. package/agent/apps/desktop/src/app/chat/composer/url-dialog.tsx +11 -15
  174. package/agent/apps/desktop/src/app/chat/composer/voice-activity.tsx +8 -4
  175. package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.test.ts +57 -0
  176. package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.ts +70 -16
  177. package/agent/apps/desktop/src/app/chat/hooks/use-file-drop-zone.ts +52 -16
  178. package/agent/apps/desktop/src/app/chat/index.tsx +234 -81
  179. package/agent/apps/desktop/src/app/chat/perf-probe.tsx +69 -21
  180. package/agent/apps/desktop/src/app/chat/right-rail/preview-console.tsx +44 -40
  181. package/agent/apps/desktop/src/app/chat/right-rail/preview-file.tsx +71 -25
  182. package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx +40 -1
  183. package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.tsx +55 -53
  184. package/agent/apps/desktop/src/app/chat/right-rail/preview.tsx +35 -17
  185. package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.test.tsx +67 -0
  186. package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.tsx +74 -0
  187. package/agent/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx +356 -0
  188. package/agent/apps/desktop/src/app/chat/sidebar/index.tsx +1189 -364
  189. package/agent/apps/desktop/src/app/chat/sidebar/load-more-row.tsx +30 -0
  190. package/agent/apps/desktop/src/app/chat/sidebar/order.test.ts +21 -0
  191. package/agent/apps/desktop/src/app/chat/sidebar/order.ts +17 -0
  192. package/agent/apps/desktop/src/app/chat/sidebar/profile-switcher.tsx +524 -0
  193. package/agent/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx +80 -45
  194. package/agent/apps/desktop/src/app/chat/sidebar/session-row.tsx +120 -25
  195. package/agent/apps/desktop/src/app/chat/sidebar/virtual-session-list.tsx +7 -13
  196. package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.test.ts +149 -0
  197. package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.ts +326 -0
  198. package/agent/apps/desktop/src/app/chat/thread-loading.ts +7 -2
  199. package/agent/apps/desktop/src/app/command-center/index.tsx +320 -581
  200. package/agent/apps/desktop/src/app/command-palette/index.tsx +681 -0
  201. package/agent/apps/desktop/src/app/command-palette/marketplace-theme-page.tsx +157 -0
  202. package/agent/apps/desktop/src/app/cron/index.tsx +392 -324
  203. package/agent/apps/desktop/src/app/cron/job-state.ts +29 -0
  204. package/agent/apps/desktop/src/app/desktop-controller.tsx +618 -123
  205. package/agent/apps/desktop/src/app/floating-hud.ts +22 -0
  206. package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.test.tsx +265 -0
  207. package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts +260 -14
  208. package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-request.ts +48 -4
  209. package/agent/apps/desktop/src/app/hooks/use-keybinds.ts +270 -0
  210. package/agent/apps/desktop/src/app/hooks/use-refresh-hotkey.ts +45 -0
  211. package/agent/apps/desktop/src/app/layout-constants.ts +19 -0
  212. package/agent/apps/desktop/src/app/messaging/index.tsx +136 -241
  213. package/agent/apps/desktop/src/app/messaging/platform-icon.tsx +95 -0
  214. package/agent/apps/desktop/src/app/model-visibility-overlay.tsx +31 -0
  215. package/agent/apps/desktop/src/app/overlays/overlay-search-input.tsx +18 -62
  216. package/agent/apps/desktop/src/app/overlays/overlay-split-layout.tsx +59 -7
  217. package/agent/apps/desktop/src/app/overlays/overlay-view.tsx +9 -5
  218. package/agent/apps/desktop/src/app/page-search-shell.tsx +42 -20
  219. package/agent/apps/desktop/src/app/profiles/create-profile-dialog.tsx +165 -0
  220. package/agent/apps/desktop/src/app/profiles/delete-profile-dialog.tsx +65 -0
  221. package/agent/apps/desktop/src/app/profiles/index.tsx +174 -199
  222. package/agent/apps/desktop/src/app/profiles/rename-profile-dialog.tsx +125 -0
  223. package/agent/apps/desktop/src/app/right-sidebar/files/dnd-manager.ts +27 -0
  224. package/agent/apps/desktop/src/app/right-sidebar/files/ipc.test.ts +100 -0
  225. package/agent/apps/desktop/src/app/right-sidebar/files/ipc.ts +12 -18
  226. package/agent/apps/desktop/src/app/right-sidebar/files/remote-picker.tsx +177 -0
  227. package/agent/apps/desktop/src/app/right-sidebar/files/tree.tsx +35 -21
  228. package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.test.ts +75 -3
  229. package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.ts +152 -5
  230. package/agent/apps/desktop/src/app/right-sidebar/index.test.tsx +75 -0
  231. package/agent/apps/desktop/src/app/right-sidebar/index.tsx +166 -129
  232. package/agent/apps/desktop/src/app/right-sidebar/store.ts +19 -4
  233. package/agent/apps/desktop/src/app/right-sidebar/terminal/buffer.ts +65 -0
  234. package/agent/apps/desktop/src/app/right-sidebar/terminal/index.tsx +29 -34
  235. package/agent/apps/desktop/src/app/right-sidebar/terminal/persistent.tsx +18 -6
  236. package/agent/apps/desktop/src/app/right-sidebar/terminal/selection.ts +93 -32
  237. package/agent/apps/desktop/src/app/right-sidebar/terminal/use-terminal-session.ts +381 -119
  238. package/agent/apps/desktop/src/app/routes.ts +9 -0
  239. package/agent/apps/desktop/src/app/session/hooks/use-cwd-actions.ts +17 -7
  240. package/agent/apps/desktop/src/app/session/hooks/use-message-stream.ts +365 -47
  241. package/agent/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx +198 -0
  242. package/agent/apps/desktop/src/app/session/hooks/use-model-controls.ts +70 -34
  243. package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.test.tsx +1061 -0
  244. package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.ts +1143 -165
  245. package/agent/apps/desktop/src/app/session/hooks/use-route-resume.test.tsx +341 -2
  246. package/agent/apps/desktop/src/app/session/hooks/use-route-resume.ts +176 -5
  247. package/agent/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx +259 -0
  248. package/agent/apps/desktop/src/app/session/hooks/use-session-actions.ts +452 -149
  249. package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.test.tsx +327 -0
  250. package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.ts +133 -4
  251. package/agent/apps/desktop/src/app/session-picker-overlay.tsx +32 -0
  252. package/agent/apps/desktop/src/app/session-switcher.tsx +107 -0
  253. package/agent/apps/desktop/src/app/settings/about-settings.tsx +45 -36
  254. package/agent/apps/desktop/src/app/settings/appearance-settings.tsx +243 -162
  255. package/agent/apps/desktop/src/app/settings/config-settings.tsx +86 -66
  256. package/agent/apps/desktop/src/app/settings/constants.ts +459 -122
  257. package/agent/apps/desktop/src/app/settings/credential-key-ui.tsx +373 -0
  258. package/agent/apps/desktop/src/app/settings/env-credentials.tsx +198 -0
  259. package/agent/apps/desktop/src/app/settings/env-var-actions-menu.tsx +136 -0
  260. package/agent/apps/desktop/src/app/settings/field-copy.ts +56 -0
  261. package/agent/apps/desktop/src/app/settings/gateway-settings.tsx +385 -72
  262. package/agent/apps/desktop/src/app/settings/helpers.test.ts +156 -1
  263. package/agent/apps/desktop/src/app/settings/helpers.ts +30 -2
  264. package/agent/apps/desktop/src/app/settings/index.tsx +118 -84
  265. package/agent/apps/desktop/src/app/settings/keys-settings.tsx +62 -419
  266. package/agent/apps/desktop/src/app/settings/mcp-settings.tsx +65 -60
  267. package/agent/apps/desktop/src/app/settings/model-settings.test.tsx +129 -5
  268. package/agent/apps/desktop/src/app/settings/model-settings.tsx +370 -65
  269. package/agent/apps/desktop/src/app/settings/notifications-settings.tsx +150 -0
  270. package/agent/apps/desktop/src/app/settings/primitives.tsx +5 -11
  271. package/agent/apps/desktop/src/app/settings/provider-config-panel.test.tsx +142 -0
  272. package/agent/apps/desktop/src/app/settings/provider-config-panel.tsx +182 -0
  273. package/agent/apps/desktop/src/app/settings/providers-settings.test.tsx +171 -0
  274. package/agent/apps/desktop/src/app/settings/providers-settings.tsx +471 -0
  275. package/agent/apps/desktop/src/app/settings/sessions-settings.tsx +183 -71
  276. package/agent/apps/desktop/src/app/settings/toolset-config-panel.test.tsx +135 -1
  277. package/agent/apps/desktop/src/app/settings/toolset-config-panel.tsx +180 -57
  278. package/agent/apps/desktop/src/app/settings/types.ts +9 -6
  279. package/agent/apps/desktop/src/app/settings/uninstall-section.tsx +185 -0
  280. package/agent/apps/desktop/src/app/settings/use-deep-link-highlight.ts +60 -0
  281. package/agent/apps/desktop/src/app/shell/app-shell.tsx +59 -13
  282. package/agent/apps/desktop/src/app/shell/gateway-menu-panel.tsx +37 -32
  283. package/agent/apps/desktop/src/app/shell/hooks/use-overlay-routing.ts +6 -3
  284. package/agent/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx +212 -53
  285. package/agent/apps/desktop/src/app/shell/keybind-panel.tsx +215 -0
  286. package/agent/apps/desktop/src/app/shell/model-edit-submenu.test.tsx +84 -0
  287. package/agent/apps/desktop/src/app/shell/model-edit-submenu.tsx +244 -0
  288. package/agent/apps/desktop/src/app/shell/model-menu-panel.tsx +392 -0
  289. package/agent/apps/desktop/src/app/shell/statusbar-controls.tsx +23 -33
  290. package/agent/apps/desktop/src/app/shell/titlebar-controls.tsx +79 -95
  291. package/agent/apps/desktop/src/app/shell/titlebar.ts +8 -2
  292. package/agent/apps/desktop/src/app/skills/index.test.tsx +11 -0
  293. package/agent/apps/desktop/src/app/skills/index.tsx +79 -64
  294. package/agent/apps/desktop/src/app/types.ts +85 -0
  295. package/agent/apps/desktop/src/app/updates-overlay.tsx +110 -105
  296. package/agent/apps/desktop/src/components/assistant-ui/ansi-text.tsx +34 -0
  297. package/agent/apps/desktop/src/components/assistant-ui/block-direction.test.tsx +129 -0
  298. package/agent/apps/desktop/src/components/assistant-ui/clarify-tool.tsx +102 -81
  299. package/agent/apps/desktop/src/components/assistant-ui/directive-text.tsx +92 -15
  300. package/agent/apps/desktop/src/components/assistant-ui/markdown-text.test.ts +38 -0
  301. package/agent/apps/desktop/src/components/assistant-ui/markdown-text.tsx +304 -45
  302. package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.test.tsx +80 -0
  303. package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.tsx +48 -0
  304. package/agent/apps/desktop/src/components/assistant-ui/streaming.test.tsx +142 -90
  305. package/agent/apps/desktop/src/components/assistant-ui/thread-list.tsx +337 -0
  306. package/agent/apps/desktop/src/components/assistant-ui/thread.tsx +667 -190
  307. package/agent/apps/desktop/src/components/assistant-ui/tool-approval-group.test.tsx +299 -0
  308. package/agent/apps/desktop/src/components/assistant-ui/tool-approval.test.tsx +133 -0
  309. package/agent/apps/desktop/src/components/assistant-ui/tool-approval.tsx +239 -0
  310. package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.test.ts +31 -0
  311. package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.ts +152 -134
  312. package/agent/apps/desktop/src/components/assistant-ui/tool-fallback.tsx +142 -150
  313. package/agent/apps/desktop/src/components/assistant-ui/tooltip-icon-button.tsx +14 -12
  314. package/agent/apps/desktop/src/components/assistant-ui/user-message-edit.test.tsx +141 -0
  315. package/agent/apps/desktop/src/components/assistant-ui/user-message-text.tsx +152 -0
  316. package/agent/apps/desktop/src/components/boot-failure-overlay.tsx +150 -33
  317. package/agent/apps/desktop/src/components/boot-failure-reauth.test.ts +100 -0
  318. package/agent/apps/desktop/src/components/boot-failure-reauth.ts +81 -0
  319. package/agent/apps/desktop/src/components/brand-mark.tsx +19 -0
  320. package/agent/apps/desktop/src/components/chat/code-card.tsx +1 -1
  321. package/agent/apps/desktop/src/components/chat/composer-dock.ts +31 -0
  322. package/agent/apps/desktop/src/components/chat/diff-lines.tsx +1 -1
  323. package/agent/apps/desktop/src/components/chat/disclosure-row.tsx +13 -3
  324. package/agent/apps/desktop/src/components/chat/expandable-block.tsx +52 -0
  325. package/agent/apps/desktop/src/components/chat/generated-image-result.tsx +174 -0
  326. package/agent/apps/desktop/src/components/chat/image-generation-placeholder.tsx +70 -37
  327. package/agent/apps/desktop/src/components/chat/intro.tsx +8 -7
  328. package/agent/apps/desktop/src/components/chat/preview-attachment.tsx +4 -2
  329. package/agent/apps/desktop/src/components/chat/shiki-highlighter.test.ts +37 -0
  330. package/agent/apps/desktop/src/components/chat/shiki-highlighter.tsx +96 -22
  331. package/agent/apps/desktop/src/components/chat/status-row.tsx +70 -0
  332. package/agent/apps/desktop/src/components/chat/status-section.tsx +42 -0
  333. package/agent/apps/desktop/src/components/chat/terminal-output.tsx +54 -0
  334. package/agent/apps/desktop/src/components/chat/zoomable-image.tsx +70 -109
  335. package/agent/apps/desktop/src/components/desktop-install-overlay.tsx +154 -84
  336. package/agent/apps/desktop/src/components/desktop-onboarding-overlay.test.tsx +38 -8
  337. package/agent/apps/desktop/src/components/desktop-onboarding-overlay.tsx +789 -233
  338. package/agent/apps/desktop/src/components/error-boundary.tsx +77 -0
  339. package/agent/apps/desktop/src/components/gateway-connecting-overlay.test.tsx +144 -0
  340. package/agent/apps/desktop/src/components/gateway-connecting-overlay.tsx +7 -1
  341. package/agent/apps/desktop/src/components/haptics-provider.tsx +24 -0
  342. package/agent/apps/desktop/src/components/language-switcher.test.tsx +53 -0
  343. package/agent/apps/desktop/src/components/language-switcher.tsx +175 -0
  344. package/agent/apps/desktop/src/components/model-picker.tsx +42 -40
  345. package/agent/apps/desktop/src/components/model-visibility-dialog.tsx +166 -0
  346. package/agent/apps/desktop/src/components/notifications.tsx +48 -27
  347. package/agent/apps/desktop/src/components/pane-shell/index.ts +1 -1
  348. package/agent/apps/desktop/src/components/pane-shell/pane-shell.tsx +146 -9
  349. package/agent/apps/desktop/src/components/prompt-overlays.tsx +234 -0
  350. package/agent/apps/desktop/src/components/session-picker.tsx +108 -0
  351. package/agent/apps/desktop/src/components/ui/action-status.tsx +25 -0
  352. package/agent/apps/desktop/src/components/ui/badge.tsx +35 -0
  353. package/agent/apps/desktop/src/components/ui/button.tsx +37 -13
  354. package/agent/apps/desktop/src/components/ui/confirm-dialog.tsx +109 -0
  355. package/agent/apps/desktop/src/components/ui/control.ts +25 -0
  356. package/agent/apps/desktop/src/components/ui/copy-button.test.tsx +36 -0
  357. package/agent/apps/desktop/src/components/ui/copy-button.tsx +38 -27
  358. package/agent/apps/desktop/src/components/ui/dialog.tsx +39 -11
  359. package/agent/apps/desktop/src/components/ui/dropdown-menu.tsx +98 -24
  360. package/agent/apps/desktop/src/components/ui/error-state.tsx +50 -0
  361. package/agent/apps/desktop/src/components/ui/fade-text.tsx +9 -2
  362. package/agent/apps/desktop/src/components/ui/{braille-spinner.tsx → glyph-spinner.tsx} +15 -13
  363. package/agent/apps/desktop/src/components/ui/input.tsx +5 -2
  364. package/agent/apps/desktop/src/components/ui/kbd.tsx +83 -12
  365. package/agent/apps/desktop/src/components/ui/log-view.tsx +19 -0
  366. package/agent/apps/desktop/src/components/ui/pagination.tsx +12 -5
  367. package/agent/apps/desktop/src/components/ui/popover.tsx +44 -0
  368. package/agent/apps/desktop/src/components/ui/search-field.tsx +80 -0
  369. package/agent/apps/desktop/src/components/ui/segmented-control.tsx +51 -0
  370. package/agent/apps/desktop/src/components/ui/select.tsx +10 -3
  371. package/agent/apps/desktop/src/components/ui/sheet.tsx +8 -2
  372. package/agent/apps/desktop/src/components/ui/sidebar.tsx +18 -25
  373. package/agent/apps/desktop/src/components/ui/switch.tsx +38 -15
  374. package/agent/apps/desktop/src/components/ui/textarea.tsx +4 -11
  375. package/agent/apps/desktop/src/components/ui/tool-icon.tsx +65 -0
  376. package/agent/apps/desktop/src/components/ui/tooltip.tsx +31 -4
  377. package/agent/apps/desktop/src/fonts/JetBrainsMono-Bold.woff2 +0 -0
  378. package/agent/apps/desktop/src/fonts/JetBrainsMono-Italic.woff2 +0 -0
  379. package/agent/apps/desktop/src/fonts/JetBrainsMono-Regular.woff2 +0 -0
  380. package/agent/apps/desktop/src/global.d.ts +181 -4
  381. package/agent/apps/desktop/src/hermes.test.ts +60 -0
  382. package/agent/apps/desktop/src/hermes.ts +190 -13
  383. package/agent/apps/desktop/src/hooks/use-image-download.ts +85 -0
  384. package/agent/apps/desktop/src/hooks/use-resize-observer.ts +13 -4
  385. package/agent/apps/desktop/src/hooks/use-worktree-info.ts +68 -0
  386. package/agent/apps/desktop/src/i18n/catalog.ts +12 -0
  387. package/agent/apps/desktop/src/i18n/context.test.tsx +232 -0
  388. package/agent/apps/desktop/src/i18n/context.tsx +183 -0
  389. package/agent/apps/desktop/src/i18n/define-locale.ts +41 -0
  390. package/agent/apps/desktop/src/i18n/en.ts +1921 -0
  391. package/agent/apps/desktop/src/i18n/index.ts +20 -0
  392. package/agent/apps/desktop/src/i18n/ja.ts +2053 -0
  393. package/agent/apps/desktop/src/i18n/languages.test.ts +43 -0
  394. package/agent/apps/desktop/src/i18n/languages.ts +86 -0
  395. package/agent/apps/desktop/src/i18n/runtime.test.ts +75 -0
  396. package/agent/apps/desktop/src/i18n/runtime.ts +53 -0
  397. package/agent/apps/desktop/src/i18n/types.ts +1559 -0
  398. package/agent/apps/desktop/src/i18n/zh-hant.ts +1992 -0
  399. package/agent/apps/desktop/src/i18n/zh.ts +2099 -0
  400. package/agent/apps/desktop/src/lib/ansi.test.ts +123 -0
  401. package/agent/apps/desktop/src/lib/ansi.ts +186 -0
  402. package/agent/apps/desktop/src/lib/chat-messages.test.ts +79 -0
  403. package/agent/apps/desktop/src/lib/chat-messages.ts +68 -29
  404. package/agent/apps/desktop/src/lib/chat-runtime.test.ts +65 -1
  405. package/agent/apps/desktop/src/lib/chat-runtime.ts +39 -3
  406. package/agent/apps/desktop/src/lib/completion-sound.ts +519 -0
  407. package/agent/apps/desktop/src/lib/desktop-fs.test.ts +116 -0
  408. package/agent/apps/desktop/src/lib/desktop-fs.ts +113 -0
  409. package/agent/apps/desktop/src/lib/desktop-slash-commands.test.ts +89 -6
  410. package/agent/apps/desktop/src/lib/desktop-slash-commands.ts +270 -131
  411. package/agent/apps/desktop/src/lib/external-link.test.tsx +27 -0
  412. package/agent/apps/desktop/src/lib/external-link.tsx +9 -2
  413. package/agent/apps/desktop/src/lib/gateway-events.test.ts +27 -0
  414. package/agent/apps/desktop/src/lib/gateway-events.ts +16 -0
  415. package/agent/apps/desktop/src/lib/gateway-ws-url.test.ts +78 -0
  416. package/agent/apps/desktop/src/lib/gateway-ws-url.ts +91 -0
  417. package/agent/apps/desktop/src/lib/generated-images.test.ts +97 -0
  418. package/agent/apps/desktop/src/lib/generated-images.ts +116 -0
  419. package/agent/apps/desktop/src/lib/haptics.ts +17 -0
  420. package/agent/apps/desktop/src/lib/icons.ts +10 -2
  421. package/agent/apps/desktop/src/lib/keybinds/actions.ts +137 -0
  422. package/agent/apps/desktop/src/lib/keybinds/combo.test.ts +86 -0
  423. package/agent/apps/desktop/src/lib/keybinds/combo.ts +195 -0
  424. package/agent/apps/desktop/src/lib/local-preview.ts +23 -2
  425. package/agent/apps/desktop/src/lib/markdown-preprocess.ts +20 -7
  426. package/agent/apps/desktop/src/lib/media.remote.test.ts +90 -0
  427. package/agent/apps/desktop/src/lib/media.ts +40 -1
  428. package/agent/apps/desktop/src/lib/model-status-label.test.ts +59 -0
  429. package/agent/apps/desktop/src/lib/model-status-label.ts +122 -0
  430. package/agent/apps/desktop/src/lib/mutable-ref.ts +6 -0
  431. package/agent/apps/desktop/src/lib/profile-color.ts +58 -0
  432. package/agent/apps/desktop/src/lib/query-client.ts +13 -0
  433. package/agent/apps/desktop/src/lib/remend-tail.test.ts +105 -0
  434. package/agent/apps/desktop/src/lib/remend-tail.ts +108 -0
  435. package/agent/apps/desktop/src/lib/session-export.ts +6 -3
  436. package/agent/apps/desktop/src/lib/session-ids.test.ts +44 -0
  437. package/agent/apps/desktop/src/lib/session-ids.ts +26 -0
  438. package/agent/apps/desktop/src/lib/session-search.test.ts +66 -0
  439. package/agent/apps/desktop/src/lib/session-search.ts +21 -0
  440. package/agent/apps/desktop/src/lib/session-source.ts +126 -0
  441. package/agent/apps/desktop/src/lib/storage.test.ts +25 -0
  442. package/agent/apps/desktop/src/lib/storage.ts +35 -1
  443. package/agent/apps/desktop/src/lib/todos.test.ts +46 -1
  444. package/agent/apps/desktop/src/lib/todos.ts +37 -0
  445. package/agent/apps/desktop/src/lib/tool-result-summary.ts +5 -1
  446. package/agent/apps/desktop/src/lib/update-copy.test.ts +38 -0
  447. package/agent/apps/desktop/src/lib/update-copy.ts +44 -0
  448. package/agent/apps/desktop/src/lib/use-enter-animation.ts +2 -2
  449. package/agent/apps/desktop/src/lib/yolo-session.ts +50 -0
  450. package/agent/apps/desktop/src/main.tsx +19 -19
  451. package/agent/apps/desktop/src/store/boot.ts +4 -3
  452. package/agent/apps/desktop/src/store/clarify.test.ts +81 -0
  453. package/agent/apps/desktop/src/store/clarify.ts +50 -13
  454. package/agent/apps/desktop/src/store/command-palette.ts +20 -0
  455. package/agent/apps/desktop/src/store/compaction.test.ts +53 -0
  456. package/agent/apps/desktop/src/store/compaction.ts +38 -0
  457. package/agent/apps/desktop/src/store/completion-sound.ts +32 -0
  458. package/agent/apps/desktop/src/store/composer-input-history.test.ts +147 -0
  459. package/agent/apps/desktop/src/store/composer-input-history.ts +158 -0
  460. package/agent/apps/desktop/src/store/composer-queue.test.ts +68 -0
  461. package/agent/apps/desktop/src/store/composer-queue.ts +76 -0
  462. package/agent/apps/desktop/src/store/composer-status.test.ts +99 -0
  463. package/agent/apps/desktop/src/store/composer-status.ts +277 -0
  464. package/agent/apps/desktop/src/store/composer.test.ts +106 -0
  465. package/agent/apps/desktop/src/store/composer.ts +116 -0
  466. package/agent/apps/desktop/src/store/cron.ts +19 -0
  467. package/agent/apps/desktop/src/store/gateway.ts +280 -6
  468. package/agent/apps/desktop/src/store/keybinds.ts +143 -0
  469. package/agent/apps/desktop/src/store/layout.ts +107 -9
  470. package/agent/apps/desktop/src/store/model-presets.test.ts +51 -0
  471. package/agent/apps/desktop/src/store/model-presets.ts +86 -0
  472. package/agent/apps/desktop/src/store/model-visibility.test.ts +99 -0
  473. package/agent/apps/desktop/src/store/model-visibility.ts +161 -0
  474. package/agent/apps/desktop/src/store/native-notifications.test.ts +192 -0
  475. package/agent/apps/desktop/src/store/native-notifications.ts +203 -0
  476. package/agent/apps/desktop/src/store/notifications.ts +10 -7
  477. package/agent/apps/desktop/src/store/onboarding.test.ts +271 -1
  478. package/agent/apps/desktop/src/store/onboarding.ts +268 -38
  479. package/agent/apps/desktop/src/store/preview.ts +10 -1
  480. package/agent/apps/desktop/src/store/profile.test.ts +89 -0
  481. package/agent/apps/desktop/src/store/profile.ts +395 -0
  482. package/agent/apps/desktop/src/store/prompts.test.ts +127 -0
  483. package/agent/apps/desktop/src/store/prompts.ts +117 -0
  484. package/agent/apps/desktop/src/store/session-switcher.test.ts +115 -0
  485. package/agent/apps/desktop/src/store/session-switcher.ts +128 -0
  486. package/agent/apps/desktop/src/store/session-sync.ts +25 -0
  487. package/agent/apps/desktop/src/store/session.test.ts +268 -2
  488. package/agent/apps/desktop/src/store/session.ts +392 -18
  489. package/agent/apps/desktop/src/store/subagents.ts +3 -0
  490. package/agent/apps/desktop/src/store/system-actions.ts +48 -0
  491. package/agent/apps/desktop/src/store/thread-scroll.ts +58 -5
  492. package/agent/apps/desktop/src/store/todos.test.ts +47 -0
  493. package/agent/apps/desktop/src/store/todos.ts +64 -0
  494. package/agent/apps/desktop/src/store/tool-dismiss.ts +45 -0
  495. package/agent/apps/desktop/src/store/translucency.ts +38 -0
  496. package/agent/apps/desktop/src/store/updates.test.ts +187 -2
  497. package/agent/apps/desktop/src/store/updates.ts +268 -18
  498. package/agent/apps/desktop/src/store/windows.test.ts +143 -0
  499. package/agent/apps/desktop/src/store/windows.ts +115 -0
  500. package/agent/apps/desktop/src/styles.css +510 -119
  501. package/agent/apps/desktop/src/themes/color.ts +142 -0
  502. package/agent/apps/desktop/src/themes/context.tsx +128 -75
  503. package/agent/apps/desktop/src/themes/install.test.ts +119 -0
  504. package/agent/apps/desktop/src/themes/install.ts +95 -0
  505. package/agent/apps/desktop/src/themes/presets.test.ts +33 -0
  506. package/agent/apps/desktop/src/themes/presets.ts +13 -4
  507. package/agent/apps/desktop/src/themes/profile-theme.test.ts +41 -0
  508. package/agent/apps/desktop/src/themes/types.ts +35 -0
  509. package/agent/apps/desktop/src/themes/user-themes.test.ts +63 -0
  510. package/agent/apps/desktop/src/themes/user-themes.ts +122 -0
  511. package/agent/apps/desktop/src/themes/vscode.test.ts +171 -0
  512. package/agent/apps/desktop/src/themes/vscode.ts +343 -0
  513. package/agent/apps/desktop/src/types/hermes.ts +138 -1
  514. package/agent/apps/desktop/tsconfig.json +2 -2
  515. package/agent/apps/desktop/vite.config.ts +18 -0
  516. package/agent/apps/shared/package.json +1 -1
  517. package/agent/apps/shared/src/json-rpc-gateway.ts +63 -2
  518. package/agent/apps/shared/tsconfig.json +2 -2
  519. package/agent/cli-config.yaml.example +78 -1
  520. package/agent/cli.py +2177 -3162
  521. package/agent/cron/blueprint_catalog.py +713 -0
  522. package/agent/cron/jobs.py +226 -110
  523. package/agent/cron/scheduler.py +468 -193
  524. package/agent/cron/scheduler_provider.py +177 -0
  525. package/agent/cron/scripts/__init__.py +1 -0
  526. package/agent/cron/scripts/classify_items.py +226 -0
  527. package/agent/cron/suggestion_catalog.py +154 -0
  528. package/agent/cron/suggestions.py +257 -0
  529. package/agent/docs/chronos-managed-cron-contract.md +196 -0
  530. package/agent/docs/design/profile-builder.md +146 -0
  531. package/agent/docs/middleware/README.md +260 -0
  532. package/agent/docs/observability/README.md +316 -0
  533. package/agent/docs/plans/2026-06-09-003-fix-telegram-stream-overflow-continuations-plan.md +240 -0
  534. package/agent/docs/rca-ssl-cacert-post-git-pull.md +54 -0
  535. package/agent/docs/relay-connector-contract.md +285 -0
  536. package/agent/gateway/authz_mixin.py +536 -0
  537. package/agent/gateway/channel_directory.py +65 -3
  538. package/agent/gateway/config.py +222 -12
  539. package/agent/gateway/display_config.py +10 -0
  540. package/agent/gateway/hooks.py +17 -0
  541. package/agent/gateway/kanban_watchers.py +1146 -0
  542. package/agent/gateway/message_timestamps.py +166 -0
  543. package/agent/gateway/platforms/ADDING_A_PLATFORM.md +29 -0
  544. package/agent/gateway/platforms/api_server.py +216 -38
  545. package/agent/gateway/platforms/base.py +210 -58
  546. package/agent/gateway/platforms/email.py +122 -12
  547. package/agent/gateway/platforms/feishu.py +80 -11
  548. package/agent/gateway/platforms/feishu_meeting_invite.py +212 -0
  549. package/agent/gateway/platforms/matrix.py +1498 -297
  550. package/agent/gateway/platforms/qqbot/adapter.py +6 -0
  551. package/agent/gateway/platforms/signal.py +8 -0
  552. package/agent/gateway/platforms/slack.py +308 -12
  553. package/agent/gateway/platforms/telegram.py +831 -24
  554. package/agent/gateway/platforms/webhook.py +109 -21
  555. package/agent/gateway/platforms/weixin.py +113 -2
  556. package/agent/gateway/platforms/whatsapp.py +94 -288
  557. package/agent/gateway/platforms/whatsapp_cloud.py +1956 -0
  558. package/agent/gateway/platforms/whatsapp_common.py +367 -0
  559. package/agent/gateway/platforms/yuanbao.py +608 -191
  560. package/agent/gateway/platforms/yuanbao_proto.py +232 -23
  561. package/agent/gateway/relay/__init__.py +375 -0
  562. package/agent/gateway/relay/adapter.py +222 -0
  563. package/agent/gateway/relay/auth.py +168 -0
  564. package/agent/gateway/relay/descriptor.py +118 -0
  565. package/agent/gateway/relay/transport.py +101 -0
  566. package/agent/gateway/relay/ws_transport.py +327 -0
  567. package/agent/gateway/response_filters.py +53 -0
  568. package/agent/gateway/rich_sent_store.py +80 -0
  569. package/agent/gateway/run.py +2940 -5001
  570. package/agent/gateway/session.py +109 -8
  571. package/agent/gateway/session_context.py +22 -4
  572. package/agent/gateway/slash_commands.py +3854 -0
  573. package/agent/gateway/status.py +141 -21
  574. package/agent/gateway/stream_consumer.py +288 -31
  575. package/agent/hermes-already-has-routines.md +1 -1
  576. package/agent/hermes_cli/__init__.py +62 -17
  577. package/agent/hermes_cli/_parser.py +30 -0
  578. package/agent/hermes_cli/_subprocess_compat.py +61 -0
  579. package/agent/hermes_cli/active_sessions.py +320 -0
  580. package/agent/hermes_cli/auth.py +707 -59
  581. package/agent/hermes_cli/auth_commands.py +39 -22
  582. package/agent/hermes_cli/backup.py +109 -7
  583. package/agent/hermes_cli/banner.py +88 -0
  584. package/agent/hermes_cli/blueprint_cmd.py +318 -0
  585. package/agent/hermes_cli/cli_agent_setup_mixin.py +684 -0
  586. package/agent/hermes_cli/cli_commands_mixin.py +2293 -0
  587. package/agent/hermes_cli/commands.py +215 -91
  588. package/agent/hermes_cli/config.py +967 -130
  589. package/agent/hermes_cli/container_boot.py +76 -11
  590. package/agent/hermes_cli/cron.py +5 -11
  591. package/agent/hermes_cli/curator.py +21 -0
  592. package/agent/hermes_cli/dashboard_auth/__init__.py +2 -0
  593. package/agent/hermes_cli/dashboard_auth/base.py +62 -0
  594. package/agent/hermes_cli/dashboard_auth/cookies.py +32 -19
  595. package/agent/hermes_cli/dashboard_auth/login_page.py +156 -6
  596. package/agent/hermes_cli/dashboard_auth/middleware.py +28 -4
  597. package/agent/hermes_cli/dashboard_auth/prefix.py +46 -2
  598. package/agent/hermes_cli/dashboard_auth/public_paths.py +6 -0
  599. package/agent/hermes_cli/dashboard_auth/routes.py +158 -2
  600. package/agent/hermes_cli/dashboard_auth/ws_tickets.py +85 -11
  601. package/agent/hermes_cli/dashboard_register.py +427 -0
  602. package/agent/hermes_cli/debug.py +155 -50
  603. package/agent/hermes_cli/doctor.py +255 -14
  604. package/agent/hermes_cli/dump.py +60 -6
  605. package/agent/hermes_cli/env_loader.py +33 -0
  606. package/agent/hermes_cli/gateway.py +755 -103
  607. package/agent/hermes_cli/gateway_enroll.py +250 -0
  608. package/agent/hermes_cli/gateway_windows.py +254 -11
  609. package/agent/hermes_cli/gui_uninstall.py +285 -0
  610. package/agent/hermes_cli/inventory.py +105 -4
  611. package/agent/hermes_cli/kanban.py +58 -71
  612. package/agent/hermes_cli/kanban_db.py +391 -14
  613. package/agent/hermes_cli/kanban_decompose.py +2 -2
  614. package/agent/hermes_cli/kanban_specify.py +3 -1
  615. package/agent/hermes_cli/logs.py +2 -0
  616. package/agent/hermes_cli/main.py +2889 -5287
  617. package/agent/hermes_cli/managed_scope.py +214 -0
  618. package/agent/hermes_cli/managed_uv.py +254 -0
  619. package/agent/hermes_cli/mcp_catalog.py +6 -3
  620. package/agent/hermes_cli/mcp_config.py +145 -21
  621. package/agent/hermes_cli/mcp_security.py +96 -0
  622. package/agent/hermes_cli/mcp_startup.py +32 -3
  623. package/agent/hermes_cli/memory_providers.py +149 -0
  624. package/agent/hermes_cli/memory_setup.py +97 -42
  625. package/agent/hermes_cli/middleware.py +313 -0
  626. package/agent/hermes_cli/model_catalog.py +31 -0
  627. package/agent/hermes_cli/model_cost_guard.py +134 -0
  628. package/agent/hermes_cli/model_normalize.py +2 -1
  629. package/agent/hermes_cli/model_setup_flows.py +2759 -0
  630. package/agent/hermes_cli/model_switch.py +242 -27
  631. package/agent/hermes_cli/models.py +284 -44
  632. package/agent/hermes_cli/nous_account.py +33 -6
  633. package/agent/hermes_cli/nous_billing.py +406 -0
  634. package/agent/hermes_cli/nous_subscription.py +202 -5
  635. package/agent/hermes_cli/platforms.py +1 -0
  636. package/agent/hermes_cli/plugins.py +218 -18
  637. package/agent/hermes_cli/plugins_cmd.py +249 -105
  638. package/agent/hermes_cli/portal_cli.py +56 -16
  639. package/agent/hermes_cli/profile_distribution.py +6 -1
  640. package/agent/hermes_cli/profiles.py +283 -32
  641. package/agent/hermes_cli/provider_catalog.py +170 -0
  642. package/agent/hermes_cli/providers.py +4 -1
  643. package/agent/hermes_cli/pty_bridge.py +53 -4
  644. package/agent/hermes_cli/runtime_provider.py +216 -34
  645. package/agent/hermes_cli/secret_prompt.py +4 -4
  646. package/agent/hermes_cli/secrets_cli.py +24 -0
  647. package/agent/hermes_cli/send_cmd.py +28 -2
  648. package/agent/hermes_cli/service_manager.py +166 -19
  649. package/agent/hermes_cli/session_listing.py +97 -0
  650. package/agent/hermes_cli/setup.py +158 -94
  651. package/agent/hermes_cli/setup_whatsapp_cloud.py +541 -0
  652. package/agent/hermes_cli/skills_config.py +8 -2
  653. package/agent/hermes_cli/skills_hub.py +149 -7
  654. package/agent/hermes_cli/status.py +2 -2
  655. package/agent/hermes_cli/subcommands/__init__.py +18 -0
  656. package/agent/hermes_cli/subcommands/_shared.py +29 -0
  657. package/agent/hermes_cli/subcommands/acp.py +52 -0
  658. package/agent/hermes_cli/subcommands/auth.py +109 -0
  659. package/agent/hermes_cli/subcommands/backup.py +38 -0
  660. package/agent/hermes_cli/subcommands/claw.py +92 -0
  661. package/agent/hermes_cli/subcommands/config.py +49 -0
  662. package/agent/hermes_cli/subcommands/cron.py +163 -0
  663. package/agent/hermes_cli/subcommands/dashboard.py +143 -0
  664. package/agent/hermes_cli/subcommands/debug.py +77 -0
  665. package/agent/hermes_cli/subcommands/doctor.py +35 -0
  666. package/agent/hermes_cli/subcommands/dump.py +28 -0
  667. package/agent/hermes_cli/subcommands/gateway.py +332 -0
  668. package/agent/hermes_cli/subcommands/gui.py +63 -0
  669. package/agent/hermes_cli/subcommands/hooks.py +77 -0
  670. package/agent/hermes_cli/subcommands/import_cmd.py +31 -0
  671. package/agent/hermes_cli/subcommands/insights.py +25 -0
  672. package/agent/hermes_cli/subcommands/login.py +78 -0
  673. package/agent/hermes_cli/subcommands/logout.py +28 -0
  674. package/agent/hermes_cli/subcommands/logs.py +78 -0
  675. package/agent/hermes_cli/subcommands/mcp.py +108 -0
  676. package/agent/hermes_cli/subcommands/memory.py +53 -0
  677. package/agent/hermes_cli/subcommands/model.py +72 -0
  678. package/agent/hermes_cli/subcommands/pairing.py +36 -0
  679. package/agent/hermes_cli/subcommands/plugins.py +94 -0
  680. package/agent/hermes_cli/subcommands/postinstall.py +23 -0
  681. package/agent/hermes_cli/subcommands/profile.py +203 -0
  682. package/agent/hermes_cli/subcommands/prompt_size.py +36 -0
  683. package/agent/hermes_cli/subcommands/security.py +62 -0
  684. package/agent/hermes_cli/subcommands/setup.py +58 -0
  685. package/agent/hermes_cli/subcommands/skills.py +298 -0
  686. package/agent/hermes_cli/subcommands/slack.py +60 -0
  687. package/agent/hermes_cli/subcommands/status.py +28 -0
  688. package/agent/hermes_cli/subcommands/tools.py +95 -0
  689. package/agent/hermes_cli/subcommands/uninstall.py +41 -0
  690. package/agent/hermes_cli/subcommands/update.py +70 -0
  691. package/agent/hermes_cli/subcommands/version.py +18 -0
  692. package/agent/hermes_cli/subcommands/webhook.py +76 -0
  693. package/agent/hermes_cli/subcommands/whatsapp.py +22 -0
  694. package/agent/hermes_cli/suggestions_cmd.py +153 -0
  695. package/agent/hermes_cli/telegram_managed_bot.py +358 -0
  696. package/agent/hermes_cli/tips.py +3 -4
  697. package/agent/hermes_cli/tools_config.py +155 -28
  698. package/agent/hermes_cli/uninstall.py +231 -35
  699. package/agent/hermes_cli/web_server.py +6190 -973
  700. package/agent/hermes_cli/win_pty_bridge.py +179 -0
  701. package/agent/hermes_cli/write_approval_commands.py +209 -0
  702. package/agent/hermes_constants.py +164 -33
  703. package/agent/hermes_logging.py +74 -2
  704. package/agent/hermes_state.py +919 -106
  705. package/agent/hermes_time.py +20 -0
  706. package/agent/locales/af.yaml +23 -0
  707. package/agent/locales/de.yaml +23 -0
  708. package/agent/locales/en.yaml +20 -0
  709. package/agent/locales/es.yaml +23 -0
  710. package/agent/locales/fr.yaml +23 -0
  711. package/agent/locales/ga.yaml +23 -0
  712. package/agent/locales/hu.yaml +23 -0
  713. package/agent/locales/it.yaml +23 -0
  714. package/agent/locales/ja.yaml +23 -0
  715. package/agent/locales/ko.yaml +23 -0
  716. package/agent/locales/pt.yaml +23 -0
  717. package/agent/locales/ru.yaml +23 -0
  718. package/agent/locales/tr.yaml +23 -0
  719. package/agent/locales/uk.yaml +23 -0
  720. package/agent/locales/zh-hant.yaml +23 -0
  721. package/agent/locales/zh.yaml +23 -0
  722. package/agent/model_tools.py +204 -40
  723. package/agent/optional-mcps/clawpump/manifest.yaml +4 -2
  724. package/agent/optional-mcps/clawpump-stdio/manifest.yaml +2 -0
  725. package/agent/optional-mcps/unreal-engine/manifest.yaml +54 -0
  726. package/agent/optional-skills/blockchain/hyperliquid/SKILL.md +2 -2
  727. package/agent/optional-skills/blockchain/hyperliquid/scripts/hyperliquid_client.py +1 -1
  728. package/agent/optional-skills/creative/kanban-video-orchestrator/SKILL.md +1 -1
  729. package/agent/optional-skills/creative/kanban-video-orchestrator/assets/setup.sh.tmpl +4 -3
  730. package/agent/optional-skills/creative/kanban-video-orchestrator/references/kanban-setup.md +6 -4
  731. package/agent/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md +2 -2
  732. package/agent/{skills/software-development → optional-skills/devops}/hermes-s6-container-supervision/SKILL.md +2 -0
  733. package/agent/optional-skills/devops/watchers/SKILL.md +1 -1
  734. package/agent/optional-skills/devops/watchers/scripts/watch_github.py +2 -1
  735. package/agent/optional-skills/payments/mpp-agent/SKILL.md +124 -0
  736. package/agent/optional-skills/payments/stripe-link-cli/SKILL.md +184 -0
  737. package/agent/optional-skills/payments/stripe-projects/SKILL.md +120 -0
  738. package/agent/optional-skills/productivity/canvas/SKILL.md +1 -1
  739. package/agent/optional-skills/productivity/canvas/scripts/canvas_api.py +4 -1
  740. package/agent/optional-skills/productivity/shop/SKILL.md +224 -0
  741. package/agent/optional-skills/productivity/shop/references/catalog-mcp.md +236 -0
  742. package/agent/optional-skills/productivity/shop/references/direct-api.md +278 -0
  743. package/agent/optional-skills/productivity/shop/references/legal.md +3 -0
  744. package/agent/optional-skills/productivity/shop/references/safety.md +36 -0
  745. package/agent/optional-skills/productivity/shopify/SKILL.md +1 -1
  746. package/agent/optional-skills/productivity/siyuan/SKILL.md +1 -1
  747. package/agent/optional-skills/productivity/telephony/SKILL.md +4 -4
  748. package/agent/optional-skills/productivity/telephony/scripts/telephony.py +15 -15
  749. package/agent/optional-skills/security/1password/SKILL.md +1 -1
  750. package/agent/{skills/red-teaming → optional-skills/security}/godmode/SKILL.md +3 -4
  751. package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/auto_jailbreak.py +3 -1
  752. package/agent/optional-skills/software-development/rest-graphql-debug/SKILL.md +1 -1
  753. package/agent/{skills → optional-skills}/software-development/subagent-driven-development/SKILL.md +5 -5
  754. package/agent/package-lock.json +4082 -7907
  755. package/agent/package.json +18 -3
  756. package/agent/plugins/browser/firecrawl/provider.py +4 -1
  757. package/agent/plugins/cron/__init__.py +344 -0
  758. package/agent/plugins/cron/chronos/__init__.py +241 -0
  759. package/agent/plugins/cron/chronos/_nas_client.py +123 -0
  760. package/agent/plugins/cron/chronos/plugin.yaml +9 -0
  761. package/agent/plugins/cron/chronos/verify.py +103 -0
  762. package/agent/plugins/dashboard_auth/basic/__init__.py +491 -0
  763. package/agent/plugins/dashboard_auth/basic/plugin.yaml +7 -0
  764. package/agent/plugins/dashboard_auth/nous/__init__.py +12 -14
  765. package/agent/plugins/dashboard_auth/self_hosted/__init__.py +736 -0
  766. package/agent/plugins/dashboard_auth/self_hosted/plugin.yaml +8 -0
  767. package/agent/plugins/disk-cleanup/disk_cleanup.py +100 -20
  768. package/agent/plugins/google_meet/audio_bridge.py +4 -0
  769. package/agent/plugins/google_meet/meet_bot.py +7 -1
  770. package/agent/plugins/hermes-achievements/dashboard/dist/index.js +9 -15
  771. package/agent/plugins/image_gen/fal/__init__.py +35 -6
  772. package/agent/plugins/image_gen/krea/__init__.py +56 -13
  773. package/agent/plugins/image_gen/openai/__init__.py +122 -24
  774. package/agent/plugins/image_gen/openai-codex/__init__.py +28 -2
  775. package/agent/plugins/image_gen/xai/__init__.py +92 -12
  776. package/agent/plugins/kanban/dashboard/dist/index.js +63 -48
  777. package/agent/plugins/kanban/dashboard/plugin_api.py +39 -35
  778. package/agent/plugins/memory/__init__.py +48 -5
  779. package/agent/plugins/memory/byterover/__init__.py +1 -0
  780. package/agent/plugins/memory/hindsight/README.md +1 -1
  781. package/agent/plugins/memory/hindsight/__init__.py +138 -24
  782. package/agent/plugins/memory/hindsight/plugin.yaml +1 -1
  783. package/agent/plugins/memory/honcho/README.md +13 -10
  784. package/agent/plugins/memory/honcho/cli.py +247 -122
  785. package/agent/plugins/memory/honcho/client.py +112 -102
  786. package/agent/plugins/memory/openviking/README.md +12 -1
  787. package/agent/plugins/memory/openviking/__init__.py +2281 -107
  788. package/agent/plugins/memory/openviking/plugin.yaml +1 -2
  789. package/agent/plugins/memory/supermemory/README.md +22 -10
  790. package/agent/plugins/memory/supermemory/__init__.py +142 -37
  791. package/agent/plugins/memory/supermemory/plugin.yaml +1 -1
  792. package/agent/plugins/model-providers/anthropic/__init__.py +1 -0
  793. package/agent/plugins/model-providers/bedrock/__init__.py +1 -0
  794. package/agent/plugins/model-providers/copilot-acp/__init__.py +1 -0
  795. package/agent/plugins/model-providers/custom/__init__.py +8 -2
  796. package/agent/plugins/model-providers/kimi-coding/__init__.py +16 -7
  797. package/agent/plugins/model-providers/minimax/__init__.py +60 -8
  798. package/agent/plugins/model-providers/opencode-zen/__init__.py +12 -3
  799. package/agent/plugins/model-providers/openrouter/__init__.py +75 -4
  800. package/agent/plugins/model-providers/xiaomi/__init__.py +2 -0
  801. package/agent/plugins/model-providers/zai/__init__.py +1 -0
  802. package/agent/plugins/observability/langfuse/__init__.py +147 -14
  803. package/agent/plugins/observability/nemo_relay/README.md +559 -0
  804. package/agent/plugins/observability/nemo_relay/__init__.py +962 -0
  805. package/agent/plugins/observability/nemo_relay/plugin.yaml +20 -0
  806. package/agent/plugins/platforms/discord/adapter.py +932 -61
  807. package/agent/plugins/platforms/discord/voice_mixer.py +379 -0
  808. package/agent/plugins/platforms/google_chat/adapter.py +9 -3
  809. package/agent/plugins/platforms/google_chat/oauth.py +1 -1
  810. package/agent/plugins/platforms/homeassistant/__init__.py +3 -0
  811. package/agent/{gateway/platforms/homeassistant.py → plugins/platforms/homeassistant/adapter.py} +128 -0
  812. package/agent/plugins/platforms/homeassistant/plugin.yaml +22 -0
  813. package/agent/plugins/platforms/irc/adapter.py +4 -1
  814. package/agent/plugins/platforms/line/adapter.py +16 -1
  815. package/agent/plugins/platforms/mattermost/adapter.py +100 -24
  816. package/agent/plugins/platforms/photon/README.md +179 -0
  817. package/agent/plugins/platforms/photon/__init__.py +4 -0
  818. package/agent/plugins/platforms/photon/adapter.py +1586 -0
  819. package/agent/plugins/platforms/photon/auth.py +1046 -0
  820. package/agent/plugins/platforms/photon/cli.py +439 -0
  821. package/agent/plugins/platforms/photon/plugin.yaml +88 -0
  822. package/agent/plugins/platforms/photon/sidecar/README.md +52 -0
  823. package/agent/plugins/platforms/photon/sidecar/index.mjs +720 -0
  824. package/agent/plugins/platforms/photon/sidecar/package-lock.json +1730 -0
  825. package/agent/plugins/platforms/photon/sidecar/package.json +25 -0
  826. package/agent/plugins/platforms/photon/sidecar/patch-spectrum-mixed-attachments.mjs +155 -0
  827. package/agent/plugins/platforms/raft/__init__.py +3 -0
  828. package/agent/plugins/platforms/raft/adapter.py +774 -0
  829. package/agent/plugins/platforms/raft/plugin.yaml +19 -0
  830. package/agent/plugins/platforms/simplex/adapter.py +777 -220
  831. package/agent/plugins/platforms/simplex/plugin.yaml +21 -2
  832. package/agent/plugins/platforms/teams/adapter.py +175 -5
  833. package/agent/plugins/plugin_utils.py +135 -0
  834. package/agent/plugins/video_gen/fal/__init__.py +10 -3
  835. package/agent/plugins/web/searxng/provider.py +15 -2
  836. package/agent/plugins/web/xai/provider.py +2 -2
  837. package/agent/providers/base.py +22 -3
  838. package/agent/pyproject.toml +115 -21
  839. package/agent/run_agent.py +733 -39
  840. package/agent/scripts/build_skills_index.py +51 -19
  841. package/agent/scripts/check_subprocess_stdin.py +177 -0
  842. package/agent/scripts/contributor_audit.py +2 -0
  843. package/agent/scripts/docker_config_migrate.py +67 -0
  844. package/agent/scripts/install.cmd +3 -3
  845. package/agent/scripts/install.ps1 +580 -154
  846. package/agent/scripts/install.sh +402 -185
  847. package/agent/scripts/lib/node-bootstrap.sh +39 -4
  848. package/agent/scripts/release.py +183 -0
  849. package/agent/scripts/run_tests.sh +1 -0
  850. package/agent/scripts/run_tests_parallel.py +18 -23
  851. package/agent/scripts/whatsapp-bridge/bridge.js +25 -4
  852. package/agent/setup.py +59 -0
  853. package/agent/skills/autonomous-ai-agents/codex/SKILL.md +19 -0
  854. package/agent/skills/autonomous-ai-agents/hermes-agent/SKILL.md +10 -3
  855. package/agent/skills/{mcp/native-mcp/SKILL.md → autonomous-ai-agents/hermes-agent/references/native-mcp.md} +0 -13
  856. package/agent/skills/{devops/webhook-subscriptions/SKILL.md → autonomous-ai-agents/hermes-agent/references/webhooks.md} +1 -11
  857. package/agent/skills/clawpump/SKILL.md +4 -1
  858. package/agent/skills/devops/kanban-orchestrator/SKILL.md +1 -0
  859. package/agent/skills/devops/kanban-worker/SKILL.md +1 -0
  860. package/agent/skills/github/github-auth/SKILL.md +2 -2
  861. package/agent/skills/github/github-auth/scripts/gh-env.sh +2 -2
  862. package/agent/skills/github/github-code-review/SKILL.md +2 -2
  863. package/agent/skills/github/github-issues/SKILL.md +2 -2
  864. package/agent/skills/github/github-pr-workflow/SKILL.md +2 -2
  865. package/agent/skills/github/github-repo-management/SKILL.md +2 -2
  866. package/agent/skills/media/gif-search/SKILL.md +1 -1
  867. package/agent/skills/media/youtube-content/SKILL.md +10 -7
  868. package/agent/skills/media/youtube-content/scripts/fetch_transcript.py +3 -3
  869. package/agent/skills/note-taking/obsidian/SKILL.md +1 -1
  870. package/agent/skills/productivity/airtable/SKILL.md +2 -2
  871. package/agent/skills/productivity/google-workspace/scripts/setup.py +33 -7
  872. package/agent/skills/productivity/notion/SKILL.md +2 -2
  873. package/agent/skills/productivity/teams-meeting-pipeline/SKILL.md +1 -1
  874. package/agent/skills/research/llm-wiki/SKILL.md +1 -1
  875. package/agent/skills/social-media/xurl/SKILL.md +9 -0
  876. package/agent/skills/software-development/hermes-agent-skill-authoring/SKILL.md +1 -1
  877. package/agent/skills/software-development/plan/SKILL.md +285 -5
  878. package/agent/skills/software-development/requesting-code-review/SKILL.md +2 -2
  879. package/agent/skills/software-development/simplify-code/SKILL.md +212 -0
  880. package/agent/skills/software-development/spike/SKILL.md +2 -2
  881. package/agent/skills/software-development/systematic-debugging/SKILL.md +1 -1
  882. package/agent/skills/software-development/test-driven-development/SKILL.md +1 -1
  883. package/agent/tools/approval.py +302 -4
  884. package/agent/tools/async_delegation.py +386 -0
  885. package/agent/tools/blueprints.py +325 -0
  886. package/agent/tools/browser_cdp_tool.py +3 -3
  887. package/agent/tools/browser_tool.py +34 -6
  888. package/agent/tools/checkpoint_manager.py +31 -1
  889. package/agent/tools/clarify_tool.py +55 -5
  890. package/agent/tools/code_execution_tool.py +31 -14
  891. package/agent/tools/computer_use/cua_backend.py +81 -3
  892. package/agent/tools/computer_use/tool.py +79 -5
  893. package/agent/tools/computer_use/vision_routing.py +55 -3
  894. package/agent/tools/credential_files.py +31 -12
  895. package/agent/tools/cronjob_tools.py +30 -20
  896. package/agent/tools/delegate_tool.py +356 -31
  897. package/agent/tools/env_probe.py +1 -0
  898. package/agent/tools/environments/docker.py +163 -8
  899. package/agent/tools/environments/file_sync.py +2 -1
  900. package/agent/tools/environments/local.py +74 -23
  901. package/agent/tools/environments/singularity.py +4 -1
  902. package/agent/tools/environments/ssh.py +78 -11
  903. package/agent/tools/file_operations.py +277 -41
  904. package/agent/tools/file_tools.py +166 -28
  905. package/agent/tools/image_generation_tool.py +515 -29
  906. package/agent/tools/kanban_tools.py +99 -0
  907. package/agent/tools/lazy_deps.py +33 -2
  908. package/agent/tools/mcp_oauth.py +5 -5
  909. package/agent/tools/mcp_oauth_manager.py +7 -5
  910. package/agent/tools/mcp_tool.py +840 -33
  911. package/agent/tools/memory_tool.py +335 -38
  912. package/agent/tools/osv_check.py +15 -1
  913. package/agent/tools/process_registry.py +155 -11
  914. package/agent/tools/read_extract.py +248 -0
  915. package/agent/tools/read_terminal_tool.py +93 -0
  916. package/agent/tools/schema_sanitizer.py +38 -0
  917. package/agent/tools/send_message_tool.py +163 -49
  918. package/agent/tools/session_search_tool.py +189 -7
  919. package/agent/tools/skill_manager_tool.py +202 -3
  920. package/agent/tools/skill_usage.py +52 -4
  921. package/agent/tools/skills_hub.py +184 -44
  922. package/agent/tools/skills_sync.py +232 -5
  923. package/agent/tools/skills_tool.py +125 -11
  924. package/agent/tools/terminal_tool.py +148 -26
  925. package/agent/tools/tirith_security.py +2 -0
  926. package/agent/tools/todo_tool.py +32 -1
  927. package/agent/tools/transcription_tools.py +13 -5
  928. package/agent/tools/tts_tool.py +332 -38
  929. package/agent/tools/url_safety.py +52 -1
  930. package/agent/tools/vision_tools.py +124 -39
  931. package/agent/tools/voice_mode.py +4 -3
  932. package/agent/tools/web_tools.py +45 -15
  933. package/agent/tools/write_approval.py +493 -0
  934. package/agent/toolsets.py +34 -10
  935. package/agent/trajectory_compressor.py +81 -10
  936. package/agent/tui_gateway/entry.py +43 -6
  937. package/agent/tui_gateway/server.py +3335 -330
  938. package/agent/tui_gateway/slash_worker.py +61 -0
  939. package/agent/tui_gateway/ws.py +67 -9
  940. package/agent/ui-tui/eslint.config.mjs +0 -4
  941. package/agent/ui-tui/package.json +6 -6
  942. package/agent/ui-tui/packages/hermes-ink/package.json +1 -1
  943. package/agent/ui-tui/packages/hermes-ink/src/ink/app-mouse.test.ts +34 -1
  944. package/agent/ui-tui/packages/hermes-ink/src/ink/app-rawmode-mouse.test.ts +91 -0
  945. package/agent/ui-tui/packages/hermes-ink/src/ink/components/App.tsx +35 -2
  946. package/agent/ui-tui/packages/hermes-ink/src/ink/events/input-event.ts +4 -11
  947. package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.test.ts +23 -57
  948. package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts +11 -135
  949. package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.test.ts +185 -0
  950. package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.ts +37 -3
  951. package/agent/ui-tui/packages/hermes-ink/src/utils/execFileNoThrow.ts +5 -5
  952. package/agent/ui-tui/src/__tests__/appChromeStatusRule.test.tsx +217 -0
  953. package/agent/ui-tui/src/__tests__/appChromeStatusRuleDevCredits.test.tsx +73 -0
  954. package/agent/ui-tui/src/__tests__/approvalAction.test.ts +11 -0
  955. package/agent/ui-tui/src/__tests__/billingCommand.test.ts +301 -0
  956. package/agent/ui-tui/src/__tests__/blockLayout.test.ts +122 -0
  957. package/agent/ui-tui/src/__tests__/brandingMcpCount.test.ts +111 -0
  958. package/agent/ui-tui/src/__tests__/completionApply.test.ts +51 -0
  959. package/agent/ui-tui/src/__tests__/createGatewayEventHandler.test.ts +487 -2
  960. package/agent/ui-tui/src/__tests__/createSlashHandler.test.ts +54 -0
  961. package/agent/ui-tui/src/__tests__/creditsCommand.test.ts +144 -0
  962. package/agent/ui-tui/src/__tests__/gatewayClient.test.ts +120 -99
  963. package/agent/ui-tui/src/__tests__/gracefulExit.test.ts +11 -0
  964. package/agent/ui-tui/src/__tests__/memoryMonitor.test.ts +102 -0
  965. package/agent/ui-tui/src/__tests__/paths.test.ts +41 -1
  966. package/agent/ui-tui/src/__tests__/terminalModes.test.ts +22 -0
  967. package/agent/ui-tui/src/__tests__/text.test.ts +23 -0
  968. package/agent/ui-tui/src/__tests__/textInputFastEcho.test.ts +37 -0
  969. package/agent/ui-tui/src/__tests__/turnControllerNotice.test.ts +43 -0
  970. package/agent/ui-tui/src/__tests__/useInputHandlers.test.ts +38 -1
  971. package/agent/ui-tui/src/__tests__/virtualHeights.test.ts +8 -0
  972. package/agent/ui-tui/src/app/createGatewayEventHandler.ts +102 -7
  973. package/agent/ui-tui/src/app/interfaces.ts +64 -1
  974. package/agent/ui-tui/src/app/overlayStore.ts +18 -2
  975. package/agent/ui-tui/src/app/slash/commands/billing.ts +332 -0
  976. package/agent/ui-tui/src/app/slash/commands/core.ts +31 -2
  977. package/agent/ui-tui/src/app/slash/commands/credits.ts +57 -0
  978. package/agent/ui-tui/src/app/slash/commands/ops.ts +28 -0
  979. package/agent/ui-tui/src/app/slash/commands/session.ts +32 -4
  980. package/agent/ui-tui/src/app/slash/registry.ts +4 -0
  981. package/agent/ui-tui/src/app/turnController.ts +145 -2
  982. package/agent/ui-tui/src/app/uiStore.ts +2 -0
  983. package/agent/ui-tui/src/app/useInputHandlers.ts +42 -4
  984. package/agent/ui-tui/src/app/useMainApp.ts +54 -8
  985. package/agent/ui-tui/src/app/useSessionLifecycle.ts +40 -31
  986. package/agent/ui-tui/src/app/useSubmission.ts +23 -31
  987. package/agent/ui-tui/src/components/appChrome.tsx +112 -5
  988. package/agent/ui-tui/src/components/appLayout.tsx +9 -0
  989. package/agent/ui-tui/src/components/appOverlays.tsx +25 -1
  990. package/agent/ui-tui/src/components/billingOverlay.tsx +684 -0
  991. package/agent/ui-tui/src/components/branding.tsx +15 -3
  992. package/agent/ui-tui/src/components/messageLine.tsx +25 -3
  993. package/agent/ui-tui/src/components/pluginsHub.tsx +238 -0
  994. package/agent/ui-tui/src/components/prompts.tsx +31 -17
  995. package/agent/ui-tui/src/components/streamingAssistant.tsx +63 -55
  996. package/agent/ui-tui/src/components/textInput.tsx +16 -0
  997. package/agent/ui-tui/src/config/env.ts +12 -0
  998. package/agent/ui-tui/src/config/limits.ts +13 -0
  999. package/agent/ui-tui/src/domain/blockLayout.ts +146 -0
  1000. package/agent/ui-tui/src/domain/paths.ts +24 -0
  1001. package/agent/ui-tui/src/domain/slash.ts +40 -0
  1002. package/agent/ui-tui/src/entry.tsx +35 -4
  1003. package/agent/ui-tui/src/gatewayClient.ts +22 -10
  1004. package/agent/ui-tui/src/gatewayTypes.ts +130 -1
  1005. package/agent/ui-tui/src/lib/gracefulExit.ts +24 -4
  1006. package/agent/ui-tui/src/lib/memory.test.ts +162 -0
  1007. package/agent/ui-tui/src/lib/memory.ts +60 -1
  1008. package/agent/ui-tui/src/lib/memoryMonitor.ts +79 -4
  1009. package/agent/ui-tui/src/lib/osc52.ts +1 -1
  1010. package/agent/ui-tui/src/lib/text.test.ts +32 -1
  1011. package/agent/ui-tui/src/lib/text.ts +29 -2
  1012. package/agent/ui-tui/src/lib/virtualHeights.ts +13 -0
  1013. package/agent/ui-tui/src/types.ts +5 -0
  1014. package/agent/ui-tui/tsconfig.build.json +0 -1
  1015. package/agent/ui-tui/tsconfig.json +2 -1
  1016. package/agent/utils.py +66 -2
  1017. package/agent/uv.lock +300 -684
  1018. package/agent/web/index.html +2 -2
  1019. package/agent/web/package.json +11 -6
  1020. package/agent/web/public/claw-bg.webp +0 -0
  1021. package/agent/web/public/claw-logo.webp +0 -0
  1022. package/agent/web/src/App.tsx +138 -48
  1023. package/agent/web/src/components/AutomationBlueprints.tsx +225 -0
  1024. package/agent/web/src/components/Backdrop.tsx +15 -0
  1025. package/agent/web/src/components/ChatSessionList.tsx +260 -0
  1026. package/agent/web/src/components/ChatSidebar.tsx +262 -78
  1027. package/agent/web/src/components/ConfirmDialog.tsx +122 -0
  1028. package/agent/web/src/components/ModelPickerDialog.tsx +111 -16
  1029. package/agent/web/src/components/ModelReloadConfirm.tsx +40 -0
  1030. package/agent/web/src/components/ProfileScopeBanner.tsx +30 -0
  1031. package/agent/web/src/components/ProfileSwitcher.tsx +67 -0
  1032. package/agent/web/src/components/ReasoningPicker.tsx +167 -0
  1033. package/agent/web/src/components/SkillEditorDialog.tsx +215 -0
  1034. package/agent/web/src/components/ThemeSwitcher.tsx +119 -4
  1035. package/agent/web/src/components/ToolsetConfigDrawer.tsx +457 -0
  1036. package/agent/web/src/contexts/PageHeaderProvider.tsx +7 -4
  1037. package/agent/web/src/contexts/ProfileProvider.tsx +137 -0
  1038. package/agent/web/src/contexts/SystemActions.tsx +6 -8
  1039. package/agent/web/src/contexts/profile-context.ts +19 -0
  1040. package/agent/web/src/contexts/useProfileScope.ts +6 -0
  1041. package/agent/web/src/i18n/af.ts +5 -4
  1042. package/agent/web/src/i18n/de.ts +5 -4
  1043. package/agent/web/src/i18n/en.ts +58 -4
  1044. package/agent/web/src/i18n/es.ts +5 -3
  1045. package/agent/web/src/i18n/fr.ts +5 -3
  1046. package/agent/web/src/i18n/ga.ts +5 -4
  1047. package/agent/web/src/i18n/hu.ts +5 -4
  1048. package/agent/web/src/i18n/it.ts +5 -4
  1049. package/agent/web/src/i18n/ja.ts +5 -4
  1050. package/agent/web/src/i18n/ko.ts +5 -4
  1051. package/agent/web/src/i18n/pt.ts +5 -3
  1052. package/agent/web/src/i18n/ru.ts +5 -4
  1053. package/agent/web/src/i18n/tr.ts +5 -4
  1054. package/agent/web/src/i18n/types.ts +59 -1
  1055. package/agent/web/src/i18n/uk.ts +5 -3
  1056. package/agent/web/src/i18n/zh-hant.ts +5 -4
  1057. package/agent/web/src/i18n/zh.ts +5 -4
  1058. package/agent/web/src/index.css +2 -2
  1059. package/agent/web/src/lib/api.ts +819 -52
  1060. package/agent/web/src/lib/dashboard-flags.ts +16 -7
  1061. package/agent/web/src/lib/reasoning-effort.test.ts +48 -0
  1062. package/agent/web/src/lib/reasoning-effort.ts +36 -0
  1063. package/agent/web/src/lib/session-refresh.test.ts +21 -0
  1064. package/agent/web/src/lib/session-refresh.ts +26 -0
  1065. package/agent/web/src/pages/ChannelsPage.tsx +529 -68
  1066. package/agent/web/src/pages/ChatPage.tsx +249 -56
  1067. package/agent/web/src/pages/ConfigPage.tsx +11 -1
  1068. package/agent/web/src/pages/CronPage.tsx +219 -31
  1069. package/agent/web/src/pages/EnvPage.tsx +25 -6
  1070. package/agent/web/src/pages/FilesPage.tsx +525 -0
  1071. package/agent/web/src/pages/McpPage.tsx +80 -3
  1072. package/agent/web/src/pages/ModelsPage.tsx +97 -12
  1073. package/agent/web/src/pages/PluginsPage.tsx +1 -1
  1074. package/agent/web/src/pages/ProfileBuilderPage.tsx +611 -0
  1075. package/agent/web/src/pages/ProfilesPage.tsx +1038 -172
  1076. package/agent/web/src/pages/SessionsPage.tsx +144 -13
  1077. package/agent/web/src/pages/SkillsPage.tsx +851 -70
  1078. package/agent/web/src/pages/SystemPage.tsx +340 -4
  1079. package/agent/web/src/pages/WalletPage.tsx +401 -0
  1080. package/agent/web/src/pages/WebhooksPage.tsx +145 -15
  1081. package/agent/web/src/pages/X402Page.tsx +207 -0
  1082. package/agent/web/src/plugins/registry.ts +28 -11
  1083. package/agent/web/src/plugins/sdk.d.ts +160 -0
  1084. package/agent/web/src/themes/context.tsx +112 -5
  1085. package/agent/web/src/themes/fonts.ts +167 -0
  1086. package/agent/web/src/themes/index.ts +7 -0
  1087. package/agent/web/tsconfig.app.json +0 -1
  1088. package/agent/web/vite.config.ts +1 -8
  1089. package/agent/web/vitest.config.ts +16 -0
  1090. package/package.json +1 -1
  1091. package/agent/apps/desktop/package-lock.json +0 -18363
  1092. package/agent/apps/desktop/src/app/chat/composer/skin-slash-popover.tsx +0 -56
  1093. package/agent/apps/desktop/src/components/assistant-ui/thread-virtualizer.tsx +0 -382
  1094. package/agent/apps/desktop/src/components/assistant-ui/todo-tool.tsx +0 -109
  1095. package/agent/apps/desktop/src/components/chat/generated-image-context.tsx +0 -19
  1096. package/agent/optional-skills/productivity/shop-app/SKILL.md +0 -340
  1097. package/agent/skills/autonomous-ai-agents/kanban-codex-lane/SKILL.md +0 -277
  1098. package/agent/skills/autonomous-ai-agents/kanban-codex-lane/templates/pmb-codex-lane-prompt.md +0 -57
  1099. package/agent/skills/diagramming/DESCRIPTION.md +0 -3
  1100. package/agent/skills/domain/DESCRIPTION.md +0 -24
  1101. package/agent/skills/gifs/DESCRIPTION.md +0 -3
  1102. package/agent/skills/inference-sh/DESCRIPTION.md +0 -19
  1103. package/agent/skills/mcp/DESCRIPTION.md +0 -3
  1104. package/agent/skills/media/spotify/SKILL.md +0 -135
  1105. package/agent/skills/mlops/training/DESCRIPTION.md +0 -3
  1106. package/agent/skills/mlops/vector-databases/DESCRIPTION.md +0 -3
  1107. package/agent/skills/productivity/linear/SKILL.md +0 -380
  1108. package/agent/skills/productivity/linear/scripts/linear_api.py +0 -445
  1109. package/agent/skills/software-development/debugging-hermes-tui-commands/SKILL.md +0 -152
  1110. package/agent/skills/software-development/writing-plans/SKILL.md +0 -297
  1111. package/agent/ui-tui/package-lock.json +0 -7449
  1112. package/agent/ui-tui/packages/hermes-ink/package-lock.json +0 -1289
  1113. package/agent/web/package-lock.json +0 -8887
  1114. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/PORT_NOTES.md +0 -0
  1115. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/SKILL.md +0 -0
  1116. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/prompts/system.md +0 -0
  1117. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/macaron.md +0 -0
  1118. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/mono-ink.md +0 -0
  1119. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/neon.md +0 -0
  1120. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/warm.md +0 -0
  1121. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/prompt-construction.md +0 -0
  1122. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/style-presets.md +0 -0
  1123. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/blueprint.md +0 -0
  1124. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/chalkboard.md +0 -0
  1125. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/editorial.md +0 -0
  1126. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/elegant.md +0 -0
  1127. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/fantasy-animation.md +0 -0
  1128. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat-doodle.md +0 -0
  1129. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat.md +0 -0
  1130. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/ink-notes.md +0 -0
  1131. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/intuition-machine.md +0 -0
  1132. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/minimal.md +0 -0
  1133. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/nature.md +0 -0
  1134. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/notion.md +0 -0
  1135. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/pixel-art.md +0 -0
  1136. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/playful.md +0 -0
  1137. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/retro.md +0 -0
  1138. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/scientific.md +0 -0
  1139. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/screen-print.md +0 -0
  1140. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch-notes.md +0 -0
  1141. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch.md +0 -0
  1142. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vector-illustration.md +0 -0
  1143. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vintage.md +0 -0
  1144. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/warm.md +0 -0
  1145. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/watercolor.md +0 -0
  1146. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles.md +0 -0
  1147. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/usage.md +0 -0
  1148. /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/workflow.md +0 -0
  1149. /package/agent/{skills → optional-skills}/creative/baoyu-comic/PORT_NOTES.md +0 -0
  1150. /package/agent/{skills → optional-skills}/creative/baoyu-comic/SKILL.md +0 -0
  1151. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/analysis-framework.md +0 -0
  1152. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/chalk.md +0 -0
  1153. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ink-brush.md +0 -0
  1154. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ligne-claire.md +0 -0
  1155. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/manga.md +0 -0
  1156. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/minimalist.md +0 -0
  1157. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/realistic.md +0 -0
  1158. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/auto-selection.md +0 -0
  1159. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/base-prompt.md +0 -0
  1160. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/character-template.md +0 -0
  1161. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/cinematic.md +0 -0
  1162. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/dense.md +0 -0
  1163. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/four-panel.md +0 -0
  1164. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/mixed.md +0 -0
  1165. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/splash.md +0 -0
  1166. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/standard.md +0 -0
  1167. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/webtoon.md +0 -0
  1168. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/ohmsha-guide.md +0 -0
  1169. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/partial-workflows.md +0 -0
  1170. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/concept-story.md +0 -0
  1171. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/four-panel.md +0 -0
  1172. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/ohmsha.md +0 -0
  1173. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/shoujo.md +0 -0
  1174. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/wuxia.md +0 -0
  1175. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/storyboard-template.md +0 -0
  1176. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/action.md +0 -0
  1177. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/dramatic.md +0 -0
  1178. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/energetic.md +0 -0
  1179. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/neutral.md +0 -0
  1180. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/romantic.md +0 -0
  1181. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/vintage.md +0 -0
  1182. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/warm.md +0 -0
  1183. /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/workflow.md +0 -0
  1184. /package/agent/{skills → optional-skills}/creative/creative-ideation/SKILL.md +0 -0
  1185. /package/agent/{skills → optional-skills}/creative/creative-ideation/references/full-prompt-library.md +0 -0
  1186. /package/agent/{skills → optional-skills}/creative/pixel-art/ATTRIBUTION.md +0 -0
  1187. /package/agent/{skills → optional-skills}/creative/pixel-art/SKILL.md +0 -0
  1188. /package/agent/{skills → optional-skills}/creative/pixel-art/references/palettes.md +0 -0
  1189. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/__init__.py +0 -0
  1190. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/palettes.py +0 -0
  1191. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art.py +0 -0
  1192. /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art_video.py +0 -0
  1193. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/SKILL.md +0 -0
  1194. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/analysis-modules.md +0 -0
  1195. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/methods-guide.md +0 -0
  1196. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/abliteration-config.yaml +0 -0
  1197. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/analysis-study.yaml +0 -0
  1198. /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/batch-abliteration.yaml +0 -0
  1199. /package/agent/{skills → optional-skills}/mlops/research/DESCRIPTION.md +0 -0
  1200. /package/agent/{skills → optional-skills}/mlops/research/dspy/SKILL.md +0 -0
  1201. /package/agent/{skills → optional-skills}/mlops/research/dspy/references/examples.md +0 -0
  1202. /package/agent/{skills → optional-skills}/mlops/research/dspy/references/modules.md +0 -0
  1203. /package/agent/{skills → optional-skills}/mlops/research/dspy/references/optimizers.md +0 -0
  1204. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/jailbreak-templates.md +0 -0
  1205. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/refusal-detection.md +0 -0
  1206. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/godmode_race.py +0 -0
  1207. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/load_godmode.py +0 -0
  1208. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/parseltongue.py +0 -0
  1209. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill-subtle.json +0 -0
  1210. /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill.json +0 -0
  1211. /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/context-budget-discipline.md +0 -0
  1212. /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/gates-taxonomy.md +0 -0
@@ -13,15 +13,18 @@ This module provides:
13
13
  """
14
14
 
15
15
  import copy
16
+ import json
16
17
  import logging
17
18
  import os
18
19
  import platform
19
20
  import re
21
+ import shutil
20
22
  import stat
21
23
  import subprocess
22
24
  import sys
23
25
  import tempfile
24
26
  import threading
27
+ import time
25
28
  from dataclasses import dataclass
26
29
  from pathlib import Path
27
30
  from typing import Dict, Any, Optional, List, Tuple
@@ -36,6 +39,60 @@ logger = logging.getLogger(__name__)
36
39
  _CONFIG_PARSE_WARNED: set = set()
37
40
 
38
41
 
42
+ def _backup_corrupt_config(config_path: Path) -> Optional[Path]:
43
+ """Preserve a corrupted ``config.yaml`` by copying it to a timestamped ``.bak``.
44
+
45
+ When the YAML can't be parsed, ``load_config()`` silently falls back to
46
+ ``DEFAULT_CONFIG`` and the user's broken file stays on disk untouched.
47
+ That file is still the user's only copy of their intended overrides — if
48
+ they re-run the setup wizard or ``hermes config set`` (which rewrites
49
+ ``config.yaml``), the broken-but-recoverable content is gone for good.
50
+
51
+ This snapshots the corrupted file to ``config.yaml.corrupt.<ts>.bak`` so
52
+ the user can diff/repair it. Unlike Gemini CLI's policy-file recovery
53
+ (which resets the live file to a clean state), we deliberately leave
54
+ ``config.yaml`` in place: hermes never silently mutates the user's config,
55
+ and leaving it means a hand-fixed file is re-read on the next load. The
56
+ backup is best-effort — any failure (permissions, symlink, disk full) is
57
+ swallowed so config loading is never blocked by backup problems.
58
+
59
+ Returns the backup path on success, else ``None``. Symlinks are not
60
+ followed/copied (mirrors the Gemini #21541 lstat guard) to avoid
61
+ clobbering whatever a malicious/misconfigured symlink points at.
62
+ """
63
+ try:
64
+ if config_path.is_symlink():
65
+ return None
66
+ st = config_path.stat()
67
+ if st.st_size == 0:
68
+ # Empty file isn't worth preserving and yaml.safe_load returns {}
69
+ # for it anyway (so it wouldn't reach here), but guard regardless.
70
+ return None
71
+ ts = time.strftime("%Y%m%d-%H%M%S")
72
+ backup_path = config_path.with_name(f"{config_path.name}.corrupt.{ts}.bak")
73
+ # Don't clobber an existing backup from the same second; if there's
74
+ # already a corrupt backup for this exact mtime, assume we've snapshotted
75
+ # this corruption already and skip (the dedup cache normally prevents a
76
+ # second call, but a process restart can clear it).
77
+ sibling_baks = list(
78
+ config_path.parent.glob(f"{config_path.name}.corrupt.*.bak")
79
+ )
80
+ for existing in sibling_baks:
81
+ try:
82
+ if existing.stat().st_size == st.st_size:
83
+ # Same size as the current broken file — likely the same
84
+ # corruption already preserved. Avoid backup churn.
85
+ return None
86
+ except OSError:
87
+ continue
88
+ if backup_path.exists():
89
+ return None
90
+ shutil.copy2(config_path, backup_path)
91
+ return backup_path
92
+ except Exception:
93
+ return None
94
+
95
+
39
96
  def _warn_config_parse_failure(config_path: Path, exc: Exception) -> None:
40
97
  """Surface a config.yaml parse failure to user, log, and stderr.
41
98
 
@@ -48,7 +105,11 @@ def _warn_config_parse_failure(config_path: Path, exc: Exception) -> None:
48
105
  Now: warn once per (path, mtime_ns, size) on stderr **and** in
49
106
  ``agent.log`` / ``errors.log`` at WARNING level so ``hermes logs``
50
107
  surfaces it. Re-warns automatically if the file changes (different
51
- mtime/size), so users editing the config see the next failure.
108
+ mtime/size), so users editing the config see the next failure. On the
109
+ first warning for a given broken file we also snapshot it to a
110
+ timestamped ``.bak`` (best-effort) so the user's recoverable content
111
+ survives any later rewrite of ``config.yaml`` by the setup wizard or
112
+ ``hermes config set``.
52
113
  """
53
114
  try:
54
115
  st = config_path.stat()
@@ -59,12 +120,16 @@ def _warn_config_parse_failure(config_path: Path, exc: Exception) -> None:
59
120
  return
60
121
  _CONFIG_PARSE_WARNED.add(key)
61
122
 
123
+ backup_path = _backup_corrupt_config(config_path)
124
+
62
125
  msg = (
63
126
  f"Failed to parse {config_path}: {exc}. "
64
127
  f"Falling back to default config — every user override "
65
128
  f"(auxiliary providers, fallback chain, model settings) is being IGNORED. "
66
129
  f"Fix the YAML and restart."
67
130
  )
131
+ if backup_path is not None:
132
+ msg += f" A copy of the corrupted file was saved to {backup_path}."
68
133
  logger.warning(msg)
69
134
  try:
70
135
  sys.stderr.write(f"⚠️ hermes config: {msg}\n")
@@ -158,7 +223,10 @@ _LAST_EXPANDED_CONFIG_BY_PATH: Dict[str, Any] = {}
158
223
  # save_config() + migrate_config() write via atomic_yaml_write which
159
224
  # produces a fresh inode, so stat() sees a new mtime_ns and the next
160
225
  # load repopulates automatically — no explicit invalidation hook.
161
- _LOAD_CONFIG_CACHE: Dict[str, Tuple[int, int, Dict[str, Any]]] = {}
226
+ # Cached tuple is (user_mtime_ns, user_size, managed_mtime_ns, managed_size,
227
+ # merged_value) — the managed-file signature is folded in so editing the
228
+ # managed-scope config.yaml invalidates the cache (see managed_scope).
229
+ _LOAD_CONFIG_CACHE: Dict[str, Tuple[int, int, int, int, Dict[str, Any]]] = {}
162
230
  # (path, mtime_ns, size) -> cached raw yaml dict. Same pattern as
163
231
  # _LOAD_CONFIG_CACHE but for read_raw_config() — used when callers want
164
232
  # the user's on-disk values without defaults merged in.
@@ -205,6 +273,11 @@ _EXTRA_ENV_KEYS = frozenset({
205
273
  "IRC_SERVER", "IRC_PORT", "IRC_NICKNAME", "IRC_CHANNEL",
206
274
  "IRC_USE_TLS", "IRC_SERVER_PASSWORD", "IRC_NICKSERV_PASSWORD",
207
275
  "TERMINAL_ENV", "TERMINAL_SSH_KEY", "TERMINAL_SSH_PORT",
276
+ # Deprecated tool-progress env vars — replaced by display.tool_progress in
277
+ # config.yaml. Kept known here so .env sanitization/reload still handle
278
+ # them for existing users (gateway reads them as a back-compat fallback),
279
+ # without surfacing them in user-facing OPTIONAL_ENV_VARS listings.
280
+ "HERMES_TOOL_PROGRESS", "HERMES_TOOL_PROGRESS_MODE",
208
281
  "WHATSAPP_MODE", "WHATSAPP_ENABLED",
209
282
  "MATTERMOST_HOME_CHANNEL", "MATTERMOST_HOME_CHANNEL_NAME", "MATTERMOST_REPLY_MODE",
210
283
  "MATRIX_PASSWORD", "MATRIX_ENCRYPTION", "MATRIX_DEVICE_ID", "MATRIX_HOME_ROOM",
@@ -280,52 +353,124 @@ def get_managed_update_command() -> Optional[str]:
280
353
  return None
281
354
 
282
355
 
356
+ def _install_method_project_root(project_root: Optional[Path] = None) -> Path:
357
+ """Resolve the directory that holds the *running code* (the install tree).
358
+
359
+ This is the parent of ``hermes_cli/`` — i.e. the git checkout for source
360
+ installs, ``/opt/hermes`` inside the published image, the venv's
361
+ site-packages root for pip installs. It is a property of the running
362
+ interpreter, NOT of ``$HERMES_HOME``, which is why a code-scoped stamp
363
+ here is immune to two installs sharing one data directory.
364
+ """
365
+ if project_root is not None:
366
+ return project_root
367
+ return Path(__file__).parent.parent.resolve()
368
+
369
+
283
370
  def detect_install_method(project_root: Optional[Path] = None) -> str:
284
371
  """Detect how Hermes was installed: 'docker', 'nixos', 'homebrew', 'git', or 'pip'.
285
372
 
286
373
  Resolution order:
287
- 1. Stamped ``~/.hermes/.install_method`` file (written by installers)
288
- 2. HERMES_MANAGED env / .managed marker (NixOS, Homebrew)
289
- 3. .git directory presence -> 'git'
290
- 4. Fallback -> 'pip'
374
+ 1. Code-scoped stamp ``<install tree>/.install_method`` (next to the
375
+ running code) the authoritative marker.
376
+ 2. Legacy home-scoped stamp ``$HERMES_HOME/.install_method`` — read for
377
+ backward compatibility, but a ``docker`` value is IGNORED when we are
378
+ not actually running inside a container (see below).
379
+ 3. HERMES_MANAGED env / .managed marker (NixOS, Homebrew)
380
+ 4. .git directory presence -> 'git'
381
+ 5. Fallback -> 'pip'
382
+
383
+ Why the stamp is code-scoped, not home-scoped (issue: shared ``~/.hermes``)
384
+ --------------------------------------------------------------------------
385
+ The install method describes *the binary that is running*, but
386
+ ``$HERMES_HOME`` is a shared DATA directory — the Docker docs deliberately
387
+ bind-mount it (``~/.hermes:/opt/data``) so config/sessions/memory persist
388
+ and can be shared with a host-side Desktop/CLI install. When a
389
+ containerised gateway and a host install share one ``$HERMES_HOME``, a
390
+ home-scoped stamp is a single slot describing two different installs:
391
+ the container stamps ``docker`` on every boot, the host install then reads
392
+ ``docker`` and ``hermes update`` refuses to run ("doesn't apply inside the
393
+ Docker container") even though the host binary is a perfectly updatable
394
+ git/pip install. Scoping the stamp to the install tree gives each install
395
+ its own truthful marker.
396
+
397
+ Self-healing for already-poisoned homes: a legacy ``docker`` value in the
398
+ home-scoped stamp is only honoured when we are genuinely in a container.
399
+ On a host install that read a contaminating ``docker`` stamp, we fall
400
+ through to managed/.git/pip detection instead — so existing shared-home
401
+ setups recover without the user touching anything.
291
402
 
292
403
  Note: running inside a container is NOT treated as "docker" on its own.
293
- The two supported install paths both self-identify via the
294
- ``.install_method`` stamp (caught by step 1), so neither relies on
295
- container detection here:
404
+ The supported installs self-identify via the code-scoped stamp:
296
405
  - the curl installer (scripts/install.sh, the README/website install
297
- command) git-clones the repo and stamps ``git``;
298
- - the published ``nousresearch/hermes-agent`` image stamps ``docker``
299
- at boot via ``docker/stage2-hook.sh``.
300
- An unsupported manual install dropped into a container (no stamp) was
301
- wrongly classified as the published image by bare container detection,
302
- so ``hermes update`` bailed with "doesn't apply inside the Docker
303
- container". Without that fallback such installs fall through to the
304
- ``.git``/pip checks and behave like any off-path install. See issue #34397.
406
+ command) git-clones the repo and stamps ``git`` next to the code;
407
+ - the published ``nousresearch/hermes-agent`` image bakes a ``docker``
408
+ stamp into ``/opt/hermes`` at build time.
409
+ An unsupported manual install dropped into a container (no stamp) falls
410
+ through to the ``.git``/pip checks and behaves like any off-path install.
411
+ See issue #34397.
305
412
  """
306
- stamp = get_hermes_home() / ".install_method"
413
+ root = _install_method_project_root(project_root)
414
+
415
+ # 1. Code-scoped stamp — authoritative, immune to shared $HERMES_HOME.
307
416
  try:
308
- method = stamp.read_text(encoding="utf-8").strip().lower()
417
+ method = (root / ".install_method").read_text(encoding="utf-8").strip().lower()
309
418
  if method:
310
419
  return method
311
420
  except OSError:
312
421
  pass
422
+
423
+ # 2. Legacy home-scoped stamp — back-compat. Ignore a ``docker`` value
424
+ # when we are not actually containerised: that is the signature of a
425
+ # host install whose shared $HERMES_HOME was stamped by a co-located
426
+ # container, and honouring it wrongly blocks ``hermes update``.
427
+ try:
428
+ method = (
429
+ (get_hermes_home() / ".install_method")
430
+ .read_text(encoding="utf-8")
431
+ .strip()
432
+ .lower()
433
+ )
434
+ if method and not (method == "docker" and not _running_in_container()):
435
+ return method
436
+ except OSError:
437
+ pass
438
+
313
439
  managed = get_managed_system()
314
440
  if managed:
315
441
  return managed.lower().replace(" ", "-")
316
- if project_root is None:
317
- project_root = Path(__file__).parent.parent.resolve()
318
- if (project_root / ".git").is_dir():
442
+ if (root / ".git").is_dir():
319
443
  return "git"
320
444
  return "pip"
321
445
 
322
446
 
323
- def stamp_install_method(method: str) -> None:
324
- """Write the install method to ~/.hermes/.install_method."""
325
- stamp = get_hermes_home() / ".install_method"
447
+ def _running_in_container() -> bool:
448
+ """Thin wrapper around ``hermes_constants.is_container`` (import-safe)."""
449
+ try:
450
+ from hermes_constants import is_container
451
+
452
+ return is_container()
453
+ except Exception:
454
+ return False
455
+
456
+
457
+ def stamp_install_method(method: str, project_root: Optional[Path] = None) -> None:
458
+ """Write the install method next to the running code (code-scoped stamp).
459
+
460
+ The stamp lives in the install tree (``<install tree>/.install_method``),
461
+ not in ``$HERMES_HOME``, so that two installs sharing one data directory
462
+ do not overwrite each other's marker. See ``detect_install_method`` for
463
+ the full rationale.
464
+
465
+ Best-effort: if the install tree is read-only (e.g. the immutable
466
+ ``/opt/hermes`` in the published image, which instead bakes the stamp at
467
+ build time) the write silently no-ops and detection falls back to its
468
+ other signals.
469
+ """
470
+ root = _install_method_project_root(project_root)
326
471
  try:
327
- stamp.parent.mkdir(parents=True, exist_ok=True)
328
- stamp.write_text(method + "\n", encoding="utf-8")
472
+ root.mkdir(parents=True, exist_ok=True)
473
+ (root / ".install_method").write_text(method + "\n", encoding="utf-8")
329
474
  except OSError:
330
475
  pass
331
476
 
@@ -742,6 +887,9 @@ DEFAULT_CONFIG = {
742
887
  "credential_pool_strategies": {},
743
888
  "toolsets": ["hermes-cli"],
744
889
  "mcp_servers": {},
890
+ # Global active chat session cap across CLI, TUI/dashboard, and messaging.
891
+ # None/0 = unbounded.
892
+ "max_concurrent_sessions": None,
745
893
  "agent": {
746
894
  "max_turns": 90,
747
895
  # Inactivity timeout for gateway agent execution (seconds).
@@ -781,6 +929,15 @@ DEFAULT_CONFIG = {
781
929
  # plausible-looking output when a real path is blocked. Costs ~80
782
930
  # tokens in the cached system prompt. Set False to disable globally.
783
931
  "task_completion_guidance": True,
932
+ # Universal parallel-tool-call guidance — short prompt block applied to
933
+ # all models that tells the model to batch independent tool calls
934
+ # (reads, searches, web fetches, read-only commands) into one turn
935
+ # instead of one call per turn. The runtime already runs independent
936
+ # calls concurrently, so this just steers the model to produce the
937
+ # batch — cutting round-trips and the resent-context cost that
938
+ # compounds over a long conversation. Costs ~70 tokens in the cached
939
+ # system prompt. Set False to disable globally.
940
+ "parallel_tool_call_guidance": True,
784
941
  # Local-environment toolchain probe — surfaces Python/pip/uv/PEP-668
785
942
  # state in the system prompt when something non-default is detected
786
943
  # (e.g. python3 has no pip module, pip→python version mismatch, PEP
@@ -796,6 +953,21 @@ DEFAULT_CONFIG = {
796
953
  # identity slot (SOUL.md). Empty by default. The HERMES_ENVIRONMENT_HINT
797
954
  # env var overrides this (build-time/container mechanism).
798
955
  "environment_hint": "",
956
+ # Coding posture — on interactive coding surfaces (CLI, TUI, desktop
957
+ # app, ACP) in a code workspace, Hermes adds a coding operating brief
958
+ # + a live git/workspace snapshot to the system prompt. See
959
+ # agent/coding_context.py.
960
+ # "auto" (default) — prompt-only posture when the surface is
961
+ # interactive AND cwd is a code workspace.
962
+ # Toolsets are never touched; messaging platforms
963
+ # unaffected.
964
+ # "focus" — auto + collapse the toolset to the lean coding
965
+ # set (+ enabled MCP servers) + demote non-coding
966
+ # skill categories to names-only in the prompt's
967
+ # skill index. Explicit opt-in.
968
+ # "on" — force the prompt posture everywhere.
969
+ # "off" — disable entirely.
970
+ "coding_context": "auto",
799
971
  # Staged inactivity warning: send a warning to the user at this
800
972
  # threshold before escalating to a full timeout. The warning fires
801
973
  # once per run and does not interrupt the agent. 0 = disable warning.
@@ -854,6 +1026,13 @@ DEFAULT_CONFIG = {
854
1026
  # (terminal and execute_code). Skill-declared required_environment_variables
855
1027
  # are passed through automatically; this list is for non-skill use cases.
856
1028
  "env_passthrough": [],
1029
+ # HOME handling for host tool subprocesses:
1030
+ # auto — host keeps the real OS-user HOME; containers use
1031
+ # HERMES_HOME/home for persistent state (default)
1032
+ # real — force the real OS-user HOME
1033
+ # profile — force HERMES_HOME/home when it exists (old strict
1034
+ # per-profile CLI config isolation)
1035
+ "home_mode": "auto",
857
1036
  # Extra files to source in the login shell when building the
858
1037
  # per-session environment snapshot. Use this when tools like nvm,
859
1038
  # pyenv, asdf, or custom PATH entries are registered by files that
@@ -1010,11 +1189,34 @@ DEFAULT_CONFIG = {
1010
1189
  "min_interval_hours": 24,
1011
1190
  },
1012
1191
 
1192
+ # Hard cap (chars) for a single automatic context file such as SOUL.md,
1193
+ # AGENTS.md, CLAUDE.md, .hermes.md, or .cursorrules before Hermes applies
1194
+ # head/tail truncation. ``null`` (the default) lets the cap scale with the
1195
+ # model's context window (floor 20K, ceiling 500K) so large-context models
1196
+ # rarely truncate a project doc. Set a positive integer to pin a fixed cap
1197
+ # and override the dynamic behavior. Separate from read_file tool limits.
1198
+ "context_file_max_chars": None,
1199
+
1013
1200
  # Maximum characters returned by a single read_file call. Reads that
1014
1201
  # exceed this are rejected with guidance to use offset+limit.
1015
1202
  # 100K chars ≈ 25–35K tokens across typical tokenisers.
1016
1203
  "file_read_max_chars": 100_000,
1017
1204
 
1205
+ # Seconds to wait at agent-build time for in-flight MCP server discovery
1206
+ # to finish before the agent snapshots its tool list. MCP discovery runs
1207
+ # in a background thread so a slow/dead server can't freeze startup; this
1208
+ # bounds how long the first agent build blocks on it. The wait returns
1209
+ # the INSTANT discovery completes, so users with no MCP servers (the common
1210
+ # case) or fast servers pay ~0s regardless of this value — the bound is
1211
+ # only reached when a server is genuinely still connecting. The old 0.75s
1212
+ # default was a touch short for HTTP/OAuth servers on a cold connect; a
1213
+ # modest bump lets more of them land in the FIRST turn's snapshot. This is
1214
+ # only a turn-1 latency/UX knob: a server that misses this window is still
1215
+ # picked up automatically on the next turn by the between-turns refresh
1216
+ # (see agent/turn_context.py), so correctness never depends on it. Keep it
1217
+ # small so a slow/dead server adds little to first-response latency.
1218
+ "mcp_discovery_timeout": 1.5,
1219
+
1018
1220
  # Tool-output truncation thresholds. When terminal output or a
1019
1221
  # single read_file page exceeds these limits, Hermes truncates the
1020
1222
  # payload sent to the model (keeping head + tail for terminal,
@@ -1076,6 +1278,31 @@ DEFAULT_CONFIG = {
1076
1278
  # Default False matches historical behavior; set to
1077
1279
  # True if you'd rather pause than silently lose
1078
1280
  # context turns when your aux model is flaky.
1281
+ "codex_gpt55_autoraise": True, # When True, gpt-5.5 on the ChatGPT Codex OAuth
1282
+ # route raises its compaction trigger to 85% (vs the
1283
+ # global `threshold` above). Codex hard-caps gpt-5.5
1284
+ # at a 272K window, so the default 50% would compact
1285
+ # at ~136K and waste half the usable context. Set to
1286
+ # False to opt back down to the global threshold
1287
+ # (e.g. 0.50) for Codex gpt-5.5 sessions. Only this
1288
+ # exact route is affected — gpt-5.5 on OpenAI's
1289
+ # direct API, OpenRouter, and Copilot keep the
1290
+ # global threshold regardless.
1291
+ },
1292
+
1293
+ # Kanban subsystem (orchestrator workers + dispatcher-driven child tasks).
1294
+ # See tools/kanban_tools.py and hermes_cli/kanban_db.py for the actual
1295
+ # implementations. Per-platform notification opt-out is handled by the
1296
+ # kanban dashboard (see ``hermes dashboard`` -> Notifications).
1297
+ "kanban": {
1298
+ # Auto-subscribe the originating gateway/TUI session to task
1299
+ # completion + block events when ``kanban_create`` is called from
1300
+ # inside a session that has a persistent delivery channel. The
1301
+ # agent that dispatched the task will get notified automatically
1302
+ # instead of having to poll. Disable to mirror pre-feature
1303
+ # behaviour — e.g. for a profile that prefers explicit
1304
+ # ``kanban_notify-subscribe`` calls per task.
1305
+ "auto_subscribe_on_create": True,
1079
1306
  },
1080
1307
 
1081
1308
  # Anthropic prompt caching (Claude via OpenRouter or native Anthropic API).
@@ -1213,6 +1440,14 @@ DEFAULT_CONFIG = {
1213
1440
  "timeout": 30,
1214
1441
  "extra_body": {},
1215
1442
  },
1443
+ "tts_audio_tags": {
1444
+ "provider": "auto",
1445
+ "model": "",
1446
+ "base_url": "",
1447
+ "api_key": "",
1448
+ "timeout": 30,
1449
+ "extra_body": {},
1450
+ },
1216
1451
  # Triage specifier — flesh out a rough one-liner in the Kanban
1217
1452
  # Triage column into a concrete spec, then promote it to ``todo``.
1218
1453
  # Invoked by ``hermes kanban specify`` (single id or --all). Set a
@@ -1264,6 +1499,20 @@ DEFAULT_CONFIG = {
1264
1499
  "timeout": 600,
1265
1500
  "extra_body": {},
1266
1501
  },
1502
+ # Monitor — urgency/importance classifier used by the important-mail
1503
+ # monitor catalog automation (cron/scripts/classify_items.py). Scores
1504
+ # candidate items 0-10 against the user's criteria so only above-
1505
+ # threshold items get delivered. "auto" = main chat model; override to
1506
+ # a cheap fast model (e.g. openrouter google/gemini-3-flash-preview,
1507
+ # haiku) since per-item scoring is high-volume and a small model is fine.
1508
+ "monitor": {
1509
+ "provider": "auto",
1510
+ "model": "",
1511
+ "base_url": "",
1512
+ "api_key": "",
1513
+ "timeout": 60,
1514
+ "extra_body": {},
1515
+ },
1267
1516
  },
1268
1517
 
1269
1518
  "display": {
@@ -1284,6 +1533,12 @@ DEFAULT_CONFIG = {
1284
1533
  # behavior of showing tool-call summaries inline.
1285
1534
  "resume_skip_tool_only": True,
1286
1535
  "busy_input_mode": "interrupt", # interrupt | queue | steer
1536
+ # Which interface bare `hermes` (and `hermes chat`) launches by default:
1537
+ # "cli" — the classic prompt_toolkit REPL (default, preserves prior behavior)
1538
+ # "tui" — the modern Ink TUI (same as passing `--tui`)
1539
+ # Explicit flags always win over this setting: `--cli` forces the classic
1540
+ # REPL and `--tui` (or HERMES_TUI=1) forces the TUI regardless of config.
1541
+ "interface": "cli",
1287
1542
  # When true, `hermes --tui` auto-resumes the most recent human-
1288
1543
  # facing session on launch instead of forging a fresh one.
1289
1544
  # Mirrors `hermes -c` muscle memory. Default off so existing
@@ -1296,6 +1551,12 @@ DEFAULT_CONFIG = {
1296
1551
  "tui_agents_nudge": True,
1297
1552
  "bell_on_complete": False,
1298
1553
  "show_reasoning": False,
1554
+ # Background self-improvement review notifications surfaced in chat.
1555
+ # "off" — no chat notification (the review still runs and writes)
1556
+ # "on" — generic "💾 Memory updated" line (default)
1557
+ # "verbose" — include a compact content preview of what changed
1558
+ # Per-platform overrides via display.platforms.<platform>.memory_notifications.
1559
+ "memory_notifications": "on",
1299
1560
  "streaming": False,
1300
1561
  "timestamps": False, # Show [HH:MM] on user and assistant labels
1301
1562
  "final_response_markdown": "strip", # render | strip | raw
@@ -1304,6 +1565,10 @@ DEFAULT_CONFIG = {
1304
1565
  # behaves badly with replayed scrollback.
1305
1566
  "persistent_output": True,
1306
1567
  "persistent_output_max_lines": 200,
1568
+ # Print a one-line summary of resolved modal prompts (approval /
1569
+ # clarify) into scrollback so the question and decision survive the
1570
+ # panel repaint. Set false to keep scrollback untouched.
1571
+ "persist_prompts": True,
1307
1572
  "inline_diffs": True, # Show inline diff previews for write actions (write_file, patch, skill_manage)
1308
1573
  # File-mutation verifier footer. When true (default), the agent
1309
1574
  # appends a one-line advisory to its final response whenever a
@@ -1313,6 +1578,11 @@ DEFAULT_CONFIG = {
1313
1578
  # class of over-claim that otherwise forces users to run
1314
1579
  # `git status` to verify edits landed. Set false to suppress.
1315
1580
  "file_mutation_verifier": True,
1581
+ # Nous credits status-bar notices (usage bands, grant-spent, depleted /
1582
+ # restored). When false, no credits notices are emitted — balance data
1583
+ # is still captured and /usage keeps working. Off switch for sub +
1584
+ # top-up users who find the gauge noisy.
1585
+ "credits_notices": True,
1316
1586
  # Turn-completion explainer. When true (default), the agent appends a
1317
1587
  # one-line explanation to its final response whenever a turn ends
1318
1588
  # abnormally with no usable reply — empty content after retries, a
@@ -1330,6 +1600,14 @@ DEFAULT_CONFIG = {
1330
1600
  # TUI busy indicator style: kaomoji (default), emoji, unicode (braille
1331
1601
  # spinner), or ascii. Live-swappable via `/indicator <style>`.
1332
1602
  "tui_status_indicator": "kaomoji",
1603
+ # Seconds between prompt_toolkit redraws in the classic CLI when idle.
1604
+ # Default 1.0 keeps the wall-clock status-bar read-outs (idle-since-
1605
+ # last-turn) ticking and keeps the bottom chrome alive during idle —
1606
+ # without it prompt_toolkit stops repainting the status bar after a
1607
+ # turn and it can go stale/disappear (#45592).
1608
+ # Set 0 to disable the background refresh if it fights terminal
1609
+ # auto-scroll in non-fullscreen mode on some emulators (#48309).
1610
+ "cli_refresh_interval": 1.0,
1333
1611
  "user_message_preview": { # CLI: how many submitted user-message lines to echo back in scrollback
1334
1612
  "first_lines": 2,
1335
1613
  "last_lines": 2,
@@ -1338,6 +1616,12 @@ DEFAULT_CONFIG = {
1338
1616
  "tool_progress_command": False, # Enable /verbose command in messaging gateway
1339
1617
  "tool_progress_overrides": {}, # DEPRECATED — use display.platforms instead
1340
1618
  "tool_preview_length": 0, # Max chars for tool call previews (0 = no limit, show full paths/commands)
1619
+ # How gateway tool-progress is grouped on platforms that support message
1620
+ # editing: "accumulate" (default) edits one bubble in place; "separate"
1621
+ # sends one message per tool (the pre-v0.9 behavior, noisier). Only
1622
+ # applies where tool_progress is already enabled. Per-platform override
1623
+ # via display.platforms.<platform>.tool_progress_grouping.
1624
+ "tool_progress_grouping": "accumulate",
1341
1625
  # Auto-delete system-notice replies (e.g. "✨ New session started!",
1342
1626
  # "♻ Restarting gateway…", "⚡ Stopped…") after N seconds on platforms
1343
1627
  # that support message deletion (currently Telegram; other platforms
@@ -1413,6 +1697,34 @@ DEFAULT_CONFIG = {
1413
1697
  "client_id": "", # agent:{instance_id} — Portal provisions this
1414
1698
  "portal_url": "", # blank → use plugin default (production Portal)
1415
1699
  },
1700
+ # Username/password gate configuration — read by the bundled
1701
+ # ``dashboard_auth/basic`` plugin (a self-hosted "just put a
1702
+ # password on my dashboard" provider that needs no OAuth IDP).
1703
+ # The plugin registers a password provider when ``username`` plus
1704
+ # either ``password_hash`` (preferred — no plaintext at rest) or
1705
+ # ``password`` (plaintext, hashed in-memory at load) are set. Each
1706
+ # key is overridable by an env var
1707
+ # (``HERMES_DASHBOARD_BASIC_AUTH_USERNAME`` /
1708
+ # ``_PASSWORD_HASH`` / ``_PASSWORD`` / ``_SECRET`` /
1709
+ # ``_TTL_SECONDS``), env winning when non-empty. Leave ``username``
1710
+ # empty (the default) to keep the plugin a no-op — loopback /
1711
+ # ``--insecure`` operators and OAuth users are unaffected.
1712
+ #
1713
+ # ``secret`` is the HMAC key used to sign the stateless session
1714
+ # tokens this provider mints. When empty, a random per-process key
1715
+ # is generated — fine for a single process, but sessions then
1716
+ # don't survive a restart or span multiple workers. Set an
1717
+ # explicit ``secret`` (32+ random bytes, base64/hex/raw) for
1718
+ # stable multi-worker / restart-surviving sessions. Compute a
1719
+ # ``password_hash`` with
1720
+ # ``python -c "from plugins.dashboard_auth.basic import hash_password; print(hash_password('PW'))"``.
1721
+ "basic_auth": {
1722
+ "username": "", # blank → plugin no-op (no password provider)
1723
+ "password_hash": "", # scrypt$... (preferred — no plaintext at rest)
1724
+ "password": "", # plaintext fallback (hashed in-memory at load)
1725
+ "secret": "", # token-signing key; blank → random per-process
1726
+ "session_ttl_seconds": 0, # 0 → plugin default (12h)
1727
+ },
1416
1728
  # Public URL override (env: ``HERMES_DASHBOARD_PUBLIC_URL``).
1417
1729
  # When set, this is the complete authority — scheme + host +
1418
1730
  # optional path prefix (e.g. ``https://example.com/hermes``) —
@@ -1445,7 +1757,7 @@ DEFAULT_CONFIG = {
1445
1757
  # Each provider supports an optional `max_text_length:` override for the
1446
1758
  # per-request input-character cap. Omit it to use the provider's documented
1447
1759
  # limit (OpenAI 4096, xAI 15000, MiniMax 10000, ElevenLabs 5k-40k model-aware,
1448
- # Gemini 5000, Edge 5000, Mistral 4000, NeuTTS/KittenTTS 2000).
1760
+ # Gemini 32000, Edge 5000, Mistral 4000, NeuTTS/KittenTTS 2000).
1449
1761
  "tts": {
1450
1762
  "provider": "edge", # "edge" (free) | "elevenlabs" (premium) | "openai" | "xai" | "minimax" | "mistral" | "gemini" | "neutts" (local) | "kittentts" (local) | "piper" (local)
1451
1763
  "edge": {
@@ -1461,6 +1773,19 @@ DEFAULT_CONFIG = {
1461
1773
  "voice": "alloy",
1462
1774
  # Voices: alloy, echo, fable, onyx, nova, shimmer
1463
1775
  },
1776
+ "gemini": {
1777
+ "model": "gemini-2.5-flash-preview-tts",
1778
+ "voice": "Kore",
1779
+ # When true, Gemini 3.1 TTS uses a hidden auxiliary-model rewrite
1780
+ # pass to insert freeform square-bracket audio tags into the TTS
1781
+ # script. Visible chat replies are unchanged.
1782
+ "audio_tags": False,
1783
+ # Optional local Markdown/text file with Gemini TTS performance
1784
+ # direction. It may include AUDIO PROFILE, SCENE, DIRECTOR'S NOTES,
1785
+ # SAMPLE CONTEXT, and either a `{transcript}` placeholder or no
1786
+ # transcript section; Hermes appends the live transcript when absent.
1787
+ "persona_prompt_file": "",
1788
+ },
1464
1789
  "xai": {
1465
1790
  "voice_id": "eve", # or custom voice ID — see https://docs.x.ai/developers/model-capabilities/audio/custom-voices
1466
1791
  "language": "en",
@@ -1542,6 +1867,19 @@ DEFAULT_CONFIG = {
1542
1867
  "memory": {
1543
1868
  "memory_enabled": True,
1544
1869
  "user_profile_enabled": True,
1870
+ # Approval gate for memory writes (add/replace/remove), applied to BOTH
1871
+ # foreground agent turns and the background self-improvement review fork
1872
+ # (the source of unprompted "wrong assumption" saves users reported).
1873
+ # false (default) — write freely; the gate is off (pre-gate behaviour)
1874
+ # true — require approval: foreground writes prompt inline
1875
+ # (entries are small enough to review in a chat
1876
+ # bubble); background-review writes are staged
1877
+ # instead of committed (a daemon thread cannot block
1878
+ # on a prompt). Review staged entries with
1879
+ # /memory pending, /memory approve <id>,
1880
+ # /memory reject <id>.
1881
+ # To disable memory entirely, use memory_enabled: false instead.
1882
+ "write_approval": False,
1545
1883
  "memory_char_limit": 2200, # ~800 tokens at 2.75 chars/token
1546
1884
  "user_char_limit": 1375, # ~500 tokens at 2.75 chars/token
1547
1885
  # External memory provider plugin (empty = built-in only).
@@ -1572,17 +1910,19 @@ DEFAULT_CONFIG = {
1572
1910
  "inherit_mcp_toolsets": True,
1573
1911
  "max_iterations": 50, # per-subagent iteration cap (each subagent gets its own budget,
1574
1912
  # independent of the parent's max_iterations)
1575
- "child_timeout_seconds": 600, # wall-clock timeout for each child agent (floor 30s,
1576
- # no ceiling). High-reasoning models on large tasks
1577
- # (e.g. gpt-5.5 xhigh, opus-4.6) need generous budgets;
1578
- # raise if children time out before producing output.
1913
+ "child_timeout_seconds": 0, # optional wall-clock cap per child agent. 0 (default)
1914
+ # = no timeout: children fail only from real errors
1915
+ # (API, tools, iteration budget), never a delegation
1916
+ # stopwatch. Set a positive number of seconds
1917
+ # (floor 30s) to enforce a hard cap.
1579
1918
  "reasoning_effort": "", # reasoning effort for subagents: "xhigh", "high", "medium",
1580
1919
  # "low", "minimal", "none" (empty = inherit parent's level)
1581
1920
  "max_concurrent_children": 3, # max parallel children per batch; floor of 1 enforced, no ceiling
1921
+ "max_async_children": 3, # max concurrent background (background=true) subagents; new dispatches rejected at capacity
1582
1922
  # Orchestrator role controls (see tools/delegate_tool.py:_get_max_spawn_depth
1583
- # and _get_orchestrator_enabled). Values are clamped to [1, 3] with a
1584
- # warning log if out of range.
1585
- "max_spawn_depth": 1, # depth cap (1 = flat [default], 2 = orchestrator→leaf, 3 = three-level)
1923
+ # and _get_orchestrator_enabled). Floored at 1, no upper ceiling —
1924
+ # raise deliberately, each level multiplies API cost.
1925
+ "max_spawn_depth": 1, # depth (1 = flat [default], 2 = orchestrator→leaf, 3+ = deeper)
1586
1926
  "orchestrator_enabled": True, # kill switch for role="orchestrator"
1587
1927
  # When a subagent hits a dangerous-command approval prompt, the parent's
1588
1928
  # prompt_toolkit TUI owns stdin — a thread-local input() call from the
@@ -1646,6 +1986,18 @@ DEFAULT_CONFIG = {
1646
1986
  # External hub installs (trusted/community sources) are always
1647
1987
  # scanned regardless of this setting.
1648
1988
  "guard_agent_created": False,
1989
+ # Approval gate for skill_manage (create/edit/patch/write_file/delete/
1990
+ # remove_file), applied to BOTH foreground agent turns and the
1991
+ # background self-improvement review fork.
1992
+ # false (default) — write freely; the gate is off (pre-gate behaviour)
1993
+ # true — require approval: stage the write for review
1994
+ # instead of committing (a SKILL.md is too large to
1995
+ # review inline, so skills always stage rather than
1996
+ # prompt). List with /skills pending, inspect with
1997
+ # /skills diff <id> (full diff — CLI/dashboard/file,
1998
+ # never crammed into a chat bubble), apply with
1999
+ # /skills approve <id> or drop with /skills reject <id>.
2000
+ "write_approval": False,
1649
2001
  },
1650
2002
 
1651
2003
  # Curator — background skill maintenance.
@@ -1669,6 +2021,14 @@ DEFAULT_CONFIG = {
1669
2021
  # Archive a skill (move to skills/.archive/) after this many days
1670
2022
  # without use. Archived skills are recoverable — no auto-deletion.
1671
2023
  "archive_after_days": 90,
2024
+ # Run the LLM consolidation (umbrella-building) pass. OFF by default.
2025
+ # When off, a curator run does ONLY the deterministic inactivity prune
2026
+ # (mark stale / archive long-unused skills) and skips the forked
2027
+ # aux-model review entirely — no umbrella-building, no aux-model cost.
2028
+ # Set to true to opt back into merging overlapping skills into
2029
+ # class-level umbrellas. `hermes curator run --consolidate` overrides
2030
+ # this for a single invocation.
2031
+ "consolidate": False,
1672
2032
  # Also prune (archive) bundled built-in skills after the inactivity
1673
2033
  # period, not just agent-created ones. ON by default. Built-ins are
1674
2034
  # normally restored on every `hermes update`, so pruning them only
@@ -1744,6 +2104,28 @@ DEFAULT_CONFIG = {
1744
2104
  # real memory cost. Default 32 MiB matches the historical hardcoded
1745
2105
  # cap. Set to 0 for no cap. Env override: DISCORD_MAX_ATTACHMENT_BYTES.
1746
2106
  "max_attachment_bytes": 33554432,
2107
+ # Voice-channel audio effects (the continuous mixer). OFF by default.
2108
+ # When enabled, the bot installs a software mixer on the outgoing voice
2109
+ # stream so a low ambient "thinking" bed, verbal acknowledgements, and
2110
+ # TTS replies can OVERLAP (ducking the ambient under speech) instead of
2111
+ # stop-and-swap — the Grok-voice-mode feel. discord.py ships no mixer;
2112
+ # this is implemented in plugins/platforms/discord/voice_mixer.py.
2113
+ "voice_fx": {
2114
+ "enabled": False, # master switch for the mixer subsystem
2115
+ "ambient_enabled": True, # play the idle "thinking" bed while tools run
2116
+ "ambient_path": "", # custom loop audio file; "" = synthesised pad
2117
+ "ambient_gain": 0.18, # idle bed loudness, 0.0–1.0
2118
+ "duck_gain": 0.06, # ambient loudness while speech plays
2119
+ "speech_gain": 1.0, # TTS / ack loudness, 0.0–1.0
2120
+ "ack_enabled": True, # speak a short phrase before the first tool call
2121
+ "ack_phrases": [ # picked at random; set [] to disable phrases
2122
+ "Let me look into that.",
2123
+ "One moment.",
2124
+ "Checking on that now.",
2125
+ "Give me a sec.",
2126
+ "On it.",
2127
+ ],
2128
+ },
1747
2129
  },
1748
2130
 
1749
2131
  # WhatsApp platform settings (gateway mode)
@@ -1759,6 +2141,9 @@ DEFAULT_CONFIG = {
1759
2141
  "reactions": False, # Add 👀/✅/❌ reactions to messages during processing
1760
2142
  "channel_prompts": {}, # Per-chat/topic ephemeral system prompts (topics inherit from parent group)
1761
2143
  "allowed_chats": "", # If set, bot ONLY responds in these group/supergroup chat IDs (whitelist)
2144
+ "extra": {
2145
+ "rich_messages": True, # Bot API 10.1 rich messages (tables/task lists/details/math) render natively; set False to force legacy MarkdownV2
2146
+ },
1762
2147
  },
1763
2148
 
1764
2149
  # Mattermost platform settings (gateway mode)
@@ -1812,6 +2197,22 @@ DEFAULT_CONFIG = {
1812
2197
  # User-defined quick commands that bypass the agent loop (type: exec only)
1813
2198
  "quick_commands": {},
1814
2199
 
2200
+ # Per-platform system-prompt hint overrides. Lets an admin append to or
2201
+ # replace Hermes' built-in platform hint for a single messaging platform
2202
+ # (WhatsApp, Slack, Telegram, ...) without affecting other platforms.
2203
+ # Useful for enterprise/managed profiles that ship platform-aware skills.
2204
+ # Each key is a platform name; the value is either:
2205
+ # { "append": "extra text" } — keep the default hint, append text
2206
+ # { "replace": "full text" } — substitute the default hint entirely
2207
+ # "extra text" — shorthand for { "append": ... }
2208
+ # `replace` wins over `append` if both are given. Example:
2209
+ # platform_hints:
2210
+ # whatsapp:
2211
+ # append: >
2212
+ # When tabular output would be useful, invoke the
2213
+ # table_formatting skill instead of emitting a Markdown table.
2214
+ "platform_hints": {},
2215
+
1815
2216
  # Shell-script hooks — declarative bridge that invokes shell scripts
1816
2217
  # on plugin-hook events (pre_tool_call, post_tool_call, pre_llm_call,
1817
2218
  # subagent_stop, etc.). Each entry maps an event name to a list of
@@ -1862,6 +2263,33 @@ DEFAULT_CONFIG = {
1862
2263
  },
1863
2264
 
1864
2265
  "cron": {
2266
+ # Active cron SCHEDULER provider (Axis B — the trigger that decides
2267
+ # WHEN a due job fires). Empty string = the built-in in-process 60s
2268
+ # ticker (default). Name an installed provider (plugins/cron/<name>/ or
2269
+ # $HERMES_HOME/plugins/<name>/) to relocate the trigger — e.g. "chronos",
2270
+ # the NAS-mediated managed-cron provider for scale-to-zero deployments.
2271
+ # An unknown or unavailable provider falls back to the built-in, so cron
2272
+ # never loses its trigger.
2273
+ "provider": "",
2274
+ # Chronos (NAS-mediated managed cron) settings. Only consulted when
2275
+ # provider == "chronos". All non-secret (URLs + the JWT audience): the
2276
+ # agent holds NO external-scheduler credentials. For hosted agents, NAS
2277
+ # sets these at provision time. The outbound provision call reuses the
2278
+ # agent's existing Nous Portal token — there is no token key here.
2279
+ "chronos": {
2280
+ # NAS / portal base URL the agent calls to arm/cancel one-shots
2281
+ # and that mints the inbound fire JWT (used as the expected issuer).
2282
+ "portal_url": "https://portal.nousresearch.com",
2283
+ # The agent's OWN publicly-reachable base URL for NAS→agent fires
2284
+ # (NAS POSTs {callback_url}/api/cron/fire). Empty → Chronos is
2285
+ # unavailable and the resolver falls back to the built-in ticker.
2286
+ "callback_url": "",
2287
+ # This agent's expected JWT audience (e.g. "agent:{instance_id}").
2288
+ "expected_audience": "",
2289
+ # NAS JWKS URL for verifying the inbound fire JWT's signature.
2290
+ # Empty → the fire endpoint refuses all tokens (no unsigned decode).
2291
+ "nas_jwks_url": "",
2292
+ },
1865
2293
  # Wrap delivered cron responses with a header (task name) and footer
1866
2294
  # ("The agent cannot see this message"). Set to false for clean output.
1867
2295
  "wrap_response": True,
@@ -1897,11 +2325,11 @@ DEFAULT_CONFIG = {
1897
2325
  # raise these to keep more early failure evidence.
1898
2326
  "worker_log_rotate_bytes": 2 * 1024 * 1024,
1899
2327
  "worker_log_backup_count": 1,
1900
- # Profile that decomposes tasks in the Triage column. When unset,
1901
- # falls back to the default profile (the one `hermes` launches with
1902
- # no -p flag). Set this to a dedicated 'orchestrator' profile if you
1903
- # want decomposition to use a different model/skills from your main
1904
- # working profile.
2328
+ # Profile assigned to the root/orchestration task after Triage
2329
+ # decomposition. When unset, falls back to the default profile (the
2330
+ # one `hermes` launches with no -p flag). This does not control the
2331
+ # decomposer prompt, model, or skills; configure that LLM path under
2332
+ # auxiliary.kanban_decomposer.
1905
2333
  "orchestrator_profile": "",
1906
2334
  # Where a child task lands if the orchestrator can't match an
1907
2335
  # assignee to any installed profile. When unset, falls back to the
@@ -2019,6 +2447,17 @@ DEFAULT_CONFIG = {
2019
2447
  # Gateway settings — control how messaging platforms (Telegram, Discord,
2020
2448
  # Slack, etc.) deliver agent-produced files as native attachments.
2021
2449
  "gateway": {
2450
+ # Inject a human-readable timestamp prefix (e.g.
2451
+ # "[Tue 2026-04-28 13:40:53 CEST]") onto user messages IN THE MODEL'S
2452
+ # CONTEXT so the agent has temporal awareness of when each message was
2453
+ # sent. Off by default — when off, the model sees clean message text.
2454
+ # Persisted transcripts always stay clean (the timestamp is stored as
2455
+ # message metadata regardless of this toggle), so turning it on later
2456
+ # surfaces send-times for past messages too.
2457
+ "message_timestamps": {
2458
+ "enabled": False,
2459
+ },
2460
+
2022
2461
  # When false (default), any file path the agent emits is delivered
2023
2462
  # as a native attachment as long as it isn't under the credential /
2024
2463
  # system-path denylist (/etc, /proc, ~/.ssh, ~/.aws, ~/.hermes/.env,
@@ -2094,7 +2533,7 @@ DEFAULT_CONFIG = {
2094
2533
  # delivered as a fresh message if the preview has been visible at
2095
2534
  # least this many seconds, so the platform timestamp reflects
2096
2535
  # completion time. Telegram only; other platforms ignore it.
2097
- "fresh_final_after_seconds": 60.0,
2536
+ "fresh_final_after_seconds": 0.0,
2098
2537
  },
2099
2538
 
2100
2539
  # Session storage — controls automatic cleanup of ~/.hermes/state.db.
@@ -2139,23 +2578,48 @@ DEFAULT_CONFIG = {
2139
2578
  # never fires again. Users can wipe the section to re-see all hints.
2140
2579
  "onboarding": {
2141
2580
  "seen": {},
2581
+ # Structured profile-build path offered on the very first gateway
2582
+ # message ever. "ask" (default) -> offer to build a user profile
2583
+ # (opt-in, consent-gated; the agent asks before any lookup and never
2584
+ # reads connected accounts silently). "off" -> plain intro only.
2585
+ # The offer fires at most once (latched under onboarding.seen).
2586
+ "profile_build": "ask",
2142
2587
  },
2143
2588
 
2144
2589
  # ``hermes update`` behaviour.
2145
2590
  "updates": {
2146
2591
  # Run a full ``hermes backup``-style zip of HERMES_HOME before every
2147
2592
  # ``hermes update``. Backups land in ``<HERMES_HOME>/backups/`` and
2148
- # can be restored with ``hermes import <path>``. Off by default —
2149
- # on large HERMES_HOME directories the zip can add minutes to every
2150
- # update. Set to true to re-enable, or pass ``--backup`` to opt in
2151
- # for a single update run.
2152
- "pre_update_backup": False,
2593
+ # can be restored with ``hermes import <path>``. Defaults to true
2594
+ # after the #48200 incident: a ``hermes update --yes`` run that
2595
+ # computed a wrong path silently wiped the user's ``.env``,
2596
+ # ``MEMORY.md``, ``kanban.db``, custom skills, and scripts in one
2597
+ # go. The cost of a few minutes of zip time per update is
2598
+ # negligible compared to the alternative. Set to false to opt
2599
+ # out, or pass ``--no-backup`` for a single update run.
2600
+ "pre_update_backup": True,
2153
2601
  # How many pre-update backup zips to retain. Older ones are pruned
2154
2602
  # automatically after each successful backup. Values below 1 are
2155
2603
  # floored to 1 — the backup just created is always preserved. To
2156
2604
  # disable backups entirely, set ``pre_update_backup: false`` above
2157
2605
  # rather than ``backup_keep: 0``.
2158
2606
  "backup_keep": 5,
2607
+ # What `hermes update` does with uncommitted local changes to the
2608
+ # source tree when it runs NON-interactively — i.e. triggered from
2609
+ # the desktop/chat app or the gateway, where there's no TTY to answer
2610
+ # a restore prompt. Interactive (terminal) updates are unaffected:
2611
+ # they always stash the changes and ask whether to restore, exactly
2612
+ # as they always have.
2613
+ # "stash" — auto-stash the changes, pull, then auto-restore them
2614
+ # on top of the updated code (the safe default; nothing
2615
+ # is ever lost — conflicts are preserved in a git stash).
2616
+ # "discard" — auto-stash the changes and throw the stash away after
2617
+ # the pull. Use this only if you never intend to keep
2618
+ # local edits to the source tree on this machine.
2619
+ # Stash-and-drop (not `reset --hard` + `clean -fd`) so
2620
+ # ignored paths — node_modules, venv, build outputs —
2621
+ # are never touched.
2622
+ "non_interactive_local_changes": "stash",
2159
2623
  },
2160
2624
 
2161
2625
  # Language Server Protocol — semantic diagnostics from real
@@ -2285,7 +2749,7 @@ DEFAULT_CONFIG = {
2285
2749
 
2286
2750
 
2287
2751
  # Config schema version - bump this when adding new required fields
2288
- "_config_version": 25,
2752
+ "_config_version": 30,
2289
2753
  }
2290
2754
 
2291
2755
  # =============================================================================
@@ -3016,6 +3480,7 @@ OPTIONAL_ENV_VARS = {
3016
3480
  "Required scopes: chat:write, app_mentions:read, channels:history, groups:history, "
3017
3481
  "im:history, im:read, im:write, users:read, files:read, files:write",
3018
3482
  "prompt": "Slack Bot Token (xoxb-...)",
3483
+ "help": "In your Slack app, add the required bot scopes, install the app to the workspace, then copy OAuth & Permissions > Bot User OAuth Token.",
3019
3484
  "url": "https://api.slack.com/apps",
3020
3485
  "password": True,
3021
3486
  "category": "messaging",
@@ -3025,10 +3490,19 @@ OPTIONAL_ENV_VARS = {
3025
3490
  "App-Level Tokens. Also ensure Event Subscriptions include: message.im, "
3026
3491
  "message.channels, message.groups, app_mention",
3027
3492
  "prompt": "Slack App Token (xapp-...)",
3493
+ "help": "In your Slack app, enable Socket Mode, then create Basic Information > App-Level Tokens with the connections:write scope.",
3028
3494
  "url": "https://api.slack.com/apps",
3029
3495
  "password": True,
3030
3496
  "category": "messaging",
3031
3497
  },
3498
+ "SLACK_ALLOWED_USERS": {
3499
+ "description": "Comma-separated Slack member IDs allowed to use Hermes, e.g. U01ABC2DEF3. Without this, Slack may connect but deny messages by default.",
3500
+ "prompt": "Allowed Slack member IDs",
3501
+ "help": "In Slack, open your profile, choose More or the three-dot menu, then Copy member ID. Add multiple IDs comma-separated.",
3502
+ "url": "https://api.slack.com/apps",
3503
+ "password": False,
3504
+ "category": "messaging",
3505
+ },
3032
3506
  "MATTERMOST_URL": {
3033
3507
  "description": "Mattermost server URL (e.g. https://mm.example.com)",
3034
3508
  "prompt": "Mattermost server URL",
@@ -3341,30 +3815,13 @@ OPTIONAL_ENV_VARS = {
3341
3815
  "password": True,
3342
3816
  "category": "setting",
3343
3817
  },
3344
- "HERMES_MAX_ITERATIONS": {
3345
- "description": "Maximum tool-calling iterations per conversation (default: 90)",
3346
- "prompt": "Max iterations",
3347
- "url": None,
3348
- "password": False,
3349
- "category": "setting",
3350
- },
3351
3818
  # HERMES_TOOL_PROGRESS and HERMES_TOOL_PROGRESS_MODE are deprecated —
3352
3819
  # now configured via display.tool_progress in config.yaml (off|new|all|verbose).
3353
- # Gateway falls back to these env vars for backward compatibility.
3354
- "HERMES_TOOL_PROGRESS": {
3355
- "description": "(deprecated) Use display.tool_progress in config.yaml instead",
3356
- "prompt": "Tool progress (deprecated use config.yaml)",
3357
- "url": None,
3358
- "password": False,
3359
- "category": "setting",
3360
- },
3361
- "HERMES_TOOL_PROGRESS_MODE": {
3362
- "description": "(deprecated) Use display.tool_progress in config.yaml instead",
3363
- "prompt": "Progress mode (deprecated — use config.yaml)",
3364
- "url": None,
3365
- "password": False,
3366
- "category": "setting",
3367
- },
3820
+ # The gateway still falls back to these env vars for backward compatibility,
3821
+ # so they live in _EXTRA_ENV_KEYS (known to .env sanitization/reload) but
3822
+ # are intentionally NOT listed here: OPTIONAL_ENV_VARS feeds user-facing
3823
+ # surfaces (dashboard keys page, setup checklists) and deprecated knobs
3824
+ # shouldn't be offered there.
3368
3825
  "HERMES_PREFILL_MESSAGES_FILE": {
3369
3826
  "description": "Path to JSON file with ephemeral prefill messages for few-shot priming",
3370
3827
  "prompt": "Prefill messages file path",
@@ -3678,6 +4135,42 @@ def _normalize_custom_provider_entry(
3678
4135
  return normalized
3679
4136
 
3680
4137
 
4138
+ def _custom_provider_entry_to_provider_config(
4139
+ entry: Any,
4140
+ *,
4141
+ provider_key: str = "",
4142
+ ) -> Optional[Dict[str, Any]]:
4143
+ """Translate a legacy custom provider entry to the v12 providers shape."""
4144
+ normalized = _normalize_custom_provider_entry(
4145
+ dict(entry) if isinstance(entry, dict) else entry,
4146
+ provider_key=provider_key,
4147
+ )
4148
+ if normalized is None:
4149
+ return None
4150
+
4151
+ provider_entry: Dict[str, Any] = {"api": normalized["base_url"]}
4152
+
4153
+ for field in (
4154
+ "name",
4155
+ "api_key",
4156
+ "key_env",
4157
+ "models",
4158
+ "context_length",
4159
+ "rate_limit_delay",
4160
+ "discover_models",
4161
+ "extra_body",
4162
+ ):
4163
+ if field in normalized:
4164
+ provider_entry[field] = normalized[field]
4165
+
4166
+ if "model" in normalized:
4167
+ provider_entry["default_model"] = normalized["model"]
4168
+ if "api_mode" in normalized:
4169
+ provider_entry["transport"] = normalized["api_mode"]
4170
+
4171
+ return provider_entry
4172
+
4173
+
3681
4174
  def providers_dict_to_custom_providers(providers_dict: Any) -> List[Dict[str, Any]]:
3682
4175
  """Normalize ``providers`` config entries into the legacy custom-provider shape."""
3683
4176
  if not isinstance(providers_dict, dict):
@@ -3807,15 +4300,46 @@ def get_custom_provider_context_length(
3807
4300
  return None
3808
4301
 
3809
4302
 
4303
+ def _coerce_config_version(value: Any) -> int:
4304
+ """Return a safe integer config version, treating invalid values as legacy."""
4305
+ if isinstance(value, bool):
4306
+ return 0
4307
+ try:
4308
+ version = int(value)
4309
+ except (TypeError, ValueError):
4310
+ return 0
4311
+ return max(version, 0)
4312
+
4313
+
3810
4314
  def check_config_version() -> Tuple[int, int]:
3811
4315
  """
3812
- Check config version.
3813
-
4316
+ Check the raw on-disk config schema version.
4317
+
4318
+ ``load_config()`` deliberately starts from ``DEFAULT_CONFIG`` and deep-merges
4319
+ the user's file, which is correct for runtime reads but wrong for deciding
4320
+ whether the user's persisted schema has been migrated. A config file with no
4321
+ raw ``_config_version`` must remain visible as legacy instead of inheriting
4322
+ the latest default version in memory.
4323
+
3814
4324
  Returns (current_version, latest_version).
3815
4325
  """
3816
- config = load_config()
3817
- current = config.get("_config_version", 0)
3818
- latest = DEFAULT_CONFIG.get("_config_version", 1)
4326
+ latest = _coerce_config_version(DEFAULT_CONFIG.get("_config_version", 1)) or 1
4327
+ config_path = get_config_path()
4328
+ if not config_path.exists():
4329
+ return latest, latest
4330
+
4331
+ try:
4332
+ with open(config_path, encoding="utf-8") as f:
4333
+ config = yaml.safe_load(f) or {}
4334
+ except Exception as e:
4335
+ # Invalid YAML needs a parse warning, not an automatic schema rewrite
4336
+ # that could replace the user's broken file with defaults.
4337
+ _warn_config_parse_failure(config_path, e)
4338
+ return latest, latest
4339
+
4340
+ if not isinstance(config, dict):
4341
+ config = {}
4342
+ current = _coerce_config_version(config.get("_config_version"))
3819
4343
  return current, latest
3820
4344
 
3821
4345
 
@@ -3829,7 +4353,7 @@ _KNOWN_ROOT_KEYS = {
3829
4353
  "fallback_providers", "credential_pool_strategies", "toolsets",
3830
4354
  "agent", "terminal", "display", "compression", "delegation",
3831
4355
  "auxiliary", "custom_providers", "context", "memory", "gateway",
3832
- "sessions", "streaming",
4356
+ "sessions", "streaming", "updates", "mcp_servers",
3833
4357
  }
3834
4358
 
3835
4359
  # Valid fields inside a custom_providers list entry
@@ -4153,8 +4677,7 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A
4153
4677
  if not isinstance(entry, dict):
4154
4678
  continue
4155
4679
  old_name = entry.get("name", "")
4156
- old_url = entry.get("base_url", "") or entry.get("url", "") or ""
4157
- old_key = entry.get("api_key", "")
4680
+ old_url = entry.get("base_url", "") or entry.get("url", "") or entry.get("api", "") or ""
4158
4681
  if not old_url:
4159
4682
  continue # skip entries with no URL
4160
4683
 
@@ -4174,20 +4697,22 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A
4174
4697
  key = f"endpoint-{migrated_count}"
4175
4698
 
4176
4699
  # Don't overwrite existing entries
4177
- if key in providers_dict:
4178
- key = f"{key}-{migrated_count}"
4179
-
4180
- new_entry = {"api": old_url}
4181
- if old_name:
4182
- new_entry["name"] = old_name
4183
- if old_key and old_key not in {"no-key", "no-key-required", ""}:
4184
- new_entry["api_key"] = old_key
4185
-
4186
- # Carry over model and api_mode if present
4187
- if entry.get("model"):
4188
- new_entry["default_model"] = entry["model"]
4189
- if entry.get("api_mode"):
4190
- new_entry["transport"] = entry["api_mode"]
4700
+ base_key = key
4701
+ suffix = migrated_count
4702
+ while key in providers_dict:
4703
+ key = f"{base_key}-{suffix}"
4704
+ suffix += 1
4705
+
4706
+ new_entry = _custom_provider_entry_to_provider_config(
4707
+ entry,
4708
+ provider_key=key,
4709
+ )
4710
+ if new_entry is None:
4711
+ continue
4712
+ if not old_name:
4713
+ new_entry.pop("name", None)
4714
+ if new_entry.get("api_key") in {"no-key", "no-key-required", ""}:
4715
+ new_entry.pop("api_key", None)
4191
4716
 
4192
4717
  providers_dict[key] = new_entry
4193
4718
  migrated_count += 1
@@ -4508,6 +5033,89 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A
4508
5033
  if not quiet:
4509
5034
  print(" ✓ Lowered model_catalog.ttl_hours to 1 (hourly picker refresh)")
4510
5035
 
5036
+ # ── Version 28 → 29: rename memory/skills write_mode → write_approval ──
5037
+ # The tri-state write_mode (on|off|approve) was replaced by a clear boolean
5038
+ # write_approval (default false = gate off, writes flow freely; true =
5039
+ # require approval). Only an explicit "approve" carried gating intent, so
5040
+ # it maps to true; everything else (on/off/unset) → false. The old
5041
+ # "off = block all writes" mode is dropped — memory_enabled: false disables
5042
+ # memory entirely. Only rewrite a key the user actually persisted; never
5043
+ # invent one.
5044
+ if current_ver < 29:
5045
+ config = read_raw_config()
5046
+ touched = False
5047
+ for subsystem in ("memory", "skills"):
5048
+ sub = config.get(subsystem)
5049
+ if not isinstance(sub, dict) or "write_mode" not in sub:
5050
+ continue
5051
+ old = sub.pop("write_mode")
5052
+ old_norm = old.strip().lower() if isinstance(old, str) else old
5053
+ sub["write_approval"] = (old_norm == "approve")
5054
+ config[subsystem] = sub
5055
+ touched = True
5056
+ results["config_added"].append(
5057
+ f"{subsystem}.write_mode → write_approval={sub['write_approval']}"
5058
+ )
5059
+ if touched:
5060
+ save_config(config)
5061
+ if not quiet:
5062
+ print(" ✓ Renamed write_mode → write_approval (boolean gate)")
5063
+
5064
+ # ── Version 29 → 30: seed curator.consolidate (default false) ──
5065
+ # Consolidation (the LLM umbrella-building fork) is now an opt-in toggle,
5066
+ # OFF by default. The deterministic inactivity prune still runs whenever
5067
+ # the curator is enabled; only the opinionated, aux-model-cost LLM pass is
5068
+ # gated. The runtime deep-merge already supplies the default, but we seed
5069
+ # the key so it's visible/editable in config.yaml. Existing installs that
5070
+ # WANT the old always-consolidate behavior must set it to true explicitly.
5071
+ # Only add the key when a curator section exists and lacks it — never
5072
+ # clobber a value the user already set.
5073
+ if current_ver < 30:
5074
+ config = read_raw_config()
5075
+ raw_curator = config.get("curator")
5076
+ if isinstance(raw_curator, dict) and "consolidate" not in raw_curator:
5077
+ raw_curator["consolidate"] = False
5078
+ config["curator"] = raw_curator
5079
+ save_config(config)
5080
+ results["config_added"].append("curator.consolidate=false")
5081
+ if not quiet:
5082
+ print(
5083
+ " ✓ Seeded curator.consolidate: false "
5084
+ "(LLM consolidation is now opt-in; pruning stays on)"
5085
+ )
5086
+
5087
+ # ── Post-migration: disable exfiltration-shaped MCP stdio entries ──
5088
+ # Users can hand-edit mcp_servers, and older installs may already contain a
5089
+ # malicious entry. Preserve the stanza for auditability but mark it
5090
+ # disabled so the next startup will not spawn it. (#45620)
5091
+ config = read_raw_config()
5092
+ raw_mcp_servers = config.get("mcp_servers")
5093
+ if isinstance(raw_mcp_servers, dict):
5094
+ try:
5095
+ from hermes_cli.mcp_security import validate_mcp_server_entry as _validate_mcp_server_entry
5096
+ except Exception:
5097
+ _validate_mcp_server_entry = None
5098
+ if _validate_mcp_server_entry:
5099
+ mcp_touched = False
5100
+ for server_name, entry in raw_mcp_servers.items():
5101
+ if not isinstance(entry, dict):
5102
+ continue
5103
+ issues = _validate_mcp_server_entry(server_name, entry)
5104
+ if not issues:
5105
+ continue
5106
+ entry["enabled"] = False
5107
+ mcp_touched = True
5108
+ results["warnings"].append(
5109
+ f"Disabled suspicious MCP server '{server_name}'"
5110
+ )
5111
+ if not quiet:
5112
+ for issue in issues:
5113
+ print(f" ⚠ {issue}")
5114
+ print(f" ⚠ Disabled MCP server '{server_name}' pending review")
5115
+ if mcp_touched:
5116
+ config["mcp_servers"] = raw_mcp_servers
5117
+ save_config(config)
5118
+
4511
5119
  if current_ver < latest_ver and not quiet:
4512
5120
  print(f"Config version: {current_ver} → {latest_ver}")
4513
5121
 
@@ -4680,6 +5288,29 @@ def _deep_merge(base: dict, override: dict) -> dict:
4680
5288
  return result
4681
5289
 
4682
5290
 
5291
+ def _strip_dotted_keys(cfg: dict, dotted_keys: set) -> Tuple[dict, set]:
5292
+ """Remove the given dotted leaf keys from a nested config dict.
5293
+
5294
+ Returns ``(pruned_cfg, set_of_stripped_keys_that_were_present)``. Used by
5295
+ ``save_config`` to drop managed-scope leaves before persisting, so a bulk
5296
+ write never writes a user value that would lose to the managed layer on the
5297
+ next load. Only keys actually present in ``cfg`` are reported as stripped.
5298
+ """
5299
+ stripped: set = set()
5300
+ for dotted in dotted_keys:
5301
+ parts = dotted.split(".")
5302
+ node = cfg
5303
+ for p in parts[:-1]:
5304
+ if not isinstance(node, dict) or p not in node:
5305
+ node = None
5306
+ break
5307
+ node = node[p]
5308
+ if isinstance(node, dict) and parts[-1] in node:
5309
+ del node[parts[-1]]
5310
+ stripped.add(dotted)
5311
+ return cfg, stripped
5312
+
5313
+
4683
5314
  def _expand_env_vars(obj):
4684
5315
  """Recursively expand ``${VAR}`` references in config values.
4685
5316
 
@@ -4950,6 +5581,94 @@ def load_config_readonly() -> Dict[str, Any]:
4950
5581
  return _load_config_impl(want_deepcopy=False)
4951
5582
 
4952
5583
 
5584
+ TERMINAL_CONFIG_ENV_MAP = {
5585
+ "backend": "TERMINAL_ENV",
5586
+ "modal_mode": "TERMINAL_MODAL_MODE",
5587
+ "cwd": "TERMINAL_CWD",
5588
+ "timeout": "TERMINAL_TIMEOUT",
5589
+ "lifetime_seconds": "TERMINAL_LIFETIME_SECONDS",
5590
+ "docker_image": "TERMINAL_DOCKER_IMAGE",
5591
+ "docker_forward_env": "TERMINAL_DOCKER_FORWARD_ENV",
5592
+ "singularity_image": "TERMINAL_SINGULARITY_IMAGE",
5593
+ "modal_image": "TERMINAL_MODAL_IMAGE",
5594
+ "daytona_image": "TERMINAL_DAYTONA_IMAGE",
5595
+ "ssh_host": "TERMINAL_SSH_HOST",
5596
+ "ssh_user": "TERMINAL_SSH_USER",
5597
+ "ssh_port": "TERMINAL_SSH_PORT",
5598
+ "ssh_key": "TERMINAL_SSH_KEY",
5599
+ "container_cpu": "TERMINAL_CONTAINER_CPU",
5600
+ "container_memory": "TERMINAL_CONTAINER_MEMORY",
5601
+ "container_disk": "TERMINAL_CONTAINER_DISK",
5602
+ "container_persistent": "TERMINAL_CONTAINER_PERSISTENT",
5603
+ "docker_volumes": "TERMINAL_DOCKER_VOLUMES",
5604
+ "docker_env": "TERMINAL_DOCKER_ENV",
5605
+ "docker_mount_cwd_to_workspace": "TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE",
5606
+ "docker_extra_args": "TERMINAL_DOCKER_EXTRA_ARGS",
5607
+ "docker_run_as_host_user": "TERMINAL_DOCKER_RUN_AS_HOST_USER",
5608
+ "docker_persist_across_processes": "TERMINAL_DOCKER_PERSIST_ACROSS_PROCESSES",
5609
+ "docker_orphan_reaper": "TERMINAL_DOCKER_ORPHAN_REAPER",
5610
+ "sandbox_dir": "TERMINAL_SANDBOX_DIR",
5611
+ "persistent_shell": "TERMINAL_PERSISTENT_SHELL",
5612
+ }
5613
+
5614
+
5615
+ def _terminal_env_value(value: Any) -> str:
5616
+ if isinstance(value, (list, dict)):
5617
+ return json.dumps(value)
5618
+ return str(value)
5619
+
5620
+
5621
+ def terminal_config_env_var_for_key(key: str) -> Optional[str]:
5622
+ """Return the env var mirrored by a ``terminal.*`` config key."""
5623
+ prefix = "terminal."
5624
+ if not key.startswith(prefix):
5625
+ return None
5626
+ return TERMINAL_CONFIG_ENV_MAP.get(key[len(prefix):])
5627
+
5628
+
5629
+ def apply_terminal_config_to_env(
5630
+ *,
5631
+ env: Optional[Dict[str, str]] = None,
5632
+ config: Optional[Dict[str, Any]] = None,
5633
+ override: Optional[bool] = None,
5634
+ ) -> Dict[str, str]:
5635
+ """Bridge ``terminal.*`` config into the env vars terminal tools read.
5636
+
5637
+ ``tools.terminal_tool`` is intentionally environment-driven because it also
5638
+ runs in child processes (TUI, dashboard PTY, gateway workers). This helper
5639
+ gives those child-process launch paths the same config bridge as classic
5640
+ CLI without importing ``cli.py`` and paying for its startup side effects.
5641
+
5642
+ When the user config contains a ``terminal`` section, config.yaml is
5643
+ authoritative and overrides existing env values. Otherwise defaults only
5644
+ backfill missing env vars so exported/.env values keep working.
5645
+ """
5646
+ target = os.environ if env is None else env
5647
+
5648
+ raw_config = read_raw_config()
5649
+ file_has_terminal_config = isinstance(raw_config.get("terminal"), dict)
5650
+ should_override = file_has_terminal_config if override is None else override
5651
+
5652
+ cfg = config if config is not None else load_config_readonly()
5653
+ terminal_cfg = cfg.get("terminal", {}) if isinstance(cfg, dict) else {}
5654
+ if not isinstance(terminal_cfg, dict):
5655
+ return target
5656
+
5657
+ for cfg_key, env_var in TERMINAL_CONFIG_ENV_MAP.items():
5658
+ if cfg_key not in terminal_cfg:
5659
+ continue
5660
+ value = terminal_cfg[cfg_key]
5661
+ if cfg_key == "cwd":
5662
+ raw_cwd = str(value or "").strip()
5663
+ if raw_cwd in {".", "auto", "cwd"}:
5664
+ continue
5665
+ if isinstance(value, str):
5666
+ value = os.path.expanduser(value)
5667
+ if should_override or env_var not in target:
5668
+ target[env_var] = _terminal_env_value(value)
5669
+ return target
5670
+
5671
+
4953
5672
  def _load_config_impl(*, want_deepcopy: bool) -> Dict[str, Any]:
4954
5673
  with _CONFIG_LOCK:
4955
5674
  ensure_hermes_home()
@@ -4958,17 +5677,44 @@ def _load_config_impl(*, want_deepcopy: bool) -> Dict[str, Any]:
4958
5677
 
4959
5678
  try:
4960
5679
  st = config_path.stat()
4961
- cache_key: Optional[Tuple[int, int]] = (st.st_mtime_ns, st.st_size)
5680
+ user_sig: Optional[Tuple[int, int]] = (st.st_mtime_ns, st.st_size)
4962
5681
  except FileNotFoundError:
4963
- cache_key = None
5682
+ user_sig = None
5683
+
5684
+ # Managed scope: fold the managed config file's (mtime, size) into the
5685
+ # cache signature so editing /etc/hermes/config.yaml invalidates the
5686
+ # cached merged result. (0, 0) means "no managed config file".
5687
+ from hermes_cli import managed_scope
5688
+
5689
+ managed_dir = managed_scope.get_managed_dir()
5690
+ managed_cfg_path = (managed_dir / "config.yaml") if managed_dir else None
5691
+ try:
5692
+ mst = managed_cfg_path.stat() if managed_cfg_path else None
5693
+ managed_sig = (mst.st_mtime_ns, mst.st_size) if mst else (0, 0)
5694
+ except OSError:
5695
+ managed_sig = (0, 0)
5696
+
5697
+ # Combined cache signature: user file + managed file. None only when the
5698
+ # user config is absent AND no managed file exists (nothing to cache on).
5699
+ if user_sig is not None:
5700
+ cache_sig: Optional[Tuple[int, int, int, int]] = (
5701
+ user_sig[0],
5702
+ user_sig[1],
5703
+ managed_sig[0],
5704
+ managed_sig[1],
5705
+ )
5706
+ elif managed_sig != (0, 0):
5707
+ cache_sig = (0, 0, managed_sig[0], managed_sig[1])
5708
+ else:
5709
+ cache_sig = None
4964
5710
 
4965
5711
  cached = _LOAD_CONFIG_CACHE.get(path_key)
4966
- if cached is not None and cache_key is not None and cached[:2] == cache_key:
4967
- return copy.deepcopy(cached[2]) if want_deepcopy else cached[2]
5712
+ if cached is not None and cache_sig is not None and cached[:4] == cache_sig:
5713
+ return copy.deepcopy(cached[4]) if want_deepcopy else cached[4]
4968
5714
 
4969
5715
  config = copy.deepcopy(DEFAULT_CONFIG)
4970
5716
 
4971
- if cache_key is not None:
5717
+ if user_sig is not None:
4972
5718
  try:
4973
5719
  with open(config_path, encoding="utf-8") as f:
4974
5720
  user_config = yaml.safe_load(f) or {}
@@ -4986,14 +5732,24 @@ def _load_config_impl(*, want_deepcopy: bool) -> Dict[str, Any]:
4986
5732
 
4987
5733
  normalized = _normalize_root_model_keys(_normalize_max_turns_config(config))
4988
5734
  expanded = _expand_env_vars(normalized)
5735
+ # Managed scope wins at the leaf. Applied AFTER user expansion so a user
5736
+ # ${VAR} cannot shadow a managed literal: managed values are expanded only
5737
+ # against the process environment, never against user-config-defined refs.
5738
+ # This deliberately inverts the usual env-over-config precedence for the
5739
+ # keys the managed layer pins — see docs/design/managed-scope.md §4.1.
5740
+ managed_config = managed_scope.load_managed_config()
5741
+ if managed_config:
5742
+ managed_expanded = _expand_env_vars(managed_config)
5743
+ expanded = _deep_merge(expanded, managed_expanded)
4989
5744
  _LAST_EXPANDED_CONFIG_BY_PATH[path_key] = copy.deepcopy(expanded)
4990
- if cache_key is not None:
5745
+ if cache_sig is not None:
4991
5746
  # Cache stores a separate deepcopy so subsequent ``load_config()``
4992
5747
  # (deepcopy=True) callers can mutate freely without affecting the
4993
5748
  # cached value, and ``load_config_readonly()`` (deepcopy=False)
4994
- # callers all see the same stable cached object.
5749
+ # callers all see the same stable cached object. The cached tuple is
5750
+ # (user_mtime, user_size, managed_mtime, managed_size, value).
4995
5751
  cached_copy = copy.deepcopy(expanded)
4996
- _LOAD_CONFIG_CACHE[path_key] = (cache_key[0], cache_key[1], cached_copy)
5752
+ _LOAD_CONFIG_CACHE[path_key] = (*cache_sig, cached_copy)
4997
5753
  # On the readonly path return the same cached object subsequent
4998
5754
  # calls will see — keeps "two readonly calls return the same
4999
5755
  # object" invariant that callers may rely on for identity checks.
@@ -5090,6 +5846,22 @@ def save_config(config: Dict[str, Any]):
5090
5846
  if is_managed():
5091
5847
  managed_error("save configuration")
5092
5848
  return
5849
+ # Managed scope: strip any leaf the managed layer pins, so a bulk write
5850
+ # (wizard / programmatic save) never persists a user value that would
5851
+ # silently lose to managed on the next load. Single-key `config set`
5852
+ # hard-rejects (see set_config_value); this is the mechanical safety net
5853
+ # for bulk writes so the unmanaged remainder still lands.
5854
+ from hermes_cli import managed_scope
5855
+
5856
+ managed_keys = managed_scope.managed_config_keys()
5857
+ if managed_keys:
5858
+ config, _stripped = _strip_dotted_keys(copy.deepcopy(config), managed_keys)
5859
+ if _stripped:
5860
+ print(
5861
+ f"Note: {len(_stripped)} managed setting(s) were not saved "
5862
+ f"(managed by your administrator): {', '.join(sorted(_stripped))}",
5863
+ file=sys.stderr,
5864
+ )
5093
5865
  from utils import atomic_yaml_write
5094
5866
 
5095
5867
  ensure_hermes_home()
@@ -5356,6 +6128,19 @@ def save_env_value(key: str, value: str):
5356
6128
  if is_managed():
5357
6129
  managed_error(f"set {key}")
5358
6130
  return
6131
+ # Managed scope guard: a managed env key can't be set by the user — the
6132
+ # managed .env wins at load anyway. Distinct from is_managed() above.
6133
+ from hermes_cli import managed_scope
6134
+
6135
+ if managed_scope.is_env_managed(key):
6136
+ managed_dir = managed_scope.get_managed_dir()
6137
+ src = (managed_dir / ".env") if managed_dir else "the managed scope"
6138
+ print(
6139
+ f"Cannot set {key}: it is managed by your administrator ({src}) "
6140
+ f"and cannot be changed.",
6141
+ file=sys.stderr,
6142
+ )
6143
+ return
5359
6144
  if not _ENV_VAR_NAME_RE.match(key):
5360
6145
  raise ValueError(f"Invalid environment variable name: {key!r}")
5361
6146
  _reject_denylisted_env_var(key)
@@ -5405,19 +6190,21 @@ def save_env_value(key: str, value: str):
5405
6190
  f.flush()
5406
6191
  os.fsync(f.fileno())
5407
6192
  atomic_replace(tmp_path, env_path)
5408
- # Restore original permissions before _secure_file may tighten them.
6193
+ # Preserve the original file mode (e.g. 0640 for Docker volume mounts)
6194
+ # instead of letting _secure_file unconditionally tighten to 0600.
5409
6195
  if original_mode is not None:
5410
6196
  try:
5411
6197
  os.chmod(env_path, original_mode)
5412
6198
  except OSError:
5413
6199
  pass
6200
+ else:
6201
+ _secure_file(env_path)
5414
6202
  except BaseException:
5415
6203
  try:
5416
6204
  os.unlink(tmp_path)
5417
6205
  except OSError:
5418
6206
  pass
5419
6207
  raise
5420
- _secure_file(env_path)
5421
6208
 
5422
6209
  os.environ[key] = value
5423
6210
  invalidate_env_cache()
@@ -5431,6 +6218,18 @@ def remove_env_value(key: str) -> bool:
5431
6218
  if is_managed():
5432
6219
  managed_error(f"remove {key}")
5433
6220
  return False
6221
+ # Managed scope guard: a managed env key can't be removed by the user.
6222
+ from hermes_cli import managed_scope
6223
+
6224
+ if managed_scope.is_env_managed(key):
6225
+ managed_dir = managed_scope.get_managed_dir()
6226
+ src = (managed_dir / ".env") if managed_dir else "the managed scope"
6227
+ print(
6228
+ f"Cannot remove {key}: it is managed by your administrator ({src}) "
6229
+ f"and cannot be changed.",
6230
+ file=sys.stderr,
6231
+ )
6232
+ return False
5434
6233
  if not _ENV_VAR_NAME_RE.match(key):
5435
6234
  raise ValueError(f"Invalid environment variable name: {key!r}")
5436
6235
  env_path = get_env_path()
@@ -5462,18 +6261,22 @@ def remove_env_value(key: str) -> bool:
5462
6261
  f.flush()
5463
6262
  os.fsync(f.fileno())
5464
6263
  atomic_replace(tmp_path, env_path)
6264
+ # Preserve the original file mode (e.g. 0640 for Docker volume
6265
+ # mounts) instead of letting _secure_file unconditionally tighten
6266
+ # to 0600. Mirrors save_env_value().
5465
6267
  if original_mode is not None:
5466
6268
  try:
5467
6269
  os.chmod(env_path, original_mode)
5468
6270
  except OSError:
5469
6271
  pass
6272
+ else:
6273
+ _secure_file(env_path)
5470
6274
  except BaseException:
5471
6275
  try:
5472
6276
  os.unlink(tmp_path)
5473
6277
  except OSError:
5474
6278
  pass
5475
6279
  raise
5476
- _secure_file(env_path)
5477
6280
 
5478
6281
  os.environ.pop(key, None)
5479
6282
  invalidate_env_cache()
@@ -5561,12 +6364,38 @@ def redact_key(key: str) -> str:
5561
6364
  def show_config():
5562
6365
  """Display current configuration."""
5563
6366
  config = load_config()
5564
-
6367
+
5565
6368
  print()
5566
6369
  print(color("┌─────────────────────────────────────────────────────────┐", Colors.CYAN))
5567
6370
  print(color("│ ⚕ Hermes Configuration │", Colors.CYAN))
5568
6371
  print(color("└─────────────────────────────────────────────────────────┘", Colors.CYAN))
5569
-
6372
+
6373
+ # Managed scope: surface that some settings are administrator-pinned so the
6374
+ # user understands why their config.yaml value may not be the effective one.
6375
+ from hermes_cli import managed_scope
6376
+
6377
+ _managed_keys = managed_scope.managed_config_keys()
6378
+ _managed_env = managed_scope.load_managed_env()
6379
+ if _managed_keys or _managed_env:
6380
+ _managed_dir = managed_scope.get_managed_dir()
6381
+ print()
6382
+ print(color(
6383
+ f" ⚷ Some settings are managed by your administrator ({_managed_dir}) "
6384
+ f"and cannot be changed",
6385
+ Colors.YELLOW,
6386
+ Colors.BOLD,
6387
+ ))
6388
+ if _managed_keys:
6389
+ print(color(
6390
+ f" Managed config keys: {', '.join(sorted(_managed_keys))}",
6391
+ Colors.YELLOW,
6392
+ ))
6393
+ if _managed_env:
6394
+ print(color(
6395
+ f" Managed env keys: {', '.join(sorted(_managed_env))}",
6396
+ Colors.YELLOW,
6397
+ ))
6398
+
5570
6399
  # Paths
5571
6400
  print()
5572
6401
  print(color("◆ Paths", Colors.CYAN, Colors.BOLD))
@@ -5601,7 +6430,21 @@ def show_config():
5601
6430
  print()
5602
6431
  print(color("◆ Model", Colors.CYAN, Colors.BOLD))
5603
6432
  print(f" Model: {config.get('model', 'not set')}")
5604
- print(f" Max turns: {config.get('agent', {}).get('max_turns', DEFAULT_CONFIG['agent']['max_turns'])}")
6433
+ _cfg_max_turns = config.get('agent', {}).get('max_turns', DEFAULT_CONFIG['agent']['max_turns'])
6434
+ print(f" Max turns: {_cfg_max_turns}")
6435
+ # Warn on stale HERMES_MAX_ITERATIONS ghost in .env that disagrees with
6436
+ # config.yaml (issue #17534). Read the .env FILE directly so we catch the
6437
+ # ghost even when the gateway bridge already overrode os.environ.
6438
+ try:
6439
+ _env_ghost = load_env().get("HERMES_MAX_ITERATIONS")
6440
+ if _env_ghost is not None and str(_env_ghost).strip() != str(_cfg_max_turns).strip():
6441
+ print(color(
6442
+ f" ⚠ .env has stale HERMES_MAX_ITERATIONS={_env_ghost} "
6443
+ f"(run 'hermes doctor --fix' to remove)",
6444
+ Colors.YELLOW,
6445
+ ))
6446
+ except Exception:
6447
+ pass
5605
6448
 
5606
6449
  # Display
5607
6450
  print()
@@ -5770,6 +6613,22 @@ def set_config_value(key: str, value: str):
5770
6613
  if is_managed():
5771
6614
  managed_error("set configuration values")
5772
6615
  return
6616
+ # Managed scope guard (D2): a key pinned by the managed layer cannot be set by
6617
+ # the user — the next load would override it anyway. Hard-reject and name the
6618
+ # source. Distinct from is_managed() above (the package-manager write-lock).
6619
+ # Env-shaped keys (API keys / tokens) route to save_env_value below, which has
6620
+ # its own managed-env-key guard; this catches the config.yaml keys.
6621
+ from hermes_cli import managed_scope
6622
+
6623
+ if managed_scope.is_key_managed(key):
6624
+ managed_dir = managed_scope.get_managed_dir()
6625
+ src = (managed_dir / "config.yaml") if managed_dir else "the managed scope"
6626
+ print(
6627
+ f"Cannot set '{key}': it is managed by your administrator ({src}) "
6628
+ f"and cannot be changed. Contact your administrator to modify it.",
6629
+ file=sys.stderr,
6630
+ )
6631
+ sys.exit(1)
5773
6632
  # Check if it's an API key (goes to .env)
5774
6633
  api_keys = [
5775
6634
  'OPENROUTER_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'VOICE_TOOLS_OPENAI_KEY',
@@ -5824,31 +6683,9 @@ def set_config_value(key: str, value: str):
5824
6683
 
5825
6684
  # Keep .env in sync for keys that terminal_tool reads directly from env vars.
5826
6685
  # config.yaml is authoritative, but terminal_tool only reads TERMINAL_ENV etc.
5827
- _config_to_env_sync = {
5828
- "terminal.backend": "TERMINAL_ENV",
5829
- "terminal.modal_mode": "TERMINAL_MODAL_MODE",
5830
- "terminal.docker_image": "TERMINAL_DOCKER_IMAGE",
5831
- "terminal.singularity_image": "TERMINAL_SINGULARITY_IMAGE",
5832
- "terminal.modal_image": "TERMINAL_MODAL_IMAGE",
5833
- "terminal.daytona_image": "TERMINAL_DAYTONA_IMAGE",
5834
- "terminal.docker_mount_cwd_to_workspace": "TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE",
5835
- "terminal.docker_run_as_host_user": "TERMINAL_DOCKER_RUN_AS_HOST_USER",
5836
- "terminal.docker_persist_across_processes": "TERMINAL_DOCKER_PERSIST_ACROSS_PROCESSES",
5837
- "terminal.docker_orphan_reaper": "TERMINAL_DOCKER_ORPHAN_REAPER",
5838
- "terminal.docker_env": "TERMINAL_DOCKER_ENV",
5839
- # terminal.cwd intentionally excluded — CLI resolves at runtime,
5840
- # gateway bridges it in gateway/run.py. Persisting to .env causes
5841
- # stale values to poison child processes.
5842
- "terminal.timeout": "TERMINAL_TIMEOUT",
5843
- "terminal.sandbox_dir": "TERMINAL_SANDBOX_DIR",
5844
- "terminal.persistent_shell": "TERMINAL_PERSISTENT_SHELL",
5845
- "terminal.container_cpu": "TERMINAL_CONTAINER_CPU",
5846
- "terminal.container_memory": "TERMINAL_CONTAINER_MEMORY",
5847
- "terminal.container_disk": "TERMINAL_CONTAINER_DISK",
5848
- "terminal.container_persistent": "TERMINAL_CONTAINER_PERSISTENT",
5849
- }
5850
- if key in _config_to_env_sync:
5851
- save_env_value(_config_to_env_sync[key], str(value))
6686
+ env_var = terminal_config_env_var_for_key(key)
6687
+ if env_var and key != "terminal.cwd":
6688
+ save_env_value(env_var, _terminal_env_value(value))
5852
6689
 
5853
6690
  print(f"✓ Set {key} = {value} in {config_path}")
5854
6691