@loicngr/kobo 1.7.34 → 1.8.1

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 (205) hide show
  1. package/AGENTS.md +53 -52
  2. package/CHANGELOG.md +12 -1
  3. package/README.md +59 -37
  4. package/dist/mcp-server/kobo-tasks-server.js +4 -0
  5. package/dist/server/index.js +39 -1
  6. package/dist/server/middleware/network-auth-middleware.js +27 -0
  7. package/dist/server/routes/changelog.js +1 -1
  8. package/dist/server/routes/settings.js +49 -0
  9. package/dist/server/routes/workspaces.js +28 -9
  10. package/dist/server/services/agent/engines/claude-code/engine.js +37 -4
  11. package/dist/server/services/agent/engines/claude-code/stop-hook.js +56 -0
  12. package/dist/server/services/agent/orchestrator.js +4 -0
  13. package/dist/server/services/cron-service.js +12 -6
  14. package/dist/server/services/network-access-service.js +67 -0
  15. package/dist/server/services/settings-service.js +50 -1
  16. package/dist/server/services/templates-service.js +80 -11
  17. package/dist/server/services/wakeup-service.js +9 -0
  18. package/dist/server/services/worktree-purge-service.js +2 -2
  19. package/dist/server/services/worktree-service.js +64 -2
  20. package/package.json +7 -7
  21. package/src/client/dist/spa/assets/ActivityFeed-qE7kgNNI.js +8 -0
  22. package/src/client/dist/spa/assets/ChangelogPage-P-cSkc52.js +1 -0
  23. package/src/client/dist/spa/assets/ClosePopup-BWi4AXm0.js +1 -0
  24. package/src/client/dist/spa/assets/CreatePage-CdeIZxPs.js +2 -0
  25. package/src/client/dist/spa/assets/DiffViewer-aEMR55x8.js +8 -0
  26. package/src/client/dist/spa/assets/HealthPage-Oq6n1qu0.js +1 -0
  27. package/src/client/dist/spa/assets/{MainLayout-DtTxmFXf.css → MainLayout-DXz5Vnxf.css} +1 -1
  28. package/src/client/dist/spa/assets/MainLayout-y1VMkOwI.js +37 -0
  29. package/src/client/dist/spa/assets/{QBadge-CIC5n8w7.js → QBadge-DoPfOZ_x.js} +1 -1
  30. package/src/client/dist/spa/assets/{QBanner-BxBEdhfp.js → QBanner-C5hABcB2.js} +1 -1
  31. package/src/client/dist/spa/assets/QChip-xeL4Nxy_.js +1 -0
  32. package/src/client/dist/spa/assets/QExpansionItem-QIQTvw7U.js +1 -0
  33. package/src/client/dist/spa/assets/{QIcon-C6C3QeM4.js → QIcon-CfOEsLsF.js} +1 -1
  34. package/src/client/dist/spa/assets/QInput-7G0OXYP-.js +1 -0
  35. package/src/client/dist/spa/assets/{QList-DRW_oyZ4.js → QList-kGlJAb-p.js} +1 -1
  36. package/src/client/dist/spa/assets/{QPage-C6xc9fOe.js → QPage-B-ixPPKx.js} +1 -1
  37. package/src/client/dist/spa/assets/QScrollArea-cWkysQ9Q.js +1 -0
  38. package/src/client/dist/spa/assets/QSelect-PP69fnCX.js +36 -0
  39. package/src/client/dist/spa/assets/{use-id-By86THzm.js → QSeparator-Vt0kN1I8.js} +1 -1
  40. package/src/client/dist/spa/assets/QSpace-e68xsYE1.js +1 -0
  41. package/src/client/dist/spa/assets/{QSpinnerDots-atHe_AUn.js → QSpinnerDots-BOryc_9y.js} +1 -1
  42. package/src/client/dist/spa/assets/QTooltip-Brh5sChu.js +1 -0
  43. package/src/client/dist/spa/assets/SearchPage-BcXygFyE.js +1 -0
  44. package/src/client/dist/spa/assets/SettingsPage-Co97HPUU.js +16 -0
  45. package/src/client/dist/spa/assets/SettingsPage-DfYhuzv0.css +1 -0
  46. package/src/client/dist/spa/assets/TouchPan-BfqX_pZK.js +1 -0
  47. package/src/client/dist/spa/assets/{WorkspacePage-36QGRRCt.css → WorkspacePage-Bo6aKwl_.css} +1 -1
  48. package/src/client/dist/spa/assets/WorkspacePage-e-ktRS-M.js +4 -0
  49. package/src/client/dist/spa/assets/build-path-tree-D7rqJH7g.js +1 -0
  50. package/src/client/dist/spa/assets/chunk-QTnfLwEv.js +1 -0
  51. package/src/client/dist/spa/assets/{css.worker-BtW9exzf.js → css.worker-CBlhdqTa.js} +31 -31
  52. package/src/client/dist/spa/assets/{cssMode-CdNIff6x.js → cssMode-DwJTukMi.js} +1 -1
  53. package/src/client/dist/spa/assets/documents-eaoEREpp.js +1 -0
  54. package/src/client/dist/spa/assets/{editor.api2-CEAFHkwY.js → editor.api2-CCQ74UFa.js} +163 -163
  55. package/src/client/dist/spa/assets/{editor.main-DuQa2C4S.js → editor.main-sdLzVGjZ.js} +2 -2
  56. package/src/client/dist/spa/assets/editor.worker-Cu3tR8iJ.js +26 -0
  57. package/src/client/dist/spa/assets/expand-template-DTSWVfFm.js +1 -0
  58. package/src/client/dist/spa/assets/formatters-DnqeH9c3.js +1 -0
  59. package/src/client/dist/spa/assets/{freemarker2-DXz2Sr_a.js → freemarker2-DnJdVbFY.js} +1 -1
  60. package/src/client/dist/spa/assets/{handlebars-B6DHJ8jd.js → handlebars-CQ9FsB3q.js} +1 -1
  61. package/src/client/dist/spa/assets/{html-_UvZHIUo.js → html-DbwSidb2.js} +1 -1
  62. package/src/client/dist/spa/assets/{html.worker-Dg1SpGQ4.js → html.worker-BKBrNna1.js} +24 -24
  63. package/src/client/dist/spa/assets/{htmlMode-DZ0ixGc6.js → htmlMode-TsbX8xv5.js} +1 -1
  64. package/src/client/dist/spa/assets/i18n-CrwtYRcs.js +1 -0
  65. package/src/client/dist/spa/assets/index-UPQqj74q.js +84 -0
  66. package/src/client/dist/spa/assets/{javascript-C6Gf4Ys2.js → javascript-CdAxNn6v.js} +1 -1
  67. package/src/client/dist/spa/assets/json.worker-BaLYt9Ss.js +58 -0
  68. package/src/client/dist/spa/assets/{jsonMode-aE8EPidy.js → jsonMode-DsX17jzq.js} +1 -1
  69. package/src/client/dist/spa/assets/kobo-commands-Cu30N0Ht.js +9 -0
  70. package/src/client/dist/spa/assets/layout-DOF1L6Vf.js +1 -0
  71. package/src/client/dist/spa/assets/{liquid-CSp27lUC.js → liquid-Bc7-UBG5.js} +1 -1
  72. package/src/client/dist/spa/assets/{lspLanguageFeatures-UxO-LpRp.js → lspLanguageFeatures-yZgUujPG.js} +1 -1
  73. package/src/client/dist/spa/assets/{mdx-BSCEgQSy.js → mdx-BvDiub4z.js} +1 -1
  74. package/src/client/dist/spa/assets/{monaco.contribution-DqqsS1kc.js → monaco.contribution-CFTqxJMP.js} +2 -2
  75. package/src/client/dist/spa/assets/network-auth-DtdN0iy_.js +1 -0
  76. package/src/client/dist/spa/assets/{permissionModes-CJN6Olox.js → permissionModes-CUZkcBev.js} +1 -1
  77. package/src/client/dist/spa/assets/{python-CA5lSk1U.js → python-DZZeQhwZ.js} +1 -1
  78. package/src/client/dist/spa/assets/{razor-BznM4hXD.js → razor-RaxEa4MG.js} +1 -1
  79. package/src/client/dist/spa/assets/render-chat-markdown-D1XyjSW1.js +66 -0
  80. package/src/client/dist/spa/assets/{ts.worker-DI5g4t5j.js → ts.worker-PmaSgaZk.js} +185 -170
  81. package/src/client/dist/spa/assets/{tsMode-NbGeOvoN.js → tsMode-C6gZAb22.js} +1 -1
  82. package/src/client/dist/spa/assets/{typescript-kHDph5jb.js → typescript-BCAqEI1V.js} +1 -1
  83. package/src/client/dist/spa/assets/{use-checkbox-BnkSQgTJ.js → use-checkbox-DE50asz4.js} +1 -1
  84. package/src/client/dist/spa/assets/{use-onboarding-C4tGLHsr.js → use-onboarding-gBmXW7wm.js} +2 -2
  85. package/src/client/dist/spa/assets/use-quasar-Dyujo9Ue.js +1 -0
  86. package/src/client/dist/spa/assets/{vue.runtime.esm-bundler-BAtKyT0Y.js → vue.runtime.esm-bundler-JZnIeD9D.js} +2 -2
  87. package/src/client/dist/spa/assets/{workers-DFjmWZva.js → workers-okv2EabB.js} +1 -1
  88. package/src/client/dist/spa/assets/{xml-BXzZhhmL.js → xml-iWTTgx9j.js} +1 -1
  89. package/src/client/dist/spa/assets/{yaml-DQNlz7_W.js → yaml-am8-T4BQ.js} +1 -1
  90. package/src/client/dist/spa/index.html +7 -13
  91. package/src/mcp-server/kobo-tasks-server.ts +4 -0
  92. package/src/client/dist/spa/assets/ActivityFeed-COdkQaiZ.js +0 -8
  93. package/src/client/dist/spa/assets/ChangelogPage-BXD8H3j-.js +0 -1
  94. package/src/client/dist/spa/assets/ClosePopup-DD10nToj.js +0 -1
  95. package/src/client/dist/spa/assets/CreatePage-DX4TjLqr.js +0 -2
  96. package/src/client/dist/spa/assets/DiffViewer-BUjVXGyZ.js +0 -8
  97. package/src/client/dist/spa/assets/HealthPage-DM7fvP4v.js +0 -1
  98. package/src/client/dist/spa/assets/MainLayout-CbUZOTwz.js +0 -37
  99. package/src/client/dist/spa/assets/QBtn-DwemGTZv.js +0 -1
  100. package/src/client/dist/spa/assets/QCheckbox-o3UHW596.js +0 -1
  101. package/src/client/dist/spa/assets/QChip-BpS8c1sW.js +0 -1
  102. package/src/client/dist/spa/assets/QExpansionItem-C9vmJqEO.js +0 -1
  103. package/src/client/dist/spa/assets/QInput-CLZtb8E0.js +0 -1
  104. package/src/client/dist/spa/assets/QItemLabel-BYSjzk-t.js +0 -1
  105. package/src/client/dist/spa/assets/QItemSection-O9WBXftL.js +0 -1
  106. package/src/client/dist/spa/assets/QMenu-D_9kEp2i.js +0 -1
  107. package/src/client/dist/spa/assets/QRadio-B_TurTzx.js +0 -1
  108. package/src/client/dist/spa/assets/QScrollArea-B5jf9S4y.js +0 -1
  109. package/src/client/dist/spa/assets/QScrollObserver-Cxj52Zfg.js +0 -1
  110. package/src/client/dist/spa/assets/QSelect-DERXhq6x.js +0 -36
  111. package/src/client/dist/spa/assets/QSpace-CrVsndpV.js +0 -1
  112. package/src/client/dist/spa/assets/QToggle-Dwr3hSLw.js +0 -1
  113. package/src/client/dist/spa/assets/QTooltip-CyRLTG6i.js +0 -1
  114. package/src/client/dist/spa/assets/SearchPage-OaTRqd2Q.js +0 -1
  115. package/src/client/dist/spa/assets/SettingsPage-B7H6sD7r.css +0 -1
  116. package/src/client/dist/spa/assets/SettingsPage-BBk3MB8w.js +0 -9
  117. package/src/client/dist/spa/assets/TouchPan-BmfIMD00.js +0 -1
  118. package/src/client/dist/spa/assets/WorkspacePage-k_qzLoLC.js +0 -4
  119. package/src/client/dist/spa/assets/build-path-tree-D4_LR3mz.js +0 -1
  120. package/src/client/dist/spa/assets/chunk-DtRyYLXJ.js +0 -1
  121. package/src/client/dist/spa/assets/documents-N8PwB_Gh.js +0 -1
  122. package/src/client/dist/spa/assets/editor.worker-DWlYVeeX.js +0 -26
  123. package/src/client/dist/spa/assets/engineFeatures-DChekJQO.js +0 -1
  124. package/src/client/dist/spa/assets/expand-template-BXNt_oWH.js +0 -1
  125. package/src/client/dist/spa/assets/formatters-wq5wP2If.js +0 -1
  126. package/src/client/dist/spa/assets/i18n-DXMuiAyu.js +0 -1
  127. package/src/client/dist/spa/assets/index-CNin8jPU.js +0 -82
  128. package/src/client/dist/spa/assets/json.worker-CCzEOxDx.js +0 -58
  129. package/src/client/dist/spa/assets/kobo-commands-Ip0ObeCE.js +0 -9
  130. package/src/client/dist/spa/assets/notifications-C4MxuXC7.js +0 -1
  131. package/src/client/dist/spa/assets/render-chat-markdown-BJsZCnSw.js +0 -66
  132. package/src/client/dist/spa/assets/touch-yfnu5R3D.js +0 -1
  133. package/src/client/dist/spa/assets/use-quasar-DcJRs0ay.js +0 -1
  134. package/src/client/dist/spa/assets/vue-i18n-C5Tx4bGk.js +0 -3
  135. /package/src/client/dist/spa/assets/{_plugin-vue_export-helper-BzmG9fMN.js → _plugin-vue_export-helper-BDNMzG2s.js} +0 -0
  136. /package/src/client/dist/spa/assets/{abap-DiwvWnMr.js → abap-08VXUWAP.js} +0 -0
  137. /package/src/client/dist/spa/assets/{apex-CmtZjKlf.js → apex-BWPQTe0t.js} +0 -0
  138. /package/src/client/dist/spa/assets/{azcli-DL2My_i-.js → azcli-Bc_sGQ0U.js} +0 -0
  139. /package/src/client/dist/spa/assets/{bat-B-nC98wG.js → bat-i0X4ZdIN.js} +0 -0
  140. /package/src/client/dist/spa/assets/{bicep-Ju5MwOgh.js → bicep-B5-_aFwp.js} +0 -0
  141. /package/src/client/dist/spa/assets/{cameligo-8Eu1TyBr.js → cameligo-DMUM7wLl.js} +0 -0
  142. /package/src/client/dist/spa/assets/{clojure-u-RpMkH3.js → clojure-Cm7r79vr.js} +0 -0
  143. /package/src/client/dist/spa/assets/{coffee-CdA7bbTe.js → coffee-Ba7i2nA0.js} +0 -0
  144. /package/src/client/dist/spa/assets/{cpp-CzNFP8ks.js → cpp-C7h46wYY.js} +0 -0
  145. /package/src/client/dist/spa/assets/{csharp-j1LThmcE.js → csharp-BKxtCVv1.js} +0 -0
  146. /package/src/client/dist/spa/assets/{csp-CLRC61y6.js → csp-bTuwJoIa.js} +0 -0
  147. /package/src/client/dist/spa/assets/{css-r6rC_7P2.js → css-DIMkf-bt.js} +0 -0
  148. /package/src/client/dist/spa/assets/{cypher-CW08XVUh.js → cypher-CVaqCwHa.js} +0 -0
  149. /package/src/client/dist/spa/assets/{dart-Cs9aL5T_.js → dart-onAF5SnQ.js} +0 -0
  150. /package/src/client/dist/spa/assets/{dockerfile-BWM0M184.js → dockerfile-DZFCIeNp.js} +0 -0
  151. /package/src/client/dist/spa/assets/{ecl-MJJuer5P.js → ecl-D05T4iGw.js} +0 -0
  152. /package/src/client/dist/spa/assets/{elixir-D2AIuXqn.js → elixir-6RTg0lbw.js} +0 -0
  153. /package/src/client/dist/spa/assets/{flow9-B2H24giC.js → flow9-C5_-GSwl.js} +0 -0
  154. /package/src/client/dist/spa/assets/{fsharp-CFNadkg7.js → fsharp-C8Ef5oNN.js} +0 -0
  155. /package/src/client/dist/spa/assets/{go-dSur1iB2.js → go-C-y9NEjX.js} +0 -0
  156. /package/src/client/dist/spa/assets/{graphql-qyhAo11d.js → graphql-fmXr3nnJ.js} +0 -0
  157. /package/src/client/dist/spa/assets/{hcl-DFzjMyzm.js → hcl-CpzslTdj.js} +0 -0
  158. /package/src/client/dist/spa/assets/{ini-TdzA8TIl.js → ini-sBoK_t0W.js} +0 -0
  159. /package/src/client/dist/spa/assets/{java-CSGA9pkE.js → java-BEtHBSE6.js} +0 -0
  160. /package/src/client/dist/spa/assets/{julia-9izz5OsY.js → julia-Bri6UV-V.js} +0 -0
  161. /package/src/client/dist/spa/assets/{kotlin-DIUPrqKg.js → kotlin-BOotOW0E.js} +0 -0
  162. /package/src/client/dist/spa/assets/{less-B8d93iCg.js → less-B9JPFI3C.js} +0 -0
  163. /package/src/client/dist/spa/assets/{lexon-DWtEIyu7.js → lexon-CfSJPG6W.js} +0 -0
  164. /package/src/client/dist/spa/assets/{lua-Ciq0OGgt.js → lua-CsQS60Ue.js} +0 -0
  165. /package/src/client/dist/spa/assets/{m3-Cki6JWj_.js → m3-D-oSqn_W.js} +0 -0
  166. /package/src/client/dist/spa/assets/{markdown-Cu47xwU0.js → markdown-Cimd5fb3.js} +0 -0
  167. /package/src/client/dist/spa/assets/{mips-BM8ui995.js → mips-CIPQ_RoX.js} +0 -0
  168. /package/src/client/dist/spa/assets/{msdax-DqLio0_c.js → msdax-DauUninz.js} +0 -0
  169. /package/src/client/dist/spa/assets/{mysql-v1wbjJOq.js → mysql-SOo6toE5.js} +0 -0
  170. /package/src/client/dist/spa/assets/{objective-c-CQl3PGSB.js → objective-c-FvmIjYaQ.js} +0 -0
  171. /package/src/client/dist/spa/assets/{pascal-D4iW0ZtD.js → pascal-DrH0SRf2.js} +0 -0
  172. /package/src/client/dist/spa/assets/{pascaligo-BdC9CZdj.js → pascaligo-D-ptJ9y-.js} +0 -0
  173. /package/src/client/dist/spa/assets/{perl-BL10m4XD.js → perl-oz_6vUea.js} +0 -0
  174. /package/src/client/dist/spa/assets/{pgsql-Be_oqVo3.js → pgsql-DTj74zXo.js} +0 -0
  175. /package/src/client/dist/spa/assets/{php-BtvXSFRI.js → php-nr791fC2.js} +0 -0
  176. /package/src/client/dist/spa/assets/{pla-B2vUy15C.js → pla-CopQ2nXW.js} +0 -0
  177. /package/src/client/dist/spa/assets/{postiats-CbmTTfXr.js → postiats-43DmfD33.js} +0 -0
  178. /package/src/client/dist/spa/assets/{powerquery-DszLhJGx.js → powerquery-D3hlyOfw.js} +0 -0
  179. /package/src/client/dist/spa/assets/{powershell-B0dYktF6.js → powershell-DmHpPYUd.js} +0 -0
  180. /package/src/client/dist/spa/assets/{protobuf-CZvaj1VX.js → protobuf-C531GsRP.js} +0 -0
  181. /package/src/client/dist/spa/assets/{pug-CPDx1B3S.js → pug-Z5eAx3Zn.js} +0 -0
  182. /package/src/client/dist/spa/assets/{qsharp-CDP9TFLl.js → qsharp-DkqhCAOL.js} +0 -0
  183. /package/src/client/dist/spa/assets/{r-8DbbFX2l.js → r-BwWrilGY.js} +0 -0
  184. /package/src/client/dist/spa/assets/{redis-DRWj9MtJ.js → redis-ClamHrr6.js} +0 -0
  185. /package/src/client/dist/spa/assets/{redshift-C6cElE_5.js → redshift-DT7zqm-g.js} +0 -0
  186. /package/src/client/dist/spa/assets/{restructuredtext-W9pS9n3m.js → restructuredtext-BYgofb2h.js} +0 -0
  187. /package/src/client/dist/spa/assets/{ruby-BKnzWnk-.js → ruby-DezsRK8O.js} +0 -0
  188. /package/src/client/dist/spa/assets/{rust-YPCclWwe.js → rust-DdL9SqIa.js} +0 -0
  189. /package/src/client/dist/spa/assets/{sb-BgM4DTFb.js → sb-CcwsVR0C.js} +0 -0
  190. /package/src/client/dist/spa/assets/{scala-fz1OPLMl.js → scala-DHpiXF5c.js} +0 -0
  191. /package/src/client/dist/spa/assets/{scheme-8Uz1RIbu.js → scheme-BeGwcela.js} +0 -0
  192. /package/src/client/dist/spa/assets/{scss-Djo3IYXr.js → scss-gp-XZpBa.js} +0 -0
  193. /package/src/client/dist/spa/assets/{shell-CINF5Tx_.js → shell-CC2rA5mh.js} +0 -0
  194. /package/src/client/dist/spa/assets/{solidity-GgiNEuUm.js → solidity-BEEn4gHE.js} +0 -0
  195. /package/src/client/dist/spa/assets/{sophia-Culj97P9.js → sophia-CRfGWb83.js} +0 -0
  196. /package/src/client/dist/spa/assets/{sparql-C2ZlpxOY.js → sparql-D_Lu-MrJ.js} +0 -0
  197. /package/src/client/dist/spa/assets/{sql-BEf5Pg7Y.js → sql-NEE52Syq.js} +0 -0
  198. /package/src/client/dist/spa/assets/{st-CT6UUoeH.js → st-DbInun42.js} +0 -0
  199. /package/src/client/dist/spa/assets/{swift-B5g0xTG3.js → swift-Bxkupp3x.js} +0 -0
  200. /package/src/client/dist/spa/assets/{systemverilog-CEgQz9DR.js → systemverilog-Bz4Y3fRF.js} +0 -0
  201. /package/src/client/dist/spa/assets/{tcl-D0qL2L0I.js → tcl-DISqw1ZD.js} +0 -0
  202. /package/src/client/dist/spa/assets/{twig-BFUAVf1E.js → twig-De2hgUGE.js} +0 -0
  203. /package/src/client/dist/spa/assets/{typespec-CjVVcNKm.js → typespec-B8J7ngcE.js} +0 -0
  204. /package/src/client/dist/spa/assets/{vb-CZJr-DQz.js → vb-DV3o63ZY.js} +0 -0
  205. /package/src/client/dist/spa/assets/{wgsl-ivoXUo2e.js → wgsl-DpFanUEy.js} +0 -0
