factory-factory 0.3.21 → 0.4.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 (1363) hide show
  1. package/README.md +61 -344
  2. package/dist/client/assets/{arc-DMA75yUX.js → arc-lK3Ig9GV.js} +1 -1
  3. package/dist/client/assets/{architectureDiagram-3BPJPVTR-DfVbmuXe.js → architectureDiagram-3BPJPVTR-CeUbfBqe.js} +1 -1
  4. package/dist/client/assets/{blockDiagram-GPEHLZMM-BBr1PMaO.js → blockDiagram-GPEHLZMM-DiVGPkq2.js} +1 -1
  5. package/dist/client/assets/{c4Diagram-AAUBKEIU-C6xSY9MY.js → c4Diagram-AAUBKEIU-CT8hJB-h.js} +1 -1
  6. package/dist/client/assets/channel-CvY1mvIl.js +1 -0
  7. package/dist/client/assets/chunk-2J33WTMH-B0h8q8fD.js +1 -0
  8. package/dist/client/assets/{chunk-4BX2VUAB-DohmszzB.js → chunk-4BX2VUAB-km6NpEMz.js} +1 -1
  9. package/dist/client/assets/{chunk-55IACEB6-BxtEguq8.js → chunk-55IACEB6-COUWt3oA.js} +1 -1
  10. package/dist/client/assets/{chunk-727SXJPM--7mPe6NT.js → chunk-727SXJPM-Bp-ts2at.js} +1 -1
  11. package/dist/client/assets/{chunk-AQP2D5EJ-CUtRyyX-.js → chunk-AQP2D5EJ-dyo9i4xa.js} +3 -3
  12. package/dist/client/assets/{chunk-FMBD7UC4-Zvl7moTS.js → chunk-FMBD7UC4-B4C3KvLe.js} +1 -1
  13. package/dist/client/assets/{chunk-ND2GUHAM-DHRE6LfZ.js → chunk-ND2GUHAM-Qhi4WqzZ.js} +1 -1
  14. package/dist/client/assets/{chunk-QZHKN3VN-DCUx43mp.js → chunk-QZHKN3VN-Ce3Q_vrs.js} +1 -1
  15. package/dist/client/assets/classDiagram-4FO5ZUOK-Db7ZXJ6w.js +1 -0
  16. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-Db7ZXJ6w.js +1 -0
  17. package/dist/client/assets/{cose-bilkent-S5V4N54A-CTS7yey1.js → cose-bilkent-S5V4N54A-D6QofwFq.js} +1 -1
  18. package/dist/client/assets/{dagre-BM42HDAG-CDvFWhCS.js → dagre-BM42HDAG-CDzpFdgD.js} +1 -1
  19. package/dist/client/assets/{diagram-2AECGRRQ-BP89d-WY.js → diagram-2AECGRRQ-dl1_7lyq.js} +2 -2
  20. package/dist/client/assets/{diagram-5GNKFQAL-BXDVw1Xi.js → diagram-5GNKFQAL-B0ynMwBY.js} +1 -1
  21. package/dist/client/assets/{diagram-KO2AKTUF-DBYI-mqR.js → diagram-KO2AKTUF-B9ABz-Wk.js} +1 -1
  22. package/dist/client/assets/{diagram-LMA3HP47-D359JaJw.js → diagram-LMA3HP47-DZxXcfiX.js} +1 -1
  23. package/dist/client/assets/{diagram-OG6HWLK6-BMr7mzB4.js → diagram-OG6HWLK6-BncGM9Gv.js} +1 -1
  24. package/dist/client/assets/{erDiagram-TEJ5UH35-CeCqd6tA.js → erDiagram-TEJ5UH35-B9hxScvO.js} +1 -1
  25. package/dist/client/assets/{flowDiagram-I6XJVG4X-ZUKSazw4.js → flowDiagram-I6XJVG4X-DgZ2jwJQ.js} +4 -4
  26. package/dist/client/assets/{ganttDiagram-6RSMTGT7-Cq-q9kPK.js → ganttDiagram-6RSMTGT7-BrnNMNRY.js} +1 -1
  27. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-zVissJIm.js → gitGraphDiagram-PVQCEYII-BJmed6oD.js} +3 -3
  28. package/dist/client/assets/index-Btlruz7h.css +1 -0
  29. package/dist/client/assets/index-DEKuJM5_.js +674 -0
  30. package/dist/client/assets/infoDiagram-5YYISTIA-CtLzR4ip.js +2 -0
  31. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-Cux5p5kO.js → ishikawaDiagram-YF4QCWOH-BLrvV5ke.js} +3 -3
  32. package/dist/client/assets/{journeyDiagram-JHISSGLW-1ke9LkJX.js → journeyDiagram-JHISSGLW-B2-TMwJI.js} +1 -1
  33. package/dist/client/assets/{kanban-definition-UN3LZRKU-DP9dkc_Z.js → kanban-definition-UN3LZRKU-B5mGKgYF.js} +1 -1
  34. package/dist/client/assets/{linear-CyyRtH0c.js → linear-ConE9euY.js} +1 -1
  35. package/dist/client/assets/{mindmap-definition-RKZ34NQL-BcU2pjK2.js → mindmap-definition-RKZ34NQL-B0BaMk8D.js} +3 -3
  36. package/dist/client/assets/{mobile-baseline-CSjGr1_M.js → mobile-baseline-DWCt7ZGJ.js} +2 -2
  37. package/dist/client/assets/{pieDiagram-4H26LBE5-Bl7PuiuI.js → pieDiagram-4H26LBE5-DgPu3GTs.js} +2 -2
  38. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Dx0ckMzC.js → quadrantDiagram-W4KKPZXB-DDbeggF4.js} +3 -3
  39. package/dist/client/assets/{requirementDiagram-4Y6WPE33-CXkIn3ft.js → requirementDiagram-4Y6WPE33-Cc3ozz7U.js} +1 -1
  40. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DIjHC-x5.js → sankeyDiagram-5OEKKPKP-DmYoq588.js} +1 -1
  41. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BEdLD9KB.js → sequenceDiagram-3UESZ5HK-EVqdDzgf.js} +4 -4
  42. package/dist/client/assets/stateDiagram-AJRCARHV-BRd6IxSa.js +1 -0
  43. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-DR2vMoX5.js +1 -0
  44. package/dist/client/assets/{terminal-instance-W5XOlvlg.js → terminal-instance-CDKJdDaO.js} +8 -8
  45. package/dist/client/assets/{timeline-definition-PNZ67QCA-CkV6p1W8.js → timeline-definition-PNZ67QCA-CyxBqgqv.js} +1 -1
  46. package/dist/client/assets/{vennDiagram-CIIHVFJN-ubGCO9Lw.js → vennDiagram-CIIHVFJN-CLslQYVi.js} +1 -1
  47. package/dist/client/assets/{wardley-L42UT6IY-_NZpI_YF.js → wardley-L42UT6IY-DyEDLg3x.js} +1 -1
  48. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-DMXZwsFF.js → wardleyDiagram-YWT4CUSO-CjJBpd3P.js} +1 -1
  49. package/dist/client/assets/{xychartDiagram-2RQKCTM6-T9YSJ2sB.js → xychartDiagram-2RQKCTM6-DwzoT5Dl.js} +1 -1
  50. package/dist/client/index.html +2 -2
  51. package/dist/client/working.png +0 -0
  52. package/dist/prisma/generated/browser.d.ts +1 -0
  53. package/dist/prisma/generated/browser.d.ts.map +1 -1
  54. package/dist/prisma/generated/client.d.ts +1 -0
  55. package/dist/prisma/generated/client.d.ts.map +1 -1
  56. package/dist/prisma/generated/commonInputTypes.d.ts +90 -0
  57. package/dist/prisma/generated/commonInputTypes.d.ts.map +1 -1
  58. package/dist/prisma/generated/enums.d.ts +17 -0
  59. package/dist/prisma/generated/enums.d.ts.map +1 -1
  60. package/dist/prisma/generated/enums.js +15 -1
  61. package/dist/prisma/generated/enums.js.map +1 -1
  62. package/dist/prisma/generated/internal/class.d.ts +3 -0
  63. package/dist/prisma/generated/internal/class.d.ts.map +1 -1
  64. package/dist/prisma/generated/internal/class.js +4 -4
  65. package/dist/prisma/generated/internal/class.js.map +1 -1
  66. package/dist/prisma/generated/internal/prismaNamespace.d.ts +99 -1
  67. package/dist/prisma/generated/internal/prismaNamespace.d.ts.map +1 -1
  68. package/dist/prisma/generated/internal/prismaNamespace.js +20 -1
  69. package/dist/prisma/generated/internal/prismaNamespace.js.map +1 -1
  70. package/dist/prisma/generated/internal/prismaNamespaceBrowser.d.ts +20 -0
  71. package/dist/prisma/generated/internal/prismaNamespaceBrowser.d.ts.map +1 -1
  72. package/dist/prisma/generated/internal/prismaNamespaceBrowser.js +20 -1
  73. package/dist/prisma/generated/internal/prismaNamespaceBrowser.js.map +1 -1
  74. package/dist/prisma/generated/models/UserSettings.d.ts +32 -1
  75. package/dist/prisma/generated/models/UserSettings.d.ts.map +1 -1
  76. package/dist/prisma/generated/models/Workspace.d.ts +1631 -92
  77. package/dist/prisma/generated/models/Workspace.d.ts.map +1 -1
  78. package/dist/prisma/generated/models/WorkspaceNotification.d.ts +684 -0
  79. package/dist/prisma/generated/models/WorkspaceNotification.d.ts.map +1 -0
  80. package/dist/prisma/generated/models/WorkspaceNotification.js +2 -0
  81. package/dist/prisma/generated/models/WorkspaceNotification.js.map +1 -0
  82. package/dist/prisma/generated/models.d.ts +1 -0
  83. package/dist/prisma/generated/models.d.ts.map +1 -1
  84. package/dist/prompts/ratchet/dispatch.md +2 -0
  85. package/dist/scripts/check-no-direct-process-env.d.mts.map +1 -1
  86. package/dist/scripts/check-no-direct-process-env.mjs +1 -0
  87. package/dist/scripts/check-no-direct-process-env.mjs.map +1 -1
  88. package/dist/src/backend/app-context.d.ts +90 -26
  89. package/dist/src/backend/app-context.d.ts.map +1 -1
  90. package/dist/src/backend/app-context.js +145 -43
  91. package/dist/src/backend/app-context.js.map +1 -1
  92. package/dist/src/backend/app-context.test.d.ts +2 -0
  93. package/dist/src/backend/app-context.test.d.ts.map +1 -0
  94. package/dist/src/backend/app-context.test.js +51 -0
  95. package/dist/src/backend/app-context.test.js.map +1 -0
  96. package/dist/src/backend/clients/git.client.d.ts.map +1 -1
  97. package/dist/src/backend/clients/git.client.js +5 -0
  98. package/dist/src/backend/clients/git.client.js.map +1 -1
  99. package/dist/src/backend/clients/git.client.test.js +24 -0
  100. package/dist/src/backend/clients/git.client.test.js.map +1 -1
  101. package/dist/src/backend/fatal-error-handlers.d.ts +19 -0
  102. package/dist/src/backend/fatal-error-handlers.d.ts.map +1 -0
  103. package/dist/src/backend/fatal-error-handlers.js +37 -0
  104. package/dist/src/backend/fatal-error-handlers.js.map +1 -0
  105. package/dist/src/backend/fatal-error-handlers.test.d.ts +2 -0
  106. package/dist/src/backend/fatal-error-handlers.test.d.ts.map +1 -0
  107. package/dist/src/backend/fatal-error-handlers.test.js +95 -0
  108. package/dist/src/backend/fatal-error-handlers.test.js.map +1 -0
  109. package/dist/src/backend/index.js +7 -20
  110. package/dist/src/backend/index.js.map +1 -1
  111. package/dist/src/backend/interceptors/branch-rename.interceptor.d.ts.map +1 -1
  112. package/dist/src/backend/interceptors/branch-rename.interceptor.js +80 -6
  113. package/dist/src/backend/interceptors/branch-rename.interceptor.js.map +1 -1
  114. package/dist/src/backend/interceptors/branch-rename.interceptor.test.js +126 -21
  115. package/dist/src/backend/interceptors/branch-rename.interceptor.test.js.map +1 -1
  116. package/dist/src/backend/interceptors/conversation-rename.interceptor.d.ts.map +1 -1
  117. package/dist/src/backend/interceptors/conversation-rename.interceptor.js +39 -22
  118. package/dist/src/backend/interceptors/conversation-rename.interceptor.js.map +1 -1
  119. package/dist/src/backend/interceptors/conversation-rename.interceptor.test.js +79 -1
  120. package/dist/src/backend/interceptors/conversation-rename.interceptor.test.js.map +1 -1
  121. package/dist/src/backend/interceptors/pre-pr-rename.interceptor.d.ts.map +1 -1
  122. package/dist/src/backend/interceptors/pre-pr-rename.interceptor.js +4 -2
  123. package/dist/src/backend/interceptors/pre-pr-rename.interceptor.js.map +1 -1
  124. package/dist/src/backend/interceptors/pre-pr-rename.interceptor.test.js +36 -7
  125. package/dist/src/backend/interceptors/pre-pr-rename.interceptor.test.js.map +1 -1
  126. package/dist/src/backend/interceptors/pre-push-rename.interceptor.d.ts.map +1 -1
  127. package/dist/src/backend/interceptors/pre-push-rename.interceptor.js +50 -2
  128. package/dist/src/backend/interceptors/pre-push-rename.interceptor.js.map +1 -1
  129. package/dist/src/backend/interceptors/pre-push-rename.interceptor.test.js +126 -7
  130. package/dist/src/backend/interceptors/pre-push-rename.interceptor.test.js.map +1 -1
  131. package/dist/src/backend/lib/application-error.d.ts +6 -0
  132. package/dist/src/backend/lib/application-error.d.ts.map +1 -0
  133. package/dist/src/backend/lib/application-error.js +9 -0
  134. package/dist/src/backend/lib/application-error.js.map +1 -0
  135. package/dist/src/backend/lib/application-error.test.d.ts +2 -0
  136. package/dist/src/backend/lib/application-error.test.d.ts.map +1 -0
  137. package/dist/src/backend/lib/application-error.test.js +23 -0
  138. package/dist/src/backend/lib/application-error.test.js.map +1 -0
  139. package/dist/src/backend/lib/constants.d.ts +2 -0
  140. package/dist/src/backend/lib/constants.d.ts.map +1 -1
  141. package/dist/src/backend/lib/constants.js +2 -0
  142. package/dist/src/backend/lib/constants.js.map +1 -1
  143. package/dist/src/backend/lib/env.d.ts.map +1 -1
  144. package/dist/src/backend/lib/env.js +4 -3
  145. package/dist/src/backend/lib/env.js.map +1 -1
  146. package/dist/src/backend/lib/env.test.js +17 -1
  147. package/dist/src/backend/lib/env.test.js.map +1 -1
  148. package/dist/src/backend/lib/file-helpers.d.ts +7 -0
  149. package/dist/src/backend/lib/file-helpers.d.ts.map +1 -1
  150. package/dist/src/backend/lib/file-helpers.js +65 -15
  151. package/dist/src/backend/lib/file-helpers.js.map +1 -1
  152. package/dist/src/backend/lib/file-helpers.test.js +82 -2
  153. package/dist/src/backend/lib/file-helpers.test.js.map +1 -1
  154. package/dist/src/backend/lib/git-helpers.d.ts +1 -7
  155. package/dist/src/backend/lib/git-helpers.d.ts.map +1 -1
  156. package/dist/src/backend/lib/git-helpers.js +4 -33
  157. package/dist/src/backend/lib/git-helpers.js.map +1 -1
  158. package/dist/src/backend/lib/git-helpers.test.js +17 -10
  159. package/dist/src/backend/lib/git-helpers.test.js.map +1 -1
  160. package/dist/src/backend/lib/process-liveness.d.ts +2 -0
  161. package/dist/src/backend/lib/process-liveness.d.ts.map +1 -0
  162. package/dist/src/backend/lib/process-liveness.js +26 -0
  163. package/dist/src/backend/lib/process-liveness.js.map +1 -0
  164. package/dist/src/backend/lib/process-liveness.test.d.ts +2 -0
  165. package/dist/src/backend/lib/process-liveness.test.d.ts.map +1 -0
  166. package/dist/src/backend/lib/process-liveness.test.js +35 -0
  167. package/dist/src/backend/lib/process-liveness.test.js.map +1 -0
  168. package/dist/src/backend/lib/request-trust.d.ts +4 -0
  169. package/dist/src/backend/lib/request-trust.d.ts.map +1 -0
  170. package/dist/src/backend/lib/request-trust.js +116 -0
  171. package/dist/src/backend/lib/request-trust.js.map +1 -0
  172. package/dist/src/backend/lib/request-trust.test.d.ts +2 -0
  173. package/dist/src/backend/lib/request-trust.test.d.ts.map +1 -0
  174. package/dist/src/backend/lib/request-trust.test.js +64 -0
  175. package/dist/src/backend/lib/request-trust.test.js.map +1 -0
  176. package/dist/src/backend/lib/session-model.js +1 -1
  177. package/dist/src/backend/lib/session-model.js.map +1 -1
  178. package/dist/src/backend/lib/session-summaries.d.ts +4 -5
  179. package/dist/src/backend/lib/session-summaries.d.ts.map +1 -1
  180. package/dist/src/backend/lib/session-summaries.js.map +1 -1
  181. package/dist/src/backend/lib/shell.d.ts +13 -1
  182. package/dist/src/backend/lib/shell.d.ts.map +1 -1
  183. package/dist/src/backend/lib/shell.js +124 -13
  184. package/dist/src/backend/lib/shell.js.map +1 -1
  185. package/dist/src/backend/lib/shell.test.js +72 -0
  186. package/dist/src/backend/lib/shell.test.js.map +1 -1
  187. package/dist/src/backend/lib/topic-broadcaster.d.ts +18 -0
  188. package/dist/src/backend/lib/topic-broadcaster.d.ts.map +1 -0
  189. package/dist/src/backend/lib/topic-broadcaster.js +56 -0
  190. package/dist/src/backend/lib/topic-broadcaster.js.map +1 -0
  191. package/dist/src/backend/lib/topic-broadcaster.test.d.ts +2 -0
  192. package/dist/src/backend/lib/topic-broadcaster.test.d.ts.map +1 -0
  193. package/dist/src/backend/lib/topic-broadcaster.test.js +126 -0
  194. package/dist/src/backend/lib/topic-broadcaster.test.js.map +1 -0
  195. package/dist/src/backend/lib/websocket-send.d.ts +12 -0
  196. package/dist/src/backend/lib/websocket-send.d.ts.map +1 -0
  197. package/dist/src/backend/lib/websocket-send.js +46 -0
  198. package/dist/src/backend/lib/websocket-send.js.map +1 -0
  199. package/dist/src/backend/lib/websocket-send.test.d.ts +2 -0
  200. package/dist/src/backend/lib/websocket-send.test.d.ts.map +1 -0
  201. package/dist/src/backend/lib/websocket-send.test.js +103 -0
  202. package/dist/src/backend/lib/websocket-send.test.js.map +1 -0
  203. package/dist/src/backend/lib/workspace-derived-state.d.ts +9 -1
  204. package/dist/src/backend/lib/workspace-derived-state.d.ts.map +1 -1
  205. package/dist/src/backend/lib/workspace-derived-state.js +6 -1
  206. package/dist/src/backend/lib/workspace-derived-state.js.map +1 -1
  207. package/dist/src/backend/lib/workspace-derived-state.test.js +11 -3
  208. package/dist/src/backend/lib/workspace-derived-state.test.js.map +1 -1
  209. package/dist/src/backend/middleware/cors.middleware.d.ts.map +1 -1
  210. package/dist/src/backend/middleware/cors.middleware.js +3 -2
  211. package/dist/src/backend/middleware/cors.middleware.js.map +1 -1
  212. package/dist/src/backend/middleware/index.d.ts +1 -1
  213. package/dist/src/backend/middleware/index.d.ts.map +1 -1
  214. package/dist/src/backend/middleware/index.js +1 -1
  215. package/dist/src/backend/middleware/index.js.map +1 -1
  216. package/dist/src/backend/middleware/middleware.test.js +18 -1
  217. package/dist/src/backend/middleware/middleware.test.js.map +1 -1
  218. package/dist/src/backend/middleware/request-logger.middleware.d.ts +1 -2
  219. package/dist/src/backend/middleware/request-logger.middleware.d.ts.map +1 -1
  220. package/dist/src/backend/middleware/request-logger.middleware.js +0 -2
  221. package/dist/src/backend/middleware/request-logger.middleware.js.map +1 -1
  222. package/dist/src/backend/orchestration/data-backup.service.d.ts.map +1 -1
  223. package/dist/src/backend/orchestration/data-backup.service.js +3 -1
  224. package/dist/src/backend/orchestration/data-backup.service.js.map +1 -1
  225. package/dist/src/backend/orchestration/data-backup.service.test.js +32 -0
  226. package/dist/src/backend/orchestration/data-backup.service.test.js.map +1 -1
  227. package/dist/src/backend/orchestration/domain-bridges.orchestrator.d.ts +24 -11
  228. package/dist/src/backend/orchestration/domain-bridges.orchestrator.d.ts.map +1 -1
  229. package/dist/src/backend/orchestration/domain-bridges.orchestrator.js +99 -73
  230. package/dist/src/backend/orchestration/domain-bridges.orchestrator.js.map +1 -1
  231. package/dist/src/backend/orchestration/domain-bridges.orchestrator.test.js +264 -65
  232. package/dist/src/backend/orchestration/domain-bridges.orchestrator.test.js.map +1 -1
  233. package/dist/src/backend/orchestration/event-coalescer.integration.test.d.ts +2 -0
  234. package/dist/src/backend/orchestration/event-coalescer.integration.test.d.ts.map +1 -0
  235. package/dist/src/backend/orchestration/event-coalescer.integration.test.js +47 -0
  236. package/dist/src/backend/orchestration/event-coalescer.integration.test.js.map +1 -0
  237. package/dist/src/backend/orchestration/event-collector-lifecycle.integration.test.d.ts +2 -0
  238. package/dist/src/backend/orchestration/event-collector-lifecycle.integration.test.d.ts.map +1 -0
  239. package/dist/src/backend/orchestration/event-collector-lifecycle.integration.test.js +34 -0
  240. package/dist/src/backend/orchestration/event-collector-lifecycle.integration.test.js.map +1 -0
  241. package/dist/src/backend/orchestration/event-collector.orchestrator.d.ts +37 -10
  242. package/dist/src/backend/orchestration/event-collector.orchestrator.d.ts.map +1 -1
  243. package/dist/src/backend/orchestration/event-collector.orchestrator.js +317 -121
  244. package/dist/src/backend/orchestration/event-collector.orchestrator.js.map +1 -1
  245. package/dist/src/backend/orchestration/event-collector.orchestrator.test.js +503 -44
  246. package/dist/src/backend/orchestration/event-collector.orchestrator.test.js.map +1 -1
  247. package/dist/src/backend/orchestration/health.service.js +2 -2
  248. package/dist/src/backend/orchestration/health.service.js.map +1 -1
  249. package/dist/src/backend/orchestration/linear-config.helper.js +2 -2
  250. package/dist/src/backend/orchestration/linear-config.helper.js.map +1 -1
  251. package/dist/src/backend/orchestration/reconciliation.service.d.ts +39 -0
  252. package/dist/src/backend/orchestration/reconciliation.service.d.ts.map +1 -0
  253. package/dist/src/backend/{services/ratchet/service → orchestration}/reconciliation.service.js +19 -32
  254. package/dist/src/backend/orchestration/reconciliation.service.js.map +1 -0
  255. package/dist/src/backend/orchestration/reconciliation.service.test.d.ts.map +1 -0
  256. package/dist/src/backend/{services/ratchet/service → orchestration}/reconciliation.service.test.js +15 -47
  257. package/dist/src/backend/orchestration/reconciliation.service.test.js.map +1 -0
  258. package/dist/src/backend/orchestration/scheduler.service.d.ts +2 -1
  259. package/dist/src/backend/orchestration/scheduler.service.d.ts.map +1 -1
  260. package/dist/src/backend/orchestration/scheduler.service.js +107 -44
  261. package/dist/src/backend/orchestration/scheduler.service.js.map +1 -1
  262. package/dist/src/backend/orchestration/scheduler.service.test.js +266 -59
  263. package/dist/src/backend/orchestration/scheduler.service.test.js.map +1 -1
  264. package/dist/src/backend/orchestration/snapshot-reconciliation.orchestrator.d.ts +18 -11
  265. package/dist/src/backend/orchestration/snapshot-reconciliation.orchestrator.d.ts.map +1 -1
  266. package/dist/src/backend/orchestration/snapshot-reconciliation.orchestrator.js +59 -49
  267. package/dist/src/backend/orchestration/snapshot-reconciliation.orchestrator.js.map +1 -1
  268. package/dist/src/backend/orchestration/snapshot-reconciliation.orchestrator.test.js +156 -24
  269. package/dist/src/backend/orchestration/snapshot-reconciliation.orchestrator.test.js.map +1 -1
  270. package/dist/src/backend/orchestration/types.d.ts +2 -2
  271. package/dist/src/backend/orchestration/types.d.ts.map +1 -1
  272. package/dist/src/backend/orchestration/workspace-archive.orchestrator.d.ts +22 -9
  273. package/dist/src/backend/orchestration/workspace-archive.orchestrator.d.ts.map +1 -1
  274. package/dist/src/backend/orchestration/workspace-archive.orchestrator.js +38 -32
  275. package/dist/src/backend/orchestration/workspace-archive.orchestrator.js.map +1 -1
  276. package/dist/src/backend/orchestration/workspace-archive.orchestrator.test.js +52 -14
  277. package/dist/src/backend/orchestration/workspace-archive.orchestrator.test.js.map +1 -1
  278. package/dist/src/backend/orchestration/workspace-children.orchestrator.d.ts +22 -0
  279. package/dist/src/backend/orchestration/workspace-children.orchestrator.d.ts.map +1 -0
  280. package/dist/src/backend/orchestration/workspace-children.orchestrator.js +93 -0
  281. package/dist/src/backend/orchestration/workspace-children.orchestrator.js.map +1 -0
  282. package/dist/src/backend/orchestration/workspace-children.orchestrator.test.d.ts +2 -0
  283. package/dist/src/backend/orchestration/workspace-children.orchestrator.test.d.ts.map +1 -0
  284. package/dist/src/backend/orchestration/workspace-children.orchestrator.test.js +136 -0
  285. package/dist/src/backend/orchestration/workspace-children.orchestrator.test.js.map +1 -0
  286. package/dist/src/backend/orchestration/workspace-init-issue-prompts.js +3 -3
  287. package/dist/src/backend/orchestration/workspace-init-issue-prompts.js.map +1 -1
  288. package/dist/src/backend/orchestration/workspace-init-script-pipeline.d.ts +1 -1
  289. package/dist/src/backend/orchestration/workspace-init-script-pipeline.d.ts.map +1 -1
  290. package/dist/src/backend/orchestration/workspace-init-script-pipeline.js.map +1 -1
  291. package/dist/src/backend/orchestration/workspace-init.orchestrator.d.ts.map +1 -1
  292. package/dist/src/backend/orchestration/workspace-init.orchestrator.js +35 -21
  293. package/dist/src/backend/orchestration/workspace-init.orchestrator.js.map +1 -1
  294. package/dist/src/backend/orchestration/workspace-init.orchestrator.test.js +278 -102
  295. package/dist/src/backend/orchestration/workspace-init.orchestrator.test.js.map +1 -1
  296. package/dist/src/backend/orchestration/workspace-notification-delivery.orchestrator.d.ts +24 -0
  297. package/dist/src/backend/orchestration/workspace-notification-delivery.orchestrator.d.ts.map +1 -0
  298. package/dist/src/backend/orchestration/workspace-notification-delivery.orchestrator.js +66 -0
  299. package/dist/src/backend/orchestration/workspace-notification-delivery.orchestrator.js.map +1 -0
  300. package/dist/src/backend/orchestration/workspace-notification-delivery.orchestrator.test.d.ts +2 -0
  301. package/dist/src/backend/orchestration/workspace-notification-delivery.orchestrator.test.d.ts.map +1 -0
  302. package/dist/src/backend/orchestration/workspace-notification-delivery.orchestrator.test.js +235 -0
  303. package/dist/src/backend/orchestration/workspace-notification-delivery.orchestrator.test.js.map +1 -0
  304. package/dist/src/backend/prompts/branch-rename.d.ts.map +1 -1
  305. package/dist/src/backend/prompts/branch-rename.js +20 -13
  306. package/dist/src/backend/prompts/branch-rename.js.map +1 -1
  307. package/dist/src/backend/prompts/branch-rename.test.d.ts +2 -0
  308. package/dist/src/backend/prompts/branch-rename.test.d.ts.map +1 -0
  309. package/dist/src/backend/prompts/branch-rename.test.js +56 -0
  310. package/dist/src/backend/prompts/branch-rename.test.js.map +1 -0
  311. package/dist/src/backend/prompts/ratchet-dispatch.d.ts.map +1 -1
  312. package/dist/src/backend/prompts/ratchet-dispatch.js +28 -6
  313. package/dist/src/backend/prompts/ratchet-dispatch.js.map +1 -1
  314. package/dist/src/backend/prompts/ratchet-dispatch.test.js +55 -0
  315. package/dist/src/backend/prompts/ratchet-dispatch.test.js.map +1 -1
  316. package/dist/src/backend/routers/health.router.d.ts.map +1 -1
  317. package/dist/src/backend/routers/health.router.js +5 -5
  318. package/dist/src/backend/routers/health.router.js.map +1 -1
  319. package/dist/src/backend/routers/health.router.test.js +3 -5
  320. package/dist/src/backend/routers/health.router.test.js.map +1 -1
  321. package/dist/src/backend/routers/websocket/chat-connection-registry.d.ts +36 -0
  322. package/dist/src/backend/routers/websocket/chat-connection-registry.d.ts.map +1 -0
  323. package/dist/src/backend/routers/websocket/chat-connection-registry.js +153 -0
  324. package/dist/src/backend/routers/websocket/chat-connection-registry.js.map +1 -0
  325. package/dist/src/backend/routers/websocket/chat-connection-registry.test.d.ts +2 -0
  326. package/dist/src/backend/routers/websocket/chat-connection-registry.test.d.ts.map +1 -0
  327. package/dist/src/backend/routers/websocket/chat-connection-registry.test.js +281 -0
  328. package/dist/src/backend/routers/websocket/chat-connection-registry.test.js.map +1 -0
  329. package/dist/src/backend/routers/websocket/chat.handler.d.ts +2 -5
  330. package/dist/src/backend/routers/websocket/chat.handler.d.ts.map +1 -1
  331. package/dist/src/backend/routers/websocket/chat.handler.js +35 -45
  332. package/dist/src/backend/routers/websocket/chat.handler.js.map +1 -1
  333. package/dist/src/backend/routers/websocket/chat.handler.test.js +132 -44
  334. package/dist/src/backend/routers/websocket/chat.handler.test.js.map +1 -1
  335. package/dist/src/backend/routers/websocket/context-injection.test.js +450 -14
  336. package/dist/src/backend/routers/websocket/context-injection.test.js.map +1 -1
  337. package/dist/src/backend/routers/websocket/dev-logs.handler.d.ts +3 -6
  338. package/dist/src/backend/routers/websocket/dev-logs.handler.d.ts.map +1 -1
  339. package/dist/src/backend/routers/websocket/dev-logs.handler.js +25 -68
  340. package/dist/src/backend/routers/websocket/dev-logs.handler.js.map +1 -1
  341. package/dist/src/backend/routers/websocket/dev-logs.handler.test.js +75 -3
  342. package/dist/src/backend/routers/websocket/dev-logs.handler.test.js.map +1 -1
  343. package/dist/src/backend/routers/websocket/index.d.ts +2 -1
  344. package/dist/src/backend/routers/websocket/index.d.ts.map +1 -1
  345. package/dist/src/backend/routers/websocket/index.js +2 -1
  346. package/dist/src/backend/routers/websocket/index.js.map +1 -1
  347. package/dist/src/backend/routers/websocket/message-utils.d.ts +6 -0
  348. package/dist/src/backend/routers/websocket/message-utils.d.ts.map +1 -1
  349. package/dist/src/backend/routers/websocket/message-utils.js +28 -0
  350. package/dist/src/backend/routers/websocket/message-utils.js.map +1 -1
  351. package/dist/src/backend/routers/websocket/message-utils.test.d.ts +2 -0
  352. package/dist/src/backend/routers/websocket/message-utils.test.d.ts.map +1 -0
  353. package/dist/src/backend/routers/websocket/message-utils.test.js +54 -0
  354. package/dist/src/backend/routers/websocket/message-utils.test.js.map +1 -0
  355. package/dist/src/backend/routers/websocket/post-run-logs.handler.d.ts +3 -6
  356. package/dist/src/backend/routers/websocket/post-run-logs.handler.d.ts.map +1 -1
  357. package/dist/src/backend/routers/websocket/post-run-logs.handler.js +15 -57
  358. package/dist/src/backend/routers/websocket/post-run-logs.handler.js.map +1 -1
  359. package/dist/src/backend/routers/websocket/post-run-logs.handler.test.js +76 -12
  360. package/dist/src/backend/routers/websocket/post-run-logs.handler.test.js.map +1 -1
  361. package/dist/src/backend/routers/websocket/push-channel.handler.d.ts +12 -0
  362. package/dist/src/backend/routers/websocket/push-channel.handler.d.ts.map +1 -0
  363. package/dist/src/backend/routers/websocket/push-channel.handler.js +40 -0
  364. package/dist/src/backend/routers/websocket/push-channel.handler.js.map +1 -0
  365. package/dist/src/backend/routers/websocket/setup-terminal.handler.d.ts +1 -4
  366. package/dist/src/backend/routers/websocket/setup-terminal.handler.d.ts.map +1 -1
  367. package/dist/src/backend/routers/websocket/setup-terminal.handler.js +14 -46
  368. package/dist/src/backend/routers/websocket/setup-terminal.handler.js.map +1 -1
  369. package/dist/src/backend/routers/websocket/setup-terminal.handler.test.js +66 -6
  370. package/dist/src/backend/routers/websocket/setup-terminal.handler.test.js.map +1 -1
  371. package/dist/src/backend/routers/websocket/snapshots.handler.d.ts +10 -11
  372. package/dist/src/backend/routers/websocket/snapshots.handler.d.ts.map +1 -1
  373. package/dist/src/backend/routers/websocket/snapshots.handler.js +112 -70
  374. package/dist/src/backend/routers/websocket/snapshots.handler.js.map +1 -1
  375. package/dist/src/backend/routers/websocket/snapshots.handler.test.js +387 -80
  376. package/dist/src/backend/routers/websocket/snapshots.handler.test.js.map +1 -1
  377. package/dist/src/backend/routers/websocket/terminal.handler.d.ts +3 -6
  378. package/dist/src/backend/routers/websocket/terminal.handler.d.ts.map +1 -1
  379. package/dist/src/backend/routers/websocket/terminal.handler.js +130 -111
  380. package/dist/src/backend/routers/websocket/terminal.handler.js.map +1 -1
  381. package/dist/src/backend/routers/websocket/terminal.handler.test.js +291 -25
  382. package/dist/src/backend/routers/websocket/terminal.handler.test.js.map +1 -1
  383. package/dist/src/backend/routers/websocket/upgrade-utils.d.ts +33 -5
  384. package/dist/src/backend/routers/websocket/upgrade-utils.d.ts.map +1 -1
  385. package/dist/src/backend/routers/websocket/upgrade-utils.js +103 -8
  386. package/dist/src/backend/routers/websocket/upgrade-utils.js.map +1 -1
  387. package/dist/src/backend/routers/websocket/upgrade-utils.test.js +353 -3
  388. package/dist/src/backend/routers/websocket/upgrade-utils.test.js.map +1 -1
  389. package/dist/src/backend/routers/websocket/websocket.integration.test.js +54 -23
  390. package/dist/src/backend/routers/websocket/websocket.integration.test.js.map +1 -1
  391. package/dist/src/backend/schemas/websocket/setup-terminal-message.schema.d.ts +0 -2
  392. package/dist/src/backend/schemas/websocket/setup-terminal-message.schema.d.ts.map +1 -1
  393. package/dist/src/backend/schemas/websocket/setup-terminal-message.schema.js +0 -3
  394. package/dist/src/backend/schemas/websocket/setup-terminal-message.schema.js.map +1 -1
  395. package/dist/src/backend/schemas/websocket/terminal-message.schema.d.ts +0 -2
  396. package/dist/src/backend/schemas/websocket/terminal-message.schema.d.ts.map +1 -1
  397. package/dist/src/backend/schemas/websocket/terminal-message.schema.js +0 -1
  398. package/dist/src/backend/schemas/websocket/terminal-message.schema.js.map +1 -1
  399. package/dist/src/backend/server.d.ts +3 -2
  400. package/dist/src/backend/server.d.ts.map +1 -1
  401. package/dist/src/backend/server.js +195 -128
  402. package/dist/src/backend/server.js.map +1 -1
  403. package/dist/src/backend/server.upgrade.test.js +359 -126
  404. package/dist/src/backend/server.upgrade.test.js.map +1 -1
  405. package/dist/src/backend/services/auto-iteration/service/auto-iteration.service.d.ts +4 -0
  406. package/dist/src/backend/services/auto-iteration/service/auto-iteration.service.d.ts.map +1 -1
  407. package/dist/src/backend/services/auto-iteration/service/auto-iteration.service.js +78 -20
  408. package/dist/src/backend/services/auto-iteration/service/auto-iteration.service.js.map +1 -1
  409. package/dist/src/backend/services/auto-iteration/service/auto-iteration.service.test.js +166 -5
  410. package/dist/src/backend/services/auto-iteration/service/auto-iteration.service.test.js.map +1 -1
  411. package/dist/src/backend/services/auto-iteration/service/bridges.d.ts +1 -0
  412. package/dist/src/backend/services/auto-iteration/service/bridges.d.ts.map +1 -1
  413. package/dist/src/backend/services/auto-iteration/service/git-ops.d.ts.map +1 -1
  414. package/dist/src/backend/services/auto-iteration/service/git-ops.js +32 -11
  415. package/dist/src/backend/services/auto-iteration/service/git-ops.js.map +1 -1
  416. package/dist/src/backend/services/auto-iteration/service/git-ops.test.d.ts +2 -0
  417. package/dist/src/backend/services/auto-iteration/service/git-ops.test.d.ts.map +1 -0
  418. package/dist/src/backend/services/auto-iteration/service/git-ops.test.js +59 -0
  419. package/dist/src/backend/services/auto-iteration/service/git-ops.test.js.map +1 -0
  420. package/dist/src/backend/services/check-service-accessor-boundaries.test.d.ts +2 -0
  421. package/dist/src/backend/services/check-service-accessor-boundaries.test.d.ts.map +1 -0
  422. package/dist/src/backend/services/check-service-accessor-boundaries.test.js +386 -0
  423. package/dist/src/backend/services/check-service-accessor-boundaries.test.js.map +1 -0
  424. package/dist/src/backend/services/check-single-writer.test.js +82 -8
  425. package/dist/src/backend/services/check-single-writer.test.js.map +1 -1
  426. package/dist/src/backend/services/config.service.d.ts +8 -0
  427. package/dist/src/backend/services/config.service.d.ts.map +1 -1
  428. package/dist/src/backend/services/config.service.js +29 -5
  429. package/dist/src/backend/services/config.service.js.map +1 -1
  430. package/dist/src/backend/services/config.service.test.js +59 -0
  431. package/dist/src/backend/services/config.service.test.js.map +1 -1
  432. package/dist/src/backend/services/constants.d.ts +4 -1
  433. package/dist/src/backend/services/constants.d.ts.map +1 -1
  434. package/dist/src/backend/services/constants.js +4 -1
  435. package/dist/src/backend/services/constants.js.map +1 -1
  436. package/dist/src/backend/services/decision-log/index.d.ts +1 -1
  437. package/dist/src/backend/services/decision-log/index.d.ts.map +1 -1
  438. package/dist/src/backend/services/decision-log/index.js +1 -1
  439. package/dist/src/backend/services/decision-log/index.js.map +1 -1
  440. package/dist/src/backend/services/decision-log/service/decision-log-domain-exports.test.d.ts +2 -0
  441. package/dist/src/backend/services/decision-log/service/decision-log-domain-exports.test.d.ts.map +1 -0
  442. package/dist/src/backend/services/decision-log/service/decision-log-domain-exports.test.js +8 -0
  443. package/dist/src/backend/services/decision-log/service/decision-log-domain-exports.test.js.map +1 -0
  444. package/dist/src/backend/{orchestration/decision-log-query.service.d.ts → services/decision-log/service/decision-log.service.d.ts} +4 -4
  445. package/dist/src/backend/services/decision-log/service/decision-log.service.d.ts.map +1 -0
  446. package/dist/src/backend/{orchestration/decision-log-query.service.js → services/decision-log/service/decision-log.service.js} +4 -4
  447. package/dist/src/backend/services/decision-log/service/decision-log.service.js.map +1 -0
  448. package/dist/src/backend/services/decision-log/service/decision-log.service.test.d.ts +2 -0
  449. package/dist/src/backend/services/decision-log/service/decision-log.service.test.d.ts.map +1 -0
  450. package/dist/src/backend/services/decision-log/service/decision-log.service.test.js +45 -0
  451. package/dist/src/backend/services/decision-log/service/decision-log.service.test.js.map +1 -0
  452. package/dist/src/backend/services/decision-log/service/index.d.ts +2 -0
  453. package/dist/src/backend/services/decision-log/service/index.d.ts.map +1 -0
  454. package/dist/src/backend/services/decision-log/service/index.js +2 -0
  455. package/dist/src/backend/services/decision-log/service/index.js.map +1 -0
  456. package/dist/src/backend/services/env-schemas.d.ts +4 -0
  457. package/dist/src/backend/services/env-schemas.d.ts.map +1 -1
  458. package/dist/src/backend/services/env-schemas.js +4 -0
  459. package/dist/src/backend/services/env-schemas.js.map +1 -1
  460. package/dist/src/backend/services/github/service/bridges.d.ts +51 -0
  461. package/dist/src/backend/services/github/service/bridges.d.ts.map +1 -1
  462. package/dist/src/backend/services/github/service/github-cli/errors.d.ts.map +1 -1
  463. package/dist/src/backend/services/github/service/github-cli/errors.js +6 -1
  464. package/dist/src/backend/services/github/service/github-cli/errors.js.map +1 -1
  465. package/dist/src/backend/services/github/service/github-cli/schemas.d.ts +82 -0
  466. package/dist/src/backend/services/github/service/github-cli/schemas.d.ts.map +1 -1
  467. package/dist/src/backend/services/github/service/github-cli/schemas.js +63 -0
  468. package/dist/src/backend/services/github/service/github-cli/schemas.js.map +1 -1
  469. package/dist/src/backend/services/github/service/github-cli/types.d.ts +6 -0
  470. package/dist/src/backend/services/github/service/github-cli/types.d.ts.map +1 -1
  471. package/dist/src/backend/services/github/service/github-cli.service.d.ts +11 -9
  472. package/dist/src/backend/services/github/service/github-cli.service.d.ts.map +1 -1
  473. package/dist/src/backend/services/github/service/github-cli.service.js +282 -83
  474. package/dist/src/backend/services/github/service/github-cli.service.js.map +1 -1
  475. package/dist/src/backend/services/github/service/github-cli.service.test.js +461 -67
  476. package/dist/src/backend/services/github/service/github-cli.service.test.js.map +1 -1
  477. package/dist/src/backend/services/github/service/index.d.ts +3 -2
  478. package/dist/src/backend/services/github/service/index.d.ts.map +1 -1
  479. package/dist/src/backend/services/github/service/index.js +2 -1
  480. package/dist/src/backend/services/github/service/index.js.map +1 -1
  481. package/dist/src/backend/services/github/service/pr-fetch-registry.d.ts +14 -6
  482. package/dist/src/backend/services/github/service/pr-fetch-registry.d.ts.map +1 -1
  483. package/dist/src/backend/services/github/service/pr-fetch-registry.js +79 -11
  484. package/dist/src/backend/services/github/service/pr-fetch-registry.js.map +1 -1
  485. package/dist/src/backend/services/github/service/pr-fetch-registry.test.d.ts +2 -0
  486. package/dist/src/backend/services/github/service/pr-fetch-registry.test.d.ts.map +1 -0
  487. package/dist/src/backend/services/github/service/pr-fetch-registry.test.js +112 -0
  488. package/dist/src/backend/services/github/service/pr-fetch-registry.test.js.map +1 -0
  489. package/dist/src/backend/services/github/service/pr-snapshot.service.d.ts +19 -3
  490. package/dist/src/backend/services/github/service/pr-snapshot.service.d.ts.map +1 -1
  491. package/dist/src/backend/services/github/service/pr-snapshot.service.js +115 -16
  492. package/dist/src/backend/services/github/service/pr-snapshot.service.js.map +1 -1
  493. package/dist/src/backend/services/github/service/pr-snapshot.service.test.js +255 -9
  494. package/dist/src/backend/services/github/service/pr-snapshot.service.test.js.map +1 -1
  495. package/dist/src/backend/services/linear/service/linear-client.service.d.ts.map +1 -1
  496. package/dist/src/backend/services/linear/service/linear-client.service.js +33 -20
  497. package/dist/src/backend/services/linear/service/linear-client.service.js.map +1 -1
  498. package/dist/src/backend/services/linear/service/linear-client.service.test.js +76 -0
  499. package/dist/src/backend/services/linear/service/linear-client.service.test.js.map +1 -1
  500. package/dist/src/backend/services/periodic-task/index.d.ts +0 -1
  501. package/dist/src/backend/services/periodic-task/index.d.ts.map +1 -1
  502. package/dist/src/backend/services/periodic-task/index.js +0 -1
  503. package/dist/src/backend/services/periodic-task/index.js.map +1 -1
  504. package/dist/src/backend/services/periodic-task/resources/periodic-task.accessor.d.ts +13 -1
  505. package/dist/src/backend/services/periodic-task/resources/periodic-task.accessor.d.ts.map +1 -1
  506. package/dist/src/backend/services/periodic-task/resources/periodic-task.accessor.js +47 -13
  507. package/dist/src/backend/services/periodic-task/resources/periodic-task.accessor.js.map +1 -1
  508. package/dist/src/backend/services/periodic-task/resources/periodic-task.accessor.test.js +77 -0
  509. package/dist/src/backend/services/periodic-task/resources/periodic-task.accessor.test.js.map +1 -1
  510. package/dist/src/backend/services/periodic-task/service/periodic-task.service.d.ts +105 -0
  511. package/dist/src/backend/services/periodic-task/service/periodic-task.service.d.ts.map +1 -1
  512. package/dist/src/backend/services/periodic-task/service/periodic-task.service.js +100 -9
  513. package/dist/src/backend/services/periodic-task/service/periodic-task.service.js.map +1 -1
  514. package/dist/src/backend/services/periodic-task/service/periodic-task.service.test.js +227 -1
  515. package/dist/src/backend/services/periodic-task/service/periodic-task.service.test.js.map +1 -1
  516. package/dist/src/backend/services/ratchet/service/bridges.d.ts +51 -14
  517. package/dist/src/backend/services/ratchet/service/bridges.d.ts.map +1 -1
  518. package/dist/src/backend/services/ratchet/service/fixer-session.service.d.ts +7 -1
  519. package/dist/src/backend/services/ratchet/service/fixer-session.service.d.ts.map +1 -1
  520. package/dist/src/backend/services/ratchet/service/fixer-session.service.js +56 -10
  521. package/dist/src/backend/services/ratchet/service/fixer-session.service.js.map +1 -1
  522. package/dist/src/backend/services/ratchet/service/fixer-session.service.test.js +154 -27
  523. package/dist/src/backend/services/ratchet/service/fixer-session.service.test.js.map +1 -1
  524. package/dist/src/backend/services/ratchet/service/index.d.ts +2 -3
  525. package/dist/src/backend/services/ratchet/service/index.d.ts.map +1 -1
  526. package/dist/src/backend/services/ratchet/service/index.js +1 -2
  527. package/dist/src/backend/services/ratchet/service/index.js.map +1 -1
  528. package/dist/src/backend/services/ratchet/service/ratchet-active-session.helpers.d.ts +10 -12
  529. package/dist/src/backend/services/ratchet/service/ratchet-active-session.helpers.d.ts.map +1 -1
  530. package/dist/src/backend/services/ratchet/service/ratchet-active-session.helpers.js +57 -58
  531. package/dist/src/backend/services/ratchet/service/ratchet-active-session.helpers.js.map +1 -1
  532. package/dist/src/backend/services/ratchet/service/ratchet-decision-logging.helpers.d.ts +2 -5
  533. package/dist/src/backend/services/ratchet/service/ratchet-decision-logging.helpers.d.ts.map +1 -1
  534. package/dist/src/backend/services/ratchet/service/ratchet-decision-logging.helpers.js +7 -4
  535. package/dist/src/backend/services/ratchet/service/ratchet-decision-logging.helpers.js.map +1 -1
  536. package/dist/src/backend/services/ratchet/service/ratchet-domain-exports.test.js +1 -4
  537. package/dist/src/backend/services/ratchet/service/ratchet-domain-exports.test.js.map +1 -1
  538. package/dist/src/backend/services/ratchet/service/ratchet-fixer-dispatch.helpers.d.ts +6 -6
  539. package/dist/src/backend/services/ratchet/service/ratchet-fixer-dispatch.helpers.d.ts.map +1 -1
  540. package/dist/src/backend/services/ratchet/service/ratchet-fixer-dispatch.helpers.js +184 -24
  541. package/dist/src/backend/services/ratchet/service/ratchet-fixer-dispatch.helpers.js.map +1 -1
  542. package/dist/src/backend/services/ratchet/service/ratchet-pr-state.helpers.d.ts +18 -33
  543. package/dist/src/backend/services/ratchet/service/ratchet-pr-state.helpers.d.ts.map +1 -1
  544. package/dist/src/backend/services/ratchet/service/ratchet-pr-state.helpers.js +120 -54
  545. package/dist/src/backend/services/ratchet/service/ratchet-pr-state.helpers.js.map +1 -1
  546. package/dist/src/backend/services/ratchet/service/ratchet-pr-state.helpers.test.js +584 -21
  547. package/dist/src/backend/services/ratchet/service/ratchet-pr-state.helpers.test.js.map +1 -1
  548. package/dist/src/backend/services/ratchet/service/ratchet-provider-resolver.service.d.ts.map +1 -1
  549. package/dist/src/backend/services/ratchet/service/ratchet-provider-resolver.service.js +4 -4
  550. package/dist/src/backend/services/ratchet/service/ratchet-provider-resolver.service.js.map +1 -1
  551. package/dist/src/backend/services/ratchet/service/ratchet-provider-resolver.service.test.js +14 -14
  552. package/dist/src/backend/services/ratchet/service/ratchet-provider-resolver.service.test.js.map +1 -1
  553. package/dist/src/backend/services/ratchet/service/ratchet-state-machine.d.ts +11 -0
  554. package/dist/src/backend/services/ratchet/service/ratchet-state-machine.d.ts.map +1 -0
  555. package/dist/src/backend/services/ratchet/service/ratchet-state-machine.js +77 -0
  556. package/dist/src/backend/services/ratchet/service/ratchet-state-machine.js.map +1 -0
  557. package/dist/src/backend/services/ratchet/service/ratchet-state-machine.test.d.ts +2 -0
  558. package/dist/src/backend/services/ratchet/service/ratchet-state-machine.test.d.ts.map +1 -0
  559. package/dist/src/backend/services/ratchet/service/ratchet-state-machine.test.js +66 -0
  560. package/dist/src/backend/services/ratchet/service/ratchet-state-machine.test.js.map +1 -0
  561. package/dist/src/backend/services/ratchet/service/ratchet-workspace-check-coordinator.d.ts +2 -1
  562. package/dist/src/backend/services/ratchet/service/ratchet-workspace-check-coordinator.d.ts.map +1 -1
  563. package/dist/src/backend/services/ratchet/service/ratchet-workspace-check-coordinator.js +35 -9
  564. package/dist/src/backend/services/ratchet/service/ratchet-workspace-check-coordinator.js.map +1 -1
  565. package/dist/src/backend/services/ratchet/service/ratchet-workspace-check-coordinator.test.js +91 -0
  566. package/dist/src/backend/services/ratchet/service/ratchet-workspace-check-coordinator.test.js.map +1 -1
  567. package/dist/src/backend/services/ratchet/service/ratchet.service.d.ts +20 -57
  568. package/dist/src/backend/services/ratchet/service/ratchet.service.d.ts.map +1 -1
  569. package/dist/src/backend/services/ratchet/service/ratchet.service.js +289 -235
  570. package/dist/src/backend/services/ratchet/service/ratchet.service.js.map +1 -1
  571. package/dist/src/backend/services/ratchet/service/ratchet.service.test.js +1932 -482
  572. package/dist/src/backend/services/ratchet/service/ratchet.service.test.js.map +1 -1
  573. package/dist/src/backend/services/ratchet/service/ratchet.types.d.ts +22 -15
  574. package/dist/src/backend/services/ratchet/service/ratchet.types.d.ts.map +1 -1
  575. package/dist/src/backend/services/registry.d.ts +46 -3
  576. package/dist/src/backend/services/registry.d.ts.map +1 -1
  577. package/dist/src/backend/services/registry.js +45 -2
  578. package/dist/src/backend/services/registry.js.map +1 -1
  579. package/dist/src/backend/services/resources.integration.test.js +281 -97
  580. package/dist/src/backend/services/resources.integration.test.js.map +1 -1
  581. package/dist/src/backend/services/run-script/service/bridges.d.ts +4 -0
  582. package/dist/src/backend/services/run-script/service/bridges.d.ts.map +1 -1
  583. package/dist/src/backend/services/{factory-config.service.d.ts → run-script/service/factory-config.service.d.ts} +1 -1
  584. package/dist/src/backend/services/run-script/service/factory-config.service.d.ts.map +1 -0
  585. package/dist/src/backend/services/{factory-config.service.js → run-script/service/factory-config.service.js} +1 -1
  586. package/dist/src/backend/services/run-script/service/factory-config.service.js.map +1 -0
  587. package/dist/src/backend/services/run-script/service/factory-config.service.test.d.ts.map +1 -0
  588. package/dist/src/backend/services/{factory-config.service.test.js → run-script/service/factory-config.service.test.js} +1 -1
  589. package/dist/src/backend/services/run-script/service/factory-config.service.test.js.map +1 -0
  590. package/dist/src/backend/services/run-script/service/index.d.ts +2 -0
  591. package/dist/src/backend/services/run-script/service/index.d.ts.map +1 -1
  592. package/dist/src/backend/services/run-script/service/index.js +2 -0
  593. package/dist/src/backend/services/run-script/service/index.js.map +1 -1
  594. package/dist/src/backend/services/{port-allocation.service.d.ts → run-script/service/port-allocation.service.d.ts} +2 -1
  595. package/dist/src/backend/services/run-script/service/port-allocation.service.d.ts.map +1 -0
  596. package/dist/src/backend/services/{port-allocation.service.js → run-script/service/port-allocation.service.js} +12 -4
  597. package/dist/src/backend/services/run-script/service/port-allocation.service.js.map +1 -0
  598. package/dist/src/backend/services/run-script/service/port-allocation.service.test.d.ts +2 -0
  599. package/dist/src/backend/services/run-script/service/port-allocation.service.test.d.ts.map +1 -0
  600. package/dist/src/backend/services/run-script/service/port-allocation.service.test.js +43 -0
  601. package/dist/src/backend/services/run-script/service/port-allocation.service.test.js.map +1 -0
  602. package/dist/src/backend/services/{run-script-config-persistence.service.d.ts → run-script/service/run-script-config-persistence.service.d.ts} +10 -1
  603. package/dist/src/backend/services/run-script/service/run-script-config-persistence.service.d.ts.map +1 -0
  604. package/dist/src/backend/services/{run-script-config-persistence.service.js → run-script/service/run-script-config-persistence.service.js} +46 -2
  605. package/dist/src/backend/services/run-script/service/run-script-config-persistence.service.js.map +1 -0
  606. package/dist/src/backend/services/run-script/service/run-script-config-persistence.service.test.d.ts.map +1 -0
  607. package/dist/src/backend/services/{run-script-config-persistence.service.test.js → run-script/service/run-script-config-persistence.service.test.js} +47 -0
  608. package/dist/src/backend/services/run-script/service/run-script-config-persistence.service.test.js.map +1 -0
  609. package/dist/src/backend/services/run-script/service/run-script-domain-exports.test.js +11 -1
  610. package/dist/src/backend/services/run-script/service/run-script-domain-exports.test.js.map +1 -1
  611. package/dist/src/backend/services/run-script/service/run-script-process-utils.d.ts.map +1 -1
  612. package/dist/src/backend/services/run-script/service/run-script-process-utils.js +1 -1
  613. package/dist/src/backend/services/run-script/service/run-script-process-utils.js.map +1 -1
  614. package/dist/src/backend/services/run-script/service/run-script-proxy.service.d.ts.map +1 -0
  615. package/dist/src/backend/services/{run-script-proxy.service.js → run-script/service/run-script-proxy.service.js} +3 -3
  616. package/dist/src/backend/services/run-script/service/run-script-proxy.service.js.map +1 -0
  617. package/dist/src/backend/services/run-script/service/run-script-proxy.service.test.d.ts.map +1 -0
  618. package/dist/src/backend/services/{run-script-proxy.service.test.js → run-script/service/run-script-proxy.service.test.js} +3 -3
  619. package/dist/src/backend/services/run-script/service/run-script-proxy.service.test.js.map +1 -0
  620. package/dist/src/backend/services/run-script/service/run-script-state-machine.service.d.ts +9 -9
  621. package/dist/src/backend/services/run-script/service/run-script-state-machine.service.d.ts.map +1 -1
  622. package/dist/src/backend/services/run-script/service/run-script-state-machine.service.js +8 -8
  623. package/dist/src/backend/services/run-script/service/run-script-state-machine.service.js.map +1 -1
  624. package/dist/src/backend/services/run-script/service/run-script.service.d.ts +5 -0
  625. package/dist/src/backend/services/run-script/service/run-script.service.d.ts.map +1 -1
  626. package/dist/src/backend/services/run-script/service/run-script.service.js +144 -47
  627. package/dist/src/backend/services/run-script/service/run-script.service.js.map +1 -1
  628. package/dist/src/backend/services/run-script/service/run-script.service.test.js +281 -29
  629. package/dist/src/backend/services/run-script/service/run-script.service.test.js.map +1 -1
  630. package/dist/src/backend/services/run-script/service/startup-script.service.d.ts.map +1 -1
  631. package/dist/src/backend/services/run-script/service/startup-script.service.js +4 -7
  632. package/dist/src/backend/services/run-script/service/startup-script.service.js.map +1 -1
  633. package/dist/src/backend/services/run-script/service/startup-script.service.test.js +5 -1
  634. package/dist/src/backend/services/run-script/service/startup-script.service.test.js.map +1 -1
  635. package/dist/src/backend/services/service-registry-check.helpers.d.ts +6 -0
  636. package/dist/src/backend/services/service-registry-check.helpers.d.ts.map +1 -0
  637. package/dist/src/backend/services/service-registry-check.helpers.js +22 -0
  638. package/dist/src/backend/services/service-registry-check.helpers.js.map +1 -0
  639. package/dist/src/backend/services/service-registry-check.test.d.ts +2 -0
  640. package/dist/src/backend/services/service-registry-check.test.d.ts.map +1 -0
  641. package/dist/src/backend/services/service-registry-check.test.js +36 -0
  642. package/dist/src/backend/services/service-registry-check.test.js.map +1 -0
  643. package/dist/src/backend/services/session/index.d.ts +0 -2
  644. package/dist/src/backend/services/session/index.d.ts.map +1 -1
  645. package/dist/src/backend/services/session/index.js +0 -2
  646. package/dist/src/backend/services/session/index.js.map +1 -1
  647. package/dist/src/backend/services/session/resources/agent-session.accessor.d.ts +4 -3
  648. package/dist/src/backend/services/session/resources/agent-session.accessor.d.ts.map +1 -1
  649. package/dist/src/backend/services/session/resources/agent-session.accessor.js +6 -15
  650. package/dist/src/backend/services/session/resources/agent-session.accessor.js.map +1 -1
  651. package/dist/src/backend/services/session/resources/agent-session.accessor.test.js +30 -15
  652. package/dist/src/backend/services/session/resources/agent-session.accessor.test.js.map +1 -1
  653. package/dist/src/backend/services/session/service/acp/acp-runtime-manager.d.ts +13 -0
  654. package/dist/src/backend/services/session/service/acp/acp-runtime-manager.d.ts.map +1 -1
  655. package/dist/src/backend/services/session/service/acp/acp-runtime-manager.js +212 -27
  656. package/dist/src/backend/services/session/service/acp/acp-runtime-manager.js.map +1 -1
  657. package/dist/src/backend/services/session/service/acp/acp-runtime-manager.test.js +335 -40
  658. package/dist/src/backend/services/session/service/acp/acp-runtime-manager.test.js.map +1 -1
  659. package/dist/src/backend/services/session/service/acp/child-workspace-mcp-server.d.ts +11 -0
  660. package/dist/src/backend/services/session/service/acp/child-workspace-mcp-server.d.ts.map +1 -0
  661. package/dist/src/backend/services/session/service/acp/child-workspace-mcp-server.js +318 -0
  662. package/dist/src/backend/services/session/service/acp/child-workspace-mcp-server.js.map +1 -0
  663. package/dist/src/backend/services/session/service/acp/child-workspace-mcp-server.test.d.ts +2 -0
  664. package/dist/src/backend/services/session/service/acp/child-workspace-mcp-server.test.d.ts.map +1 -0
  665. package/dist/src/backend/services/session/service/acp/child-workspace-mcp-server.test.js +119 -0
  666. package/dist/src/backend/services/session/service/acp/child-workspace-mcp-server.test.js.map +1 -0
  667. package/dist/src/backend/services/session/service/acp/codex-app-server-adapter/codex-rpc-client.d.ts +18 -1
  668. package/dist/src/backend/services/session/service/acp/codex-app-server-adapter/codex-rpc-client.d.ts.map +1 -1
  669. package/dist/src/backend/services/session/service/acp/codex-app-server-adapter/codex-rpc-client.js +54 -6
  670. package/dist/src/backend/services/session/service/acp/codex-app-server-adapter/codex-rpc-client.js.map +1 -1
  671. package/dist/src/backend/services/session/service/acp/codex-app-server-adapter/codex-rpc-client.test.js +76 -1
  672. package/dist/src/backend/services/session/service/acp/codex-app-server-adapter/codex-rpc-client.test.js.map +1 -1
  673. package/dist/src/backend/services/session/service/acp/types.d.ts +7 -0
  674. package/dist/src/backend/services/session/service/acp/types.d.ts.map +1 -1
  675. package/dist/src/backend/services/session/service/bridges.d.ts +10 -5
  676. package/dist/src/backend/services/session/service/bridges.d.ts.map +1 -1
  677. package/dist/src/backend/services/session/service/chat/chat-event-forwarder.service.d.ts.map +1 -1
  678. package/dist/src/backend/services/session/service/chat/chat-event-forwarder.service.js +2 -14
  679. package/dist/src/backend/services/session/service/chat/chat-event-forwarder.service.js.map +1 -1
  680. package/dist/src/backend/services/session/service/chat/chat-message-handlers/handlers/load-session.handler.d.ts.map +1 -1
  681. package/dist/src/backend/services/session/service/chat/chat-message-handlers/handlers/load-session.handler.js +59 -76
  682. package/dist/src/backend/services/session/service/chat/chat-message-handlers/handlers/load-session.handler.js.map +1 -1
  683. package/dist/src/backend/services/session/service/chat/chat-message-handlers/handlers/load-session.handler.test.js +204 -1
  684. package/dist/src/backend/services/session/service/chat/chat-message-handlers/handlers/load-session.handler.test.js.map +1 -1
  685. package/dist/src/backend/services/session/service/chat/chat-message-handlers/handlers/queue-message.handler.d.ts.map +1 -1
  686. package/dist/src/backend/services/session/service/chat/chat-message-handlers/handlers/queue-message.handler.js +2 -11
  687. package/dist/src/backend/services/session/service/chat/chat-message-handlers/handlers/queue-message.handler.js.map +1 -1
  688. package/dist/src/backend/services/session/service/chat/chat-message-handlers/handlers/queue-message.handler.test.js +5 -18
  689. package/dist/src/backend/services/session/service/chat/chat-message-handlers/handlers/queue-message.handler.test.js.map +1 -1
  690. package/dist/src/backend/services/session/service/chat/chat-message-handlers/handlers/start.handler.d.ts.map +1 -1
  691. package/dist/src/backend/services/session/service/chat/chat-message-handlers/handlers/start.handler.js +9 -0
  692. package/dist/src/backend/services/session/service/chat/chat-message-handlers/handlers/start.handler.js.map +1 -1
  693. package/dist/src/backend/services/session/service/chat/chat-message-handlers/handlers/start.handler.test.js +34 -0
  694. package/dist/src/backend/services/session/service/chat/chat-message-handlers/handlers/start.handler.test.js.map +1 -1
  695. package/dist/src/backend/services/session/service/chat/chat-message-handlers.service.d.ts +11 -0
  696. package/dist/src/backend/services/session/service/chat/chat-message-handlers.service.d.ts.map +1 -1
  697. package/dist/src/backend/services/session/service/chat/chat-message-handlers.service.js +148 -26
  698. package/dist/src/backend/services/session/service/chat/chat-message-handlers.service.js.map +1 -1
  699. package/dist/src/backend/services/session/service/chat/chat-message-handlers.service.test.js +339 -1
  700. package/dist/src/backend/services/session/service/chat/chat-message-handlers.service.test.js.map +1 -1
  701. package/dist/src/backend/services/session/service/data/codex-session-history-loader.service.d.ts.map +1 -1
  702. package/dist/src/backend/services/session/service/data/codex-session-history-loader.service.js +200 -26
  703. package/dist/src/backend/services/session/service/data/codex-session-history-loader.service.js.map +1 -1
  704. package/dist/src/backend/services/session/service/data/codex-session-history-loader.service.test.js +352 -1
  705. package/dist/src/backend/services/session/service/data/codex-session-history-loader.service.test.js.map +1 -1
  706. package/dist/src/backend/services/session/service/data/session-data.service.d.ts +19 -160
  707. package/dist/src/backend/services/session/service/data/session-data.service.d.ts.map +1 -1
  708. package/dist/src/backend/services/session/service/data/session-data.service.js +68 -36
  709. package/dist/src/backend/services/session/service/data/session-data.service.js.map +1 -1
  710. package/dist/src/backend/services/session/service/data/session-data.service.test.d.ts +2 -0
  711. package/dist/src/backend/services/session/service/data/session-data.service.test.d.ts.map +1 -0
  712. package/dist/src/backend/services/session/service/data/session-data.service.test.js +103 -0
  713. package/dist/src/backend/services/session/service/data/session-data.service.test.js.map +1 -0
  714. package/dist/src/backend/services/session/service/data/session-history-loader.service.d.ts.map +1 -1
  715. package/dist/src/backend/services/session/service/data/session-history-loader.service.js +4 -1
  716. package/dist/src/backend/services/session/service/data/session-history-loader.service.js.map +1 -1
  717. package/dist/src/backend/services/session/service/data/session-history-loader.service.test.js +37 -0
  718. package/dist/src/backend/services/session/service/data/session-history-loader.service.test.js.map +1 -1
  719. package/dist/src/backend/services/session/service/data/session-provider-resolver.service.d.ts +9 -0
  720. package/dist/src/backend/services/session/service/data/session-provider-resolver.service.d.ts.map +1 -1
  721. package/dist/src/backend/services/session/service/data/session-provider-resolver.service.js +15 -5
  722. package/dist/src/backend/services/session/service/data/session-provider-resolver.service.js.map +1 -1
  723. package/dist/src/backend/services/session/service/data/session-provider-resolver.service.test.js +23 -10
  724. package/dist/src/backend/services/session/service/data/session-provider-resolver.service.test.js.map +1 -1
  725. package/dist/src/backend/services/session/service/index.d.ts +5 -4
  726. package/dist/src/backend/services/session/service/index.d.ts.map +1 -1
  727. package/dist/src/backend/services/session/service/index.js +3 -3
  728. package/dist/src/backend/services/session/service/index.js.map +1 -1
  729. package/dist/src/backend/services/session/service/lifecycle/acp-event-processor.d.ts +17 -4
  730. package/dist/src/backend/services/session/service/lifecycle/acp-event-processor.d.ts.map +1 -1
  731. package/dist/src/backend/services/session/service/lifecycle/acp-event-processor.js +89 -13
  732. package/dist/src/backend/services/session/service/lifecycle/acp-event-processor.js.map +1 -1
  733. package/dist/src/backend/services/session/service/lifecycle/acp-event-processor.slash-commands.test.d.ts +2 -0
  734. package/dist/src/backend/services/session/service/lifecycle/acp-event-processor.slash-commands.test.d.ts.map +1 -0
  735. package/dist/src/backend/services/session/service/lifecycle/acp-event-processor.slash-commands.test.js +159 -0
  736. package/dist/src/backend/services/session/service/lifecycle/acp-event-processor.slash-commands.test.js.map +1 -0
  737. package/dist/src/backend/services/session/service/lifecycle/acp-event-processor.text-streaming.test.d.ts +2 -0
  738. package/dist/src/backend/services/session/service/lifecycle/acp-event-processor.text-streaming.test.d.ts.map +1 -0
  739. package/dist/src/backend/services/session/service/lifecycle/acp-event-processor.text-streaming.test.js +230 -0
  740. package/dist/src/backend/services/session/service/lifecycle/acp-event-processor.text-streaming.test.js.map +1 -0
  741. package/dist/src/backend/services/session/service/lifecycle/session-pr-discovery.service.d.ts +2 -1
  742. package/dist/src/backend/services/session/service/lifecycle/session-pr-discovery.service.d.ts.map +1 -1
  743. package/dist/src/backend/services/session/service/lifecycle/session-pr-discovery.service.js +3 -28
  744. package/dist/src/backend/services/session/service/lifecycle/session-pr-discovery.service.js.map +1 -1
  745. package/dist/src/backend/services/session/service/lifecycle/session-pr-discovery.service.test.d.ts +2 -0
  746. package/dist/src/backend/services/session/service/lifecycle/session-pr-discovery.service.test.d.ts.map +1 -0
  747. package/dist/src/backend/services/session/service/lifecycle/session-pr-discovery.service.test.js +33 -0
  748. package/dist/src/backend/services/session/service/lifecycle/session-pr-discovery.service.test.js.map +1 -0
  749. package/dist/src/backend/services/session/service/lifecycle/session.config.service.d.ts.map +1 -1
  750. package/dist/src/backend/services/session/service/lifecycle/session.config.service.js +15 -4
  751. package/dist/src/backend/services/session/service/lifecycle/session.config.service.js.map +1 -1
  752. package/dist/src/backend/services/session/service/lifecycle/session.config.service.test.js +59 -6
  753. package/dist/src/backend/services/session/service/lifecycle/session.config.service.test.js.map +1 -1
  754. package/dist/src/backend/services/session/service/lifecycle/session.lifecycle.service.d.ts +23 -3
  755. package/dist/src/backend/services/session/service/lifecycle/session.lifecycle.service.d.ts.map +1 -1
  756. package/dist/src/backend/services/session/service/lifecycle/session.lifecycle.service.js +336 -40
  757. package/dist/src/backend/services/session/service/lifecycle/session.lifecycle.service.js.map +1 -1
  758. package/dist/src/backend/services/session/service/lifecycle/session.lifecycle.service.test.d.ts +2 -0
  759. package/dist/src/backend/services/session/service/lifecycle/session.lifecycle.service.test.d.ts.map +1 -0
  760. package/dist/src/backend/services/session/service/lifecycle/session.lifecycle.service.test.js +852 -0
  761. package/dist/src/backend/services/session/service/lifecycle/session.lifecycle.service.test.js.map +1 -0
  762. package/dist/src/backend/services/session/service/lifecycle/session.prompt-builder.d.ts +9 -0
  763. package/dist/src/backend/services/session/service/lifecycle/session.prompt-builder.d.ts.map +1 -1
  764. package/dist/src/backend/services/session/service/lifecycle/session.prompt-builder.js +21 -0
  765. package/dist/src/backend/services/session/service/lifecycle/session.prompt-builder.js.map +1 -1
  766. package/dist/src/backend/services/session/service/lifecycle/session.prompt-builder.test.js +32 -1
  767. package/dist/src/backend/services/session/service/lifecycle/session.prompt-builder.test.js.map +1 -1
  768. package/dist/src/backend/services/session/service/lifecycle/session.repository.d.ts +1 -1
  769. package/dist/src/backend/services/session/service/lifecycle/session.repository.d.ts.map +1 -1
  770. package/dist/src/backend/services/session/service/lifecycle/session.repository.js +3 -3
  771. package/dist/src/backend/services/session/service/lifecycle/session.repository.js.map +1 -1
  772. package/dist/src/backend/services/session/service/lifecycle/session.repository.test.js +1 -1
  773. package/dist/src/backend/services/session/service/lifecycle/session.repository.test.js.map +1 -1
  774. package/dist/src/backend/services/session/service/lifecycle/session.service.coverage.test.js +6 -0
  775. package/dist/src/backend/services/session/service/lifecycle/session.service.coverage.test.js.map +1 -1
  776. package/dist/src/backend/services/session/service/lifecycle/session.service.d.ts +12 -6
  777. package/dist/src/backend/services/session/service/lifecycle/session.service.d.ts.map +1 -1
  778. package/dist/src/backend/services/session/service/lifecycle/session.service.js +20 -5
  779. package/dist/src/backend/services/session/service/lifecycle/session.service.js.map +1 -1
  780. package/dist/src/backend/services/session/service/lifecycle/session.service.test.js +207 -20
  781. package/dist/src/backend/services/session/service/lifecycle/session.service.test.js.map +1 -1
  782. package/dist/src/backend/services/session/service/session-domain.service.d.ts +2 -0
  783. package/dist/src/backend/services/session/service/session-domain.service.d.ts.map +1 -1
  784. package/dist/src/backend/services/session/service/session-domain.service.js +28 -1
  785. package/dist/src/backend/services/session/service/session-domain.service.js.map +1 -1
  786. package/dist/src/backend/services/session/service/session-domain.service.test.js +37 -16
  787. package/dist/src/backend/services/session/service/session-domain.service.test.js.map +1 -1
  788. package/dist/src/backend/services/session/service/session-event-bus.d.ts +21 -0
  789. package/dist/src/backend/services/session/service/session-event-bus.d.ts.map +1 -0
  790. package/dist/src/backend/services/session/service/session-event-bus.js +28 -0
  791. package/dist/src/backend/services/session/service/session-event-bus.js.map +1 -0
  792. package/dist/src/backend/services/session/service/store/session-publisher.js +4 -4
  793. package/dist/src/backend/services/session/service/store/session-publisher.js.map +1 -1
  794. package/dist/src/backend/services/session/service/store/session-queue.test.js +2 -0
  795. package/dist/src/backend/services/session/service/store/session-queue.test.js.map +1 -1
  796. package/dist/src/backend/services/session/service/store/session-replay-builder.d.ts.map +1 -1
  797. package/dist/src/backend/services/session/service/store/session-replay-builder.js +26 -3
  798. package/dist/src/backend/services/session/service/store/session-replay-builder.js.map +1 -1
  799. package/dist/src/backend/services/session/service/store/session-replay-builder.test.js +136 -0
  800. package/dist/src/backend/services/session/service/store/session-replay-builder.test.js.map +1 -1
  801. package/dist/src/backend/services/session/service/store/session-runtime-machine.test.js +2 -0
  802. package/dist/src/backend/services/session/service/store/session-runtime-machine.test.js.map +1 -1
  803. package/dist/src/backend/services/session/service/store/session-store-registry.d.ts.map +1 -1
  804. package/dist/src/backend/services/session/service/store/session-store-registry.js +2 -0
  805. package/dist/src/backend/services/session/service/store/session-store-registry.js.map +1 -1
  806. package/dist/src/backend/services/session/service/store/session-store.types.d.ts +8 -0
  807. package/dist/src/backend/services/session/service/store/session-store.types.d.ts.map +1 -1
  808. package/dist/src/backend/services/session/service/store/session-transcript.d.ts +1 -0
  809. package/dist/src/backend/services/session/service/store/session-transcript.d.ts.map +1 -1
  810. package/dist/src/backend/services/session/service/store/session-transcript.js +83 -15
  811. package/dist/src/backend/services/session/service/store/session-transcript.js.map +1 -1
  812. package/dist/src/backend/services/session/service/store/session-transcript.test.js +149 -30
  813. package/dist/src/backend/services/session/service/store/session-transcript.test.js.map +1 -1
  814. package/dist/src/backend/services/session/service/store/slash-command-cache.service.d.ts.map +1 -1
  815. package/dist/src/backend/services/session/service/store/slash-command-cache.service.js +64 -23
  816. package/dist/src/backend/services/session/service/store/slash-command-cache.service.js.map +1 -1
  817. package/dist/src/backend/services/session/service/store/slash-command-cache.service.test.js +119 -7
  818. package/dist/src/backend/services/session/service/store/slash-command-cache.service.test.js.map +1 -1
  819. package/dist/src/backend/services/session/service/store/slash-command-disk-scanner.d.ts +7 -0
  820. package/dist/src/backend/services/session/service/store/slash-command-disk-scanner.d.ts.map +1 -0
  821. package/dist/src/backend/services/session/service/store/slash-command-disk-scanner.js +94 -0
  822. package/dist/src/backend/services/session/service/store/slash-command-disk-scanner.js.map +1 -0
  823. package/dist/src/backend/services/session/service/store/slash-command-disk-scanner.test.d.ts +2 -0
  824. package/dist/src/backend/services/session/service/store/slash-command-disk-scanner.test.d.ts.map +1 -0
  825. package/dist/src/backend/services/session/service/store/slash-command-disk-scanner.test.js +52 -0
  826. package/dist/src/backend/services/session/service/store/slash-command-disk-scanner.test.js.map +1 -0
  827. package/dist/src/backend/services/session/types.d.ts +25 -0
  828. package/dist/src/backend/services/session/types.d.ts.map +1 -0
  829. package/dist/src/backend/services/session/types.js +2 -0
  830. package/dist/src/backend/services/session/types.js.map +1 -0
  831. package/dist/src/backend/services/settings/index.d.ts +1 -3
  832. package/dist/src/backend/services/settings/index.d.ts.map +1 -1
  833. package/dist/src/backend/services/settings/index.js +1 -3
  834. package/dist/src/backend/services/settings/index.js.map +1 -1
  835. package/dist/src/backend/services/settings/resources/user-settings.accessor.d.ts +4 -1
  836. package/dist/src/backend/services/settings/resources/user-settings.accessor.d.ts.map +1 -1
  837. package/dist/src/backend/services/settings/resources/user-settings.accessor.js +55 -13
  838. package/dist/src/backend/services/settings/resources/user-settings.accessor.js.map +1 -1
  839. package/dist/src/backend/services/settings/service/health.service.d.ts +6 -0
  840. package/dist/src/backend/services/settings/service/health.service.d.ts.map +1 -0
  841. package/dist/src/backend/services/settings/service/health.service.js +8 -0
  842. package/dist/src/backend/services/settings/service/health.service.js.map +1 -0
  843. package/dist/src/backend/services/settings/service/health.service.test.d.ts +2 -0
  844. package/dist/src/backend/services/settings/service/health.service.test.d.ts.map +1 -0
  845. package/dist/src/backend/services/settings/service/health.service.test.js +16 -0
  846. package/dist/src/backend/services/settings/service/health.service.test.js.map +1 -0
  847. package/dist/src/backend/services/settings/service/index.d.ts +3 -0
  848. package/dist/src/backend/services/settings/service/index.d.ts.map +1 -0
  849. package/dist/src/backend/services/settings/service/index.js +3 -0
  850. package/dist/src/backend/services/settings/service/index.js.map +1 -0
  851. package/dist/src/backend/services/settings/service/settings-domain-exports.test.d.ts +2 -0
  852. package/dist/src/backend/services/settings/service/settings-domain-exports.test.d.ts.map +1 -0
  853. package/dist/src/backend/services/settings/service/settings-domain-exports.test.js +9 -0
  854. package/dist/src/backend/services/settings/service/settings-domain-exports.test.js.map +1 -0
  855. package/dist/src/backend/services/settings/service/user-settings.service.d.ts +27 -0
  856. package/dist/src/backend/services/settings/service/user-settings.service.d.ts.map +1 -0
  857. package/dist/src/backend/services/settings/service/user-settings.service.js +23 -0
  858. package/dist/src/backend/services/settings/service/user-settings.service.js.map +1 -0
  859. package/dist/src/backend/services/settings/service/user-settings.service.test.d.ts +2 -0
  860. package/dist/src/backend/services/settings/service/user-settings.service.test.d.ts.map +1 -0
  861. package/dist/src/backend/services/settings/service/user-settings.service.test.js +58 -0
  862. package/dist/src/backend/services/settings/service/user-settings.service.test.js.map +1 -0
  863. package/dist/src/backend/services/terminal/index.d.ts +0 -1
  864. package/dist/src/backend/services/terminal/index.d.ts.map +1 -1
  865. package/dist/src/backend/services/terminal/index.js +0 -1
  866. package/dist/src/backend/services/terminal/index.js.map +1 -1
  867. package/dist/src/backend/services/terminal/resources/terminal-session.accessor.d.ts +1 -1
  868. package/dist/src/backend/services/terminal/resources/terminal-session.accessor.d.ts.map +1 -1
  869. package/dist/src/backend/services/terminal/resources/terminal-session.accessor.js +2 -2
  870. package/dist/src/backend/services/terminal/resources/terminal-session.accessor.js.map +1 -1
  871. package/dist/src/backend/services/terminal/service/index.d.ts +2 -0
  872. package/dist/src/backend/services/terminal/service/index.d.ts.map +1 -1
  873. package/dist/src/backend/services/terminal/service/index.js +1 -0
  874. package/dist/src/backend/services/terminal/service/index.js.map +1 -1
  875. package/dist/src/backend/services/terminal/service/terminal-domain-exports.test.js +4 -1
  876. package/dist/src/backend/services/terminal/service/terminal-domain-exports.test.js.map +1 -1
  877. package/dist/src/backend/services/terminal/service/terminal-session.service.d.ts +95 -0
  878. package/dist/src/backend/services/terminal/service/terminal-session.service.d.ts.map +1 -0
  879. package/dist/src/backend/services/terminal/service/terminal-session.service.js +43 -0
  880. package/dist/src/backend/services/terminal/service/terminal-session.service.js.map +1 -0
  881. package/dist/src/backend/services/terminal/service/terminal-session.service.test.d.ts +2 -0
  882. package/dist/src/backend/services/terminal/service/terminal-session.service.test.d.ts.map +1 -0
  883. package/dist/src/backend/services/terminal/service/terminal-session.service.test.js +46 -0
  884. package/dist/src/backend/services/terminal/service/terminal-session.service.test.js.map +1 -0
  885. package/dist/src/backend/services/terminal/service/terminal.service.d.ts +2 -0
  886. package/dist/src/backend/services/terminal/service/terminal.service.d.ts.map +1 -1
  887. package/dist/src/backend/services/terminal/service/terminal.service.js +22 -4
  888. package/dist/src/backend/services/terminal/service/terminal.service.js.map +1 -1
  889. package/dist/src/backend/services/terminal/service/terminal.service.test.js +77 -1
  890. package/dist/src/backend/services/terminal/service/terminal.service.test.js.map +1 -1
  891. package/dist/src/backend/services/workspace/index.d.ts +0 -2
  892. package/dist/src/backend/services/workspace/index.d.ts.map +1 -1
  893. package/dist/src/backend/services/workspace/index.js +0 -2
  894. package/dist/src/backend/services/workspace/index.js.map +1 -1
  895. package/dist/src/backend/services/workspace/resources/project.accessor.d.ts +1 -1
  896. package/dist/src/backend/services/workspace/resources/project.accessor.d.ts.map +1 -1
  897. package/dist/src/backend/services/workspace/resources/project.accessor.js +26 -1
  898. package/dist/src/backend/services/workspace/resources/project.accessor.js.map +1 -1
  899. package/dist/src/backend/services/workspace/resources/workspace-notification.accessor.d.ts +19 -0
  900. package/dist/src/backend/services/workspace/resources/workspace-notification.accessor.d.ts.map +1 -0
  901. package/dist/src/backend/services/workspace/resources/workspace-notification.accessor.js +34 -0
  902. package/dist/src/backend/services/workspace/resources/workspace-notification.accessor.js.map +1 -0
  903. package/dist/src/backend/services/workspace/resources/workspace.accessor.d.ts +91 -5
  904. package/dist/src/backend/services/workspace/resources/workspace.accessor.d.ts.map +1 -1
  905. package/dist/src/backend/services/workspace/resources/workspace.accessor.js +328 -5
  906. package/dist/src/backend/services/workspace/resources/workspace.accessor.js.map +1 -1
  907. package/dist/src/backend/services/workspace/resources/workspace.accessor.test.js +601 -0
  908. package/dist/src/backend/services/workspace/resources/workspace.accessor.test.js.map +1 -1
  909. package/dist/src/backend/services/workspace/service/bridges.d.ts +1 -2
  910. package/dist/src/backend/services/workspace/service/bridges.d.ts.map +1 -1
  911. package/dist/src/backend/services/workspace/service/index.d.ts +12 -1
  912. package/dist/src/backend/services/workspace/service/index.d.ts.map +1 -1
  913. package/dist/src/backend/services/workspace/service/index.js +11 -1
  914. package/dist/src/backend/services/workspace/service/index.js.map +1 -1
  915. package/dist/src/backend/services/workspace/service/lifecycle/activity.service.d.ts +2 -2
  916. package/dist/src/backend/services/workspace/service/lifecycle/activity.service.d.ts.map +1 -1
  917. package/dist/src/backend/services/workspace/service/lifecycle/activity.service.js +20 -3
  918. package/dist/src/backend/services/workspace/service/lifecycle/activity.service.js.map +1 -1
  919. package/dist/src/backend/services/workspace/service/lifecycle/activity.service.test.js +30 -0
  920. package/dist/src/backend/services/workspace/service/lifecycle/activity.service.test.js.map +1 -1
  921. package/dist/src/backend/services/workspace/service/lifecycle/creation.service.d.ts +18 -0
  922. package/dist/src/backend/services/workspace/service/lifecycle/creation.service.d.ts.map +1 -1
  923. package/dist/src/backend/services/workspace/service/lifecycle/creation.service.js +57 -25
  924. package/dist/src/backend/services/workspace/service/lifecycle/creation.service.js.map +1 -1
  925. package/dist/src/backend/services/workspace/service/lifecycle/creation.service.test.js +50 -10
  926. package/dist/src/backend/services/workspace/service/lifecycle/creation.service.test.js.map +1 -1
  927. package/dist/src/backend/services/workspace/service/lifecycle/data.service.d.ts +183 -122
  928. package/dist/src/backend/services/workspace/service/lifecycle/data.service.d.ts.map +1 -1
  929. package/dist/src/backend/services/workspace/service/lifecycle/data.service.js +29 -8
  930. package/dist/src/backend/services/workspace/service/lifecycle/data.service.js.map +1 -1
  931. package/dist/src/backend/services/workspace/service/lifecycle/data.service.test.d.ts +2 -0
  932. package/dist/src/backend/services/workspace/service/lifecycle/data.service.test.d.ts.map +1 -0
  933. package/dist/src/backend/services/workspace/service/lifecycle/data.service.test.js +36 -0
  934. package/dist/src/backend/services/workspace/service/lifecycle/data.service.test.js.map +1 -0
  935. package/dist/src/backend/services/workspace/service/lifecycle/state-machine.service.d.ts +2 -0
  936. package/dist/src/backend/services/workspace/service/lifecycle/state-machine.service.d.ts.map +1 -1
  937. package/dist/src/backend/services/workspace/service/lifecycle/state-machine.service.js +49 -25
  938. package/dist/src/backend/services/workspace/service/lifecycle/state-machine.service.js.map +1 -1
  939. package/dist/src/backend/services/workspace/service/lifecycle/state-machine.service.test.js +127 -4
  940. package/dist/src/backend/services/workspace/service/lifecycle/state-machine.service.test.js.map +1 -1
  941. package/dist/src/backend/services/workspace/service/lifecycle/workspace-auto-iteration.service.d.ts +206 -0
  942. package/dist/src/backend/services/workspace/service/lifecycle/workspace-auto-iteration.service.d.ts.map +1 -0
  943. package/dist/src/backend/services/workspace/service/lifecycle/workspace-auto-iteration.service.js +26 -0
  944. package/dist/src/backend/services/workspace/service/lifecycle/workspace-auto-iteration.service.js.map +1 -0
  945. package/dist/src/backend/services/workspace/service/lifecycle/workspace-pr-snapshot.service.d.ts +76 -0
  946. package/dist/src/backend/services/workspace/service/lifecycle/workspace-pr-snapshot.service.d.ts.map +1 -0
  947. package/dist/src/backend/services/workspace/service/lifecycle/workspace-pr-snapshot.service.js +32 -0
  948. package/dist/src/backend/services/workspace/service/lifecycle/workspace-pr-snapshot.service.js.map +1 -0
  949. package/dist/src/backend/services/workspace/service/lifecycle/workspace-pr-snapshot.service.test.d.ts +2 -0
  950. package/dist/src/backend/services/workspace/service/lifecycle/workspace-pr-snapshot.service.test.d.ts.map +1 -0
  951. package/dist/src/backend/services/workspace/service/lifecycle/workspace-pr-snapshot.service.test.js +65 -0
  952. package/dist/src/backend/services/workspace/service/lifecycle/workspace-pr-snapshot.service.test.js.map +1 -0
  953. package/dist/src/backend/services/workspace/service/lifecycle/workspace-ratchet.service.d.ts +243 -0
  954. package/dist/src/backend/services/workspace/service/lifecycle/workspace-ratchet.service.d.ts.map +1 -0
  955. package/dist/src/backend/services/workspace/service/lifecycle/workspace-ratchet.service.js +42 -0
  956. package/dist/src/backend/services/workspace/service/lifecycle/workspace-ratchet.service.js.map +1 -0
  957. package/dist/src/backend/services/workspace/service/lifecycle/workspace-relationships.service.d.ts +186 -0
  958. package/dist/src/backend/services/workspace/service/lifecycle/workspace-relationships.service.d.ts.map +1 -0
  959. package/dist/src/backend/services/workspace/service/lifecycle/workspace-relationships.service.js +11 -0
  960. package/dist/src/backend/services/workspace/service/lifecycle/workspace-relationships.service.js.map +1 -0
  961. package/dist/src/backend/services/workspace/service/lifecycle/workspace-run-script.service.d.ts +161 -0
  962. package/dist/src/backend/services/workspace/service/lifecycle/workspace-run-script.service.d.ts.map +1 -0
  963. package/dist/src/backend/services/workspace/service/lifecycle/workspace-run-script.service.js +51 -0
  964. package/dist/src/backend/services/workspace/service/lifecycle/workspace-run-script.service.js.map +1 -0
  965. package/dist/src/backend/services/workspace/service/lifecycle/workspace-state-capabilities.test.d.ts +2 -0
  966. package/dist/src/backend/services/workspace/service/lifecycle/workspace-state-capabilities.test.d.ts.map +1 -0
  967. package/dist/src/backend/services/workspace/service/lifecycle/workspace-state-capabilities.test.js +96 -0
  968. package/dist/src/backend/services/workspace/service/lifecycle/workspace-state-capabilities.test.js.map +1 -0
  969. package/dist/src/backend/services/workspace/service/pr-discovery-schedule.d.ts +2 -0
  970. package/dist/src/backend/services/workspace/service/pr-discovery-schedule.d.ts.map +1 -0
  971. package/dist/src/backend/services/workspace/service/pr-discovery-schedule.js +11 -0
  972. package/dist/src/backend/services/workspace/service/pr-discovery-schedule.js.map +1 -0
  973. package/dist/src/backend/services/workspace/service/pr-discovery-schedule.test.d.ts +2 -0
  974. package/dist/src/backend/services/workspace/service/pr-discovery-schedule.test.d.ts.map +1 -0
  975. package/dist/src/backend/services/workspace/service/pr-discovery-schedule.test.js +24 -0
  976. package/dist/src/backend/services/workspace/service/pr-discovery-schedule.test.js.map +1 -0
  977. package/dist/src/backend/services/workspace/service/query/project-management.service.d.ts +33 -21
  978. package/dist/src/backend/services/workspace/service/query/project-management.service.d.ts.map +1 -1
  979. package/dist/src/backend/services/workspace/service/query/workspace-maintenance.service.d.ts +446 -0
  980. package/dist/src/backend/services/workspace/service/query/workspace-maintenance.service.d.ts.map +1 -0
  981. package/dist/src/backend/services/workspace/service/query/workspace-maintenance.service.js +23 -0
  982. package/dist/src/backend/services/workspace/service/query/workspace-maintenance.service.js.map +1 -0
  983. package/dist/src/backend/services/workspace/service/query/workspace-maintenance.service.test.d.ts +2 -0
  984. package/dist/src/backend/services/workspace/service/query/workspace-maintenance.service.test.d.ts.map +1 -0
  985. package/dist/src/backend/services/workspace/service/query/workspace-maintenance.service.test.js +36 -0
  986. package/dist/src/backend/services/workspace/service/query/workspace-maintenance.service.test.js.map +1 -0
  987. package/dist/src/backend/services/workspace/service/query/workspace-notification.service.d.ts +58 -0
  988. package/dist/src/backend/services/workspace/service/query/workspace-notification.service.d.ts.map +1 -0
  989. package/dist/src/backend/services/workspace/service/query/workspace-notification.service.js +24 -0
  990. package/dist/src/backend/services/workspace/service/query/workspace-notification.service.js.map +1 -0
  991. package/dist/src/backend/services/workspace/service/query/workspace-notification.service.test.d.ts +2 -0
  992. package/dist/src/backend/services/workspace/service/query/workspace-notification.service.test.d.ts.map +1 -0
  993. package/dist/src/backend/services/workspace/service/query/workspace-notification.service.test.js +28 -0
  994. package/dist/src/backend/services/workspace/service/query/workspace-notification.service.test.js.map +1 -0
  995. package/dist/src/backend/services/workspace/service/query/workspace-query.service.d.ts +58 -56
  996. package/dist/src/backend/services/workspace/service/query/workspace-query.service.d.ts.map +1 -1
  997. package/dist/src/backend/services/workspace/service/query/workspace-query.service.js +75 -101
  998. package/dist/src/backend/services/workspace/service/query/workspace-query.service.js.map +1 -1
  999. package/dist/src/backend/services/workspace/service/query/workspace-query.service.test.js +165 -59
  1000. package/dist/src/backend/services/workspace/service/query/workspace-query.service.test.js.map +1 -1
  1001. package/dist/src/backend/services/{workspace-snapshot-store.service.d.ts → workspace/service/snapshot/workspace-snapshot-store.service.d.ts} +26 -46
  1002. package/dist/src/backend/services/workspace/service/snapshot/workspace-snapshot-store.service.d.ts.map +1 -0
  1003. package/dist/src/backend/services/{workspace-snapshot-store.service.js → workspace/service/snapshot/workspace-snapshot-store.service.js} +153 -26
  1004. package/dist/src/backend/services/workspace/service/snapshot/workspace-snapshot-store.service.js.map +1 -0
  1005. package/dist/src/backend/services/workspace/service/snapshot/workspace-snapshot-store.service.test.d.ts.map +1 -0
  1006. package/dist/src/backend/services/{workspace-snapshot-store.service.test.js → workspace/service/snapshot/workspace-snapshot-store.service.test.js} +261 -18
  1007. package/dist/src/backend/services/workspace/service/snapshot/workspace-snapshot-store.service.test.js.map +1 -0
  1008. package/dist/src/backend/services/workspace/service/state/kanban-state.d.ts +18 -3
  1009. package/dist/src/backend/services/workspace/service/state/kanban-state.d.ts.map +1 -1
  1010. package/dist/src/backend/services/workspace/service/state/kanban-state.js +136 -16
  1011. package/dist/src/backend/services/workspace/service/state/kanban-state.js.map +1 -1
  1012. package/dist/src/backend/services/workspace/service/state/kanban-state.test.js +319 -63
  1013. package/dist/src/backend/services/workspace/service/state/kanban-state.test.js.map +1 -1
  1014. package/dist/src/backend/services/workspace/service/workspace-domain-exports.test.js +18 -1
  1015. package/dist/src/backend/services/workspace/service/workspace-domain-exports.test.js.map +1 -1
  1016. package/dist/src/backend/services/workspace/service/worktree/git-clone.service.d.ts.map +1 -0
  1017. package/dist/src/backend/services/{git-clone.service.js → workspace/service/worktree/git-clone.service.js} +22 -6
  1018. package/dist/src/backend/services/workspace/service/worktree/git-clone.service.js.map +1 -0
  1019. package/dist/src/backend/services/workspace/service/worktree/git-clone.service.test.d.ts.map +1 -0
  1020. package/dist/src/backend/services/{git-clone.service.test.js → workspace/service/worktree/git-clone.service.test.js} +82 -2
  1021. package/dist/src/backend/services/workspace/service/worktree/git-clone.service.test.js.map +1 -0
  1022. package/dist/src/backend/services/{git-ops.service.d.ts → workspace/service/worktree/git-ops.service.d.ts} +3 -4
  1023. package/dist/src/backend/services/workspace/service/worktree/git-ops.service.d.ts.map +1 -0
  1024. package/dist/src/backend/services/{git-ops.service.js → workspace/service/worktree/git-ops.service.js} +61 -34
  1025. package/dist/src/backend/services/workspace/service/worktree/git-ops.service.js.map +1 -0
  1026. package/dist/src/backend/services/workspace/service/worktree/git-ops.service.test.d.ts.map +1 -0
  1027. package/dist/src/backend/services/workspace/service/worktree/git-ops.service.test.js +227 -0
  1028. package/dist/src/backend/services/workspace/service/worktree/git-ops.service.test.js.map +1 -0
  1029. package/dist/src/backend/services/workspace/service/worktree/worktree-init.test.js +28 -20
  1030. package/dist/src/backend/services/workspace/service/worktree/worktree-init.test.js.map +1 -1
  1031. package/dist/src/backend/services/workspace/service/worktree/worktree-lifecycle.service.d.ts +1 -1
  1032. package/dist/src/backend/services/workspace/service/worktree/worktree-lifecycle.service.d.ts.map +1 -1
  1033. package/dist/src/backend/services/workspace/service/worktree/worktree-lifecycle.service.js +43 -6
  1034. package/dist/src/backend/services/workspace/service/worktree/worktree-lifecycle.service.js.map +1 -1
  1035. package/dist/src/backend/services/workspace/service/worktree/worktree-lifecycle.service.test.js +122 -7
  1036. package/dist/src/backend/services/workspace/service/worktree/worktree-lifecycle.service.test.js.map +1 -1
  1037. package/dist/src/backend/services/workspace/types.d.ts +32 -0
  1038. package/dist/src/backend/services/workspace/types.d.ts.map +1 -0
  1039. package/dist/src/backend/services/workspace/types.js +2 -0
  1040. package/dist/src/backend/services/workspace/types.js.map +1 -0
  1041. package/dist/src/backend/services/workspace-git-state.service.d.ts +99 -0
  1042. package/dist/src/backend/services/workspace-git-state.service.d.ts.map +1 -0
  1043. package/dist/src/backend/services/workspace-git-state.service.js +393 -0
  1044. package/dist/src/backend/services/workspace-git-state.service.js.map +1 -0
  1045. package/dist/src/backend/services/workspace-git-state.service.test.d.ts +2 -0
  1046. package/dist/src/backend/services/workspace-git-state.service.test.d.ts.map +1 -0
  1047. package/dist/src/backend/services/workspace-git-state.service.test.js +476 -0
  1048. package/dist/src/backend/services/workspace-git-state.service.test.js.map +1 -0
  1049. package/dist/src/backend/testing/typescript-import-test-utils.d.ts +11 -0
  1050. package/dist/src/backend/testing/typescript-import-test-utils.d.ts.map +1 -0
  1051. package/dist/src/backend/testing/typescript-import-test-utils.js +47 -0
  1052. package/dist/src/backend/testing/typescript-import-test-utils.js.map +1 -0
  1053. package/dist/src/backend/trpc/admin.router.test.js +18 -26
  1054. package/dist/src/backend/trpc/admin.router.test.js.map +1 -1
  1055. package/dist/src/backend/trpc/admin.trpc.d.ts +5 -3
  1056. package/dist/src/backend/trpc/admin.trpc.d.ts.map +1 -1
  1057. package/dist/src/backend/trpc/admin.trpc.js +11 -15
  1058. package/dist/src/backend/trpc/admin.trpc.js.map +1 -1
  1059. package/dist/src/backend/trpc/application-error-mapper.d.ts +4 -0
  1060. package/dist/src/backend/trpc/application-error-mapper.d.ts.map +1 -0
  1061. package/dist/src/backend/trpc/application-error-mapper.js +16 -0
  1062. package/dist/src/backend/trpc/application-error-mapper.js.map +1 -0
  1063. package/dist/src/backend/trpc/application-error-mapper.test.d.ts +2 -0
  1064. package/dist/src/backend/trpc/application-error-mapper.test.d.ts.map +1 -0
  1065. package/dist/src/backend/trpc/application-error-mapper.test.js +23 -0
  1066. package/dist/src/backend/trpc/application-error-mapper.test.js.map +1 -0
  1067. package/dist/src/backend/trpc/auto-iteration.trpc.d.ts +1 -1
  1068. package/dist/src/backend/trpc/auto-iteration.trpc.d.ts.map +1 -1
  1069. package/dist/src/backend/trpc/auto-iteration.trpc.js +20 -12
  1070. package/dist/src/backend/trpc/auto-iteration.trpc.js.map +1 -1
  1071. package/dist/src/backend/trpc/auto-iteration.trpc.test.js +10 -9
  1072. package/dist/src/backend/trpc/auto-iteration.trpc.test.js.map +1 -1
  1073. package/dist/src/backend/trpc/closed-sessions.router.test.js +3 -4
  1074. package/dist/src/backend/trpc/closed-sessions.router.test.js.map +1 -1
  1075. package/dist/src/backend/trpc/closed-sessions.trpc.d.ts +5 -5
  1076. package/dist/src/backend/trpc/closed-sessions.trpc.d.ts.map +1 -1
  1077. package/dist/src/backend/trpc/closed-sessions.trpc.js +8 -4
  1078. package/dist/src/backend/trpc/closed-sessions.trpc.js.map +1 -1
  1079. package/dist/src/backend/trpc/context-dependencies.test.d.ts +2 -0
  1080. package/dist/src/backend/trpc/context-dependencies.test.d.ts.map +1 -0
  1081. package/dist/src/backend/trpc/context-dependencies.test.js +134 -0
  1082. package/dist/src/backend/trpc/context-dependencies.test.js.map +1 -0
  1083. package/dist/src/backend/trpc/decision-log.router.test.js +11 -8
  1084. package/dist/src/backend/trpc/decision-log.router.test.js.map +1 -1
  1085. package/dist/src/backend/trpc/decision-log.trpc.d.ts +3 -3
  1086. package/dist/src/backend/trpc/decision-log.trpc.d.ts.map +1 -1
  1087. package/dist/src/backend/trpc/decision-log.trpc.js +6 -7
  1088. package/dist/src/backend/trpc/decision-log.trpc.js.map +1 -1
  1089. package/dist/src/backend/trpc/github.router.test.js +41 -6
  1090. package/dist/src/backend/trpc/github.router.test.js.map +1 -1
  1091. package/dist/src/backend/trpc/github.trpc.d.ts +6 -5
  1092. package/dist/src/backend/trpc/github.trpc.d.ts.map +1 -1
  1093. package/dist/src/backend/trpc/github.trpc.js +36 -8
  1094. package/dist/src/backend/trpc/github.trpc.js.map +1 -1
  1095. package/dist/src/backend/trpc/index.d.ts +509 -606
  1096. package/dist/src/backend/trpc/index.d.ts.map +1 -1
  1097. package/dist/src/backend/trpc/index.js +0 -1
  1098. package/dist/src/backend/trpc/index.js.map +1 -1
  1099. package/dist/src/backend/trpc/issue-filter.d.ts.map +1 -1
  1100. package/dist/src/backend/trpc/issue-filter.js.map +1 -1
  1101. package/dist/src/backend/trpc/linear.router.test.js +21 -21
  1102. package/dist/src/backend/trpc/linear.router.test.js.map +1 -1
  1103. package/dist/src/backend/trpc/linear.trpc.d.ts +4 -4
  1104. package/dist/src/backend/trpc/linear.trpc.d.ts.map +1 -1
  1105. package/dist/src/backend/trpc/linear.trpc.js +9 -10
  1106. package/dist/src/backend/trpc/linear.trpc.js.map +1 -1
  1107. package/dist/src/backend/trpc/periodic-task.trpc.d.ts +17 -17
  1108. package/dist/src/backend/trpc/periodic-task.trpc.d.ts.map +1 -1
  1109. package/dist/src/backend/trpc/periodic-task.trpc.js +17 -17
  1110. package/dist/src/backend/trpc/periodic-task.trpc.js.map +1 -1
  1111. package/dist/src/backend/trpc/project.router.test.js +135 -39
  1112. package/dist/src/backend/trpc/project.router.test.js.map +1 -1
  1113. package/dist/src/backend/trpc/project.trpc.d.ts +37 -25
  1114. package/dist/src/backend/trpc/project.trpc.d.ts.map +1 -1
  1115. package/dist/src/backend/trpc/project.trpc.js +55 -50
  1116. package/dist/src/backend/trpc/project.trpc.js.map +1 -1
  1117. package/dist/src/backend/trpc/session.router.test.js +49 -10
  1118. package/dist/src/backend/trpc/session.router.test.js.map +1 -1
  1119. package/dist/src/backend/trpc/session.trpc.d.ts +50 -392
  1120. package/dist/src/backend/trpc/session.trpc.d.ts.map +1 -1
  1121. package/dist/src/backend/trpc/session.trpc.js +48 -31
  1122. package/dist/src/backend/trpc/session.trpc.js.map +1 -1
  1123. package/dist/src/backend/trpc/trpc.d.ts +11 -4
  1124. package/dist/src/backend/trpc/trpc.d.ts.map +1 -1
  1125. package/dist/src/backend/trpc/trpc.js +50 -10
  1126. package/dist/src/backend/trpc/trpc.js.map +1 -1
  1127. package/dist/src/backend/trpc/trpc.test.d.ts +2 -0
  1128. package/dist/src/backend/trpc/trpc.test.d.ts.map +1 -0
  1129. package/dist/src/backend/trpc/trpc.test.js +76 -0
  1130. package/dist/src/backend/trpc/trpc.test.js.map +1 -0
  1131. package/dist/src/backend/trpc/user-settings.router.test.js +28 -14
  1132. package/dist/src/backend/trpc/user-settings.router.test.js.map +1 -1
  1133. package/dist/src/backend/trpc/user-settings.trpc.d.ts +6 -3
  1134. package/dist/src/backend/trpc/user-settings.trpc.d.ts.map +1 -1
  1135. package/dist/src/backend/trpc/user-settings.trpc.js +14 -14
  1136. package/dist/src/backend/trpc/user-settings.trpc.js.map +1 -1
  1137. package/dist/src/backend/trpc/workspace/children.router.test.d.ts +2 -0
  1138. package/dist/src/backend/trpc/workspace/children.router.test.d.ts.map +1 -0
  1139. package/dist/src/backend/trpc/workspace/children.router.test.js +257 -0
  1140. package/dist/src/backend/trpc/workspace/children.router.test.js.map +1 -0
  1141. package/dist/src/backend/trpc/workspace/children.trpc.d.ts +154 -0
  1142. package/dist/src/backend/trpc/workspace/children.trpc.d.ts.map +1 -0
  1143. package/dist/src/backend/trpc/workspace/children.trpc.js +163 -0
  1144. package/dist/src/backend/trpc/workspace/children.trpc.js.map +1 -0
  1145. package/dist/src/backend/trpc/workspace/composition.test.d.ts +2 -0
  1146. package/dist/src/backend/trpc/workspace/composition.test.d.ts.map +1 -0
  1147. package/dist/src/backend/trpc/workspace/composition.test.js +51 -0
  1148. package/dist/src/backend/trpc/workspace/composition.test.js.map +1 -0
  1149. package/dist/src/backend/trpc/workspace/files.router.test.js +6 -0
  1150. package/dist/src/backend/trpc/workspace/files.router.test.js.map +1 -1
  1151. package/dist/src/backend/trpc/workspace/files.trpc.d.ts.map +1 -1
  1152. package/dist/src/backend/trpc/workspace/files.trpc.js +19 -22
  1153. package/dist/src/backend/trpc/workspace/files.trpc.js.map +1 -1
  1154. package/dist/src/backend/trpc/workspace/git.router.test.js +137 -147
  1155. package/dist/src/backend/trpc/workspace/git.router.test.js.map +1 -1
  1156. package/dist/src/backend/trpc/workspace/git.trpc.d.ts +2 -2
  1157. package/dist/src/backend/trpc/workspace/git.trpc.d.ts.map +1 -1
  1158. package/dist/src/backend/trpc/workspace/git.trpc.js +53 -75
  1159. package/dist/src/backend/trpc/workspace/git.trpc.js.map +1 -1
  1160. package/dist/src/backend/trpc/workspace/ide.router.test.js +10 -9
  1161. package/dist/src/backend/trpc/workspace/ide.router.test.js.map +1 -1
  1162. package/dist/src/backend/trpc/workspace/ide.trpc.d.ts.map +1 -1
  1163. package/dist/src/backend/trpc/workspace/ide.trpc.js +4 -4
  1164. package/dist/src/backend/trpc/workspace/ide.trpc.js.map +1 -1
  1165. package/dist/src/backend/trpc/workspace/init.router.test.js +42 -43
  1166. package/dist/src/backend/trpc/workspace/init.router.test.js.map +1 -1
  1167. package/dist/src/backend/trpc/workspace/init.trpc.d.ts +25 -18
  1168. package/dist/src/backend/trpc/workspace/init.trpc.d.ts.map +1 -1
  1169. package/dist/src/backend/trpc/workspace/init.trpc.js +16 -13
  1170. package/dist/src/backend/trpc/workspace/init.trpc.js.map +1 -1
  1171. package/dist/src/backend/trpc/workspace/run-script.router.test.js +29 -11
  1172. package/dist/src/backend/trpc/workspace/run-script.router.test.js.map +1 -1
  1173. package/dist/src/backend/trpc/workspace/run-script.trpc.d.ts.map +1 -1
  1174. package/dist/src/backend/trpc/workspace/run-script.trpc.js +7 -8
  1175. package/dist/src/backend/trpc/workspace/run-script.trpc.js.map +1 -1
  1176. package/dist/src/backend/trpc/workspace/workspace-helpers.d.ts +49 -35
  1177. package/dist/src/backend/trpc/workspace/workspace-helpers.d.ts.map +1 -1
  1178. package/dist/src/backend/trpc/workspace/workspace-helpers.js +7 -8
  1179. package/dist/src/backend/trpc/workspace/workspace-helpers.js.map +1 -1
  1180. package/dist/src/backend/trpc/workspace/workspace-helpers.test.js +17 -15
  1181. package/dist/src/backend/trpc/workspace/workspace-helpers.test.js.map +1 -1
  1182. package/dist/src/backend/trpc/workspace.router.test.js +230 -80
  1183. package/dist/src/backend/trpc/workspace.router.test.js.map +1 -1
  1184. package/dist/src/backend/trpc/workspace.trpc.d.ts +1510 -162
  1185. package/dist/src/backend/trpc/workspace.trpc.d.ts.map +1 -1
  1186. package/dist/src/backend/trpc/workspace.trpc.js +64 -47
  1187. package/dist/src/backend/trpc/workspace.trpc.js.map +1 -1
  1188. package/dist/src/cli/database-path.d.ts.map +1 -1
  1189. package/dist/src/cli/database-path.js +4 -2
  1190. package/dist/src/cli/database-path.js.map +1 -1
  1191. package/dist/src/cli/database-path.test.js +10 -0
  1192. package/dist/src/cli/database-path.test.js.map +1 -1
  1193. package/dist/src/cli/serve-env.d.ts.map +1 -1
  1194. package/dist/src/cli/serve-env.js +5 -3
  1195. package/dist/src/cli/serve-env.js.map +1 -1
  1196. package/dist/src/cli/serve-env.test.js +30 -0
  1197. package/dist/src/cli/serve-env.test.js.map +1 -1
  1198. package/dist/src/shared/acp-protocol/protocol/constants.d.ts +1 -0
  1199. package/dist/src/shared/acp-protocol/protocol/constants.d.ts.map +1 -1
  1200. package/dist/src/shared/acp-protocol/protocol/constants.js +1 -0
  1201. package/dist/src/shared/acp-protocol/protocol/constants.js.map +1 -1
  1202. package/dist/src/shared/acp-protocol/protocol/index.d.ts +1 -0
  1203. package/dist/src/shared/acp-protocol/protocol/index.d.ts.map +1 -1
  1204. package/dist/src/shared/acp-protocol/protocol/index.js +1 -0
  1205. package/dist/src/shared/acp-protocol/protocol/index.js.map +1 -1
  1206. package/dist/src/shared/acp-protocol/protocol/messages.d.ts +8 -1
  1207. package/dist/src/shared/acp-protocol/protocol/messages.d.ts.map +1 -1
  1208. package/dist/src/shared/acp-protocol/protocol/messages.js +2 -0
  1209. package/dist/src/shared/acp-protocol/protocol/messages.js.map +1 -1
  1210. package/dist/src/shared/acp-protocol/protocol/renderer-window.d.ts +3 -0
  1211. package/dist/src/shared/acp-protocol/protocol/renderer-window.d.ts.map +1 -0
  1212. package/dist/src/shared/acp-protocol/protocol/renderer-window.js +37 -0
  1213. package/dist/src/shared/acp-protocol/protocol/renderer-window.js.map +1 -0
  1214. package/dist/src/shared/acp-protocol/protocol/websocket.d.ts +9 -0
  1215. package/dist/src/shared/acp-protocol/protocol/websocket.d.ts.map +1 -1
  1216. package/dist/src/shared/acp-protocol/protocol/websocket.js +1 -0
  1217. package/dist/src/shared/acp-protocol/protocol/websocket.js.map +1 -1
  1218. package/dist/src/shared/core/enums.d.ts +6 -0
  1219. package/dist/src/shared/core/enums.d.ts.map +1 -1
  1220. package/dist/src/shared/core/enums.js +5 -0
  1221. package/dist/src/shared/core/enums.js.map +1 -1
  1222. package/dist/src/shared/core/index.d.ts +2 -2
  1223. package/dist/src/shared/core/index.d.ts.map +1 -1
  1224. package/dist/src/shared/core/index.js +2 -2
  1225. package/dist/src/shared/core/index.js.map +1 -1
  1226. package/dist/src/shared/core/workspace-sidebar-status.d.ts +4 -2
  1227. package/dist/src/shared/core/workspace-sidebar-status.d.ts.map +1 -1
  1228. package/dist/src/shared/core/workspace-sidebar-status.js +11 -0
  1229. package/dist/src/shared/core/workspace-sidebar-status.js.map +1 -1
  1230. package/dist/src/shared/issue-start-prompt.d.ts.map +1 -1
  1231. package/dist/src/shared/issue-start-prompt.js +38 -3
  1232. package/dist/src/shared/issue-start-prompt.js.map +1 -1
  1233. package/dist/src/shared/proxy-utils.d.ts +2 -0
  1234. package/dist/src/shared/proxy-utils.d.ts.map +1 -1
  1235. package/dist/src/shared/proxy-utils.js +20 -2
  1236. package/dist/src/shared/proxy-utils.js.map +1 -1
  1237. package/dist/src/shared/schemas/export-data.schema.d.ts +10 -0
  1238. package/dist/src/shared/schemas/export-data.schema.d.ts.map +1 -1
  1239. package/dist/src/shared/schemas/export-data.schema.js +3 -1
  1240. package/dist/src/shared/schemas/export-data.schema.js.map +1 -1
  1241. package/dist/src/shared/session-runtime.d.ts +15 -3
  1242. package/dist/src/shared/session-runtime.d.ts.map +1 -1
  1243. package/dist/src/shared/session-runtime.js +33 -0
  1244. package/dist/src/shared/session-runtime.js.map +1 -1
  1245. package/dist/src/shared/workspace-flow-state.d.ts +4 -2
  1246. package/dist/src/shared/workspace-flow-state.d.ts.map +1 -1
  1247. package/dist/src/shared/workspace-flow-state.js +16 -1
  1248. package/dist/src/shared/workspace-flow-state.js.map +1 -1
  1249. package/dist/src/shared/workspace-notifications.d.ts +10 -0
  1250. package/dist/src/shared/workspace-notifications.d.ts.map +1 -0
  1251. package/dist/src/shared/workspace-notifications.js +11 -0
  1252. package/dist/src/shared/workspace-notifications.js.map +1 -0
  1253. package/dist/src/shared/workspace-snapshot.d.ts +1020 -0
  1254. package/dist/src/shared/workspace-snapshot.d.ts.map +1 -0
  1255. package/dist/src/shared/workspace-snapshot.js +113 -0
  1256. package/dist/src/shared/workspace-snapshot.js.map +1 -0
  1257. package/dist/src/shared/workspace-status-reason.d.ts +6 -3
  1258. package/dist/src/shared/workspace-status-reason.d.ts.map +1 -1
  1259. package/dist/src/shared/workspace-status-reason.js +35 -0
  1260. package/dist/src/shared/workspace-status-reason.js.map +1 -1
  1261. package/dist/src/test-utils/application-graph.d.ts +69 -0
  1262. package/dist/src/test-utils/application-graph.d.ts.map +1 -0
  1263. package/dist/src/test-utils/application-graph.js +353 -0
  1264. package/dist/src/test-utils/application-graph.js.map +1 -0
  1265. package/dist/tsconfig.backend.tsbuildinfo +1 -1
  1266. package/package.json +5 -8
  1267. package/prisma/migrations/20260622135020_add_child_workspaces/migration.sql +99 -0
  1268. package/prisma/migrations/20260622135100_add_child_workspaces/migration.sql +39 -0
  1269. package/prisma/migrations/20260622195338_add_notification_direction/migration.sql +22 -0
  1270. package/prisma/migrations/20260622220000_add_parent_workspace_id_index/migration.sql +2 -0
  1271. package/prisma/migrations/20260715201522_add_ratchet_dispatch_record/migration.sql +125 -0
  1272. package/prisma/migrations/20260717150000_add_pr_discovery_schedule/migration.sql +8 -0
  1273. package/prisma/migrations/20260717160000_add_ratchet_review_trigger_mode/migration.sql +2 -0
  1274. package/prisma/schema.prisma +55 -1
  1275. package/prompts/ratchet/dispatch.md +2 -0
  1276. package/dist/client/assets/channel-DTP0LAbj.js +0 -1
  1277. package/dist/client/assets/chunk-2J33WTMH-BfBptE9H.js +0 -1
  1278. package/dist/client/assets/classDiagram-4FO5ZUOK-Ctn2iFx-.js +0 -1
  1279. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-Ctn2iFx-.js +0 -1
  1280. package/dist/client/assets/index-QJUv8N-T.js +0 -662
  1281. package/dist/client/assets/index-z2dDRh55.css +0 -1
  1282. package/dist/client/assets/infoDiagram-5YYISTIA-EcbEZrdJ.js +0 -2
  1283. package/dist/client/assets/stateDiagram-AJRCARHV-JRGDQ0Z0.js +0 -1
  1284. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-YFRSfKcY.js +0 -1
  1285. package/dist/src/backend/clients/index.d.ts +0 -2
  1286. package/dist/src/backend/clients/index.d.ts.map +0 -1
  1287. package/dist/src/backend/clients/index.js +0 -2
  1288. package/dist/src/backend/clients/index.js.map +0 -1
  1289. package/dist/src/backend/orchestration/decision-log-query.service.d.ts.map +0 -1
  1290. package/dist/src/backend/orchestration/decision-log-query.service.js.map +0 -1
  1291. package/dist/src/backend/orchestration/decision-log-query.service.test.d.ts +0 -2
  1292. package/dist/src/backend/orchestration/decision-log-query.service.test.d.ts.map +0 -1
  1293. package/dist/src/backend/orchestration/decision-log-query.service.test.js +0 -44
  1294. package/dist/src/backend/orchestration/decision-log-query.service.test.js.map +0 -1
  1295. package/dist/src/backend/services/factory-config.service.d.ts.map +0 -1
  1296. package/dist/src/backend/services/factory-config.service.js.map +0 -1
  1297. package/dist/src/backend/services/factory-config.service.test.d.ts.map +0 -1
  1298. package/dist/src/backend/services/factory-config.service.test.js.map +0 -1
  1299. package/dist/src/backend/services/git-clone.service.d.ts.map +0 -1
  1300. package/dist/src/backend/services/git-clone.service.js.map +0 -1
  1301. package/dist/src/backend/services/git-clone.service.test.d.ts.map +0 -1
  1302. package/dist/src/backend/services/git-clone.service.test.js.map +0 -1
  1303. package/dist/src/backend/services/git-ops.service.d.ts.map +0 -1
  1304. package/dist/src/backend/services/git-ops.service.js.map +0 -1
  1305. package/dist/src/backend/services/git-ops.service.test.d.ts.map +0 -1
  1306. package/dist/src/backend/services/git-ops.service.test.js +0 -133
  1307. package/dist/src/backend/services/git-ops.service.test.js.map +0 -1
  1308. package/dist/src/backend/services/port-allocation.service.d.ts.map +0 -1
  1309. package/dist/src/backend/services/port-allocation.service.js.map +0 -1
  1310. package/dist/src/backend/services/ratchet/service/ratchet-review-poll.helpers.d.ts +0 -16
  1311. package/dist/src/backend/services/ratchet/service/ratchet-review-poll.helpers.d.ts.map +0 -1
  1312. package/dist/src/backend/services/ratchet/service/ratchet-review-poll.helpers.js +0 -52
  1313. package/dist/src/backend/services/ratchet/service/ratchet-review-poll.helpers.js.map +0 -1
  1314. package/dist/src/backend/services/ratchet/service/reconciliation.service.d.ts +0 -21
  1315. package/dist/src/backend/services/ratchet/service/reconciliation.service.d.ts.map +0 -1
  1316. package/dist/src/backend/services/ratchet/service/reconciliation.service.js.map +0 -1
  1317. package/dist/src/backend/services/ratchet/service/reconciliation.service.test.d.ts.map +0 -1
  1318. package/dist/src/backend/services/ratchet/service/reconciliation.service.test.js.map +0 -1
  1319. package/dist/src/backend/services/run-script-config-persistence.service.d.ts.map +0 -1
  1320. package/dist/src/backend/services/run-script-config-persistence.service.js.map +0 -1
  1321. package/dist/src/backend/services/run-script-config-persistence.service.test.d.ts.map +0 -1
  1322. package/dist/src/backend/services/run-script-config-persistence.service.test.js.map +0 -1
  1323. package/dist/src/backend/services/run-script-proxy.service.d.ts.map +0 -1
  1324. package/dist/src/backend/services/run-script-proxy.service.js.map +0 -1
  1325. package/dist/src/backend/services/run-script-proxy.service.test.d.ts.map +0 -1
  1326. package/dist/src/backend/services/run-script-proxy.service.test.js.map +0 -1
  1327. package/dist/src/backend/services/session/service/chat/chat-connection.service.d.ts +0 -19
  1328. package/dist/src/backend/services/session/service/chat/chat-connection.service.d.ts.map +0 -1
  1329. package/dist/src/backend/services/session/service/chat/chat-connection.service.js +0 -78
  1330. package/dist/src/backend/services/session/service/chat/chat-connection.service.js.map +0 -1
  1331. package/dist/src/backend/services/session/service/chat/chat-connection.service.test.d.ts +0 -2
  1332. package/dist/src/backend/services/session/service/chat/chat-connection.service.test.d.ts.map +0 -1
  1333. package/dist/src/backend/services/session/service/chat/chat-connection.service.test.js +0 -214
  1334. package/dist/src/backend/services/session/service/chat/chat-connection.service.test.js.map +0 -1
  1335. package/dist/src/backend/services/workspace/service/query/user-settings-query.service.d.ts +0 -24
  1336. package/dist/src/backend/services/workspace/service/query/user-settings-query.service.d.ts.map +0 -1
  1337. package/dist/src/backend/services/workspace/service/query/user-settings-query.service.js +0 -17
  1338. package/dist/src/backend/services/workspace/service/query/user-settings-query.service.js.map +0 -1
  1339. package/dist/src/backend/services/workspace-snapshot-store.service.d.ts.map +0 -1
  1340. package/dist/src/backend/services/workspace-snapshot-store.service.js.map +0 -1
  1341. package/dist/src/backend/services/workspace-snapshot-store.service.test.d.ts.map +0 -1
  1342. package/dist/src/backend/services/workspace-snapshot-store.service.test.js.map +0 -1
  1343. package/dist/src/backend/trpc/procedures/index.d.ts +0 -2
  1344. package/dist/src/backend/trpc/procedures/index.d.ts.map +0 -1
  1345. package/dist/src/backend/trpc/procedures/index.js +0 -2
  1346. package/dist/src/backend/trpc/procedures/index.js.map +0 -1
  1347. package/dist/src/backend/trpc/procedures/project-scoped.d.ts +0 -6
  1348. package/dist/src/backend/trpc/procedures/project-scoped.d.ts.map +0 -1
  1349. package/dist/src/backend/trpc/procedures/project-scoped.js +0 -18
  1350. package/dist/src/backend/trpc/procedures/project-scoped.js.map +0 -1
  1351. package/dist/src/backend/trpc/procedures/project-scoped.test.d.ts +0 -2
  1352. package/dist/src/backend/trpc/procedures/project-scoped.test.d.ts.map +0 -1
  1353. package/dist/src/backend/trpc/procedures/project-scoped.test.js +0 -17
  1354. package/dist/src/backend/trpc/procedures/project-scoped.test.js.map +0 -1
  1355. /package/dist/src/backend/{services/ratchet/service → orchestration}/reconciliation.service.test.d.ts +0 -0
  1356. /package/dist/src/backend/services/{factory-config.service.test.d.ts → run-script/service/factory-config.service.test.d.ts} +0 -0
  1357. /package/dist/src/backend/services/{run-script-config-persistence.service.test.d.ts → run-script/service/run-script-config-persistence.service.test.d.ts} +0 -0
  1358. /package/dist/src/backend/services/{run-script-proxy.service.d.ts → run-script/service/run-script-proxy.service.d.ts} +0 -0
  1359. /package/dist/src/backend/services/{run-script-proxy.service.test.d.ts → run-script/service/run-script-proxy.service.test.d.ts} +0 -0
  1360. /package/dist/src/backend/services/{workspace-snapshot-store.service.test.d.ts → workspace/service/snapshot/workspace-snapshot-store.service.test.d.ts} +0 -0
  1361. /package/dist/src/backend/services/{git-clone.service.d.ts → workspace/service/worktree/git-clone.service.d.ts} +0 -0
  1362. /package/dist/src/backend/services/{git-clone.service.test.d.ts → workspace/service/worktree/git-clone.service.test.d.ts} +0 -0
  1363. /package/dist/src/backend/services/{git-ops.service.test.d.ts → workspace/service/worktree/git-ops.service.test.d.ts} +0 -0
