factory-factory 0.2.4 → 0.2.5

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 (287) hide show
  1. package/dist/client/assets/{_basePickBy-CCB8XrLV.js → _basePickBy-BXw54oMQ.js} +1 -1
  2. package/dist/client/assets/{_baseUniq-CnpEKy-E.js → _baseUniq-_2-lN-JZ.js} +1 -1
  3. package/dist/client/assets/{arc-BVt3NpFL.js → arc-DaCKgXo4.js} +1 -1
  4. package/dist/client/assets/{architectureDiagram-VXUJARFQ-DQXtRVG1.js → architectureDiagram-VXUJARFQ-D5dlDvyJ.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-VD42YOAC-C_GKWlYL.js → blockDiagram-VD42YOAC-CEAiD2S_.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-YG6GDRKO-njABqJg1.js → c4Diagram-YG6GDRKO-DTNRx2b5.js} +1 -1
  7. package/dist/client/assets/channel-BusoDFL5.js +1 -0
  8. package/dist/client/assets/{chunk-4BX2VUAB-DNMCzzfR.js → chunk-4BX2VUAB-yvp0WNZ0.js} +1 -1
  9. package/dist/client/assets/{chunk-55IACEB6-CajLRh0F.js → chunk-55IACEB6-Cse3ExNd.js} +1 -1
  10. package/dist/client/assets/{chunk-B4BG7PRW-BLSFZNLe.js → chunk-B4BG7PRW-Bl2GpPLn.js} +1 -1
  11. package/dist/client/assets/{chunk-DI55MBZ5-DtQPQERJ.js → chunk-DI55MBZ5-CV5Cqkss.js} +1 -1
  12. package/dist/client/assets/{chunk-FMBD7UC4-BhtFvMT5.js → chunk-FMBD7UC4-CP3yjlO6.js} +1 -1
  13. package/dist/client/assets/{chunk-QN33PNHL-DBHUU683.js → chunk-QN33PNHL-DUdJaEPB.js} +1 -1
  14. package/dist/client/assets/{chunk-QZHKN3VN-B1rA-SBe.js → chunk-QZHKN3VN-DYTUnPIc.js} +1 -1
  15. package/dist/client/assets/{chunk-TZMSLE5B-BJUlg6Pf.js → chunk-TZMSLE5B-CtWiqR24.js} +1 -1
  16. package/dist/client/assets/classDiagram-2ON5EDUG-C3RMRasv.js +1 -0
  17. package/dist/client/assets/classDiagram-v2-WZHVMYZB-C3RMRasv.js +1 -0
  18. package/dist/client/assets/clone-D6B-nWvg.js +1 -0
  19. package/dist/client/assets/{cose-bilkent-S5V4N54A-CyjRxz-A.js → cose-bilkent-S5V4N54A-B67RdhOB.js} +1 -1
  20. package/dist/client/assets/{dagre-6UL2VRFP-CURGwkx9.js → dagre-6UL2VRFP-D9u_oxJL.js} +1 -1
  21. package/dist/client/assets/{diagram-PSM6KHXK-CaOIVsbD.js → diagram-PSM6KHXK-DOWfPnpa.js} +1 -1
  22. package/dist/client/assets/{diagram-QEK2KX5R-0zoz9z8_.js → diagram-QEK2KX5R-CqJwZbAA.js} +1 -1
  23. package/dist/client/assets/{diagram-S2PKOQOG-D6WyG3pJ.js → diagram-S2PKOQOG-D0ri9ztm.js} +1 -1
  24. package/dist/client/assets/{erDiagram-Q2GNP2WA-BwfTai9D.js → erDiagram-Q2GNP2WA-0cRKPUY7.js} +1 -1
  25. package/dist/client/assets/{flowDiagram-NV44I4VS-z6idjLXa.js → flowDiagram-NV44I4VS-Cj_5-ww1.js} +1 -1
  26. package/dist/client/assets/{ganttDiagram-JELNMOA3-BLAK0GDW.js → ganttDiagram-JELNMOA3-Cwm2arni.js} +1 -1
  27. package/dist/client/assets/{gitGraphDiagram-NY62KEGX-DsC9TQow.js → gitGraphDiagram-NY62KEGX-DKyLuO26.js} +1 -1
  28. package/dist/client/assets/{graph-72LNxX96.js → graph-BhRkLhV1.js} +1 -1
  29. package/dist/client/assets/{index-C0E1P_GL.js → index-1HR_eYGL.js} +222 -184
  30. package/dist/client/assets/index-DHENAn70.css +1 -0
  31. package/dist/client/assets/{infoDiagram-WHAUD3N6-CInk5tUs.js → infoDiagram-WHAUD3N6-DuzPbELj.js} +1 -1
  32. package/dist/client/assets/{journeyDiagram-XKPGCS4Q-nTrHP_HR.js → journeyDiagram-XKPGCS4Q-BfearpNr.js} +1 -1
  33. package/dist/client/assets/{kanban-definition-3W4ZIXB7-xiF9MwcX.js → kanban-definition-3W4ZIXB7-R5KMtAj4.js} +1 -1
  34. package/dist/client/assets/{layout-BxYnyUX2.js → layout-B-tL36G2.js} +1 -1
  35. package/dist/client/assets/{linear-BOpSSl-n.js → linear-CvIBiHN-.js} +1 -1
  36. package/dist/client/assets/{mindmap-definition-VGOIOE7T-BxYEWuiO.js → mindmap-definition-VGOIOE7T-CHYN0wBz.js} +1 -1
  37. package/dist/client/assets/{pieDiagram-ADFJNKIX-DHQkQuZp.js → pieDiagram-ADFJNKIX-LkA8WwD-.js} +1 -1
  38. package/dist/client/assets/{quadrantDiagram-AYHSOK5B--dbc4LsQ.js → quadrantDiagram-AYHSOK5B-B5s8Qc9j.js} +1 -1
  39. package/dist/client/assets/{requirementDiagram-UZGBJVZJ-Dr-h39fn.js → requirementDiagram-UZGBJVZJ-BMLpjBJA.js} +1 -1
  40. package/dist/client/assets/{sankeyDiagram-TZEHDZUN-BwKHqT4R.js → sankeyDiagram-TZEHDZUN-BIj9RGlp.js} +1 -1
  41. package/dist/client/assets/{sequenceDiagram-WL72ISMW-DizdB6A9.js → sequenceDiagram-WL72ISMW-uGM-Jh_O.js} +1 -1
  42. package/dist/client/assets/{stateDiagram-FKZM4ZOC-C-J72Wtl.js → stateDiagram-FKZM4ZOC-CQQhnUok.js} +1 -1
  43. package/dist/client/assets/stateDiagram-v2-4FDKWEC3-BUJPEM7D.js +1 -0
  44. package/dist/client/assets/{terminal-instance-qIJHj7KS.js → terminal-instance-DpK3PfW4.js} +1 -1
  45. package/dist/client/assets/{timeline-definition-IT6M3QCI-DAGw_FKd.js → timeline-definition-IT6M3QCI-BusXMYfI.js} +1 -1
  46. package/dist/client/assets/{treemap-KMMF4GRG-Bo4f4Al3.js → treemap-KMMF4GRG-XF3-w8dz.js} +1 -1
  47. package/dist/client/assets/{xychartDiagram-PRI3JC2R-CAJ9NVad.js → xychartDiagram-PRI3JC2R-DhKTXxb3.js} +1 -1
  48. package/dist/client/index.html +2 -2
  49. package/dist/prompts/ratchet/dispatch.md +31 -0
  50. package/dist/src/backend/app-context.d.ts +4 -0
  51. package/dist/src/backend/app-context.d.ts.map +1 -1
  52. package/dist/src/backend/app-context.js +4 -0
  53. package/dist/src/backend/app-context.js.map +1 -1
  54. package/dist/src/backend/claude/protocol.d.ts +2 -2
  55. package/dist/src/backend/claude/protocol.d.ts.map +1 -1
  56. package/dist/src/backend/claude/protocol.js +13 -4
  57. package/dist/src/backend/claude/protocol.js.map +1 -1
  58. package/dist/src/backend/claude/protocol.test.js +18 -0
  59. package/dist/src/backend/claude/protocol.test.js.map +1 -1
  60. package/dist/src/backend/claude/session.d.ts.map +1 -1
  61. package/dist/src/backend/claude/session.js +40 -6
  62. package/dist/src/backend/claude/session.js.map +1 -1
  63. package/dist/src/backend/claude/session.test.js +46 -0
  64. package/dist/src/backend/claude/session.test.js.map +1 -1
  65. package/dist/src/backend/claude/types.d.ts +3 -0
  66. package/dist/src/backend/claude/types.d.ts.map +1 -1
  67. package/dist/src/backend/claude/types.js +5 -0
  68. package/dist/src/backend/claude/types.js.map +1 -1
  69. package/dist/src/backend/prompts/markdown-loader.d.ts +22 -0
  70. package/dist/src/backend/prompts/markdown-loader.d.ts.map +1 -0
  71. package/dist/src/backend/prompts/markdown-loader.js +77 -0
  72. package/dist/src/backend/prompts/markdown-loader.js.map +1 -0
  73. package/dist/src/backend/prompts/markdown-loader.test.d.ts +2 -0
  74. package/dist/src/backend/prompts/markdown-loader.test.d.ts.map +1 -0
  75. package/dist/src/backend/prompts/markdown-loader.test.js +190 -0
  76. package/dist/src/backend/prompts/markdown-loader.test.js.map +1 -0
  77. package/dist/src/backend/prompts/quick-actions.d.ts.map +1 -1
  78. package/dist/src/backend/prompts/quick-actions.js +34 -92
  79. package/dist/src/backend/prompts/quick-actions.js.map +1 -1
  80. package/dist/src/backend/prompts/ratchet-dispatch.d.ts +3 -0
  81. package/dist/src/backend/prompts/ratchet-dispatch.d.ts.map +1 -0
  82. package/dist/src/backend/prompts/ratchet-dispatch.js +53 -0
  83. package/dist/src/backend/prompts/ratchet-dispatch.js.map +1 -0
  84. package/dist/src/backend/prompts/ratchet-dispatch.test.d.ts +2 -0
  85. package/dist/src/backend/prompts/ratchet-dispatch.test.d.ts.map +1 -0
  86. package/dist/src/backend/prompts/ratchet-dispatch.test.js +34 -0
  87. package/dist/src/backend/prompts/ratchet-dispatch.test.js.map +1 -0
  88. package/dist/src/backend/prompts/workflows.d.ts.map +1 -1
  89. package/dist/src/backend/prompts/workflows.js +23 -76
  90. package/dist/src/backend/prompts/workflows.js.map +1 -1
  91. package/dist/src/backend/resource_accessors/workspace.accessor.d.ts +4 -3
  92. package/dist/src/backend/resource_accessors/workspace.accessor.d.ts.map +1 -1
  93. package/dist/src/backend/resource_accessors/workspace.accessor.js +4 -2
  94. package/dist/src/backend/resource_accessors/workspace.accessor.js.map +1 -1
  95. package/dist/src/backend/routers/websocket/chat.handler.d.ts.map +1 -1
  96. package/dist/src/backend/routers/websocket/chat.handler.js +1 -30
  97. package/dist/src/backend/routers/websocket/chat.handler.js.map +1 -1
  98. package/dist/src/backend/services/chat-event-forwarder.service.d.ts.map +1 -1
  99. package/dist/src/backend/services/chat-event-forwarder.service.js +9 -13
  100. package/dist/src/backend/services/chat-event-forwarder.service.js.map +1 -1
  101. package/dist/src/backend/services/chat-message-handlers/handlers/get-history.handler.d.ts +2 -1
  102. package/dist/src/backend/services/chat-message-handlers/handlers/get-history.handler.d.ts.map +1 -1
  103. package/dist/src/backend/services/chat-message-handlers/handlers/get-history.handler.js.map +1 -1
  104. package/dist/src/backend/services/chat-message-handlers/handlers/get-queue.handler.d.ts +2 -1
  105. package/dist/src/backend/services/chat-message-handlers/handlers/get-queue.handler.d.ts.map +1 -1
  106. package/dist/src/backend/services/chat-message-handlers/handlers/get-queue.handler.js +7 -3
  107. package/dist/src/backend/services/chat-message-handlers/handlers/get-queue.handler.js.map +1 -1
  108. package/dist/src/backend/services/chat-message-handlers/handlers/handlers.test.js +124 -8
  109. package/dist/src/backend/services/chat-message-handlers/handlers/handlers.test.js.map +1 -1
  110. package/dist/src/backend/services/chat-message-handlers/handlers/list-sessions.handler.d.ts +2 -1
  111. package/dist/src/backend/services/chat-message-handlers/handlers/list-sessions.handler.d.ts.map +1 -1
  112. package/dist/src/backend/services/chat-message-handlers/handlers/list-sessions.handler.js.map +1 -1
  113. package/dist/src/backend/services/chat-message-handlers/handlers/load-session.handler.d.ts +2 -1
  114. package/dist/src/backend/services/chat-message-handlers/handlers/load-session.handler.d.ts.map +1 -1
  115. package/dist/src/backend/services/chat-message-handlers/handlers/load-session.handler.js +41 -31
  116. package/dist/src/backend/services/chat-message-handlers/handlers/load-session.handler.js.map +1 -1
  117. package/dist/src/backend/services/chat-message-handlers/handlers/permission-response.handler.d.ts +2 -1
  118. package/dist/src/backend/services/chat-message-handlers/handlers/permission-response.handler.d.ts.map +1 -1
  119. package/dist/src/backend/services/chat-message-handlers/handlers/permission-response.handler.js +1 -2
  120. package/dist/src/backend/services/chat-message-handlers/handlers/permission-response.handler.js.map +1 -1
  121. package/dist/src/backend/services/chat-message-handlers/handlers/question-response.handler.d.ts +2 -1
  122. package/dist/src/backend/services/chat-message-handlers/handlers/question-response.handler.d.ts.map +1 -1
  123. package/dist/src/backend/services/chat-message-handlers/handlers/question-response.handler.js +1 -2
  124. package/dist/src/backend/services/chat-message-handlers/handlers/question-response.handler.js.map +1 -1
  125. package/dist/src/backend/services/chat-message-handlers/handlers/queue-message.handler.d.ts +2 -1
  126. package/dist/src/backend/services/chat-message-handlers/handlers/queue-message.handler.d.ts.map +1 -1
  127. package/dist/src/backend/services/chat-message-handlers/handlers/queue-message.handler.js +6 -7
  128. package/dist/src/backend/services/chat-message-handlers/handlers/queue-message.handler.js.map +1 -1
  129. package/dist/src/backend/services/chat-message-handlers/handlers/remove-queued-message.handler.d.ts +2 -1
  130. package/dist/src/backend/services/chat-message-handlers/handlers/remove-queued-message.handler.d.ts.map +1 -1
  131. package/dist/src/backend/services/chat-message-handlers/handlers/remove-queued-message.handler.js +1 -2
  132. package/dist/src/backend/services/chat-message-handlers/handlers/remove-queued-message.handler.js.map +1 -1
  133. package/dist/src/backend/services/chat-message-handlers/handlers/rewind-files.handler.d.ts +2 -1
  134. package/dist/src/backend/services/chat-message-handlers/handlers/rewind-files.handler.d.ts.map +1 -1
  135. package/dist/src/backend/services/chat-message-handlers/handlers/rewind-files.handler.js +8 -9
  136. package/dist/src/backend/services/chat-message-handlers/handlers/rewind-files.handler.js.map +1 -1
  137. package/dist/src/backend/services/chat-message-handlers/handlers/set-model.handler.d.ts +2 -1
  138. package/dist/src/backend/services/chat-message-handlers/handlers/set-model.handler.d.ts.map +1 -1
  139. package/dist/src/backend/services/chat-message-handlers/handlers/set-model.handler.js +4 -5
  140. package/dist/src/backend/services/chat-message-handlers/handlers/set-model.handler.js.map +1 -1
  141. package/dist/src/backend/services/chat-message-handlers/handlers/set-thinking-budget.handler.d.ts +2 -1
  142. package/dist/src/backend/services/chat-message-handlers/handlers/set-thinking-budget.handler.d.ts.map +1 -1
  143. package/dist/src/backend/services/chat-message-handlers/handlers/set-thinking-budget.handler.js +4 -5
  144. package/dist/src/backend/services/chat-message-handlers/handlers/set-thinking-budget.handler.js.map +1 -1
  145. package/dist/src/backend/services/chat-message-handlers/handlers/start.handler.d.ts +2 -1
  146. package/dist/src/backend/services/chat-message-handlers/handlers/start.handler.d.ts.map +1 -1
  147. package/dist/src/backend/services/chat-message-handlers/handlers/start.handler.js +4 -2
  148. package/dist/src/backend/services/chat-message-handlers/handlers/start.handler.js.map +1 -1
  149. package/dist/src/backend/services/chat-message-handlers/handlers/stop.handler.d.ts +2 -1
  150. package/dist/src/backend/services/chat-message-handlers/handlers/stop.handler.d.ts.map +1 -1
  151. package/dist/src/backend/services/chat-message-handlers/handlers/stop.handler.js +7 -2
  152. package/dist/src/backend/services/chat-message-handlers/handlers/stop.handler.js.map +1 -1
  153. package/dist/src/backend/services/chat-message-handlers/handlers/user-input.handler.d.ts +2 -1
  154. package/dist/src/backend/services/chat-message-handlers/handlers/user-input.handler.d.ts.map +1 -1
  155. package/dist/src/backend/services/chat-message-handlers/handlers/user-input.handler.js +1 -2
  156. package/dist/src/backend/services/chat-message-handlers/handlers/user-input.handler.js.map +1 -1
  157. package/dist/src/backend/services/chat-message-handlers/registry.d.ts +5 -1
  158. package/dist/src/backend/services/chat-message-handlers/registry.d.ts.map +1 -1
  159. package/dist/src/backend/services/chat-message-handlers/registry.js.map +1 -1
  160. package/dist/src/backend/services/chat-message-handlers.service.d.ts.map +1 -1
  161. package/dist/src/backend/services/chat-message-handlers.service.js +4 -8
  162. package/dist/src/backend/services/chat-message-handlers.service.js.map +1 -1
  163. package/dist/src/backend/services/chat-transport-adapter.service.d.ts +2 -0
  164. package/dist/src/backend/services/chat-transport-adapter.service.d.ts.map +1 -1
  165. package/dist/src/backend/services/chat-transport-adapter.service.js +32 -1
  166. package/dist/src/backend/services/chat-transport-adapter.service.js.map +1 -1
  167. package/dist/src/backend/services/chat-transport-adapter.service.test.js +14 -0
  168. package/dist/src/backend/services/chat-transport-adapter.service.test.js.map +1 -1
  169. package/dist/src/backend/services/data-backup.service.d.ts +2 -622
  170. package/dist/src/backend/services/data-backup.service.d.ts.map +1 -1
  171. package/dist/src/backend/services/data-backup.service.js +3 -143
  172. package/dist/src/backend/services/data-backup.service.js.map +1 -1
  173. package/dist/src/backend/services/file-lock.service.d.ts.map +1 -1
  174. package/dist/src/backend/services/file-lock.service.js +3 -5
  175. package/dist/src/backend/services/file-lock.service.js.map +1 -1
  176. package/dist/src/backend/services/file-lock.service.test.js +55 -0
  177. package/dist/src/backend/services/file-lock.service.test.js.map +1 -1
  178. package/dist/src/backend/services/github-cli.service.d.ts.map +1 -1
  179. package/dist/src/backend/services/github-cli.service.js +118 -19
  180. package/dist/src/backend/services/github-cli.service.js.map +1 -1
  181. package/dist/src/backend/services/github-cli.service.test.js +126 -0
  182. package/dist/src/backend/services/github-cli.service.test.js.map +1 -1
  183. package/dist/src/backend/services/index.d.ts +1 -0
  184. package/dist/src/backend/services/index.d.ts.map +1 -1
  185. package/dist/src/backend/services/index.js +1 -0
  186. package/dist/src/backend/services/index.js.map +1 -1
  187. package/dist/src/backend/services/message-state-machine.d.ts.map +1 -1
  188. package/dist/src/backend/services/message-state-machine.js +11 -5
  189. package/dist/src/backend/services/message-state-machine.js.map +1 -1
  190. package/dist/src/backend/services/message-state.service.d.ts +16 -18
  191. package/dist/src/backend/services/message-state.service.d.ts.map +1 -1
  192. package/dist/src/backend/services/message-state.service.js +8 -14
  193. package/dist/src/backend/services/message-state.service.js.map +1 -1
  194. package/dist/src/backend/services/message-state.service.test.js +36 -45
  195. package/dist/src/backend/services/message-state.service.test.js.map +1 -1
  196. package/dist/src/backend/services/ratchet.service.d.ts +14 -69
  197. package/dist/src/backend/services/ratchet.service.d.ts.map +1 -1
  198. package/dist/src/backend/services/ratchet.service.js +198 -412
  199. package/dist/src/backend/services/ratchet.service.js.map +1 -1
  200. package/dist/src/backend/services/ratchet.service.test.js +199 -424
  201. package/dist/src/backend/services/ratchet.service.test.js.map +1 -1
  202. package/dist/src/backend/services/run-script-state-machine.service.d.ts +27 -0
  203. package/dist/src/backend/services/run-script-state-machine.service.d.ts.map +1 -0
  204. package/dist/src/backend/services/run-script-state-machine.service.js +149 -0
  205. package/dist/src/backend/services/run-script-state-machine.service.js.map +1 -0
  206. package/dist/src/backend/services/run-script-state-machine.service.test.d.ts +2 -0
  207. package/dist/src/backend/services/run-script-state-machine.service.test.d.ts.map +1 -0
  208. package/dist/src/backend/services/run-script-state-machine.service.test.js +453 -0
  209. package/dist/src/backend/services/run-script-state-machine.service.test.js.map +1 -0
  210. package/dist/src/backend/services/run-script.service.d.ts +0 -7
  211. package/dist/src/backend/services/run-script.service.d.ts.map +1 -1
  212. package/dist/src/backend/services/run-script.service.js +145 -76
  213. package/dist/src/backend/services/run-script.service.js.map +1 -1
  214. package/dist/src/backend/services/session-runtime-store.service.d.ts +38 -0
  215. package/dist/src/backend/services/session-runtime-store.service.d.ts.map +1 -0
  216. package/dist/src/backend/services/session-runtime-store.service.js +111 -0
  217. package/dist/src/backend/services/session-runtime-store.service.js.map +1 -0
  218. package/dist/src/backend/services/session-runtime-store.service.test.d.ts +2 -0
  219. package/dist/src/backend/services/session-runtime-store.service.test.d.ts.map +1 -0
  220. package/dist/src/backend/services/session-runtime-store.service.test.js +15 -0
  221. package/dist/src/backend/services/session-runtime-store.service.test.js.map +1 -0
  222. package/dist/src/backend/services/session.service.test.js.map +1 -1
  223. package/dist/src/backend/services/workspace-query.service.d.ts +1 -0
  224. package/dist/src/backend/services/workspace-query.service.d.ts.map +1 -1
  225. package/dist/src/backend/services/workspace-query.service.js +8 -0
  226. package/dist/src/backend/services/workspace-query.service.js.map +1 -1
  227. package/dist/src/backend/services/worktree-lifecycle.service.d.ts.map +1 -1
  228. package/dist/src/backend/services/worktree-lifecycle.service.js +139 -19
  229. package/dist/src/backend/services/worktree-lifecycle.service.js.map +1 -1
  230. package/dist/src/backend/services/worktree-lifecycle.service.test.js +48 -1
  231. package/dist/src/backend/services/worktree-lifecycle.service.test.js.map +1 -1
  232. package/dist/src/backend/trpc/admin.trpc.d.ts +13 -13
  233. package/dist/src/backend/trpc/admin.trpc.d.ts.map +1 -1
  234. package/dist/src/backend/trpc/admin.trpc.js +2 -1
  235. package/dist/src/backend/trpc/admin.trpc.js.map +1 -1
  236. package/dist/src/backend/trpc/index.d.ts +18 -23
  237. package/dist/src/backend/trpc/index.d.ts.map +1 -1
  238. package/dist/src/backend/trpc/session.trpc.d.ts +3 -3
  239. package/dist/src/backend/trpc/workspace/run-script.trpc.d.ts +0 -7
  240. package/dist/src/backend/trpc/workspace/run-script.trpc.d.ts.map +1 -1
  241. package/dist/src/backend/trpc/workspace.trpc.d.ts +2 -7
  242. package/dist/src/backend/trpc/workspace.trpc.d.ts.map +1 -1
  243. package/dist/src/backend/trpc/workspace.trpc.js +12 -1
  244. package/dist/src/backend/trpc/workspace.trpc.js.map +1 -1
  245. package/dist/src/shared/ci-status.d.ts +9 -0
  246. package/dist/src/shared/ci-status.d.ts.map +1 -0
  247. package/dist/src/shared/ci-status.js +44 -0
  248. package/dist/src/shared/ci-status.js.map +1 -0
  249. package/dist/src/shared/claude/index.d.ts +1 -0
  250. package/dist/src/shared/claude/index.d.ts.map +1 -1
  251. package/dist/src/shared/claude/index.js +1 -0
  252. package/dist/src/shared/claude/index.js.map +1 -1
  253. package/dist/src/shared/claude/protocol.d.ts +6 -3
  254. package/dist/src/shared/claude/protocol.d.ts.map +1 -1
  255. package/dist/src/shared/claude/protocol.js.map +1 -1
  256. package/dist/src/shared/schemas/export-data.schema.d.ts +865 -0
  257. package/dist/src/shared/schemas/export-data.schema.d.ts.map +1 -0
  258. package/dist/src/shared/schemas/export-data.schema.js +167 -0
  259. package/dist/src/shared/schemas/export-data.schema.js.map +1 -0
  260. package/dist/src/shared/schemas/persisted-stores.schema.d.ts +19 -0
  261. package/dist/src/shared/schemas/persisted-stores.schema.d.ts.map +1 -0
  262. package/dist/src/shared/schemas/persisted-stores.schema.js +17 -0
  263. package/dist/src/shared/schemas/persisted-stores.schema.js.map +1 -0
  264. package/dist/src/shared/session-runtime.d.ts +17 -0
  265. package/dist/src/shared/session-runtime.d.ts.map +1 -0
  266. package/dist/src/shared/session-runtime.js +9 -0
  267. package/dist/src/shared/session-runtime.js.map +1 -0
  268. package/dist/src/shared/websocket/chat-message.schema.d.ts +24 -11
  269. package/dist/src/shared/websocket/chat-message.schema.d.ts.map +1 -1
  270. package/dist/src/shared/websocket/chat-message.schema.js +4 -1
  271. package/dist/src/shared/websocket/chat-message.schema.js.map +1 -1
  272. package/dist/src/shared/websocket/index.d.ts +1 -1
  273. package/dist/src/shared/websocket/index.d.ts.map +1 -1
  274. package/dist/src/shared/websocket/index.js.map +1 -1
  275. package/dist/src/shared/workspace-sidebar-status.d.ts +20 -0
  276. package/dist/src/shared/workspace-sidebar-status.d.ts.map +1 -0
  277. package/dist/src/shared/workspace-sidebar-status.js +73 -0
  278. package/dist/src/shared/workspace-sidebar-status.js.map +1 -0
  279. package/dist/tsconfig.backend.tsbuildinfo +1 -1
  280. package/package.json +2 -2
  281. package/prompts/ratchet/dispatch.md +31 -0
  282. package/dist/client/assets/channel-DXVcwOT7.js +0 -1
  283. package/dist/client/assets/classDiagram-2ON5EDUG-DSXhUv2N.js +0 -1
  284. package/dist/client/assets/classDiagram-v2-WZHVMYZB-DSXhUv2N.js +0 -1
  285. package/dist/client/assets/clone-Cel7XaEC.js +0 -1
  286. package/dist/client/assets/index-DI1BSYUN.css +0 -1
  287. package/dist/client/assets/stateDiagram-v2-4FDKWEC3-waz9l7Vx.js +0 -1
