@clawpump/claw-agent 0.1.5 → 0.1.6

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 +6188 -975
  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 +3 -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
@@ -0,0 +1,1146 @@
1
+ """Kanban board watcher methods for GatewayRunner.
2
+
3
+ Extracted verbatim from ``gateway/run.py`` (god-file decomposition Phase 3).
4
+ These are the background-loop methods that subscribe to kanban boards, deliver
5
+ notifications/artifacts, and drive the multi-agent dispatcher. They use only
6
+ ``self`` state, so they live on a mixin that ``GatewayRunner`` inherits — the
7
+ ``self._kanban_*`` call sites resolve identically via the MRO, making this a
8
+ behavior-neutral move that lifts ~1,000 LOC out of run.py.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import logging
15
+ import os
16
+ import sqlite3
17
+ import time
18
+ from pathlib import Path
19
+ from typing import Any, Optional
20
+
21
+ # Match the logger run.py uses (logging.getLogger(__name__) where __name__ ==
22
+ # "gateway.run") so extracted log records keep their original logger name.
23
+ logger = logging.getLogger("gateway.run")
24
+
25
+
26
+ def _acquire_singleton_lock(lock_path) -> "tuple[Optional[object], str]":
27
+ """Take an exclusive, non-blocking advisory lock for the sole dispatcher.
28
+
29
+ Only one gateway process machine-wide may run the embedded kanban
30
+ dispatcher: concurrent dispatchers double the reclaim frequency (each
31
+ runs its own ``release_stale_claims`` → promote → dispatch loop), double
32
+ claim-attempt events in the event log, and — with ``wal_autocheckpoint=0`` —
33
+ concurrent manual WAL checkpoints can corrupt index pages. The
34
+ ``dispatch_in_gateway`` config flag is the primary control; this lock is the
35
+ backstop that survives config drift and same-profile restart races.
36
+
37
+ Delegates to :func:`gateway.status._try_acquire_file_lock` (``fcntl`` on
38
+ POSIX, ``msvcrt`` on Windows) so the guard is cross-platform.
39
+
40
+ Returns ``(handle, "held")`` on success — the caller keeps the file handle
41
+ for the process lifetime and **must** release it via
42
+ :func:`_release_singleton_lock` when done. ``(None, "contended")`` when
43
+ another process holds the lock (caller must NOT dispatch). ``(None,
44
+ "unavailable")`` when locking cannot be performed (non-POSIX filesystem
45
+ without flock, or the status.py helpers are unimportable) — caller falls
46
+ back to config-only control.
47
+ """
48
+ try:
49
+ from gateway.status import _try_acquire_file_lock # deferred; same package
50
+ except ImportError:
51
+ return None, "unavailable"
52
+ try:
53
+ Path(lock_path).parent.mkdir(parents=True, exist_ok=True)
54
+ handle = open(str(lock_path), "a+", encoding="utf-8")
55
+ except OSError:
56
+ return None, "unavailable"
57
+ if not _try_acquire_file_lock(handle):
58
+ handle.close()
59
+ return None, "contended"
60
+ return handle, "held"
61
+
62
+
63
+ def _release_singleton_lock(handle) -> None:
64
+ """Release a dispatcher singleton lock acquired via :func:`_acquire_singleton_lock`."""
65
+ if handle is None:
66
+ return
67
+ try:
68
+ from gateway.status import _release_file_lock
69
+ _release_file_lock(handle)
70
+ except Exception:
71
+ pass
72
+ try:
73
+ handle.close()
74
+ except Exception:
75
+ pass
76
+
77
+
78
+ class GatewayKanbanWatchersMixin:
79
+ """Kanban watcher / notifier / dispatcher loops for GatewayRunner."""
80
+
81
+ async def _kanban_notifier_watcher(self, interval: float = 5.0) -> None:
82
+ """Poll ``kanban_notify_subs`` and deliver terminal events to users.
83
+
84
+ For each subscription row, fetches ``task_events`` newer than the
85
+ stored cursor with kind in the terminal set (``completed``,
86
+ ``blocked``, ``gave_up``, ``crashed``, ``timed_out``). Sends one
87
+ message per new event to ``(platform, chat_id, thread_id)``,
88
+ then advances the cursor. When a task reaches a terminal state
89
+ (``completed`` / ``archived``), the subscription is removed.
90
+
91
+ Runs in the gateway event loop; all SQLite work is pushed to a
92
+ thread via ``asyncio.to_thread`` so the loop never blocks on the
93
+ WAL lock. Failures in one tick don't stop subsequent ticks.
94
+
95
+ **Multi-board:** iterates every board discovered on disk per
96
+ tick. Subscriptions live inside each board's own DB and cannot
97
+ cross boards, so delivery semantics are unchanged — this is
98
+ purely a fan-out of the single-DB poll.
99
+ """
100
+ # Gate: only the dispatch-owning gateway opens kanban DBs for notifier polling.
101
+ # Non-dispatch gateways have no subscriptions to deliver — all kanban state lives
102
+ # in the dispatch owner's per-board DBs. This prevents N-gateway -shm contention.
103
+ # TODO: gate per-board when per-board dispatcher_owner tracking lands.
104
+ try:
105
+ from hermes_cli.config import load_config as _load_config
106
+ except Exception:
107
+ logger.warning("kanban notifier: config loader unavailable; disabled")
108
+ return
109
+ env_override = os.environ.get("HERMES_KANBAN_DISPATCH_IN_GATEWAY", "").strip().lower()
110
+ if env_override in {"0", "false", "no", "off"}:
111
+ logger.info("kanban notifier: disabled via HERMES_KANBAN_DISPATCH_IN_GATEWAY env")
112
+ return
113
+ try:
114
+ cfg = _load_config()
115
+ except Exception as exc:
116
+ logger.warning("kanban notifier: cannot load config (%s); disabled", exc)
117
+ return
118
+ kanban_cfg = cfg.get("kanban", {}) if isinstance(cfg, dict) else {}
119
+ if not kanban_cfg.get("dispatch_in_gateway", True):
120
+ logger.info(
121
+ "kanban notifier: disabled via config kanban.dispatch_in_gateway=false"
122
+ )
123
+ return
124
+ from gateway.config import Platform as _Platform
125
+ try:
126
+ from hermes_cli import kanban_db as _kb
127
+ except Exception:
128
+ logger.warning("kanban notifier: kanban_db not importable; notifier disabled")
129
+ return
130
+
131
+ TERMINAL_KINDS = ("completed", "blocked", "gave_up", "crashed", "timed_out")
132
+ # Subscriptions are removed only when the task reaches a truly final
133
+ # status (done / archived). We used to also unsub on any terminal
134
+ # event kind (gave_up / crashed / timed_out / blocked), but that
135
+ # silently dropped the user out of the loop whenever the dispatcher
136
+ # respawned the task: a worker that crashes, gets reclaimed, runs
137
+ # again, and crashes a second time would only notify on the first
138
+ # crash because the subscription was deleted after the first event.
139
+ # Same shape as the reblock-after-unblock cycle that PR #22941
140
+ # fixed for `blocked`. Keeping the subscription alive until the
141
+ # task is genuinely done lets the cursor (advanced atomically by
142
+ # claim_unseen_events_for_sub) handle dedup, and any retry-loop
143
+ # event reaches the user.
144
+ # Per-subscription send-failure counter. Adapter.send raising
145
+ # means the chat is dead (deleted, bot kicked, etc.) — after N
146
+ # consecutive send failures the sub is dropped so we don't spin
147
+ # against a dead chat every 5 seconds forever.
148
+ MAX_SEND_FAILURES = 3
149
+ sub_fail_counts: dict[tuple, int] = getattr(
150
+ self, "_kanban_sub_fail_counts", {}
151
+ )
152
+ self._kanban_sub_fail_counts = sub_fail_counts
153
+ notifier_profile = getattr(self, "_kanban_notifier_profile", None)
154
+ if not notifier_profile:
155
+ notifier_profile = self._active_profile_name()
156
+ self._kanban_notifier_profile = notifier_profile
157
+
158
+ # Initial delay so the gateway can finish wiring adapters.
159
+ await asyncio.sleep(5)
160
+
161
+ while self._running:
162
+ try:
163
+ def _collect():
164
+ deliveries: list[dict] = []
165
+ active_platforms = {
166
+ getattr(platform, "value", str(platform)).lower()
167
+ for platform in self.adapters.keys()
168
+ }
169
+ if not active_platforms:
170
+ logger.debug("kanban notifier: no connected adapters; skipping tick")
171
+ return deliveries
172
+
173
+ # Enumerate every board on disk, but poll each resolved DB
174
+ # path once. Multiple slugs can point at the same DB when
175
+ # HERMES_KANBAN_DB pins the board path; without this guard
176
+ # one gateway could collect the same subscription/event
177
+ # more than once before advancing the cursor.
178
+ try:
179
+ boards = _kb.list_boards(include_archived=False)
180
+ except Exception:
181
+ boards = [_kb.read_board_metadata(_kb.DEFAULT_BOARD)]
182
+ seen_db_paths: set[str] = set()
183
+ for board_meta in boards:
184
+ slug = board_meta.get("slug") or _kb.DEFAULT_BOARD
185
+ db_path = board_meta.get("db_path")
186
+ try:
187
+ resolved_db_path = str(Path(db_path).expanduser().resolve()) if db_path else str(_kb.kanban_db_path(slug).resolve())
188
+ except Exception:
189
+ resolved_db_path = f"slug:{slug}"
190
+ if resolved_db_path in seen_db_paths:
191
+ logger.debug(
192
+ "kanban notifier: skipping duplicate board slug %s for DB %s",
193
+ slug, resolved_db_path,
194
+ )
195
+ continue
196
+ seen_db_paths.add(resolved_db_path)
197
+ try:
198
+ conn = _kb.connect(board=slug)
199
+ except Exception as exc:
200
+ logger.debug("kanban notifier: cannot open board %s: %s", slug, exc)
201
+ continue
202
+ try:
203
+ # `connect()` runs the schema + idempotent migration
204
+ # on first open per process, so an explicit
205
+ # `init_db()` here would be redundant. Worse:
206
+ # `init_db()` deliberately busts the per-process
207
+ # cache and re-runs the migration on a *second*
208
+ # connection, which races the first and used to
209
+ # log a benign but noisy `duplicate column name`
210
+ # traceback (and intermittent "database is locked"
211
+ # — issue #21378) on every gateway start against
212
+ # a legacy DB. `_add_column_if_missing` now
213
+ # tolerates that race, but we still skip the
214
+ # redundant call to avoid the wasted work.
215
+ subs = _kb.list_notify_subs(conn)
216
+ if not subs:
217
+ logger.debug("kanban notifier: board %s has no subscriptions", slug)
218
+ for sub in subs:
219
+ owner_profile = sub.get("notifier_profile") or None
220
+ if owner_profile and owner_profile != notifier_profile:
221
+ logger.debug(
222
+ "kanban notifier: subscription for %s owned by profile %s; current profile %s skipping",
223
+ sub.get("task_id"), owner_profile, notifier_profile,
224
+ )
225
+ continue
226
+ platform = (sub.get("platform") or "").lower()
227
+ if platform not in active_platforms:
228
+ logger.debug(
229
+ "kanban notifier: subscription for %s on %s skipped; adapter not connected",
230
+ sub.get("task_id"), platform or "<missing>",
231
+ )
232
+ continue
233
+ old_cursor, cursor, events = _kb.claim_unseen_events_for_sub(
234
+ conn,
235
+ task_id=sub["task_id"],
236
+ platform=sub["platform"],
237
+ chat_id=sub["chat_id"],
238
+ thread_id=sub.get("thread_id") or "",
239
+ kinds=TERMINAL_KINDS,
240
+ )
241
+ if not events:
242
+ continue
243
+ task = _kb.get_task(conn, sub["task_id"])
244
+ logger.debug(
245
+ "kanban notifier: claimed %d event(s) for %s on board %s cursor %s→%s",
246
+ len(events), sub["task_id"], slug, old_cursor, cursor,
247
+ )
248
+ deliveries.append({
249
+ "sub": sub,
250
+ "old_cursor": old_cursor,
251
+ "cursor": cursor,
252
+ "events": events,
253
+ "task": task,
254
+ "board": slug,
255
+ })
256
+ finally:
257
+ conn.close()
258
+ return deliveries
259
+
260
+ deliveries = await asyncio.to_thread(_collect)
261
+ for d in deliveries:
262
+ sub = d["sub"]
263
+ task = d["task"]
264
+ board_slug = d.get("board")
265
+ platform_str = (sub["platform"] or "").lower()
266
+ try:
267
+ plat = _Platform(platform_str)
268
+ except ValueError:
269
+ # Unknown platform string; skip and advance cursor so
270
+ # we don't replay forever.
271
+ await asyncio.to_thread(
272
+ self._kanban_advance, sub, d["cursor"], board_slug,
273
+ )
274
+ continue
275
+ adapter = self.adapters.get(plat)
276
+ if adapter is None:
277
+ logger.debug(
278
+ "kanban notifier: adapter %s disconnected before delivery for %s; rewinding claim",
279
+ platform_str, sub["task_id"],
280
+ )
281
+ await asyncio.to_thread(
282
+ self._kanban_rewind,
283
+ sub,
284
+ d["cursor"],
285
+ d.get("old_cursor", 0),
286
+ board_slug,
287
+ )
288
+ continue
289
+ title = (task.title if task else sub["task_id"])[:120]
290
+ for ev in d["events"]:
291
+ kind = ev.kind
292
+ # Identity prefix: attribute terminal pings to the
293
+ # worker that did the work. Makes fleets (where one
294
+ # chat subscribes to many tasks) legible at a glance.
295
+ who = (task.assignee if task and task.assignee else None)
296
+ tag = f"@{who} " if who else ""
297
+ if kind == "completed":
298
+ # Prefer the run's summary (the worker's
299
+ # intentional human-facing handoff, carried
300
+ # in the event payload), then fall back to
301
+ # task.result for legacy rows written before
302
+ # runs shipped.
303
+ handoff = ""
304
+ payload_summary = None
305
+ if ev.payload and ev.payload.get("summary"):
306
+ payload_summary = str(ev.payload["summary"])
307
+ if payload_summary:
308
+ lines = payload_summary.strip().splitlines()
309
+ h = lines[0][:200] if lines else payload_summary[:200]
310
+ handoff = f"\n{h}"
311
+ elif task and task.result:
312
+ lines = task.result.strip().splitlines()
313
+ r = lines[0][:160] if lines else task.result[:160]
314
+ handoff = f"\n{r}"
315
+ msg = (
316
+ f"✔ {tag}Kanban {sub['task_id']} done"
317
+ f" — {title}{handoff}"
318
+ )
319
+ elif kind == "blocked":
320
+ reason = ""
321
+ if ev.payload and ev.payload.get("reason"):
322
+ reason = f": {str(ev.payload['reason'])[:160]}"
323
+ msg = f"⏸ {tag}Kanban {sub['task_id']} blocked{reason}"
324
+ elif kind == "gave_up":
325
+ err = ""
326
+ if ev.payload and ev.payload.get("error"):
327
+ err = f"\n{str(ev.payload['error'])[:200]}"
328
+ msg = (
329
+ f"✖ {tag}Kanban {sub['task_id']} gave up "
330
+ f"after repeated spawn failures{err}"
331
+ )
332
+ elif kind == "crashed":
333
+ msg = (
334
+ f"✖ {tag}Kanban {sub['task_id']} worker crashed "
335
+ f"(pid gone); dispatcher will retry"
336
+ )
337
+ elif kind == "timed_out":
338
+ limit = 0
339
+ if ev.payload and ev.payload.get("limit_seconds"):
340
+ limit = int(ev.payload["limit_seconds"])
341
+ msg = (
342
+ f"⏱ {tag}Kanban {sub['task_id']} timed out "
343
+ f"(max_runtime={limit}s); will retry"
344
+ )
345
+ else:
346
+ continue
347
+ metadata: dict[str, Any] = {}
348
+ if sub.get("thread_id"):
349
+ metadata["thread_id"] = sub["thread_id"]
350
+ sub_key = (
351
+ sub["task_id"], sub["platform"],
352
+ sub["chat_id"], sub.get("thread_id") or "",
353
+ )
354
+ try:
355
+ await adapter.send(
356
+ sub["chat_id"], msg, metadata=metadata,
357
+ )
358
+ logger.debug(
359
+ "kanban notifier: delivered %s event for %s to %s/%s on board %s",
360
+ kind, sub["task_id"], platform_str, sub["chat_id"], board_slug,
361
+ )
362
+ # After delivering the text notification, surface
363
+ # any artifact paths the worker referenced in
364
+ # ``kanban_complete(summary=..., artifacts=[...])``
365
+ # (or the legacy ``result`` field) as native
366
+ # uploads. ``extract_local_files`` finds bare
367
+ # absolute paths in the summary;
368
+ # ``send_document`` / ``send_image_file`` uploads
369
+ # them. Only fires on the ``completed`` event so
370
+ # we never spam attachments on retries.
371
+ if kind == "completed":
372
+ try:
373
+ await self._deliver_kanban_artifacts(
374
+ adapter=adapter,
375
+ chat_id=sub["chat_id"],
376
+ metadata=metadata,
377
+ event_payload=getattr(ev, "payload", None),
378
+ task=task,
379
+ )
380
+ except Exception as art_exc:
381
+ logger.debug(
382
+ "kanban notifier: artifact delivery for %s failed: %s",
383
+ sub["task_id"], art_exc,
384
+ )
385
+ # Reset the failure counter on success.
386
+ sub_fail_counts.pop(sub_key, None)
387
+ except Exception as exc:
388
+ fails = sub_fail_counts.get(sub_key, 0) + 1
389
+ sub_fail_counts[sub_key] = fails
390
+ logger.warning(
391
+ "kanban notifier: send failed for %s on %s "
392
+ "(attempt %d/%d): %s",
393
+ sub["task_id"], platform_str, fails,
394
+ MAX_SEND_FAILURES, exc,
395
+ )
396
+ if fails >= MAX_SEND_FAILURES:
397
+ logger.warning(
398
+ "kanban notifier: dropping subscription "
399
+ "%s on %s after %d consecutive send failures",
400
+ sub["task_id"], platform_str, fails,
401
+ )
402
+ await asyncio.to_thread(self._kanban_unsub, sub, board_slug)
403
+ sub_fail_counts.pop(sub_key, None)
404
+ else:
405
+ await asyncio.to_thread(
406
+ self._kanban_rewind,
407
+ sub,
408
+ d["cursor"],
409
+ d.get("old_cursor", 0),
410
+ board_slug,
411
+ )
412
+ # Rewind the pre-send claim on transient failure so
413
+ # a later tick can retry. After too many failures,
414
+ # dropping the subscription is the terminal action.
415
+ break
416
+ else:
417
+ # All events delivered; advance cursor. The cursor
418
+ # is the dedup mechanism — it prevents re-delivery
419
+ # of the same event on subsequent ticks.
420
+ await asyncio.to_thread(
421
+ self._kanban_advance, sub, d["cursor"], board_slug,
422
+ )
423
+ # Unsubscribe only when the task has reached a truly
424
+ # final status (done / archived). For blocked /
425
+ # gave_up / crashed / timed_out the subscription is
426
+ # kept alive so the user gets notified again if the
427
+ # dispatcher respawns the task and it cycles into the
428
+ # same state. See the longer comment on TERMINAL_KINDS
429
+ # above for the failure mode this prevents.
430
+ task_terminal = task and task.status in {"done", "archived"}
431
+ if task_terminal:
432
+ await asyncio.to_thread(
433
+ self._kanban_unsub, sub, board_slug,
434
+ )
435
+ except Exception as exc:
436
+ logger.warning("kanban notifier tick failed: %s", exc)
437
+ # Sleep with cancellation checks.
438
+ for _ in range(int(max(1, interval))):
439
+ if not self._running:
440
+ return
441
+ await asyncio.sleep(1)
442
+
443
+ def _kanban_advance(
444
+ self, sub: dict, cursor: int, board: Optional[str] = None,
445
+ ) -> None:
446
+ """Sync helper: advance a subscription's cursor. Runs in to_thread.
447
+
448
+ ``board`` scopes the DB connection to the board that owns this
449
+ subscription. Unsub cursors in one board can't touch another's.
450
+ """
451
+ from hermes_cli import kanban_db as _kb
452
+ conn = _kb.connect(board=board)
453
+ try:
454
+ _kb.advance_notify_cursor(
455
+ conn,
456
+ task_id=sub["task_id"],
457
+ platform=sub["platform"],
458
+ chat_id=sub["chat_id"],
459
+ thread_id=sub.get("thread_id") or "",
460
+ new_cursor=cursor,
461
+ )
462
+ finally:
463
+ conn.close()
464
+
465
+ def _kanban_unsub(self, sub: dict, board: Optional[str] = None) -> None:
466
+ from hermes_cli import kanban_db as _kb
467
+ conn = _kb.connect(board=board)
468
+ try:
469
+ _kb.remove_notify_sub(
470
+ conn,
471
+ task_id=sub["task_id"],
472
+ platform=sub["platform"],
473
+ chat_id=sub["chat_id"],
474
+ thread_id=sub.get("thread_id") or "",
475
+ )
476
+ finally:
477
+ conn.close()
478
+
479
+ def _kanban_rewind(
480
+ self,
481
+ sub: dict,
482
+ claimed_cursor: int,
483
+ old_cursor: int,
484
+ board: Optional[str] = None,
485
+ ) -> None:
486
+ """Sync helper: undo a claimed notification cursor after send failure."""
487
+ from hermes_cli import kanban_db as _kb
488
+ conn = _kb.connect(board=board)
489
+ try:
490
+ _kb.rewind_notify_cursor(
491
+ conn,
492
+ task_id=sub["task_id"],
493
+ platform=sub["platform"],
494
+ chat_id=sub["chat_id"],
495
+ thread_id=sub.get("thread_id") or "",
496
+ claimed_cursor=claimed_cursor,
497
+ old_cursor=old_cursor,
498
+ )
499
+ finally:
500
+ conn.close()
501
+
502
+ async def _deliver_kanban_artifacts(
503
+ self,
504
+ *,
505
+ adapter,
506
+ chat_id: str,
507
+ metadata: dict,
508
+ event_payload: Optional[dict],
509
+ task,
510
+ ) -> None:
511
+ """Upload artifact files referenced by a completed kanban task.
512
+
513
+ Workers passing ``kanban_complete(artifacts=[...])`` ship absolute
514
+ file paths through the completion event so downstream humans get
515
+ the deliverable as a native upload instead of a path printed in
516
+ chat.
517
+
518
+ Sources scanned, in priority order:
519
+ 1. ``event_payload['artifacts']`` (explicit list — preferred)
520
+ 2. ``event_payload['summary']`` (truncated first line)
521
+ 3. ``task.result`` (legacy fallback)
522
+
523
+ Files are deduplicated, missing files are silently skipped (the
524
+ path may have been mentioned for reference only), and delivery
525
+ errors are logged but do not break the notifier loop.
526
+ """
527
+ from pathlib import Path as _Path
528
+
529
+ candidates: list[str] = []
530
+ seen: set[str] = set()
531
+
532
+ def _add(path: str) -> None:
533
+ if not path:
534
+ return
535
+ expanded = os.path.expanduser(path)
536
+ if expanded in seen:
537
+ return
538
+ if not os.path.isfile(expanded):
539
+ return
540
+ seen.add(expanded)
541
+ candidates.append(expanded)
542
+
543
+ # 1. Explicit artifacts list in payload.
544
+ if isinstance(event_payload, dict):
545
+ raw = event_payload.get("artifacts")
546
+ if isinstance(raw, (list, tuple)):
547
+ for item in raw:
548
+ if isinstance(item, str):
549
+ _add(item)
550
+
551
+ # 2. Paths embedded in the payload summary.
552
+ summary = event_payload.get("summary")
553
+ if isinstance(summary, str) and summary:
554
+ paths, _ = adapter.extract_local_files(summary)
555
+ for p in paths:
556
+ _add(p)
557
+
558
+ # 3. Legacy: paths embedded in task.result.
559
+ if task is not None and getattr(task, "result", None):
560
+ result_text = str(task.result)
561
+ paths, _ = adapter.extract_local_files(result_text)
562
+ for p in paths:
563
+ _add(p)
564
+
565
+ if not candidates:
566
+ return
567
+
568
+ from gateway.platforms.base import BasePlatformAdapter
569
+ candidates = BasePlatformAdapter.filter_local_delivery_paths(candidates)
570
+ if not candidates:
571
+ return
572
+
573
+ _IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".webp"}
574
+ _VIDEO_EXTS = {".mp4", ".mov", ".avi", ".mkv", ".webm", ".3gp"}
575
+
576
+ from urllib.parse import quote as _quote
577
+
578
+ # Partition images so they ride a single send_multiple_images call
579
+ # on platforms that support batch image uploads (Signal/Slack RPCs).
580
+ image_paths = [p for p in candidates if _Path(p).suffix.lower() in _IMAGE_EXTS]
581
+ other_paths = [p for p in candidates if _Path(p).suffix.lower() not in _IMAGE_EXTS]
582
+
583
+ if image_paths:
584
+ try:
585
+ batch = [(f"file://{_quote(p)}", "") for p in image_paths]
586
+ await adapter.send_multiple_images(
587
+ chat_id=chat_id, images=batch, metadata=metadata,
588
+ )
589
+ except Exception as exc:
590
+ logger.warning(
591
+ "kanban notifier: image batch upload failed: %s", exc,
592
+ )
593
+
594
+ for path in other_paths:
595
+ ext = _Path(path).suffix.lower()
596
+ try:
597
+ if ext in _VIDEO_EXTS:
598
+ await adapter.send_video(
599
+ chat_id=chat_id, video_path=path, metadata=metadata,
600
+ )
601
+ else:
602
+ await adapter.send_document(
603
+ chat_id=chat_id, file_path=path, metadata=metadata,
604
+ )
605
+ except Exception as exc:
606
+ logger.warning(
607
+ "kanban notifier: artifact upload (%s) failed: %s",
608
+ path, exc,
609
+ )
610
+
611
+ async def _kanban_dispatcher_watcher(self) -> None:
612
+ """Embedded kanban dispatcher — one tick every `dispatch_interval_seconds`.
613
+
614
+ Gated by `kanban.dispatch_in_gateway` in config.yaml (default True).
615
+ When true, the gateway hosts the single dispatcher for this profile:
616
+ no separate `hermes kanban daemon` process needed. When false, the
617
+ loop exits immediately and an external daemon is expected.
618
+
619
+ Each tick calls :func:`kanban_db.dispatch_once` inside
620
+ ``asyncio.to_thread`` so the SQLite WAL lock never blocks the
621
+ event loop. Failures in one tick don't stop subsequent ticks —
622
+ same pattern as `_kanban_notifier_watcher`.
623
+
624
+ Shutdown: the loop checks ``self._running`` between ticks; gateway
625
+ stop() flips it to False and cancels pending tasks, and the
626
+ in-flight ``to_thread`` returns on its own after the current
627
+ ``dispatch_once`` call finishes (typically <1ms on an idle board).
628
+ """
629
+ # Read config once at boot. If the user flips the flag later, they
630
+ # restart the gateway; same pattern as every other background
631
+ # watcher here. Honours HERMES_KANBAN_DISPATCH_IN_GATEWAY env var
632
+ # as an escape hatch (false-y value disables without editing YAML).
633
+ try:
634
+ from hermes_cli.config import load_config as _load_config
635
+ except Exception:
636
+ logger.warning("kanban dispatcher: config loader unavailable; disabled")
637
+ return
638
+ env_override = os.environ.get("HERMES_KANBAN_DISPATCH_IN_GATEWAY", "").strip().lower()
639
+ if env_override in {"0", "false", "no", "off"}:
640
+ logger.info("kanban dispatcher: disabled via HERMES_KANBAN_DISPATCH_IN_GATEWAY env")
641
+ return
642
+
643
+ try:
644
+ cfg = _load_config()
645
+ except Exception as exc:
646
+ logger.warning("kanban dispatcher: cannot load config (%s); disabled", exc)
647
+ return
648
+ kanban_cfg = cfg.get("kanban", {}) if isinstance(cfg, dict) else {}
649
+ if not kanban_cfg.get("dispatch_in_gateway", True):
650
+ logger.info(
651
+ "kanban dispatcher: disabled via config kanban.dispatch_in_gateway=false"
652
+ )
653
+ return
654
+
655
+ try:
656
+ from hermes_cli import kanban_db as _kb
657
+ except Exception:
658
+ logger.warning("kanban dispatcher: kanban_db not importable; dispatcher disabled")
659
+ return
660
+
661
+ # Single-dispatcher backstop. dispatch_in_gateway defaults to true, so a
662
+ # new profile gateway (or a same-profile restart race) can silently
663
+ # start a second dispatcher; concurrent dispatchers double reclaim
664
+ # frequency, double claim-attempt events, and — with
665
+ # wal_autocheckpoint=0 — concurrent manual WAL checkpoints can corrupt
666
+ # index pages. The lock lives at the machine-global kanban root
667
+ # (shared across profiles by design), so it serialises ALL gateways.
668
+ self._kanban_dispatcher_lock_handle = None
669
+ _lock_path = _kb.kanban_home() / "kanban" / ".dispatcher.lock"
670
+ _lock_handle, _lock_state = _acquire_singleton_lock(_lock_path)
671
+ if _lock_state == "contended":
672
+ logger.info(
673
+ "kanban dispatcher: another gateway already holds the dispatcher "
674
+ "lock (%s); this gateway will NOT dispatch.", _lock_path,
675
+ )
676
+ return
677
+ if _lock_state == "held":
678
+ self._kanban_dispatcher_lock_handle = _lock_handle # hold for process lifetime
679
+ logger.info("kanban dispatcher: holding singleton dispatcher lock (%s)", _lock_path)
680
+ else:
681
+ logger.warning(
682
+ "kanban dispatcher: advisory lock unavailable at %s; proceeding "
683
+ "on config control alone.", _lock_path,
684
+ )
685
+
686
+ try:
687
+ interval = float(kanban_cfg.get("dispatch_interval_seconds", 60) or 60)
688
+ except (ValueError, TypeError):
689
+ logger.warning(
690
+ "kanban dispatcher: invalid dispatch_interval_seconds=%r, using default 60",
691
+ kanban_cfg.get("dispatch_interval_seconds"),
692
+ )
693
+ interval = 60.0
694
+ interval = max(interval, 1.0) # sanity floor — tighter than this is a footgun
695
+
696
+ # Read max_spawn config to limit concurrent kanban tasks
697
+ max_spawn = kanban_cfg.get("max_spawn", None)
698
+ if max_spawn is not None:
699
+ logger.info(f"kanban dispatcher: max_spawn={max_spawn}")
700
+
701
+ # Cap the number of simultaneously running tasks so slow workers
702
+ # (local LLMs, resource-constrained hosts) don't pile up and time
703
+ # out. When set, the dispatcher skips spawning when the board
704
+ # already has this many tasks in 'running' status.
705
+ raw_max_in_progress = kanban_cfg.get("max_in_progress", None)
706
+ max_in_progress = None
707
+ if raw_max_in_progress is not None:
708
+ try:
709
+ max_in_progress = int(raw_max_in_progress)
710
+ except (TypeError, ValueError):
711
+ logger.warning(
712
+ "kanban dispatcher: invalid kanban.max_in_progress=%r; ignoring",
713
+ raw_max_in_progress,
714
+ )
715
+ max_in_progress = None
716
+ else:
717
+ if max_in_progress < 1:
718
+ logger.warning(
719
+ "kanban dispatcher: kanban.max_in_progress=%r is below 1; ignoring",
720
+ raw_max_in_progress,
721
+ )
722
+ max_in_progress = None
723
+ else:
724
+ logger.info(f"kanban dispatcher: max_in_progress={max_in_progress}")
725
+
726
+ raw_failure_limit = kanban_cfg.get("failure_limit", _kb.DEFAULT_FAILURE_LIMIT)
727
+ try:
728
+ failure_limit = int(raw_failure_limit)
729
+ except (TypeError, ValueError):
730
+ logger.warning(
731
+ "kanban dispatcher: invalid kanban.failure_limit=%r; using default %d",
732
+ raw_failure_limit,
733
+ _kb.DEFAULT_FAILURE_LIMIT,
734
+ )
735
+ failure_limit = _kb.DEFAULT_FAILURE_LIMIT
736
+ if failure_limit < 1:
737
+ logger.warning(
738
+ "kanban dispatcher: kanban.failure_limit=%r is below 1; using default %d",
739
+ raw_failure_limit,
740
+ _kb.DEFAULT_FAILURE_LIMIT,
741
+ )
742
+ failure_limit = _kb.DEFAULT_FAILURE_LIMIT
743
+
744
+ # Read stale_timeout_seconds — 0 disables stale detection.
745
+ raw_stale = kanban_cfg.get("dispatch_stale_timeout_seconds", 0)
746
+ try:
747
+ stale_timeout_seconds = int(raw_stale or 0)
748
+ except (TypeError, ValueError):
749
+ logger.warning(
750
+ "kanban dispatcher: invalid kanban.dispatch_stale_timeout_seconds=%r; "
751
+ "disabling stale detection",
752
+ raw_stale,
753
+ )
754
+ stale_timeout_seconds = 0
755
+
756
+ # Read kanban.default_assignee — fallback profile for tasks
757
+ # created without an explicit assignee (e.g. via the dashboard).
758
+ # When set, the dispatcher applies it to unassigned ready tasks
759
+ # instead of skipping them indefinitely (#27145). Empty string
760
+ # (the schema default) means "no fallback, keep skipping" —
761
+ # backward-compatible with existing installs.
762
+ default_assignee = (kanban_cfg.get("default_assignee") or "").strip() or None
763
+ if default_assignee:
764
+ logger.info(
765
+ "kanban dispatcher: default_assignee=%r (unassigned ready tasks "
766
+ "will route to this profile)",
767
+ default_assignee,
768
+ )
769
+
770
+ # Read kanban.max_in_progress_per_profile — per-profile concurrency
771
+ # cap (#21582). When set, no single profile gets more than N
772
+ # workers running at once, even if the global max_in_progress
773
+ # would allow it. Prevents one profile's local model / API quota
774
+ # / browser pool from being overwhelmed by a fan-out.
775
+ raw_per_profile = kanban_cfg.get("max_in_progress_per_profile", None)
776
+ max_in_progress_per_profile = None
777
+ if raw_per_profile is not None:
778
+ try:
779
+ max_in_progress_per_profile = int(raw_per_profile)
780
+ except (TypeError, ValueError):
781
+ logger.warning(
782
+ "kanban dispatcher: invalid kanban.max_in_progress_per_profile=%r; ignoring",
783
+ raw_per_profile,
784
+ )
785
+ max_in_progress_per_profile = None
786
+ else:
787
+ if max_in_progress_per_profile < 1:
788
+ logger.warning(
789
+ "kanban dispatcher: kanban.max_in_progress_per_profile=%r is below 1; ignoring",
790
+ raw_per_profile,
791
+ )
792
+ max_in_progress_per_profile = None
793
+ else:
794
+ logger.info(
795
+ "kanban dispatcher: max_in_progress_per_profile=%d",
796
+ max_in_progress_per_profile,
797
+ )
798
+
799
+ # Initial delay so the gateway finishes wiring adapters before the
800
+ # dispatcher spawns workers (those workers may hit gateway notify
801
+ # subscriptions etc.). Matches the notifier watcher's delay.
802
+ await asyncio.sleep(5)
803
+
804
+ # Health telemetry mirrored from `_cmd_daemon`: warn when ready
805
+ # queue is non-empty but spawns are 0 for N consecutive ticks —
806
+ # usually means broken PATH, missing venv, or credential loss.
807
+ HEALTH_WINDOW = 6
808
+ bad_ticks = 0
809
+ last_warn_at = 0
810
+ # Avoid hot-looping corrupt-looking board DBs, but do not suppress
811
+ # same-fingerprint retries forever: transient WAL/open races can
812
+ # surface as "database disk image is malformed" for one tick.
813
+ CORRUPT_BOARD_RETRY_AFTER_SECONDS = 300
814
+ disabled_corrupt_boards: dict[
815
+ str, tuple[tuple[str, int | None, int | None], float]
816
+ ] = {}
817
+
818
+ def _board_db_fingerprint(slug: str) -> tuple[str, int | None, int | None]:
819
+ path = _kb.kanban_db_path(slug)
820
+ try:
821
+ resolved = str(path.expanduser().resolve())
822
+ except Exception:
823
+ resolved = str(path)
824
+ try:
825
+ stat = path.stat()
826
+ except OSError:
827
+ return (resolved, None, None)
828
+ return (resolved, stat.st_mtime_ns, stat.st_size)
829
+
830
+ def _is_corrupt_board_db_error(exc: Exception) -> bool:
831
+ corrupt_guard_error = getattr(_kb, "KanbanDbCorruptError", None)
832
+ if corrupt_guard_error is not None and isinstance(exc, corrupt_guard_error):
833
+ return True
834
+ if not isinstance(exc, sqlite3.DatabaseError):
835
+ return False
836
+ msg = str(exc).lower()
837
+ return (
838
+ "file is not a database" in msg
839
+ or "database disk image is malformed" in msg
840
+ )
841
+
842
+ def _tick_once_for_board(slug: str) -> "Optional[object]":
843
+ """Run one dispatch_once for a specific board.
844
+
845
+ Runs in a worker thread via `asyncio.to_thread`. `board=slug`
846
+ is passed through `dispatch_once` so `resolve_workspace` and
847
+ `_default_spawn` see the right paths. The per-board DB is
848
+ opened explicitly so concurrent boards never share a
849
+ connection handle or accidentally claim across each other.
850
+ """
851
+ conn = None
852
+ fingerprint = _board_db_fingerprint(slug)
853
+ disabled_entry = disabled_corrupt_boards.get(slug)
854
+ if disabled_entry is not None:
855
+ disabled_fingerprint, disabled_at = disabled_entry
856
+ age = time.monotonic() - disabled_at
857
+ if (
858
+ disabled_fingerprint == fingerprint
859
+ and age < CORRUPT_BOARD_RETRY_AFTER_SECONDS
860
+ ):
861
+ return None
862
+ if disabled_fingerprint == fingerprint:
863
+ logger.info(
864
+ "kanban dispatcher: board %s database fingerprint unchanged "
865
+ "after %.0fs quarantine; retrying dispatch",
866
+ slug,
867
+ age,
868
+ )
869
+ else:
870
+ logger.info(
871
+ "kanban dispatcher: board %s database changed; retrying dispatch",
872
+ slug,
873
+ )
874
+ disabled_corrupt_boards.pop(slug, None)
875
+ try:
876
+ conn = _kb.connect(board=slug)
877
+ # `connect()` runs the schema + idempotent migration on
878
+ # first open per process; the previous explicit
879
+ # `init_db()` call here busted the per-process cache and
880
+ # re-ran the migration on a second connection, racing
881
+ # the first. See the matching comment in
882
+ # `_kanban_notifier_watcher` and issue #21378.
883
+ return _kb.dispatch_once(
884
+ conn,
885
+ board=slug,
886
+ max_spawn=max_spawn,
887
+ max_in_progress=max_in_progress,
888
+ failure_limit=failure_limit,
889
+ stale_timeout_seconds=stale_timeout_seconds,
890
+ default_assignee=default_assignee,
891
+ max_in_progress_per_profile=max_in_progress_per_profile,
892
+ )
893
+ except sqlite3.DatabaseError as exc:
894
+ if _is_corrupt_board_db_error(exc):
895
+ disabled_corrupt_boards[slug] = (fingerprint, time.monotonic())
896
+ logger.error(
897
+ "kanban dispatcher: board %s database %s is not a valid "
898
+ "SQLite database; pausing dispatch for this board until "
899
+ "the file changes, the gateway restarts, or the "
900
+ "quarantine timer expires. Move or restore the file, "
901
+ "then run `hermes kanban init` if you need a fresh board.",
902
+ slug,
903
+ fingerprint[0],
904
+ )
905
+ return None
906
+ logger.exception("kanban dispatcher: tick failed on board %s", slug)
907
+ return None
908
+ except Exception as exc:
909
+ if _is_corrupt_board_db_error(exc):
910
+ disabled_corrupt_boards[slug] = (fingerprint, time.monotonic())
911
+ logger.error(
912
+ "kanban dispatcher: board %s database %s is not a valid "
913
+ "SQLite database; pausing dispatch for this board until "
914
+ "the file changes, the gateway restarts, or the "
915
+ "quarantine timer expires. Move or restore the file, "
916
+ "then run `hermes kanban init` if you need a fresh board.",
917
+ slug,
918
+ fingerprint[0],
919
+ )
920
+ return None
921
+ logger.exception("kanban dispatcher: tick failed on board %s", slug)
922
+ return None
923
+ finally:
924
+ if conn is not None:
925
+ try:
926
+ conn.close()
927
+ except Exception:
928
+ pass
929
+
930
+ def _tick_once() -> "list[tuple[str, Optional[object]]]":
931
+ """Run one dispatch_once per board. Returns (slug, result) pairs.
932
+
933
+ Enumerating boards on every tick keeps the dispatcher honest
934
+ when users create a new board mid-run: no restart required,
935
+ the next tick picks it up automatically.
936
+ """
937
+ try:
938
+ boards = _kb.list_boards(include_archived=False)
939
+ except Exception:
940
+ boards = [_kb.read_board_metadata(_kb.DEFAULT_BOARD)]
941
+ out: list[tuple[str, "Optional[object]"]] = []
942
+ for b in boards:
943
+ slug = b.get("slug") or _kb.DEFAULT_BOARD
944
+ out.append((slug, _tick_once_for_board(slug)))
945
+ return out
946
+
947
+ def _ready_nonempty() -> bool:
948
+ """Cheap probe: is there at least one ready+assigned+unclaimed
949
+ task on ANY board whose assignee maps to a real Hermes profile
950
+ (i.e. one the dispatcher would actually spawn for)?
951
+
952
+ Tasks assigned to control-plane lanes (e.g. ``orion-cc``,
953
+ ``orion-research``) are pulled by terminals via
954
+ ``claim_task`` directly and never spawnable, so a queue full
955
+ of those is "correctly idle", not "stuck". Filtering them out
956
+ here keeps the stuck-warn fire only on real failures (broken
957
+ PATH, missing venv, credential loss for a real Hermes profile).
958
+ """
959
+ try:
960
+ boards = _kb.list_boards(include_archived=False)
961
+ except Exception:
962
+ boards = [_kb.read_board_metadata(_kb.DEFAULT_BOARD)]
963
+ for b in boards:
964
+ slug = b.get("slug") or _kb.DEFAULT_BOARD
965
+ conn = None
966
+ try:
967
+ conn = _kb.connect(board=slug)
968
+ if _kb.has_spawnable_ready(conn):
969
+ return True
970
+ if _kb.has_spawnable_review(conn):
971
+ return True
972
+ except Exception:
973
+ continue
974
+ finally:
975
+ if conn is not None:
976
+ try:
977
+ conn.close()
978
+ except Exception:
979
+ pass
980
+ return False
981
+
982
+ # Auto-decompose: turn fresh triage tasks into ready workgraphs
983
+ # before the dispatcher fans out workers. Gated by
984
+ # ``kanban.auto_decompose`` (default True). Capped by
985
+ # ``kanban.auto_decompose_per_tick`` (default 3) so a bulk-load
986
+ # of triage tasks doesn't burst-spend the aux LLM in one tick;
987
+ # remainder defers to subsequent ticks.
988
+ auto_decompose_enabled = bool(kanban_cfg.get("auto_decompose", True))
989
+ try:
990
+ auto_decompose_per_tick = int(
991
+ kanban_cfg.get("auto_decompose_per_tick", 3) or 3
992
+ )
993
+ except (TypeError, ValueError):
994
+ auto_decompose_per_tick = 3
995
+ if auto_decompose_per_tick < 1:
996
+ auto_decompose_per_tick = 1
997
+
998
+ def _auto_decompose_tick() -> int:
999
+ """Run the auto-decomposer for up to N triage tasks across all
1000
+ boards. Returns the number of triage tasks that were
1001
+ successfully decomposed or specified this tick.
1002
+ """
1003
+ try:
1004
+ from hermes_cli import kanban_decompose as _decomp
1005
+ except Exception as exc: # pragma: no cover
1006
+ logger.warning(
1007
+ "kanban auto-decompose: import failed (%s); skipping", exc,
1008
+ )
1009
+ return 0
1010
+ try:
1011
+ boards = _kb.list_boards(include_archived=False)
1012
+ except Exception:
1013
+ boards = [_kb.read_board_metadata(_kb.DEFAULT_BOARD)]
1014
+ attempted = 0
1015
+ successes = 0
1016
+ for b in boards:
1017
+ slug = b.get("slug") or _kb.DEFAULT_BOARD
1018
+ if attempted >= auto_decompose_per_tick:
1019
+ break
1020
+ # Pin this board for the duration of the call — same
1021
+ # pattern as the dashboard specify endpoint. The
1022
+ # decomposer module connects with no board kwarg and
1023
+ # relies on the env var.
1024
+ prev_env = os.environ.get("HERMES_KANBAN_BOARD")
1025
+ try:
1026
+ os.environ["HERMES_KANBAN_BOARD"] = slug
1027
+ try:
1028
+ triage_ids = _decomp.list_triage_ids()
1029
+ except Exception as exc:
1030
+ logger.debug(
1031
+ "kanban auto-decompose: list_triage_ids failed on board %s (%s)",
1032
+ slug, exc,
1033
+ )
1034
+ triage_ids = []
1035
+ for tid in triage_ids:
1036
+ if attempted >= auto_decompose_per_tick:
1037
+ break
1038
+ attempted += 1
1039
+ try:
1040
+ outcome = _decomp.decompose_task(
1041
+ tid, author="auto-decomposer",
1042
+ )
1043
+ except Exception:
1044
+ logger.exception(
1045
+ "kanban auto-decompose: decompose_task crashed on %s",
1046
+ tid,
1047
+ )
1048
+ continue
1049
+ if outcome.ok:
1050
+ successes += 1
1051
+ if outcome.fanout and outcome.child_ids:
1052
+ logger.info(
1053
+ "kanban auto-decompose [%s]: %s → %d children",
1054
+ slug, tid, len(outcome.child_ids),
1055
+ )
1056
+ else:
1057
+ logger.info(
1058
+ "kanban auto-decompose [%s]: %s → single task (no fanout)",
1059
+ slug, tid,
1060
+ )
1061
+ else:
1062
+ # Common no-op reasons (no aux client configured) shouldn't
1063
+ # spam logs every tick. Log at debug.
1064
+ logger.debug(
1065
+ "kanban auto-decompose [%s]: %s skipped: %s",
1066
+ slug, tid, outcome.reason,
1067
+ )
1068
+ finally:
1069
+ if prev_env is None:
1070
+ os.environ.pop("HERMES_KANBAN_BOARD", None)
1071
+ else:
1072
+ os.environ["HERMES_KANBAN_BOARD"] = prev_env
1073
+ return successes
1074
+
1075
+ logger.info(
1076
+ "kanban dispatcher: embedded in gateway (interval=%.1fs)", interval
1077
+ )
1078
+ while self._running:
1079
+ try:
1080
+ # Reap zombie children before per-board work so a board DB
1081
+ # failure cannot block cleanup of unrelated workers.
1082
+ pids = await asyncio.to_thread(_kb.reap_worker_zombies)
1083
+ if pids:
1084
+ logger.info(
1085
+ "kanban dispatcher: reaped %d zombie worker(s), pids=%s",
1086
+ len(pids),
1087
+ pids,
1088
+ )
1089
+ except Exception:
1090
+ logger.exception("kanban dispatcher: zombie reaper failed")
1091
+
1092
+ try:
1093
+ if auto_decompose_enabled:
1094
+ await asyncio.to_thread(_auto_decompose_tick)
1095
+ results = await asyncio.to_thread(_tick_once)
1096
+ any_spawned = False
1097
+ for slug, res in (results or []):
1098
+ if res is not None and getattr(res, "spawned", None):
1099
+ any_spawned = True
1100
+ # Quiet by default — only log when something actually
1101
+ # happened, so an idle gateway stays silent.
1102
+ logger.info(
1103
+ "kanban dispatcher [%s]: spawned=%d reclaimed=%d "
1104
+ "crashed=%d timed_out=%d promoted=%d auto_blocked=%d",
1105
+ slug,
1106
+ len(res.spawned),
1107
+ res.reclaimed,
1108
+ len(res.crashed) if hasattr(res.crashed, "__len__") else 0,
1109
+ len(res.timed_out) if hasattr(res.timed_out, "__len__") else 0,
1110
+ res.promoted,
1111
+ len(res.auto_blocked) if hasattr(res.auto_blocked, "__len__") else 0,
1112
+ )
1113
+ # Health telemetry (aggregate across boards)
1114
+ ready_pending = await asyncio.to_thread(_ready_nonempty)
1115
+ if ready_pending and not any_spawned:
1116
+ bad_ticks += 1
1117
+ else:
1118
+ bad_ticks = 0
1119
+ if bad_ticks >= HEALTH_WINDOW:
1120
+ now = int(time.time())
1121
+ if now - last_warn_at >= 300:
1122
+ logger.warning(
1123
+ "kanban dispatcher stuck: ready queue non-empty for "
1124
+ "%d consecutive ticks but 0 workers spawned. Check "
1125
+ "profile health (venv, PATH, credentials) and "
1126
+ "`hermes kanban list --status ready`.",
1127
+ bad_ticks,
1128
+ )
1129
+ last_warn_at = now
1130
+ except asyncio.CancelledError:
1131
+ logger.debug("kanban dispatcher: cancelled")
1132
+ _release_singleton_lock(self._kanban_dispatcher_lock_handle)
1133
+ self._kanban_dispatcher_lock_handle = None
1134
+ raise
1135
+ except Exception:
1136
+ logger.exception("kanban dispatcher: unexpected watcher error")
1137
+
1138
+ # Sleep in 1s slices so shutdown is snappy — otherwise a stop()
1139
+ # waits up to `interval` seconds for the current sleep to finish.
1140
+ slept = 0.0
1141
+ while slept < interval and self._running:
1142
+ await asyncio.sleep(min(1.0, interval - slept))
1143
+ slept += 1.0
1144
+
1145
+ _release_singleton_lock(self._kanban_dispatcher_lock_handle)
1146
+ self._kanban_dispatcher_lock_handle = None