@@ -3,21 +3,30 @@ import { SERVICE_TIMEOUT_MS } from '../../../../backend/services/constants.js';
3
3
  import { CIStatus, RatchetState, SessionStatus } from '../../../../shared/core/index.js';
4
4
  import { unsafeCoerce } from '../../../../test-utils/unsafe-coerce.js';
5
5
  vi.mock('@/backend/services/workspace', () => ({
6
- workspaceAccessor: {
6
+ workspaceDataService: {
7
7
  findById: vi.fn(),
8
- findWithPRsForRatchet: vi.fn(),
9
- findForRatchetById: vi.fn(),
10
- update: vi.fn(),
8
+ },
9
+ workspaceRatchetService: {
10
+ findCandidates: vi.fn(),
11
+ findCandidateById: vi.fn(),
12
+ clearActiveSession: vi.fn(),
13
+ enable: vi.fn(),
14
+ disable: vi.fn(),
15
+ transitionStateIfEnabled: vi.fn(),
16
+ settleIdleWhileDisabled: vi.fn(),
17
+ recordDispatchIfEnabled: vi.fn(),
18
+ adoptActiveSessionIfEnabled: vi.fn(),
19
+ recordSessionEnd: vi.fn(),
11
20
  },
12
21
  }));
13
22
  vi.mock('@/backend/services/session', () => ({
14
- agentSessionAccessor: {
15
- findById: vi.fn(),
16
- findByWorkspaceId: vi.fn(),
23
+ sessionDataService: {
24
+ findAgentSessionById: vi.fn(),
25
+ findAgentSessionsByWorkspaceId: vi.fn(),
17
26
  },
18
27
  }));
19
28
  vi.mock('@/backend/services/settings', () => ({
20
- userSettingsAccessor: {
29
+ userSettingsService: {
21
30
  get: vi.fn(),
22
31
  getDefaultSessionProvider: vi.fn(),
23
32
  },
@@ -35,27 +44,39 @@ vi.mock('@/backend/services/logger.service', () => ({
35
44
  error: vi.fn(),
36
45
  }),
37
46
  }));
38
- import { agentSessionAccessor } from '../../../../backend/services/session/index.js';
39
- import { userSettingsAccessor } from '../../../../backend/services/settings/index.js';
40
- import { workspaceAccessor } from '../../../../backend/services/workspace/index.js';
47
+ import { userSettingsService } from '../../../../backend/services/settings/index.js';
48
+ import { workspaceDataService, workspaceRatchetService } from '../../../../backend/services/workspace/index.js';
41
49
  import { fixerSessionService } from './fixer-session.service.js';
42
- import { RATCHET_STATE_CHANGED, RATCHET_TOGGLED, ratchetService, } from './ratchet.service.js';
50
+ import { RATCHET_DISPATCH_CHANGED, RATCHET_STATE_CHANGED, RATCHET_TOGGLED, ratchetService, } from './ratchet.service.js';
51
+ import { buildRatchetingLogContext } from './ratchet-decision-logging.helpers.js';
52
+ import { computeDispatchSnapshotKey, computeLatestReviewActivityAtMs, determineRatchetState, } from './ratchet-pr-state.helpers.js';
43
53
  const mockSessionBridge = {
54
+ findSessionById: vi.fn(),
55
+ findSessionsByWorkspaceId: vi.fn(),
56
+ acquireFixerSession: vi.fn(),
44
57
  isSessionRunning: vi.fn(),
45
58
  isSessionWorking: vi.fn(),
46
59
  stopSession: vi.fn(),
47
60
  startSession: vi.fn(),
61
+ restartSession: vi.fn(),
48
62
  sendSessionMessage: vi.fn(),
49
63
  injectCommittedUserMessage: vi.fn(),
50
64
  };
65
+ const mockWorkspaceBridge = {
66
+ findFixerContext: vi.fn(),
67
+ recordSessionEnd: vi.fn(),
68
+ };
51
69
  const mockGitHubBridge = {
52
70
  extractPRInfo: vi.fn(),
53
71
  getPRFullDetails: vi.fn(),
54
72
  getReviewComments: vi.fn(),
73
+ getResolvedReviewCommentIds: vi.fn(),
55
74
  computeCIStatus: vi.fn(),
56
75
  getAuthenticatedUsername: vi.fn(),
57
76
  fetchAndComputePRState: vi.fn(),
58
- startFetch: vi.fn(),
77
+ isRecentlyFetched: vi.fn(),
78
+ isFetchInFlight: vi.fn(),
79
+ startFetch: vi.fn(() => 41),
59
80
  registerFetch: vi.fn(),
60
81
  cancelFetch: vi.fn(),
61
82
  };
@@ -70,15 +91,24 @@ describe('ratchet service (state-change + idle dispatch)', () => {
70
91
  unsafeCoerce(ratchetService).isShuttingDown = false;
71
92
  unsafeCoerce(ratchetService).workspaceCheckTimeoutMs =
72
93
  SERVICE_TIMEOUT_MS.ratchetWorkspaceCheck;
73
- unsafeCoerce(ratchetService).reviewPollTrackers.clear();
74
94
  ratchetService.configure({
75
95
  session: mockSessionBridge,
76
96
  github: mockGitHubBridge,
77
97
  snapshot: mockSnapshotBridge,
78
- });
98
+ workspace: mockWorkspaceBridge,
99
+ });
100
+ vi.mocked(workspaceRatchetService.transitionStateIfEnabled).mockResolvedValue(true);
101
+ vi.mocked(workspaceRatchetService.settleIdleWhileDisabled).mockResolvedValue(true);
102
+ vi.mocked(workspaceRatchetService.recordDispatchIfEnabled).mockResolvedValue(true);
103
+ vi.mocked(workspaceRatchetService.adoptActiveSessionIfEnabled).mockResolvedValue(true);
104
+ vi.mocked(workspaceRatchetService.recordSessionEnd).mockResolvedValue(true);
105
+ vi.mocked(mockWorkspaceBridge.recordSessionEnd).mockResolvedValue(true);
106
+ vi.mocked(mockSessionBridge.findSessionsByWorkspaceId).mockResolvedValue([]);
107
+ vi.mocked(mockSessionBridge.findSessionsByWorkspaceId).mockResolvedValue([]);
79
108
  vi.mocked(mockGitHubBridge.getAuthenticatedUsername).mockResolvedValue(null);
109
+ vi.mocked(mockGitHubBridge.getResolvedReviewCommentIds).mockResolvedValue(new Set());
80
110
  vi.mocked(mockSessionBridge.isSessionWorking).mockReturnValue(false);
81
- vi.mocked(userSettingsAccessor.get).mockResolvedValue({
111
+ vi.mocked(userSettingsService.get).mockResolvedValue({
82
112
  id: 'settings-1',
83
113
  userId: 'default',
84
114
  preferredIde: 'cursor',
@@ -89,6 +119,7 @@ describe('ratchet service (state-change + idle dispatch)', () => {
89
119
  cachedSlashCommands: null,
90
120
  ratchetEnabled: false,
91
121
  ratchetReplyToPrComments: true,
122
+ ratchetReviewTriggerMode: 'CHANGES_REQUESTED',
92
123
  defaultSessionProvider: 'CLAUDE',
93
124
  defaultClaudeModel: 'sonnet',
94
125
  defaultCodexModel: 'default',
@@ -99,10 +130,18 @@ describe('ratchet service (state-change + idle dispatch)', () => {
99
130
  createdAt: new Date('2026-01-01T00:00:00.000Z'),
100
131
  updatedAt: new Date('2026-01-01T00:00:00.000Z'),
101
132
  });
102
- vi.mocked(userSettingsAccessor.getDefaultSessionProvider).mockResolvedValue('CLAUDE');
133
+ vi.mocked(userSettingsService.getDefaultSessionProvider).mockResolvedValue('CLAUDE');
134
+ });
135
+ it('records session completion through the configured workspace bridge', async () => {
136
+ await ratchetService.recordSessionEnd('workspace-1', 'session-1', 'COMPLETED');
137
+ expect(mockWorkspaceBridge.recordSessionEnd).toHaveBeenCalledWith('workspace-1', 'session-1', 'COMPLETED');
138
+ expect(workspaceRatchetService.recordSessionEnd).not.toHaveBeenCalled();
139
+ });
140
+ afterEach(() => {
141
+ ratchetService.removeAllListeners();
103
142
  });
104
143
  it('checks workspaces and processes each', async () => {
105
- vi.mocked(workspaceAccessor.findWithPRsForRatchet).mockResolvedValue([
144
+ vi.mocked(workspaceRatchetService.findCandidates).mockResolvedValue([
106
145
  {
107
146
  id: 'ws-1',
108
147
  prUrl: 'https://github.com/example/repo/pull/1',
@@ -116,6 +155,8 @@ describe('ratchet service (state-change + idle dispatch)', () => {
116
155
  ratchetActiveSessionId: null,
117
156
  ratchetLastCiRunId: null,
118
157
  prReviewLastCheckedAt: null,
158
+ ratchetDispatchOutcome: null,
159
+ ratchetDispatchRetryCount: 0,
119
160
  },
120
161
  ]);
121
162
  vi.spyOn(unsafeCoerce(ratchetService), 'processWorkspace').mockResolvedValue({
@@ -139,9 +180,10 @@ describe('ratchet service (state-change + idle dispatch)', () => {
139
180
  ratchetActiveSessionId: null,
140
181
  ratchetLastCiRunId: null,
141
182
  prReviewLastCheckedAt: null,
183
+ ratchetDispatchOutcome: null,
184
+ ratchetDispatchRetryCount: 0,
142
185
  };
143
186
  const fetchPRStateSpy = vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState');
144
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
145
187
  const triggerSpy = vi.spyOn(unsafeCoerce(ratchetService), 'triggerFixer');
146
188
  const result = await unsafeCoerce(ratchetService).processWorkspace(workspace);
147
189
  expect(fetchPRStateSpy).not.toHaveBeenCalled();
@@ -150,6 +192,70 @@ describe('ratchet service (state-change + idle dispatch)', () => {
150
192
  action: { type: 'DISABLED', reason: 'Workspace ratcheting disabled' },
151
193
  newState: RatchetState.IDLE,
152
194
  });
195
+ expect(workspaceRatchetService.settleIdleWhileDisabled).toHaveBeenCalledWith('ws-disabled', RatchetState.IDLE);
196
+ });
197
+ it('settles a disabled workspace to IDLE via CAS and emits an accurate fromState', async () => {
198
+ const workspace = {
199
+ id: 'ws-disabled-settle',
200
+ prUrl: 'https://github.com/example/repo/pull/2',
201
+ prNumber: 2,
202
+ prState: 'OPEN',
203
+ prCiStatus: CIStatus.UNKNOWN,
204
+ ratchetEnabled: false,
205
+ ratchetState: RatchetState.CI_FAILED,
206
+ ratchetActiveSessionId: null,
207
+ ratchetLastCiRunId: null,
208
+ prReviewLastCheckedAt: null,
209
+ ratchetDispatchOutcome: null,
210
+ ratchetDispatchRetryCount: 0,
211
+ };
212
+ vi.mocked(workspaceRatchetService.settleIdleWhileDisabled).mockResolvedValue(true);
213
+ const events = [];
214
+ ratchetService.on(RATCHET_STATE_CHANGED, (event) => {
215
+ events.push(event);
216
+ });
217
+ const result = await unsafeCoerce(ratchetService).processWorkspace(workspace);
218
+ expect(workspaceRatchetService.settleIdleWhileDisabled).toHaveBeenCalledWith('ws-disabled-settle', RatchetState.CI_FAILED);
219
+ expect(result).toMatchObject({
220
+ previousState: RatchetState.CI_FAILED,
221
+ newState: RatchetState.IDLE,
222
+ action: { type: 'DISABLED' },
223
+ });
224
+ expect(events).toEqual([
225
+ {
226
+ workspaceId: 'ws-disabled-settle',
227
+ fromState: RatchetState.CI_FAILED,
228
+ toState: RatchetState.IDLE,
229
+ },
230
+ ]);
231
+ });
232
+ it('does not emit for a disabled workspace when the settle CAS loses', async () => {
233
+ const workspace = {
234
+ id: 'ws-disabled-settle-race',
235
+ prUrl: 'https://github.com/example/repo/pull/2',
236
+ prNumber: 2,
237
+ prState: 'OPEN',
238
+ prCiStatus: CIStatus.UNKNOWN,
239
+ ratchetEnabled: false,
240
+ ratchetState: RatchetState.CI_FAILED,
241
+ ratchetActiveSessionId: null,
242
+ ratchetLastCiRunId: null,
243
+ prReviewLastCheckedAt: null,
244
+ ratchetDispatchOutcome: null,
245
+ ratchetDispatchRetryCount: 0,
246
+ };
247
+ vi.mocked(workspaceRatchetService.settleIdleWhileDisabled).mockResolvedValue(false);
248
+ const events = [];
249
+ ratchetService.on(RATCHET_STATE_CHANGED, (event) => {
250
+ events.push(event);
251
+ });
252
+ const result = await unsafeCoerce(ratchetService).processWorkspace(workspace);
253
+ expect(result).toMatchObject({
254
+ previousState: RatchetState.CI_FAILED,
255
+ newState: RatchetState.CI_FAILED,
256
+ action: { type: 'DISABLED' },
257
+ });
258
+ expect(events).toEqual([]);
153
259
  });
154
260
  it('does not dispatch when workspace is not idle', async () => {
155
261
  const workspace = {
@@ -163,6 +269,8 @@ describe('ratchet service (state-change + idle dispatch)', () => {
163
269
  ratchetActiveSessionId: null,
164
270
  ratchetLastCiRunId: null,
165
271
  prReviewLastCheckedAt: new Date('2026-01-01T00:00:00Z'),
272
+ ratchetDispatchOutcome: null,
273
+ ratchetDispatchRetryCount: 0,
166
274
  };
167
275
  vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({
168
276
  ciStatus: CIStatus.SUCCESS,
@@ -182,8 +290,7 @@ describe('ratchet service (state-change + idle dispatch)', () => {
182
290
  },
183
291
  ],
184
292
  });
185
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
186
- vi.mocked(agentSessionAccessor.findByWorkspaceId).mockResolvedValue([
293
+ vi.mocked(mockSessionBridge.findSessionsByWorkspaceId).mockResolvedValue([
187
294
  {
188
295
  id: 'chat-1',
189
296
  workflow: 'default-followup',
@@ -212,6 +319,8 @@ describe('ratchet service (state-change + idle dispatch)', () => {
212
319
  ratchetActiveSessionId: null,
213
320
  ratchetLastCiRunId: '2026-01-01T00:00:00Z',
214
321
  prReviewLastCheckedAt: new Date('2026-01-01T00:00:00Z'),
322
+ ratchetDispatchOutcome: null,
323
+ ratchetDispatchRetryCount: 0,
215
324
  };
216
325
  vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({
217
326
  ciStatus: CIStatus.FAILURE,
@@ -222,8 +331,7 @@ describe('ratchet service (state-change + idle dispatch)', () => {
222
331
  prState: 'OPEN',
223
332
  prNumber: 4,
224
333
  });
225
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
226
- vi.mocked(agentSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
334
+ vi.mocked(mockSessionBridge.findSessionsByWorkspaceId).mockResolvedValue([]);
227
335
  vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
228
336
  status: 'started',
229
337
  sessionId: 'ratchet-session',
@@ -233,11 +341,69 @@ describe('ratchet service (state-change + idle dispatch)', () => {
233
341
  expect(result).toMatchObject({
234
342
  action: { type: 'TRIGGERED_FIXER' },
235
343
  });
236
- const finalUpdatePayload = vi.mocked(workspaceAccessor.update).mock.calls.at(-1)?.[1];
237
- expect(finalUpdatePayload.ratchetLastCiRunId).toBe('2026-01-02T00:00:00Z');
344
+ expect(workspaceRatchetService.recordDispatchIfEnabled).toHaveBeenCalledWith('ws-change', {
345
+ sessionId: 'ratchet-session',
346
+ snapshotKey: '2026-01-02T00:00:00Z',
347
+ retryCount: 0,
348
+ });
349
+ const finalUpdatePayload = vi
350
+ .mocked(workspaceRatchetService.transitionStateIfEnabled)
351
+ .mock.calls.at(-1)?.[2];
352
+ expect(finalUpdatePayload).not.toHaveProperty('ratchetLastCiRunId');
238
353
  expect(finalUpdatePayload).not.toHaveProperty('prReviewLastCheckedAt');
239
354
  expect(mockSnapshotBridge.recordReviewCheck).toHaveBeenCalledWith('ws-change', expect.any(Date));
240
355
  });
356
+ it('stops fixer and returns disabled when active-session recording loses disable race', async () => {
357
+ const workspace = {
358
+ id: 'ws-disable-race-dispatch',
359
+ prUrl: 'https://github.com/example/repo/pull/40',
360
+ prNumber: 40,
361
+ prState: 'OPEN',
362
+ prCiStatus: CIStatus.UNKNOWN,
363
+ ratchetEnabled: true,
364
+ ratchetState: RatchetState.IDLE,
365
+ ratchetActiveSessionId: null,
366
+ ratchetLastCiRunId: 'old-snapshot',
367
+ prReviewLastCheckedAt: new Date('2026-01-01T00:00:00Z'),
368
+ ratchetDispatchOutcome: null,
369
+ ratchetDispatchRetryCount: 0,
370
+ };
371
+ vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({
372
+ ciStatus: CIStatus.FAILURE,
373
+ snapshotKey: 'new-snapshot',
374
+ hasChangesRequested: false,
375
+ latestReviewActivityAtMs: null,
376
+ statusCheckRollup: null,
377
+ prState: 'OPEN',
378
+ prNumber: 40,
379
+ });
380
+ vi.mocked(mockSessionBridge.findSessionsByWorkspaceId).mockResolvedValue([]);
381
+ vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
382
+ status: 'started',
383
+ sessionId: 'raced-session',
384
+ promptSent: true,
385
+ });
386
+ vi.mocked(workspaceRatchetService.recordDispatchIfEnabled).mockResolvedValue(false);
387
+ vi.mocked(workspaceRatchetService.transitionStateIfEnabled).mockResolvedValue(false);
388
+ vi.mocked(workspaceDataService.findById).mockResolvedValue({
389
+ id: 'ws-disable-race-dispatch',
390
+ ratchetEnabled: false,
391
+ });
392
+ vi.mocked(mockSessionBridge.isSessionRunning).mockReturnValue(true);
393
+ const events = [];
394
+ ratchetService.on(RATCHET_DISPATCH_CHANGED, (event) => {
395
+ events.push(event);
396
+ });
397
+ const result = await unsafeCoerce(ratchetService).processWorkspace(workspace);
398
+ expect(result).toMatchObject({
399
+ newState: RatchetState.IDLE,
400
+ action: { type: 'DISABLED', reason: 'Workspace ratcheting disabled' },
401
+ });
402
+ expect(workspaceRatchetService.recordDispatchIfEnabled).toHaveBeenCalledWith('ws-disable-race-dispatch', { sessionId: 'raced-session', snapshotKey: 'new-snapshot', retryCount: 0 });
403
+ expect(mockSessionBridge.stopSession).toHaveBeenCalledWith('raced-session');
404
+ expect(mockSnapshotBridge.recordReviewCheck).not.toHaveBeenCalled();
405
+ expect(events).toHaveLength(0);
406
+ });
241
407
  it('dispatches when session is running but idle', async () => {
242
408
  const workspace = {
243
409
  id: 'ws-idle-session',
@@ -250,6 +416,8 @@ describe('ratchet service (state-change + idle dispatch)', () => {
250
416
  ratchetActiveSessionId: null,
251
417
  ratchetLastCiRunId: '2026-01-01T00:00:00Z',
252
418
  prReviewLastCheckedAt: new Date('2026-01-01T00:00:00Z'),
419
+ ratchetDispatchOutcome: null,
420
+ ratchetDispatchRetryCount: 0,
253
421
  };
254
422
  vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({
255
423
  ciStatus: CIStatus.FAILURE,
@@ -260,8 +428,7 @@ describe('ratchet service (state-change + idle dispatch)', () => {
260
428
  prState: 'OPEN',
261
429
  prNumber: 44,
262
430
  });
263
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
264
- vi.mocked(agentSessionAccessor.findByWorkspaceId).mockResolvedValue([
431
+ vi.mocked(mockSessionBridge.findSessionsByWorkspaceId).mockResolvedValue([
265
432
  {
266
433
  id: 'chat-idle-1',
267
434
  workflow: 'default-followup',
@@ -294,6 +461,8 @@ describe('ratchet service (state-change + idle dispatch)', () => {
294
461
  ratchetActiveSessionId: null,
295
462
  ratchetLastCiRunId: '2026-01-02T00:00:00Z',
296
463
  prReviewLastCheckedAt: new Date('2026-01-02T00:00:00Z'),
464
+ ratchetDispatchOutcome: null,
465
+ ratchetDispatchRetryCount: 0,
297
466
  };
298
467
  vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({
299
468
  ciStatus: CIStatus.FAILURE,
@@ -304,8 +473,7 @@ describe('ratchet service (state-change + idle dispatch)', () => {
304
473
  prState: 'OPEN',
305
474
  prNumber: 5,
306
475
  });
307
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
308
- vi.mocked(agentSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
476
+ vi.mocked(mockSessionBridge.findSessionsByWorkspaceId).mockResolvedValue([]);
309
477
  const result = await unsafeCoerce(ratchetService).processWorkspace(workspace);
310
478
  expect(result).toMatchObject({
311
479
  action: {
@@ -313,7 +481,7 @@ describe('ratchet service (state-change + idle dispatch)', () => {
313
481
  reason: 'PR state unchanged since last ratchet dispatch',
314
482
  },
315
483
  });
316
- expect(agentSessionAccessor.findByWorkspaceId).not.toHaveBeenCalled();
484
+ expect(mockSessionBridge.findSessionsByWorkspaceId).not.toHaveBeenCalled();
317
485
  expect(mockSnapshotBridge.recordReviewCheck).not.toHaveBeenCalled();
318
486
  });
319
487
  it('does not dispatch repeatedly for unchanged CHANGES_REQUESTED state', async () => {
@@ -328,6 +496,8 @@ describe('ratchet service (state-change + idle dispatch)', () => {
328
496
  ratchetActiveSessionId: null,
329
497
  ratchetLastCiRunId: 'ci:SUCCESS|changes-requested:1735776000000',
330
498
  prReviewLastCheckedAt: new Date('2026-01-02T00:00:00Z'),
499
+ ratchetDispatchOutcome: null,
500
+ ratchetDispatchRetryCount: 0,
331
501
  };
332
502
  vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({
333
503
  ciStatus: CIStatus.SUCCESS,
@@ -347,8 +517,7 @@ describe('ratchet service (state-change + idle dispatch)', () => {
347
517
  },
348
518
  ],
349
519
  });
350
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
351
- vi.mocked(agentSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
520
+ vi.mocked(mockSessionBridge.findSessionsByWorkspaceId).mockResolvedValue([]);
352
521
  const triggerSpy = vi.spyOn(unsafeCoerce(ratchetService), 'triggerFixer');
353
522
  const result = await unsafeCoerce(ratchetService).processWorkspace(workspace);
354
523
  expect(triggerSpy).not.toHaveBeenCalled();
@@ -358,9 +527,9 @@ describe('ratchet service (state-change + idle dispatch)', () => {
358
527
  reason: 'PR state unchanged since last ratchet dispatch',
359
528
  },
360
529
  });
361
- expect(agentSessionAccessor.findByWorkspaceId).not.toHaveBeenCalled();
530
+ expect(mockSessionBridge.findSessionsByWorkspaceId).not.toHaveBeenCalled();
362
531
  });
363
- it('does not dispatch for CHANGES_REQUESTED when there are no PR review comments', async () => {
532
+ it('dispatches for CHANGES_REQUESTED when there are no PR review comments', async () => {
364
533
  const workspace = {
365
534
  id: 'ws-review-no-comments',
366
535
  prUrl: 'https://github.com/example/repo/pull/56',
@@ -372,6 +541,8 @@ describe('ratchet service (state-change + idle dispatch)', () => {
372
541
  ratchetActiveSessionId: null,
373
542
  ratchetLastCiRunId: 'ci:SUCCESS|changes-requested:old',
374
543
  prReviewLastCheckedAt: new Date('2026-01-02T00:00:00Z'),
544
+ ratchetDispatchOutcome: null,
545
+ ratchetDispatchRetryCount: 0,
375
546
  };
376
547
  vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({
377
548
  ciStatus: CIStatus.SUCCESS,
@@ -383,19 +554,20 @@ describe('ratchet service (state-change + idle dispatch)', () => {
383
554
  prNumber: 56,
384
555
  reviewComments: [],
385
556
  });
386
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
387
- vi.mocked(agentSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
557
+ vi.mocked(mockSessionBridge.findSessionsByWorkspaceId).mockResolvedValue([]);
558
+ vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
559
+ status: 'started',
560
+ sessionId: 'ratchet-session',
561
+ promptSent: true,
562
+ });
388
563
  const triggerSpy = vi.spyOn(unsafeCoerce(ratchetService), 'triggerFixer');
389
564
  const result = await unsafeCoerce(ratchetService).processWorkspace(workspace);
390
- expect(triggerSpy).not.toHaveBeenCalled();
565
+ expect(triggerSpy).toHaveBeenCalled();
391
566
  expect(result).toMatchObject({
392
- action: {
393
- type: 'WAITING',
394
- reason: 'No CI failures or PR review comments to address',
395
- },
567
+ action: { type: 'TRIGGERED_FIXER' },
396
568
  });
397
569
  });
398
- it('dispatches for PR review summary feedback without changes requested', async () => {
570
+ it('dispatches for PR review summary feedback in all-feedback mode', async () => {
399
571
  const workspace = {
400
572
  id: 'ws-review-summary',
401
573
  prUrl: 'https://github.com/example/repo/pull/57',
@@ -407,8 +579,17 @@ describe('ratchet service (state-change + idle dispatch)', () => {
407
579
  ratchetActiveSessionId: null,
408
580
  ratchetLastCiRunId: 'ci:SUCCESS|no-changes-requested:old|merge:clean',
409
581
  prReviewLastCheckedAt: new Date('2026-01-02T00:00:00Z'),
582
+ ratchetDispatchOutcome: null,
583
+ ratchetDispatchRetryCount: 0,
410
584
  };
411
- vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({
585
+ vi.mocked(userSettingsService.get).mockResolvedValue({
586
+ ratchetReviewTriggerMode: 'ALL_REVIEW_FEEDBACK',
587
+ ratchetReplyToPrComments: true,
588
+ ratchetPermissions: 'YOLO',
589
+ });
590
+ const fetchPRStateSpy = vi
591
+ .spyOn(unsafeCoerce(ratchetService), 'fetchPRState')
592
+ .mockResolvedValue({
412
593
  ciStatus: CIStatus.SUCCESS,
413
594
  snapshotKey: 'ci:SUCCESS|no-changes-requested:1767315600000|merge:clean',
414
595
  hasChangesRequested: false,
@@ -427,8 +608,7 @@ describe('ratchet service (state-change + idle dispatch)', () => {
427
608
  },
428
609
  ],
429
610
  });
430
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
431
- vi.mocked(agentSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
611
+ vi.mocked(mockSessionBridge.findSessionsByWorkspaceId).mockResolvedValue([]);
432
612
  vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
433
613
  status: 'started',
434
614
  sessionId: 'ratchet-session',
@@ -440,9 +620,10 @@ describe('ratchet service (state-change + idle dispatch)', () => {
440
620
  newState: RatchetState.READY,
441
621
  });
442
622
  expect(fixerSessionService.acquireAndDispatch).toHaveBeenCalled();
623
+ expect(fetchPRStateSpy).toHaveBeenCalledWith(workspace, null, expect.objectContaining({ reviewTriggerMode: 'ALL_REVIEW_FEEDBACK' }), expect.any(AbortSignal));
443
624
  });
444
625
  it('treats closed PR as IDLE and does not dispatch', () => {
445
- const state = unsafeCoerce(ratchetService).determineRatchetState({
626
+ const state = determineRatchetState({
446
627
  ciStatus: CIStatus.SUCCESS,
447
628
  snapshotKey: '2026-01-02T00:00:00Z',
448
629
  hasChangesRequested: false,
@@ -464,6 +645,8 @@ describe('ratchet service (state-change + idle dispatch)', () => {
464
645
  ratchetActiveSessionId: null,
465
646
  ratchetLastCiRunId: null,
466
647
  prReviewLastCheckedAt: null,
648
+ ratchetDispatchOutcome: null,
649
+ ratchetDispatchRetryCount: 0,
467
650
  };
468
651
  vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({
469
652
  ciStatus: CIStatus.SUCCESS,
@@ -483,8 +666,7 @@ describe('ratchet service (state-change + idle dispatch)', () => {
483
666
  },
484
667
  ],
485
668
  });
486
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
487
- vi.mocked(agentSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
669
+ vi.mocked(mockSessionBridge.findSessionsByWorkspaceId).mockResolvedValue([]);
488
670
  vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
489
671
  status: 'started',
490
672
  sessionId: 'ratchet-session',
@@ -496,11 +678,56 @@ describe('ratchet service (state-change + idle dispatch)', () => {
496
678
  expect(result).toMatchObject({
497
679
  action: { type: 'ERROR', error: 'Failed to deliver initial ratchet prompt' },
498
680
  });
499
- expect(workspaceAccessor.update).toHaveBeenCalledWith(workspace.id, {
500
- ratchetActiveSessionId: null,
501
- });
681
+ expect(workspaceRatchetService.clearActiveSession).toHaveBeenCalledWith(workspace.id);
502
682
  expect(mockSessionBridge.stopSession).toHaveBeenCalledWith('ratchet-session');
503
683
  });
684
+ it('routes an aborted prompt-failure path through orphan cleanup', async () => {
685
+ const controller = new AbortController();
686
+ const timeoutError = new Error('Workspace check timed out');
687
+ let finishPointerClear;
688
+ vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
689
+ status: 'started',
690
+ sessionId: 'aborted-prompt-session',
691
+ promptSent: false,
692
+ });
693
+ vi.mocked(workspaceRatchetService.clearActiveSession).mockImplementation(() => new Promise((resolve) => {
694
+ finishPointerClear = () => resolve({});
695
+ }));
696
+ vi.mocked(mockSessionBridge.isSessionRunning).mockReturnValue(true);
697
+ const trigger = unsafeCoerce(ratchetService).triggerFixer({
698
+ id: 'ws-aborted-prompt',
699
+ prUrl: 'https://github.com/example/repo/pull/7',
700
+ }, {
701
+ ciStatus: CIStatus.FAILURE,
702
+ snapshotKey: 'failed:7',
703
+ prNumber: 7,
704
+ reviewComments: [],
705
+ hasMergeConflict: false,
706
+ }, 0, controller.signal);
707
+ await vi.waitFor(() => expect(workspaceRatchetService.clearActiveSession).toHaveBeenCalled());
708
+ controller.abort(timeoutError);
709
+ finishPointerClear();
710
+ await expect(trigger).rejects.toBe(timeoutError);
711
+ expect(workspaceRatchetService.recordSessionEnd).toHaveBeenCalledWith('ws-aborted-prompt', 'aborted-prompt-session', 'COMPLETED');
712
+ expect(mockSessionBridge.stopSession).toHaveBeenCalledWith('aborted-prompt-session');
713
+ });
714
+ it('publishes a dispatch invalidation after lifecycle settlement', async () => {
715
+ const events = [];
716
+ ratchetService.on(RATCHET_DISPATCH_CHANGED, (event) => {
717
+ events.push(event);
718
+ });
719
+ await ratchetService.recordSessionEnd('ws-1', 'session-1', 'DIED');
720
+ expect(events).toContainEqual({ workspaceId: 'ws-1' });
721
+ });
722
+ it('does not publish a dispatch change when lifecycle settlement loses its CAS', async () => {
723
+ const events = [];
724
+ ratchetService.on(RATCHET_DISPATCH_CHANGED, (event) => {
725
+ events.push(event);
726
+ });
727
+ vi.mocked(mockWorkspaceBridge.recordSessionEnd).mockResolvedValue(false);
728
+ await ratchetService.recordSessionEnd('ws-1', 'session-1', 'DIED');
729
+ expect(events).toHaveLength(0);
730
+ });
504
731
  it('does not dispatch on a clean PR with no new review activity', async () => {
505
732
  const recentCheck = new Date();
506
733
  const workspace = {
@@ -514,6 +741,8 @@ describe('ratchet service (state-change + idle dispatch)', () => {
514
741
  ratchetActiveSessionId: null,
515
742
  ratchetLastCiRunId: '2026-01-01T00:00:00Z',
516
743
  prReviewLastCheckedAt: recentCheck,
744
+ ratchetDispatchOutcome: null,
745
+ ratchetDispatchRetryCount: 0,
517
746
  };
518
747
  vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({
519
748
  ciStatus: CIStatus.SUCCESS,
@@ -524,8 +753,7 @@ describe('ratchet service (state-change + idle dispatch)', () => {
524
753
  prState: 'OPEN',
525
754
  prNumber: 8,
526
755
  });
527
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
528
- vi.mocked(agentSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
756
+ vi.mocked(mockSessionBridge.findSessionsByWorkspaceId).mockResolvedValue([]);
529
757
  const triggerSpy = vi.spyOn(unsafeCoerce(ratchetService), 'triggerFixer');
530
758
  const result = await unsafeCoerce(ratchetService).processWorkspace(workspace);
531
759
  expect(triggerSpy).not.toHaveBeenCalled();
@@ -536,198 +764,557 @@ describe('ratchet service (state-change + idle dispatch)', () => {
536
764
  },
537
765
  });
538
766
  });
539
- it('self-heals when review check timestamp is stale with no active session', async () => {
540
- const staleCheckedAt = new Date(Date.now() - 15 * 60_000);
767
+ it('dispatches stale actionable review comments when the PR snapshot changes', async () => {
768
+ const recentCheck = new Date();
541
769
  const workspace = {
542
- id: 'ws-stale-check',
543
- prUrl: 'https://github.com/example/repo/pull/13',
544
- prNumber: 13,
770
+ id: 'ws-stale-actionable-review',
771
+ prUrl: 'https://github.com/example/repo/pull/43',
772
+ prNumber: 43,
545
773
  prState: 'OPEN',
546
- prCiStatus: CIStatus.UNKNOWN,
774
+ prCiStatus: CIStatus.FAILURE,
547
775
  ratchetEnabled: true,
548
- ratchetState: RatchetState.READY,
776
+ ratchetState: RatchetState.CI_FAILED,
549
777
  ratchetActiveSessionId: null,
550
- ratchetLastCiRunId: '2026-01-01T00:00:00Z',
551
- prReviewLastCheckedAt: staleCheckedAt,
778
+ ratchetLastCiRunId: 'failed-snapshot',
779
+ prReviewLastCheckedAt: recentCheck,
780
+ ratchetDispatchOutcome: 'COMPLETED',
781
+ ratchetDispatchRetryCount: 0,
552
782
  };
553
783
  vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({
554
784
  ciStatus: CIStatus.SUCCESS,
555
- snapshotKey: '2026-01-03T00:00:00Z',
785
+ snapshotKey: 'successful-snapshot',
556
786
  hasChangesRequested: false,
557
- latestReviewActivityAtMs: staleCheckedAt.getTime() - 1000,
787
+ hasMergeConflict: false,
788
+ latestReviewActivityAtMs: recentCheck.getTime() - 1000,
558
789
  statusCheckRollup: null,
559
790
  prState: 'OPEN',
560
- prNumber: 13,
791
+ prNumber: 43,
792
+ reviewComments: [
793
+ {
794
+ author: 'reviewer',
795
+ body: 'Please handle this edge case.',
796
+ path: 'src/example.ts',
797
+ line: 10,
798
+ url: 'https://github.com/example/repo/pull/43#discussion_r1',
799
+ },
800
+ ],
801
+ });
802
+ vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
803
+ status: 'started',
804
+ sessionId: 'ratchet-review-session',
805
+ promptSent: true,
561
806
  });
562
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
563
- vi.mocked(agentSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
564
807
  const result = await unsafeCoerce(ratchetService).processWorkspace(workspace);
565
- expect(result).not.toMatchObject({
566
- action: {
567
- type: 'WAITING',
568
- reason: 'PR is clean (green CI and no new review activity)',
569
- },
808
+ expect(result).toMatchObject({
809
+ action: { type: 'TRIGGERED_FIXER', sessionId: 'ratchet-review-session' },
810
+ });
811
+ expect(workspaceRatchetService.recordDispatchIfEnabled).toHaveBeenCalledWith('ws-stale-actionable-review', {
812
+ sessionId: 'ratchet-review-session',
813
+ snapshotKey: 'successful-snapshot',
814
+ retryCount: 0,
570
815
  });
571
816
  });
572
- it('resets dispatch tracking when ratchet session process is not running', async () => {
817
+ it('returns disabled when stale check-state update loses disable race', async () => {
818
+ const recentCheck = new Date();
573
819
  const workspace = {
574
- id: 'ws-stale-active',
575
- prUrl: 'https://github.com/example/repo/pull/9',
576
- prNumber: 9,
820
+ id: 'ws-disable-race-state',
821
+ prUrl: 'https://github.com/example/repo/pull/41',
822
+ prNumber: 41,
577
823
  prState: 'OPEN',
578
824
  prCiStatus: CIStatus.UNKNOWN,
579
825
  ratchetEnabled: true,
580
- ratchetState: RatchetState.CI_FAILED,
581
- ratchetActiveSessionId: 'ratchet-session',
582
- ratchetLastCiRunId: 'previous-snapshot-key',
583
- prReviewLastCheckedAt: new Date('2026-01-01T00:00:00Z'),
826
+ ratchetState: RatchetState.READY,
827
+ ratchetActiveSessionId: null,
828
+ ratchetLastCiRunId: 'old-snapshot',
829
+ prReviewLastCheckedAt: recentCheck,
830
+ ratchetDispatchOutcome: null,
831
+ ratchetDispatchRetryCount: 0,
584
832
  };
585
- vi.mocked(agentSessionAccessor.findById).mockResolvedValue({
586
- id: 'ratchet-session',
587
- provider: 'CLAUDE',
588
- status: SessionStatus.RUNNING,
833
+ vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({
834
+ ciStatus: CIStatus.SUCCESS,
835
+ snapshotKey: 'new-snapshot',
836
+ hasChangesRequested: false,
837
+ latestReviewActivityAtMs: recentCheck.getTime() - 1000,
838
+ statusCheckRollup: null,
839
+ prState: 'OPEN',
840
+ prNumber: 41,
589
841
  });
590
- vi.mocked(mockSessionBridge.isSessionRunning).mockReturnValue(false);
591
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
592
- vi.mocked(mockSnapshotBridge.recordReviewCheck).mockResolvedValue();
593
- const action = await unsafeCoerce(ratchetService).getActiveRatchetSession(workspace);
594
- expect(action).toBeNull();
595
- expect(workspaceAccessor.update).toHaveBeenCalledWith(workspace.id, {
596
- ratchetActiveSessionId: null,
597
- ratchetLastCiRunId: null,
842
+ vi.mocked(workspaceRatchetService.transitionStateIfEnabled).mockResolvedValue(false);
843
+ vi.mocked(workspaceDataService.findById).mockResolvedValue({
844
+ id: 'ws-disable-race-state',
845
+ ratchetEnabled: false,
846
+ });
847
+ vi.mocked(mockSessionBridge.findSessionsByWorkspaceId).mockResolvedValue([]);
848
+ const events = [];
849
+ ratchetService.on(RATCHET_STATE_CHANGED, (event) => {
850
+ events.push(event);
851
+ });
852
+ const result = await unsafeCoerce(ratchetService).processWorkspace(workspace);
853
+ expect(result).toMatchObject({
854
+ newState: RatchetState.IDLE,
855
+ action: { type: 'DISABLED', reason: 'Workspace ratcheting disabled' },
598
856
  });
599
- expect(mockSnapshotBridge.recordReviewCheck).toHaveBeenCalledWith(workspace.id, null);
857
+ expect(events).toHaveLength(0);
858
+ ratchetService.removeAllListeners();
600
859
  });
601
- it('resets dispatch tracking when ratchet session is not found in database', async () => {
860
+ it('reports a concurrent state change without emitting when the check loses the CAS while still enabled', async () => {
861
+ const recentCheck = new Date();
602
862
  const workspace = {
603
- id: 'ws-missing-session',
604
- prUrl: 'https://github.com/example/repo/pull/12',
605
- prNumber: 12,
863
+ id: 'ws-superseded',
864
+ prUrl: 'https://github.com/example/repo/pull/42',
865
+ prNumber: 42,
606
866
  prState: 'OPEN',
607
867
  prCiStatus: CIStatus.UNKNOWN,
608
868
  ratchetEnabled: true,
609
- ratchetState: RatchetState.CI_FAILED,
610
- ratchetActiveSessionId: 'deleted-session',
611
- ratchetLastCiRunId: 'previous-snapshot-key',
612
- prReviewLastCheckedAt: new Date('2026-01-01T00:00:00Z'),
613
- };
614
- vi.mocked(agentSessionAccessor.findById).mockResolvedValue(null);
615
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
616
- vi.mocked(mockSnapshotBridge.recordReviewCheck).mockResolvedValue();
617
- const action = await unsafeCoerce(ratchetService).getActiveRatchetSession(workspace);
618
- expect(action).toBeNull();
619
- expect(workspaceAccessor.update).toHaveBeenCalledWith(workspace.id, {
869
+ ratchetState: RatchetState.CI_RUNNING,
620
870
  ratchetActiveSessionId: null,
621
- ratchetLastCiRunId: null,
871
+ ratchetLastCiRunId: 'old-snapshot',
872
+ prReviewLastCheckedAt: recentCheck,
873
+ ratchetDispatchOutcome: null,
874
+ ratchetDispatchRetryCount: 0,
875
+ };
876
+ vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({
877
+ ciStatus: CIStatus.SUCCESS,
878
+ snapshotKey: 'new-snapshot',
879
+ hasChangesRequested: false,
880
+ latestReviewActivityAtMs: recentCheck.getTime() - 1000,
881
+ statusCheckRollup: null,
882
+ prState: 'OPEN',
883
+ prNumber: 42,
884
+ });
885
+ vi.mocked(workspaceRatchetService.transitionStateIfEnabled).mockResolvedValue(false);
886
+ vi.mocked(workspaceDataService.findById).mockResolvedValue({
887
+ id: 'ws-superseded',
888
+ ratchetEnabled: true,
889
+ ratchetState: RatchetState.IDLE,
622
890
  });
623
- expect(mockSnapshotBridge.recordReviewCheck).toHaveBeenCalledWith(workspace.id, null);
891
+ vi.mocked(mockSessionBridge.findSessionsByWorkspaceId).mockResolvedValue([]);
892
+ const events = [];
893
+ ratchetService.on(RATCHET_STATE_CHANGED, (event) => {
894
+ events.push(event);
895
+ });
896
+ const result = await unsafeCoerce(ratchetService).processWorkspace(workspace);
897
+ expect(workspaceRatchetService.transitionStateIfEnabled).toHaveBeenCalledWith('ws-superseded', RatchetState.CI_RUNNING, expect.objectContaining({ ratchetState: RatchetState.READY }));
898
+ expect(result).toMatchObject({
899
+ previousState: RatchetState.CI_RUNNING,
900
+ newState: RatchetState.CI_RUNNING,
901
+ action: {
902
+ type: 'WAITING',
903
+ reason: 'Ratchet state changed concurrently during this check; re-evaluating next cycle',
904
+ },
905
+ });
906
+ expect(events).toHaveLength(0);
907
+ expect(mockSnapshotBridge.recordCIObservation).not.toHaveBeenCalled();
908
+ expect(mockSnapshotBridge.recordReviewCheck).not.toHaveBeenCalled();
624
909
  });
625
- it('does not reset dispatch tracking when ratchet session completed normally', async () => {
910
+ it('redispatches with an incremented retry count when the previous fixer died', async () => {
626
911
  const workspace = {
627
- id: 'ws-idle-active',
628
- prUrl: 'https://github.com/example/repo/pull/11',
629
- prNumber: 11,
912
+ id: 'ws-died-retry',
913
+ prUrl: 'https://github.com/example/repo/pull/13',
914
+ prNumber: 13,
630
915
  prState: 'OPEN',
631
- prCiStatus: CIStatus.UNKNOWN,
916
+ prCiStatus: CIStatus.FAILURE,
632
917
  ratchetEnabled: true,
633
918
  ratchetState: RatchetState.CI_FAILED,
634
- ratchetActiveSessionId: 'ratchet-session',
635
- ratchetLastCiRunId: 'snapshot-key',
919
+ ratchetActiveSessionId: null,
920
+ ratchetLastCiRunId: 'same-snapshot',
636
921
  prReviewLastCheckedAt: new Date('2026-01-01T00:00:00Z'),
922
+ ratchetDispatchOutcome: 'DIED',
923
+ ratchetDispatchRetryCount: 1,
637
924
  };
638
- vi.mocked(agentSessionAccessor.findById).mockResolvedValue({
639
- id: 'ratchet-session',
640
- provider: 'CLAUDE',
641
- status: SessionStatus.RUNNING,
925
+ vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({
926
+ ciStatus: CIStatus.FAILURE,
927
+ snapshotKey: 'same-snapshot',
928
+ hasChangesRequested: false,
929
+ latestReviewActivityAtMs: null,
930
+ statusCheckRollup: null,
931
+ prState: 'OPEN',
932
+ prNumber: 13,
642
933
  });
643
- vi.mocked(mockSessionBridge.isSessionRunning).mockReturnValue(true);
644
- vi.mocked(mockSessionBridge.isSessionWorking).mockReturnValue(false);
645
- vi.mocked(mockSessionBridge.stopSession).mockResolvedValue();
646
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
647
- const action = await unsafeCoerce(ratchetService).getActiveRatchetSession(workspace);
648
- expect(action).toBeNull();
649
- expect(workspaceAccessor.update).toHaveBeenCalledWith(workspace.id, {
650
- ratchetActiveSessionId: null,
934
+ vi.mocked(mockSessionBridge.findSessionsByWorkspaceId).mockResolvedValue([]);
935
+ vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
936
+ status: 'started',
937
+ sessionId: 'retry-session',
938
+ promptSent: true,
939
+ });
940
+ const result = await unsafeCoerce(ratchetService).processWorkspace(workspace);
941
+ expect(result).toMatchObject({
942
+ action: { type: 'TRIGGERED_FIXER' },
943
+ });
944
+ expect(workspaceRatchetService.recordDispatchIfEnabled).toHaveBeenCalledWith('ws-died-retry', {
945
+ sessionId: 'retry-session',
946
+ snapshotKey: 'same-snapshot',
947
+ retryCount: 2,
651
948
  });
652
- expect(mockSessionBridge.stopSession).toHaveBeenCalledWith('ratchet-session');
653
- expect(mockSnapshotBridge.recordReviewCheck).not.toHaveBeenCalled();
654
949
  });
655
- it('cleans up completed ratchet sessions before skipping clean PRs', async () => {
950
+ it('redispatches a died fixer even when the PR is otherwise clean', async () => {
951
+ const checkedAt = new Date('2026-01-02T00:00:00Z');
656
952
  const workspace = {
657
- id: 'ws-clean-active',
658
- prUrl: 'https://github.com/example/repo/pull/13',
659
- prNumber: 13,
953
+ id: 'ws-died-clean',
954
+ prUrl: 'https://github.com/example/repo/pull/14',
955
+ prNumber: 14,
660
956
  prState: 'OPEN',
661
957
  prCiStatus: CIStatus.SUCCESS,
662
958
  ratchetEnabled: true,
663
959
  ratchetState: RatchetState.READY,
664
- ratchetActiveSessionId: 'ratchet-session',
960
+ ratchetActiveSessionId: null,
665
961
  ratchetLastCiRunId: 'clean-snapshot',
666
- prReviewLastCheckedAt: new Date('2026-01-01T00:00:00Z'),
962
+ prReviewLastCheckedAt: checkedAt,
963
+ ratchetDispatchOutcome: 'DIED',
964
+ ratchetDispatchRetryCount: 0,
667
965
  };
668
- const prStateInfo = {
966
+ vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({
669
967
  ciStatus: CIStatus.SUCCESS,
670
968
  snapshotKey: 'clean-snapshot',
671
969
  hasChangesRequested: false,
672
- latestReviewActivityAtMs: null,
970
+ latestReviewActivityAtMs: checkedAt.getTime() - 1000,
673
971
  statusCheckRollup: null,
674
972
  prState: 'OPEN',
675
- prNumber: 13,
973
+ prNumber: 14,
676
974
  reviewComments: [],
677
- };
678
- vi.mocked(agentSessionAccessor.findById).mockResolvedValue({
679
- id: 'ratchet-session',
680
- provider: 'CLAUDE',
681
- status: SessionStatus.RUNNING,
682
975
  });
683
- vi.mocked(mockSessionBridge.isSessionRunning).mockReturnValue(true);
684
- vi.mocked(mockSessionBridge.isSessionWorking).mockReturnValue(false);
685
- vi.mocked(mockSessionBridge.stopSession).mockResolvedValue();
686
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
687
- const result = await unsafeCoerce(ratchetService).collectRatchetingActivityChecks(workspace, prStateInfo, true, false);
688
- expect(result).toEqual({
689
- activeRatchetSession: null,
690
- hasOtherActiveSession: false,
976
+ vi.mocked(mockSessionBridge.findSessionsByWorkspaceId).mockResolvedValue([]);
977
+ vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
978
+ status: 'started',
979
+ sessionId: 'retry-session',
980
+ promptSent: true,
691
981
  });
692
- expect(workspaceAccessor.update).toHaveBeenCalledWith(workspace.id, {
693
- ratchetActiveSessionId: null,
982
+ const events = [];
983
+ ratchetService.on(RATCHET_DISPATCH_CHANGED, (event) => {
984
+ events.push(event);
694
985
  });
695
- expect(mockSessionBridge.stopSession).toHaveBeenCalledWith('ratchet-session');
696
- expect(agentSessionAccessor.findByWorkspaceId).not.toHaveBeenCalled();
697
- });
698
- it('decides to trigger fixer when context is actionable', () => {
699
- const decision = unsafeCoerce(ratchetService).decideRatchetAction({
700
- workspace: { ratchetEnabled: true },
701
- prStateInfo: { prState: 'OPEN', ciStatus: CIStatus.FAILURE },
986
+ const result = await unsafeCoerce(ratchetService).processWorkspace(workspace);
987
+ expect(result).toMatchObject({
988
+ action: { type: 'TRIGGERED_FIXER' },
989
+ });
990
+ expect(workspaceRatchetService.recordDispatchIfEnabled).toHaveBeenCalledWith('ws-died-clean', {
991
+ sessionId: 'retry-session',
992
+ snapshotKey: 'clean-snapshot',
993
+ retryCount: 1,
994
+ });
995
+ expect(events).toContainEqual({ workspaceId: 'ws-died-clean' });
996
+ });
997
+ it('stops retrying a died fixer once the retry budget is exhausted', async () => {
998
+ const workspace = {
999
+ id: 'ws-died-exhausted',
1000
+ prUrl: 'https://github.com/example/repo/pull/15',
1001
+ prNumber: 15,
1002
+ prState: 'OPEN',
1003
+ prCiStatus: CIStatus.FAILURE,
1004
+ ratchetEnabled: true,
1005
+ ratchetState: RatchetState.CI_FAILED,
1006
+ ratchetActiveSessionId: null,
1007
+ ratchetLastCiRunId: 'same-snapshot',
1008
+ prReviewLastCheckedAt: new Date('2026-01-01T00:00:00Z'),
1009
+ ratchetDispatchOutcome: 'DIED',
1010
+ ratchetDispatchRetryCount: 3,
1011
+ };
1012
+ vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({
1013
+ ciStatus: CIStatus.FAILURE,
1014
+ snapshotKey: 'same-snapshot',
1015
+ hasChangesRequested: false,
1016
+ latestReviewActivityAtMs: null,
1017
+ statusCheckRollup: null,
1018
+ prState: 'OPEN',
1019
+ prNumber: 15,
1020
+ });
1021
+ const result = await unsafeCoerce(ratchetService).processWorkspace(workspace);
1022
+ expect(result).toMatchObject({
1023
+ action: {
1024
+ type: 'WAITING',
1025
+ reason: 'Fixer died 4 times for this PR state; waiting for PR state to change',
1026
+ },
1027
+ });
1028
+ expect(fixerSessionService.acquireAndDispatch).not.toHaveBeenCalled();
1029
+ });
1030
+ it('resets the retry count when the PR state changes after a death', async () => {
1031
+ const workspace = {
1032
+ id: 'ws-died-changed',
1033
+ prUrl: 'https://github.com/example/repo/pull/16',
1034
+ prNumber: 16,
1035
+ prState: 'OPEN',
1036
+ prCiStatus: CIStatus.FAILURE,
1037
+ ratchetEnabled: true,
1038
+ ratchetState: RatchetState.CI_FAILED,
1039
+ ratchetActiveSessionId: null,
1040
+ ratchetLastCiRunId: 'old-snapshot',
1041
+ prReviewLastCheckedAt: new Date('2026-01-01T00:00:00Z'),
1042
+ ratchetDispatchOutcome: 'DIED',
1043
+ ratchetDispatchRetryCount: 3,
1044
+ };
1045
+ vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({
1046
+ ciStatus: CIStatus.FAILURE,
1047
+ snapshotKey: 'new-snapshot',
1048
+ hasChangesRequested: false,
1049
+ latestReviewActivityAtMs: null,
1050
+ statusCheckRollup: null,
1051
+ prState: 'OPEN',
1052
+ prNumber: 16,
1053
+ });
1054
+ vi.mocked(mockSessionBridge.findSessionsByWorkspaceId).mockResolvedValue([]);
1055
+ vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
1056
+ status: 'started',
1057
+ sessionId: 'fresh-session',
1058
+ promptSent: true,
1059
+ });
1060
+ const result = await unsafeCoerce(ratchetService).processWorkspace(workspace);
1061
+ expect(result).toMatchObject({
1062
+ action: { type: 'TRIGGERED_FIXER' },
1063
+ });
1064
+ expect(workspaceRatchetService.recordDispatchIfEnabled).toHaveBeenCalledWith('ws-died-changed', { sessionId: 'fresh-session', snapshotKey: 'new-snapshot', retryCount: 0 });
1065
+ });
1066
+ it('does not redispatch when the previous fixer completed and PR state is unchanged', async () => {
1067
+ const workspace = {
1068
+ id: 'ws-completed-unchanged',
1069
+ prUrl: 'https://github.com/example/repo/pull/17',
1070
+ prNumber: 17,
1071
+ prState: 'OPEN',
1072
+ prCiStatus: CIStatus.FAILURE,
1073
+ ratchetEnabled: true,
1074
+ ratchetState: RatchetState.CI_FAILED,
1075
+ ratchetActiveSessionId: null,
1076
+ ratchetLastCiRunId: 'same-snapshot',
1077
+ prReviewLastCheckedAt: new Date('2026-01-01T00:00:00Z'),
1078
+ ratchetDispatchOutcome: 'COMPLETED',
1079
+ ratchetDispatchRetryCount: 2,
1080
+ };
1081
+ vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({
1082
+ ciStatus: CIStatus.FAILURE,
1083
+ snapshotKey: 'same-snapshot',
1084
+ hasChangesRequested: false,
1085
+ latestReviewActivityAtMs: null,
1086
+ statusCheckRollup: null,
1087
+ prState: 'OPEN',
1088
+ prNumber: 17,
1089
+ });
1090
+ const result = await unsafeCoerce(ratchetService).processWorkspace(workspace);
1091
+ expect(result).toMatchObject({
1092
+ action: {
1093
+ type: 'WAITING',
1094
+ reason: 'PR state unchanged since last ratchet dispatch',
1095
+ },
1096
+ });
1097
+ expect(fixerSessionService.acquireAndDispatch).not.toHaveBeenCalled();
1098
+ });
1099
+ it('dispatches a fresh fixer after switching PRs with identical merge-conflict state', async () => {
1100
+ const previousSnapshotKey = computeDispatchSnapshotKey(123, CIStatus.SUCCESS, false, null, null, true);
1101
+ const currentSnapshotKey = computeDispatchSnapshotKey(456, CIStatus.SUCCESS, false, null, null, true);
1102
+ expect(previousSnapshotKey).toBe('pr:123|ci:SUCCESS|no-changes-requested:none|merge:conflict');
1103
+ expect(currentSnapshotKey).toBe('pr:456|ci:SUCCESS|no-changes-requested:none|merge:conflict');
1104
+ const workspace = {
1105
+ id: 'ws-pr-switch-merge-conflict',
1106
+ prUrl: 'https://github.com/example/repo/pull/456',
1107
+ prNumber: 456,
1108
+ prState: 'OPEN',
1109
+ prCiStatus: CIStatus.SUCCESS,
1110
+ ratchetEnabled: true,
1111
+ ratchetState: RatchetState.MERGE_CONFLICT,
1112
+ ratchetActiveSessionId: null,
1113
+ ratchetLastCiRunId: previousSnapshotKey,
1114
+ prReviewLastCheckedAt: new Date('2026-01-01T00:00:00Z'),
1115
+ ratchetDispatchOutcome: 'COMPLETED',
1116
+ ratchetDispatchRetryCount: 2,
1117
+ };
1118
+ vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({
1119
+ ciStatus: CIStatus.SUCCESS,
1120
+ snapshotKey: currentSnapshotKey,
1121
+ hasChangesRequested: false,
1122
+ hasMergeConflict: true,
1123
+ latestReviewActivityAtMs: null,
1124
+ statusCheckRollup: null,
1125
+ prState: 'OPEN',
1126
+ prNumber: 456,
1127
+ reviewComments: [],
1128
+ });
1129
+ vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
1130
+ status: 'started',
1131
+ sessionId: 'fresh-pr-session',
1132
+ promptSent: true,
1133
+ });
1134
+ const result = await unsafeCoerce(ratchetService).processWorkspace(workspace);
1135
+ expect(result).toMatchObject({
1136
+ action: { type: 'TRIGGERED_FIXER' },
1137
+ });
1138
+ expect(workspaceRatchetService.recordDispatchIfEnabled).toHaveBeenCalledWith(workspace.id, {
1139
+ sessionId: 'fresh-pr-session',
1140
+ snapshotKey: currentSnapshotKey,
1141
+ retryCount: 0,
1142
+ });
1143
+ });
1144
+ it('settles the dispatch as DIED when ratchet session process is not running', async () => {
1145
+ const workspace = {
1146
+ id: 'ws-stale-active',
1147
+ prUrl: 'https://github.com/example/repo/pull/9',
1148
+ prNumber: 9,
1149
+ prState: 'OPEN',
1150
+ prCiStatus: CIStatus.UNKNOWN,
1151
+ ratchetEnabled: true,
1152
+ ratchetState: RatchetState.CI_FAILED,
1153
+ ratchetActiveSessionId: 'ratchet-session',
1154
+ ratchetLastCiRunId: 'previous-snapshot-key',
1155
+ prReviewLastCheckedAt: new Date('2026-01-01T00:00:00Z'),
1156
+ ratchetDispatchOutcome: 'RUNNING',
1157
+ ratchetDispatchRetryCount: 0,
1158
+ };
1159
+ vi.mocked(mockSessionBridge.findSessionById).mockResolvedValue({
1160
+ id: 'ratchet-session',
1161
+ provider: 'CLAUDE',
1162
+ status: SessionStatus.RUNNING,
1163
+ });
1164
+ vi.mocked(mockSessionBridge.isSessionRunning).mockReturnValue(false);
1165
+ const result = await unsafeCoerce(ratchetService).checkActiveFixerSession(workspace);
1166
+ expect(result).toEqual({ kind: 'settled', outcome: 'DIED' });
1167
+ expect(mockWorkspaceBridge.recordSessionEnd).toHaveBeenCalledWith(workspace.id, 'ratchet-session', 'DIED');
1168
+ });
1169
+ it('settles the dispatch as DIED when ratchet session is not found in database', async () => {
1170
+ const workspace = {
1171
+ id: 'ws-missing-session',
1172
+ prUrl: 'https://github.com/example/repo/pull/12',
1173
+ prNumber: 12,
1174
+ prState: 'OPEN',
1175
+ prCiStatus: CIStatus.UNKNOWN,
1176
+ ratchetEnabled: true,
1177
+ ratchetState: RatchetState.CI_FAILED,
1178
+ ratchetActiveSessionId: 'deleted-session',
1179
+ ratchetLastCiRunId: 'previous-snapshot-key',
1180
+ prReviewLastCheckedAt: new Date('2026-01-01T00:00:00Z'),
1181
+ ratchetDispatchOutcome: 'RUNNING',
1182
+ ratchetDispatchRetryCount: 0,
1183
+ };
1184
+ vi.mocked(mockSessionBridge.findSessionById).mockResolvedValue(null);
1185
+ const events = [];
1186
+ ratchetService.on(RATCHET_DISPATCH_CHANGED, (event) => {
1187
+ events.push(event);
1188
+ });
1189
+ const result = await unsafeCoerce(ratchetService).checkActiveFixerSession(workspace);
1190
+ expect(result).toEqual({ kind: 'settled', outcome: 'DIED' });
1191
+ expect(mockWorkspaceBridge.recordSessionEnd).toHaveBeenCalledWith(workspace.id, 'deleted-session', 'DIED');
1192
+ expect(events).toEqual([{ workspaceId: 'ws-missing-session' }]);
1193
+ });
1194
+ it('reports a concurrent end when another path already settled the dispatch', async () => {
1195
+ const workspace = {
1196
+ id: 'ws-concurrent-end',
1197
+ prUrl: 'https://github.com/example/repo/pull/12',
1198
+ prNumber: 12,
1199
+ prState: 'OPEN',
1200
+ prCiStatus: CIStatus.UNKNOWN,
1201
+ ratchetEnabled: true,
1202
+ ratchetState: RatchetState.CI_FAILED,
1203
+ ratchetActiveSessionId: 'finished-session',
1204
+ ratchetLastCiRunId: 'previous-snapshot-key',
1205
+ prReviewLastCheckedAt: new Date('2026-01-01T00:00:00Z'),
1206
+ ratchetDispatchOutcome: 'RUNNING',
1207
+ ratchetDispatchRetryCount: 0,
1208
+ };
1209
+ vi.mocked(mockSessionBridge.findSessionById).mockResolvedValue(null);
1210
+ vi.mocked(mockWorkspaceBridge.recordSessionEnd).mockResolvedValue(false);
1211
+ const events = [];
1212
+ ratchetService.on(RATCHET_DISPATCH_CHANGED, (event) => {
1213
+ events.push(event);
1214
+ });
1215
+ const result = await unsafeCoerce(ratchetService).checkActiveFixerSession(workspace);
1216
+ expect(result).toEqual({ kind: 'ended_concurrently' });
1217
+ expect(events).toHaveLength(0);
1218
+ });
1219
+ it('waits for the next cycle when the fixer ends concurrently mid-check', async () => {
1220
+ const workspace = {
1221
+ id: 'ws-concurrent-wait',
1222
+ prUrl: 'https://github.com/example/repo/pull/12',
1223
+ prNumber: 12,
1224
+ prState: 'OPEN',
1225
+ prCiStatus: CIStatus.UNKNOWN,
1226
+ ratchetEnabled: true,
1227
+ ratchetState: RatchetState.CI_FAILED,
1228
+ ratchetActiveSessionId: 'finished-session',
1229
+ ratchetLastCiRunId: 'previous-snapshot-key',
1230
+ prReviewLastCheckedAt: new Date('2026-01-01T00:00:00Z'),
1231
+ ratchetDispatchOutcome: 'RUNNING',
1232
+ ratchetDispatchRetryCount: 0,
1233
+ };
1234
+ vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({
1235
+ ciStatus: CIStatus.FAILURE,
1236
+ snapshotKey: 'new-snapshot',
1237
+ hasChangesRequested: false,
1238
+ latestReviewActivityAtMs: null,
1239
+ statusCheckRollup: null,
1240
+ prState: 'OPEN',
1241
+ prNumber: 12,
1242
+ });
1243
+ vi.mocked(mockSessionBridge.findSessionById).mockResolvedValue(null);
1244
+ vi.mocked(mockWorkspaceBridge.recordSessionEnd).mockResolvedValue(false);
1245
+ const result = await unsafeCoerce(ratchetService).processWorkspace(workspace);
1246
+ expect(result).toMatchObject({
1247
+ action: {
1248
+ type: 'WAITING',
1249
+ reason: 'Fixer session ended during this check; re-evaluating next cycle',
1250
+ },
1251
+ });
1252
+ expect(fixerSessionService.acquireAndDispatch).not.toHaveBeenCalled();
1253
+ });
1254
+ it('settles the dispatch as COMPLETED when ratchet session finished its work', async () => {
1255
+ const workspace = {
1256
+ id: 'ws-idle-active',
1257
+ prUrl: 'https://github.com/example/repo/pull/11',
1258
+ prNumber: 11,
1259
+ prState: 'OPEN',
1260
+ prCiStatus: CIStatus.UNKNOWN,
1261
+ ratchetEnabled: true,
1262
+ ratchetState: RatchetState.CI_FAILED,
1263
+ ratchetActiveSessionId: 'ratchet-session',
1264
+ ratchetLastCiRunId: 'snapshot-key',
1265
+ prReviewLastCheckedAt: new Date('2026-01-01T00:00:00Z'),
1266
+ ratchetDispatchOutcome: 'RUNNING',
1267
+ ratchetDispatchRetryCount: 0,
1268
+ };
1269
+ vi.mocked(mockSessionBridge.findSessionById).mockResolvedValue({
1270
+ id: 'ratchet-session',
1271
+ provider: 'CLAUDE',
1272
+ status: SessionStatus.RUNNING,
1273
+ });
1274
+ vi.mocked(mockSessionBridge.isSessionRunning).mockReturnValue(true);
1275
+ vi.mocked(mockSessionBridge.isSessionWorking).mockReturnValue(false);
1276
+ vi.mocked(mockSessionBridge.stopSession).mockResolvedValue();
1277
+ const result = await unsafeCoerce(ratchetService).checkActiveFixerSession(workspace);
1278
+ expect(result).toEqual({ kind: 'settled', outcome: 'COMPLETED' });
1279
+ expect(mockWorkspaceBridge.recordSessionEnd).toHaveBeenCalledWith(workspace.id, 'ratchet-session', 'COMPLETED');
1280
+ expect(mockSessionBridge.stopSession).toHaveBeenCalledWith('ratchet-session');
1281
+ });
1282
+ it('decides to trigger fixer when context is actionable', async () => {
1283
+ const decision = await unsafeCoerce(ratchetService).decideRatchetAction({
1284
+ workspace: { id: 'ws-decide', ratchetEnabled: true },
1285
+ prStateInfo: { prState: 'OPEN', ciStatus: CIStatus.FAILURE },
702
1286
  isCleanPrWithNoNewReviewActivity: false,
703
- activeRatchetSession: null,
1287
+ activeFixerCheck: { kind: 'none' },
1288
+ dispatchOutcome: null,
1289
+ dispatchRetryCount: 0,
704
1290
  hasStateChangedSinceLastDispatch: true,
705
- hasOtherActiveSession: false,
706
1291
  });
707
- expect(decision).toEqual({ type: 'TRIGGER_FIXER' });
1292
+ expect(decision).toEqual({ type: 'TRIGGER_FIXER', retryCount: 0 });
708
1293
  });
709
- it('waits when CI is not in terminal state (PENDING)', () => {
710
- const decision = unsafeCoerce(ratchetService).decideRatchetAction({
711
- workspace: { ratchetEnabled: true },
1294
+ it('waits when CI is not in terminal state (PENDING)', async () => {
1295
+ const decision = await unsafeCoerce(ratchetService).decideRatchetAction({
1296
+ workspace: { id: 'ws-decide', ratchetEnabled: true },
712
1297
  prStateInfo: { prState: 'OPEN', ciStatus: CIStatus.PENDING },
713
1298
  isCleanPrWithNoNewReviewActivity: false,
714
- activeRatchetSession: null,
1299
+ activeFixerCheck: { kind: 'none' },
1300
+ dispatchOutcome: null,
1301
+ dispatchRetryCount: 0,
715
1302
  hasStateChangedSinceLastDispatch: true,
716
- hasOtherActiveSession: false,
717
1303
  });
718
1304
  expect(decision).toEqual({
719
1305
  type: 'RETURN_ACTION',
720
1306
  action: { type: 'WAITING', reason: 'Waiting for CI to complete (not in terminal state)' },
721
1307
  });
722
1308
  });
723
- it('waits when CI is not in terminal state (UNKNOWN)', () => {
724
- const decision = unsafeCoerce(ratchetService).decideRatchetAction({
725
- workspace: { ratchetEnabled: true },
1309
+ it('waits when CI is not in terminal state (UNKNOWN)', async () => {
1310
+ const decision = await unsafeCoerce(ratchetService).decideRatchetAction({
1311
+ workspace: { id: 'ws-decide', ratchetEnabled: true },
726
1312
  prStateInfo: { prState: 'OPEN', ciStatus: CIStatus.UNKNOWN },
727
1313
  isCleanPrWithNoNewReviewActivity: false,
728
- activeRatchetSession: null,
1314
+ activeFixerCheck: { kind: 'none' },
1315
+ dispatchOutcome: null,
1316
+ dispatchRetryCount: 0,
729
1317
  hasStateChangedSinceLastDispatch: true,
730
- hasOtherActiveSession: false,
731
1318
  });
732
1319
  expect(decision).toEqual({
733
1320
  type: 'RETURN_ACTION',
@@ -746,6 +1333,8 @@ describe('ratchet service (state-change + idle dispatch)', () => {
746
1333
  ratchetActiveSessionId: null,
747
1334
  ratchetLastCiRunId: 'prev-snapshot',
748
1335
  prReviewLastCheckedAt: null,
1336
+ ratchetDispatchOutcome: null,
1337
+ ratchetDispatchRetryCount: 0,
749
1338
  };
750
1339
  const prStateInfo = {
751
1340
  ciStatus: CIStatus.SUCCESS,
@@ -756,21 +1345,37 @@ describe('ratchet service (state-change + idle dispatch)', () => {
756
1345
  prState: 'OPEN',
757
1346
  prNumber: 10,
758
1347
  };
759
- const logContext = unsafeCoerce(ratchetService).buildRatchetingLogContext(workspace, RatchetState.READY, RatchetState.READY, { type: 'WAITING', reason: 'noop' }, prStateInfo, 10, { hasNewReviewActivitySinceLastDispatch: true });
760
- expect(logContext.reviewTimestampComparison.hasNewReviewActivitySinceLastDispatch).toBe(true);
1348
+ const logContext = buildRatchetingLogContext({
1349
+ workspace: workspace,
1350
+ previousState: RatchetState.READY,
1351
+ newState: RatchetState.READY,
1352
+ action: { type: 'WAITING', reason: 'noop' },
1353
+ prStateInfo: prStateInfo,
1354
+ prNumber: 10,
1355
+ decisionContext: { hasNewReviewActivitySinceLastDispatch: true },
1356
+ });
1357
+ expect(logContext.reviewTimestampComparison?.hasNewReviewActivitySinceLastDispatch).toBe(true);
761
1358
  });
762
1359
  it('ignores review activity authored by the authenticated user', () => {
763
- const latestActivity = unsafeCoerce(ratchetService).computeLatestReviewActivityAtMs({
1360
+ const latestActivity = computeLatestReviewActivityAtMs({
764
1361
  reviews: [
765
- { submittedAt: '2026-01-02T00:00:00Z', author: { login: 'ratchet-bot' } },
766
- { submittedAt: '2026-01-01T00:00:00Z', author: { login: 'reviewer' } },
1362
+ {
1363
+ submittedAt: '2026-01-02T00:00:00Z',
1364
+ author: { login: 'ratchet-bot' },
1365
+ state: 'CHANGES_REQUESTED',
1366
+ },
1367
+ {
1368
+ submittedAt: '2026-01-01T00:00:00Z',
1369
+ author: { login: 'reviewer' },
1370
+ state: 'CHANGES_REQUESTED',
1371
+ },
767
1372
  ],
768
1373
  comments: [{ updatedAt: '2026-01-02T01:00:00Z', author: { login: 'ratchet-bot' } }],
769
- }, [{ updatedAt: '2026-01-01T02:00:00Z', author: { login: 'reviewer2' } }], 'ratchet-bot');
1374
+ }, [{ updatedAt: '2026-01-01T02:00:00Z', author: { login: 'reviewer2' } }], 'ratchet-bot', 'CHANGES_REQUESTED');
770
1375
  expect(latestActivity).toBe(new Date('2026-01-01T02:00:00Z').getTime());
771
1376
  });
772
1377
  describe('determineRatchetState', () => {
773
- const callDetermineRatchetState = (pr) => unsafeCoerce(ratchetService).determineRatchetState(pr);
1378
+ const callDetermineRatchetState = (pr) => determineRatchetState(pr);
774
1379
  it('returns MERGED for merged PR', () => {
775
1380
  expect(callDetermineRatchetState({
776
1381
  ciStatus: CIStatus.SUCCESS,
@@ -829,7 +1434,7 @@ describe('ratchet service (state-change + idle dispatch)', () => {
829
1434
  });
830
1435
  });
831
1436
  describe('computeDispatchSnapshotKey', () => {
832
- const callComputeSnapshotKey = (ciStatus, hasChangesRequested, latestReviewActivityAtMs, statusChecks) => unsafeCoerce(ratchetService).computeDispatchSnapshotKey(ciStatus, hasChangesRequested, latestReviewActivityAtMs, statusChecks);
1437
+ const callComputeSnapshotKey = (ciStatus, hasChangesRequested, latestReviewActivityAtMs, statusChecks) => computeDispatchSnapshotKey(123, ciStatus, hasChangesRequested, latestReviewActivityAtMs, statusChecks);
833
1438
  it('includes CI status in key for non-failure states', () => {
834
1439
  const key = callComputeSnapshotKey(CIStatus.SUCCESS, false, null, null);
835
1440
  expect(key).toContain('ci:SUCCESS');
@@ -873,36 +1478,60 @@ describe('ratchet service (state-change + idle dispatch)', () => {
873
1478
  });
874
1479
  });
875
1480
  describe('computeLatestReviewActivityAtMs edge cases', () => {
876
- const callCompute = (prDetails, reviewComments, authenticatedUsername) => unsafeCoerce(ratchetService).computeLatestReviewActivityAtMs(prDetails, reviewComments, authenticatedUsername);
1481
+ const callCompute = (prDetails, reviewComments, authenticatedUsername) => computeLatestReviewActivityAtMs(prDetails, reviewComments, authenticatedUsername, 'CHANGES_REQUESTED');
877
1482
  it('returns null when there are no reviews or comments', () => {
878
1483
  expect(callCompute({ reviews: [], comments: [] }, [], null)).toBeNull();
879
1484
  });
880
1485
  it('returns null when all activity is from the authenticated user', () => {
881
1486
  expect(callCompute({
882
- reviews: [{ submittedAt: '2026-01-01T00:00:00Z', author: { login: 'me' } }],
1487
+ reviews: [
1488
+ {
1489
+ submittedAt: '2026-01-01T00:00:00Z',
1490
+ author: { login: 'me' },
1491
+ state: 'CHANGES_REQUESTED',
1492
+ },
1493
+ ],
883
1494
  comments: [{ updatedAt: '2026-01-02T00:00:00Z', author: { login: 'me' } }],
884
1495
  }, [{ updatedAt: '2026-01-03T00:00:00Z', author: { login: 'me' } }], 'me')).toBeNull();
885
1496
  });
886
1497
  it('does not filter when authenticatedUsername is null', () => {
887
1498
  const result = callCompute({
888
- reviews: [{ submittedAt: '2026-01-01T00:00:00Z', author: { login: 'bot' } }],
1499
+ reviews: [
1500
+ {
1501
+ submittedAt: '2026-01-01T00:00:00Z',
1502
+ author: { login: 'bot' },
1503
+ state: 'CHANGES_REQUESTED',
1504
+ },
1505
+ ],
889
1506
  comments: [],
890
1507
  }, [], null);
891
1508
  expect(result).toBe(new Date('2026-01-01T00:00:00Z').getTime());
892
1509
  });
893
- it('ignores reviews that have null submittedAt', () => {
1510
+ it('ignores reviews with null submittedAt and ordinary PR comments', () => {
894
1511
  const result = callCompute({
895
- reviews: [{ submittedAt: null, author: { login: 'reviewer' } }],
1512
+ reviews: [
1513
+ {
1514
+ submittedAt: null,
1515
+ author: { login: 'reviewer' },
1516
+ state: 'CHANGES_REQUESTED',
1517
+ },
1518
+ ],
896
1519
  comments: [{ updatedAt: '2026-01-02T00:00:00Z', author: { login: 'commenter' } }],
897
1520
  }, [], null);
898
- expect(result).toBe(new Date('2026-01-02T00:00:00Z').getTime());
1521
+ expect(result).toBeNull();
899
1522
  });
900
1523
  it('returns the latest timestamp across all sources', () => {
901
1524
  const result = callCompute({
902
- reviews: [{ submittedAt: '2026-01-01T00:00:00Z', author: { login: 'a' } }],
1525
+ reviews: [
1526
+ {
1527
+ submittedAt: '2026-01-01T00:00:00Z',
1528
+ author: { login: 'a' },
1529
+ state: 'CHANGES_REQUESTED',
1530
+ },
1531
+ ],
903
1532
  comments: [{ updatedAt: '2026-01-03T00:00:00Z', author: { login: 'b' } }],
904
1533
  }, [{ updatedAt: '2026-01-02T00:00:00Z', author: { login: 'c' } }], null);
905
- expect(result).toBe(new Date('2026-01-03T00:00:00Z').getTime());
1534
+ expect(result).toBe(new Date('2026-01-02T00:00:00Z').getTime());
906
1535
  });
907
1536
  });
908
1537
  describe('processWorkspace error handling', () => {
@@ -916,15 +1545,37 @@ describe('ratchet service (state-change + idle dispatch)', () => {
916
1545
  ratchetActiveSessionId: null,
917
1546
  ratchetLastCiRunId: null,
918
1547
  prReviewLastCheckedAt: null,
1548
+ ratchetDispatchOutcome: null,
1549
+ ratchetDispatchRetryCount: 0,
919
1550
  };
920
1551
  vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue(null);
921
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
922
1552
  const result = await unsafeCoerce(ratchetService).processWorkspace(workspace);
923
1553
  expect(result).toMatchObject({
924
1554
  action: { type: 'ERROR', error: 'Failed to fetch PR state' },
925
1555
  newState: RatchetState.IDLE,
926
1556
  });
927
1557
  });
1558
+ it('returns WAITING action when fetchPRState skips a recent fetch', async () => {
1559
+ const workspace = {
1560
+ id: 'ws-recent-fetch',
1561
+ prUrl: 'https://github.com/example/repo/pull/99',
1562
+ prNumber: 99,
1563
+ ratchetEnabled: true,
1564
+ ratchetState: RatchetState.IDLE,
1565
+ ratchetActiveSessionId: null,
1566
+ ratchetLastCiRunId: null,
1567
+ prReviewLastCheckedAt: null,
1568
+ ratchetDispatchOutcome: null,
1569
+ ratchetDispatchRetryCount: 0,
1570
+ };
1571
+ vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({ skipped: true, reason: 'recently_fetched' });
1572
+ const result = await unsafeCoerce(ratchetService).processWorkspace(workspace);
1573
+ expect(result).toMatchObject({
1574
+ action: { type: 'WAITING', reason: 'recently_fetched' },
1575
+ newState: RatchetState.IDLE,
1576
+ });
1577
+ expect(workspaceRatchetService.clearActiveSession).not.toHaveBeenCalled();
1578
+ });
928
1579
  it('returns WAITING when shutting down', async () => {
929
1580
  unsafeCoerce(ratchetService).isShuttingDown = true;
930
1581
  const workspace = {
@@ -936,6 +1587,8 @@ describe('ratchet service (state-change + idle dispatch)', () => {
936
1587
  ratchetActiveSessionId: null,
937
1588
  ratchetLastCiRunId: null,
938
1589
  prReviewLastCheckedAt: null,
1590
+ ratchetDispatchOutcome: null,
1591
+ ratchetDispatchRetryCount: 0,
939
1592
  };
940
1593
  const result = await unsafeCoerce(ratchetService).processWorkspace(workspace);
941
1594
  expect(result).toMatchObject({
@@ -954,6 +1607,8 @@ describe('ratchet service (state-change + idle dispatch)', () => {
954
1607
  ratchetActiveSessionId: null,
955
1608
  ratchetLastCiRunId: null,
956
1609
  prReviewLastCheckedAt: null,
1610
+ ratchetDispatchOutcome: null,
1611
+ ratchetDispatchRetryCount: 0,
957
1612
  };
958
1613
  vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockRejectedValue(new Error('Unexpected explosion'));
959
1614
  const result = await unsafeCoerce(ratchetService).processWorkspace(workspace);
@@ -971,13 +1626,34 @@ describe('ratchet service (state-change + idle dispatch)', () => {
971
1626
  expect(result).toEqual({ checked: 0, stateChanges: 0, actionsTriggered: 0, results: [] });
972
1627
  });
973
1628
  it('returns zero counts when no workspaces have PRs', async () => {
974
- vi.mocked(workspaceAccessor.findWithPRsForRatchet).mockResolvedValue([]);
1629
+ vi.mocked(workspaceRatchetService.findCandidates).mockResolvedValue([]);
975
1630
  const result = await ratchetService.checkAllWorkspaces();
976
1631
  expect(result).toEqual({ checked: 0, stateChanges: 0, actionsTriggered: 0, results: [] });
977
1632
  });
1633
+ it('loads the global review trigger mode once for the whole batch', async () => {
1634
+ const workspaces = ['ws-one', 'ws-two'].map((id) => ({
1635
+ id,
1636
+ prUrl: `https://github.com/example/repo/pull/${id}`,
1637
+ prNumber: 1,
1638
+ prState: 'OPEN',
1639
+ prCiStatus: CIStatus.UNKNOWN,
1640
+ ratchetEnabled: true,
1641
+ ratchetState: RatchetState.IDLE,
1642
+ ratchetActiveSessionId: null,
1643
+ ratchetLastCiRunId: null,
1644
+ prReviewLastCheckedAt: null,
1645
+ ratchetDispatchOutcome: null,
1646
+ ratchetDispatchRetryCount: 0,
1647
+ }));
1648
+ vi.mocked(workspaceRatchetService.findCandidates).mockResolvedValue(workspaces);
1649
+ vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({ skipped: true, reason: 'recently_fetched' });
1650
+ const result = await ratchetService.checkAllWorkspaces();
1651
+ expect(result.checked).toBe(2);
1652
+ expect(userSettingsService.get).toHaveBeenCalledTimes(1);
1653
+ });
978
1654
  it('returns an error result when one workspace check times out', async () => {
979
1655
  unsafeCoerce(ratchetService).workspaceCheckTimeoutMs = 5;
980
- vi.mocked(workspaceAccessor.findWithPRsForRatchet).mockResolvedValue([
1656
+ vi.mocked(workspaceRatchetService.findCandidates).mockResolvedValue([
981
1657
  {
982
1658
  id: 'ws-timeout',
983
1659
  prUrl: 'https://github.com/example/repo/pull/1',
@@ -991,6 +1667,8 @@ describe('ratchet service (state-change + idle dispatch)', () => {
991
1667
  ratchetActiveSessionId: null,
992
1668
  ratchetLastCiRunId: null,
993
1669
  prReviewLastCheckedAt: null,
1670
+ ratchetDispatchOutcome: null,
1671
+ ratchetDispatchRetryCount: 0,
994
1672
  },
995
1673
  {
996
1674
  id: 'ws-fast',
@@ -1005,11 +1683,14 @@ describe('ratchet service (state-change + idle dispatch)', () => {
1005
1683
  ratchetActiveSessionId: null,
1006
1684
  ratchetLastCiRunId: null,
1007
1685
  prReviewLastCheckedAt: null,
1686
+ ratchetDispatchOutcome: null,
1687
+ ratchetDispatchRetryCount: 0,
1008
1688
  },
1009
1689
  ]);
1010
- vi.spyOn(unsafeCoerce(ratchetService), 'processWorkspace').mockImplementation((workspace) => {
1690
+ vi.spyOn(unsafeCoerce(ratchetService), 'processWorkspace').mockImplementation((workspace, _opts, signal) => {
1011
1691
  if (workspace.id === 'ws-timeout') {
1012
- return new Promise(() => {
1692
+ return new Promise((_resolve, reject) => {
1693
+ signal.addEventListener('abort', () => reject(signal.reason), { once: true });
1013
1694
  });
1014
1695
  }
1015
1696
  return Promise.resolve({
@@ -1035,22 +1716,279 @@ describe('ratchet service (state-change + idle dispatch)', () => {
1035
1716
  }),
1036
1717
  ]));
1037
1718
  });
1038
- });
1039
- describe('shutdown behavior', () => {
1040
- it('stops immediately while monitor loop is sleeping', async () => {
1041
- vi.useFakeTimers();
1042
- vi.spyOn(ratchetService, 'checkAllWorkspaces').mockResolvedValue({
1043
- checked: 0,
1044
- stateChanges: 0,
1045
- actionsTriggered: 0,
1046
- results: [],
1719
+ it('does not count concurrency queue wait toward the workspace timeout', async () => {
1720
+ unsafeCoerce(ratchetService).workspaceCheckTimeoutMs = 5;
1721
+ const workspaces = Array.from({ length: 4 }, (_, index) => ({
1722
+ id: `ws-queued-timeout-${index}`,
1723
+ prUrl: `https://github.com/example/repo/pull/${index + 1}`,
1724
+ prNumber: index + 1,
1725
+ prState: 'OPEN',
1726
+ prCiStatus: CIStatus.UNKNOWN,
1727
+ defaultSessionProvider: 'WORKSPACE_DEFAULT',
1728
+ ratchetSessionProvider: 'WORKSPACE_DEFAULT',
1729
+ ratchetEnabled: true,
1730
+ ratchetState: RatchetState.IDLE,
1731
+ ratchetActiveSessionId: null,
1732
+ ratchetLastCiRunId: null,
1733
+ prReviewLastCheckedAt: null,
1734
+ ratchetDispatchOutcome: null,
1735
+ ratchetDispatchRetryCount: 0,
1736
+ }));
1737
+ vi.mocked(workspaceRatchetService.findCandidates).mockResolvedValue(workspaces);
1738
+ const processWorkspaceSpy = vi.spyOn(unsafeCoerce(ratchetService), 'processWorkspace');
1739
+ processWorkspaceSpy.mockImplementation((workspace, _opts, signal) => {
1740
+ if (workspace.id !== 'ws-queued-timeout-3') {
1741
+ return new Promise((_resolve, reject) => {
1742
+ signal.addEventListener('abort', () => reject(signal.reason), { once: true });
1743
+ });
1744
+ }
1745
+ return Promise.resolve({
1746
+ workspaceId: workspace.id,
1747
+ previousState: RatchetState.IDLE,
1748
+ newState: RatchetState.IDLE,
1749
+ action: { type: 'WAITING', reason: 'ran after queue' },
1750
+ });
1047
1751
  });
1048
- ratchetService.start();
1049
- await Promise.resolve();
1050
- await Promise.resolve();
1051
- await expect(ratchetService.stop()).resolves.toBeUndefined();
1052
- vi.useRealTimers();
1053
- });
1752
+ const result = await ratchetService.checkAllWorkspaces();
1753
+ expect(processWorkspaceSpy).toHaveBeenCalledWith(expect.objectContaining({ id: 'ws-queued-timeout-3' }), undefined, expect.any(AbortSignal), expect.any(Function), 'CHANGES_REQUESTED');
1754
+ expect(result.results[3]).toMatchObject({
1755
+ workspaceId: 'ws-queued-timeout-3',
1756
+ action: { type: 'WAITING', reason: 'ran after queue' },
1757
+ });
1758
+ });
1759
+ it('lets a committed dispatch finish instead of reporting a timeout error', async () => {
1760
+ unsafeCoerce(ratchetService).workspaceCheckTimeoutMs = 5;
1761
+ const workspace = {
1762
+ id: 'ws-committed-dispatch',
1763
+ prUrl: 'https://github.com/example/repo/pull/1',
1764
+ prNumber: 1,
1765
+ ratchetEnabled: true,
1766
+ ratchetState: RatchetState.IDLE,
1767
+ };
1768
+ vi.mocked(workspaceRatchetService.findCandidates).mockResolvedValue([workspace]);
1769
+ let finishCommit;
1770
+ const commitBarrier = new Promise((resolve) => {
1771
+ finishCommit = resolve;
1772
+ });
1773
+ vi.spyOn(unsafeCoerce(ratchetService), 'processWorkspace').mockImplementation(async (workspaceArg, _opts, signal, commitSideEffects) => {
1774
+ commitSideEffects();
1775
+ await commitBarrier;
1776
+ signal.throwIfAborted();
1777
+ return {
1778
+ workspaceId: workspaceArg.id,
1779
+ previousState: RatchetState.IDLE,
1780
+ newState: RatchetState.IDLE,
1781
+ action: { type: 'TRIGGERED_FIXER', sessionId: 'session-1', promptSent: true },
1782
+ };
1783
+ });
1784
+ const resultPromise = ratchetService.checkAllWorkspaces();
1785
+ await new Promise((resolve) => setTimeout(resolve, 10));
1786
+ finishCommit();
1787
+ const result = await resultPromise;
1788
+ expect(result.results[0]).toMatchObject({
1789
+ workspaceId: workspace.id,
1790
+ action: { type: 'TRIGGERED_FIXER', sessionId: 'session-1' },
1791
+ });
1792
+ });
1793
+ it('runs at most three workspace checks concurrently', async () => {
1794
+ const workspaces = Array.from({ length: 7 }, (_, index) => ({
1795
+ id: `ws-${index}`,
1796
+ prUrl: `https://github.com/example/repo/pull/${index + 1}`,
1797
+ prNumber: index + 1,
1798
+ prState: 'OPEN',
1799
+ prCiStatus: CIStatus.UNKNOWN,
1800
+ defaultSessionProvider: 'WORKSPACE_DEFAULT',
1801
+ ratchetSessionProvider: 'WORKSPACE_DEFAULT',
1802
+ ratchetEnabled: true,
1803
+ ratchetState: RatchetState.IDLE,
1804
+ ratchetActiveSessionId: null,
1805
+ ratchetLastCiRunId: null,
1806
+ prReviewLastCheckedAt: null,
1807
+ ratchetDispatchOutcome: null,
1808
+ ratchetDispatchRetryCount: 0,
1809
+ }));
1810
+ vi.mocked(workspaceRatchetService.findCandidates).mockResolvedValue(workspaces);
1811
+ let active = 0;
1812
+ let maximumActive = 0;
1813
+ let releaseChecks;
1814
+ const checkBarrier = new Promise((resolve) => {
1815
+ releaseChecks = resolve;
1816
+ });
1817
+ vi.spyOn(unsafeCoerce(ratchetService), 'processWorkspace').mockImplementation(async (workspace) => {
1818
+ active += 1;
1819
+ maximumActive = Math.max(maximumActive, active);
1820
+ await checkBarrier;
1821
+ active -= 1;
1822
+ return {
1823
+ workspaceId: workspace.id,
1824
+ previousState: RatchetState.IDLE,
1825
+ newState: RatchetState.IDLE,
1826
+ action: { type: 'WAITING', reason: 'noop' },
1827
+ };
1828
+ });
1829
+ const resultPromise = ratchetService.checkAllWorkspaces();
1830
+ await vi.waitFor(() => expect(active).toBe(3));
1831
+ expect(maximumActive).toBe(3);
1832
+ releaseChecks();
1833
+ const result = await resultPromise;
1834
+ expect(maximumActive).toBe(3);
1835
+ expect(result.checked).toBe(7);
1836
+ expect(result.results).toHaveLength(7);
1837
+ });
1838
+ });
1839
+ describe('shutdown behavior', () => {
1840
+ it('stops immediately while monitor loop is sleeping', async () => {
1841
+ vi.useFakeTimers();
1842
+ vi.spyOn(ratchetService, 'checkAllWorkspaces').mockResolvedValue({
1843
+ checked: 0,
1844
+ stateChanges: 0,
1845
+ actionsTriggered: 0,
1846
+ results: [],
1847
+ });
1848
+ ratchetService.start();
1849
+ await Promise.resolve();
1850
+ await Promise.resolve();
1851
+ await expect(ratchetService.stop()).resolves.toBeUndefined();
1852
+ vi.useRealTimers();
1853
+ });
1854
+ });
1855
+ describe('markPrClosed', () => {
1856
+ afterEach(() => {
1857
+ ratchetService.removeAllListeners();
1858
+ });
1859
+ it('resets ratchet state to IDLE via CAS on fromState and emits a state change', async () => {
1860
+ vi.mocked(workspaceDataService.findById).mockResolvedValue({
1861
+ id: 'ws-closed',
1862
+ ratchetEnabled: true,
1863
+ ratchetState: RatchetState.CI_FAILED,
1864
+ });
1865
+ vi.mocked(workspaceRatchetService.transitionStateIfEnabled).mockResolvedValue(true);
1866
+ const stateEvents = [];
1867
+ ratchetService.on(RATCHET_STATE_CHANGED, (event) => {
1868
+ stateEvents.push(event);
1869
+ });
1870
+ await ratchetService.markPrClosed('ws-closed');
1871
+ expect(workspaceRatchetService.transitionStateIfEnabled).toHaveBeenCalledWith('ws-closed', RatchetState.CI_FAILED, {
1872
+ ratchetState: RatchetState.IDLE,
1873
+ ratchetLastCheckedAt: expect.any(Date),
1874
+ });
1875
+ expect(stateEvents).toEqual([
1876
+ {
1877
+ workspaceId: 'ws-closed',
1878
+ fromState: RatchetState.CI_FAILED,
1879
+ toState: RatchetState.IDLE,
1880
+ },
1881
+ ]);
1882
+ });
1883
+ it('emits the fromState that actually won the compare-and-swap after losing a race', async () => {
1884
+ vi.mocked(workspaceDataService.findById)
1885
+ .mockResolvedValueOnce({
1886
+ id: 'ws-closed',
1887
+ ratchetEnabled: true,
1888
+ ratchetState: RatchetState.CI_FAILED,
1889
+ })
1890
+ .mockResolvedValueOnce({
1891
+ id: 'ws-closed',
1892
+ ratchetEnabled: true,
1893
+ ratchetState: RatchetState.CI_RUNNING,
1894
+ });
1895
+ vi.mocked(workspaceRatchetService.transitionStateIfEnabled)
1896
+ .mockResolvedValueOnce(false)
1897
+ .mockResolvedValueOnce(true);
1898
+ const stateEvents = [];
1899
+ ratchetService.on(RATCHET_STATE_CHANGED, (event) => {
1900
+ stateEvents.push(event);
1901
+ });
1902
+ await ratchetService.markPrClosed('ws-closed');
1903
+ expect(workspaceRatchetService.transitionStateIfEnabled).toHaveBeenNthCalledWith(1, 'ws-closed', RatchetState.CI_FAILED, expect.objectContaining({ ratchetState: RatchetState.IDLE }));
1904
+ expect(workspaceRatchetService.transitionStateIfEnabled).toHaveBeenNthCalledWith(2, 'ws-closed', RatchetState.CI_RUNNING, expect.objectContaining({ ratchetState: RatchetState.IDLE }));
1905
+ expect(stateEvents).toEqual([
1906
+ {
1907
+ workspaceId: 'ws-closed',
1908
+ fromState: RatchetState.CI_RUNNING,
1909
+ toState: RatchetState.IDLE,
1910
+ },
1911
+ ]);
1912
+ });
1913
+ it('is a no-op when ratchet state is already IDLE', async () => {
1914
+ vi.mocked(workspaceDataService.findById).mockResolvedValue({
1915
+ id: 'ws-closed',
1916
+ ratchetEnabled: true,
1917
+ ratchetState: RatchetState.IDLE,
1918
+ });
1919
+ await ratchetService.markPrClosed('ws-closed');
1920
+ expect(workspaceRatchetService.transitionStateIfEnabled).not.toHaveBeenCalled();
1921
+ });
1922
+ it('does not emit when ratcheting was disabled concurrently', async () => {
1923
+ vi.mocked(workspaceDataService.findById)
1924
+ .mockResolvedValueOnce({
1925
+ id: 'ws-closed',
1926
+ ratchetEnabled: true,
1927
+ ratchetState: RatchetState.CI_FAILED,
1928
+ })
1929
+ .mockResolvedValueOnce({
1930
+ id: 'ws-closed',
1931
+ ratchetEnabled: false,
1932
+ ratchetState: RatchetState.IDLE,
1933
+ });
1934
+ vi.mocked(workspaceRatchetService.transitionStateIfEnabled).mockResolvedValue(false);
1935
+ const stateEvents = [];
1936
+ ratchetService.on(RATCHET_STATE_CHANGED, (event) => {
1937
+ stateEvents.push(event);
1938
+ });
1939
+ await ratchetService.markPrClosed('ws-closed');
1940
+ expect(stateEvents).toEqual([]);
1941
+ });
1942
+ it('is a no-op when workspace is not found', async () => {
1943
+ vi.mocked(workspaceDataService.findById).mockResolvedValue(null);
1944
+ await ratchetService.markPrClosed('ws-missing');
1945
+ expect(workspaceRatchetService.transitionStateIfEnabled).not.toHaveBeenCalled();
1946
+ });
1947
+ it('does not continue to side effects after a timed-out await completes', async () => {
1948
+ unsafeCoerce(ratchetService).workspaceCheckTimeoutMs = 5;
1949
+ const workspace = {
1950
+ id: 'ws-zombie',
1951
+ prUrl: 'https://github.com/example/repo/pull/1',
1952
+ prNumber: 1,
1953
+ prState: 'OPEN',
1954
+ prCiStatus: CIStatus.FAILURE,
1955
+ defaultSessionProvider: 'WORKSPACE_DEFAULT',
1956
+ ratchetSessionProvider: 'WORKSPACE_DEFAULT',
1957
+ ratchetEnabled: true,
1958
+ ratchetState: RatchetState.IDLE,
1959
+ ratchetActiveSessionId: null,
1960
+ ratchetLastCiRunId: null,
1961
+ prReviewLastCheckedAt: null,
1962
+ ratchetDispatchOutcome: null,
1963
+ ratchetDispatchRetryCount: 0,
1964
+ };
1965
+ vi.mocked(workspaceRatchetService.findCandidates).mockResolvedValue([workspace]);
1966
+ let releaseFetch;
1967
+ const fetchBarrier = new Promise((resolve) => {
1968
+ releaseFetch = resolve;
1969
+ });
1970
+ const finishSpy = vi.spyOn(unsafeCoerce(ratchetService), 'finishRatchetCheck');
1971
+ vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockImplementation(async () => {
1972
+ await fetchBarrier;
1973
+ return {
1974
+ ciStatus: CIStatus.FAILURE,
1975
+ snapshotKey: 'failed:1',
1976
+ hasChangesRequested: false,
1977
+ hasMergeConflict: false,
1978
+ latestReviewActivityAtMs: null,
1979
+ statusCheckRollup: [],
1980
+ prState: 'OPEN',
1981
+ prNumber: 1,
1982
+ reviewComments: [],
1983
+ };
1984
+ });
1985
+ const result = await ratchetService.checkAllWorkspaces();
1986
+ expect(result.results[0]?.action.type).toBe('ERROR');
1987
+ releaseFetch();
1988
+ await new Promise((resolve) => setTimeout(resolve, 0));
1989
+ expect(finishSpy).not.toHaveBeenCalled();
1990
+ expect(workspaceRatchetService.transitionStateIfEnabled).not.toHaveBeenCalled();
1991
+ });
1054
1992
  });
1055
1993
  describe('checkWorkspaceById', () => {
1056
1994
  it('returns null when shutting down', async () => {
@@ -1060,10 +1998,151 @@ describe('ratchet service (state-change + idle dispatch)', () => {
1060
1998
  });
1061
1999
  it('returns null when workspace not found', async () => {
1062
2000
  unsafeCoerce(ratchetService).isShuttingDown = false;
1063
- vi.mocked(workspaceAccessor.findForRatchetById).mockResolvedValue(null);
2001
+ vi.mocked(workspaceRatchetService.findCandidateById).mockResolvedValue(null);
1064
2002
  const result = await ratchetService.checkWorkspaceById('ws-nonexistent');
1065
2003
  expect(result).toBeNull();
1066
2004
  });
2005
+ it('skips the PR fetch when the workspace was recently fetched', async () => {
2006
+ const workspace = {
2007
+ id: 'ws-cooldown',
2008
+ prUrl: 'https://github.com/example/repo/pull/9',
2009
+ prNumber: 9,
2010
+ prState: 'OPEN',
2011
+ prCiStatus: CIStatus.UNKNOWN,
2012
+ ratchetEnabled: true,
2013
+ ratchetState: RatchetState.CI_RUNNING,
2014
+ ratchetActiveSessionId: null,
2015
+ ratchetLastCiRunId: null,
2016
+ prReviewLastCheckedAt: null,
2017
+ ratchetDispatchOutcome: null,
2018
+ ratchetDispatchRetryCount: 0,
2019
+ };
2020
+ vi.mocked(workspaceRatchetService.findCandidateById).mockResolvedValue(workspace);
2021
+ vi.mocked(mockGitHubBridge.extractPRInfo).mockReturnValue({
2022
+ owner: 'example',
2023
+ repo: 'repo',
2024
+ number: 9,
2025
+ });
2026
+ vi.mocked(mockGitHubBridge.isRecentlyFetched).mockReturnValue(true);
2027
+ const result = await ratchetService.checkWorkspaceById('ws-cooldown');
2028
+ expect(result?.action).toEqual({ type: 'WAITING', reason: 'recently_fetched' });
2029
+ expect(mockGitHubBridge.getPRFullDetails).not.toHaveBeenCalled();
2030
+ });
2031
+ it('bypasses the PR fetch cooldown when requested by an event-driven check', async () => {
2032
+ const workspace = {
2033
+ id: 'ws-bypass',
2034
+ prUrl: 'https://github.com/example/repo/pull/9',
2035
+ prNumber: 9,
2036
+ prState: 'OPEN',
2037
+ prCiStatus: CIStatus.UNKNOWN,
2038
+ defaultSessionProvider: 'WORKSPACE_DEFAULT',
2039
+ ratchetSessionProvider: 'WORKSPACE_DEFAULT',
2040
+ ratchetEnabled: true,
2041
+ ratchetState: RatchetState.CI_RUNNING,
2042
+ ratchetActiveSessionId: null,
2043
+ ratchetLastCiRunId: null,
2044
+ prReviewLastCheckedAt: null,
2045
+ ratchetDispatchOutcome: null,
2046
+ ratchetDispatchRetryCount: 0,
2047
+ };
2048
+ vi.mocked(workspaceRatchetService.findCandidateById).mockResolvedValue(workspace);
2049
+ vi.mocked(mockGitHubBridge.extractPRInfo).mockReturnValue({
2050
+ owner: 'example',
2051
+ repo: 'repo',
2052
+ number: 9,
2053
+ });
2054
+ vi.mocked(mockGitHubBridge.isRecentlyFetched).mockReturnValue(true);
2055
+ vi.mocked(mockGitHubBridge.getPRFullDetails).mockResolvedValue({
2056
+ state: 'OPEN',
2057
+ number: 9,
2058
+ url: 'https://github.com/example/repo/pull/9',
2059
+ reviewDecision: null,
2060
+ mergeStateStatus: 'CLEAN',
2061
+ reviews: [],
2062
+ comments: [],
2063
+ statusCheckRollup: null,
2064
+ });
2065
+ vi.mocked(mockGitHubBridge.getReviewComments).mockResolvedValue([]);
2066
+ vi.mocked(mockGitHubBridge.computeCIStatus).mockReturnValue(CIStatus.SUCCESS);
2067
+ const result = await ratchetService.checkWorkspaceById('ws-bypass', {
2068
+ bypassPrFetchCooldown: true,
2069
+ });
2070
+ expect(mockGitHubBridge.getPRFullDetails).toHaveBeenCalledWith('example/repo', 9, expect.any(AbortSignal));
2071
+ expect(result?.newState).toBe(RatchetState.READY);
2072
+ expect(result?.action).not.toEqual({ type: 'WAITING', reason: 'recently_fetched' });
2073
+ });
2074
+ it('reruns a bypassed check once when the first attempt was dedup-skipped', async () => {
2075
+ const workspace = {
2076
+ id: 'ws-rerun',
2077
+ prUrl: 'https://github.com/example/repo/pull/9',
2078
+ prNumber: 9,
2079
+ prState: 'OPEN',
2080
+ prCiStatus: CIStatus.UNKNOWN,
2081
+ defaultSessionProvider: 'WORKSPACE_DEFAULT',
2082
+ ratchetSessionProvider: 'WORKSPACE_DEFAULT',
2083
+ ratchetEnabled: true,
2084
+ ratchetState: RatchetState.CI_RUNNING,
2085
+ ratchetActiveSessionId: null,
2086
+ ratchetLastCiRunId: null,
2087
+ prReviewLastCheckedAt: null,
2088
+ ratchetDispatchOutcome: null,
2089
+ ratchetDispatchRetryCount: 0,
2090
+ };
2091
+ vi.mocked(workspaceRatchetService.findCandidateById).mockResolvedValue(workspace);
2092
+ vi.mocked(mockGitHubBridge.extractPRInfo).mockReturnValue({
2093
+ owner: 'example',
2094
+ repo: 'repo',
2095
+ number: 9,
2096
+ });
2097
+ vi.mocked(mockGitHubBridge.isFetchInFlight).mockReturnValueOnce(true).mockReturnValue(false);
2098
+ vi.mocked(mockGitHubBridge.getPRFullDetails).mockResolvedValue({
2099
+ state: 'OPEN',
2100
+ number: 9,
2101
+ url: 'https://github.com/example/repo/pull/9',
2102
+ reviewDecision: null,
2103
+ mergeStateStatus: 'CLEAN',
2104
+ reviews: [],
2105
+ comments: [],
2106
+ statusCheckRollup: null,
2107
+ });
2108
+ vi.mocked(mockGitHubBridge.getReviewComments).mockResolvedValue([]);
2109
+ vi.mocked(mockGitHubBridge.computeCIStatus).mockReturnValue(CIStatus.SUCCESS);
2110
+ const result = await ratchetService.checkWorkspaceById('ws-rerun', {
2111
+ bypassPrFetchCooldown: true,
2112
+ });
2113
+ expect(mockGitHubBridge.getPRFullDetails).toHaveBeenCalledTimes(1);
2114
+ expect(result?.newState).toBe(RatchetState.READY);
2115
+ expect(result?.action).not.toEqual({ type: 'WAITING', reason: 'recently_fetched' });
2116
+ });
2117
+ it('does not rerun a bypassed check more than once', async () => {
2118
+ const workspace = {
2119
+ id: 'ws-rerun-cap',
2120
+ prUrl: 'https://github.com/example/repo/pull/9',
2121
+ prNumber: 9,
2122
+ prState: 'OPEN',
2123
+ prCiStatus: CIStatus.UNKNOWN,
2124
+ ratchetEnabled: true,
2125
+ ratchetState: RatchetState.CI_RUNNING,
2126
+ ratchetActiveSessionId: null,
2127
+ ratchetLastCiRunId: null,
2128
+ prReviewLastCheckedAt: null,
2129
+ ratchetDispatchOutcome: null,
2130
+ ratchetDispatchRetryCount: 0,
2131
+ };
2132
+ vi.mocked(workspaceRatchetService.findCandidateById).mockResolvedValue(workspace);
2133
+ vi.mocked(mockGitHubBridge.extractPRInfo).mockReturnValue({
2134
+ owner: 'example',
2135
+ repo: 'repo',
2136
+ number: 9,
2137
+ });
2138
+ vi.mocked(mockGitHubBridge.isFetchInFlight).mockReturnValue(true);
2139
+ const result = await ratchetService.checkWorkspaceById('ws-rerun-cap', {
2140
+ bypassPrFetchCooldown: true,
2141
+ });
2142
+ expect(result?.action).toEqual({ type: 'WAITING', reason: 'recently_fetched' });
2143
+ expect(mockGitHubBridge.isFetchInFlight).toHaveBeenCalledTimes(2);
2144
+ expect(mockGitHubBridge.getPRFullDetails).not.toHaveBeenCalled();
2145
+ });
1067
2146
  it('deduplicates concurrent checks for the same workspace', async () => {
1068
2147
  unsafeCoerce(ratchetService).isShuttingDown = false;
1069
2148
  const workspace = {
@@ -1079,8 +2158,10 @@ describe('ratchet service (state-change + idle dispatch)', () => {
1079
2158
  ratchetActiveSessionId: null,
1080
2159
  ratchetLastCiRunId: null,
1081
2160
  prReviewLastCheckedAt: null,
2161
+ ratchetDispatchOutcome: null,
2162
+ ratchetDispatchRetryCount: 0,
1082
2163
  };
1083
- vi.mocked(workspaceAccessor.findForRatchetById).mockResolvedValue(workspace);
2164
+ vi.mocked(workspaceRatchetService.findCandidateById).mockResolvedValue(workspace);
1084
2165
  let releaseCheck;
1085
2166
  const checkBarrier = new Promise((resolve) => {
1086
2167
  releaseCheck = () => resolve();
@@ -1099,7 +2180,7 @@ describe('ratchet service (state-change + idle dispatch)', () => {
1099
2180
  });
1100
2181
  const first = ratchetService.checkWorkspaceById('ws-1');
1101
2182
  const second = ratchetService.checkWorkspaceById('ws-1');
1102
- await Promise.resolve();
2183
+ await vi.waitFor(() => expect(processWorkspaceSpy).toHaveBeenCalledTimes(1));
1103
2184
  expect(processWorkspaceSpy).toHaveBeenCalledTimes(1);
1104
2185
  releaseCheck();
1105
2186
  const [firstResult, secondResult] = await Promise.all([first, second]);
@@ -1107,311 +2188,615 @@ describe('ratchet service (state-change + idle dispatch)', () => {
1107
2188
  expect(secondResult).toEqual(result);
1108
2189
  expect(processWorkspaceSpy).toHaveBeenCalledTimes(1);
1109
2190
  });
2191
+ it('deduplicates a same-workspace check before it enters the concurrency queue', async () => {
2192
+ const makeWorkspace = (id) => ({
2193
+ id,
2194
+ prUrl: `https://github.com/example/repo/pull/${id}`,
2195
+ prNumber: 1,
2196
+ ratchetEnabled: true,
2197
+ ratchetState: RatchetState.IDLE,
2198
+ });
2199
+ const workspaces = [
2200
+ makeWorkspace('blocker-1'),
2201
+ makeWorkspace('blocker-2'),
2202
+ makeWorkspace('blocker-3'),
2203
+ makeWorkspace('queued-target'),
2204
+ ];
2205
+ vi.mocked(workspaceRatchetService.findCandidates).mockResolvedValue(workspaces);
2206
+ vi.mocked(workspaceRatchetService.findCandidateById).mockResolvedValue(workspaces[3]);
2207
+ let releaseBlockers;
2208
+ const blockerBarrier = new Promise((resolve) => {
2209
+ releaseBlockers = resolve;
2210
+ });
2211
+ let activeBlockers = 0;
2212
+ let targetRuns = 0;
2213
+ vi.spyOn(unsafeCoerce(ratchetService), 'processWorkspace').mockImplementation(async (workspace) => {
2214
+ if (workspace.id.startsWith('blocker-')) {
2215
+ activeBlockers += 1;
2216
+ await blockerBarrier;
2217
+ }
2218
+ else {
2219
+ targetRuns += 1;
2220
+ }
2221
+ return {
2222
+ workspaceId: workspace.id,
2223
+ previousState: RatchetState.IDLE,
2224
+ newState: RatchetState.IDLE,
2225
+ action: { type: 'WAITING', reason: 'noop' },
2226
+ };
2227
+ });
2228
+ const batch = ratchetService.checkAllWorkspaces();
2229
+ await vi.waitFor(() => expect(activeBlockers).toBe(3));
2230
+ const direct = ratchetService.checkWorkspaceById('queued-target');
2231
+ await Promise.resolve();
2232
+ expect(targetRuns).toBe(0);
2233
+ releaseBlockers();
2234
+ const [batchResult, directResult] = await Promise.all([batch, direct]);
2235
+ expect(targetRuns).toBe(1);
2236
+ expect(directResult).toEqual(batchResult.results[3]);
2237
+ });
2238
+ it('keeps a limiter slot occupied until a timed-out runner finishes cleanup', async () => {
2239
+ unsafeCoerce(ratchetService).workspaceCheckTimeoutMs = 5;
2240
+ const makeWorkspace = (id) => ({
2241
+ id,
2242
+ prUrl: `https://github.com/example/repo/pull/${id}`,
2243
+ prNumber: 1,
2244
+ ratchetEnabled: true,
2245
+ ratchetState: RatchetState.IDLE,
2246
+ });
2247
+ const workspaces = [
2248
+ makeWorkspace('timed-out-1'),
2249
+ makeWorkspace('timed-out-2'),
2250
+ makeWorkspace('timed-out-3'),
2251
+ makeWorkspace('after-timeouts'),
2252
+ ];
2253
+ vi.mocked(workspaceRatchetService.findCandidates).mockResolvedValue(workspaces);
2254
+ let finishCleanup;
2255
+ const cleanupBarrier = new Promise((resolve) => {
2256
+ finishCleanup = resolve;
2257
+ });
2258
+ let activeTimedOutChecks = 0;
2259
+ let targetRuns = 0;
2260
+ vi.spyOn(unsafeCoerce(ratchetService), 'processWorkspace').mockImplementation(async (workspace, _opts, signal) => {
2261
+ if (workspace.id.startsWith('timed-out-')) {
2262
+ activeTimedOutChecks += 1;
2263
+ await new Promise((resolve) => {
2264
+ signal.addEventListener('abort', () => resolve(), { once: true });
2265
+ });
2266
+ await cleanupBarrier;
2267
+ signal.throwIfAborted();
2268
+ }
2269
+ targetRuns += workspace.id === 'after-timeouts' ? 1 : 0;
2270
+ return {
2271
+ workspaceId: workspace.id,
2272
+ previousState: RatchetState.IDLE,
2273
+ newState: RatchetState.IDLE,
2274
+ action: { type: 'WAITING', reason: 'noop' },
2275
+ };
2276
+ });
2277
+ const batch = ratchetService.checkAllWorkspaces();
2278
+ await vi.waitFor(() => expect(activeTimedOutChecks).toBe(3));
2279
+ await new Promise((resolve) => setTimeout(resolve, 10));
2280
+ expect(targetRuns).toBe(0);
2281
+ finishCleanup();
2282
+ await batch;
2283
+ expect(targetRuns).toBe(1);
2284
+ });
2285
+ it('runs a direct workspace check without waiting for the batch limiter', async () => {
2286
+ const makeWorkspace = (id) => ({
2287
+ id,
2288
+ prUrl: `https://github.com/example/repo/pull/${id}`,
2289
+ prNumber: 1,
2290
+ ratchetEnabled: true,
2291
+ ratchetState: RatchetState.IDLE,
2292
+ });
2293
+ const batchWorkspaces = [
2294
+ makeWorkspace('batch-blocker-1'),
2295
+ makeWorkspace('batch-blocker-2'),
2296
+ makeWorkspace('batch-blocker-3'),
2297
+ ];
2298
+ const directWorkspace = makeWorkspace('direct-target');
2299
+ vi.mocked(workspaceRatchetService.findCandidates).mockResolvedValue(batchWorkspaces);
2300
+ vi.mocked(workspaceRatchetService.findCandidateById).mockResolvedValue(directWorkspace);
2301
+ let releaseBatch;
2302
+ const batchBarrier = new Promise((resolve) => {
2303
+ releaseBatch = resolve;
2304
+ });
2305
+ let activeBatchChecks = 0;
2306
+ let directRuns = 0;
2307
+ vi.spyOn(unsafeCoerce(ratchetService), 'processWorkspace').mockImplementation(async (workspace) => {
2308
+ if (workspace.id.startsWith('batch-blocker-')) {
2309
+ activeBatchChecks += 1;
2310
+ await batchBarrier;
2311
+ }
2312
+ else {
2313
+ directRuns += 1;
2314
+ }
2315
+ return {
2316
+ workspaceId: workspace.id,
2317
+ previousState: RatchetState.IDLE,
2318
+ newState: RatchetState.IDLE,
2319
+ action: { type: 'WAITING', reason: 'noop' },
2320
+ };
2321
+ });
2322
+ const batch = ratchetService.checkAllWorkspaces();
2323
+ await vi.waitFor(() => expect(activeBatchChecks).toBe(3));
2324
+ const directResult = await ratchetService.checkWorkspaceById(directWorkspace.id);
2325
+ expect(directRuns).toBe(1);
2326
+ expect(directResult?.workspaceId).toBe(directWorkspace.id);
2327
+ releaseBatch();
2328
+ await batch;
2329
+ });
1110
2330
  });
1111
2331
  describe('decideRatchetAction edge cases', () => {
1112
2332
  const callDecide = (context) => unsafeCoerce(ratchetService).decideRatchetAction(context);
1113
- it('returns COMPLETED for merged PR', () => {
1114
- expect(callDecide({
1115
- workspace: { ratchetEnabled: true },
2333
+ const arrangeOtherWorkingSession = () => {
2334
+ vi.mocked(mockSessionBridge.findSessionsByWorkspaceId).mockResolvedValue([
2335
+ { id: 'other-session' },
2336
+ ]);
2337
+ vi.mocked(mockSessionBridge.isSessionWorking).mockReturnValue(true);
2338
+ };
2339
+ it('returns COMPLETED for merged PR', async () => {
2340
+ expect(await callDecide({
2341
+ workspace: { id: 'ws-decide', ratchetEnabled: true },
1116
2342
  prStateInfo: { prState: 'MERGED', ciStatus: CIStatus.SUCCESS },
1117
2343
  isCleanPrWithNoNewReviewActivity: false,
1118
- activeRatchetSession: null,
2344
+ activeFixerCheck: { kind: 'none' },
2345
+ dispatchOutcome: null,
2346
+ dispatchRetryCount: 0,
1119
2347
  hasStateChangedSinceLastDispatch: true,
1120
- hasOtherActiveSession: false,
1121
2348
  })).toEqual({ type: 'RETURN_ACTION', action: { type: 'COMPLETED' } });
1122
2349
  });
1123
- it('returns WAITING for non-open PR', () => {
1124
- const result = callDecide({
1125
- workspace: { ratchetEnabled: true },
2350
+ it('returns WAITING for non-open PR', async () => {
2351
+ const result = (await callDecide({
2352
+ workspace: { id: 'ws-decide', ratchetEnabled: true },
1126
2353
  prStateInfo: { prState: 'CLOSED', ciStatus: CIStatus.SUCCESS },
1127
2354
  isCleanPrWithNoNewReviewActivity: false,
1128
- activeRatchetSession: null,
2355
+ activeFixerCheck: { kind: 'none' },
2356
+ dispatchOutcome: null,
2357
+ dispatchRetryCount: 0,
1129
2358
  hasStateChangedSinceLastDispatch: true,
1130
- hasOtherActiveSession: false,
1131
- });
2359
+ }));
1132
2360
  expect(result.action?.reason).toBe('PR is not open');
1133
2361
  });
1134
- it('returns active session when one exists', () => {
2362
+ it('returns active session when one exists', async () => {
1135
2363
  const activeSession = { type: 'FIXER_ACTIVE', sessionId: 's-1' };
1136
- const result = callDecide({
1137
- workspace: { ratchetEnabled: true },
2364
+ const result = (await callDecide({
2365
+ workspace: { id: 'ws-decide', ratchetEnabled: true },
1138
2366
  prStateInfo: { prState: 'OPEN', ciStatus: CIStatus.FAILURE },
1139
2367
  isCleanPrWithNoNewReviewActivity: false,
1140
- activeRatchetSession: activeSession,
2368
+ activeFixerCheck: { kind: 'active', action: activeSession },
2369
+ dispatchOutcome: 'RUNNING',
2370
+ dispatchRetryCount: 0,
1141
2371
  hasStateChangedSinceLastDispatch: true,
1142
- hasOtherActiveSession: false,
1143
- });
2372
+ }));
1144
2373
  expect(result.action).toEqual(activeSession);
1145
2374
  });
1146
- it('returns WAITING when other active session blocks dispatch', () => {
1147
- const result = callDecide({
1148
- workspace: { ratchetEnabled: true },
2375
+ it('returns WAITING when other active session blocks dispatch', async () => {
2376
+ arrangeOtherWorkingSession();
2377
+ const result = (await callDecide({
2378
+ workspace: { id: 'ws-decide', ratchetEnabled: true },
1149
2379
  prStateInfo: { prState: 'OPEN', ciStatus: CIStatus.FAILURE },
1150
2380
  isCleanPrWithNoNewReviewActivity: false,
1151
- activeRatchetSession: null,
2381
+ activeFixerCheck: { kind: 'none' },
2382
+ dispatchOutcome: null,
2383
+ dispatchRetryCount: 0,
1152
2384
  hasStateChangedSinceLastDispatch: true,
1153
- hasOtherActiveSession: true,
1154
- });
2385
+ }));
1155
2386
  expect(result.action?.reason).toBe('Workspace has another working session');
1156
2387
  });
1157
- it('returns WAITING when there are no CI failures or PR review comments', () => {
1158
- const result = callDecide({
1159
- workspace: { ratchetEnabled: true },
2388
+ it('returns WAITING when there are no CI failures or PR review comments', async () => {
2389
+ const result = (await callDecide({
2390
+ workspace: { id: 'ws-decide', ratchetEnabled: true },
1160
2391
  prStateInfo: {
1161
2392
  prState: 'OPEN',
1162
2393
  ciStatus: CIStatus.SUCCESS,
1163
2394
  reviewComments: [],
1164
2395
  },
1165
2396
  isCleanPrWithNoNewReviewActivity: false,
1166
- activeRatchetSession: null,
2397
+ activeFixerCheck: { kind: 'none' },
2398
+ dispatchOutcome: null,
2399
+ dispatchRetryCount: 0,
1167
2400
  hasStateChangedSinceLastDispatch: true,
1168
- hasOtherActiveSession: false,
1169
- });
2401
+ }));
1170
2402
  expect(result.action?.reason).toBe('No CI failures or PR review comments to address');
1171
2403
  });
1172
- });
1173
- describe('getActiveRatchetSession edge cases', () => {
1174
- const callGetActiveRatchetSession = (workspace) => unsafeCoerce(ratchetService).getActiveRatchetSession(workspace);
1175
- it('returns null when ratchetActiveSessionId is null', async () => {
1176
- const result = await callGetActiveRatchetSession({
1177
- id: 'ws-1',
1178
- ratchetActiveSessionId: null,
2404
+ it('waits when the fixer ended concurrently during the check', async () => {
2405
+ const result = (await callDecide({
2406
+ workspace: { id: 'ws-decide', ratchetEnabled: true },
2407
+ prStateInfo: { prState: 'OPEN', ciStatus: CIStatus.FAILURE },
2408
+ isCleanPrWithNoNewReviewActivity: false,
2409
+ activeFixerCheck: { kind: 'ended_concurrently' },
2410
+ dispatchOutcome: 'COMPLETED',
2411
+ dispatchRetryCount: 0,
2412
+ hasStateChangedSinceLastDispatch: false,
2413
+ }));
2414
+ expect(result.action?.reason).toBe('Fixer session ended during this check; re-evaluating next cycle');
2415
+ });
2416
+ it('retries a died fixer ahead of the unchanged-state gate', async () => {
2417
+ const result = await callDecide({
2418
+ workspace: { id: 'ws-decide', ratchetEnabled: true },
2419
+ prStateInfo: { prState: 'OPEN', ciStatus: CIStatus.FAILURE },
2420
+ isCleanPrWithNoNewReviewActivity: false,
2421
+ activeFixerCheck: { kind: 'settled', outcome: 'DIED' },
2422
+ dispatchOutcome: 'DIED',
2423
+ dispatchRetryCount: 2,
2424
+ hasStateChangedSinceLastDispatch: false,
1179
2425
  });
1180
- expect(result).toBeNull();
2426
+ expect(result).toEqual({ type: 'TRIGGER_FIXER', retryCount: 3 });
1181
2427
  });
1182
- it('returns null and resets dispatch tracking when session DB record is missing', async () => {
1183
- vi.mocked(agentSessionAccessor.findById).mockResolvedValue(null);
1184
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
1185
- vi.mocked(mockSnapshotBridge.recordReviewCheck).mockResolvedValue();
1186
- const result = await callGetActiveRatchetSession({
1187
- id: 'ws-2',
1188
- ratchetActiveSessionId: 'gone-session',
1189
- });
1190
- expect(result).toBeNull();
1191
- expect(workspaceAccessor.update).toHaveBeenCalledWith('ws-2', {
2428
+ it('does not retry a died fixer while another session is working', async () => {
2429
+ arrangeOtherWorkingSession();
2430
+ const result = (await callDecide({
2431
+ workspace: { id: 'ws-decide', ratchetEnabled: true },
2432
+ prStateInfo: { prState: 'OPEN', ciStatus: CIStatus.FAILURE },
2433
+ isCleanPrWithNoNewReviewActivity: false,
2434
+ activeFixerCheck: { kind: 'settled', outcome: 'DIED' },
2435
+ dispatchOutcome: 'DIED',
2436
+ dispatchRetryCount: 0,
2437
+ hasStateChangedSinceLastDispatch: false,
2438
+ }));
2439
+ expect(result.action?.reason).toBe('Workspace has another working session');
2440
+ });
2441
+ it('gives up on a died fixer once the retry budget is spent', async () => {
2442
+ const result = (await callDecide({
2443
+ workspace: { id: 'ws-decide', ratchetEnabled: true },
2444
+ prStateInfo: { prState: 'OPEN', ciStatus: CIStatus.FAILURE },
2445
+ isCleanPrWithNoNewReviewActivity: false,
2446
+ activeFixerCheck: { kind: 'none' },
2447
+ dispatchOutcome: 'DIED',
2448
+ dispatchRetryCount: 3,
2449
+ hasStateChangedSinceLastDispatch: false,
2450
+ }));
2451
+ expect(result.action?.reason).toBe('Fixer died 4 times for this PR state; waiting for PR state to change');
2452
+ });
2453
+ });
2454
+ describe('checkActiveFixerSession edge cases', () => {
2455
+ const callCheckActiveFixerSession = (workspace) => unsafeCoerce(ratchetService).checkActiveFixerSession(workspace);
2456
+ it('returns none when ratchetActiveSessionId is null', async () => {
2457
+ const result = await callCheckActiveFixerSession({
2458
+ id: 'ws-1',
1192
2459
  ratchetActiveSessionId: null,
1193
- ratchetLastCiRunId: null,
1194
2460
  });
1195
- expect(mockSnapshotBridge.recordReviewCheck).toHaveBeenCalledWith('ws-2', null);
2461
+ expect(result).toEqual({ kind: 'none' });
1196
2462
  });
1197
- it('returns null and resets dispatch tracking when session is not RUNNING', async () => {
1198
- vi.mocked(agentSessionAccessor.findById).mockResolvedValue({
2463
+ it('settles a non-RUNNING session as COMPLETED when its status is not FAILED', async () => {
2464
+ vi.mocked(mockSessionBridge.findSessionById).mockResolvedValue({
1199
2465
  id: 'completed-session',
1200
2466
  provider: 'CLAUDE',
1201
2467
  status: SessionStatus.IDLE,
1202
2468
  });
1203
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
1204
- vi.mocked(mockSnapshotBridge.recordReviewCheck).mockResolvedValue();
1205
- const result = await callGetActiveRatchetSession({
2469
+ const result = await callCheckActiveFixerSession({
1206
2470
  id: 'ws-3',
1207
2471
  ratchetActiveSessionId: 'completed-session',
1208
2472
  });
1209
- expect(result).toBeNull();
1210
- expect(workspaceAccessor.update).toHaveBeenCalledWith('ws-3', {
1211
- ratchetActiveSessionId: null,
1212
- ratchetLastCiRunId: null,
2473
+ expect(result).toEqual({ kind: 'settled', outcome: 'COMPLETED' });
2474
+ expect(mockWorkspaceBridge.recordSessionEnd).toHaveBeenCalledWith('ws-3', 'completed-session', 'COMPLETED');
2475
+ });
2476
+ it('settles a FAILED session as DIED', async () => {
2477
+ vi.mocked(mockSessionBridge.findSessionById).mockResolvedValue({
2478
+ id: 'failed-session',
2479
+ provider: 'CLAUDE',
2480
+ status: SessionStatus.FAILED,
2481
+ });
2482
+ const result = await callCheckActiveFixerSession({
2483
+ id: 'ws-4',
2484
+ ratchetActiveSessionId: 'failed-session',
1213
2485
  });
1214
- expect(mockSnapshotBridge.recordReviewCheck).toHaveBeenCalledWith('ws-3', null);
2486
+ expect(result).toEqual({ kind: 'settled', outcome: 'DIED' });
2487
+ expect(mockWorkspaceBridge.recordSessionEnd).toHaveBeenCalledWith('ws-4', 'failed-session', 'DIED');
2488
+ });
2489
+ it('settles a provider-mismatched session as DIED and stops it', async () => {
2490
+ vi.mocked(mockSessionBridge.findSessionById).mockResolvedValue({
2491
+ id: 'codex-session',
2492
+ provider: 'CODEX',
2493
+ status: SessionStatus.RUNNING,
2494
+ });
2495
+ vi.mocked(mockSessionBridge.stopSession).mockResolvedValue();
2496
+ const result = await callCheckActiveFixerSession({
2497
+ id: 'ws-5',
2498
+ ratchetActiveSessionId: 'codex-session',
2499
+ defaultSessionProvider: 'WORKSPACE_DEFAULT',
2500
+ ratchetSessionProvider: 'WORKSPACE_DEFAULT',
2501
+ });
2502
+ expect(result).toEqual({ kind: 'settled', outcome: 'DIED' });
2503
+ expect(mockWorkspaceBridge.recordSessionEnd).toHaveBeenCalledWith('ws-5', 'codex-session', 'DIED');
2504
+ expect(mockSessionBridge.stopSession).toHaveBeenCalledWith('codex-session');
2505
+ });
2506
+ it('returns the active fixer when the session is working', async () => {
2507
+ vi.mocked(mockSessionBridge.findSessionById).mockResolvedValue({
2508
+ id: 'working-session',
2509
+ provider: 'CLAUDE',
2510
+ status: SessionStatus.RUNNING,
2511
+ });
2512
+ vi.mocked(mockSessionBridge.isSessionRunning).mockReturnValue(true);
2513
+ vi.mocked(mockSessionBridge.isSessionWorking).mockReturnValue(true);
2514
+ const result = await callCheckActiveFixerSession({
2515
+ id: 'ws-6',
2516
+ ratchetActiveSessionId: 'working-session',
2517
+ });
2518
+ expect(result).toEqual({
2519
+ kind: 'active',
2520
+ action: { type: 'FIXER_ACTIVE', sessionId: 'working-session' },
2521
+ });
2522
+ expect(mockWorkspaceBridge.recordSessionEnd).not.toHaveBeenCalled();
2523
+ });
2524
+ it('does not settle or stop a fixer after cancellation during session lookup', async () => {
2525
+ const controller = new AbortController();
2526
+ const timeoutError = new Error('Workspace check timed out');
2527
+ let finishLookup;
2528
+ vi.mocked(mockSessionBridge.findSessionById).mockImplementation(() => new Promise((resolve) => {
2529
+ finishLookup = resolve;
2530
+ }));
2531
+ const check = unsafeCoerce(ratchetService).checkActiveFixerSession({
2532
+ id: 'ws-cancelled-active-check',
2533
+ ratchetActiveSessionId: 'session-1',
2534
+ }, controller.signal);
2535
+ await vi.waitFor(() => expect(mockSessionBridge.findSessionById).toHaveBeenCalled());
2536
+ controller.abort(timeoutError);
2537
+ finishLookup(null);
2538
+ await expect(check).rejects.toBe(timeoutError);
2539
+ expect(mockWorkspaceBridge.recordSessionEnd).not.toHaveBeenCalled();
2540
+ expect(mockSessionBridge.stopSession).not.toHaveBeenCalled();
2541
+ });
2542
+ it('publishes a successful fallback settlement before observing post-write cancellation', async () => {
2543
+ const controller = new AbortController();
2544
+ const timeoutError = new Error('Workspace check timed out');
2545
+ vi.mocked(mockSessionBridge.findSessionById).mockResolvedValue(null);
2546
+ vi.mocked(mockWorkspaceBridge.recordSessionEnd).mockImplementation(() => {
2547
+ controller.abort(timeoutError);
2548
+ return Promise.resolve(true);
2549
+ });
2550
+ const events = [];
2551
+ ratchetService.on(RATCHET_DISPATCH_CHANGED, (event) => {
2552
+ events.push(event);
2553
+ });
2554
+ const check = unsafeCoerce(ratchetService).checkActiveFixerSession({
2555
+ id: 'ws-cancelled-after-settlement',
2556
+ ratchetActiveSessionId: 'session-1',
2557
+ ratchetDispatchRetryCount: 2,
2558
+ }, controller.signal);
2559
+ await expect(check).rejects.toBe(timeoutError);
2560
+ expect(events).toEqual([{ workspaceId: 'ws-cancelled-after-settlement' }]);
1215
2561
  });
1216
2562
  });
1217
- describe('review comment polling', () => {
1218
- const cleanWorkspace = {
1219
- id: 'ws-poll',
1220
- prUrl: 'https://github.com/example/repo/pull/100',
1221
- prNumber: 100,
1222
- prState: 'OPEN',
1223
- prCiStatus: CIStatus.UNKNOWN,
1224
- ratchetEnabled: true,
1225
- ratchetState: RatchetState.READY,
1226
- ratchetActiveSessionId: null,
1227
- ratchetLastCiRunId: 'old-snapshot',
1228
- prReviewLastCheckedAt: new Date(),
1229
- };
1230
- const cleanPrState = {
1231
- ciStatus: CIStatus.SUCCESS,
1232
- snapshotKey: 'new-snapshot',
1233
- hasChangesRequested: false,
1234
- latestReviewActivityAtMs: new Date('2026-01-01T00:00:00Z').getTime(),
1235
- statusCheckRollup: null,
1236
- prState: 'OPEN',
1237
- prNumber: 100,
1238
- reviewComments: [],
1239
- };
1240
- const prStateWithComments = {
1241
- ...cleanPrState,
1242
- snapshotKey: 'snapshot-with-comments',
1243
- hasChangesRequested: true,
1244
- latestReviewActivityAtMs: new Date('2026-01-03T00:00:00Z').getTime(),
1245
- reviewComments: [
1246
- {
1247
- author: 'reviewer',
1248
- body: 'Please update this line',
1249
- path: 'src/file.ts',
1250
- line: 21,
1251
- url: 'https://github.com/example/repo/pull/100#discussion_r2',
1252
- },
1253
- ],
1254
- };
1255
- const getTrackers = () => unsafeCoerce(ratchetService).reviewPollTrackers;
1256
- const callProcessWorkspace = (workspace) => unsafeCoerce(ratchetService).processWorkspace(workspace);
1257
- it('creates tracker when PR is first seen as clean with state change', async () => {
1258
- vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue(cleanPrState);
1259
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
1260
- vi.mocked(agentSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
1261
- const result = await callProcessWorkspace(cleanWorkspace);
1262
- expect(result.action.type).toBe('WAITING');
1263
- expect(getTrackers().has('ws-poll')).toBe(true);
1264
- expect(getTrackers().get('ws-poll')?.snapshotKey).toBe('new-snapshot');
1265
- expect(getTrackers().get('ws-poll')?.pollCount).toBe(0);
1266
- });
1267
- it('creates tracker even when state has not changed (continuous polling)', async () => {
1268
- const workspace = { ...cleanWorkspace, ratchetLastCiRunId: 'new-snapshot' };
1269
- vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue(cleanPrState);
1270
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
1271
- vi.mocked(agentSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
1272
- await callProcessWorkspace(workspace);
1273
- expect(getTrackers().has('ws-poll')).toBe(true);
1274
- expect(getTrackers().get('ws-poll')?.pollCount).toBe(0);
1275
- });
1276
- it('skips re-poll when not enough time has elapsed', async () => {
1277
- getTrackers().set('ws-poll', {
1278
- snapshotKey: 'new-snapshot',
1279
- lastPolledAt: Date.now(),
1280
- pollCount: 0,
1281
- });
1282
- const fetchSpy = vi
1283
- .spyOn(unsafeCoerce(ratchetService), 'fetchPRState')
1284
- .mockResolvedValue(cleanPrState);
1285
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
1286
- vi.mocked(agentSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
1287
- await callProcessWorkspace(cleanWorkspace);
1288
- expect(fetchSpy).toHaveBeenCalledTimes(1);
1289
- expect(getTrackers().get('ws-poll')?.pollCount).toBe(0);
1290
- });
1291
- it('re-polls and finds comments when time has elapsed', async () => {
1292
- getTrackers().set('ws-poll', {
1293
- snapshotKey: 'new-snapshot',
1294
- lastPolledAt: Date.now() - 3 * 60_000,
1295
- pollCount: 0,
1296
- });
1297
- const fetchSpy = vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState');
1298
- fetchSpy.mockResolvedValueOnce(cleanPrState);
1299
- fetchSpy.mockResolvedValueOnce(prStateWithComments);
1300
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
1301
- vi.mocked(agentSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
2563
+ describe('triggerFixer error handling', () => {
2564
+ it('commits the workspace check as soon as the fixer session starts', async () => {
2565
+ const commitSideEffects = vi.fn();
2566
+ vi.mocked(fixerSessionService.acquireAndDispatch).mockImplementation(async (input) => {
2567
+ await input.afterStart?.({ sessionId: 'started-session', prompt: 'prompt' });
2568
+ return {
2569
+ status: 'started',
2570
+ sessionId: 'started-session',
2571
+ promptSent: true,
2572
+ };
2573
+ });
2574
+ const result = await unsafeCoerce(ratchetService).triggerFixer({
2575
+ id: 'ws-started-dispatch',
2576
+ prUrl: 'https://github.com/example/repo/pull/20',
2577
+ }, {
2578
+ ciStatus: CIStatus.FAILURE,
2579
+ snapshotKey: 'failed:20',
2580
+ prNumber: 20,
2581
+ reviewComments: [],
2582
+ hasMergeConflict: false,
2583
+ }, 0, new AbortController().signal, commitSideEffects);
2584
+ expect(result).toMatchObject({
2585
+ type: 'TRIGGERED_FIXER',
2586
+ sessionId: 'started-session',
2587
+ });
2588
+ expect(commitSideEffects).toHaveBeenCalledTimes(2);
2589
+ });
2590
+ it('cleans up a fixer when cancellation races with session startup', async () => {
2591
+ const controller = new AbortController();
2592
+ const timeoutError = new Error('Workspace check timed out');
2593
+ const commitSideEffects = vi.fn();
2594
+ vi.mocked(fixerSessionService.acquireAndDispatch).mockImplementation(async (input) => {
2595
+ controller.abort(timeoutError);
2596
+ try {
2597
+ await input.afterStart?.({ sessionId: 'cancelled-startup-session', prompt: 'prompt' });
2598
+ return {
2599
+ status: 'started',
2600
+ sessionId: 'cancelled-startup-session',
2601
+ promptSent: true,
2602
+ };
2603
+ }
2604
+ catch (error) {
2605
+ return {
2606
+ status: 'error',
2607
+ error: error instanceof Error ? error.message : String(error),
2608
+ };
2609
+ }
2610
+ });
2611
+ vi.mocked(mockSessionBridge.isSessionRunning).mockReturnValue(true);
2612
+ const trigger = unsafeCoerce(ratchetService).triggerFixer({
2613
+ id: 'ws-cancelled-startup',
2614
+ prUrl: 'https://github.com/example/repo/pull/20',
2615
+ }, {
2616
+ ciStatus: CIStatus.FAILURE,
2617
+ snapshotKey: 'failed:20',
2618
+ prNumber: 20,
2619
+ reviewComments: [],
2620
+ hasMergeConflict: false,
2621
+ }, 0, controller.signal, commitSideEffects);
2622
+ await expect(trigger).rejects.toBe(timeoutError);
2623
+ expect(commitSideEffects).toHaveBeenCalledTimes(1);
2624
+ expect(workspaceRatchetService.recordSessionEnd).toHaveBeenCalledWith('ws-cancelled-startup', 'cancelled-startup-session', 'COMPLETED');
2625
+ expect(mockSessionBridge.stopSession).toHaveBeenCalledWith('cancelled-startup-session');
2626
+ });
2627
+ it('does not clean up a fixer after its dispatch record is persisted', async () => {
2628
+ const controller = new AbortController();
2629
+ const timeoutError = new Error('Workspace check timed out');
2630
+ let finishRecord;
1302
2631
  vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
1303
2632
  status: 'started',
1304
- sessionId: 'ratchet-poll-session',
2633
+ sessionId: 'cancelled-session',
1305
2634
  promptSent: true,
1306
2635
  });
1307
- const result = await callProcessWorkspace(cleanWorkspace);
1308
- expect(fetchSpy).toHaveBeenCalledTimes(2);
1309
- expect(result.action.type).toBe('TRIGGERED_FIXER');
1310
- expect(getTrackers().has('ws-poll')).toBe(false);
1311
- });
1312
- it('increments pollCount when re-poll still finds clean PR', async () => {
1313
- getTrackers().set('ws-poll', {
1314
- snapshotKey: 'new-snapshot',
1315
- lastPolledAt: Date.now() - 3 * 60_000,
1316
- pollCount: 0,
1317
- });
1318
- const fetchSpy = vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState');
1319
- fetchSpy.mockResolvedValue(cleanPrState);
1320
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
1321
- vi.mocked(agentSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
1322
- await callProcessWorkspace(cleanWorkspace);
1323
- expect(fetchSpy).toHaveBeenCalledTimes(2);
1324
- expect(getTrackers().get('ws-poll')?.pollCount).toBe(1);
1325
- });
1326
- it('continues polling after many cycles without exhausting', async () => {
1327
- getTrackers().set('ws-poll', {
1328
- snapshotKey: 'new-snapshot',
1329
- lastPolledAt: Date.now() - 3 * 60_000,
1330
- pollCount: 50,
1331
- });
1332
- const fetchSpy = vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState');
1333
- fetchSpy.mockResolvedValue(cleanPrState);
1334
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
1335
- vi.mocked(agentSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
1336
- await callProcessWorkspace(cleanWorkspace);
1337
- expect(fetchSpy).toHaveBeenCalledTimes(2);
1338
- expect(getTrackers().get('ws-poll')?.pollCount).toBe(51);
1339
- });
1340
- it('resets tracker when snapshotKey changes', async () => {
1341
- getTrackers().set('ws-poll', {
1342
- snapshotKey: 'different-snapshot',
1343
- lastPolledAt: Date.now() - 10 * 60_000,
1344
- pollCount: 3,
1345
- });
1346
- vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue(cleanPrState);
1347
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
1348
- vi.mocked(agentSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
1349
- await callProcessWorkspace(cleanWorkspace);
1350
- const tracker = getTrackers().get('ws-poll');
1351
- expect(tracker?.snapshotKey).toBe('new-snapshot');
1352
- expect(tracker?.pollCount).toBe(0);
1353
- });
1354
- it('cleans up tracker when decision is not clean PR', async () => {
1355
- getTrackers().set('ws-poll', {
1356
- snapshotKey: 'new-snapshot',
1357
- lastPolledAt: Date.now(),
1358
- pollCount: 1,
1359
- });
1360
- const failedPrState = {
1361
- ...cleanPrState,
2636
+ vi.mocked(workspaceRatchetService.recordDispatchIfEnabled).mockImplementation(() => new Promise((resolve) => {
2637
+ finishRecord = resolve;
2638
+ }));
2639
+ vi.mocked(mockSessionBridge.isSessionRunning).mockReturnValue(true);
2640
+ const commitSideEffects = vi.fn();
2641
+ const events = [];
2642
+ ratchetService.on(RATCHET_DISPATCH_CHANGED, (event) => {
2643
+ events.push(event);
2644
+ });
2645
+ const trigger = unsafeCoerce(ratchetService).triggerFixer({
2646
+ id: 'ws-cancelled-dispatch',
2647
+ prUrl: 'https://github.com/example/repo/pull/20',
2648
+ }, {
1362
2649
  ciStatus: CIStatus.FAILURE,
1363
- snapshotKey: 'failed-snapshot',
1364
- };
1365
- vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue(failedPrState);
1366
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
1367
- vi.mocked(agentSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
2650
+ snapshotKey: 'failed:20',
2651
+ prNumber: 20,
2652
+ reviewComments: [],
2653
+ hasMergeConflict: false,
2654
+ }, 0, controller.signal, commitSideEffects);
2655
+ await vi.waitFor(() => expect(workspaceRatchetService.recordDispatchIfEnabled).toHaveBeenCalled());
2656
+ controller.abort(timeoutError);
2657
+ finishRecord(true);
2658
+ await expect(trigger).rejects.toBe(timeoutError);
2659
+ expect(commitSideEffects).toHaveBeenCalledTimes(1);
2660
+ expect(workspaceRatchetService.recordSessionEnd).not.toHaveBeenCalled();
2661
+ expect(mockSessionBridge.stopSession).not.toHaveBeenCalled();
2662
+ expect(events).toEqual([{ workspaceId: 'ws-cancelled-dispatch' }]);
2663
+ });
2664
+ it('persists and commits a dispatch while prompt completion is pending', async () => {
2665
+ let finishPrompt;
2666
+ const promptCompletion = new Promise((resolve) => {
2667
+ finishPrompt = resolve;
2668
+ });
2669
+ let finishRecord;
2670
+ vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
2671
+ status: 'started',
2672
+ sessionId: 'pending-prompt-session',
2673
+ promptSent: true,
2674
+ promptCompletion,
2675
+ });
2676
+ vi.mocked(workspaceRatchetService.recordDispatchIfEnabled).mockImplementation(() => new Promise((resolve) => {
2677
+ finishRecord = resolve;
2678
+ }));
2679
+ const commitSideEffects = vi.fn();
2680
+ const trigger = unsafeCoerce(ratchetService).triggerFixer({
2681
+ id: 'ws-pending-prompt',
2682
+ prUrl: 'https://github.com/example/repo/pull/20',
2683
+ }, {
2684
+ ciStatus: CIStatus.FAILURE,
2685
+ snapshotKey: 'failed:20',
2686
+ prNumber: 20,
2687
+ reviewComments: [],
2688
+ hasMergeConflict: false,
2689
+ }, 0, new AbortController().signal, commitSideEffects);
2690
+ await vi.waitFor(() => expect(workspaceRatchetService.recordDispatchIfEnabled).toHaveBeenCalled());
2691
+ expect(commitSideEffects).toHaveBeenCalledTimes(1);
2692
+ finishRecord(true);
2693
+ await expect(trigger).resolves.toMatchObject({ type: 'TRIGGERED_FIXER' });
2694
+ expect(commitSideEffects).toHaveBeenCalledTimes(1);
2695
+ finishPrompt(true);
2696
+ await promptCompletion;
2697
+ });
2698
+ it('settles a persisted dispatch as died when prompt completion fails', async () => {
1368
2699
  vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
1369
2700
  status: 'started',
1370
- sessionId: 'fixer',
2701
+ sessionId: 'failed-prompt-session',
1371
2702
  promptSent: true,
2703
+ promptCompletion: Promise.resolve(false),
1372
2704
  });
1373
- await callProcessWorkspace(cleanWorkspace);
1374
- expect(getTrackers().has('ws-poll')).toBe(false);
1375
- });
1376
- it('continues to next cycle when fetchPRState returns null during re-poll', async () => {
1377
- getTrackers().set('ws-poll', {
1378
- snapshotKey: 'new-snapshot',
1379
- lastPolledAt: Date.now() - 3 * 60_000,
1380
- pollCount: 0,
1381
- });
1382
- const fetchSpy = vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState');
1383
- fetchSpy.mockResolvedValueOnce(cleanPrState);
1384
- fetchSpy.mockResolvedValueOnce(null);
1385
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
1386
- vi.mocked(agentSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
1387
- const result = await callProcessWorkspace(cleanWorkspace);
1388
- expect(result.action.type).toBe('WAITING');
1389
- expect(getTrackers().get('ws-poll')?.pollCount).toBe(0);
1390
- });
1391
- it('skips re-poll when shutting down', async () => {
1392
- getTrackers().set('ws-poll', {
1393
- snapshotKey: 'new-snapshot',
1394
- lastPolledAt: Date.now() - 3 * 60_000,
1395
- pollCount: 0,
2705
+ vi.mocked(mockSessionBridge.isSessionRunning).mockReturnValue(true);
2706
+ const result = await unsafeCoerce(ratchetService).triggerFixer({
2707
+ id: 'ws-failed-prompt',
2708
+ prUrl: 'https://github.com/example/repo/pull/20',
2709
+ }, {
2710
+ ciStatus: CIStatus.FAILURE,
2711
+ snapshotKey: 'failed:20',
2712
+ prNumber: 20,
2713
+ reviewComments: [],
2714
+ hasMergeConflict: false,
2715
+ }, 0);
2716
+ expect(result).toMatchObject({ type: 'TRIGGERED_FIXER' });
2717
+ await vi.waitFor(() => expect(workspaceRatchetService.recordSessionEnd).toHaveBeenCalledWith('ws-failed-prompt', 'failed-prompt-session', 'DIED'));
2718
+ expect(mockSessionBridge.stopSession).toHaveBeenCalledWith('failed-prompt-session');
2719
+ });
2720
+ it('does not stop a newer active session when an old prompt completion fails', async () => {
2721
+ vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
2722
+ status: 'started',
2723
+ sessionId: 'stale-prompt-session',
2724
+ promptSent: true,
2725
+ promptCompletion: Promise.resolve(false),
1396
2726
  });
1397
- unsafeCoerce(ratchetService).isShuttingDown = false;
1398
- const fetchSpy = vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState');
1399
- fetchSpy.mockResolvedValueOnce(cleanPrState);
1400
- fetchSpy.mockResolvedValueOnce(cleanPrState);
1401
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
1402
- vi.mocked(agentSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
1403
- const handlePollSpy = vi.spyOn(unsafeCoerce(ratchetService), 'handleReviewCommentPoll');
1404
- handlePollSpy.mockImplementationOnce(async (...args) => {
1405
- unsafeCoerce(ratchetService).isShuttingDown = true;
1406
- handlePollSpy.mockRestore();
1407
- return await unsafeCoerce(ratchetService).handleReviewCommentPoll(...args);
1408
- });
1409
- const result = await callProcessWorkspace(cleanWorkspace);
1410
- expect(result.action.type).toBe('WAITING');
1411
- expect(fetchSpy).toHaveBeenCalledTimes(1);
2727
+ vi.mocked(workspaceRatchetService.recordSessionEnd).mockResolvedValue(false);
2728
+ vi.mocked(mockSessionBridge.isSessionRunning).mockReturnValue(true);
2729
+ await unsafeCoerce(ratchetService).triggerFixer({
2730
+ id: 'ws-replaced-prompt',
2731
+ prUrl: 'https://github.com/example/repo/pull/20',
2732
+ }, {
2733
+ ciStatus: CIStatus.FAILURE,
2734
+ snapshotKey: 'failed:20',
2735
+ prNumber: 20,
2736
+ reviewComments: [],
2737
+ hasMergeConflict: false,
2738
+ }, 0);
2739
+ await vi.waitFor(() => expect(workspaceRatchetService.recordSessionEnd).toHaveBeenCalled());
2740
+ expect(mockSessionBridge.stopSession).not.toHaveBeenCalled();
2741
+ });
2742
+ it('cleans up a fixer through prompt failure after its dispatch record is persisted', async () => {
2743
+ const controller = new AbortController();
2744
+ const timeoutError = new Error('Workspace check timed out');
2745
+ let finishRecord;
2746
+ vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
2747
+ status: 'started',
2748
+ sessionId: 'cancelled-session',
2749
+ promptSent: true,
2750
+ promptCompletion: Promise.resolve(false),
2751
+ });
2752
+ vi.mocked(workspaceRatchetService.recordDispatchIfEnabled).mockImplementation(() => new Promise((resolve) => {
2753
+ finishRecord = resolve;
2754
+ }));
2755
+ vi.mocked(mockSessionBridge.isSessionRunning).mockReturnValue(true);
2756
+ const commitSideEffects = vi.fn();
2757
+ const trigger = unsafeCoerce(ratchetService).triggerFixer({
2758
+ id: 'ws-cancelled-dispatch',
2759
+ prUrl: 'https://github.com/example/repo/pull/20',
2760
+ }, {
2761
+ ciStatus: CIStatus.FAILURE,
2762
+ snapshotKey: 'failed:20',
2763
+ prNumber: 20,
2764
+ reviewComments: [],
2765
+ hasMergeConflict: false,
2766
+ }, 0, controller.signal, commitSideEffects);
2767
+ await vi.waitFor(() => expect(workspaceRatchetService.recordDispatchIfEnabled).toHaveBeenCalled());
2768
+ controller.abort(timeoutError);
2769
+ finishRecord(true);
2770
+ await expect(trigger).rejects.toBe(timeoutError);
2771
+ expect(commitSideEffects).toHaveBeenCalledTimes(1);
2772
+ await vi.waitFor(() => expect(workspaceRatchetService.recordSessionEnd).toHaveBeenCalledWith('ws-cancelled-dispatch', 'cancelled-session', 'DIED'));
2773
+ expect(mockSessionBridge.stopSession).toHaveBeenCalledWith('cancelled-session');
2774
+ });
2775
+ it('cleans up a started fixer when dispatch persistence fails', async () => {
2776
+ vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
2777
+ status: 'started',
2778
+ sessionId: 'unrecorded-session',
2779
+ promptSent: true,
2780
+ });
2781
+ vi.mocked(workspaceRatchetService.recordDispatchIfEnabled).mockRejectedValue(new Error('dispatch record write failed'));
2782
+ vi.mocked(workspaceRatchetService.recordSessionEnd).mockRejectedValue(new Error('dispatch cleanup write failed'));
2783
+ vi.mocked(mockSessionBridge.isSessionRunning).mockReturnValue(true);
2784
+ const commitSideEffects = vi.fn();
2785
+ const result = await unsafeCoerce(ratchetService).triggerFixer({
2786
+ id: 'ws-unrecorded-dispatch',
2787
+ prUrl: 'https://github.com/example/repo/pull/20',
2788
+ }, {
2789
+ ciStatus: CIStatus.FAILURE,
2790
+ snapshotKey: 'failed:20',
2791
+ prNumber: 20,
2792
+ reviewComments: [],
2793
+ hasMergeConflict: false,
2794
+ }, 0, new AbortController().signal, commitSideEffects);
2795
+ expect(result).toMatchObject({ type: 'ERROR', error: 'dispatch record write failed' });
2796
+ expect(commitSideEffects).toHaveBeenCalledTimes(1);
2797
+ expect(workspaceRatchetService.recordSessionEnd).toHaveBeenCalledWith('ws-unrecorded-dispatch', 'unrecorded-session', 'COMPLETED');
2798
+ expect(mockSessionBridge.stopSession).toHaveBeenCalledWith('unrecorded-session');
1412
2799
  });
1413
- });
1414
- describe('triggerFixer error handling', () => {
1415
2800
  it('handles acquireAndDispatch throwing an error', async () => {
1416
2801
  const workspace = {
1417
2802
  id: 'ws-trigger-fail',
@@ -1422,12 +2807,14 @@ describe('ratchet service (state-change + idle dispatch)', () => {
1422
2807
  ratchetActiveSessionId: null,
1423
2808
  ratchetLastCiRunId: null,
1424
2809
  prReviewLastCheckedAt: null,
2810
+ ratchetDispatchOutcome: null,
2811
+ ratchetDispatchRetryCount: 0,
1425
2812
  };
1426
2813
  vi.mocked(fixerSessionService.acquireAndDispatch).mockRejectedValue(new Error('Session creation failed'));
1427
2814
  const result = await unsafeCoerce(ratchetService).triggerFixer(workspace, {
1428
2815
  ciStatus: CIStatus.FAILURE,
1429
2816
  prNumber: 20,
1430
- });
2817
+ }, 0);
1431
2818
  expect(result).toMatchObject({
1432
2819
  type: 'ERROR',
1433
2820
  error: 'Session creation failed',
@@ -1443,20 +2830,57 @@ describe('ratchet service (state-change + idle dispatch)', () => {
1443
2830
  ratchetActiveSessionId: null,
1444
2831
  ratchetLastCiRunId: null,
1445
2832
  prReviewLastCheckedAt: null,
2833
+ ratchetDispatchOutcome: null,
2834
+ ratchetDispatchRetryCount: 2,
1446
2835
  };
1447
2836
  vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
1448
2837
  status: 'already_active',
1449
2838
  sessionId: 'existing-session',
1450
2839
  });
1451
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
2840
+ const events = [];
2841
+ ratchetService.on(RATCHET_DISPATCH_CHANGED, (event) => {
2842
+ events.push(event);
2843
+ });
1452
2844
  const result = await unsafeCoerce(ratchetService).triggerFixer(workspace, {
1453
2845
  ciStatus: CIStatus.FAILURE,
1454
2846
  prNumber: 21,
1455
- });
2847
+ }, 0);
1456
2848
  expect(result).toMatchObject({
1457
2849
  type: 'FIXER_ACTIVE',
1458
2850
  sessionId: 'existing-session',
1459
2851
  });
2852
+ expect(workspaceRatchetService.adoptActiveSessionIfEnabled).toHaveBeenCalledWith('ws-already-active', 'existing-session');
2853
+ expect(workspaceRatchetService.recordDispatchIfEnabled).not.toHaveBeenCalled();
2854
+ expect(events).toEqual([{ workspaceId: 'ws-already-active' }]);
2855
+ });
2856
+ it('stops already-active fixer when active-session recording loses disable race', async () => {
2857
+ const workspace = {
2858
+ id: 'ws-already-active-disabled',
2859
+ prUrl: 'https://github.com/example/repo/pull/24',
2860
+ prNumber: 24,
2861
+ ratchetEnabled: true,
2862
+ ratchetState: RatchetState.CI_FAILED,
2863
+ ratchetActiveSessionId: null,
2864
+ ratchetLastCiRunId: null,
2865
+ prReviewLastCheckedAt: null,
2866
+ ratchetDispatchOutcome: null,
2867
+ ratchetDispatchRetryCount: 0,
2868
+ };
2869
+ vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
2870
+ status: 'already_active',
2871
+ sessionId: 'existing-session',
2872
+ });
2873
+ vi.mocked(workspaceRatchetService.adoptActiveSessionIfEnabled).mockResolvedValue(false);
2874
+ vi.mocked(mockSessionBridge.isSessionRunning).mockReturnValue(true);
2875
+ const result = await unsafeCoerce(ratchetService).triggerFixer(workspace, {
2876
+ ciStatus: CIStatus.FAILURE,
2877
+ prNumber: 24,
2878
+ }, 0);
2879
+ expect(result).toMatchObject({
2880
+ type: 'DISABLED',
2881
+ reason: 'Workspace ratcheting disabled',
2882
+ });
2883
+ expect(mockSessionBridge.stopSession).toHaveBeenCalledWith('existing-session');
1460
2884
  });
1461
2885
  it('handles skipped result from acquireAndDispatch', async () => {
1462
2886
  const workspace = {
@@ -1468,6 +2892,8 @@ describe('ratchet service (state-change + idle dispatch)', () => {
1468
2892
  ratchetActiveSessionId: null,
1469
2893
  ratchetLastCiRunId: null,
1470
2894
  prReviewLastCheckedAt: null,
2895
+ ratchetDispatchOutcome: null,
2896
+ ratchetDispatchRetryCount: 0,
1471
2897
  };
1472
2898
  vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
1473
2899
  status: 'skipped',
@@ -1476,7 +2902,7 @@ describe('ratchet service (state-change + idle dispatch)', () => {
1476
2902
  const result = await unsafeCoerce(ratchetService).triggerFixer(workspace, {
1477
2903
  ciStatus: CIStatus.FAILURE,
1478
2904
  prNumber: 22,
1479
- });
2905
+ }, 0);
1480
2906
  expect(result).toMatchObject({
1481
2907
  type: 'ERROR',
1482
2908
  error: 'No worktree path',
@@ -1488,12 +2914,13 @@ describe('ratchet service (state-change + idle dispatch)', () => {
1488
2914
  ratchetService.removeAllListeners();
1489
2915
  });
1490
2916
  it('emits ratchet_toggled when enabling workspace ratcheting', async () => {
1491
- vi.mocked(workspaceAccessor.findById).mockResolvedValue({
2917
+ vi.mocked(workspaceDataService.findById).mockResolvedValue({
1492
2918
  id: 'ws-enable',
1493
2919
  ratchetState: RatchetState.CI_FAILED,
1494
2920
  ratchetActiveSessionId: null,
2921
+ ratchetDispatchOutcome: 'DIED',
2922
+ ratchetDispatchRetryCount: 2,
1495
2923
  });
1496
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
1497
2924
  const toggledEvents = [];
1498
2925
  const stateEvents = [];
1499
2926
  ratchetService.on(RATCHET_TOGGLED, (event) => {
@@ -1503,7 +2930,7 @@ describe('ratchet service (state-change + idle dispatch)', () => {
1503
2930
  stateEvents.push(event);
1504
2931
  });
1505
2932
  await ratchetService.setWorkspaceRatcheting('ws-enable', true);
1506
- expect(workspaceAccessor.update).toHaveBeenCalledWith('ws-enable', { ratchetEnabled: true });
2933
+ expect(workspaceRatchetService.enable).toHaveBeenCalledWith('ws-enable');
1507
2934
  expect(toggledEvents).toEqual([
1508
2935
  {
1509
2936
  workspaceId: 'ws-enable',
@@ -1514,12 +2941,11 @@ describe('ratchet service (state-change + idle dispatch)', () => {
1514
2941
  expect(stateEvents).toHaveLength(0);
1515
2942
  });
1516
2943
  it('emits ratchet_toggled and ratchet_state_changed when disabling non-idle workspace', async () => {
1517
- vi.mocked(workspaceAccessor.findById).mockResolvedValue({
2944
+ vi.mocked(workspaceDataService.findById).mockResolvedValue({
1518
2945
  id: 'ws-disable',
1519
2946
  ratchetState: RatchetState.CI_RUNNING,
1520
2947
  ratchetActiveSessionId: null,
1521
2948
  });
1522
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
1523
2949
  const toggledEvents = [];
1524
2950
  const stateEvents = [];
1525
2951
  ratchetService.on(RATCHET_TOGGLED, (event) => {
@@ -1529,12 +2955,7 @@ describe('ratchet service (state-change + idle dispatch)', () => {
1529
2955
  stateEvents.push(event);
1530
2956
  });
1531
2957
  await ratchetService.setWorkspaceRatcheting('ws-disable', false);
1532
- expect(workspaceAccessor.update).toHaveBeenCalledWith('ws-disable', {
1533
- ratchetEnabled: false,
1534
- ratchetState: RatchetState.IDLE,
1535
- ratchetActiveSessionId: null,
1536
- ratchetLastCiRunId: null,
1537
- });
2958
+ expect(workspaceRatchetService.disable).toHaveBeenCalledWith('ws-disable');
1538
2959
  expect(stateEvents).toEqual([
1539
2960
  {
1540
2961
  workspaceId: 'ws-disable',
@@ -1550,6 +2971,30 @@ describe('ratchet service (state-change + idle dispatch)', () => {
1550
2971
  },
1551
2972
  ]);
1552
2973
  });
2974
+ it('stops ratchet workflow sessions found after disabling workspace', async () => {
2975
+ vi.mocked(workspaceDataService.findById).mockResolvedValue({
2976
+ id: 'ws-disable-raced-session',
2977
+ ratchetState: RatchetState.CI_RUNNING,
2978
+ ratchetActiveSessionId: null,
2979
+ });
2980
+ vi.mocked(mockSessionBridge.findSessionsByWorkspaceId).mockResolvedValue([
2981
+ {
2982
+ id: 'raced-ratchet-session',
2983
+ workflow: 'ratchet',
2984
+ status: SessionStatus.RUNNING,
2985
+ },
2986
+ {
2987
+ id: 'manual-session',
2988
+ workflow: 'default',
2989
+ status: SessionStatus.RUNNING,
2990
+ },
2991
+ ]);
2992
+ vi.mocked(mockSessionBridge.isSessionRunning).mockImplementation((sessionId) => sessionId === 'raced-ratchet-session');
2993
+ await ratchetService.setWorkspaceRatcheting('ws-disable-raced-session', false);
2994
+ expect(mockSessionBridge.findSessionsByWorkspaceId).toHaveBeenCalledWith('ws-disable-raced-session');
2995
+ expect(mockSessionBridge.stopSession).toHaveBeenCalledTimes(1);
2996
+ expect(mockSessionBridge.stopSession).toHaveBeenCalledWith('raced-ratchet-session');
2997
+ });
1553
2998
  });
1554
2999
  describe('event emission', () => {
1555
3000
  afterEach(() => {
@@ -1565,8 +3010,9 @@ describe('ratchet service (state-change + idle dispatch)', () => {
1565
3010
  ratchetActiveSessionId: null,
1566
3011
  ratchetLastCiRunId: null,
1567
3012
  prReviewLastCheckedAt: null,
3013
+ ratchetDispatchOutcome: null,
3014
+ ratchetDispatchRetryCount: 0,
1568
3015
  };
1569
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
1570
3016
  const events = [];
1571
3017
  ratchetService.on(RATCHET_STATE_CHANGED, (event) => {
1572
3018
  events.push(event);
@@ -1589,8 +3035,9 @@ describe('ratchet service (state-change + idle dispatch)', () => {
1589
3035
  ratchetActiveSessionId: null,
1590
3036
  ratchetLastCiRunId: null,
1591
3037
  prReviewLastCheckedAt: null,
3038
+ ratchetDispatchOutcome: null,
3039
+ ratchetDispatchRetryCount: 0,
1592
3040
  };
1593
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
1594
3041
  const events = [];
1595
3042
  ratchetService.on(RATCHET_STATE_CHANGED, (event) => {
1596
3043
  events.push(event);
@@ -1608,6 +3055,8 @@ describe('ratchet service (state-change + idle dispatch)', () => {
1608
3055
  ratchetActiveSessionId: null,
1609
3056
  ratchetLastCiRunId: null,
1610
3057
  prReviewLastCheckedAt: null,
3058
+ ratchetDispatchOutcome: null,
3059
+ ratchetDispatchRetryCount: 0,
1611
3060
  };
1612
3061
  vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({
1613
3062
  ciStatus: CIStatus.FAILURE,
@@ -1618,8 +3067,7 @@ describe('ratchet service (state-change + idle dispatch)', () => {
1618
3067
  prState: 'OPEN',
1619
3068
  prNumber: 32,
1620
3069
  });
1621
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
1622
- vi.mocked(agentSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
3070
+ vi.mocked(mockSessionBridge.findSessionsByWorkspaceId).mockResolvedValue([]);
1623
3071
  vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
1624
3072
  status: 'started',
1625
3073
  sessionId: 'ratchet-session-32',
@@ -1648,6 +3096,8 @@ describe('ratchet service (state-change + idle dispatch)', () => {
1648
3096
  ratchetActiveSessionId: null,
1649
3097
  ratchetLastCiRunId: null,
1650
3098
  prReviewLastCheckedAt: null,
3099
+ ratchetDispatchOutcome: null,
3100
+ ratchetDispatchRetryCount: 0,
1651
3101
  };
1652
3102
  vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue({
1653
3103
  ciStatus: CIStatus.FAILURE,
@@ -1658,8 +3108,7 @@ describe('ratchet service (state-change + idle dispatch)', () => {
1658
3108
  prState: 'OPEN',
1659
3109
  prNumber: 33,
1660
3110
  });
1661
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
1662
- vi.mocked(agentSessionAccessor.findByWorkspaceId).mockResolvedValue([]);
3111
+ vi.mocked(mockSessionBridge.findSessionsByWorkspaceId).mockResolvedValue([]);
1663
3112
  vi.mocked(fixerSessionService.acquireAndDispatch).mockResolvedValue({
1664
3113
  status: 'started',
1665
3114
  sessionId: 'ratchet-session-33',
@@ -1682,9 +3131,10 @@ describe('ratchet service (state-change + idle dispatch)', () => {
1682
3131
  ratchetActiveSessionId: null,
1683
3132
  ratchetLastCiRunId: null,
1684
3133
  prReviewLastCheckedAt: null,
3134
+ ratchetDispatchOutcome: null,
3135
+ ratchetDispatchRetryCount: 0,
1685
3136
  };
1686
3137
  vi.spyOn(unsafeCoerce(ratchetService), 'fetchPRState').mockResolvedValue(null);
1687
- vi.mocked(workspaceAccessor.update).mockResolvedValue({});
1688
3138
  const events = [];
1689
3139
  ratchetService.on(RATCHET_STATE_CHANGED, (event) => {
1690
3140
  events.push(event);