package/AGENTS.md CHANGED
@@ -4,21 +4,21 @@ Guidance for AI coding agents (Claude Code, Cursor, etc.) working on this reposi
4
4
 
5
5
  ## What this project is
6
6
 
7
- **Kōbō** (工房 Japanese for "workshop") orchestrates multiple Claude Code agents across isolated git worktrees. Each "workspace" is a self-contained mission with its own worktree, branch, Claude session, optional dev server, optional Notion source-of-truth, and a dedicated MCP tools server. A Vue 3 UI lets the human track progress, read live agent output, and manage the lifecycle.
7
+ **Kōbō** (工房, Japanese for "workshop") orchestrates multiple Claude Code agents across isolated git worktrees. Each "workspace" is a self-contained mission with its own worktree, branch, Claude session, optional dev server, optional Notion source-of-truth, and a dedicated MCP tools server. A Vue 3 UI lets the human track progress, read live agent output, and manage the lifecycle.
8
8
 
9
- Single-user, single-machine dev tool. No auth, no multi-tenant concerns.
9
+ Single-user dev tool, local by default. The server binds `127.0.0.1` unless the opt-in "network access" setting is enabled, which binds all interfaces and gates non-loopback HTTP/WS requests behind a shared token (localhost always exempt). No multi-tenant concerns.
10
10
 