@@ -1,27 +1,16 @@
1
1
  import { CIStatus, RatchetState, SessionStatus } from '../../../prisma/generated/client.js';
2
2
  import { beforeEach, describe, expect, it, vi } from 'vitest';
3
- vi.mock('../db', () => ({
4
- prisma: {
5
- $transaction: vi.fn(),
6
- },
7
- }));
8
- vi.mock('../resource_accessors/user-settings.accessor', () => ({
9
- userSettingsAccessor: {
10
- get: vi.fn(),
11
- },
12
- }));
13
3
  vi.mock('../resource_accessors/workspace.accessor', () => ({
14
4
  workspaceAccessor: {
15
5
  findWithPRsForRatchet: vi.fn(),
16
6
  findForRatchetById: vi.fn(),
17
- findById: vi.fn(),
18
7
  update: vi.fn(),
19
8
  },
20
9
  }));
21
10
  vi.mock('../resource_accessors/claude-session.accessor', () => ({
22
11
  claudeSessionAccessor: {
23
- create: vi.fn(),
24
12
  findById: vi.fn(),
13
+ findByWorkspaceId: vi.fn(),
25
14
  },
26
15
  }));
27
16
  vi.mock('./github-cli.service', () => ({
@@ -30,27 +19,25 @@ vi.mock('./github-cli.service', () => ({
30
19
  getPRFullDetails: vi.fn(),
31
20
  getReviewComments: vi.fn(),
32
21
  computeCIStatus: vi.fn(),
33
- mergePR: vi.fn(),
22
+ getAuthenticatedUsername: vi.fn(),
23
+ },
24
+ }));
25
+ vi.mock('./fixer-session.service', () => ({
26
+ fixerSessionService: {
27
+ acquireAndDispatch: vi.fn(),
34
28
  },
35
29
  }));
36
30
  vi.mock('./session.service', () => ({
37
31
  sessionService: {
38
- getClient: vi.fn(),
39
- isSessionActive: vi.fn(),
40
- startClaudeSession: vi.fn(),
32
+ isSessionRunning: vi.fn(),
33
+ stopClaudeSession: vi.fn(),
41
34
  },
42
35
  }));
43
36
  vi.mock('./message-state.service', () => ({
44
37
  messageStateService: {
45
- getRecentUserAndAgentMessages: vi.fn(),
46
38
  injectCommittedUserMessage: vi.fn(),
47
39
  },
48
40
  }));
49
- vi.mock('./config.service', () => ({
50
- configService: {
51
- getMaxSessionsPerWorkspace: vi.fn(() => 5),
52
- },
53
- }));
54
41
  vi.mock('./logger.service', () => ({
55
42
  createLogger: () => ({
56
43
  debug: vi.fn(),
@@ -60,525 +47,313 @@ vi.mock('./logger.service', () => ({
60
47
  }),
61
48
  }));
62
49
  import { claudeSessionAccessor } from '../resource_accessors/claude-session.accessor.js';
63
- import { userSettingsAccessor } from '../resource_accessors/user-settings.accessor.js';
64
50
  import { workspaceAccessor } from '../resource_accessors/workspace.accessor.js';
51
+ import { fixerSessionService } from './fixer-session.service.js';
52
+ import { githubCLIService } from './github-cli.service.js';
65
53
  import { ratchetService } from './ratchet.service.js';
66
54
  import { sessionService } from './session.service.js';
67
- describe('ratchet service', () => {
55
+ describe('ratchet service (state-change + idle dispatch)', () => {
68
56
  beforeEach(() => {
69
57
  vi.clearAllMocks();
70
58
  ratchetService.isShuttingDown = false;
59
+ vi.mocked(githubCLIService.getAuthenticatedUsername).mockResolvedValue(null);
71
60
  });
72
- it('checks workspaces even when global ratchet default is off', async () => {
73
- vi.mocked(userSettingsAccessor.get).mockResolvedValue({
74
- ratchetEnabled: false,
75
- ratchetAutoFixCi: true,
76
- ratchetAutoFixReviews: true,
77
- ratchetAutoMerge: false,
78
- ratchetAllowedReviewers: null,
79
- });
61
+ it('checks workspaces and processes each', async () => {
80
62
  vi.mocked(workspaceAccessor.findWithPRsForRatchet).mockResolvedValue([
81
63
  {
82
64
  id: 'ws-1',
83
65
  prUrl: 'https://github.com/example/repo/pull/1',
84
66
  prNumber: 1,
67
+ prState: 'OPEN',
68
+ prCiStatus: CIStatus.UNKNOWN,
85
69
  ratchetEnabled: true,
86
70
  ratchetState: RatchetState.IDLE,
87
71
  ratchetActiveSessionId: null,
88
72
  ratchetLastCiRunId: null,
89
- ratchetLastNotifiedState: null,
90
73
  prReviewLastCheckedAt: null,
91
74
  },
92
75
  ]);
93
- const processWorkspaceSpy = vi
94
- .spyOn(ratchetService, 'processWorkspace')
95
- .mockResolvedValue({
76
+ vi.spyOn(ratchetService, 'processWorkspace').mockResolvedValue({
96
77
  workspaceId: 'ws-1',
97
78
  previousState: RatchetState.IDLE,
98
79
  newState: RatchetState.IDLE,
99
80
  action: { type: 'WAITING', reason: 'noop' },
100
81
  });
101
82
  const result = await ratchetService.checkAllWorkspaces();
102
- expect(processWorkspaceSpy).toHaveBeenCalledTimes(1);
103
83
  expect(result.checked).toBe(1);
104
84
  });
105
- it('does not execute ratchet actions when workspace ratcheting is off', async () => {
85
+ it('does not dispatch when workspace ratcheting is disabled', async () => {
106
86
  const workspace = {
107
- id: 'ws-2',
87
+ id: 'ws-disabled',
108
88
  prUrl: 'https://github.com/example/repo/pull/2',
109
89
  prNumber: 2,
90
+ prState: 'OPEN',
91
+ prCiStatus: CIStatus.UNKNOWN,
110
92
  ratchetEnabled: false,
111
93
  ratchetState: RatchetState.IDLE,
112
94
  ratchetActiveSessionId: null,
113
95
  ratchetLastCiRunId: null,
114
- ratchetLastNotifiedState: null,
115
96
  prReviewLastCheckedAt: null,
116
97
  };
117
- const prStateInfo = {
118
- ciStatus: 'FAILURE',
119
- mergeStateStatus: 'CLEAN',
98
+ vi.spyOn(ratchetService, 'fetchPRState').mockResolvedValue({
99
+ ciStatus: CIStatus.FAILURE,
100
+ snapshotKey: '2026-01-02T00:00:00Z',
120
101
  hasChangesRequested: false,
121
- hasNewReviewComments: false,
122
- failedChecks: [],
123
- ciRunId: '1001',
124
- reviews: [],
125
- comments: [],
126
- reviewComments: [],
127
- newReviewComments: [],
128
- newPRComments: [],
102
+ latestReviewActivityAtMs: null,
129
103
  prState: 'OPEN',
130
104
  prNumber: 2,
131
- };
132
- vi.spyOn(ratchetService, 'fetchPRState').mockResolvedValue(prStateInfo);
133
- vi.spyOn(ratchetService, 'determineRatchetState').mockReturnValue(RatchetState.CI_FAILED);
134
- const executeRatchetActionSpy = vi.spyOn(ratchetService, 'executeRatchetAction');
135
- vi.mocked(workspaceAccessor.findById).mockResolvedValue({
136
- ratchetEnabled: false,
137
105
  });
138
106
  vi.mocked(workspaceAccessor.update).mockResolvedValue({});
139
- const result = await ratchetService.processWorkspace(workspace, {
140
- autoFixCi: true,
141
- autoFixReviews: true,
142
- autoMerge: false,
143
- allowedReviewers: [],
144
- });
145
- expect(executeRatchetActionSpy).not.toHaveBeenCalled();
107
+ const triggerSpy = vi.spyOn(ratchetService, 'triggerFixer');
108
+ const result = await ratchetService.processWorkspace(workspace);
109
+ expect(triggerSpy).not.toHaveBeenCalled();
146
110
  expect(result).toMatchObject({
147
111
  action: { type: 'DISABLED', reason: 'Workspace ratcheting disabled' },
148
- previousState: RatchetState.IDLE,
149
112
  newState: RatchetState.IDLE,
150
113
  });
151
- expect(workspaceAccessor.update).toHaveBeenCalledWith(workspace.id, expect.objectContaining({ ratchetState: RatchetState.IDLE }));
152
114
  });
153
- it('does not execute ratchet actions when ratcheting is disabled mid-check', async () => {
115
+ it('does not dispatch when workspace is not idle', async () => {
154
116
  const workspace = {
155
- id: 'ws-3',
117
+ id: 'ws-busy',
156
118
  prUrl: 'https://github.com/example/repo/pull/3',
157
119
  prNumber: 3,
120
+ prState: 'OPEN',
121
+ prCiStatus: CIStatus.UNKNOWN,
158
122
  ratchetEnabled: true,
159
123
  ratchetState: RatchetState.IDLE,
160
124
  ratchetActiveSessionId: null,
161
125
  ratchetLastCiRunId: null,
162
- ratchetLastNotifiedState: null,
163
- prReviewLastCheckedAt: null,
126
+ prReviewLastCheckedAt: new Date('2026-01-01T00:00:00Z'),
164
127
  };
165
- const prStateInfo = {
166
- ciStatus: 'FAILURE',
167
- mergeStateStatus: 'CLEAN',
128
+ vi.spyOn(ratchetService, 'fetchPRState').mockResolvedValue({
129
+ ciStatus: CIStatus.SUCCESS,
130
+ snapshotKey: '2026-01-02T00:00:00Z',
168
131
  hasChangesRequested: false,
169
- hasNewReviewComments: false,
170
- failedChecks: [],
171
- ciRunId: '1002',
172
- reviews: [],
173
- comments: [],
174
- reviewComments: [],
175
- newReviewComments: [],
176
- newPRComments: [],
132
+ latestReviewActivityAtMs: new Date('2026-01-02T00:00:00Z').getTime(),
177
133
  prState: 'OPEN',
178
134
  prNumber: 3,
179
- };
180
- vi.spyOn(ratchetService, 'fetchPRState').mockResolvedValue(prStateInfo);
181
- vi.spyOn(ratchetService, 'determineRatchetState').mockReturnValue(RatchetState.CI_FAILED);
182
- const executeRatchetActionSpy = vi.spyOn(ratchetService, 'executeRatchetAction');
183
- vi.mocked(workspaceAccessor.findById).mockResolvedValue({
184
- ratchetEnabled: false,
185
135
  });
186
136
  vi.mocked(workspaceAccessor.update).mockResolvedValue({});
187
- const result = await ratchetService.processWorkspace(workspace, {
188
- autoFixCi: true,
189
- autoFixReviews: true,
190
- autoMerge: false,
191
- allowedReviewers: [],
192
- });
193
- expect(executeRatchetActionSpy).not.toHaveBeenCalled();
137
+ vi.mocked(claudeSessionAccessor.findByWorkspaceId).mockResolvedValue([
138
+ {
139
+ id: 'chat-1',
140
+ workflow: 'default-followup',
141
+ status: 'RUNNING',
142
+ },
143
+ ]);
144
+ const result = await ratchetService.processWorkspace(workspace);
194
145
  expect(result).toMatchObject({
195
- action: { type: 'DISABLED', reason: 'Workspace ratcheting disabled' },
196
- newState: RatchetState.IDLE,
197
- });
198
- expect(workspaceAccessor.update).toHaveBeenCalledWith(workspace.id, expect.objectContaining({ ratchetState: RatchetState.IDLE }));
199
- });
200
- });
201
- describe('Ratchet CI regression behavior', () => {
202
- beforeEach(() => {
203
- vi.clearAllMocks();
204
- });
205
- it('treats CI UNKNOWN as CI_RUNNING', () => {
206
- const state = ratchetService.determineRatchetState({
207
- ciStatus: CIStatus.UNKNOWN,
208
- mergeStateStatus: 'CLEAN',
209
- hasChangesRequested: false,
210
- hasNewReviewComments: false,
211
- failedChecks: [],
212
- ciRunId: null,
213
- reviews: [],
214
- comments: [],
215
- reviewComments: [],
216
- newReviewComments: [],
217
- newPRComments: [],
218
- prState: 'OPEN',
219
- prNumber: 12,
146
+ action: {
147
+ type: 'WAITING',
148
+ reason: 'Workspace is not idle (active non-ratchet chat session)',
149
+ },
220
150
  });
221
- expect(state).toBe(RatchetState.CI_RUNNING);
222
151
  });
223
- it('re-triggers fixer when active session is unreachable and CI fails', async () => {
152
+ it('dispatches when PR state changed since last ratchet', async () => {
224
153
  const workspace = {
225
- id: 'ws-unreachable',
226
- prUrl: 'https://github.com/example/repo/pull/55',
227
- prNumber: 55,
154
+ id: 'ws-change',
155
+ prUrl: 'https://github.com/example/repo/pull/4',
156
+ prNumber: 4,
157
+ prState: 'OPEN',
158
+ prCiStatus: CIStatus.UNKNOWN,
228
159
  ratchetEnabled: true,
229
- ratchetState: RatchetState.REVIEW_PENDING,
230
- ratchetActiveSessionId: 'session-stale',
231
- ratchetLastCiRunId: '2001',
232
- ratchetLastNotifiedState: RatchetState.CI_FAILED,
233
- prReviewLastCheckedAt: null,
160
+ ratchetState: RatchetState.IDLE,
161
+ ratchetActiveSessionId: null,
162
+ ratchetLastCiRunId: '2026-01-01T00:00:00Z',
163
+ prReviewLastCheckedAt: new Date('2026-01-01T00:00:00Z'),
234
164
  };
235
- vi.mocked(claudeSessionAccessor.findById).mockResolvedValue({
236
- id: 'session-stale',
237
- status: SessionStatus.RUNNING,
238
- });
239
- vi.mocked(sessionService.getClient).mockReturnValue(undefined);
240
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
241
- const triggerFixerSpy = vi
242
- .spyOn(ratchetService, 'triggerFixer')
243
- .mockResolvedValue({
244
- type: 'TRIGGERED_FIXER',
245
- sessionId: 'new-fixer',
246
- fixerType: 'ci',
247
- promptSent: true,
248
- });
249
- const result = await ratchetService.executeRatchetAction(workspace, RatchetState.CI_FAILED, {
165
+ vi.spyOn(ratchetService, 'fetchPRState').mockResolvedValue({
250
166
  ciStatus: CIStatus.FAILURE,
251
- mergeStateStatus: 'CLEAN',
167
+ snapshotKey: '2026-01-02T00:00:00Z',
252
168
  hasChangesRequested: false,
253
- hasNewReviewComments: false,
254
- failedChecks: [],
255
- ciRunId: '3001',
256
- reviews: [],
257
- comments: [],
258
- reviewComments: [],
259
- newReviewComments: [],
260
- newPRComments: [],
169
+ latestReviewActivityAtMs: null,
261
170
  prState: 'OPEN',
262
- prNumber: 55,
263
- }, {
264
- autoFixCi: true,
265
- autoFixReviews: true,
266
- autoMerge: false,
267
- allowedReviewers: [],
171
+ prNumber: 4,
268
172
  });
269
- expect(triggerFixerSpy).toHaveBeenCalledTimes(1);
270
- expect(workspaceAccessor.update).toHaveBeenCalledWith(workspace.id, {
271
- ratchetActiveSessionId: null,
272
- });
273
- expect(result).toEqual({
274
- type: 'TRIGGERED_FIXER',
275
- sessionId: 'new-fixer',
276
- fixerType: 'ci',
173
+ vi.mocked(workspaceAccessor.update).mockResolvedValue({});
174
+ vi.mocked(claudeSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
175
+ vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
176
+ status: 'started',
177
+ sessionId: 'ratchet-session',
277
178
  promptSent: true,
278
179
  });
180
+ const result = await ratchetService.processWorkspace(workspace);
181
+ expect(result).toMatchObject({
182
+ action: { type: 'TRIGGERED_FIXER' },
183
+ });
184
+ const finalUpdatePayload = vi.mocked(workspaceAccessor.update).mock.calls.at(-1)?.[1];
185
+ expect(finalUpdatePayload.ratchetLastCiRunId).toBe('2026-01-02T00:00:00Z');
186
+ expect(finalUpdatePayload).toHaveProperty('prReviewLastCheckedAt');
279
187
  });
280
- it('skips CI fixer dispatch when ciRunId matches ratchetLastCiRunId', async () => {
188
+ it('does not dispatch when PR state unchanged since last dispatch', async () => {
281
189
  const workspace = {
282
- id: 'ws-stale-ci',
283
- prUrl: 'https://github.com/example/repo/pull/99',
284
- prNumber: 99,
190
+ id: 'ws-unchanged',
191
+ prUrl: 'https://github.com/example/repo/pull/5',
192
+ prNumber: 5,
193
+ prState: 'OPEN',
194
+ prCiStatus: CIStatus.UNKNOWN,
285
195
  ratchetEnabled: true,
286
- ratchetState: RatchetState.CI_FAILED,
196
+ ratchetState: RatchetState.READY,
287
197
  ratchetActiveSessionId: null,
288
- ratchetLastCiRunId: 'run-already-handled',
289
- ratchetLastNotifiedState: null,
290
- prReviewLastCheckedAt: null,
198
+ ratchetLastCiRunId: '2026-01-02T00:00:00Z',
199
+ prReviewLastCheckedAt: new Date('2026-01-02T00:00:00Z'),
291
200
  };
292
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
293
- const triggerFixerSpy = vi.spyOn(ratchetService, 'triggerFixer');
294
- const result = await ratchetService.executeRatchetAction(workspace, RatchetState.CI_FAILED, {
201
+ vi.mocked(claudeSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
202
+ const action = await ratchetService.evaluateAndDispatch(workspace, {
295
203
  ciStatus: CIStatus.FAILURE,
296
- mergeStateStatus: 'CLEAN',
204
+ snapshotKey: '2026-01-02T00:00:00Z',
297
205
  hasChangesRequested: false,
298
- hasNewReviewComments: false,
299
- failedChecks: [],
300
- ciRunId: 'run-already-handled',
301
- reviews: [],
302
- comments: [],
303
- reviewComments: [],
304
- newReviewComments: [],
305
- newPRComments: [],
206
+ latestReviewActivityAtMs: null,
306
207
  prState: 'OPEN',
307
- prNumber: 99,
308
- }, {
309
- autoFixCi: true,
310
- autoFixReviews: true,
311
- autoMerge: false,
312
- allowedReviewers: [],
208
+ prNumber: 5,
313
209
  });
314
- expect(triggerFixerSpy).not.toHaveBeenCalled();
315
- expect(result).toEqual({
210
+ expect(action).toEqual({
316
211
  type: 'WAITING',
317
- reason: 'Already dispatched fixer for this CI run; waiting for new run',
212
+ reason: 'PR state unchanged since last ratchet dispatch',
318
213
  });
319
214
  });
320
- it('dispatches CI fixer when ciRunId differs from ratchetLastCiRunId', async () => {
215
+ it('does not dispatch repeatedly for unchanged CHANGES_REQUESTED state', async () => {
321
216
  const workspace = {
322
- id: 'ws-new-ci',
323
- prUrl: 'https://github.com/example/repo/pull/100',
324
- prNumber: 100,
217
+ id: 'ws-review-unchanged',
218
+ prUrl: 'https://github.com/example/repo/pull/55',
219
+ prNumber: 55,
220
+ prState: 'OPEN',
221
+ prCiStatus: CIStatus.UNKNOWN,
325
222
  ratchetEnabled: true,
326
- ratchetState: RatchetState.CI_FAILED,
223
+ ratchetState: RatchetState.REVIEW_PENDING,
327
224
  ratchetActiveSessionId: null,
328
- ratchetLastCiRunId: 'run-old',
329
- ratchetLastNotifiedState: null,
330
- prReviewLastCheckedAt: null,
225
+ ratchetLastCiRunId: 'ci:SUCCESS|changes-requested:1735776000000',
226
+ prReviewLastCheckedAt: new Date('2026-01-02T00:00:00Z'),
331
227
  };
332
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
333
- const triggerFixerSpy = vi
334
- .spyOn(ratchetService, 'triggerFixer')
335
- .mockResolvedValue({
336
- type: 'TRIGGERED_FIXER',
337
- sessionId: 'new-fixer',
338
- fixerType: 'ci',
339
- promptSent: true,
340
- });
341
- await ratchetService.executeRatchetAction(workspace, RatchetState.CI_FAILED, {
342
- ciStatus: CIStatus.FAILURE,
343
- mergeStateStatus: 'CLEAN',
344
- hasChangesRequested: false,
345
- hasNewReviewComments: false,
346
- failedChecks: [],
347
- ciRunId: 'run-new',
348
- reviews: [],
349
- comments: [],
350
- reviewComments: [],
351
- newReviewComments: [],
352
- newPRComments: [],
228
+ vi.mocked(claudeSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
229
+ const triggerSpy = vi.spyOn(ratchetService, 'triggerFixer');
230
+ const action = await ratchetService.evaluateAndDispatch(workspace, {
231
+ ciStatus: CIStatus.SUCCESS,
232
+ snapshotKey: 'ci:SUCCESS|changes-requested:1735776000000',
233
+ hasChangesRequested: true,
234
+ latestReviewActivityAtMs: new Date('2026-01-02T00:00:00Z').getTime(),
353
235
  prState: 'OPEN',
354
- prNumber: 100,
355
- }, {
356
- autoFixCi: true,
357
- autoFixReviews: true,
358
- autoMerge: false,
359
- allowedReviewers: [],
236
+ prNumber: 55,
237
+ });
238
+ expect(triggerSpy).not.toHaveBeenCalled();
239
+ expect(action).toEqual({
240
+ type: 'WAITING',
241
+ reason: 'PR state unchanged since last ratchet dispatch',
360
242
  });
361
- expect(triggerFixerSpy).toHaveBeenCalledTimes(1);
362
243
  });
363
- it('notifies on a new CI run even when state is already CI_FAILED', () => {
364
- const shouldNotify = ratchetService.shouldNotifyActiveFixer(RatchetState.CI_FAILED, RatchetState.CI_FAILED, '4002', '4001');
365
- expect(shouldNotify).toBe(true);
244
+ it('treats closed PR as IDLE and does not dispatch', () => {
245
+ const state = ratchetService.determineRatchetState({
246
+ ciStatus: CIStatus.SUCCESS,
247
+ snapshotKey: '2026-01-02T00:00:00Z',
248
+ hasChangesRequested: false,
249
+ latestReviewActivityAtMs: null,
250
+ prState: 'CLOSED',
251
+ prNumber: 6,
252
+ });
253
+ expect(state).toBe(RatchetState.IDLE);
366
254
  });
367
- it('does not consume CI run id when ratcheting is disabled', async () => {
255
+ it('handles prompt delivery failure by clearing active session and stopping runner', async () => {
368
256
  const workspace = {
369
- id: 'ws-disabled',
370
- prUrl: 'https://github.com/example/repo/pull/77',
371
- prNumber: 77,
257
+ id: 'ws-prompt-fail',
258
+ prUrl: 'https://github.com/example/repo/pull/7',
259
+ prNumber: 7,
260
+ prState: 'OPEN',
261
+ prCiStatus: CIStatus.UNKNOWN,
372
262
  ratchetEnabled: true,
373
263
  ratchetState: RatchetState.IDLE,
374
264
  ratchetActiveSessionId: null,
375
265
  ratchetLastCiRunId: null,
376
- ratchetLastNotifiedState: null,
377
266
  prReviewLastCheckedAt: null,
378
267
  };
379
268
  vi.spyOn(ratchetService, 'fetchPRState').mockResolvedValue({
380
- ciStatus: CIStatus.FAILURE,
381
- mergeStateStatus: 'CLEAN',
382
- hasChangesRequested: false,
383
- hasNewReviewComments: false,
384
- failedChecks: [],
385
- ciRunId: '5001',
386
- reviews: [],
387
- comments: [],
388
- reviewComments: [],
389
- newReviewComments: [],
390
- newPRComments: [],
269
+ ciStatus: CIStatus.SUCCESS,
270
+ snapshotKey: '2026-01-02T00:00:00Z',
271
+ hasChangesRequested: true,
272
+ latestReviewActivityAtMs: new Date('2026-01-02T00:00:00Z').getTime(),
391
273
  prState: 'OPEN',
392
- prNumber: 77,
274
+ prNumber: 7,
393
275
  });
394
- vi.spyOn(ratchetService, 'determineRatchetState').mockReturnValue(RatchetState.CI_FAILED);
395
- vi.mocked(workspaceAccessor.findById).mockResolvedValue({ ratchetEnabled: false });
396
276
  vi.mocked(workspaceAccessor.update).mockResolvedValue({});
397
- await ratchetService.processWorkspace(workspace, {
398
- autoFixCi: true,
399
- autoFixReviews: true,
400
- autoMerge: false,
401
- allowedReviewers: [],
277
+ vi.mocked(claudeSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
278
+ vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
279
+ status: 'started',
280
+ sessionId: 'ratchet-session',
281
+ promptSent: false,
402
282
  });
403
- const updateCalls = vi.mocked(workspaceAccessor.update).mock.calls;
404
- const finalUpdatePayload = updateCalls[updateCalls.length - 1]?.[1];
405
- expect(finalUpdatePayload).not.toHaveProperty('ratchetLastCiRunId');
283
+ vi.mocked(sessionService.isSessionRunning).mockReturnValue(true);
284
+ vi.mocked(sessionService.stopClaudeSession).mockResolvedValue();
285
+ const result = await ratchetService.processWorkspace(workspace);
286
+ expect(result).toMatchObject({
287
+ action: { type: 'ERROR', error: 'Failed to deliver initial ratchet prompt' },
288
+ });
289
+ expect(workspaceAccessor.update).toHaveBeenCalledWith(workspace.id, {
290
+ ratchetActiveSessionId: null,
291
+ });
292
+ expect(sessionService.stopClaudeSession).toHaveBeenCalledWith('ratchet-session');
406
293
  });
407
- it('returns run id "0" when parsed from details url', () => {
408
- const signature = ratchetService.extractFailedCiSignature([
409
- {
410
- name: 'unit-tests',
411
- conclusion: 'FAILURE',
412
- detailsUrl: 'https://github.com/acme/repo/actions/runs/0',
413
- },
414
- {
415
- name: 'lint',
416
- conclusion: 'FAILURE',
417
- detailsUrl: 'https://github.com/acme/repo/actions/runs/42',
418
- },
419
- ]);
420
- expect(signature).toBe('lint:42|unit-tests:0');
294
+ it('does not dispatch on a clean PR with no new review activity', async () => {
295
+ const workspace = {
296
+ id: 'ws-clean',
297
+ prUrl: 'https://github.com/example/repo/pull/8',
298
+ prNumber: 8,
299
+ prState: 'OPEN',
300
+ prCiStatus: CIStatus.UNKNOWN,
301
+ ratchetEnabled: true,
302
+ ratchetState: RatchetState.READY,
303
+ ratchetActiveSessionId: null,
304
+ ratchetLastCiRunId: '2026-01-01T00:00:00Z',
305
+ prReviewLastCheckedAt: new Date('2026-01-02T00:00:00Z'),
306
+ };
307
+ vi.mocked(claudeSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
308
+ const triggerSpy = vi.spyOn(ratchetService, 'triggerFixer');
309
+ const action = await ratchetService.evaluateAndDispatch(workspace, {
310
+ ciStatus: CIStatus.SUCCESS,
311
+ snapshotKey: '2026-01-03T00:00:00Z',
312
+ hasChangesRequested: false,
313
+ latestReviewActivityAtMs: new Date('2026-01-02T00:00:00Z').getTime(),
314
+ prState: 'OPEN',
315
+ prNumber: 8,
316
+ });
317
+ expect(triggerSpy).not.toHaveBeenCalled();
318
+ expect(action).toEqual({
319
+ type: 'WAITING',
320
+ reason: 'PR is clean (green CI and no new review activity)',
321
+ });
421
322
  });
422
- it('clears stale active session linkage when IDLE session has no client', async () => {
323
+ it('clears active ratchet session immediately when runtime is not running', async () => {
423
324
  const workspace = {
424
- id: 'ws-idle',
425
- prUrl: 'https://github.com/example/repo/pull/88',
426
- prNumber: 88,
325
+ id: 'ws-stale-active',
326
+ prUrl: 'https://github.com/example/repo/pull/9',
327
+ prNumber: 9,
328
+ prState: 'OPEN',
329
+ prCiStatus: CIStatus.UNKNOWN,
427
330
  ratchetEnabled: true,
428
331
  ratchetState: RatchetState.CI_FAILED,
429
- ratchetActiveSessionId: 'session-idle',
430
- ratchetLastCiRunId: 'sig-old',
431
- ratchetLastNotifiedState: RatchetState.CI_FAILED,
332
+ ratchetActiveSessionId: 'ratchet-session',
333
+ ratchetLastCiRunId: null,
432
334
  prReviewLastCheckedAt: null,
433
335
  };
434
336
  vi.mocked(claudeSessionAccessor.findById).mockResolvedValue({
435
- id: 'session-idle',
436
- status: SessionStatus.IDLE,
337
+ id: 'ratchet-session',
338
+ status: SessionStatus.RUNNING,
437
339
  });
438
- vi.mocked(sessionService.getClient).mockReturnValue(undefined);
340
+ vi.mocked(sessionService.isSessionRunning).mockReturnValue(false);
439
341
  vi.mocked(workspaceAccessor.update).mockResolvedValue({});
440
- const triggerFixerSpy = vi
441
- .spyOn(ratchetService, 'triggerFixer')
442
- .mockResolvedValue({
443
- type: 'TRIGGERED_FIXER',
444
- sessionId: 'session-idle',
445
- fixerType: 'ci',
446
- promptSent: true,
447
- });
448
- await ratchetService.executeRatchetAction(workspace, RatchetState.CI_FAILED, {
449
- ciStatus: CIStatus.FAILURE,
450
- mergeStateStatus: 'CLEAN',
451
- hasChangesRequested: false,
452
- hasNewReviewComments: false,
453
- failedChecks: [],
454
- ciRunId: 'sig-new',
455
- reviews: [],
456
- comments: [],
457
- reviewComments: [],
458
- newReviewComments: [],
459
- newPRComments: [],
460
- prState: 'OPEN',
461
- prNumber: 88,
462
- }, {
463
- autoFixCi: true,
464
- autoFixReviews: true,
465
- autoMerge: false,
466
- allowedReviewers: [],
467
- });
468
- expect(triggerFixerSpy).toHaveBeenCalledTimes(1);
342
+ const action = await ratchetService.getActiveRatchetSession(workspace);
343
+ expect(action).toBeNull();
469
344
  expect(workspaceAccessor.update).toHaveBeenCalledWith(workspace.id, {
470
345
  ratchetActiveSessionId: null,
471
346
  });
472
347
  });
473
- });
474
- describe('Ratchet Comment Detection', () => {
475
- const lastCheckedAt = new Date('2024-01-01T12:00:00Z').getTime();
476
- describe('New comment detection', () => {
477
- it('should detect comments created after lastCheckedAt', () => {
478
- const comment = {
479
- id: '1',
480
- author: { login: 'reviewer1' },
481
- body: 'New comment',
482
- createdAt: '2024-01-01T13:00:00Z',
483
- updatedAt: '2024-01-01T13:00:00Z',
484
- url: 'https://github.com/test/pr/1',
485
- };
486
- const createdTime = new Date(comment.createdAt).getTime();
487
- const updatedTime = new Date(comment.updatedAt).getTime();
488
- const isNewOrEdited = createdTime > lastCheckedAt || updatedTime > lastCheckedAt;
489
- expect(isNewOrEdited).toBe(true);
490
- });
491
- it('should not detect comments created before lastCheckedAt', () => {
492
- const comment = {
493
- id: '1',
494
- author: { login: 'reviewer1' },
495
- body: 'Old comment',
496
- createdAt: '2024-01-01T11:00:00Z',
497
- updatedAt: '2024-01-01T11:00:00Z',
498
- url: 'https://github.com/test/pr/1',
499
- };
500
- const createdTime = new Date(comment.createdAt).getTime();
501
- const updatedTime = new Date(comment.updatedAt).getTime();
502
- const isNewOrEdited = createdTime > lastCheckedAt || updatedTime > lastCheckedAt;
503
- expect(isNewOrEdited).toBe(false);
504
- });
505
- });
506
- describe('Edited comment detection', () => {
507
- it('should detect comments edited after lastCheckedAt', () => {
508
- const comment = {
509
- id: '1',
510
- author: { login: 'reviewer1' },
511
- body: 'Edited comment',
512
- createdAt: '2024-01-01T11:00:00Z',
513
- updatedAt: '2024-01-01T13:00:00Z',
514
- url: 'https://github.com/test/pr/1',
515
- };
516
- const createdTime = new Date(comment.createdAt).getTime();
517
- const updatedTime = new Date(comment.updatedAt).getTime();
518
- const isNewOrEdited = createdTime > lastCheckedAt || updatedTime > lastCheckedAt;
519
- expect(isNewOrEdited).toBe(true);
520
- });
521
- it('should detect comments with updatedAt exactly equal to lastCheckedAt as not new', () => {
522
- const comment = {
523
- id: '1',
524
- author: { login: 'reviewer1' },
525
- body: 'Comment at boundary',
526
- createdAt: '2024-01-01T11:00:00Z',
527
- updatedAt: '2024-01-01T12:00:00Z',
528
- url: 'https://github.com/test/pr/1',
529
- };
530
- const createdTime = new Date(comment.createdAt).getTime();
531
- const updatedTime = new Date(comment.updatedAt).getTime();
532
- const isNewOrEdited = createdTime > lastCheckedAt || updatedTime > lastCheckedAt;
533
- expect(isNewOrEdited).toBe(false);
534
- });
535
- it('should not detect old unedited comments', () => {
536
- const comment = {
537
- id: '1',
538
- author: { login: 'reviewer1' },
539
- body: 'Old unedited comment',
540
- createdAt: '2024-01-01T10:00:00Z',
541
- updatedAt: '2024-01-01T10:00:00Z',
542
- url: 'https://github.com/test/pr/1',
543
- };
544
- const createdTime = new Date(comment.createdAt).getTime();
545
- const updatedTime = new Date(comment.updatedAt).getTime();
546
- const isNewOrEdited = createdTime > lastCheckedAt || updatedTime > lastCheckedAt;
547
- expect(isNewOrEdited).toBe(false);
548
- });
549
- });
550
- describe('Reviewer filtering with edited comments', () => {
551
- it('should apply reviewer filter to edited comments', () => {
552
- const allowedReviewers = ['reviewer1', 'reviewer2'];
553
- const filterByReviewer = allowedReviewers.length > 0;
554
- const comments = [
555
- {
556
- id: '1',
557
- author: { login: 'reviewer1' },
558
- body: 'Allowed reviewer edit',
559
- createdAt: '2024-01-01T11:00:00Z',
560
- updatedAt: '2024-01-01T13:00:00Z',
561
- url: 'https://github.com/test/pr/1',
562
- },
563
- {
564
- id: '2',
565
- author: { login: 'reviewer3' },
566
- body: 'Disallowed reviewer edit',
567
- createdAt: '2024-01-01T11:00:00Z',
568
- updatedAt: '2024-01-01T13:00:00Z',
569
- url: 'https://github.com/test/pr/2',
570
- },
571
- ];
572
- const filteredComments = comments.filter((comment) => {
573
- const createdTime = new Date(comment.createdAt).getTime();
574
- const updatedTime = new Date(comment.updatedAt).getTime();
575
- const isNewOrEdited = createdTime > lastCheckedAt || updatedTime > lastCheckedAt;
576
- const isAllowedReviewer = !filterByReviewer || allowedReviewers.includes(comment.author.login);
577
- return isNewOrEdited && isAllowedReviewer;
578
- });
579
- expect(filteredComments).toHaveLength(1);
580
- expect(filteredComments[0].author.login).toBe('reviewer1');
581
- });
348
+ it('ignores review activity authored by the authenticated user', () => {
349
+ const latestActivity = ratchetService.computeLatestReviewActivityAtMs({
350
+ reviews: [
351
+ { submittedAt: '2026-01-02T00:00:00Z', author: { login: 'ratchet-bot' } },
352
+ { submittedAt: '2026-01-01T00:00:00Z', author: { login: 'reviewer' } },
353
+ ],
354
+ comments: [{ updatedAt: '2026-01-02T01:00:00Z', author: { login: 'ratchet-bot' } }],
355
+ }, [{ updatedAt: '2026-01-01T02:00:00Z', author: { login: 'reviewer2' } }], 'ratchet-bot');
356
+ expect(latestActivity).toBe(new Date('2026-01-01T02:00:00Z').getTime());
582
357
  });
583
358
  });
584
359
  //# sourceMappingURL=ratchet.service.test.js.map