@loicngr/kobo 2.1.28 → 2.1.30

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 (207) hide show
  1. package/AGENTS.md +60 -11
  2. package/CHANGELOG.md +20 -1
  3. package/CONFIGURATION.md +140 -35
  4. package/CONTRIBUTING.md +2 -2
  5. package/DESIGN.md +1 -1
  6. package/README.md +12 -9
  7. package/SECURITY.md +7 -2
  8. package/dist/server/db/memory-schema.js +5 -5
  9. package/dist/server/db/migrations.js +2 -0
  10. package/dist/server/db/mistral-memory-migration.js +40 -0
  11. package/dist/server/routes/engines.js +14 -0
  12. package/dist/server/routes/environment.js +1 -1
  13. package/dist/server/routes/health.js +14 -3
  14. package/dist/server/routes/settings.js +7 -1
  15. package/dist/server/routes/workspaces.js +116 -9
  16. package/dist/server/services/agent/engines/claude-code/capabilities.js +2 -0
  17. package/dist/server/services/agent/engines/claude-code/engine.js +3 -0
  18. package/dist/server/services/agent/engines/claude-code/resolve-binary.js +1 -1
  19. package/dist/server/services/agent/engines/codex/capabilities.js +2 -0
  20. package/dist/server/services/agent/engines/codex/engine.js +3 -0
  21. package/dist/server/services/agent/engines/configuration.js +14 -0
  22. package/dist/server/services/agent/engines/mistral-vibe/capabilities.js +13 -0
  23. package/dist/server/services/agent/engines/mistral-vibe/client.js +64 -0
  24. package/dist/server/services/agent/engines/mistral-vibe/configuration.js +76 -0
  25. package/dist/server/services/agent/engines/mistral-vibe/engine.js +361 -0
  26. package/dist/server/services/agent/engines/mistral-vibe/event-mapper.js +297 -0
  27. package/dist/server/services/agent/engines/mistral-vibe/options-builder.js +35 -0
  28. package/dist/server/services/agent/engines/mistral-vibe/server-requests.js +278 -0
  29. package/dist/server/services/agent/engines/mistral-vibe/spawn.js +176 -0
  30. package/dist/server/services/agent/engines/registry.js +2 -0
  31. package/dist/server/services/agent/engines/types.js +6 -0
  32. package/dist/server/services/agent/orchestrator.js +88 -25
  33. package/dist/server/services/agent/session-controller.js +5 -0
  34. package/dist/server/services/auto-loop-final-review-service.js +10 -3
  35. package/dist/server/services/dev-server-service.js +68 -4
  36. package/dist/server/services/environment-check-service.js +9 -3
  37. package/dist/server/services/memory-service.js +1 -1
  38. package/dist/server/services/mistral-options-service.js +145 -0
  39. package/dist/server/services/pr-watcher-service.js +40 -11
  40. package/dist/server/services/review-service.js +33 -9
  41. package/dist/server/services/session-handoff-service.js +28 -2
  42. package/dist/server/services/settings-defaults.js +1 -0
  43. package/dist/server/services/settings-service.js +19 -1
  44. package/dist/server/services/workspace-creation-mcp-service.js +50 -8
  45. package/dist/server/services/workspace-dialogue-service.js +2 -0
  46. package/dist/server/services/workspace-message-service.js +19 -1
  47. package/dist/server/services/workspace-permission-policy-service.js +24 -0
  48. package/dist/server/utils/bounded-process.js +1 -1
  49. package/dist/shared/launch-instructions.js +18 -7
  50. package/dist/shared/memory.js +1 -1
  51. package/dist/shared/mistral-models.js +9 -0
  52. package/dist/shared/mistral-options.js +1 -0
  53. package/dist/shared/question-selections.js +16 -0
  54. package/dist/shared/workspace-creation-tools.js +8 -2
  55. package/dist/shared/workspace-dialogue-tools.js +15 -4
  56. package/docs/deployment-vps.md +10 -3
  57. package/docs/getting-started.md +4 -3
  58. package/docs/troubleshooting.md +2 -2
  59. package/package.json +20 -14
  60. package/src/client/dist/pwa/assets/ActivityFeed-C5stmHXP.js +2 -0
  61. package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-CIDSX32d.css +1 -0
  62. package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-DG32W_0G.js +1 -0
  63. package/src/client/dist/pwa/assets/{ChangelogPage-CAAUEn5X.js → ChangelogPage-_vItY8vA.js} +1 -1
  64. package/src/client/dist/pwa/assets/{ClosePopup-CRV2hxHS.js → ClosePopup-rU8Bm-mC.js} +1 -1
  65. package/src/client/dist/pwa/assets/{ComparisonPanel-BCb7vB5E.js → ComparisonPanel-B3BMzQH0.js} +2 -2
  66. package/src/client/dist/pwa/assets/CreatePage-FlGrfmjX.js +2 -0
  67. package/src/client/dist/pwa/assets/CreatePage-oh8ihVC4.css +1 -0
  68. package/src/client/dist/pwa/assets/{DashboardPage-XE52EABP.js → DashboardPage-lJGwWQYQ.js} +1 -1
  69. package/src/client/dist/pwa/assets/DiffViewer-DIaQH3lA.js +8 -0
  70. package/src/client/dist/pwa/assets/{DocumentsPanel-L4pbQfz1.js → DocumentsPanel-Ct-nm4fX.js} +1 -1
  71. package/src/client/dist/pwa/assets/{DrawerToggleButton-DiOJm5he.js → DrawerToggleButton-M5qv4Io-.js} +1 -1
  72. package/src/client/dist/pwa/assets/GitPanel-BaHCgb3p.js +3 -0
  73. package/src/client/dist/pwa/assets/GitPanel-C8vw2qvW.css +1 -0
  74. package/src/client/dist/pwa/assets/HealthPage-DR3wa6iD.js +1 -0
  75. package/src/client/dist/pwa/assets/HealthPage-Dbn4J_DT.css +1 -0
  76. package/src/client/dist/pwa/assets/{MainLayout-DoAEgANU.css → MainLayout-CHi8qP1D.css} +1 -1
  77. package/src/client/dist/pwa/assets/MainLayout-WJabICsA.js +2 -0
  78. package/src/client/dist/pwa/assets/{MemoryPanel-DtEvh_b7.js → MemoryPanel-CRpWdfc0.js} +1 -1
  79. package/src/client/dist/pwa/assets/MemoryProposalList-DGz3lDbL.js +1 -0
  80. package/src/client/dist/pwa/assets/{QBadge-CW3Lw-kL.js → QBadge-DiwbRpte.js} +1 -1
  81. package/src/client/dist/pwa/assets/{QBanner-BT2ZemYu.js → QBanner-Cw89HSjk.js} +1 -1
  82. package/src/client/dist/pwa/assets/QBtn-kfhHFy7Q.js +1 -0
  83. package/src/client/dist/pwa/assets/{QBtnDropdown-CWPIugs8.js → QBtnDropdown-tt8rUNNR.js} +1 -1
  84. package/src/client/dist/pwa/assets/{QBtnGroup-CnM4vSJA.js → QBtnGroup-Bioxjp9q.js} +1 -1
  85. package/src/client/dist/pwa/assets/{QBtnToggle-DccOOtbh.js → QBtnToggle-Bod-x9im.js} +1 -1
  86. package/src/client/dist/pwa/assets/{QChip-Be16M4Ig.js → QChip-DvhtPv_L.js} +1 -1
  87. package/src/client/dist/pwa/assets/{QExpansionItem-D-EGL2b3.js → QExpansionItem-BkrI3UPL.js} +1 -1
  88. package/src/client/dist/pwa/assets/{QItemSection-MdJ1_ZK7.js → QItemSection-ahqRK4JF.js} +1 -1
  89. package/src/client/dist/pwa/assets/{QList-Cx1UfaUm.js → QList-CAUl0v66.js} +1 -1
  90. package/src/client/dist/pwa/assets/{QMarkupTable-UhP7fVUp.js → QMarkupTable-B4LLAly-.js} +1 -1
  91. package/src/client/dist/pwa/assets/{QPage-CqkGHokx.js → QPage-DIy_mRoT.js} +1 -1
  92. package/src/client/dist/pwa/assets/{QScrollArea-BMrqK-xd.js → QScrollArea-BvgnY-oA.js} +1 -1
  93. package/src/client/dist/pwa/assets/QSelect-BtZB1wrB.js +1 -0
  94. package/src/client/dist/pwa/assets/{QSpinnerDots-xdWyaE1i.js → QSpinnerDots-Bea6hAuJ.js} +1 -1
  95. package/src/client/dist/pwa/assets/{QTooltip-Bk8dLUtU.js → QTooltip-CDH6p745.js} +1 -1
  96. package/src/client/dist/pwa/assets/{SchedulePanel-CuxmxCKi.js → SchedulePanel-C2rWlh7l.js} +1 -1
  97. package/src/client/dist/pwa/assets/{SearchPage-DKnQPUqY.js → SearchPage-BFd4NQwJ.js} +1 -1
  98. package/src/client/dist/pwa/assets/{SettingsPage-B4rmd2WX.css → SettingsPage-Bm0TcarK.css} +1 -1
  99. package/src/client/dist/pwa/assets/SettingsPage-DDQCgnjL.js +9 -0
  100. package/src/client/dist/pwa/assets/{SplitWorkspacePage-WhQyX8fn.js → SplitWorkspacePage-DKCtOZO4.js} +1 -1
  101. package/src/client/dist/pwa/assets/TerminalPanel-BE_MSZF0.js +16 -0
  102. package/src/client/dist/pwa/assets/{ToolCallItem-DYubi-J0.js → ToolCallItem-BEYaOtgh.js} +4 -4
  103. package/src/client/dist/pwa/assets/{TouchPan-DDotyYLN.js → TouchPan-DeYoOjX5.js} +1 -1
  104. package/src/client/dist/pwa/assets/{TourReplayButton-kAnEMcy2.js → TourReplayButton-D5lMwIKt.js} +1 -1
  105. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-BHEAsIoo.js +1 -0
  106. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-v7ein__H.css +1 -0
  107. package/src/client/dist/pwa/assets/WorkspacePage-5_wsJPaJ.js +4 -0
  108. package/src/client/dist/pwa/assets/WorkspacePage-CtUXz0Cq.css +1 -0
  109. package/src/client/dist/pwa/assets/{browser-DVYLH8vl.js → browser-ogy_9LVv.js} +2 -2
  110. package/src/client/dist/pwa/assets/build-path-tree-CRXxaYkA.js +1 -0
  111. package/src/client/dist/pwa/assets/custom-sounds-iLTkCd_i.js +1 -0
  112. package/src/client/dist/pwa/assets/de-BqDU7iZH.js +17 -0
  113. package/src/client/dist/pwa/assets/detached-fullscreen-CHqMxB-8.js +1 -0
  114. package/src/client/dist/pwa/assets/dom-CRPVbmiy.js +2 -0
  115. package/src/client/dist/pwa/assets/ecl-94qehhXe.js +1 -0
  116. package/src/client/dist/pwa/assets/{editor.api-BC38eXAS.js → editor.api-Dx6TbfZG.js} +9 -9
  117. package/src/client/dist/pwa/assets/editor.worker-xrHNhPSq.js +33 -0
  118. package/src/client/dist/pwa/assets/efforts-DnJh2fW_.js +9 -0
  119. package/src/client/dist/pwa/assets/es-D4VMXQRk.js +17 -0
  120. package/src/client/dist/pwa/assets/expand-template-PUEVWny3.js +1 -0
  121. package/src/client/dist/pwa/assets/{format-OwxXBnDJ.js → format-yf9XM3dV.js} +48 -48
  122. package/src/client/dist/pwa/assets/{formatters-YgaCZcwJ.js → formatters-D0sXu9Oe.js} +1 -1
  123. package/src/client/dist/pwa/assets/fr-DIyaCFB3.js +17 -0
  124. package/src/client/dist/pwa/assets/{freemarker2-l7R42fI2.js → freemarker2-l_xIHxRf.js} +1 -1
  125. package/src/client/dist/pwa/assets/{handlebars-DFmPrgjH.js → handlebars-CYgq99yQ.js} +1 -1
  126. package/src/client/dist/pwa/assets/{html-CERIAlPM.js → html-C28GatN_.js} +1 -1
  127. package/src/client/dist/pwa/assets/i18n-2AmHIUy0.js +1 -0
  128. package/src/client/dist/pwa/assets/index-C0MApoXN.css +1 -0
  129. package/src/client/dist/pwa/assets/index-MMYMztJ_.js +20 -0
  130. package/src/client/dist/pwa/assets/it-CF6_4OS0.js +17 -0
  131. package/src/client/dist/pwa/assets/{javascript-BBYncplF.js → javascript-YCSueyyk.js} +1 -1
  132. package/src/client/dist/pwa/assets/{liquid-C1HqYyYU.js → liquid-CUBn90Gs.js} +1 -1
  133. package/src/client/dist/pwa/assets/{mdx-E74dsP22.js → mdx-C5WOHShJ.js} +1 -1
  134. package/src/client/dist/pwa/assets/monaco-contributions-DYwqbh7m.js +53 -0
  135. package/src/client/dist/pwa/assets/{monaco.contribution-DYdn9CwR.js → monaco.contribution-CoFzrPQl.js} +2 -2
  136. package/src/client/dist/pwa/assets/network-auth-BpJ6Noax.js +1 -0
  137. package/src/client/dist/pwa/assets/notify-theme-NPq4-Tup.js +1 -0
  138. package/src/client/dist/pwa/assets/permissionModes-Bbl901Hx.js +36 -0
  139. package/src/client/dist/pwa/assets/{private.use-form-DY1BkePL.js → private.use-form-BcspuzM9.js} +1 -1
  140. package/src/client/dist/pwa/assets/{project-color-_snXWYUp.js → project-color-gNyhKJlV.js} +1 -1
  141. package/src/client/dist/pwa/assets/{python-DNAAGBcX.js → python-lhKUZdPj.js} +1 -1
  142. package/src/client/dist/pwa/assets/{razor-CB3g5p_T.js → razor-Chi44rEL.js} +1 -1
  143. package/src/client/dist/pwa/assets/reactivity.esm-bundler-Dplx1ILy.js +1 -0
  144. package/src/client/dist/pwa/assets/render-chat-markdown-C9ihAYBt.js +67 -0
  145. package/src/client/dist/pwa/assets/runtime-core.esm-bundler-BAUgNzMK.js +1 -0
  146. package/src/client/dist/pwa/assets/{typescript-0kpz8bqP.js → typescript-CU2TXVm6.js} +1 -1
  147. package/src/client/dist/pwa/assets/{use-checkbox-v4aAbE6K.js → use-checkbox-DUxgXR4e.js} +1 -1
  148. package/src/client/dist/pwa/assets/{use-id-BrdYXce0.js → use-id-B7ieAx2m.js} +1 -1
  149. package/src/client/dist/pwa/assets/use-is-mobile-CHmaewI1.js +1 -0
  150. package/src/client/dist/pwa/assets/{use-search-index-status-D38yFOD_.js → use-search-index-status-CQ9S05G7.js} +1 -1
  151. package/src/client/dist/pwa/assets/use-slide-transition-BJh9xGMa.js +1 -0
  152. package/src/client/dist/pwa/assets/use-tours-BmrZbHjz.js +2 -0
  153. package/src/client/dist/pwa/assets/use-tours-CU5oA7cz.css +1 -0
  154. package/src/client/dist/pwa/assets/use-worktree-restore-DcucRlLh.js +1 -0
  155. package/src/client/dist/pwa/assets/{workspace-templates-D-lPJQ4G.js → workspace-templates-DaG42EEt.js} +1 -1
  156. package/src/client/dist/pwa/assets/{xml-BbOuqdUe.js → xml-CPO5LI6F.js} +1 -1
  157. package/src/client/dist/pwa/assets/{yaml-gasFQtfk.js → yaml-CoYs82jI.js} +1 -1
  158. package/src/client/dist/pwa/index.html +10 -10
  159. package/src/client/dist/pwa/sw.js +1 -1
  160. package/src/mcp-server/README.md +18 -6
  161. package/src/client/dist/pwa/assets/ActivityFeed-D58VS9o2.js +0 -2
  162. package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-CK3r_fVB.js +0 -1
  163. package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-Q-OlSQ1c.css +0 -1
  164. package/src/client/dist/pwa/assets/CreatePage-DasH1Uk4.js +0 -2
  165. package/src/client/dist/pwa/assets/CreatePage-DxOes_cF.css +0 -1
  166. package/src/client/dist/pwa/assets/DiffViewer-CncKlXne.js +0 -8
  167. package/src/client/dist/pwa/assets/GitPanel-WJlzy2VJ.js +0 -3
  168. package/src/client/dist/pwa/assets/GitPanel-yHvU38dp.css +0 -1
  169. package/src/client/dist/pwa/assets/HealthPage-Cnlhrn--.css +0 -1
  170. package/src/client/dist/pwa/assets/HealthPage-hJY5MurK.js +0 -1
  171. package/src/client/dist/pwa/assets/MainLayout-DgWElK7w.js +0 -2
  172. package/src/client/dist/pwa/assets/MemoryProposalList-kgFCm9zz.js +0 -1
  173. package/src/client/dist/pwa/assets/QBtn-BUSdPaf4.js +0 -1
  174. package/src/client/dist/pwa/assets/QSelect-DFWyDWCE.js +0 -1
  175. package/src/client/dist/pwa/assets/SettingsPage-CO3vIpPd.js +0 -9
  176. package/src/client/dist/pwa/assets/TerminalPanel-BY0lPq6k.js +0 -16
  177. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-B9LE8HtU.css +0 -1
  178. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-DiYt3rP3.js +0 -1
  179. package/src/client/dist/pwa/assets/WorkspacePage-BEBwwr81.css +0 -1
  180. package/src/client/dist/pwa/assets/WorkspacePage-K-1ce50d.js +0 -4
  181. package/src/client/dist/pwa/assets/build-path-tree-DEuOdIIk.js +0 -1
  182. package/src/client/dist/pwa/assets/custom-sounds-Ggw6V9iF.js +0 -1
  183. package/src/client/dist/pwa/assets/de-Bk3zQrin.js +0 -17
  184. package/src/client/dist/pwa/assets/detached-fullscreen-Du3TcYdX.js +0 -1
  185. package/src/client/dist/pwa/assets/dom-BtYTvLeN.js +0 -2
  186. package/src/client/dist/pwa/assets/ecl-D05T4iGw.js +0 -1
  187. package/src/client/dist/pwa/assets/editor.worker-BLJF_oQx.js +0 -33
  188. package/src/client/dist/pwa/assets/efforts-TGacUPcJ.js +0 -9
  189. package/src/client/dist/pwa/assets/es-f3xuAWA9.js +0 -17
  190. package/src/client/dist/pwa/assets/expand-template-DM8gq3Mo.js +0 -1
  191. package/src/client/dist/pwa/assets/fr-B6c21SG3.js +0 -17
  192. package/src/client/dist/pwa/assets/i18n-WaE4cZK9.js +0 -1
  193. package/src/client/dist/pwa/assets/index-C8zAZ-k-.css +0 -1
  194. package/src/client/dist/pwa/assets/index-CpWBGKdM.js +0 -20
  195. package/src/client/dist/pwa/assets/it-DUstDpC2.js +0 -17
  196. package/src/client/dist/pwa/assets/monaco-contributions-DHvm-kp5.js +0 -53
  197. package/src/client/dist/pwa/assets/network-auth-DTZUKbny.js +0 -1
  198. package/src/client/dist/pwa/assets/notify-theme-FuTL7HuS.js +0 -1
  199. package/src/client/dist/pwa/assets/permissionModes-D-Nexnoo.js +0 -36
  200. package/src/client/dist/pwa/assets/reactivity.esm-bundler-DY1JkAEA.js +0 -1
  201. package/src/client/dist/pwa/assets/render-chat-markdown-CafzCcmu.js +0 -69
  202. package/src/client/dist/pwa/assets/runtime-core.esm-bundler-Bqg71Vll.js +0 -1
  203. package/src/client/dist/pwa/assets/use-is-mobile-CDB_rzM9.js +0 -1
  204. package/src/client/dist/pwa/assets/use-slide-transition-B_Nu6u-t.js +0 -1
  205. package/src/client/dist/pwa/assets/use-tours-BWkHp34X.js +0 -2
  206. package/src/client/dist/pwa/assets/use-tours-CTZuBZOi.css +0 -1
  207. package/src/client/dist/pwa/assets/use-worktree-restore-DUjZr44N.js +0 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Kōbō
