specrails-desktop 2.18.2 → 2.19.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 (346) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{ActivityFeedPage-mGnv5SOe.js → ActivityFeedPage-C0_k6n5T.js} +1 -1
  3. package/client/dist/assets/AgentBrowserCapture-NHUCEnTh.js +1 -0
  4. package/client/dist/assets/AgentModeCodePane-jddXrhPY.js +2 -0
  5. package/client/dist/assets/AgentsPage-BPjf6bId.js +86 -0
  6. package/client/dist/assets/AnalyticsPage-BaJjj5mO.js +1 -0
  7. package/client/dist/assets/{BarChart-BqHUOnl9.js → BarChart-HPiao--v.js} +1 -1
  8. package/client/dist/assets/CodePage-CAD4KfkC.js +2 -0
  9. package/client/dist/assets/{DesktopAnalyticsPage-CR-m97Ui.js → DesktopAnalyticsPage-B2RBxIvj.js} +1 -1
  10. package/client/dist/assets/DocsDialog-BJDvUEGj.js +11 -0
  11. package/client/dist/assets/DocsPage-B6H-DBs8.js +11 -0
  12. package/client/dist/assets/{ExportDropdown-C-o1Q4ef.js → ExportDropdown-CYMcbLTD.js} +1 -1
  13. package/client/dist/assets/IntegrationsPage-Dx_UpQVk.js +3 -0
  14. package/client/dist/assets/JobDetailPage-BlTnsRAN.js +16 -0
  15. package/client/dist/assets/JobsPage-EB0PbXOk.js +1 -0
  16. package/client/dist/assets/LoopBuilderPage-DfWfhhJY.js +7 -0
  17. package/client/dist/assets/LoopsPage-DCVATTv5.js +1 -0
  18. package/client/dist/assets/{addspec-B9dY_F5T.js → addspec-BXV6sx5l.js} +1 -1
  19. package/client/dist/assets/{addspec-tbxr3sHa.js → addspec-CS895X6A.js} +1 -1
  20. package/client/dist/assets/{addspec-W0r16BOC2.js → addspec-CXfEOrD42.js} +1 -1
  21. package/client/dist/assets/{addspec-D43TlMVp2.js → addspec-D28kw9ow2.js} +1 -1
  22. package/client/dist/assets/{addspec-B9nhMikb.js → addspec-D39EEg02.js} +1 -1
  23. package/client/dist/assets/{addspec-DfgIZ5lO.js → addspec-DC1tgsFG.js} +1 -1
  24. package/client/dist/assets/{addspec-BtNaJHu2.js → addspec-DcHALPEp.js} +1 -1
  25. package/client/dist/assets/{addspec-BloGn_ey.js → addspec-DxW6oPsk.js} +1 -1
  26. package/client/dist/assets/{agent-Bm2tPrdr.js → agent-B5Ycau5M.js} +1 -1
  27. package/client/dist/assets/{agent-DMUrz9ZS2.js → agent-BEz08l4_2.js} +1 -1
  28. package/client/dist/assets/{agent-C4bO1l_v.js → agent-BIX51CEA.js} +1 -1
  29. package/client/dist/assets/{agent-9WdSxPU5.js → agent-Bc9Kwf65.js} +1 -1
  30. package/client/dist/assets/{agent-pO8x4jbR.js → agent-BiM7puyn.js} +1 -1
  31. package/client/dist/assets/{agent-CgKsh88Z2.js → agent-C0kYSv3m2.js} +1 -1
  32. package/client/dist/assets/agent-CCzWlfqs.js +1 -0
  33. package/client/dist/assets/{agent-ASIyVcg0.js → agent-DiCSo31C.js} +1 -1
  34. package/client/dist/assets/{agents-DfGvYtpq2.js → agents-BFrHy5d-2.js} +1 -1
  35. package/client/dist/assets/{agents-Dli56gr82.js → agents-BFy2Kq_k2.js} +1 -1
  36. package/client/dist/assets/{agents-C07al8rI.js → agents-BXDCFJQa.js} +1 -1
  37. package/client/dist/assets/{agents-BAxAwHy-.js → agents-BySH0Gvw.js} +1 -1
  38. package/client/dist/assets/{agents-Eqw_AQ6H.js → agents-CTdwacTO.js} +1 -1
  39. package/client/dist/assets/{agents-BjT_xLjv.js → agents-Cev2oFfX.js} +1 -1
  40. package/client/dist/assets/{agents-BUYBb825.js → agents-CsDEv4Fj.js} +1 -1
  41. package/client/dist/assets/{agents-52t8NVpS.js → agents-z0kA-y0Q.js} +1 -1
  42. package/client/dist/assets/dashboard-Bl2_Yjs6.js +1 -0
  43. package/client/dist/assets/dashboard-C_vS9RaM2.js +1 -0
  44. package/client/dist/assets/dashboard-CcEpALV_.js +1 -0
  45. package/client/dist/assets/dashboard-CfltY66K.js +1 -0
  46. package/client/dist/assets/dashboard-CoED_41r.js +1 -0
  47. package/client/dist/assets/dashboard-DLeC5ziO.js +1 -0
  48. package/client/dist/assets/dashboard-DxEPkALo2.js +1 -0
  49. package/client/dist/assets/dashboard-MgDVmaYS2.js +1 -0
  50. package/client/dist/assets/{dist-js-CUegfF6k.js → dist-js-ClEfL6Bo.js} +1 -1
  51. package/client/dist/assets/{dist-js-BHhtSerX.js → dist-js-b2_z7zAi.js} +1 -1
  52. package/client/dist/assets/index-9yNUBwEi.css +2 -0
  53. package/client/dist/assets/{index-B7vB20Jw.js → index-BeujshsU.js} +57 -57
  54. package/client/dist/assets/{lib-PQoHlR5u.js → lib-CfphudAm.js} +1 -1
  55. package/client/dist/assets/loops-BEUiCzEI.js +1 -0
  56. package/client/dist/assets/loops-BlrQiK122.js +1 -0
  57. package/client/dist/assets/loops-Bs0ectZ32.js +1 -0
  58. package/client/dist/assets/loops-CLZV8HHY2.js +1 -0
  59. package/client/dist/assets/loops-CQuuMlyZ.js +1 -0
  60. package/client/dist/assets/loops-DuuWQWTI.js +1 -0
  61. package/client/dist/assets/loops-KOTryzk5.js +1 -0
  62. package/client/dist/assets/loops-t1mUZtf0.js +1 -0
  63. package/client/dist/assets/nav--1ErXZrS.js +1 -0
  64. package/client/dist/assets/nav-18nqSJ8h.js +1 -0
  65. package/client/dist/assets/nav-BuonHb17.js +1 -0
  66. package/client/dist/assets/nav-C-sCoOLY2.js +1 -0
  67. package/client/dist/assets/nav-ClXz_3Ic2.js +1 -0
  68. package/client/dist/assets/nav-CtqgSQeE2.js +1 -0
  69. package/client/dist/assets/nav-S3YvCaVv.js +1 -0
  70. package/client/dist/assets/nav-czg8NkN-.js +1 -0
  71. package/client/dist/assets/settings-BOZBX-Vq.js +1 -0
  72. package/client/dist/assets/settings-CX29QcQW.js +1 -0
  73. package/client/dist/assets/settings-CfOWHPzc2.js +1 -0
  74. package/client/dist/assets/settings-DaXhxNug2.js +1 -0
  75. package/client/dist/assets/settings-DmyDpuzu.js +1 -0
  76. package/client/dist/assets/settings-DwPRLcxr2.js +1 -0
  77. package/client/dist/assets/settings-MjYVJvDE.js +1 -0
  78. package/client/dist/assets/settings-zJFdTzKE.js +1 -0
  79. package/client/dist/assets/{setup-BYoDYoPY2.js → setup-BmXfxlzA2.js} +1 -1
  80. package/client/dist/assets/{setup-faMApo0U2.js → setup-BxVydxbl2.js} +1 -1
  81. package/client/dist/assets/{setup-C5UbM0RP.js → setup-DIRZ1Lf-.js} +1 -1
  82. package/client/dist/assets/{setup-Bf9eLKxU.js → setup-DwbKSnKQ.js} +1 -1
  83. package/client/dist/assets/{setup-JFkWWZnR.js → setup-Dx13pESZ.js} +1 -1
  84. package/client/dist/assets/{setup-CqGrWb6G.js → setup-G4ZvDKxX.js} +1 -1
  85. package/client/dist/assets/{setup-tloaTobG2.js → setup-SLAi4xxQ2.js} +1 -1
  86. package/client/dist/assets/{setup-4us-Bz3H.js → setup-Z01j3st4.js} +1 -1
  87. package/client/dist/assets/{upload-PnJDPHZH.js → upload-Ox9jUq1X.js} +1 -1
  88. package/client/dist/assets/{value-BTDefcOV.js → value-BeQVX5gN.js} +1 -1
  89. package/client/dist/index.html +10 -10
  90. package/docs/codex.md +5 -5
  91. package/docs/customizing.md +4 -4
  92. package/docs/gemini.md +2 -2
  93. package/docs/getting-started.md +1 -1
  94. package/docs/guide/de/integrations/1-ai-providers.md +2 -2
  95. package/docs/guide/de/integrations/5-mcp-server.md +20 -1
  96. package/docs/guide/de/pipeline/1-rails-and-jobs.md +4 -4
  97. package/docs/guide/de/pipeline/2-the-job-detail-view.md +1 -1
  98. package/docs/guide/de/pipeline/4-picking-an-engine-per-rail.md +2 -2
  99. package/docs/guide/de/pipeline/5-the-loop-builder.md +2 -2
  100. package/docs/guide/en/integrations/1-ai-providers.md +2 -2
  101. package/docs/guide/en/integrations/5-mcp-server.md +20 -1
  102. package/docs/guide/en/pipeline/1-rails-and-jobs.md +4 -4
  103. package/docs/guide/en/pipeline/2-the-job-detail-view.md +1 -1
  104. package/docs/guide/en/pipeline/4-picking-an-engine-per-rail.md +2 -2
  105. package/docs/guide/en/pipeline/5-the-loop-builder.md +2 -2
  106. package/docs/guide/es/integrations/1-ai-providers.md +2 -2
  107. package/docs/guide/es/integrations/5-mcp-server.md +20 -1
  108. package/docs/guide/es/pipeline/1-rails-and-jobs.md +4 -4
  109. package/docs/guide/es/pipeline/2-the-job-detail-view.md +1 -1
  110. package/docs/guide/es/pipeline/4-picking-an-engine-per-rail.md +2 -2
  111. package/docs/guide/es/pipeline/5-the-loop-builder.md +2 -2
  112. package/docs/guide/fr/integrations/1-ai-providers.md +2 -2
  113. package/docs/guide/fr/integrations/5-mcp-server.md +20 -1
  114. package/docs/guide/fr/pipeline/1-rails-and-jobs.md +4 -4
  115. package/docs/guide/fr/pipeline/2-the-job-detail-view.md +1 -1
  116. package/docs/guide/fr/pipeline/4-picking-an-engine-per-rail.md +2 -2
  117. package/docs/guide/fr/pipeline/5-the-loop-builder.md +2 -2
  118. package/docs/guide/it/integrations/1-ai-providers.md +2 -2
  119. package/docs/guide/it/integrations/5-mcp-server.md +20 -1
  120. package/docs/guide/it/pipeline/1-rails-and-jobs.md +4 -4
  121. package/docs/guide/it/pipeline/2-the-job-detail-view.md +1 -1
  122. package/docs/guide/it/pipeline/4-picking-an-engine-per-rail.md +2 -2
  123. package/docs/guide/it/pipeline/5-the-loop-builder.md +2 -2
  124. package/docs/guide/ja/integrations/1-ai-providers.md +2 -2
  125. package/docs/guide/ja/integrations/5-mcp-server.md +20 -1
  126. package/docs/guide/ja/pipeline/1-rails-and-jobs.md +4 -4
  127. package/docs/guide/ja/pipeline/2-the-job-detail-view.md +1 -1
  128. package/docs/guide/ja/pipeline/4-picking-an-engine-per-rail.md +2 -2
  129. package/docs/guide/ja/pipeline/5-the-loop-builder.md +2 -2
  130. package/docs/guide/pt/integrations/1-ai-providers.md +2 -2
  131. package/docs/guide/pt/integrations/5-mcp-server.md +20 -1
  132. package/docs/guide/pt/pipeline/1-rails-and-jobs.md +4 -4
  133. package/docs/guide/pt/pipeline/2-the-job-detail-view.md +1 -1
  134. package/docs/guide/pt/pipeline/4-picking-an-engine-per-rail.md +2 -2
  135. package/docs/guide/pt/pipeline/5-the-loop-builder.md +2 -2
  136. package/docs/guide/zh/integrations/1-ai-providers.md +2 -2
  137. package/docs/guide/zh/integrations/5-mcp-server.md +20 -1
  138. package/docs/guide/zh/pipeline/1-rails-and-jobs.md +4 -4
  139. package/docs/guide/zh/pipeline/2-the-job-detail-view.md +1 -1
  140. package/docs/guide/zh/pipeline/4-picking-an-engine-per-rail.md +2 -2
  141. package/docs/guide/zh/pipeline/5-the-loop-builder.md +2 -2
  142. package/docs/internals/switch-to-agent-mode-design.md +301 -0
  143. package/docs/internals/visual-design-audit.md +184 -0
  144. package/docs/mcp.md +26 -1
  145. package/docs/running-pipelines.md +10 -10
  146. package/docs/tracking-cost.md +1 -1
  147. package/package.json +1 -1
  148. package/server/dist/agent-chat-manager.js +107 -33
  149. package/server/dist/agent-chat-router.js +170 -8
  150. package/server/dist/agent-cwd-manager.js +2 -47
  151. package/server/dist/agent-operator-prompt.js +277 -0
  152. package/server/dist/agent-store.js +27 -5
  153. package/server/dist/attachment-manager.js +138 -0
  154. package/server/dist/desktop-db.js +15 -0
  155. package/server/dist/loop-factory.js +12 -2
  156. package/server/dist/mcp/guide.js +186 -51
  157. package/server/dist/mcp/tools/analytics.js +1 -1
  158. package/server/dist/mcp/tools/app-settings.js +45 -5
  159. package/server/dist/mcp/tools/chat.js +9 -2
  160. package/server/dist/mcp/tools/code.js +3 -2
  161. package/server/dist/mcp/tools/jobs.js +18 -6
  162. package/server/dist/mcp/tools/loops.js +57 -5
  163. package/server/dist/mcp/tools/meta.js +43 -3
  164. package/server/dist/mcp/tools/rails.js +6 -6
  165. package/server/dist/mcp/tools/setup.js +2 -1
  166. package/server/dist/mcp/tools/specs.js +60 -38
  167. package/server/dist/mcp/tools/types.js +17 -8
  168. package/server/dist/mcp/tools/watch.js +71 -8
  169. package/server/dist/project-router-helpers.js +1 -1
  170. package/server/dist/project-router-tickets.js +43 -4
  171. package/server/dist/providers/claude-adapter.js +6 -0
  172. package/server/dist/providers/codex-adapter.js +10 -0
  173. package/server/dist/providers/gemini-adapter.js +3 -0
  174. package/server/dist/rails-router.js +8 -17
  175. package/server/dist/spec-models.js +1 -0
  176. package/client/dist/assets/AgentsPage-BH1isg5j.js +0 -86
  177. package/client/dist/assets/AnalyticsPage-ByyshYRI.js +0 -1
  178. package/client/dist/assets/CodePage-CU7T_HYs.js +0 -2
  179. package/client/dist/assets/DocsDialog-CEmZp_8V.js +0 -11
  180. package/client/dist/assets/DocsPage-DYbAQ1AT.js +0 -11
  181. package/client/dist/assets/IntegrationsPage-C6fKdwVH.js +0 -3
  182. package/client/dist/assets/JobDetailPage-CKfIfBgE.js +0 -16
  183. package/client/dist/assets/JobsPage-nBng0riZ.js +0 -1
  184. package/client/dist/assets/LoopBuilderPage-Co_TYqYk.js +0 -7
  185. package/client/dist/assets/LoopsPage-a2hd37Ya.js +0 -1
  186. package/client/dist/assets/agent-Dwkz5mPS.js +0 -1
  187. package/client/dist/assets/dashboard-BTiH361F.js +0 -1
  188. package/client/dist/assets/dashboard-COSm_14E.js +0 -1
  189. package/client/dist/assets/dashboard-CfOs70FN2.js +0 -1
  190. package/client/dist/assets/dashboard-Cyzp4ox9.js +0 -1
  191. package/client/dist/assets/dashboard-CzHDp14_2.js +0 -1
  192. package/client/dist/assets/dashboard-D6Woy2B5.js +0 -1
  193. package/client/dist/assets/dashboard-DwbH4oVH.js +0 -1
  194. package/client/dist/assets/dashboard-cWhU3krA2.js +0 -1
  195. package/client/dist/assets/index-B0FxQALR.css +0 -2
  196. package/client/dist/assets/loops-BDhEfnx82.js +0 -1
  197. package/client/dist/assets/loops-BM5QggwH.js +0 -1
  198. package/client/dist/assets/loops-CPjLAAxj.js +0 -1
  199. package/client/dist/assets/loops-D8Iu6Jss.js +0 -1
  200. package/client/dist/assets/loops-DKnk79TB2.js +0 -1
  201. package/client/dist/assets/loops-DLW0aFMm.js +0 -1
  202. package/client/dist/assets/loops-Dcv3dFy_2.js +0 -1
  203. package/client/dist/assets/loops-y7wFRx6M.js +0 -1
  204. package/client/dist/assets/nav-1hj2izqB2.js +0 -1
  205. package/client/dist/assets/nav-BBUKMAu-2.js +0 -1
  206. package/client/dist/assets/nav-CLqkP-26.js +0 -1
  207. package/client/dist/assets/nav-Cv46L837.js +0 -1
  208. package/client/dist/assets/nav-DE3yWQw6.js +0 -1
  209. package/client/dist/assets/nav-DgA-rjLk2.js +0 -1
  210. package/client/dist/assets/nav-EG2pr6vN.js +0 -1
  211. package/client/dist/assets/nav-r3mIJVfU.js +0 -1
  212. package/client/dist/assets/settings-BKmORxlB2.js +0 -1
  213. package/client/dist/assets/settings-BVv2JgQ8.js +0 -1
  214. package/client/dist/assets/settings-Bghm3pAV.js +0 -1
  215. package/client/dist/assets/settings-BuC_aplX.js +0 -1
  216. package/client/dist/assets/settings-Bw_Qk8E32.js +0 -1
  217. package/client/dist/assets/settings-CAKgJv7p.js +0 -1
  218. package/client/dist/assets/settings-CEUgk1Ck.js +0 -1
  219. package/client/dist/assets/settings-C_PDE20s2.js +0 -1
  220. /package/client/dist/assets/{activity-Uj576ckT.js → activity-BlvN5rhP.js} +0 -0
  221. /package/client/dist/assets/{activity-w1qMFwxh.js → activity-C5SeKbom.js} +0 -0
  222. /package/client/dist/assets/{activity-BySXFT2J2.js → activity-DIJexk6v2.js} +0 -0
  223. /package/client/dist/assets/{activity-B82z3Pna.js → activity-DLdwUYv7.js} +0 -0
  224. /package/client/dist/assets/{activity-BVpZrs0J.js → activity-DOKV_Wyf.js} +0 -0
  225. /package/client/dist/assets/{activity-BId0pwuS2.js → activity-DVuiy8TI2.js} +0 -0
  226. /package/client/dist/assets/{activity-DIWnV0Pl.js → activity-Dsf3J7iW.js} +0 -0
  227. /package/client/dist/assets/{agentstudio-C7xxqVWn.js → agentstudio-B8H2H4Db2.js} +0 -0
  228. /package/client/dist/assets/{agentstudio-D-UoNAIA.js → agentstudio-B9zthaTJ.js} +0 -0
  229. /package/client/dist/assets/{agentstudio-0o3XKrcy.js → agentstudio-BDMpU01s.js} +0 -0
  230. /package/client/dist/assets/{agentstudio-Bu8m5jDw.js → agentstudio-CSZo7p8K.js} +0 -0
  231. /package/client/dist/assets/{agentstudio-ObTpVbBP.js → agentstudio-CdD6J23W.js} +0 -0
  232. /package/client/dist/assets/{agentstudio-PzFB3vfh2.js → agentstudio-D80fZu_M2.js} +0 -0
  233. /package/client/dist/assets/{agentstudio-OJjjz6Uf2.js → agentstudio-EuKglkaF2.js} +0 -0
  234. /package/client/dist/assets/{aiedit-DRrNiaB4.js → aiedit-B_VOTUo42.js} +0 -0
  235. /package/client/dist/assets/{aiedit-sP_iao2j.js → aiedit-Cxoe_0wv.js} +0 -0
  236. /package/client/dist/assets/{aiedit-DAM_WRAl2.js → aiedit-D59sPapq2.js} +0 -0
  237. /package/client/dist/assets/{aiedit-BaLZalLa.js → aiedit-DX3BBpXF.js} +0 -0
  238. /package/client/dist/assets/{aiedit-BIQqv2XM2.js → aiedit-GYOYWCHJ2.js} +0 -0
  239. /package/client/dist/assets/{aiedit-BjlUzasF.js → aiedit-TON-4N9S.js} +0 -0
  240. /package/client/dist/assets/{aiedit-BmDufyGU.js → aiedit-ybQmdsST.js} +0 -0
  241. /package/client/dist/assets/{analytics-MVJRjcY52.js → analytics-B2jjXLCP2.js} +0 -0
  242. /package/client/dist/assets/{analytics-C4z_NgJZ.js → analytics-BTkXLTfa.js} +0 -0
  243. /package/client/dist/assets/{analytics-DL2vJCbq.js → analytics-BotqRU_R.js} +0 -0
  244. /package/client/dist/assets/{analytics-e1mkNTgy2.js → analytics-Ck3vl2Qo2.js} +0 -0
  245. /package/client/dist/assets/{analytics-Ds_PNxtO2.js → analytics-CqMJQb7N.js} +0 -0
  246. /package/client/dist/assets/{analytics-B_rxNC9X.js → analytics-DGOoMkaZ.js} +0 -0
  247. /package/client/dist/assets/{analytics-BDqqCzN9.js → analytics-DL7uwNAI2.js} +0 -0
  248. /package/client/dist/assets/{attachments-BP0Ac1yt.js → attachments-7w-BR2V2.js} +0 -0
  249. /package/client/dist/assets/{attachments-CPBVeHX72.js → attachments-B5QX1Xfa2.js} +0 -0
  250. /package/client/dist/assets/{attachments-Db7JfA8a.js → attachments-BcE7kZG-2.js} +0 -0
  251. /package/client/dist/assets/{attachments-CJaO1vCH.js → attachments-BsJJXFzT.js} +0 -0
  252. /package/client/dist/assets/{attachments-8QCKHCEF2.js → attachments-Cbh4AwjP.js} +0 -0
  253. /package/client/dist/assets/{attachments-CV2uaTGE.js → attachments-D0fGlFve.js} +0 -0
  254. /package/client/dist/assets/{attachments-CaOjdN332.js → attachments-D3DJpd3e2.js} +0 -0
  255. /package/client/dist/assets/{browser-C9rUUxo4.js → browser-9GR11iTi.js} +0 -0
  256. /package/client/dist/assets/{browser-CEqx11iy.js → browser-BOxg0p3n.js} +0 -0
  257. /package/client/dist/assets/{browser-543jbxE4.js → browser-BSh8xUWV.js} +0 -0
  258. /package/client/dist/assets/{browser-CLC0etB_2.js → browser-CaV3BJNY2.js} +0 -0
  259. /package/client/dist/assets/{browser-0xeW8fUa2.js → browser-CmWGMRGf2.js} +0 -0
  260. /package/client/dist/assets/{browser-ExmELH702.js → browser-D6wbpBOL2.js} +0 -0
  261. /package/client/dist/assets/{browser-DyG2l9Wa.js → browser-dsgDU1HB.js} +0 -0
  262. /package/client/dist/assets/{chat-DagUyoVA.js → chat-B90iLDI4.js} +0 -0
  263. /package/client/dist/assets/{chat-Cu6F3d6g2.js → chat-CAo0jCnj2.js} +0 -0
  264. /package/client/dist/assets/{chat-CN8BMAMc.js → chat-CW54IP3u.js} +0 -0
  265. /package/client/dist/assets/{chat-NQPCBQo72.js → chat-CYHUUW0p2.js} +0 -0
  266. /package/client/dist/assets/{chat-CCytD3P8.js → chat-D1PXX_p2.js} +0 -0
  267. /package/client/dist/assets/{chat-IjutwlBg.js → chat-DhpcWjOf.js} +0 -0
  268. /package/client/dist/assets/{chat-vVpwkOp52.js → chat-cFG-UfRA2.js} +0 -0
  269. /package/client/dist/assets/{code-Dhb6jbAU.js → code--qUk_yn-.js} +0 -0
  270. /package/client/dist/assets/{code-Ce-nBIwj.js → code-B1aOMCVV.js} +0 -0
  271. /package/client/dist/assets/{code-BaG05zK62.js → code-B5UrmjaE2.js} +0 -0
  272. /package/client/dist/assets/{code-Dap54vOb.js → code-CjBHU0nw.js} +0 -0
  273. /package/client/dist/assets/{code-_NOCo-Ol2.js → code-Cjv4_HkK2.js} +0 -0
  274. /package/client/dist/assets/{code-32l6Naxu2.js → code-D7GLAjrz2.js} +0 -0
  275. /package/client/dist/assets/{code-C5cibF2F.js → code-DZKjfzBN.js} +0 -0
  276. /package/client/dist/assets/{commands-Cc2WpuXa.js → commands-B9XaopQC.js} +0 -0
  277. /package/client/dist/assets/{commands-k9wbNYoi.js → commands-BCx1uDIE.js} +0 -0
  278. /package/client/dist/assets/{commands-CM99Upsz2.js → commands-Bhw9Hqsa2.js} +0 -0
  279. /package/client/dist/assets/{commands-Bc8y9eNo2.js → commands-D0RnooQF2.js} +0 -0
  280. /package/client/dist/assets/{commands-Bl3-KVfj.js → commands-DuaQT-it.js} +0 -0
  281. /package/client/dist/assets/{commands-DASXJD-l.js → commands-MqNuGGO4.js} +0 -0
  282. /package/client/dist/assets/{commands-C5WQ9blx2.js → commands-oB9q2vy_2.js} +0 -0
  283. /package/client/dist/assets/{common-CHARW6MZ2.js → common-BXzqeuiC2.js} +0 -0
  284. /package/client/dist/assets/{common-0aY9HQBP.js → common-BaJ6MF7T.js} +0 -0
  285. /package/client/dist/assets/{common-CQCJzAQq2.js → common-C-jaOdxz2.js} +0 -0
  286. /package/client/dist/assets/{common-DNhhqjlJ.js → common-CKjy-R8-.js} +0 -0
  287. /package/client/dist/assets/{common-YdyZtmaI2.js → common-CRd-Ge5u2.js} +0 -0
  288. /package/client/dist/assets/{common-5jScBJqt.js → common-DPGR3Tfo.js} +0 -0
  289. /package/client/dist/assets/{common-BdaRciTu.js → common-wEB9RBdf.js} +0 -0
  290. /package/client/dist/assets/{explore-ChtxIiSM.js → explore-B_79NLR5.js} +0 -0
  291. /package/client/dist/assets/{explore-BzdMEpuf.js → explore-D-htKul_.js} +0 -0
  292. /package/client/dist/assets/{explore-B82VMUJ_.js → explore-D5b2jgvR.js} +0 -0
  293. /package/client/dist/assets/{explore-DfcUfiPw2.js → explore-Dvp-5qOw2.js} +0 -0
  294. /package/client/dist/assets/{explore-CbW0381i2.js → explore-FaKfWCr72.js} +0 -0
  295. /package/client/dist/assets/{explore-BGz1rvQE.js → explore-IS8VyaSZ.js} +0 -0
  296. /package/client/dist/assets/{explore-yRF7wFSp2.js → explore-OZhWV0Eb2.js} +0 -0
  297. /package/client/dist/assets/{format-command-D96da4fZ.js → format-command-CHImQv6W.js} +0 -0
  298. /package/client/dist/assets/{integrations-CnrX7vqw2.js → integrations-B97cGWfo2.js} +0 -0
  299. /package/client/dist/assets/{integrations-BJvrlcc7.js → integrations-Bp3leAh3.js} +0 -0
  300. /package/client/dist/assets/{integrations-CPKYkG36.js → integrations-C3Ma3GdM.js} +0 -0
  301. /package/client/dist/assets/{integrations-BA5pM05t.js → integrations-C5MUA-0-.js} +0 -0
  302. /package/client/dist/assets/{integrations-B0OBPU9_.js → integrations-CiQbCn_W.js} +0 -0
  303. /package/client/dist/assets/{integrations-DeJFRyXz2.js → integrations-Du_0rDfi2.js} +0 -0
  304. /package/client/dist/assets/{integrations-DBou159M2.js → integrations-t4X5UVcx2.js} +0 -0
  305. /package/client/dist/assets/{jira-mi37H4Kt.js → jira-B1XaR6fj.js} +0 -0
  306. /package/client/dist/assets/{jira-Srx2P5zS.js → jira-BCHx9zUF.js} +0 -0
  307. /package/client/dist/assets/{jira-DeotPx6P2.js → jira-BqXyykbt2.js} +0 -0
  308. /package/client/dist/assets/{jira-BwLBgmfI2.js → jira-BzFvFn7t2.js} +0 -0
  309. /package/client/dist/assets/{jira-BDgxAFTF2.js → jira-D6IDTIsc2.js} +0 -0
  310. /package/client/dist/assets/{jira-NwlUCcQD.js → jira-DSZ5xFRl.js} +0 -0
  311. /package/client/dist/assets/{jira-D3GGzXOI.js → jira-DdEKW9Pu.js} +0 -0
  312. /package/client/dist/assets/{jobs-D8adm2BX.js → jobs-BX9dP3Qn.js} +0 -0
  313. /package/client/dist/assets/{jobs-B56vQEkh2.js → jobs-BgHEgCWk2.js} +0 -0
  314. /package/client/dist/assets/{jobs-K_Y8KAZ2.js → jobs-CU-khJTt.js} +0 -0
  315. /package/client/dist/assets/{jobs-BYIk6Tg_.js → jobs-Ce0SE81t.js} +0 -0
  316. /package/client/dist/assets/{jobs-C-lM-iax2.js → jobs-CeOcGHd_2.js} +0 -0
  317. /package/client/dist/assets/{jobs-CYdevGgT.js → jobs-Ct5v2p6T.js} +0 -0
  318. /package/client/dist/assets/{jobs-OwM0ZWhI2.js → jobs-m7L0K9uC2.js} +0 -0
  319. /package/client/dist/assets/{mcp-D3E4_Mmk2.js → mcp-4QbfLPa4.js} +0 -0
  320. /package/client/dist/assets/{mcp-B6nwJJmR.js → mcp-CXy61khH.js} +0 -0
  321. /package/client/dist/assets/{mcp-CNgFiTEh.js → mcp-CmOBPDjE.js} +0 -0
  322. /package/client/dist/assets/{mcp-mupPuPDO2.js → mcp-NlLc9T8u2.js} +0 -0
  323. /package/client/dist/assets/{mcp-B4irfLJ2.js → mcp-_DUUXTHB2.js} +0 -0
  324. /package/client/dist/assets/{mcp-BP7PpbDr2.js → mcp-dlOyUQQy2.js} +0 -0
  325. /package/client/dist/assets/{mcp-BOoEGTSQ.js → mcp-zqSVP7nA.js} +0 -0
  326. /package/client/dist/assets/{specs-D4z5d8uq.js → specs-57pue7UQ.js} +0 -0
  327. /package/client/dist/assets/{specs-CWNKAScX2.js → specs-BWQ-0EXt2.js} +0 -0
  328. /package/client/dist/assets/{specs-B8mwScoM2.js → specs-BYSOlQRw2.js} +0 -0
  329. /package/client/dist/assets/{specs-B5XOoH-02.js → specs-CkP0B28v2.js} +0 -0
  330. /package/client/dist/assets/{specs-DgvIRZtM.js → specs-RSYuLgF4.js} +0 -0
  331. /package/client/dist/assets/{specs-DuRzE5wE.js → specs-d-zU7_3Q.js} +0 -0
  332. /package/client/dist/assets/{specs-Bj_3fDrQ.js → specs-nM5Cpcqe.js} +0 -0
  333. /package/client/dist/assets/{terminal-DoMTHedv2.js → terminal-Adi3W-GZ2.js} +0 -0
  334. /package/client/dist/assets/{terminal-DOwfkxqa2.js → terminal-BA6FCZna2.js} +0 -0
  335. /package/client/dist/assets/{terminal-CH0fdHvP.js → terminal-BCIUSkrg.js} +0 -0
  336. /package/client/dist/assets/{terminal-B2sExUis.js → terminal-C8KlD61D.js} +0 -0
  337. /package/client/dist/assets/{terminal-DjSSa5qP.js → terminal-D3RTuBwy.js} +0 -0
  338. /package/client/dist/assets/{terminal-w0i-OCp32.js → terminal-DCkOEjSj2.js} +0 -0
  339. /package/client/dist/assets/{terminal-CPMigUX7.js → terminal-DOuNm2BI.js} +0 -0
  340. /package/client/dist/assets/{tickets-D00mSt832.js → tickets-B2q36GlQ2.js} +0 -0
  341. /package/client/dist/assets/{tickets-CQTana1u.js → tickets-Bsyv_znj.js} +0 -0
  342. /package/client/dist/assets/{tickets-D7CSuWjO.js → tickets-C_UPra_1.js} +0 -0
  343. /package/client/dist/assets/{tickets-heDAaj8C2.js → tickets-D72LbUtU2.js} +0 -0
  344. /package/client/dist/assets/{tickets-DvbFg_-c2.js → tickets-DDjU7UfE2.js} +0 -0
  345. /package/client/dist/assets/{tickets-onKZz6Dd.js → tickets-DR4ZrPMf.js} +0 -0
  346. /package/client/dist/assets/{tickets-Dq7HDy8n.js → tickets-DiUA3rXq.js} +0 -0