11
11
  ## Tech stack
12
12
 
13
- **Backend** Node.js ≥ 20, Hono (HTTP), `ws` (WebSocket), better-sqlite3 (WAL mode), nanoid, `@modelcontextprotocol/sdk`. TypeScript throughout, `tsx` for dev, `tsc` for production build.
13
+ **Backend**: Node.js ≥ 20, Hono (HTTP), `ws` (WebSocket), better-sqlite3 (WAL mode), nanoid, `@modelcontextprotocol/sdk`. TypeScript throughout, `tsx` for dev, `tsc` for production build.
14
14
 
15
- **Frontend** Vue 3, Quasar 2, Pinia, vue-router, marked + dompurify for markdown rendering. Vite via `@quasar/app-vite`.
15
+ **Frontend**: Vue 3, Quasar 2, Pinia, vue-router, marked + dompurify for markdown rendering. Vite via `@quasar/app-vite`.
16
16
 
17
- **Database** Single SQLite file under the **Kōbō home directory** (`~/.config/kobo/kobo.db` by default, overridable via `KOBO_HOME`). Fresh-install schema lives in `src/server/db/schema.ts` (`initSchema`); incremental migrations live in `src/server/db/migrations.ts`. **The project is in production** every schema change MUST ship as a migration that preserves data, never as a breaking change to `initSchema` alone. See [Database migrations](#database-migrations) below.
17
+ **Database**: a single SQLite file under the **Kōbō home directory** (`~/.config/kobo/kobo.db` by default, overridable via `KOBO_HOME`). Fresh-install schema lives in `src/server/db/schema.ts` (`initSchema`); incremental migrations live in `src/server/db/migrations.ts`. **The project is in production**, so every schema change MUST ship as a migration that preserves data, never as a breaking change to `initSchema` alone. See [Database migrations](#database-migrations) below.
18
18
 
19
- **Kōbō home directory** `KOBO_HOME` env var overrides everything. Otherwise `$XDG_CONFIG_HOME/kobo/`, else `~/.config/kobo/`. Contains `kobo.db`, `settings.json`, `skills.json`, `templates.json`. **Development uses `./data/`** via the `KOBO_HOME=./data` prefix in the `dev` npm script, so local dev never touches your real `~/.config/kobo/` and can run in parallel with a production-installed Kōbō (`npx @loicngr/kobo`). See `src/server/utils/paths.ts`.
19
+ **Kōbō home directory**: `KOBO_HOME` env var overrides everything. Otherwise `$XDG_CONFIG_HOME/kobo/`, else `~/.config/kobo/`. Contains `kobo.db`, `settings.json`, `skills.json`, `templates.json`. **Development uses `./data/`** via the `KOBO_HOME=./data` prefix in the `dev` npm script, so local dev never touches your real `~/.config/kobo/` and can run in parallel with a production-installed Kōbō (`npx @loicngr/kobo`). See `src/server/utils/paths.ts`.
20
20
 
21
- **Tests** vitest (24 backend test files, 544+ tests; 5 frontend test files, 45+ tests at time of writing). Frontend tests cover Pinia stores and pure utility modules; Vue components are not tested (type-check + manual smoke only).
21
+ **Tests**: vitest (24 backend test files, 544+ tests; 5 frontend test files, 45+ tests at time of writing). Frontend tests cover Pinia stores and pure utility modules; Vue components are not tested (type-check + manual smoke only).
22
22
 
23
23
  ## Commands
24
24
 
@@ -100,16 +100,16 @@ src/
100
100
 
101
101
  | Table | Purpose |
102
102
  |---|---|
103
- | `workspaces` | the unit of work id, name, project_path, source_branch, working_branch, status, notion_url, model, dev_server_status, `archived_at`, `worktree_purged_at`, `worktree_purge_restore_data` (JSON), `auto_loop`, `auto_loop_ready`, `no_progress_streak`, timestamps |
104
- | `tasks` | workspace sub-items title, status, `is_acceptance_criterion`, sort_order; CASCADE DELETE on workspace |
105
- | `agent_sessions` | Claude Code CLI invocations pid, `claude_session_id`, status, started_at, ended_at, `name` |
106
- | `ws_events` | persisted WebSocket events for replay on reconnect type, payload, session_id, created_at |
107
- | `pending_wakeups` | one-row-per-workspace scheduler for the `ScheduleWakeup` tool target_at (ISO UTC), prompt, reason; CASCADE DELETE on workspace |
108
- | `workspace_chat_history` | chat-input history per workspace message text + `created_at`, ordered by autoincrement id, capped at 200 entries by the service; CASCADE DELETE on workspace |
103
+ | `workspaces` | the unit of work: id, name, project_path, source_branch, working_branch, status, notion_url, model, dev_server_status, `archived_at`, `worktree_purged_at`, `worktree_purge_restore_data` (JSON), `auto_loop`, `auto_loop_ready`, `no_progress_streak`, timestamps |
104
+ | `tasks` | workspace sub-items: title, status, `is_acceptance_criterion`, sort_order; CASCADE DELETE on workspace |
105
+ | `agent_sessions` | Claude Code CLI invocations: pid, `claude_session_id`, status, started_at, ended_at, `name` |
106
+ | `ws_events` | persisted WebSocket events for replay on reconnect: type, payload, session_id, created_at |
107
+ | `pending_wakeups` | one-row-per-workspace scheduler for the `ScheduleWakeup` tool: target_at (ISO UTC), prompt, reason; CASCADE DELETE on workspace |
108
+ | `workspace_chat_history` | chat-input history per workspace: message text + `created_at`, ordered by autoincrement id, capped at 200 entries by the service; CASCADE DELETE on workspace |
109
109
 
110
110
  `status` enum: `created | extracting | brainstorming | executing | completed | idle | error | quota`. Transitions are validated in `updateWorkspaceStatus` against `VALID_TRANSITIONS`.
111
111
 
112
- `archived_at` is **orthogonal** to `status` archiving is a visibility flag, not a lifecycle state. Unarchive restores the exact pre-archive `status`.
112
+ `archived_at` is **orthogonal** to `status`. Archiving is a visibility flag, not a lifecycle state. Unarchive restores the exact pre-archive `status`.
113
113
 
114
114
  `worktree_purged_at` + `worktree_purge_restore_data` drive the disk-space purge feature (see [Worktree purge](#worktree-purge) below): when set, the workspace's worktree folder has been removed from disk but the chat history is preserved. `worktree_purge_restore_data` is a JSON blob (`{ prNumber, prUrl, forge, mergeCommitSha, originalWorktreePath, originalSourceBranch, originalWorkingBranch }`) captured at purge time for future "Restore" UX. Both fields are cleared automatically by the pr-watcher when the worktree folder reappears on disk.
115
115
 
@@ -121,8 +121,8 @@ src/
121
121
 
122
122
  ### The two files and their roles
123
123
 
124
- - **`src/server/db/schema.ts`** `initSchema(db)` is the source of truth for **fresh installs only**. It creates every table at its current shape. New installations (empty `data/` directory) run `initSchema` once and land at the latest `SCHEMA_VERSION`.
125
- - **`src/server/db/migrations.ts`** `runMigrations(db)` reads the current `version` from the `schema_version` table and sequentially applies every pending migration block up to `SCHEMA_VERSION`. Existing databases upgrade through this path.
124
+ - **`src/server/db/schema.ts`**: `initSchema(db)` is the source of truth for **fresh installs only**. It creates every table at its current shape. New installations (empty `data/` directory) run `initSchema` once and land at the latest `SCHEMA_VERSION`.
125
+ - **`src/server/db/migrations.ts`**: `runMigrations(db)` reads the current `version` from the `schema_version` table and sequentially applies every pending migration block up to `SCHEMA_VERSION`. Existing databases upgrade through this path.
126
126
 
127
127
  Both files must be kept in sync: after adding a migration, update `initSchema` so fresh installs get the same final shape without replaying migrations.
128
128
 
@@ -138,13 +138,14 @@ Every feature that touches the schema:
138
138
  - A database at the previous version can be upgraded without data loss
139
139
  - The new version matches `SCHEMA_VERSION`
140
140
  - Fresh installs and upgraded installs converge to the same schema
141
- 6. Never edit or reorder migration blocks that have already shipped they are historical. If you need to fix a mistake, add a new migration.
141
+ 6. Never edit or reorder migration blocks that have already shipped; they are historical. If you need to fix a mistake, add a new migration.
142
142
 
143
143
  ### Rules
144
144
 
145
145
  - **Migrations are append-only.** Shipped migration blocks are frozen. Fixes go in new migrations.
146
146
  - **Always idempotent where possible.** Use `IF NOT EXISTS`, check for column existence before altering, etc. Prefer migrations that can be safely re-run.
147
147
  - **`ALTER TABLE ADD COLUMN` is safe in SQLite** (even on large tables). For more invasive changes (rename, drop, change type), use the [12-step SQLite pattern](https://sqlite.org/lang_altertable.html#otheralter) within a transaction.
148
+
148
149
  - **Run migrations on every backend start.** `runMigrations(db)` is called from `getDb()` via `src/server/db/index.ts`.
149
150
  - **Test upgrades, not just fresh installs.** The `migrations.test.ts` suite must exercise "old DB → new DB" paths.
150
151
 
@@ -159,26 +160,26 @@ Clients subscribe to individual workspace ids. The server sends `WsEvent` object
159
160
  Common types: `agent:output`, `agent:status`, `agent:error`, `user:message`, `task:updated`, `devserver:status`, `workspace:archived`, `workspace:unarchived`, `sync:response`.
160
161
 
161
162
  Two emit flavors in `websocket-service.ts`:
162
- - `emit(workspaceId, type, payload)` persists to `ws_events` for later replay via `sync:request` on reconnect
163
- - `emitEphemeral(workspaceId, type, payload)` delivered once, never persisted. Use for lifecycle events (archive, status changes) that shouldn't replay.
163
+ - `emit(workspaceId, type, payload)` persists to `ws_events` for later replay via `sync:request` on reconnect
164
+ - `emitEphemeral(workspaceId, type, payload)` delivers once and never persists. Use it for lifecycle events (archive, status changes) that shouldn't replay.
164
165
 
165
166
  ## External integrations
166
167
 
167
168
  ### Forge providers
168
169
 
169
- `src/server/services/forge/` implements a `ForgeProvider` interface with three concrete providers: `github` (wraps the `gh` CLI), `gitlab` (wraps the `glab` CLI), and `none` (no-op disables PR/MR features cleanly). The public surface is two functions: `getForgeProvider(name)` in `registry.ts` (returns the provider for a named forge) and `resolveForge(projectPath)` in `resolve.ts` (reads the per-project `forge` setting, then falls back to auto-detection from the `origin` remote URL host contains `github.com` → GitHub, host contains `gitlab` → GitLab, otherwise `none`). The per-project `forge` setting (`'auto' | 'github' | 'gitlab' | 'none'`, default `'auto'`) is stored in `settings.json` and seeded by settings migration v32. PR routes (`open-pr`, `change-pr-base`) and the pr-watcher go through the resolved provider. **Kōbō ships no forge credentials** the user must install and authenticate `gh` or `glab` themselves; when the CLI is absent or unauthenticated, PR/MR actions are disabled with a tooltip rather than a raw error.
170
+ `src/server/services/forge/` implements a `ForgeProvider` interface with three concrete providers: `github` (wraps the `gh` CLI), `gitlab` (wraps the `glab` CLI), and `none` (a no-op that disables PR/MR features cleanly). The public surface is two functions: `getForgeProvider(name)` in `registry.ts` (returns the provider for a named forge) and `resolveForge(projectPath)` in `resolve.ts` (reads the per-project `forge` setting, then falls back to auto-detection from the `origin` remote URL: host contains `github.com` → GitHub, host contains `gitlab` → GitLab, otherwise `none`). The per-project `forge` setting (`'auto' | 'github' | 'gitlab' | 'none'`, default `'auto'`) is stored in `settings.json` and seeded by settings migration v32. PR routes (`open-pr`, `change-pr-base`) and the pr-watcher go through the resolved provider. **Kōbō ships no forge credentials**, so the user must install and authenticate `gh` or `glab` themselves; when the CLI is absent or unauthenticated, PR/MR actions are disabled with a tooltip rather than a raw error.
170
171
 
171
172
  ### Notion (opt-in, user-provided credentials)
172
173
 
173
- `notion-service.ts` spawns the official [`@notionhq/notion-mcp-server`](https://github.com/makenotion/notion-mcp-server) as a child process (`npx -y @notionhq/notion-mcp-server`) and talks to it over stdio using JSON-RPC / MCP. **Kōbō ships no Notion credentials** the feature only works if the user has configured their own integration token. The token is resolved in this order:
174
+ `notion-service.ts` spawns the official [`@notionhq/notion-mcp-server`](https://github.com/makenotion/notion-mcp-server) as a child process (`npx -y @notionhq/notion-mcp-server`) and talks to it over stdio using JSON-RPC / MCP. **Kōbō ships no Notion credentials**, so the feature only works if the user has configured their own integration token. The token is resolved in this order:
174
175
 
175
176
  1. `NOTION_API_TOKEN` env var
176
177
  2. `NOTION_TOKEN` env var
177
- 3. `~/.claude.json` → `mcpServers.notion.env.NOTION_TOKEN` / `NOTION_API_TOKEN` (Claude Code's MCP config the recommended path, same token shared with Claude Code)
178
+ 3. `~/.claude.json` → `mcpServers.notion.env.NOTION_TOKEN` / `NOTION_API_TOKEN` (Claude Code's MCP config: the recommended path, the same token shared with Claude Code)
178
179
 
179
180
  The MCP command and args can be overridden via `NOTION_MCP_COMMAND` (default `npx`) and `NOTION_MCP_ARGS` (default `-y @notionhq/notion-mcp-server`) for pinning a specific version or using a fork.
180
181
 
181
- When adding features touching `notion-service.ts`, remember: **no token = no feature**. The rest of Kōbō must keep working if the Notion token is absent only the explicit Notion import endpoints should fail with a clear error. Do not throw at server startup.
182
+ When adding features touching `notion-service.ts`, remember: **no token = no feature**. The rest of Kōbō must keep working if the Notion token is absent; only the explicit Notion import endpoints should fail with a clear error. Do not throw at server startup.
182
183
 
183
184
  See the "Notion integration" section of the README for the end-user setup guide.
184
185
 
@@ -186,16 +187,16 @@ See the "Notion integration" section of the README for the end-user setup guide.
186
187
 
187
188
  Two engines live under `src/server/services/agent/engines/`, both implementing the `AgentEngine` contract in `types.ts`:
188
189
 
189
- **Claude Code** (`claude-code/`) uses `@anthropic-ai/claude-agent-sdk` (in-process async iterator). Spawns no subprocess. Auth via `~/.claude.json` or `ANTHROPIC_API_KEY` env var. The engine arms a **15 s result-drain watchdog** when the SDK emits its `result` message: if the async iterator does not close cleanly within the window, `session:ended` is force-emitted so the orchestrator and auto-loop never hang on a stuck generator. The watchdog is idempotent via a `sessionEndedEmitted` guard and the timer is cleared in `finally`.
190
+ **Claude Code** (`claude-code/`): uses `@anthropic-ai/claude-agent-sdk` (in-process async iterator). Spawns no subprocess. Auth via `~/.claude.json` or `ANTHROPIC_API_KEY` env var. The engine arms a **15 s result-drain watchdog** when the SDK emits its `result` message: if the async iterator does not close cleanly within the window, `session:ended` is force-emitted so the orchestrator and auto-loop never hang on a stuck generator. The watchdog is idempotent via a `sessionEndedEmitted` guard and the timer is cleared in `finally`.
190
191
 
191
- **OpenAI Codex** (`codex/`) uses the **`codex app-server` JSON-RPC protocol** (line-delimited JSON over stdio with a long-lived `codex` subprocess). The engine layers are:
192
- - `jsonrpc/transport.ts` + `jsonrpc/peer.ts` generic JSON-RPC 2.0 stdio peer (request correlation, notifications, server-initiated requests)
193
- - `client.ts` typed `AppServerClient` wrapping the peer (initialize / thread.start / thread.resume / turn.start / turn.interrupt)
194
- - `protocol/types.ts` hand-written subset of the Codex v2 protocol types (camelCase field names `agentMessage`, `commandExecution`, etc.). The full canonical bindings are generated by `codex app-server generate-ts` if the protocol drifts.
195
- - `event-mapper.ts` translates app-server notifications (`item/started`, `item/completed`, `item/agentMessage/delta`, `turn/completed`, `thread/tokenUsage/updated`, `account/rateLimits/updated`, `error`) into Kōbō `AgentEvent` union
196
- - `server-requests.ts` handles server-initiated approval/elicitation requests (`item/commandExecution/requestApproval`, `item/fileChange/requestApproval`, `item/tool/requestUserInput`, `item/permissions/requestApproval`, plus v1 legacy aliases `execCommandApproval` / `applyPatchApproval`)
197
- - `engine.ts` `createCodexEngine()` factory wiring everything into `AgentEngine`
198
- - `spawn.ts` locates the `codex` binary via `@openai/codex` dependency and spawns `codex app-server`
192
+ **OpenAI Codex** (`codex/`): uses the **`codex app-server` JSON-RPC protocol** (line-delimited JSON over stdio with a long-lived `codex` subprocess). The engine layers are:
193
+ - `jsonrpc/transport.ts` + `jsonrpc/peer.ts`: generic JSON-RPC 2.0 stdio peer (request correlation, notifications, server-initiated requests)
194
+ - `client.ts`: typed `AppServerClient` wrapping the peer (initialize / thread.start / thread.resume / turn.start / turn.interrupt)
195
+ - `protocol/types.ts`: hand-written subset of the Codex v2 protocol types (camelCase field names like `agentMessage`, `commandExecution`, etc.). The full canonical bindings are generated by `codex app-server generate-ts` if the protocol drifts.
196
+ - `event-mapper.ts`: translates app-server notifications (`item/started`, `item/completed`, `item/agentMessage/delta`, `turn/completed`, `thread/tokenUsage/updated`, `account/rateLimits/updated`, `error`) into Kōbō `AgentEvent` union
197
+ - `server-requests.ts`: handles server-initiated approval/elicitation requests (`item/commandExecution/requestApproval`, `item/fileChange/requestApproval`, `item/tool/requestUserInput`, `item/permissions/requestApproval`, plus v1 legacy aliases `execCommandApproval` / `applyPatchApproval`)
198
+ - `engine.ts`: `createCodexEngine()` factory wiring everything into `AgentEngine`
199
+ - `spawn.ts`: locates the `codex` binary via `@openai/codex` dependency and spawns `codex app-server`
199
200
 
200
201
  Auth: delegated to the `codex` CLI which reads `OPENAI_API_KEY` from env or `~/.codex/auth.json`. Kōbō ships no Codex credentials. The `@openai/codex` package (binary) is a direct dependency.
201
202
 
@@ -203,12 +204,12 @@ Background: the engine was migrated from `@openai/codex-sdk` (one-shot `codex ex
203
204
 
204
205
  **Protocol gotchas worth remembering** (post-migration findings):
205
206
 
206
- - **`experimentalApi: true` is mandatory in the `initialize` handshake.** Without it, any turn using experimental fields most importantly `turn/start.collaborationMode` is rejected with `-32600: requires experimentalApi capability`. See `client.ts:connect()`.
207
- - **`collaborationMode` is sticky server-side.** Once a turn ran in `mode: 'plan'`, every subsequent turn on the same thread stays in plan until we explicitly send `mode: 'default'` again. The engine therefore always emits the field on `turn/start` never omits it so a Plan → Bypass switch actually takes effect. Mapping: Kōbō `plan` → `plan`, every other Kōbō mode → `default`. Plan mode is the only one that unlocks Codex's internal `request_user_input` tool.
207
+ - **`experimentalApi: true` is mandatory in the `initialize` handshake.** Without it, any turn using experimental fields (most importantly `turn/start.collaborationMode`) is rejected with `-32600: requires experimentalApi capability`. See `client.ts:connect()`.
208
+ - **`collaborationMode` is sticky server-side.** Once a turn ran in `mode: 'plan'`, every subsequent turn on the same thread stays in plan until we explicitly send `mode: 'default'` again. The engine therefore always emits the field on `turn/start`, never omitting it, so a Plan → Bypass switch actually takes effect. Mapping: Kōbō `plan` → `plan`, every other Kōbō mode → `default`. Plan mode is the only one that unlocks Codex's internal `request_user_input` tool.
208
209
  - **Permission mode vs collaboration mode are independent.** Sandbox + approvalPolicy control *what the agent may do at OS level* (read-only / workspace-write, never / on-request / unless-trusted). `collaborationMode` is a separate session-level flag that gates internal Codex behaviour (notably interactive Q&A). Kōbō hides both behind a single "permission mode" selector and maps them together.
209
- - **Sub-agents map to `collabAgentToolCall`.** Codex's analogue of Claude's Task tool is `collabAgentToolCall` (`spawnAgent` / `sendInput` / `resumeAgent` / `wait` / `closeAgent`). The mapper emits **both** a `tool:call` named `Task` (chat card) and a `subagent:progress` event (right-hand panel) per call same dual-emission Claude does. See `event-mapper.ts` `handleItemStarted` / `handleItemCompleted` for the `collabAgentToolCall` branch.
210
+ - **Sub-agents map to `collabAgentToolCall`.** Codex's analogue of Claude's Task tool is `collabAgentToolCall` (`spawnAgent` / `sendInput` / `resumeAgent` / `wait` / `closeAgent`). The mapper emits **both** a `tool:call` named `Task` (chat card) and a `subagent:progress` event (right-hand panel) per call, the same dual-emission Claude does. See `event-mapper.ts` `handleItemStarted` / `handleItemCompleted` for the `collabAgentToolCall` branch.
210
211
  - **`fileChange` items carry a unified-diff blob.** The protocol shape is `{ path, kind: PatchChangeKind, diff: string }` per change; `kind` is a discriminated union, not a string. The mapper flattens the first change into a Claude-style Edit input (`{ file_path, diff, change_kind, move_path? }`) so the existing `ToolCallItem` renderer picks it up. The client parses the unified diff into `DiffLine[]` via `parseUnifiedDiff` in `inline-diff.ts`.
211
- - **Streaming bursts trip auto-scroll.** Codex emits one `message:text` event per token-delta (50-200 per message), versus Claude which emits ~1 per content block. The naive `eventCount` watcher in `ActivityFeed.vue` triggered an animated `scrollToBottom(180)` per event, causing stacked animations and visible jank. The fix coalesces requests through `requestAnimationFrame` and only animates the *first* scroll after a quiet period subsequent scrolls during a burst snap instantly.
212
+ - **Streaming bursts trip auto-scroll.** Codex emits one `message:text` event per token-delta (50-200 per message), versus Claude which emits ~1 per content block. The naive `eventCount` watcher in `ActivityFeed.vue` triggered an animated `scrollToBottom(180)` per event, causing stacked animations and visible jank. The fix coalesces requests through `requestAnimationFrame` and only animates the *first* scroll after a quiet period; subsequent scrolls during a burst snap instantly.
212
213
  - **`MCP tools` need `default_tools_approval_mode: 'auto'` in `config.mcp_servers`.** Without it Codex flags every MCP tool call as needing user approval ("user cancelled MCP tool call"). Kōbō trusts every tool it spawns, so the options-builder pre-approves the namespace.
213
214
 
214
215
  ## Workspace operations
@@ -217,21 +218,21 @@ Background: the engine was migrated from `@openai/codex-sdk` (one-shot `codex ex
217
218
 
218
219
  `change-source-branch-service.ts` re-targets a workspace onto a new source branch. The default path is a cherry-pick of the branch-proper commits (commits in the working branch but in **neither** the old nor the new base), inspired by the sekur `deploy-preprod-rebase.yml` workflow. The route is `POST /api/workspaces/:id/change-source-branch` and returns a discriminated status: `done | aligned | conflict | too-many | dirty`.
219
220
 
220
- - **Built-in cherry-pick** `fetchAllBranches` → `listProperCommits` → `stashPush` (if dirty + aligned) → backup branch (`kobo-backup/<branch>-<unix-ts>`) → `reset --hard origin/<new>` → cherry-pick replay → optional force-push prompt → forge PR-base update via `provider.changePrBase`. Conflicts leave the worktree in a cherry-pick state for the user/agent to resolve via `POST /:id/git/resolve-with-agent`. `GitConflictError` carries an `operation: 'rebase' | 'merge' | 'cherry-pick'` discriminator.
221
- - **Custom bash override** if `effective.changeSourceBranchScript` is non-empty (per-project override or global default), the script **replaces** the built-in flow. Spawned with `bash -c`, cwd = worktree, 5 min timeout, stderr captured (last 8 KB). Exit 0 → Kōbō updates the source-branch metadata; any non-zero exit → the stderr tail is propagated as a clean error. The user-facing menu item only shows when the resolved script is non-empty empty = feature disabled (opt-in).
222
- - **Custom-script env vars** `KOBO_NEW_BASE`, `KOBO_OLD_BASE`, `KOBO_WORKING_BRANCH`, `KOBO_WORKTREE_PATH`, `KOBO_PROJECT_PATH`, `KOBO_PROJECT_NAME`, `KOBO_WORKSPACE_ID`, `KOBO_WORKSPACE_NAME`, `KOBO_FORGE`, `KOBO_PR_NUMBER` (empty when no PR/MR is open). The default script lives in `settings-defaults.ts` and is seeded into `global.changeSourceBranchScript` by settings migration v33; the client reads it through `GET /api/settings/defaults` for the "Reset to Kōbō default" button. See [CONFIGURATION.md → Custom change-source-branch script](CONFIGURATION.md#custom-change-source-branch-script).
221
+ - **Built-in cherry-pick**: `fetchAllBranches` → `listProperCommits` → `stashPush` (if dirty + aligned) → backup branch (`kobo-backup/<branch>-<unix-ts>`) → `reset --hard origin/<new>` → cherry-pick replay → optional force-push prompt → forge PR-base update via `provider.changePrBase`. Conflicts leave the worktree in a cherry-pick state for the user/agent to resolve via `POST /:id/git/resolve-with-agent`. `GitConflictError` carries an `operation: 'rebase' | 'merge' | 'cherry-pick'` discriminator.
222
+ - **Custom bash override**: if `effective.changeSourceBranchScript` is non-empty (per-project override or global default), the script **replaces** the built-in flow. Spawned with `bash -c`, cwd = worktree, 5 min timeout, stderr captured (last 8 KB). Exit 0 → Kōbō updates the source-branch metadata; any non-zero exit → the stderr tail is propagated as a clean error. The user-facing menu item only shows when the resolved script is non-empty; empty means the feature is disabled (opt-in).
223
+ - **Custom-script env vars**: `KOBO_NEW_BASE`, `KOBO_OLD_BASE`, `KOBO_WORKING_BRANCH`, `KOBO_WORKTREE_PATH`, `KOBO_PROJECT_PATH`, `KOBO_PROJECT_NAME`, `KOBO_WORKSPACE_ID`, `KOBO_WORKSPACE_NAME`, `KOBO_FORGE`, `KOBO_PR_NUMBER` (empty when no PR/MR is open). The default script lives in `settings-defaults.ts` and is seeded into `global.changeSourceBranchScript` by settings migration v33; the client reads it through `GET /api/settings/defaults` for the "Reset to Kōbō default" button. See [CONFIGURATION.md → Custom change-source-branch script](CONFIGURATION.md#custom-change-source-branch-script).
223
224
 
224
225
  ### Worktree purge
225
226
 
226
227
  `src/server/services/worktree-purge-service.ts` removes a workspace's worktree from disk while preserving the chat history and PR metadata. Triggered manually via `POST /api/workspaces/:id/purge-worktree` from the workspace context menu, or automatically by the pr-watcher when a PR transitions to MERGED **and** `global.autoPurgeOnPrMerged` is enabled (Settings → Worktrees toggle, settings migration v36).
227
228
 
228
- Sequence: `captureRestoreData` (best-effort forge lookup for PR number / URL / merge SHA) → stop agent + dev server + terminal → `archiveWorkspace` → `removeWorktree` → `markWorktreePurged(restoreData)` → emit `workspace:worktree-purged`. Permission errors on removal (EACCES / EPERM typically Docker-owned files in `node_modules` / `vendor`) are detected via regex on the error message and the warning toast carries a copy-pasteable `sudo rm -rf` + `git worktree prune` recovery command plus prevention tips (Docker `USER` directive, `setfacl` default ACL). See [CONFIGURATION.md → Auto-purge worktree on PR merged](CONFIGURATION.md#auto-purge-worktree-on-pr-merged).
229
+ Sequence: `captureRestoreData` (best-effort forge lookup for PR number / URL / merge SHA) → stop agent + dev server + terminal → `archiveWorkspace` → `removeWorktree` → `markWorktreePurged(restoreData)` → emit `workspace:worktree-purged`. Permission errors on removal (EACCES / EPERM, typically Docker-owned files in `node_modules` / `vendor`) are detected via regex on the error message and the warning toast carries a copy-pasteable `sudo rm -rf` + `git worktree prune` recovery command plus prevention tips (Docker `USER` directive, `setfacl` default ACL). See [CONFIGURATION.md → Auto-purge worktree on PR merged](CONFIGURATION.md#auto-purge-worktree-on-pr-merged).
229
230
 
230
231
  **Auto-restore on manual recreation.** When the user manually recreates the worktree folder (`gh pr checkout <pr-number>` or `git worktree add <path> <branch>`), the pr-watcher detects the folder reappearing on its next 30 s tick via `autoRestoreManuallyRecreatedWorktrees()`: it iterates archived workspaces with `worktreePurgedAt`, checks `fs.existsSync(worktreePath)`, and on a hit calls `restoreWorktreeFromDisk(id)` which clears `worktree_purged_at` + `worktree_purge_restore_data` + `archived_at` in one transaction, then emits `workspace:worktree-restored`. The client websocket store reuses the same handler as `workspace:archived`/`unarchived` to refresh both the active and archived workspace lists. No UI action needed.
231
232
 
232
233
  ### Workspace attention indicators
233
234
 
234
- `src/client/src/utils/workspace-attention.ts` derives a small set of badges (CI failure, changes-requested) from the PR snapshot + git stats stored on each workspace. `WorkspaceAttentionLabels.vue` renders them inline on the workspace cards in the left drawer. The derivation is a pure function easy to unit-test, no IO. Drawer cards therefore stay reactive to whatever the pr-watcher / bulk-info refresh writes back into the store.
235
+ `src/client/src/utils/workspace-attention.ts` derives a small set of badges (CI failure, changes-requested) from the PR snapshot + git stats stored on each workspace. `WorkspaceAttentionLabels.vue` renders them inline on the workspace cards in the left drawer. The derivation is a pure function, easy to unit-test and free of IO. Drawer cards therefore stay reactive to whatever the pr-watcher / bulk-info refresh writes back into the store.
235
236
 
236
237
  ### Bulk workspace info refresh
237
238
 
@@ -245,15 +246,15 @@ The right panel of `DiffViewer.vue` is editable when the workspace agent is stop
245
246
 
246
247
  **Service layer** throws descriptive errors; the route layer catches and maps to HTTP status codes. Error messages follow the pattern `` `Workspace '${id}' not found` `` / `` `... is already archived` ``.
247
248
 
248
- **Route layer** is thin always wrap the handler body in `try / catch` and return `c.json({ error: message }, status)`. Match the existing shape in `src/server/routes/workspaces.ts`.
249
+ **Route layer** is thin: always wrap the handler body in `try / catch` and return `c.json({ error: message }, status)`. Match the existing shape in `src/server/routes/workspaces.ts`.
249
250
 
250
251
  **Swallowed failures** are acceptable (and required) for best-effort side effects like `agentManager.stopAgent` and `devServerService.stopDevServer` during delete/archive. Log with `console.error` and continue. Never let these break the happy path.
251
252
 
252
253
  **Route ordering matters** in Hono. Static paths (`GET /archived`) MUST be declared **before** dynamic segments (`GET /:id`) or the dynamic segment captures them. There's a regression test locking this invariant in `src/__tests__/routes-workspaces.test.ts`.
253
254
 
254
- **File size** prefer focused files. `WorkspaceList.vue` and `workspaces.ts` (routes) are the largest files; don't grow them further without a clear reason. If a file approaches unwieldy, surface it as a concern, don't silently split.
255
+ **File size**: prefer focused files. `WorkspaceList.vue` and `workspaces.ts` (routes) are the largest files; don't grow them further without a clear reason. If a file approaches unwieldy, surface it as a concern rather than silently splitting it.
255
256
 
256
- **Dependencies** root `package.json` covers backend + tests. `src/client/package.json` is a separate npm tree. Install both.
257
+ **Dependencies**: root `package.json` covers backend + tests. `src/client/package.json` is a separate npm tree. Install both.
257
258
 
258
259
  ## Internationalization (i18n)
259
260
 
@@ -270,9 +271,9 @@ The frontend uses `vue-i18n` v10 with 5 supported locales: English (`en`), Frenc
270
271
 
271
272
  ## Testing discipline
272
273
 
273
- - **TDD for backend** write the failing test, confirm it fails for the right reason, implement minimally, confirm it passes, commit. One commit per logical unit. See existing tests in `src/__tests__/workspace-service.test.ts` for the setup pattern (fresh in-memory DB per test via `resetDb()`).
274
- - **Route tests** use `vi.mock()` on service modules before imports (see `src/__tests__/routes-workspaces.test.ts`). Keep mocks complete missing exports cause obscure failures.
275
- - **Frontend tests** cover Pinia stores (`workspace-store`, `settings-store`, `templates-store`) and pure utility modules (`expand-template`). Run with `cd src/client && npm test`. Vue components are **not** tested type-check via `npx tsc --noEmit` + manual smoke testing covers UI behavior.
274
+ - **TDD for backend**: write the failing test, confirm it fails for the right reason, implement minimally, confirm it passes, commit. One commit per logical unit. See existing tests in `src/__tests__/workspace-service.test.ts` for the setup pattern (fresh in-memory DB per test via `resetDb()`).
275
+ - **Route tests** use `vi.mock()` on service modules before imports (see `src/__tests__/routes-workspaces.test.ts`). Keep mocks complete; missing exports cause obscure failures.
276
+ - **Frontend tests** cover Pinia stores (`workspace-store`, `settings-store`, `templates-store`) and pure utility modules (`expand-template`). Run with `cd src/client && npm test`. Vue components are **not** tested: type-check via `npx tsc --noEmit` + manual smoke testing covers UI behavior.
276
277
  - **`beforeEach(() => vi.clearAllMocks())`** is the convention for all route test files.
277
278
 
278
279
  ## Git workflow
@@ -300,7 +301,7 @@ These rules are the source of truth and are also written to `.ai/.git-convention
300
301
  - Never commit directly to `main`/`master`/`develop`
301
302
 
302
303
  **Workflow**
303
- - Rebase on the source branch before opening a PR do not merge it in
304
+ - Rebase on the source branch before opening a PR; do not merge it in
304
305
  - Keep commits atomic and self-contained (each compiles and passes tests)
305
306
  - Squash fixup commits before pushing
306
307
  - Never force-push to shared branches
@@ -319,12 +320,12 @@ The human user of this repository prefers French for conversational exchanges. C
319
320
  Always read `DESIGN.md` (repo root) before making any visual or UI decisions. Every
320
321
  font choice, color, spacing value, and aesthetic decision is defined there. The CSS
321
322
  variables in `src/client/src/css/design-tokens.scss` are the runtime source of truth
322
- for the values documented in `DESIGN.md` never hardcode hex colors or spacing
323
+ for the values documented in `DESIGN.md`; never hardcode hex colors or spacing
323
324
  literals in components.
324
325
 
325
326
  Aesthetic direction: **Brutally Minimal × Industrial** (Linear / Anthropic Console
326
327
  reference). Dark-native, monochrome, single indigo accent (`--kobo-accent` /
327
- `#6c63ff`) used rarely. No purple gradients, no decorative illustrations, no
328
+ `#6c63ff`) used sparingly. No purple gradients, no decorative illustrations, no
328
329
  bubble-pill shapes, no spring physics. Geist + Geist Mono for technical values.
329
330
 
330
331
  When reviewing or writing UI code, flag any deviation from `DESIGN.md`. Do not
@@ -332,10 +333,10 @@ deviate from the documented system without explicit user approval.
332
333
 
333
334
  ## What NOT to do
334
335
 
335
- - Don't drop-and-recreate the database to apply schema changes. The project is in production every schema change ships as a migration that preserves data (see [Database migrations](#database-migrations)).
336
+ - Don't drop-and-recreate the database to apply schema changes. The project is in production, so every schema change ships as a migration that preserves data (see [Database migrations](#database-migrations)).
336
337
  - Don't edit or reorder migration blocks that have already shipped. Migrations are append-only; fixes go in new migrations.
337
338
  - Don't add confirmation dialogs for reversible actions (archive, unarchive). Only destructive actions (delete) get a dialog.
338
- - Don't introduce ORMs, query builders, or schema validation libraries the project is small enough for raw prepared statements and hand-written mappers.
339
+ - Don't introduce ORMs, query builders, or schema validation libraries; the project is small enough for raw prepared statements and hand-written mappers.
339
340
  - Don't break the single-source-of-truth of `CLAUDE.md` → `AGENTS.md` symlink. Edit `AGENTS.md`; `CLAUDE.md` follows automatically.
340
341
  - Don't skip `try/catch` swallowing on best-effort cleanup (agent stop, dev-server stop, worktree removal). These must never break the primary operation.
341
342
  - Don't hardcode user-visible text in the frontend. Every string must go through `$t()` / `t()` with keys in all 5 locale files. See [Internationalization (i18n)](#internationalization-i18n).
package/CHANGELOG.md CHANGED
@@ -4,7 +4,18 @@ All notable changes to Kōbō are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/). Each release is an `## <version>`
5
5
  section — the in-app "What's new" dialog reads this file.
6
6
 
7
- ## 1.7.34/35
7
+ ## 1.8.1
8
+
9
+ - feat: opt-in LAN network access plus UX and workspace fixes (#13)
10
+ - chore(docs): update CHANGELOG
11
+
12
+ ## 1.8.0
13
+
14
+ - feat(schedule): manual wakeup & cron management per workspace
15
+ - feat(agent): enforce wakeup at turn-end + keep background work alive
16
+ - feat(workspace): warn when viewing a non-latest session outside auto-loop
17
+
18
+ ## 1.7.34
8
19
 
9
20
  - chore(npm): update claude sdk
10
21
 
package/README.md CHANGED
@@ -13,19 +13,19 @@ Kōbō runs multiple coding agents in parallel, each isolated in its own git wor
13
13
 
14
14
  ## Features
15
15
 
16
- - **Isolated worktrees** each workspace is a dedicated git worktree on its own branch; parallel sessions never collide.
17
- - **Two agent engines** Claude Code (via `@anthropic-ai/claude-agent-sdk`) and OpenAI Codex (via `codex app-server`), chosen per workspace.
18
- - **Live chat** streaming text, reasoning blocks, inline Edit/Write diffs, per-turn cards, infinite scrollback; `/` autocompletes skills & commands and `@` fuzzy-autocompletes worktree file paths; every workspace's session events are exportable to CSV.
19
- - **Task tracking** per-workspace MCP server (`kobo-tasks`) lets the agent manage its own tasks, acceptance criteria, and live status.
20
- - **Git panel** Monaco-based diff viewer with **inline file editing** (edit the right-hand panel directly, save with `Ctrl/Cmd+S`, conflict-guarded via sha precondition), inline conflict resolution, `Sync` / `Push` / `Open PR` / `Change PR base` / `Change source branch` (cherry-pick of the branch-proper commits, with an optional custom bash script). Multi-forge: GitHub (`gh`), GitLab (`glab`), or no forge auto-detected from the remote, overridable per project.
21
- - **Attention indicators** workspace cards in the drawer surface CI failures and review-requested-changes inline, so failing PRs/MRs stand out at a glance.
22
- - **Auto-loop** opt-in mode that walks the task list, spawning a fresh session per task and stopping on completion, stall, or error.
23
- - **Quota-aware** 5-hour / 7-day Claude usage and Codex rate-limit buckets in the footer; sessions auto-resume after a rate-limit reset.
24
- - **Scheduled wakeups** the agent schedules a one-shot wake-up via the `ScheduleWakeup` tool; Kōbō persists it across restarts, shows a live countdown, and re-invokes the agent with the stored prompt at the chosen time.
25
- - **Cron schedules** recurring per-workspace triggers the agent registers through MCP tools (`cron_create` / `cron_delete` / `cron_list`); each tick resumes the workspace session (skipped if already active), and schedules are re-armed at boot with skip-missed semantics.
26
- - **Lifecycle scripts** shell scripts run automatically at key moments: **setup** (worktree created), **cleanup** (session ended), **archive** (workspace archived). Configured globally or per project, with their output streamed into the chat.
27
- - **Disk-space purge** free a merged workspace's disk space without losing its chat history: the worktree folder is removed (PR metadata is captured for later restore), the workspace is archived, and the chat log stays queryable. Trigger manually from the workspace menu or enable **auto-purge on PR merged** in Settings → Worktrees. Recreate the worktree later with `gh pr checkout <pr-number>` and Kōbō auto-detects the folder reappearing within 30 seconds workspace is unarchived and reactivated, no UI action needed.
28
- - **Optional integrations** Notion (import missions), Sentry (fix from issue URL), local voice transcription (whisper.cpp).
16
+ - **Isolated worktrees**: each workspace is a dedicated git worktree on its own branch, so parallel sessions never collide.
17
+ - **Two agent engines**: Claude Code (via `@anthropic-ai/claude-agent-sdk`) and OpenAI Codex (via `codex app-server`), chosen per workspace.
18
+ - **Live chat**: streaming text, reasoning blocks, inline Edit/Write diffs, per-turn cards, infinite scrollback. `/` autocompletes skills and commands, `@` fuzzy-autocompletes worktree file paths, and you can export any workspace's session events to CSV.
19
+ - **Task tracking**: a per-workspace MCP server (`kobo-tasks`) lets the agent manage its own tasks, acceptance criteria, and live status.
20
+ - **Git panel**: a Monaco-based diff viewer with **inline file editing** (edit the right-hand panel directly, save with `Ctrl/Cmd+S`, conflict-guarded via sha precondition), inline conflict resolution, and `Sync` / `Push` / `Open PR` / `Change PR base` / `Change source branch` (cherry-pick of the branch-proper commits, with an optional custom bash script). Multi-forge: GitHub (`gh`), GitLab (`glab`), or no forge, auto-detected from the remote and overridable per project.
21
+ - **Attention indicators**: workspace cards in the drawer surface CI failures and review-requested-changes inline, so failing PRs/MRs stand out at a glance.
22
+ - **Auto-loop**: an opt-in mode that walks the task list, spawning a fresh session per task and stopping on completion, stall, or error.
23
+ - **Quota-aware**: 5-hour / 7-day Claude usage and Codex rate-limit buckets sit in the footer, and sessions auto-resume after a rate-limit reset.
24
+ - **Scheduled wakeups**: the agent schedules a one-shot wake-up via the `ScheduleWakeup` tool. Kōbō persists it across restarts, shows a live countdown, and re-invokes the agent with the stored prompt at the chosen time.
25
+ - **Cron schedules**: recurring per-workspace triggers the agent registers through MCP tools (`cron_create` / `cron_delete` / `cron_list`). Each tick resumes the workspace session (skipped if already active), and schedules are re-armed at boot with skip-missed semantics.
26
+ - **Lifecycle scripts**: shell scripts run automatically at key moments. **setup** (worktree created), **cleanup** (session ended), **archive** (workspace archived). Configure them globally or per project, with their output streamed into the chat.
27
+ - **Disk-space purge**: free a merged workspace's disk space without losing its chat history. The worktree folder is removed (PR metadata is captured for later restore), the workspace is archived, and the chat log stays queryable. Trigger it manually from the workspace menu or enable **auto-purge on PR merged** in Settings → Worktrees. Recreate the worktree later with `gh pr checkout <pr-number>` and Kōbō detects the folder reappearing within 30 seconds, then unarchives and reactivates the workspace with no UI action needed.
28
+ - **Optional integrations**: Notion (import missions), Sentry (fix from issue URL), local voice transcription (whisper.cpp).
29
29
 
30
30
  ## Quick start
31
31
 
@@ -35,7 +35,7 @@ Requires Node.js ≥ 20 and a logged-in Claude Code **or** Codex CLI.
35
35
  npx @loicngr/kobo@latest
36
36
  ```
37
37
 
38
- Default port is `3000`. If you already run something on that port (or another Kōbō instance), pick your own `SERVER_PORT` is read first, `PORT` is the fallback:
38
+ Default port is `3000`. If you already run something on that port (or another Kōbō instance), pick your own. `SERVER_PORT` is read first, `PORT` is the fallback:
39
39
 
40
40
  ```bash
41
41
  SERVER_PORT=9997 PORT=9998 npx @loicngr/kobo@latest
@@ -53,7 +53,7 @@ npm install
53
53
  npm run dev:all # backend :3300 + client :8080
54
54
  ```
55
55
 
56
- A production-installed Kōbō (`npx @loicngr/kobo`) and a dev server can run side by side they use separate data directories.
56
+ A production-installed Kōbō (`npx @loicngr/kobo`) and a dev server can run side by side, since they use separate data directories.
57
57
 
58
58
  ## Configuration
59
59
 
@@ -62,35 +62,35 @@ The most common knobs:
62
62
  | Env var | Default | Purpose |
63
63
  |---|---|---|
64
64
  | `PORT` | `3000` | HTTP / WebSocket server port (overridden by `SERVER_PORT` if set) |
65
- | `SERVER_PORT` | | Preferred override for the server port; takes precedence over `PORT` |
65
+ | `SERVER_PORT` | none | Preferred override for the server port; takes precedence over `PORT` |
66
66
  | `KOBO_HOME` | `~/.config/kobo` | Data directory (SQLite, settings, voice models) |
67
- | `NOTION_API_TOKEN` | | Notion integration token |
68
- | `OPENAI_API_KEY` | | Codex engine credential (alternative to `codex login`) |
67
+ | `NOTION_API_TOKEN` | none | Notion integration token |
68
+ | `OPENAI_API_KEY` | none | Codex engine credential (alternative to `codex login`) |
69
69
 
70
- Global and per-project settings (worktree path, dev server commands, E2E framework, prompt templates, git conventions, branch prefixes, lifecycle scripts, task prompt) are edited in **Settings** at runtime — per-project values inherit from the global ones when left empty.
70
+ Global and per-project settings (worktree path, dev server commands, E2E framework, prompt templates, git conventions, branch prefixes, lifecycle scripts, task prompt) are edited in **Settings** at runtime. Per-project values inherit from the global ones when left empty.
71
71
 
72
- The full reference every env var, every setting key, MCP server registration, Notion / Sentry / Voice setup is in [`CONFIGURATION.md`](./CONFIGURATION.md).
72
+ The full reference (every env var, every setting key, MCP server registration, Notion / Sentry / Voice setup) is in [`CONFIGURATION.md`](./CONFIGURATION.md).
73
73
 
74
74
  ## Agent runtimes
75
75
 
76
- - **Claude Code.** Authenticate once with `claude /login`. Kōbō calls the embedded SDK directly no `claude` binary required at runtime.
76
+ - **Claude Code.** Authenticate once with `claude /login`. Kōbō calls the embedded SDK directly, so no `claude` binary is required at runtime.
77
77
  - **OpenAI Codex** (experimental). Run `codex login` or export `OPENAI_API_KEY`. Kōbō spawns a long-lived `codex app-server` subprocess per workspace and bridges its JSON-RPC stream to the same UI.
78
78
 
79
- Engine selection happens at workspace creation. Both share the same task tracking, permission modes, sub-agent panel, and quota footer. The mapping of Kōbō's four permission modes (`plan` / `bypass` / `strict` / `interactive`) to each engine's native sandbox + approval semantics is in [`CONFIGURATION.md`](./CONFIGURATION.md#permission-modes).
79
+ You pick the engine at workspace creation. Both share the same task tracking, permission modes, sub-agent panel, and quota footer. The mapping of Kōbō's four permission modes (`plan` / `bypass` / `strict` / `interactive`) to each engine's native sandbox + approval semantics is in [`CONFIGURATION.md`](./CONFIGURATION.md#permission-modes).
80
80
 
81
81
  ## Disk-space purge
82
82
 
83
- A merged workspace is automatically archived but its worktree folder usually carries a lot of weight (`node_modules`, `vendor`, build artefacts…). Kōbō can free that space without losing anything queryable:
83
+ A merged workspace is automatically archived, but its worktree folder usually carries a lot of weight (`node_modules`, `vendor`, build artefacts…). Kōbō frees that space without losing anything queryable:
84
84
 
85
- - **Manual** workspace context menu → *Free disk space (delete worktree)*. The worktree is removed, the chat history and PR metadata stay in the database.
86
- - **Automatic** **Settings → Worktrees → Auto-purge worktree on PR merged**. When the pr-watcher sees the OPEN → MERGED transition, it archives **and** purges.
87
- - **Restore** recreate the folder yourself (`gh pr checkout <pr>` or `git worktree add <path> <branch>`). The pr-watcher detects the directory reappearing within 30 seconds and re-activates the workspace automatically (clears purge flag + unarchives). No UI action needed.
85
+ - **Manual**: workspace context menu → *Free disk space (delete worktree)*. The worktree is removed; the chat history and PR metadata stay in the database.
86
+ - **Automatic**: **Settings → Worktrees → Auto-purge worktree on PR merged**. When the pr-watcher sees the OPEN → MERGED transition, it archives **and** purges.
87
+ - **Restore**: recreate the folder yourself (`gh pr checkout <pr>` or `git worktree add <path> <branch>`). The pr-watcher detects the directory reappearing within 30 seconds and re-activates the workspace automatically (clears purge flag, unarchives). No UI action needed.
88
88
 
89
89
  ### Avoiding permission errors during purge
90
90
 
91
91
  Docker containers usually write as `root`, so files in `node_modules` / `vendor` end up root-owned on the host. Plain `rm -rf` (which Kōbō uses under the hood) then fails with `EACCES` / `EPERM`. Pick one of these strategies depending on your setup:
92
92
 
93
- 1. **Best run your container as the host user.** Add a `USER` directive in your `Dockerfile`, or set `user: "${UID}:${GID}"` in `docker-compose.yml` with `UID`/`GID` exported in your shell. No more root-owned files; nothing extra to do.
93
+ 1. **Best: run your container as the host user.** Add a `USER` directive in your `Dockerfile`, or set `user: "${UID}:${GID}"` in `docker-compose.yml` with `UID`/`GID` exported in your shell. No more root-owned files, and nothing extra to do.
94
94
  2. **Preventive ACL on the worktrees root.** On ext4 / btrfs / xfs with a regular Docker bind mount, a default ACL grants your user access to every file created later:
95
95
  ```bash
96
96
  setfacl -d -m u:$(whoami):rwX <worktrees-root> # e.g. ~/.worktrees
@@ -112,28 +112,50 @@ When a purge does fail, Kōbō surfaces a toast with a copy-pasteable recovery c
112
112
 
113
113
  Kōbō ships first-class support for three external systems. All are opt-in and reuse credentials you may already have configured for Claude Code.
114
114
 
115
- - **Notion** import missions, tasks, and acceptance criteria from a Notion page.
116
- - **Sentry** paste an issue URL to spawn a fix workspace with the stacktrace, tags, and a TDD workflow.
117
- - **Voice transcription** local push-to-talk via [`whisper.cpp`](https://github.com/ggml-org/whisper.cpp).
115
+ - **Notion**: import missions, tasks, and acceptance criteria from a Notion page.
116
+ - **Sentry**: paste an issue URL to spawn a fix workspace with the stacktrace, tags, and a TDD workflow.
117
+ - **Voice transcription**: local push-to-talk via [`whisper.cpp`](https://github.com/ggml-org/whisper.cpp).
118
118
 
119
119
  See [`CONFIGURATION.md`](./CONFIGURATION.md) for the setup of each.
120
120
 
121
+ ## Network access
122
+
123
+ By default, Kōbō binds to `127.0.0.1` only (localhost). To control Kōbō from
124
+ another device on the same Wi-Fi or LAN:
125
+
126
+ 1. Open **Settings → Global → Network access** and enable it.
127
+ 2. Restart Kōbō when prompted, since the server must re-bind to apply the change.
128
+ 3. Scan the QR code shown in the Settings panel from your phone, or copy a LAN
129
+ URL and paste the token in the login dialog on the remote device.
130
+
131
+ > **Trusted networks only.** Kōbō uses plain HTTP, so the token travels in cleartext.
132
+ > Do not expose the port to the internet. For remote access over the internet,
133
+ > put a terminating HTTPS proxy or a VPN (e.g. Tailscale) in front of Kōbō.
134
+ >
135
+ > **Production only.** This protection applies when running a built Kōbō
136
+ > (`npm start` / `npx @loicngr/kobo`). In development (`npm run dev:all`) the Vite
137
+ > dev server is always exposed on the LAN and bypasses the token. See
138
+ > [`CONFIGURATION.md`](./CONFIGURATION.md#production-vs-development-mode-important).
139
+
140
+ See [`CONFIGURATION.md`](./CONFIGURATION.md#network-access) for token management,
141
+ QR code details, and all security caveats.
142
+
121
143
  ## Skill suites
122
144
 
123
145
  Kōbō's auto-generated prompts (review, auto-loop grooming, QA, brainstorming) can target four different skill ecosystems, selectable in **Settings → Skills**:
124
146
 
125
- - **[superpowers](https://github.com/obra/superpowers)** (default) plugin for Claude Code with the brainstorm → spec → plan → execute discipline, TDD, debugging, code review.
126
- - **[gstack](https://github.com/garrytan/gstack)** CLI slash commands for navigation, QA, design review, ship pipeline, second-opinion via Codex.
127
- - **superpowers + gstack** both, with each used for what it does best.
128
- - **custom** write your own prompts.
147
+ - **[superpowers](https://github.com/obra/superpowers)** (default): a plugin for Claude Code with the brainstorm → spec → plan → execute discipline, TDD, debugging, code review.
148
+ - **[gstack](https://github.com/garrytan/gstack)**: CLI slash commands for navigation, QA, design review, ship pipeline, second-opinion via Codex.
149
+ - **superpowers + gstack**: both, with each used for what it does best.
150
+ - **custom**: write your own prompts.
129
151
 
130
- Optionally pair with **[gbrain](https://github.com/garrytan/gbrain)** a per-project knowledge graph + semantic search exposed as an MCP server. Inherited automatically from your `~/.claude.json` config.
152
+ Optionally pair with **[gbrain](https://github.com/garrytan/gbrain)**, a per-project knowledge graph + semantic search exposed as an MCP server. It is inherited automatically from your `~/.claude.json` config.
131
153
 
132
154
  Full install instructions and the prompt-suite differences are in [`CONFIGURATION.md`](./CONFIGURATION.md#skill-suites).
133
155
 
134
156
  ## Architecture
135
157
 
136
- Hono backend, Vue 3 + Quasar SPA, SQLite (WAL) for persistence, WebSocket for live updates. Each workspace spawns its own agent engine and a dedicated MCP server (`kobo-tasks`) the agent uses to query and mutate workspace state.
158
+ Hono backend, Vue 3 + Quasar SPA, SQLite (WAL) for persistence, WebSocket for live updates. Each workspace spawns its own agent engine and a dedicated MCP server (`kobo-tasks`) that the agent uses to query and mutate workspace state.
137
159
 
138
160
  ```
139
161
  src/
@@ -166,7 +188,7 @@ PRs welcome. Branch off `develop`, follow Conventional Commits, run `make ci` be
166
188
 
167
189
  ## Release
168
190
 
169
- Releases are cut from `main`. Bump `package.json` on `develop`, merge into `main`, push. The release workflow builds, tests, publishes to npm, tags `v<version>`, and creates the GitHub Release failing early if the version or tag already exists.
191
+ Releases are cut from `main`. Bump `package.json` on `develop`, merge into `main`, push. The release workflow builds, tests, publishes to npm, tags `v<version>`, and creates the GitHub Release. It fails early if the version or tag already exists.
170
192
 
171
193
  ## License
172
194
 
@@ -676,6 +676,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
676
676
  delaySeconds,
677
677
  prompt,
678
678
  reason,
679
+ // Pin the calling session so the wakeup resumes THIS conversation (keeps
680
+ // context). The route now defaults to 'fresh' for manual UI scheduling;
681
+ // the agent must opt into 'resume' explicitly.
682
+ mode: 'resume',
679
683
  });
680
684
  return ok(result);
681
685
  }
@@ -6,6 +6,7 @@ import { Hono } from 'hono';
6
6
  import WebSocket, { WebSocketServer } from 'ws';
7
7
  import { closeDb, getDb } from './db/index.js';
8
8
  import { getPendingMigrations, runMigrations } from './db/migrations.js';
9
+ import { networkAuthMiddleware } from './middleware/network-auth-middleware.js';
9
10
  import changelogRouter from './routes/changelog.js';
10
11
  import devServerRouter from './routes/dev-server.js';
11
12
  import documentsRouter from './routes/documents.js';
@@ -30,8 +31,11 @@ import { runContentMigrationIfNeeded } from './services/content-migration-servic
30
31
  import * as cronService from './services/cron-service.js';
31
32
  import { createDailyDbBackupIfNeeded, createPreMigrationBackup } from './services/db-backup-service.js';
32
33
  import { startDevServer, stopDevServer } from './services/dev-server-service.js';
34
+ import { authorizeWsUpgrade, getLanUrls, resolveBindHost } from './services/network-access-service.js';
33
35
  import { startPrWatcher, stopPrWatcher } from './services/pr-watcher-service.js';
34
36
  import * as quotaBackoffService from './services/quota-backoff-service.js';
37
+ import { getGlobalSettings } from './services/settings-service.js';
38
+ import { reloadDefaultTemplates } from './services/templates-service.js';
35
39
  import { createTerminal, destroyAllTerminals, getTerminal } from './services/terminal-service.js';
36
40
  import { startUsagePoller, stopUsagePoller } from './services/usage/index.js';
37
41
  import * as wakeupService from './services/wakeup-service.js';
@@ -79,10 +83,23 @@ autoLoopService.rehydrate();
79
83
  restoreRetryCountsFromDb();
80
84
  quotaBackoffService.restoreOnBoot((workspaceId) => autoLoopService.onQuotaBackoffExpired(workspaceId));
81
85
  cronService.restoreOnBoot();
86
+ // Deliver any new default prompt templates to existing installs (seed-once via the
87
+ // seededDefaultSlugs watermark; never overwrites or re-adds deleted defaults).
88
+ try {
89
+ const { added } = reloadDefaultTemplates();
90
+ if (added.length > 0) {
91
+ console.log(`[templates] Added ${added.length} new default template(s): ${added.join(', ')}`);
92
+ }
93
+ }
94
+ catch (err) {
95
+ console.error('[templates] reloadDefaultTemplates on boot failed:', err);
96
+ }
82
97
  startPrWatcher();
83
98
  startUsagePoller();
84
99
  // Create Hono app
85
100
  const app = new Hono();
101
+ // Gate non-loopback requests behind the network-access token (loopback exempt).
102
+ app.use('/api/*', networkAuthMiddleware);
86
103
  // Health check (root / is handled by the SPA catch-all below)
87
104
  app.get('/api/health', (c) => c.json({ status: 'ok', version: getPackageVersion() }));
88
105
  // Mount route sub-routers
@@ -148,12 +165,21 @@ if (clientDistPath) {
148
165
  });
149
166
  }
150
167
  // Create HTTP server via @hono/node-server
168
+ const bindHost = resolveBindHost(getGlobalSettings().networkAccessEnabled);
151
169
  const server = serve({
152
170
  fetch: app.fetch,
153
171
  port: PORT,
172
+ hostname: bindHost,
154
173
  }, (info) => {
155
174
  setBackendPort(info.port);
156
- console.log(`Server running at http://localhost:${info.port}`);
175
+ if (getGlobalSettings().networkAccessEnabled) {
176
+ console.log(`Server running — network access ON (port ${info.port})`);
177
+ for (const url of getLanUrls(info.port))
178
+ console.log(` LAN: ${url}`);
179
+ }
180
+ else {
181
+ console.log(`Server running at http://localhost:${info.port} (localhost only)`);
182
+ }
157
183
  // Content migration runs AFTER the HTTP listener is up so the frontend
158
184
  // can observe progress via WS broadcasts + GET /api/migration/status.
159
185
  // Not awaited — the callback returns quickly, the migration runs in the
@@ -360,6 +386,18 @@ setMessageHandler((type, payload) => {
360
386
  // Handle WebSocket upgrade requests on /ws path
361
387
  server.on('upgrade', (request, socket, head) => {
362
388
  const { pathname } = new URL(request.url ?? '/', `http://localhost:${PORT}`);
389
+ const wsGlobal = getGlobalSettings();
390
+ if (!authorizeWsUpgrade({
391
+ address: request.socket.remoteAddress,
392
+ rawUrl: request.url,
393
+ enabled: wsGlobal.networkAccessEnabled,
394
+ expectedToken: wsGlobal.networkAccessToken,
395
+ })) {
396
+ console.warn(`[network-auth] WS 401 (missing/invalid token) from ${request.socket.remoteAddress ?? 'unknown'} ${pathname}`);
397
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
398
+ socket.destroy();
399
+ return;
400
+ }
363
401
  if (pathname === '/ws') {
364
402
  wss.handleUpgrade(request, socket, head, (ws) => {
365
403
  wss.emit('connection', ws, request);
@@ -0,0 +1,27 @@
1
+ import { getConnInfo } from '@hono/node-server/conninfo';
2
+ import { evaluateNetworkAccess } from '../services/network-access-service.js';
3
+ import { getGlobalSettings } from '../services/settings-service.js';
4
+ /**
5
+ * Gates non-loopback requests behind the network-access token.
6
+ *
7
+ * Loopback requests always pass (the host machine's own usage is frictionless).
8
+ * The client IP comes only from the OS socket via getConnInfo, never from
9
+ * X-Forwarded-For, so a remote client cannot spoof a loopback address.
10
+ */
11
+ export const networkAuthMiddleware = async (c, next) => {
12
+ const address = getConnInfo(c).remote.address;
13
+ const global = getGlobalSettings();
14
+ const decision = evaluateNetworkAccess({
15
+ address,
16
+ enabled: global.networkAccessEnabled,
17
+ expectedToken: global.networkAccessToken,
18
+ providedToken: c.req.header('X-Kobo-Token'),
19
+ });
20
+ if (decision.allow)
21
+ return next();
22
+ // Surface denied requests so "my device can't connect" is debuggable.
23
+ // Never log the token itself, only the reason and the remote address.
24
+ const reason = decision.status === 403 ? 'network access disabled' : 'missing/invalid token';
25
+ console.warn(`[network-auth] HTTP ${decision.status} (${reason}) from ${address ?? 'unknown'} ${c.req.method} ${c.req.path}`);
26
+ return c.json({ error: 'unauthorized' }, decision.status);
27
+ };