2
2
 
3
- **Run a fleet of Claude Code and Codex agents in parallel — each in its own git worktree, all from one dashboard.**
3
+ **Run a fleet of Claude Code, Codex and Mistral Vibe agents in parallel — each in its own git worktree, all from one dashboard.**
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@loicngr/kobo.svg)](https://www.npmjs.com/package/@loicngr/kobo)
6
6
  [![license](https://img.shields.io/npm/l/@loicngr/kobo.svg)](./LICENSE)
@@ -17,7 +17,7 @@ Kōbō (工房, "workshop") turns *one agent, one terminal* into a real workflow
17
17
 
18
18
  | | |
19
19
  |---|---|
20
- | **🗂️ Isolated worktrees, two engines** | Every workspace is its own git worktree and branch — parallel sessions never collide. Pick Claude Code or OpenAI Codex per workspace, and [continue in a fresh session](#fresh-sessions-and-llm-handoffs) with the same LLM or a different model/engine, preserving the worktree and mission history. |
20
+ | **🗂️ Isolated worktrees, three engines** | Every workspace is its own git worktree and branch — parallel sessions never collide. Pick Claude Code, OpenAI Codex or Mistral Vibe per workspace, and [continue in a fresh session](#fresh-sessions-and-llm-handoffs) with the same LLM or a different model/engine, preserving the worktree and mission history. |
21
21
  | **💬 Live chat, real diff review** | Streaming responses, inline Edit/Write diffs, a reasoning panel, and a Monaco diff viewer with **inline file editing**, conflict resolution, and one-click `Sync` / `Push` / `Open PR` / `Merge`. |
22
22
  | **🔁 Auto-loop, cron, wakeups** | Turn an agent loose on the task list: it works through tasks, retries on rate limits, and stops itself when there's nothing left, progress stalls, or it needs you. An optional [independent final review](CONFIGURATION.md#final-review-for-auto-loop) returns findings to the original session for automatic correction and repeats until none remain, with durable recovery after restart. Cron schedules and one-shot wakeups keep workspaces moving on their own timeline. |
23
23
  | **🔀 Create from a PR/MR, or from a ticket** | Pick an open pull/merge request from GitHub, GitLab, or Bitbucket and Kōbō resolves every local conflict for you before spinning up the workspace. Or start straight from a Notion page or a Sentry issue URL. |
@@ -28,13 +28,13 @@ Use [group messages](CONFIGURATION.md#group-workspace-messages) to send one inst
28
28
 
29
29
  ## Quick start
30
30
 
31
- Requires Node.js ≥ 24.15, Git, Bash and your own Claude Code **or** Codex authentication. No skill plugin or ticket integration is required. Kōbō is a local, single-user tool; each user installs their own instance.
31
+ Requires Node.js ≥ 26.12.1, Git, Bash and your own authentication for the chosen engine (Claude Code, Codex or Mistral Vibe). No skill plugin or ticket integration is required. Kōbō is a local, single-user tool; each user installs their own instance.
32
32
 
33
33
  ```bash
34
34
  npx @loicngr/kobo@latest
35
35
  ```
36
36
 
37
- Open <http://localhost:3000> and follow the first-run check. Use **Choose a folder** beside the project field to select a local Git repository; completing setup opens the mission form without launching an agent. New installs use the plugin-free **Standard** suite, **Plan** permissions, manual commit/push/publication preferences, and disabled optional integrations/audio. Existing preferences are preserved on upgrade.
37
+ Open <http://localhost:3000> and follow the first-run check. Use **Choose a folder** beside the project field to select a local Git repository; completing setup opens the mission form without launching an agent. New installs use the plugin-free **Standard** suite, **Plan** permissions for Claude Code/Codex (**Interactive** for Mistral Vibe), manual commit/push/publication preferences, and disabled optional integrations/audio. Existing preferences are preserved on upgrade.
38
38
 
39
39
  Follow [your first mission](./docs/getting-started.md), [troubleshooting](./docs/troubleshooting.md), or the [security and privacy model](./SECURITY.md).
40
40
 
@@ -69,11 +69,11 @@ Kōbō's production build is an installable PWA — use your browser's **Install
69
69
  - **Disk-space purge**: reclaim a merged workspace's `node_modules`/`vendor` weight without losing its chat history — see [`CONFIGURATION.md`](./CONFIGURATION.md#auto-purge-worktree-on-pr-merged).
70
70
  - **Lifecycle scripts**: shell scripts run on setup, cleanup, archive, session end, PR merge, or auto-loop stop, with output streamed into the chat.
71
71
  - **Observability**: a per-session timeline (duration, tools, tokens, errors) and a downloadable redacted diagnostic JSON.
72
- - **Optional integrations**: Notion (import missions) and Sentry (fix from issue URL), usable by both agent engines through saved connections, each independently toggled with a **Test connection** action; local voice transcription via `whisper.cpp`.
72
+ - **Optional integrations**: Notion (import missions) and Sentry (fix from issue URL), usable by all three agent engines through saved connections, each independently toggled with a **Test connection** action; local voice transcription via `whisper.cpp`.
73
73
 
74
74
  ### Persistent memory across engines
75
75
 
76
- Kōbō offers **Manual**, **Hybrid** (the default), and **Automatic** memory behavior in **Settings → Memory**. Global, project, and workspace scopes are shared by Claude Code and Codex but remain independently manageable. Hybrid applies workspace notes while asking for human approval before shared project/global changes. The workspace's right-hand **Memory** tab shows the current conversation's delivered context, pending proposals, and a provenance-aware operation journal. Memory has conservative per-response and per-conversation budgets; it cannot retract facts already transmitted into an agent or provider conversation. See the [persistent memory guide](./CONFIGURATION.md#persistent-memory) and [MCP memory tools](./src/mcp-server/README.md#persistent-memory-tools).
76
+ Kōbō offers **Manual**, **Hybrid** (the default), and **Automatic** memory behavior in **Settings → Memory**. Global, project, and workspace scopes are shared by Claude Code, Codex and Mistral Vibe but remain independently manageable. Hybrid applies workspace notes while asking for human approval before shared project/global changes. The workspace's right-hand **Memory** tab shows the current conversation's delivered context, pending proposals, and a provenance-aware operation journal. Memory has conservative per-response and per-conversation budgets; it cannot retract facts already transmitted into an agent or provider conversation. See the [persistent memory guide](./CONFIGURATION.md#persistent-memory) and [MCP memory tools](./src/mcp-server/README.md#persistent-memory-tools).
77
77
 
78
78
  ## Fresh sessions and LLM handoffs
79
79
 
@@ -83,6 +83,8 @@ Long conversation getting crowded? Use **Continue in a fresh session** at the bo
83
83
  2. Leave **Generate a handoff with the current agent** enabled to have the current conversation summarize the objective, constraints, decisions, verified progress, failed approaches and next action. Disable it when the old provider has no quota left: Kōbō builds context from the mission, tasks, Git state and source history without calling that LLM.
84
84
  3. Start the transfer. Kōbō interrupts the current work immediately, waits for confirmed shutdown, prepares the handoff and opens a fresh native conversation that continues the mission automatically.
85
85
 
86
+ Mistral Vibe can be a destination with a supported execution mode. When Vibe is the source, disable **Generate a handoff with the current agent**: its restricted summary launch is unsupported. Existing Plan selections are never silently promoted.
87
+
86
88
  The workspace, worktree, task list and conversation history remain available. The transfer links to its source session and a Markdown report in **Documents** (`.ai/handoffs/`, ignored by Git). Auto-loop keeps its intent and diagnostic budget; queued instructions retain their delivery state. Pending wakeups from the source conversation follow the fresh session once its startup is confirmed.
87
89
 
88
90
  If generation fails, choose **Retry**, **Continue without an LLM summary**, or **Cancel transfer**. Cancel and Stop leave the work stopped and restore the source conversation and LLM configuration, including when the destination failed to start. Failed attempts remain in history without being resumed automatically. If there was no source conversation, the next message starts a fresh one. A server restart preserves the transfer and report for an explicit recovery decision; it never automatically replays a potentially delivered prompt. Transfers are manual—conversation size does not trigger them automatically.
@@ -112,7 +114,7 @@ agents, while engine permissions remain a separate control.
112
114
  Notion and Sentry are optional. Save a **Direct connection** in the integration tab,
113
115
  enable it, save Settings, then use **Test connection**. Saved direct connections take
114
116
  priority over the Claude MCP selector, which is disabled while one exists. Enabled
115
- connections also reach Claude Code and Codex agents through `kobo-notion-*` and
117
+ connections also reach Claude Code, Codex and Mistral Vibe agents through `kobo-notion-*` and
116
118
  `kobo-sentry-*` MCP servers. Stop and resume a running agent to apply a connection
117
119
  change. See [managed integration MCPs](./CONFIGURATION.md#managed-integration-mcps-in-agent-sessions)
118
120
  for credential priority and native-configuration scope.
@@ -131,8 +133,9 @@ Kōbō binds to `127.0.0.1` only by default. Enabling **Settings → General →
131
133
 
132
134
  - **Claude Code**: authenticate once with `claude /login`. Kōbō calls the embedded SDK directly — no separately installed Claude CLI is required at runtime; the SDK supplies its runtime.
133
135
  - **OpenAI Codex**: run `codex login` or export `OPENAI_API_KEY`. Kōbō spawns a long-lived `codex app-server` subprocess per workspace.
136
+ - **Mistral Vibe** (experimental): install and authenticate native Vibe as the server user, with `vibe-acp` on that user's PATH. Kōbō uses ACP v1 and native model/thinking selection (Auto or discovered native values). [Setup, limits and compatibility evidence](./CONFIGURATION.md#mistral-vibe).
134
137
 
135
- Both engines share task tracking, permission modes, the sub-agent panel, and the quota footer. The mapping of Kōbō's four permission modes (`plan` / `bypass` / `strict` / `interactive`) to each engine's native sandbox semantics is in [`CONFIGURATION.md`](./CONFIGURATION.md#permission-modes).
138
+ All three engines share task tracking, memory and managed MCP. Permissions and available controls depend on the engine; Mistral Vibe offers fresh reviews under native permissions (Interactive by default; no guaranteed filesystem read-only access) and excludes Plan, live steering, account/quota controls, managed skills and sub-agent cards. The mapping of Kōbō's four permission modes (`plan` / `bypass` / `strict` / `interactive`) to each engine's native permission semantics is in [`CONFIGURATION.md`](./CONFIGURATION.md#permission-modes).
136
139
 
137
140
  ## Skill suites
138
141
 
@@ -147,7 +150,7 @@ Hono backend, Vue 3 + Quasar PWA, SQLite (WAL) for persistence, WebSocket for li
147
150
  ```
148
151
  src/
149
152
  ├── server/ # Hono backend (routes, services, db, agent orchestrator)
150
- │ ├── services/agent/engines/ # claude-code/ + codex/ engines
153
+ │ ├── services/agent/engines/ # claude-code/ + codex/ + mistral-vibe/ engines
151
154
  │ └── ...
152
155
  ├── client/ # Vue 3 + Quasar PWA
153
156
  ├── mcp-server/ # kobo-tasks MCP server, spawned per workspace
package/SECURITY.md CHANGED
@@ -50,11 +50,16 @@ capabilities. This tool boundary does not sandbox arbitrary code executed by an
50
50
  agent or other integrations running as the same OS account.
51
51
 
52
52
  Git workflow preferences express which automatic actions are wanted. They are not
53
- a sandbox for arbitrary shell commands or external MCP tools. Claude and Codex
54
- permission modes have different enforcement mechanisms; unrestricted access can
53
+ a sandbox for arbitrary shell commands or external MCP tools. Claude Code, Codex
54
+ and Mistral Vibe permission modes have different enforcement mechanisms; unrestricted access can
55
55
  modify files outside a worktree. Provider authentication is separate from Kōbō's
56
56
  network token.
57
57
 
58
+ Mistral Vibe uses native ACP execution modes and has no Plan/read-only mode.
59
+ Review prompts forbid modifications, but native permissions do not guarantee
60
+ filesystem read-only access. Kōbō-managed review tools and memory remain restricted
61
+ to reads, scoped reports and progress. See [Mistral Vibe](./CONFIGURATION.md#mistral-vibe).
62
+
58
63
  ## Data and credentials
59
64
 
60
65
  KOBO_HOME stores conversations, workspaces, backups and settings. Direct integration
@@ -44,7 +44,7 @@ export function initMemorySchema(db) {
44
44
  UNIQUE (scope_id, memory_key),
45
45
  CHECK (
46
46
  (actor_kind = 'human' AND source_workspace_id IS NULL AND source_session_id IS NULL AND source_engine IS NULL AND client_name IS NULL AND transport IS NULL) OR
47
- (actor_kind = 'internal-agent' AND source_engine IN ('claude-code', 'codex') AND client_name IS NULL AND transport IS NULL) OR
47
+ (actor_kind = 'internal-agent' AND source_engine IN ('claude-code', 'codex', 'mistral-vibe') AND client_name IS NULL AND transport IS NULL) OR
48
48
  (actor_kind = 'external-mcp' AND source_workspace_id IS NULL AND source_session_id IS NULL AND source_engine IS NULL AND client_name IS NOT NULL AND transport IN ('http', 'stdio'))
49
49
  )
50
50
  );
@@ -73,7 +73,7 @@ export function initMemorySchema(db) {
73
73
  CHECK ((target_entry_id IS NULL AND base_revision IS NULL) OR (target_entry_id IS NOT NULL AND base_revision IS NOT NULL)),
74
74
  CHECK (
75
75
  (actor_kind = 'human' AND source_workspace_id IS NULL AND source_session_id IS NULL AND source_engine IS NULL AND client_name IS NULL AND transport IS NULL) OR
76
- (actor_kind = 'internal-agent' AND source_engine IN ('claude-code', 'codex') AND client_name IS NULL AND transport IS NULL) OR
76
+ (actor_kind = 'internal-agent' AND source_engine IN ('claude-code', 'codex', 'mistral-vibe') AND client_name IS NULL AND transport IS NULL) OR
77
77
  (actor_kind = 'external-mcp' AND source_workspace_id IS NULL AND source_session_id IS NULL AND source_engine IS NULL AND client_name IS NOT NULL AND transport IN ('http', 'stdio'))
78
78
  )
79
79
  );
@@ -106,7 +106,7 @@ export function initMemorySchema(db) {
106
106
  CHECK (affected_proposals IS NULL OR affected_proposals >= 0),
107
107
  CHECK (
108
108
  (actor_kind = 'human' AND source_workspace_id IS NULL AND source_session_id IS NULL AND source_engine IS NULL AND client_name IS NULL AND transport IS NULL) OR
109
- (actor_kind = 'internal-agent' AND source_engine IN ('claude-code', 'codex') AND client_name IS NULL AND transport IS NULL) OR
109
+ (actor_kind = 'internal-agent' AND source_engine IN ('claude-code', 'codex', 'mistral-vibe') AND client_name IS NULL AND transport IS NULL) OR
110
110
  (actor_kind = 'external-mcp' AND source_workspace_id IS NULL AND source_session_id IS NULL AND source_engine IS NULL AND client_name IS NOT NULL AND transport IN ('http', 'stdio'))
111
111
  )
112
112
  );
@@ -122,7 +122,7 @@ export function initMemorySchema(db) {
122
122
  dispatch_id TEXT NOT NULL UNIQUE,
123
123
  budget_context_id TEXT REFERENCES memory_budget_contexts(id) ON DELETE SET NULL,
124
124
  budget_epoch INTEGER NOT NULL DEFAULT 0 CHECK (budget_epoch >= 0),
125
- engine TEXT NOT NULL CHECK (engine IN ('claude-code', 'codex')),
125
+ engine TEXT NOT NULL CHECK (engine IN ('claude-code', 'codex', 'mistral-vibe')),
126
126
  state TEXT NOT NULL CHECK (state IN ('prepared', 'submitted', 'initialized', 'failed', 'unknown')),
127
127
  entry_revisions_json TEXT NOT NULL,
128
128
  scope_generations_json TEXT NOT NULL,
@@ -144,7 +144,7 @@ export function initMemorySchema(db) {
144
144
  id TEXT PRIMARY KEY,
145
145
  kind TEXT NOT NULL CHECK (kind IN ('internal', 'external')),
146
146
  session_id TEXT REFERENCES agent_sessions(id) ON DELETE CASCADE,
147
- engine TEXT CHECK (engine IS NULL OR engine IN ('claude-code', 'codex')),
147
+ engine TEXT CHECK (engine IS NULL OR engine IN ('claude-code', 'codex', 'mistral-vibe')),
148
148
  conversation_key TEXT CHECK (conversation_key IS NULL OR length(trim(conversation_key)) > 0),
149
149
  native_conversation_id TEXT,
150
150
  external_context_id TEXT,
@@ -2,6 +2,7 @@ import { initAutoLoopFinalReviewSchema } from './auto-loop-final-review-schema.j
2
2
  import { initAutoLoopSchema } from './auto-loop-schema.js';
3
3
  import { initMcpMessageSchema } from './mcp-message-schema.js';
4
4
  import { migrateMemorySchema } from './memory-schema.js';
5
+ import { migrateMistralMemoryEngine } from './mistral-memory-migration.js';
5
6
  import { initProgressBarSchema } from './progress-bar-schema.js';
6
7
  import { initReviewReturnSchema, migrateDurableReviewReturns } from './review-return-schema.js';
7
8
  import { initSchema } from './schema.js';
@@ -812,6 +813,7 @@ export const migrations = [
812
813
  },
813
814
  { version: 53, name: 'workspace-group-message-receipts', migrate: initWorkspaceMessageBatchSchema },
814
815
  { version: 54, name: 'session-progress-bars', migrate: initProgressBarSchema },
816
+ { version: 55, name: 'mistral-vibe-memory-engine', migrate: migrateMistralMemoryEngine },
815
817
  ];
816
818
  /** Current schema version — always equals the highest migration version. */
817
819
  export const SCHEMA_VERSION = migrations.length > 0 ? migrations[migrations.length - 1].version : 1;
@@ -0,0 +1,40 @@
1
+ /** Rebuild only engine CHECK constraints; the migration runner owns the transaction. */
2
+ export function migrateMistralMemoryEngine(db) {
3
+ // Parents precede children so replacement FKs never point at tables being dropped.
4
+ const tables = [
5
+ 'memory_entries',
6
+ 'memory_budget_contexts',
7
+ 'memory_proposals',
8
+ 'memory_operations',
9
+ 'memory_contexts',
10
+ ];
11
+ const definitions = tables.map((name) => ({
12
+ name,
13
+ sql: db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get(name)
14
+ .sql,
15
+ }));
16
+ if (definitions.every(({ sql }) => sql.includes("'mistral-vibe'")))
17
+ return;
18
+ const indexes = tables.flatMap((name) => db
19
+ .prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND tbl_name = ? AND sql IS NOT NULL")
20
+ .all(name));
21
+ const sequence = db
22
+ .prepare("SELECT seq FROM sqlite_sequence WHERE name = 'memory_operations'")
23
+ .safeIntegers()
24
+ .get();
25
+ for (const { name, sql } of definitions) {
26
+ let replacement = sql.replaceAll("'claude-code', 'codex'", "'claude-code', 'codex', 'mistral-vibe'");
27
+ for (const table of tables)
28
+ replacement = replacement.replaceAll(new RegExp(`\\b${table}\\b`, 'g'), `${table}_mistral`);
29
+ db.exec(replacement);
30
+ db.exec(`INSERT INTO ${name}_mistral SELECT * FROM ${name}`);
31
+ }
32
+ for (const name of [...tables].reverse())
33
+ db.exec(`DROP TABLE ${name}`);
34
+ for (const name of tables)
35
+ db.exec(`ALTER TABLE ${name}_mistral RENAME TO ${name}`);
36
+ for (const { sql } of indexes)
37
+ db.exec(sql);
38
+ if (sequence)
39
+ db.prepare("UPDATE sqlite_sequence SET seq = ? WHERE name = 'memory_operations'").run(sequence.seq);
40
+ }
@@ -1,5 +1,6 @@
1
1
  import { Hono } from 'hono';
2
2
  import { listEngines } from '../services/agent/engines/registry.js';
3
+ import { getMistralOptions } from '../services/mistral-options-service.js';
3
4
  /** Hono sub-router exposing `GET /` — the list of registered agent engines with capabilities. */
4
5
  export const enginesRouter = new Hono();
5
6
  enginesRouter.get('/', (c) => c.json(listEngines().map((e) => ({
@@ -7,3 +8,16 @@ enginesRouter.get('/', (c) => c.json(listEngines().map((e) => ({
7
8
  displayName: e.displayName,
8
9
  capabilities: e.capabilities,
9
10
  }))));
11
+ enginesRouter.get('/mistral-vibe/options', async (c) => {
12
+ try {
13
+ return c.json(await getMistralOptions({
14
+ workspaceId: c.req.query('workspaceId'),
15
+ projectPath: c.req.query('projectPath'),
16
+ model: c.req.query('model'),
17
+ agentPermissionMode: c.req.query('agentPermissionMode'),
18
+ }));
19
+ }
20
+ catch (error) {
21
+ return c.json({ error: error instanceof Error ? error.message : 'Native Vibe options unavailable' }, 400);
22
+ }
23
+ });
@@ -4,7 +4,7 @@ const app = new Hono();
4
4
  app.get('/', async (c) => {
5
5
  const engine = c.req.query('engine') ?? 'claude-code';
6
6
  const projectPath = c.req.query('projectPath')?.trim();
7
- if (!['claude-code', 'codex'].includes(engine) ||
7
+ if (!['claude-code', 'codex', 'mistral-vibe'].includes(engine) ||
8
8
  (projectPath && (projectPath.length > 4096 || projectPath.includes('\0')))) {
9
9
  return c.json({ error: 'Invalid environment check request' }, 400);
10
10
  }
@@ -23,7 +23,11 @@ function integrationConfigured(integration, key) {
23
23
  }
24
24
  async function checkCli(binary) {
25
25
  try {
26
- const { stdout } = await execFileAsync(binary, ['--version'], { encoding: 'utf-8', timeout: 2_000 });
26
+ const { stdout } = await execFileAsync(binary, ['--version'], {
27
+ encoding: 'utf-8',
28
+ timeout: 2_000,
29
+ maxBuffer: 64 * 1024,
30
+ });
27
31
  return { available: true, version: stdout.trim() || null };
28
32
  }
29
33
  catch {
@@ -78,7 +82,13 @@ app.get('/report', async (c) => {
78
82
  const db = getDb();
79
83
  const dbPath = getDbPath();
80
84
  const home = getKoboHome();
81
- const cliChecks = Promise.all([checkClaudeCli(), checkCodexCli(), checkCli('gh'), checkCli('glab')]);
85
+ const cliChecks = Promise.all([
86
+ checkClaudeCli(),
87
+ checkCodexCli(),
88
+ checkCli('vibe-acp'),
89
+ checkCli('gh'),
90
+ checkCli('glab'),
91
+ ]);
82
92
  // DB schema version
83
93
  const row = db.prepare('SELECT MAX(version) as v FROM schema_migrations').get();
84
94
  const dbSchemaVersion = row?.v ?? 0;
@@ -170,7 +180,7 @@ app.get('/report', async (c) => {
170
180
  .all();
171
181
  const settingsRow = db.prepare('SELECT COUNT(*) as n FROM workspaces').get();
172
182
  const archivedRow = db.prepare('SELECT COUNT(*) as n FROM workspaces WHERE archived_at IS NOT NULL').get();
173
- const [claudeCli, codexCli, ghCli, glabCli] = await cliChecks;
183
+ const [claudeCli, codexCli, mistralCli, ghCli, glabCli] = await cliChecks;
174
184
  const report = {
175
185
  version: getPackageVersion(),
176
186
  koboHome: home,
@@ -183,6 +193,7 @@ app.get('/report', async (c) => {
183
193
  settings: { schemaVersion: SETTINGS_SCHEMA_VERSION },
184
194
  claudeCli,
185
195
  codexCli,
196
+ mistralCli,
186
197
  forgeCli: { gh: ghCli, glab: glabCli },
187
198
  workspaces: {
188
199
  total: settingsRow.n,
@@ -3,7 +3,7 @@ import { isMemoryMode } from '../../shared/memory.js';
3
3
  import { isValidSkillSuite } from '../../shared/skill-suite-prompts.js';
4
4
  import { isWorkflowPolicy } from '../../shared/workflow-policy.js';
5
5
  import { getDb } from '../db/index.js';
6
- import { getBackendPort } from '../services/agent/orchestrator.js';
6
+ import { getBackendPort, notifyCapacityAvailable } from '../services/agent/orchestrator.js';
7
7
  import { DEFAULT_NOTION_INITIAL_PROMPT, DEFAULT_SENTRY_INITIAL_PROMPT, } from '../services/initial-prompt-template-service.js';
8
8
  import { getMcpConnectionInfo } from '../services/mcp-connection-info-service.js';
9
9
  import { generateToken, getLanUrls } from '../services/network-access-service.js';
@@ -167,7 +167,13 @@ app.put('/global', async (c) => {
167
167
  return c.json({ error: 'Invalid workflowPolicy' }, 400);
168
168
  if (body.memoryMode !== undefined && !isMemoryMode(body.memoryMode))
169
169
  return c.json({ error: 'Invalid memoryMode' }, 400);
170
+ if ('maxConcurrentAgents' in body &&
171
+ (!Number.isSafeInteger(body.maxConcurrentAgents) || body.maxConcurrentAgents < 0))
172
+ return c.json({ error: 'maxConcurrentAgents must be a non-negative safe integer' }, 400);
173
+ const previousLimit = 'maxConcurrentAgents' in body ? settingsService.getGlobalSettings().maxConcurrentAgents : undefined;
170
174
  const updated = settingsService.updateGlobalSettings(body);
175
+ if (previousLimit && (updated.maxConcurrentAgents === 0 || updated.maxConcurrentAgents > previousLimit))
176
+ notifyCapacityAvailable();
171
177
  // The client assigns this response straight into its store, so echoing the
172
178
  // real credentials back would undo the masking on GET the first time
173
179
  // anyone saves anything.
@@ -4,9 +4,10 @@ import { nanoid } from 'nanoid';
4
4
  import { isMergeMethod } from '../../shared/forge-merge.js';
5
5
  import { isWorkflowPolicy, resolveWorkflowPolicy, WORKFLOW_ACTIONS, } from '../../shared/workflow-policy.js';
6
6
  import * as finalReviewService from '../services/auto-loop-final-review-service.js';
7
+ import { preflightMistralConfiguration } from '../services/mistral-options-service.js';
7
8
  import { getReviewReturnStatus } from '../services/review-return-service.js';
8
9
  import { getSearchIndexStatus, searchEvents } from '../services/search-service.js';
9
- import { createSessionHandoff, decideSessionHandoff, getCurrentSessionHandoff, SessionHandoffError, submitSessionHandoff, waitForSessionHandoff, } from '../services/session-handoff-service.js';
10
+ import { createSessionHandoff, decideSessionHandoff, getCurrentSessionHandoff, preflightSessionHandoff, SessionHandoffError, submitSessionHandoff, waitForSessionHandoff, } from '../services/session-handoff-service.js';
10
11
  import { AgentStopError, assertAgentStopped } from '../utils/agent-stop-result.js';
11
12
  import { withGitRepoLock } from '../utils/git-repo-lock.js';
12
13
  import { assertWorkspaceLifecycleAvailable, isWorkspaceLifecycleBusy, workspaceLifecycleReason, } from '../utils/workspace-lifecycle-guard.js';
@@ -18,9 +19,11 @@ import { Hono } from 'hono';
18
19
  import { bodyLimit } from 'hono/body-limit';
19
20
  import { MAX_ATTACHMENT_REQUEST_BYTES } from '../../shared/attachments.js';
20
21
  import { AUTO_LOOP_HARD_RULES, buildAutoLoopGroomingSteps, buildGroomingIntro } from '../../shared/auto-loop-prompts.js';
22
+ import { parseQuestionSelections } from '../../shared/question-selections.js';
21
23
  import { getDb } from '../db/index.js';
22
24
  import { migrationGuard } from '../middleware/migration-guard.js';
23
25
  import { workspaceCreationMaintenance } from '../middleware/workspace-creation-maintenance.js';
26
+ import { assertEngineConfigurationSupported } from '../services/agent/engines/configuration.js';
24
27
  import { listEngines } from '../services/agent/engines/registry.js';
25
28
  import * as agentManager from '../services/agent/orchestrator.js';
26
29
  import * as archiveScriptService from '../services/archive-script-service.js';
@@ -316,7 +319,7 @@ function resolveCreateAgentPermissionMode(bodyValue, _projectPath, globalSetting
316
319
  const global = globalSettings.defaultPermissionModeByEngine?.[engineId];
317
320
  if (isAgentPermissionMode(global))
318
321
  return global;
319
- return 'bypass';
322
+ return engineId === 'mistral-vibe' ? 'interactive' : 'bypass';
320
323
  }
321
324
  /**
322
325
  * Extract the numeric PR/MR number from a forge URL.
@@ -357,7 +360,9 @@ function handoffError(c, error) {
357
360
  }
358
361
  app.post('/:id/session-handoffs', migrationGuard, async (c) => {
359
362
  try {
360
- return c.json({ handoff: createSessionHandoff(c.req.param('id'), await c.req.json()) }, 202);
363
+ const input = await c.req.json();
364
+ await preflightSessionHandoff(c.req.param('id'), input);
365
+ return c.json({ handoff: createSessionHandoff(c.req.param('id'), input) }, 202);
361
366
  }
362
367
  catch (error) {
363
368
  return handoffError(c, error);
@@ -418,7 +423,8 @@ app.post('/:id/switch-engine', migrationGuard, async (c) => {
418
423
  const engine = listEngines().find((item) => item.id === body.engine);
419
424
  if (!engine)
420
425
  return c.json({ error: `Unknown engine '${body.engine}'` }, 400);
421
- const model = typeof body.model === 'string' && engine.capabilities.models.some((item) => item.id === body.model)
426
+ const model = typeof body.model === 'string' &&
427
+ (engine.id === 'mistral-vibe' || engine.capabilities.models.some((item) => item.id === body.model))
422
428
  ? body.model
423
429
  : engine.capabilities.models[0]?.id;
424
430
  if (!model)
@@ -428,6 +434,21 @@ app.post('/:id/switch-engine', migrationGuard, async (c) => {
428
434
  return c.json({ error: `Engine '${body.engine}' does not support the selected permission mode` }, 400);
429
435
  }
430
436
  const effort = typeof body.reasoningEffort === 'string' ? body.reasoningEffort : 'auto';
437
+ try {
438
+ assertEngineConfigurationSupported(engine, {
439
+ model: body.model,
440
+ reasoningEffort: body.reasoningEffort,
441
+ agentPermissionMode: body.agentPermissionMode,
442
+ });
443
+ await preflightMistralConfiguration(engine.id, { workspaceId: workspace.id }, {
444
+ model: body.model,
445
+ reasoningEffort: body.reasoningEffort,
446
+ agentPermissionMode: body.agentPermissionMode,
447
+ });
448
+ }
449
+ catch (error) {
450
+ return c.json({ error: error instanceof Error ? error.message : 'Invalid engine configuration' }, 400);
451
+ }
431
452
  const handoff = typeof body.handoff === 'string' && body.handoff.trim()
432
453
  ? body.handoff.trim().slice(0, 30_000)
433
454
  : await buildEngineHandoff(workspace, workspace.engine, body.engine);
@@ -544,6 +565,48 @@ app.post('/', workspaceCreationMaintenance, migrationGuard, creationBodyLimit, a
544
565
  }
545
566
  }
546
567
  }
568
+ if (body.engine === 'mistral-vibe' &&
569
+ body.agentPermissionMode !== undefined &&
570
+ !isAgentPermissionMode(body.agentPermissionMode))
571
+ return c.json({ error: 'Invalid agentPermissionMode', step: 'validate' }, 400);
572
+ const selectedEngine = listEngines().find((engine) => engine.id === (body.engine ?? 'claude-code'));
573
+ if (selectedEngine) {
574
+ try {
575
+ const mode = resolveCreateAgentPermissionMode(body.agentPermissionMode, body.projectPath, settingsService.getGlobalSettings(), selectedEngine.id);
576
+ assertEngineConfigurationSupported(selectedEngine, {
577
+ model: body.model,
578
+ reasoningEffort: body.reasoningEffort,
579
+ agentPermissionMode: mode,
580
+ });
581
+ await preflightMistralConfiguration(selectedEngine.id, { projectPath: body.worktreePath ?? body.projectPath }, {
582
+ model: body.model,
583
+ reasoningEffort: body.reasoningEffort,
584
+ agentPermissionMode: mode,
585
+ });
586
+ assertEngineConfigurationSupported(selectedEngine, {
587
+ model: body.brainstormModel,
588
+ reasoningEffort: body.brainstormReasoningEffort,
589
+ });
590
+ await preflightMistralConfiguration(selectedEngine.id, { projectPath: body.worktreePath ?? body.projectPath }, {
591
+ model: body.brainstormModel ?? body.model,
592
+ reasoningEffort: body.brainstormReasoningEffort ?? body.reasoningEffort,
593
+ agentPermissionMode: mode,
594
+ });
595
+ if (body.autoLoopFinalReview) {
596
+ const review = finalReviewService.parseFinalReviewConfiguration(body.autoLoopFinalReview);
597
+ await preflightMistralConfiguration(review.engine, { projectPath: body.worktreePath ?? body.projectPath }, {
598
+ ...review,
599
+ agentPermissionMode: 'interactive',
600
+ });
601
+ }
602
+ body.agentPermissionMode = mode;
603
+ }
604
+ catch (error) {
605
+ const message = error instanceof Error ? error.message : 'Invalid engine configuration';
606
+ emitCreateFailed(creationId, 'validate', message);
607
+ return c.json({ error: message, step: 'validate' }, 400);
608
+ }
609
+ }
547
610
  // Fetch the source branch from origin first. On failure this is non-fatal:
548
611
  // fall back to the local source branch if it exists (offline / no remote),
549
612
  // and only hard-block (422) when neither origin nor a local branch is usable.
@@ -1745,7 +1808,10 @@ app.get('/:id/auto-loop/final-review', (c) => {
1745
1808
  app.patch('/:id/auto-loop/final-review', migrationGuard, async (c) => {
1746
1809
  try {
1747
1810
  const body = await c.req.json();
1748
- return c.json(finalReviewService.configureFinalReview(c.req.param('id'), body?.configuration));
1811
+ const config = finalReviewService.parseFinalReviewConfiguration(body?.configuration);
1812
+ if (config)
1813
+ await preflightMistralConfiguration(config.engine, { workspaceId: c.req.param('id') }, { ...config, agentPermissionMode: 'interactive' });
1814
+ return c.json(finalReviewService.configureFinalReview(c.req.param('id'), config));
1749
1815
  }
1750
1816
  catch (error) {
1751
1817
  return c.json({ error: error instanceof Error ? error.message : 'Invalid request' }, error instanceof ReviewRequestError ? error.status : 400);
@@ -2080,6 +2146,7 @@ app.post('/:id/deferred-tool-use/answer', async (c) => {
2080
2146
  await agentManager.answerPendingQuestion(id, body.answers, body.toolCallId, {
2081
2147
  awaitingFreeForm: body.awaitingFreeForm === true,
2082
2148
  ...(typeof body.response === 'string' ? { response: body.response } : {}),
2149
+ ...(body.selections !== undefined ? { selections: parseQuestionSelections(body.selections) } : {}),
2083
2150
  });
2084
2151
  return c.json({ ok: true });
2085
2152
  }
@@ -2871,6 +2938,25 @@ app.patch('/:id', migrationGuard, async (c) => {
2871
2938
  }, 400);
2872
2939
  }
2873
2940
  }
2941
+ const selectedEngine = listEngines().find((engine) => engine.id === workspace.engine);
2942
+ if (selectedEngine &&
2943
+ (body.model !== undefined || body.reasoningEffort !== undefined || body.agentPermissionMode !== undefined)) {
2944
+ try {
2945
+ assertEngineConfigurationSupported(selectedEngine, {
2946
+ model: body.model ?? workspace.model,
2947
+ reasoningEffort: body.reasoningEffort ?? workspace.reasoningEffort,
2948
+ agentPermissionMode: body.agentPermissionMode ?? workspace.agentPermissionMode,
2949
+ });
2950
+ await preflightMistralConfiguration(selectedEngine.id, { workspaceId: workspace.id }, {
2951
+ model: body.model ?? workspace.model,
2952
+ reasoningEffort: body.reasoningEffort ?? workspace.reasoningEffort,
2953
+ agentPermissionMode: body.agentPermissionMode ?? workspace.agentPermissionMode,
2954
+ });
2955
+ }
2956
+ catch (error) {
2957
+ return c.json({ error: error instanceof Error ? error.message : 'Invalid engine configuration' }, 400);
2958
+ }
2959
+ }
2874
2960
  if (body.status !== undefined) {
2875
2961
  if (!isWorkspaceStatus(body.status)) {
2876
2962
  return c.json({ error: `Invalid status. Must be one of: ${VALID_WORKSPACE_STATUSES.join(', ')}` }, 400);
@@ -3415,6 +3501,24 @@ app.post('/:id/start', migrationGuard, async (c) => {
3415
3501
  error: 'Workspace is compacting its context; wait until compaction finishes before sending a message',
3416
3502
  }, 409);
3417
3503
  }
3504
+ const selectedEngine = listEngines().find((engine) => engine.id === workspaceEngine);
3505
+ if (selectedEngine) {
3506
+ try {
3507
+ assertEngineConfigurationSupported(selectedEngine, {
3508
+ model: workspace.model,
3509
+ reasoningEffort: workspace.reasoningEffort,
3510
+ agentPermissionMode: workspace.agentPermissionMode,
3511
+ });
3512
+ await preflightMistralConfiguration(selectedEngine.id, { workspaceId: workspace.id }, {
3513
+ model: workspace.model,
3514
+ reasoningEffort: workspace.reasoningEffort,
3515
+ agentPermissionMode: workspace.agentPermissionMode,
3516
+ }, resume);
3517
+ }
3518
+ catch (error) {
3519
+ return c.json({ error: error instanceof Error ? error.message : 'Invalid engine configuration' }, 400);
3520
+ }
3521
+ }
3418
3522
  // A manual start supersedes any scheduled retry (quota or transient
3419
3523
  // watchdog recovery). Leaving its timer armed could spawn a second agent
3420
3524
  // after the user has already resumed the workspace.
@@ -4258,7 +4362,8 @@ app.post('/:id/merge-pr', async (c) => {
4258
4362
  code: `forge_${availability.reason ?? 'unavailable'}`,
4259
4363
  }, 409);
4260
4364
  }
4261
- const snapshot = await provider.getPrStatus(workspace.worktreePath, workspace.workingBranch);
4365
+ // Record OPEN before merging, even if the watcher has not run since startup.
4366
+ const snapshot = await refreshPrSnapshot(id);
4262
4367
  if (snapshot?.state !== 'OPEN') {
4263
4368
  return c.json({ error: 'No open PR found for this workspace', code: 'pr_not_open' }, 409);
4264
4369
  }
@@ -4302,13 +4407,15 @@ app.post('/:id/delete-remote-branch', async (c) => {
4302
4407
  if (!provider.capabilities.canDeleteRemoteBranch) {
4303
4408
  return c.json({ error: 'This project cannot delete remote branches', code: 'forge_unsupported' }, 409);
4304
4409
  }
4305
- const availability = await provider.isAvailable(workspace.worktreePath);
4410
+ // The merge watcher may have already archived and purged this worktree.
4411
+ const repoPath = workspace.projectPath;
4412
+ const availability = await provider.isAvailable(repoPath);
4306
4413
  if (!availability.available)
4307
4414
  return c.json({ error: 'Forge CLI unavailable', code: 'forge_unavailable' }, 409);
4308
- const snapshot = await provider.getPrStatus(workspace.worktreePath, workspace.workingBranch);
4415
+ const snapshot = await provider.getPrStatus(repoPath, workspace.workingBranch);
4309
4416
  if (snapshot?.state !== 'MERGED')
4310
4417
  return c.json({ error: 'The PR is not merged yet', code: 'pr_not_merged' }, 409);
4311
- await provider.deleteRemoteBranch(workspace.worktreePath, workspace.workingBranch);
4418
+ await provider.deleteRemoteBranch(repoPath, workspace.workingBranch);
4312
4419
  return c.json({ success: true });
4313
4420
  }
4314
4421
  catch (err) {
@@ -18,6 +18,8 @@ export const CLAUDE_CODE_CAPABILITIES = {
18
18
  permissionModes: ['plan', 'bypass', 'strict', 'interactive'],
19
19
  supportsResume: true,
20
20
  supportsMcp: true,
21
+ supportsLiveSteering: true,
22
+ supportsReadOnly: true,
21
23
  supportsSkills: true,
22
24
  supportsSubagents: true,
23
25
  supportsQuotaStatus: true,
@@ -1049,6 +1049,9 @@ export function createClaudeCodeEngine() {
1049
1049
  // swallow — best effort
1050
1050
  }
1051
1051
  },
1052
+ hasPendingUserInput(toolCallId) {
1053
+ return pendingResolvers.has(toolCallId);
1054
+ },
1052
1055
  resolvePendingUserInput(toolCallId, response) {
1053
1056
  const resolver = pendingResolvers.get(toolCallId);
1054
1057
  if (!resolver)
@@ -3,7 +3,7 @@ const localRequire = createRequire(import.meta.url);
3
3
  /**
4
4
  * Detect the live host's platform/arch/libc from `process`. `glibcVersionRuntime`
5
5
  * lives in the diagnostic report header on glibc systems and is absent on musl.
6
- * Available since Node 12; Kōbō requires Node ≥ 20 (see AGENTS.md).
6
+ * Available since Node 12; Kōbō requires Node ≥ 26.12.1 (see AGENTS.md).
7
7
  */
8
8
  export function detectPlatform() {
9
9
  const report = process.report;
@@ -13,6 +13,8 @@ export const CODEX_CAPABILITIES = {
13
13
  permissionModes: ['plan', 'bypass', 'strict', 'interactive'],
14
14
  supportsResume: true,
15
15
  supportsMcp: true,
16
+ supportsLiveSteering: true,
17
+ supportsReadOnly: true,
16
18
  supportsSkills: false,
17
19
  supportsSubagents: true,
18
20
  supportsQuotaStatus: true,
@@ -692,6 +692,9 @@ export function createCodexEngine() {
692
692
  }
693
693
  await terminateChild();
694
694
  },
695
+ hasPendingUserInput(callId) {
696
+ return pendingByCallId.has(callId);
697
+ },
695
698
  resolvePendingUserInput(callId, response) {
696
699
  const pending = pendingByCallId.get(callId);
697
700
  if (!pending)
@@ -0,0 +1,14 @@
1
+ import { assertMistralSelection } from './mistral-vibe/configuration.js';
2
+ /** Validate native selections before creating or replacing launch state. */
3
+ export function assertEngineConfigurationSupported(engine, selection) {
4
+ if (selection.readOnly && !engine.capabilities.supportsReadOnly)
5
+ throw new Error(`Engine '${engine.id}' does not support read-only launches`);
6
+ // Native membership is checked contextually and again on the actual ACP session.
7
+ if (engine.id !== 'mistral-vibe')
8
+ return;
9
+ const mode = selection.agentPermissionMode ?? 'interactive';
10
+ if (!engine.capabilities.permissionModes.some((item) => item === mode))
11
+ throw new Error(`Engine '${engine.id}' does not support agentPermissionMode '${String(mode)}'`);
12
+ assertMistralSelection(selection.model, 'model');
13
+ assertMistralSelection(selection.reasoningEffort, 'thinking');
14
+ }
@@ -0,0 +1,13 @@
1
+ import { MISTRAL_MODELS } from '../../../../../shared/mistral-models.js';
2
+ export const MISTRAL_CAPABILITIES = {
3
+ models: MISTRAL_MODELS.map((model) => ({ id: model.id, label: model.label })),
4
+ effortLevels: [{ id: 'auto', label: 'Auto' }],
5
+ permissionModes: ['interactive', 'strict', 'bypass'],
6
+ supportsResume: true,
7
+ supportsMcp: true,
8
+ supportsLiveSteering: false,
9
+ supportsReadOnly: false,
10
+ supportsSkills: false,
11
+ supportsSubagents: false,
12
+ supportsQuotaStatus: false,
13
+ };