@@ -21,7 +21,7 @@ SpecsBoard (left) Rails (right)
21
21
 
22
22
  A **rail** is an execution lane. Drag a spec card from the SpecsBoard onto a rail and press **▶ Play** to launch the pipeline. Rails let you organise and queue work into named lanes.
23
23
 
24
- Rails run on **Claude** by default. If your project has more than one provider installed, a per-rail engine selector lets you launch a rail on **Codex** or **Gemini** instead — see [Using Codex](codex.md) and [Using Gemini](gemini.md). Only the **Ultracode** loop is Claude-only; the standard Implement and Batch loops run on any installed provider.
24
+ Rails run on **Claude** by default. If your project has more than one provider installed, a per-rail engine selector lets you launch a rail on **Codex** or **Gemini** instead — see [Using Codex](codex.md) and [Using Gemini](gemini.md). Only the **Freestyle** loop is Claude-only; the standard Implement and Batch loops run on any installed provider.
25
25
 
26
26
  > **One job at a time per project.** Each project has a single queue, so within a project only one rail job runs at a time; the rest queue behind it. Real parallelism is **across projects** — open two projects and their rails run independently. See [Running multiple rails](#running-multiple-rails).
27
27
 
@@ -31,7 +31,7 @@ Each rail has a header with:
31
31
 
32
32
  - **Status pill** — `idle`, `running`, or `failed`. (There's no separate "completed" state — a rail returns to `idle` when its job finishes cleanly.)
33
33
  - **Spec list** — the IDs of the specs assigned to this rail. Drag in more, drag out to detach. You can also use the **Move to rail** popover from a spec card; it shows a status dot per rail so you don't push work onto a busy lane.
34
- - **Loop picker** — the **Loop** this rail runs: a built-in (`Implement`, `Batch`, or `Ultracode`) or one of your published custom loops. See [Loops](#loops).
34
+ - **Loop picker** — the **Loop** this rail runs: a built-in (`Implement`, `Batch`, or `Freestyle`) or one of your published custom loops. See [Loops](#loops).
35
35
  - **Profile picker** — which agent profile this rail uses. This only appears once the project has **at least one** profile (create them on the Agents page). When present, `No profile` runs the rail in legacy mode.
36
36
  - **Engine selector** — pick which installed provider (Claude, Codex, or Gemini) runs this rail. Only renders on projects with more than one provider installed.
37
37
  - **Play / Stop button** — start or cancel.
@@ -44,7 +44,7 @@ A rail runs a **Loop**, picked in the rail header and persisted per rail. The th
44
44
  |------|---------|--------------|
45
45
  | **Implement** | `/specrails:implement` | One job covering all the specs on the rail. Runs the full Architect → Developer → Reviewer → Ship pipeline. |
46
46
  | **Batch** | `/specrails:batch-implement` | One job that works through the rail's specs sequentially, in dependency-aware waves. |
47
- | **Ultracode** | (Ultracode) | Claude implements each spec autonomously, **bypassing** the OpenSpec pipeline. One independent job per spec. Claude only. |
47
+ | **Freestyle** | (Freestyle) | Claude implements each spec autonomously, **bypassing** the OpenSpec pipeline. One independent job per spec. Claude only. |
48
48
 
49
49
  Beyond the built-ins, the **Loops** section (left sidebar, above the project list) is a global, n8n-style **visual builder** shared across all your projects. A loop is a graph of typed steps:
50
50
 
@@ -76,17 +76,17 @@ Each phase is a specialised agent invoked by the rail's engine (Claude, Codex, o
76
76
 
77
77
  In plain terms: the project's **agent profile** decides which AI agent handles each phase. The baseline trio (`sr-architect`, `sr-developer`, `sr-reviewer`) is always present; a profile's routing rules can add extra agents or swap which one runs a phase. The phase progress bar only renders when the command defines phases. For the full format, see [internals/profiles.md](internals/profiles.md).
78
78
 
79
- ### Ultracode
79
+ ### Freestyle
80
80
 
81
- `Ultracode` is a Claude-only loop that skips the Architect → Developer → Reviewer → Ship pipeline entirely. Instead of orchestrating the agent chain, it hands Claude a configurable pre-prompt plus the full spec text and lets it work autonomously with its native tools.
81
+ `Freestyle` (mode value `ultracode` on the API — its original name) is a Claude-only loop that skips the Architect → Developer → Reviewer → Ship pipeline entirely. Instead of orchestrating the agent chain, it hands Claude a configurable pre-prompt plus the full spec text and lets it work autonomously with its native tools.
82
82
 
83
- - **One job per spec.** If the rail has three specs, `Ultracode` launches three independent jobs.
83
+ - **One job per spec.** If the rail has three specs, `Freestyle` launches three independent jobs.
84
84
  - **Variable cost.** Because the run is open-ended, pressing Play opens a confirmation dialog before anything spawns.
85
- - **Model picker.** A per-rail control lets you pick **Haiku / Sonnet / Opus** (default Sonnet) for the Ultracode run.
86
- - **Claude only.** The Ultracode loop and its model picker are offered only when the rail's engine is Claude. Codex and Gemini rails can't run Ultracode, and agent profiles don't apply to Ultracode rails.
87
- - **Interactive (optional).** An `Interactive` toggle next to the Ultracode launch turns the run into a back-and-forth session: you can chat with the running job, send follow-up prompts, and click **Finalize** when you're done. Without it, the job runs to completion on its own. (Available when the rail is idle; can be disabled server-side via `SPECRAILS_INTERACTIVE_JOBS=false`.)
85
+ - **Model picker.** A per-rail control lets you pick **Haiku / Sonnet / Opus** (default Sonnet) for the Freestyle run.
86
+ - **Claude only.** The Freestyle loop and its model picker are offered only when the rail's engine is Claude. Codex and Gemini rails can't run Freestyle, and agent profiles don't apply to Freestyle rails.
87
+ - **Interactive (optional).** An `Interactive` toggle next to the Freestyle launch turns the run into a back-and-forth session: you can chat with the running job, send follow-up prompts, and click **Finalize** when you're done. Without it, the job runs to completion on its own. (Available when the rail is idle; can be disabled server-side via `SPECRAILS_INTERACTIVE_JOBS=false`.)
88
88
 
89
- You can customise the Ultracode pre-prompt per project on the [Settings page](customizing.md#ultracode-pre-prompt).
89
+ You can customise the Freestyle pre-prompt per project on the [Settings page](customizing.md#freestyle-pre-prompt).
90
90
 
91
91
  ### Running multiple rails
92
92
 
@@ -8,7 +8,7 @@ Six surfaces, all per-project:
8
8
 
9
9
  | Surface | What it captures |
10
10
  |---------|------------------|
11
- | **`job`** | Every rail run (the Architect → Developer → Reviewer → Ship pipeline, plus Ultracode and Batch runs) |
11
+ | **`job`** | Every rail run (the Architect → Developer → Reviewer → Ship pipeline, plus Freestyle and Batch runs) |
12
12
  | **`quick-spec`** | One-shot Quick spec generation (including a Quick-mode Contract Refine run) |
13
13
  | **`explore-spec`** | Every Explore conversation turn (including the Explore Contract Refine run) |
14
14
  | **`ai-edit`** | The **AI Edit / Refine** overlay (refining an agent or a ticket) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-desktop",
3
- "version": "2.18.2",
3
+ "version": "2.19.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -4,41 +4,22 @@ exports.AgentChatManager = void 0;
4
4
  const providers_1 = require("./providers");
5
5
  const spawn_lifecycle_1 = require("./spawn-lifecycle");
6
6
  const agent_cwd_manager_1 = require("./agent-cwd-manager");
7
+ const agent_operator_prompt_1 = require("./agent-operator-prompt");
7
8
  const agent_mcp_config_1 = require("./agent-mcp-config");
8
9
  const agent_tier_1 = require("./agent-tier");
10
+ const attachment_manager_1 = require("./attachment-manager");
9
11
  const agent_store_1 = require("./agent-store");
12
+ const explore_draft_title_1 = require("./explore-draft-title");
10
13
  const types_1 = require("./mcp/tools/types");
11
- // ─── AgentChatManager (design D1) ─────────────────────────────────────────────
12
- //
13
- // App-level sibling of ChatManager: spawns an AI CLI from the app-owned agent cwd
14
- // with the Specrails MCP bridge as its tool source, streams the turn over the
15
- // app-global `agent_*` WS events (no projectId — fans to all subscribers), and
16
- // persists to the app registry DB. It reuses the shared spawn→stream→settle core
17
- // (runAiCliInvocation) rather than re-implementing ChatManager's loop.
18
- const OPERATOR_SYSTEM_PROMPT = 'You are the Specrails operator agent. Drive the Specrails Desktop app on the ' +
19
- "user's behalf using the specrails_* MCP tools. Target a project with " +
20
- 'specrails_select_project (or the projectId argument); if none is selected and ' +
21
- 'the request is project-specific, ask whether to create a project or search all. ' +
22
- 'Follow HTTP-202 actions to completion with specrails_watch. Respect the ' +
23
- 'permission ladder: if a tool is refused for the current level, tell the user ' +
24
- 'which level it needs rather than working around it. Be concise; report tool ' +
25
- 'outputs faithfully, including failures. Format replies for easy reading: ' +
26
- 'separate distinct ideas into short paragraphs with a blank line between them ' +
27
- '(not one dense block), and use bullet lists for enumerations. ' +
28
- 'When the user has no projects yet, or wants to add one, first explain the UI ' +
29
- 'steps (click "Add Project" / the + in the left sidebar, enter the repo\'s ' +
30
- 'folder path, pick an AI provider, then run setup), THEN offer to do it for ' +
31
- 'them from here: ask for the repo folder path and which AI providers to set up, ' +
32
- 'then with their go-ahead call specrails_setup add_project (Edit level) and a ' +
33
- 'single specrails_setup install (Operate level) — one install provisions ALL the ' +
34
- 'chosen providers in one shot (no need to install them one by one). Setup is ' +
35
- 'QUICK-only (offline); do NOT offer or mention a "full" or "enrich" install. Use ' +
36
- 'specrails_setup prerequisites / available_providers first to check the machine.';
37
14
  class AgentChatManager {
38
15
  _broadcast;
39
16
  _db;
40
17
  _port;
41
18
  _active = new Map();
19
+ /** Conversations with a turn in-flight but not yet spawned. Closes the TOCTOU
20
+ * window the attachment-extraction await opens between the busy guard and
21
+ * `_active.set` in onSpawn (mirrors ChatManager's reservation pattern). */
22
+ _reserved = new Set();
42
23
  constructor(broadcast, db, port) {
43
24
  this._broadcast = broadcast;
44
25
  this._db = db;
@@ -59,10 +40,20 @@ class AgentChatManager {
59
40
  this._emitError(conversationId, 'Unknown conversation');
60
41
  return;
61
42
  }
62
- if (this._active.has(conversationId)) {
43
+ if (this._active.has(conversationId) || this._reserved.has(conversationId)) {
63
44
  this._emitError(conversationId, 'The agent is busy. Try again in a moment.');
64
45
  return;
65
46
  }
47
+ this._reserved.add(conversationId);
48
+ try {
49
+ await this._runTurn(conversation, userText, options);
50
+ }
51
+ finally {
52
+ this._reserved.delete(conversationId);
53
+ }
54
+ }
55
+ async _runTurn(conversation, userText, options) {
56
+ const conversationId = conversation.id;
66
57
  const tierLevel = (0, agent_tier_1.normalizeLevel)(options.tierLevel ?? conversation.tier_level);
67
58
  const adapter = (0, providers_1.getAdapter)(conversation.provider);
68
59
  // Resolve a model that is VALID for this provider. A stale model from another
@@ -71,15 +62,63 @@ class AgentChatManager {
71
62
  const catalog = new Set(adapter.modelCatalog().map((m) => m.value));
72
63
  const requested = options.model || conversation.model;
73
64
  const model = requested && catalog.has(requested) ? requested : adapter.defaultModel();
65
+ // Reasoning effort: stored per conversation; null = the app default
66
+ // ("medium"). Passed only to providers with a per-spawn knob, and only when
67
+ // the value is in the provider's catalog (a provider switch clears it, but
68
+ // belt-and-braces against stale rows).
69
+ const efforts = (adapter.capabilities.reasoningEfforts ?? []);
70
+ const storedEffort = conversation.reasoning_effort;
71
+ const reasoningEffort = efforts.length
72
+ ? (storedEffort && efforts.includes(storedEffort) ? storedEffort : 'medium')
73
+ : undefined;
74
74
  // Make the pinned project (Cursor-style selector) the MCP active project for
75
75
  // this turn, so project-scoped tools resolve without the agent having to call
76
76
  // specrails_select_project. Home (null) clears it → app-global mode.
77
77
  (0, types_1.setActiveProject)(conversation.pinned_project_id ?? null);
78
- (0, agent_store_1.addAgentMessage)(this._db, { conversationId, role: 'user', content: userText });
79
- // Tell the agent which project is pinned (so it phrases + passes projectId).
80
- const prompt = conversation.pinned_project_id
81
- ? `[Active project: projectId="${conversation.pinned_project_id}". Use it for project-scoped tools unless told otherwise.]\n\n${userText}`
82
- : userText;
78
+ // Resolve attachments (conversation-keyed) into extracted text blocks +
79
+ // absolute image paths. Extraction failure degrades to a text-only turn.
80
+ const attachmentIds = options.attachmentIds ?? [];
81
+ let userWithAttachments = userText;
82
+ let imagePaths = [];
83
+ let hasAttachments = false;
84
+ if (attachmentIds.length > 0) {
85
+ try {
86
+ const resolved = await attachment_manager_1.attachmentManager.getClaudeArgsAgent(conversationId, attachmentIds);
87
+ if (resolved.textBlocks.length > 0) {
88
+ userWithAttachments = `${userText}\n\n## Attached Resources\n\n${resolved.textBlocks.join('\n\n')}`;
89
+ hasAttachments = true;
90
+ }
91
+ imagePaths = resolved.imagePaths;
92
+ }
93
+ catch (err) {
94
+ console.error(`[agent-chat] attachment extraction failed (${conversationId}):`, err);
95
+ }
96
+ }
97
+ // The conversation may have been deleted while attachments were extracting
98
+ // (DELETE aborts the child and drops the row) — inserting would violate the FK.
99
+ if (!(0, agent_store_1.getAgentConversation)(this._db, conversationId))
100
+ return;
101
+ (0, agent_store_1.addAgentMessage)(this._db, { conversationId, role: 'user', content: userText, attachmentIds });
102
+ this._autoTitle(conversationId, conversation.title);
103
+ // Providers WITHOUT a --system-prompt flag (codex, gemini) drop opts.systemPrompt
104
+ // for chat turns, so the attachment prompt-injection note would never reach them —
105
+ // fold it into the user turn instead (same capability-gated pattern as ChatManager).
106
+ if (hasAttachments && !adapter.capabilities.systemPromptArg) {
107
+ userWithAttachments = `${attachment_manager_1.USER_ATTACHMENT_SYSTEM_NOTE}\n\n${userWithAttachments}`;
108
+ }
109
+ // Per-turn dynamic context (pinned project, permission level, provider) rides
110
+ // the USER turn, never the system prompt — byte-stability contract (see
111
+ // agent-operator-prompt.ts).
112
+ const contextPrefix = conversation.pinned_project_id
113
+ ? `[Active project: projectId="${conversation.pinned_project_id}" | Permission level: ${tierLevel} | Provider: ${adapter.id}. Use the project for project-scoped tools unless told otherwise.]`
114
+ : `[No project pinned (Home) | Permission level: ${tierLevel} | Provider: ${adapter.id}]`;
115
+ const prompt = `${contextPrefix}\n\n${userWithAttachments}`;
116
+ const systemPrompt = hasAttachments
117
+ ? `${agent_operator_prompt_1.OPERATOR_SYSTEM_PROMPT}\n\n${attachment_manager_1.USER_ATTACHMENT_SYSTEM_NOTE}`
118
+ : agent_operator_prompt_1.OPERATOR_SYSTEM_PROMPT;
119
+ // Only pass native image paths to providers that can vision-load them (codex
120
+ // `--image`); claude/gemini already have the `@path` ref folded into prompt.
121
+ const spawnImagePaths = adapter.capabilities.supportsImageInput ? imagePaths : undefined;
83
122
  const cwd = (0, agent_cwd_manager_1.ensureAgentCwd)();
84
123
  let mcpArgs = [];
85
124
  let mcpEnv = {};
@@ -114,10 +153,12 @@ class AgentChatManager {
114
153
  env: { ...process.env, ...mcpEnv },
115
154
  buildOpts: {
116
155
  prompt,
117
- systemPrompt: adapter.capabilities.systemPromptArg ? OPERATOR_SYSTEM_PROMPT : undefined,
156
+ systemPrompt: adapter.capabilities.systemPromptArg ? systemPrompt : undefined,
118
157
  model,
119
158
  sessionId: useResume ? conversation.session_id ?? undefined : undefined,
120
159
  extraArgs: mcpArgs,
160
+ imagePaths: spawnImagePaths,
161
+ reasoning_effort: reasoningEffort,
121
162
  },
122
163
  onSpawn: (child) => {
123
164
  this._active.set(conversationId, child);
@@ -160,6 +201,10 @@ class AgentChatManager {
160
201
  };
161
202
  const canResume = !!conversation.session_id && adapter.capabilities.nativeResume;
162
203
  let r = await invoke(canResume);
204
+ // Deleted mid-turn (DELETE aborts the child and drops the row): stop here —
205
+ // no auto-heal respawn, no FK-violating assistant INSERT.
206
+ if (!(0, agent_store_1.getAgentConversation)(this._db, conversationId))
207
+ return;
163
208
  // Auto-heal a stale/foreign session: a resume that produced no text (e.g. a
164
209
  // provider switch leaving another provider's session id, or codex
165
210
  // "no rollout found for thread id") retries once as a fresh turn.
@@ -167,6 +212,8 @@ class AgentChatManager {
167
212
  console.log(`[agent-chat] resume produced no text — retrying fresh conv=${conversationId}`);
168
213
  (0, agent_store_1.updateAgentConversation)(this._db, conversationId, { session_id: null });
169
214
  r = await invoke(false);
215
+ if (!(0, agent_store_1.getAgentConversation)(this._db, conversationId))
216
+ return;
170
217
  }
171
218
  if (r.spawnFailed) {
172
219
  this._emitError(conversationId, `Failed to launch ${adapter.binary}. Is it installed and on PATH?`);
@@ -191,6 +238,33 @@ class AgentChatManager {
191
238
  (r.code !== 0 ? `${adapter.binary} exited with code ${r.code}` : 'The agent returned no output.');
192
239
  this._emitError(conversationId, reason);
193
240
  }
241
+ /**
242
+ * Auto-title a conversation from its first TWO user prompts (deterministic —
243
+ * no AI spend). Turn 1 sets the title when none exists; turn 2 refines it,
244
+ * but ONLY while the current title is still turn 1's auto title (a manual
245
+ * rename is recomputable-detectable and never clobbered). Broadcasts
246
+ * `agent_title` so the sidebar list updates live.
247
+ */
248
+ _autoTitle(conversationId, currentTitle) {
249
+ try {
250
+ const userMsgs = (0, agent_store_1.listAgentMessages)(this._db, conversationId).filter((m) => m.role === 'user');
251
+ if (userMsgs.length === 0 || userMsgs.length > 2)
252
+ return;
253
+ const autoFromFirst = (0, explore_draft_title_1.generateAutoTitle)([{ role: 'user', content: userMsgs[0].content }]);
254
+ const stillAuto = currentTitle === null || (userMsgs.length === 2 && currentTitle === autoFromFirst);
255
+ if (!stillAuto)
256
+ return;
257
+ const source = userMsgs.map((m) => m.content).join(' — ');
258
+ const title = (0, explore_draft_title_1.generateAutoTitle)([{ role: 'user', content: source }]);
259
+ if (!title || title === currentTitle)
260
+ return;
261
+ (0, agent_store_1.updateAgentConversation)(this._db, conversationId, { title });
262
+ this._broadcast({ type: 'agent_title', conversationId, title, timestamp: new Date().toISOString() });
263
+ }
264
+ catch (err) {
265
+ console.error(`[agent-chat] auto-title failed (${conversationId}):`, err);
266
+ }
267
+ }
194
268
  /** Aborts the active turn for a conversation, if any. */
195
269
  abort(conversationId) {
196
270
  const child = this._active.get(conversationId);
@@ -1,10 +1,16 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.isAgentChatEnabled = isAgentChatEnabled;
4
7
  exports.createAgentChatRouter = createAgentChatRouter;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const multer_1 = __importDefault(require("multer"));
5
10
  const express_1 = require("express");
6
11
  const providers_1 = require("./providers");
7
12
  const agent_tier_1 = require("./agent-tier");
13
+ const attachment_manager_1 = require("./attachment-manager");
8
14
  const agent_store_1 = require("./agent-store");
9
15
  function isAgentChatEnabled() {
10
16
  return process.env.SPECRAILS_AGENT_CHAT !== 'false';
@@ -20,6 +26,20 @@ function validProvider(provider) {
20
26
  return null;
21
27
  }
22
28
  }
29
+ const attachmentUpload = (0, multer_1.default)({
30
+ storage: multer_1.default.memoryStorage(),
31
+ limits: { fileSize: 25 * 1024 * 1024 },
32
+ fileFilter: (req, file, cb) => {
33
+ if ((0, attachment_manager_1.isSupportedUploadedFile)({ mimetype: file.mimetype, originalname: file.originalname })) {
34
+ cb(null, true);
35
+ }
36
+ else {
37
+ ;
38
+ req.fileRejected = file.mimetype || file.originalname || 'unknown';
39
+ cb(null, false);
40
+ }
41
+ },
42
+ });
23
43
  function createAgentChatRouter(deps) {
24
44
  const { manager, desktopDb } = deps;
25
45
  const router = (0, express_1.Router)();
@@ -34,8 +54,27 @@ function createAgentChatRouter(deps) {
34
54
  // Per-provider model catalog for the header model selector.
35
55
  router.get('/models', (req, res) => {
36
56
  const provider = validProvider(req.query.provider) ?? 'claude';
37
- res.json({ provider, models: (0, providers_1.getAdapter)(provider).modelCatalog() });
57
+ const adapter = (0, providers_1.getAdapter)(provider);
58
+ res.json({
59
+ provider,
60
+ models: adapter.modelCatalog(),
61
+ // The composer gates the image affordance on this (design D22: capability,
62
+ // never provider id — gemini stays false until live-verified).
63
+ supportsImageInput: adapter.capabilities.supportsImageInput === true,
64
+ // Per-provider reasoning-effort tiers (ascending). Empty ⇒ no selector
65
+ // (gemini has no per-spawn knob).
66
+ efforts: adapter.capabilities.reasoningEfforts ?? [],
67
+ });
38
68
  });
69
+ /** Validate a requested reasoning effort against a provider's catalog. */
70
+ const validEffort = (provider, value) => {
71
+ if (value === null)
72
+ return null;
73
+ if (typeof value !== 'string')
74
+ return undefined;
75
+ const efforts = (0, providers_1.getAdapter)(provider).capabilities.reasoningEfforts ?? [];
76
+ return efforts.includes(value) ? value : undefined;
77
+ };
39
78
  router.get('/conversations', (_req, res) => {
40
79
  res.json({ conversations: (0, agent_store_1.listAgentConversations)(desktopDb) });
41
80
  });
@@ -55,6 +94,8 @@ function createAgentChatRouter(deps) {
55
94
  model: typeof body.model === 'string' ? body.model : null,
56
95
  pinnedProjectId: typeof body.pinnedProjectId === 'string' ? body.pinnedProjectId : null,
57
96
  tierLevel: body.tierLevel !== undefined ? (0, agent_tier_1.normalizeLevel)(body.tierLevel) : 0,
97
+ // Off-catalog values fall back to null (= app default "medium").
98
+ reasoningEffort: validEffort(provider, body.reasoningEffort) ?? null,
58
99
  });
59
100
  res.status(201).json({ conversation });
60
101
  });
@@ -88,13 +129,15 @@ function createAgentChatRouter(deps) {
88
129
  return;
89
130
  }
90
131
  patch.provider = v;
91
- // Switching provider invalidates the stored session id AND model (both
92
- // belong to the previous provider) — clear them so the next turn starts a
93
- // fresh session with the new provider's default model (codex rejects
94
- // claude's "sonnet"; resume of a foreign thread → "no rollout found").
132
+ // Switching provider invalidates the stored session id, model AND effort
133
+ // (all belong to the previous provider) — clear them so the next turn
134
+ // starts fresh with the new provider's defaults (codex rejects claude's
135
+ // "sonnet"; claude's "xhigh" is off-catalog for codex; resume of a
136
+ // foreign thread → "no rollout found").
95
137
  if (v !== conversation.provider) {
96
138
  patch.session_id = null;
97
139
  patch.model = null;
140
+ patch.reasoning_effort = null;
98
141
  }
99
142
  }
100
143
  // Explicit model pick — validated against the (new or current) provider's catalog.
@@ -112,13 +155,127 @@ function createAgentChatRouter(deps) {
112
155
  patch.model = body.model;
113
156
  }
114
157
  }
158
+ // Explicit effort pick — validated against the (new or current) provider's
159
+ // effort catalog; null resets to the app default ("medium").
160
+ if (body.reasoningEffort !== undefined && patch.reasoning_effort === undefined) {
161
+ const effectiveProvider = patch.provider ?? conversation.provider;
162
+ const v = validEffort(effectiveProvider, body.reasoningEffort);
163
+ if (v === undefined) {
164
+ res.status(400).json({ error: 'Effort not available for this provider' });
165
+ return;
166
+ }
167
+ patch.reasoning_effort = v;
168
+ }
115
169
  res.json({ conversation: (0, agent_store_1.updateAgentConversation)(desktopDb, conversation.id, patch) });
116
170
  });
117
171
  router.delete('/conversations/:id', (req, res) => {
118
- manager.abort(String(req.params.id));
119
- (0, agent_store_1.deleteAgentConversation)(desktopDb, String(req.params.id));
172
+ const id = String(req.params.id);
173
+ manager.abort(id);
174
+ (0, agent_store_1.deleteAgentConversation)(desktopDb, id);
175
+ // Cascade-remove the conversation's attachment directory (best-effort).
176
+ void attachment_manager_1.attachmentManager.deleteAllAgent(id).catch((e) => console.error('[agent-chat] attachment cleanup failed:', e));
120
177
  res.json({ ok: true });
121
178
  });
179
+ // ── Attachments (conversation-keyed; mirror the ticket attachment routes) ──
180
+ router.post('/conversations/:id/attachments', attachmentUpload.single('file'), (err, _req, res, next) => {
181
+ if (err instanceof multer_1.default.MulterError) {
182
+ const msg = err.code === 'LIMIT_FILE_SIZE' ? 'File too large (max 25 MB)' : `Upload rejected: ${err.code}`;
183
+ res.status(400).json({ error: msg });
184
+ return;
185
+ }
186
+ if (err) {
187
+ next(err);
188
+ return;
189
+ }
190
+ next();
191
+ }, async (req, res) => {
192
+ const id = String(req.params.id);
193
+ if (!(0, agent_store_1.getAgentConversation)(desktopDb, id)) {
194
+ res.status(404).json({ error: 'Unknown conversation' });
195
+ return;
196
+ }
197
+ const file = req.file;
198
+ if (!file) {
199
+ const rejected = req.fileRejected;
200
+ res.status(400).json({ error: rejected ? `Unsupported file type: ${rejected}` : 'No file uploaded' });
201
+ return;
202
+ }
203
+ try {
204
+ const attachment = await attachment_manager_1.attachmentManager.uploadAgent({
205
+ conversationId: id,
206
+ file: { buffer: file.buffer, originalname: file.originalname, mimetype: file.mimetype, size: file.size },
207
+ });
208
+ res.status(201).json({ attachment });
209
+ }
210
+ catch (e) {
211
+ const status = e.status ?? 500;
212
+ console.error('[agent-chat] attachment upload error:', e);
213
+ res.status(status).json({ error: e instanceof Error ? e.message : 'Upload failed' });
214
+ }
215
+ });
216
+ router.get('/conversations/:id/attachments', (req, res) => {
217
+ const id = String(req.params.id);
218
+ if (!(0, agent_store_1.getAgentConversation)(desktopDb, id)) {
219
+ res.status(404).json({ error: 'Unknown conversation' });
220
+ return;
221
+ }
222
+ res.json({ attachments: attachment_manager_1.attachmentManager.listAgent(id) });
223
+ });
224
+ router.get('/conversations/:id/attachments/:attachmentId', (req, res) => {
225
+ const id = String(req.params.id);
226
+ if (!(0, agent_store_1.getAgentConversation)(desktopDb, id)) {
227
+ res.status(404).json({ error: 'Unknown conversation' });
228
+ return;
229
+ }
230
+ const attachmentId = String(req.params.attachmentId);
231
+ let meta = null;
232
+ let abs = null;
233
+ try {
234
+ meta = attachment_manager_1.attachmentManager.getAgentMeta(id, attachmentId);
235
+ abs = meta ? attachment_manager_1.attachmentManager.getAgentFilePath(id, attachmentId) : null;
236
+ }
237
+ catch {
238
+ res.status(400).json({ error: 'Invalid attachment id' });
239
+ return;
240
+ }
241
+ if (!meta || !abs) {
242
+ res.status(404).json({ error: 'Attachment not found' });
243
+ return;
244
+ }
245
+ res.setHeader('Content-Type', meta.mimeType);
246
+ const asciiName = meta.filename.replace(/[\r\n"]/g, '_');
247
+ res.setHeader('Content-Disposition', `inline; filename="${asciiName}"; filename*=UTF-8''${encodeURIComponent(meta.filename)}`);
248
+ // The existsSync check above is not atomic with the stream's async open — a
249
+ // concurrent conversation/attachment DELETE can rm the file in between, and an
250
+ // unhandled ReadStream 'error' would take down the sidecar.
251
+ const stream = fs_1.default.createReadStream(abs);
252
+ stream.on('error', () => {
253
+ if (!res.headersSent)
254
+ res.status(404).json({ error: 'Attachment not found' });
255
+ else
256
+ res.destroy();
257
+ });
258
+ stream.pipe(res);
259
+ });
260
+ router.delete('/conversations/:id/attachments/:attachmentId', async (req, res) => {
261
+ const id = String(req.params.id);
262
+ if (!(0, agent_store_1.getAgentConversation)(desktopDb, id)) {
263
+ res.status(404).json({ error: 'Unknown conversation' });
264
+ return;
265
+ }
266
+ try {
267
+ const ok = await attachment_manager_1.attachmentManager.deleteAgent(id, String(req.params.attachmentId));
268
+ if (!ok) {
269
+ res.status(404).json({ error: 'Attachment not found' });
270
+ return;
271
+ }
272
+ res.status(204).end();
273
+ }
274
+ catch (e) {
275
+ console.error('[agent-chat] attachment delete error:', e);
276
+ res.status(400).json({ error: 'Delete failed' });
277
+ }
278
+ });
122
279
  router.post('/conversations/:id/send', (req, res) => {
123
280
  const conversation = (0, agent_store_1.getAgentConversation)(desktopDb, String(req.params.id));
124
281
  if (!conversation) {
@@ -133,11 +290,16 @@ function createAgentChatRouter(deps) {
133
290
  }
134
291
  const tierLevel = body.tierLevel !== undefined ? (0, agent_tier_1.normalizeLevel)(body.tierLevel) : undefined;
135
292
  const model = typeof body.model === 'string' ? body.model : undefined;
293
+ let attachmentIds = [];
294
+ const rawAtt = body.attachments;
295
+ if (rawAtt && typeof rawAtt === 'object' && Array.isArray(rawAtt.ids)) {
296
+ attachmentIds = (rawAtt.ids).filter((x) => typeof x === 'string');
297
+ }
136
298
  // Fire-and-forget: the turn streams over WS. Persist the chosen tier first so
137
299
  // a refresh mid-turn restores the right level.
138
300
  if (tierLevel !== undefined)
139
301
  (0, agent_store_1.updateAgentConversation)(desktopDb, conversation.id, { tier_level: tierLevel });
140
- void manager.sendMessage(conversation.id, text, { tierLevel, model });
302
+ void manager.sendMessage(conversation.id, text, { tierLevel, model, attachmentIds }).catch((e) => console.error('[agent-chat] send failed:', e));
141
303
  res.status(202).json({ accepted: true });
142
304
  });
143
305
  router.post('/conversations/:id/abort', (req, res) => {
@@ -9,6 +9,7 @@ exports.removeAgentCwd = removeAgentCwd;
9
9
  const fs_1 = __importDefault(require("fs"));
10
10
  const path_1 = __importDefault(require("path"));
11
11
  const os_1 = __importDefault(require("os"));
12
+ const agent_operator_prompt_1 = require("./agent-operator-prompt");
12
13
  // ─── App-level agent working directory (design D1) ────────────────────────────
13
14
  //
14
15
  // The agent chat spawns the AI CLI from an app-owned cwd (NOT a project path), so
@@ -23,52 +24,6 @@ function agentCwdPath() {
23
24
  return path_1.default.join(homeDir(), '.specrails', 'agent-cwd');
24
25
  }
25
26
  const INSTRUCTION_FILES = ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md'];
26
- const OPERATOR_INSTRUCTIONS = `# Specrails Operator Agent
27
-
28
- You are the in-app Specrails operator. You drive the Specrails Desktop app on the
29
- user's behalf through the \`specrails_*\` MCP tools (projects, specs, rails, jobs,
30
- chat, agents, plugins, jira, loops, code, analytics, setup, settings).
31
-
32
- ## How to work
33
- - Use \`specrails_select_project\` (or the projectId argument) to target a project.
34
- If no project is selected ("Home") and the request is project-specific, ASK the
35
- user whether to create a new project or search across all projects — do not guess.
36
- - For long-running actions that return HTTP 202, use \`specrails_watch\` to follow
37
- them to completion instead of assuming they finished.
38
- - Prefer the meta tools \`specrails_guide\` / \`specrails_search\` / \`specrails_describe\`
39
- to discover the exact action/arguments before calling a domain tool.
40
-
41
- ## Permission ladder
42
- Your actions are gated by a cumulative level the user sets live:
43
- observe (read) ▸ edit (write) ▸ operate (launch AI, costs money) ▸ autonomous
44
- (delete/kill). If a tool is refused for the current level, tell the user which
45
- level it needs — do not try to work around the refusal.
46
-
47
- ## Stance
48
- Be concise and action-oriented. Confirm what you did with concrete references
49
- (ids, names). Never fabricate results — report tool outputs faithfully, including
50
- failures.
51
-
52
- ## Formatting
53
- Make replies easy to read: separate distinct ideas into short paragraphs with a
54
- blank line between them (avoid one dense block of text), and use bullet lists for
55
- enumerations. Give the conversation some breathing room.
56
-
57
- ## Adding a project
58
- When the user has no projects yet, or asks to add one:
59
- 1. Explain the UI path: click "Add Project" (the + in the left sidebar), enter the
60
- repo's folder path, pick an AI provider, then run setup.
61
- 2. Then OFFER to do it for them from here: ask for the repo folder path and which
62
- AI providers to set up, then with their go-ahead call
63
- \`specrails_setup(add_project, path, providers)\` (Edit level) and a SINGLE
64
- \`specrails_setup(install, projectId)\` (Operate level) — one install provisions
65
- ALL the chosen providers in one shot (do not install them one at a time).
66
- Use \`specrails_setup(prerequisites)\` / \`available_providers\` first to confirm
67
- the machine is ready.
68
-
69
- Setup is QUICK-only (fast, offline). Do NOT offer, mention, or attempt a "full" or
70
- "enrich" install — that flow is deprecated and not available through you.
71
- `;
72
27
  /**
73
28
  * Ensures the app-level agent cwd exists with operator instruction files.
74
29
  * Idempotent; safe to call on every turn. Returns the cwd path.
@@ -79,7 +34,7 @@ function ensureAgentCwd() {
79
34
  for (const name of INSTRUCTION_FILES) {
80
35
  // App-owned files (not user-edited) — always (re)write so operator-prompt
81
36
  // updates take effect instead of being pinned to the first materialization.
82
- fs_1.default.writeFileSync(path_1.default.join(dir, name), OPERATOR_INSTRUCTIONS, 'utf-8');
37
+ fs_1.default.writeFileSync(path_1.default.join(dir, name), agent_operator_prompt_1.OPERATOR_INSTRUCTIONS, 'utf-8');
83
38
  }
84
39
  return dir;
85
40
  }