@repo-os/repoos 0.5.45

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 (362) hide show
  1. package/LICENSE.md +110 -0
  2. package/README.md +272 -0
  3. package/dist/.build-info.json +4 -0
  4. package/dist/.build-stamp.json +3 -0
  5. package/dist/cli/colors.d.ts +13 -0
  6. package/dist/cli/colors.js +37 -0
  7. package/dist/cli/index.d.ts +2 -0
  8. package/dist/cli/index.js +221 -0
  9. package/dist/commands/check.d.ts +170 -0
  10. package/dist/commands/check.js +1227 -0
  11. package/dist/commands/docs.d.ts +2 -0
  12. package/dist/commands/docs.js +70 -0
  13. package/dist/commands/gc.d.ts +1 -0
  14. package/dist/commands/gc.js +57 -0
  15. package/dist/commands/init.d.ts +9 -0
  16. package/dist/commands/init.js +699 -0
  17. package/dist/commands/screenshots.d.ts +7 -0
  18. package/dist/commands/screenshots.js +77 -0
  19. package/dist/commands/serve.d.ts +55 -0
  20. package/dist/commands/serve.js +204 -0
  21. package/dist/commands/status.d.ts +140 -0
  22. package/dist/commands/status.js +569 -0
  23. package/dist/commands/stop.d.ts +1 -0
  24. package/dist/commands/stop.js +59 -0
  25. package/dist/commands/tasks.d.ts +47 -0
  26. package/dist/commands/tasks.js +334 -0
  27. package/dist/commands/tunnel.d.ts +1 -0
  28. package/dist/commands/tunnel.js +1271 -0
  29. package/dist/commands/ui-harness.d.ts +53 -0
  30. package/dist/commands/ui-harness.js +62 -0
  31. package/dist/commands/ui-smoke.d.ts +6 -0
  32. package/dist/commands/ui-smoke.js +220 -0
  33. package/dist/commands/upgrade.d.ts +15 -0
  34. package/dist/commands/upgrade.js +188 -0
  35. package/dist/core/auth-store.d.ts +81 -0
  36. package/dist/core/auth-store.js +530 -0
  37. package/dist/core/auth.d.ts +75 -0
  38. package/dist/core/auth.js +124 -0
  39. package/dist/core/bootstrap.d.ts +41 -0
  40. package/dist/core/bootstrap.js +236 -0
  41. package/dist/core/build.d.ts +92 -0
  42. package/dist/core/build.js +249 -0
  43. package/dist/core/canary.d.ts +10 -0
  44. package/dist/core/canary.js +14 -0
  45. package/dist/core/check-failure-summary.d.ts +10 -0
  46. package/dist/core/check-failure-summary.js +34 -0
  47. package/dist/core/config.d.ts +160 -0
  48. package/dist/core/config.js +1288 -0
  49. package/dist/core/context-pack.d.ts +74 -0
  50. package/dist/core/context-pack.js +778 -0
  51. package/dist/core/db.d.ts +236 -0
  52. package/dist/core/db.js +747 -0
  53. package/dist/core/detect.d.ts +69 -0
  54. package/dist/core/detect.js +251 -0
  55. package/dist/core/docs.d.ts +76 -0
  56. package/dist/core/docs.js +225 -0
  57. package/dist/core/frontmatter.d.ts +29 -0
  58. package/dist/core/frontmatter.js +215 -0
  59. package/dist/core/git.d.ts +505 -0
  60. package/dist/core/git.js +1453 -0
  61. package/dist/core/indexer.d.ts +33 -0
  62. package/dist/core/indexer.js +282 -0
  63. package/dist/core/input.d.ts +66 -0
  64. package/dist/core/input.js +222 -0
  65. package/dist/core/logger.d.ts +70 -0
  66. package/dist/core/logger.js +138 -0
  67. package/dist/core/models.d.ts +45 -0
  68. package/dist/core/models.js +291 -0
  69. package/dist/core/net-probe.d.ts +4 -0
  70. package/dist/core/net-probe.js +20 -0
  71. package/dist/core/providers/deepinfra.d.ts +23 -0
  72. package/dist/core/providers/deepinfra.js +44 -0
  73. package/dist/core/providers/index.d.ts +24 -0
  74. package/dist/core/providers/index.js +59 -0
  75. package/dist/core/providers/openrouter.d.ts +28 -0
  76. package/dist/core/providers/openrouter.js +82 -0
  77. package/dist/core/providers/spend.d.ts +86 -0
  78. package/dist/core/providers/spend.js +286 -0
  79. package/dist/core/providers/types.d.ts +56 -0
  80. package/dist/core/providers/types.js +39 -0
  81. package/dist/core/repoos.d.ts +59 -0
  82. package/dist/core/repoos.js +221 -0
  83. package/dist/core/runtime.d.ts +35 -0
  84. package/dist/core/runtime.js +167 -0
  85. package/dist/core/skills-registry.d.ts +39 -0
  86. package/dist/core/skills-registry.js +148 -0
  87. package/dist/core/skills-registry.test.d.ts +1 -0
  88. package/dist/core/skills-registry.test.js +52 -0
  89. package/dist/core/sysmem.d.ts +7 -0
  90. package/dist/core/sysmem.js +55 -0
  91. package/dist/core/tailscale.d.ts +2 -0
  92. package/dist/core/tailscale.js +19 -0
  93. package/dist/core/task.d.ts +53 -0
  94. package/dist/core/task.js +310 -0
  95. package/dist/core/toml-line.d.ts +20 -0
  96. package/dist/core/toml-line.js +52 -0
  97. package/dist/core/toml-validate.d.ts +44 -0
  98. package/dist/core/toml-validate.js +335 -0
  99. package/dist/core/tunnel-assistant.d.ts +30 -0
  100. package/dist/core/tunnel-assistant.js +65 -0
  101. package/dist/core/tunnel-registry.d.ts +96 -0
  102. package/dist/core/tunnel-registry.js +267 -0
  103. package/dist/core/tunnel-registry.test.d.ts +1 -0
  104. package/dist/core/tunnel-registry.test.js +309 -0
  105. package/dist/core/tunnel.d.ts +103 -0
  106. package/dist/core/tunnel.js +320 -0
  107. package/dist/core/types.d.ts +811 -0
  108. package/dist/core/types.js +27 -0
  109. package/dist/core/worktree-gc.d.ts +57 -0
  110. package/dist/core/worktree-gc.js +194 -0
  111. package/dist/server/__tests__/integration-job.test.d.ts +1 -0
  112. package/dist/server/__tests__/integration-job.test.js +129 -0
  113. package/dist/server/agents.d.ts +1020 -0
  114. package/dist/server/agents.js +4070 -0
  115. package/dist/server/attachments.d.ts +51 -0
  116. package/dist/server/attachments.js +130 -0
  117. package/dist/server/auto-engineering.d.ts +45 -0
  118. package/dist/server/auto-engineering.js +275 -0
  119. package/dist/server/branchless-release.d.ts +22 -0
  120. package/dist/server/branchless-release.js +67 -0
  121. package/dist/server/built-in-agents.d.ts +305 -0
  122. package/dist/server/built-in-agents.js +1838 -0
  123. package/dist/server/cto-monitor.d.ts +35 -0
  124. package/dist/server/cto-monitor.js +281 -0
  125. package/dist/server/cto.d.ts +125 -0
  126. package/dist/server/cto.js +590 -0
  127. package/dist/server/deployments.d.ts +119 -0
  128. package/dist/server/deployments.js +325 -0
  129. package/dist/server/diff-snapshot.d.ts +20 -0
  130. package/dist/server/diff-snapshot.js +73 -0
  131. package/dist/server/done.d.ts +160 -0
  132. package/dist/server/done.js +488 -0
  133. package/dist/server/freeform.d.ts +55 -0
  134. package/dist/server/freeform.js +179 -0
  135. package/dist/server/handoff.d.ts +99 -0
  136. package/dist/server/handoff.js +575 -0
  137. package/dist/server/hetzner.d.ts +45 -0
  138. package/dist/server/hetzner.js +117 -0
  139. package/dist/server/icons.d.ts +8 -0
  140. package/dist/server/icons.js +195 -0
  141. package/dist/server/integration-job.d.ts +75 -0
  142. package/dist/server/integration-job.js +144 -0
  143. package/dist/server/integration-orchestrator.d.ts +211 -0
  144. package/dist/server/integration-orchestrator.js +1299 -0
  145. package/dist/server/integration-status.d.ts +48 -0
  146. package/dist/server/integration-status.js +71 -0
  147. package/dist/server/live-index.d.ts +295 -0
  148. package/dist/server/live-index.js +452 -0
  149. package/dist/server/model-test.d.ts +20 -0
  150. package/dist/server/model-test.js +100 -0
  151. package/dist/server/ntfy.d.ts +57 -0
  152. package/dist/server/ntfy.js +112 -0
  153. package/dist/server/pm-attachments.d.ts +35 -0
  154. package/dist/server/pm-attachments.js +181 -0
  155. package/dist/server/pm-runs.d.ts +58 -0
  156. package/dist/server/pm-runs.js +80 -0
  157. package/dist/server/preview.d.ts +174 -0
  158. package/dist/server/preview.js +641 -0
  159. package/dist/server/release.d.ts +88 -0
  160. package/dist/server/release.js +449 -0
  161. package/dist/server/reload.d.ts +194 -0
  162. package/dist/server/reload.js +586 -0
  163. package/dist/server/remote-validation.d.ts +132 -0
  164. package/dist/server/remote-validation.js +478 -0
  165. package/dist/server/repo-lock.d.ts +36 -0
  166. package/dist/server/repo-lock.js +141 -0
  167. package/dist/server/review-guard.d.ts +14 -0
  168. package/dist/server/review-guard.js +159 -0
  169. package/dist/server/review.d.ts +243 -0
  170. package/dist/server/review.js +1211 -0
  171. package/dist/server/routes/agents.d.ts +6 -0
  172. package/dist/server/routes/agents.js +28 -0
  173. package/dist/server/routes/auth.d.ts +60 -0
  174. package/dist/server/routes/auth.js +755 -0
  175. package/dist/server/routes/config.d.ts +20 -0
  176. package/dist/server/routes/config.js +347 -0
  177. package/dist/server/routes/debugger.d.ts +33 -0
  178. package/dist/server/routes/debugger.js +360 -0
  179. package/dist/server/routes/deployments.d.ts +3 -0
  180. package/dist/server/routes/deployments.js +16 -0
  181. package/dist/server/routes/docs.d.ts +8 -0
  182. package/dist/server/routes/docs.js +135 -0
  183. package/dist/server/routes/helpers.d.ts +21 -0
  184. package/dist/server/routes/helpers.js +171 -0
  185. package/dist/server/routes/index.d.ts +19 -0
  186. package/dist/server/routes/index.js +18 -0
  187. package/dist/server/routes/info.d.ts +17 -0
  188. package/dist/server/routes/info.js +177 -0
  189. package/dist/server/routes/inputs.d.ts +7 -0
  190. package/dist/server/routes/inputs.js +125 -0
  191. package/dist/server/routes/model-providers.d.ts +27 -0
  192. package/dist/server/routes/model-providers.js +94 -0
  193. package/dist/server/routes/models.d.ts +3 -0
  194. package/dist/server/routes/models.js +36 -0
  195. package/dist/server/routes/notify.d.ts +2 -0
  196. package/dist/server/routes/notify.js +17 -0
  197. package/dist/server/routes/playground.d.ts +29 -0
  198. package/dist/server/routes/playground.js +81 -0
  199. package/dist/server/routes/release.d.ts +20 -0
  200. package/dist/server/routes/release.js +86 -0
  201. package/dist/server/routes/router.d.ts +12 -0
  202. package/dist/server/routes/router.js +32 -0
  203. package/dist/server/routes/skill-registry.d.ts +5 -0
  204. package/dist/server/routes/skill-registry.js +51 -0
  205. package/dist/server/routes/tasks.d.ts +58 -0
  206. package/dist/server/routes/tasks.js +1442 -0
  207. package/dist/server/routes/transcribe.d.ts +2 -0
  208. package/dist/server/routes/transcribe.js +124 -0
  209. package/dist/server/routes/types.d.ts +68 -0
  210. package/dist/server/routes/types.js +1 -0
  211. package/dist/server/routes/ui.d.ts +5 -0
  212. package/dist/server/routes/ui.js +81 -0
  213. package/dist/server/routes/utils.d.ts +3 -0
  214. package/dist/server/routes/utils.js +23 -0
  215. package/dist/server/serve-reaper.d.ts +135 -0
  216. package/dist/server/serve-reaper.js +491 -0
  217. package/dist/server/server.d.ts +49 -0
  218. package/dist/server/server.js +2171 -0
  219. package/dist/server/supervisor.d.ts +89 -0
  220. package/dist/server/supervisor.js +231 -0
  221. package/dist/server/system.d.ts +198 -0
  222. package/dist/server/system.js +480 -0
  223. package/dist/server/task-check.d.ts +45 -0
  224. package/dist/server/task-check.js +71 -0
  225. package/dist/server/task-transitions.d.ts +51 -0
  226. package/dist/server/task-transitions.js +78 -0
  227. package/dist/server/task-watchdog.d.ts +158 -0
  228. package/dist/server/task-watchdog.js +681 -0
  229. package/dist/server/test-run.d.ts +28 -0
  230. package/dist/server/test-run.js +81 -0
  231. package/dist/server/watcher.d.ts +21 -0
  232. package/dist/server/watcher.js +194 -0
  233. package/dist/server/write.d.ts +94 -0
  234. package/dist/server/write.js +345 -0
  235. package/dist/ui/assets/AgentsView-CF4wRvHh.css +1 -0
  236. package/dist/ui/assets/AgentsView-D-myL-kA.js +1 -0
  237. package/dist/ui/assets/ContextView-D3TC-RSp.css +1 -0
  238. package/dist/ui/assets/ContextView-DsOqAdyE.js +4 -0
  239. package/dist/ui/assets/DashboardView-BOhsIJqO.js +1 -0
  240. package/dist/ui/assets/DashboardView-Ck9o0RP2.css +1 -0
  241. package/dist/ui/assets/DeploymentsView--iIUVfEH.css +1 -0
  242. package/dist/ui/assets/DeploymentsView-DXu_FYR2.js +1 -0
  243. package/dist/ui/assets/InputsView-24HVbVPk.js +1 -0
  244. package/dist/ui/assets/InputsView-BLb7GHtx.css +1 -0
  245. package/dist/ui/assets/LoginView-6QlGg6cx.css +1 -0
  246. package/dist/ui/assets/LoginView-BqupI0Lt.js +1 -0
  247. package/dist/ui/assets/ReleasesView-BEG1A0YH.css +1 -0
  248. package/dist/ui/assets/ReleasesView-CAgMuxpe.js +3 -0
  249. package/dist/ui/assets/SettingsView-CA-NYSMK.css +1 -0
  250. package/dist/ui/assets/SettingsView-O3iZSnjg.js +3 -0
  251. package/dist/ui/assets/WorkView-D8Tl6qwy.js +1 -0
  252. package/dist/ui/assets/WorkView-DMYL2s3W.css +1 -0
  253. package/dist/ui/assets/abnfDiagram-VCTEODGH-J5lkSnEE.js +1 -0
  254. package/dist/ui/assets/arc-BHZnmhhM.js +1 -0
  255. package/dist/ui/assets/architecture-7GRP2DOG-pLdFt-xS.js +1 -0
  256. package/dist/ui/assets/architectureDiagram-5GKGNRK7-C3k89uKX.js +36 -0
  257. package/dist/ui/assets/array-BifhSqXX.js +1 -0
  258. package/dist/ui/assets/blockDiagram-I7D4REHJ-AWNoBzop.js +129 -0
  259. package/dist/ui/assets/button-lbikqSph.js +1 -0
  260. package/dist/ui/assets/c4Diagram-7LVT6UL2-DKG1D5gd.js +38 -0
  261. package/dist/ui/assets/channel-gA4qBv3A.js +1 -0
  262. package/dist/ui/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  263. package/dist/ui/assets/chunk-4HAMMTFA-D1YR8lF_.js +62 -0
  264. package/dist/ui/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  265. package/dist/ui/assets/chunk-75Z2AOVW-5GuBxfsL.js +2 -0
  266. package/dist/ui/assets/chunk-DU6HZSFF-DfdNNqwa.js +127 -0
  267. package/dist/ui/assets/chunk-F27PBJKO-BPZzinQ4.js +1 -0
  268. package/dist/ui/assets/chunk-FOHPRMQF-l2z4K2jD.js +161 -0
  269. package/dist/ui/assets/chunk-GMAD6QVW-DGWvSKj5.js +72 -0
  270. package/dist/ui/assets/chunk-GVQU2GXP-D6Aw75wJ.js +1 -0
  271. package/dist/ui/assets/chunk-IMKFNOWR-B8mMBKCW.js +231 -0
  272. package/dist/ui/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  273. package/dist/ui/assets/chunk-L3NEJ4N5-D5OJuXYE.js +1 -0
  274. package/dist/ui/assets/chunk-OSK3NFVY-B7r9w2UW.js +10 -0
  275. package/dist/ui/assets/chunk-P2QGCYS3-C74X4AQX.js +1 -0
  276. package/dist/ui/assets/chunk-POPQ4Y6H-C5_u1gUS.js +1 -0
  277. package/dist/ui/assets/chunk-PWAF6VOD-DfoTLtxn.js +1 -0
  278. package/dist/ui/assets/chunk-SHT3W25Y-D40m2eqz.js +168 -0
  279. package/dist/ui/assets/chunk-SVP7TREG-euxi0Kap.js +88 -0
  280. package/dist/ui/assets/chunk-TICWLB2K-fkdxngzN.js +206 -0
  281. package/dist/ui/assets/chunk-XXDRQBXY-DFBUG-OT.js +1 -0
  282. package/dist/ui/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  283. package/dist/ui/assets/classDiagram-ZZMXUADV-BzW8bq2P.js +1 -0
  284. package/dist/ui/assets/classDiagram-v2-VYDZK3BY-BzW8bq2P.js +1 -0
  285. package/dist/ui/assets/cose-bilkent-JH36ORCC-BTcgMon1.js +1 -0
  286. package/dist/ui/assets/cynefin-OW5HDTMX-B5m_COsS.js +1 -0
  287. package/dist/ui/assets/cynefinDiagram-5FMLGOSQ-C-AG6CGe.js +62 -0
  288. package/dist/ui/assets/cytoscape.esm-FBntH-9t.js +321 -0
  289. package/dist/ui/assets/dagre-GXQ25YYZ-DDfDnp8L.js +4 -0
  290. package/dist/ui/assets/dagre-PrKaheQc.js +1 -0
  291. package/dist/ui/assets/defaultLocale-BFoDCU3G.js +1 -0
  292. package/dist/ui/assets/diagram-S7CK7UJ4-DjQZAlEY.js +30 -0
  293. package/dist/ui/assets/diagram-UQ7AKVKN-DAZ9yp1B.js +41 -0
  294. package/dist/ui/assets/diagram-VSXAHHWV-BWepUKJD.js +3 -0
  295. package/dist/ui/assets/diagram-VX7I27RA-EcV1J81b.js +24 -0
  296. package/dist/ui/assets/diagram-Z3DM3KII-Dbz7XU_K.js +24 -0
  297. package/dist/ui/assets/dist-DTg6UBE_.js +1 -0
  298. package/dist/ui/assets/ebnfDiagram-PWID7BFC-BUyQw8Mm.js +1 -0
  299. package/dist/ui/assets/erDiagram-RLTQ6QDP-WVf8ukKt.js +99 -0
  300. package/dist/ui/assets/eventmodeling-NTZA5JFV-DHqyLOAn.js +1 -0
  301. package/dist/ui/assets/flowDiagram-HODETNUW-CYk_ze5K.js +1 -0
  302. package/dist/ui/assets/ganttDiagram-EL5Y4UJY-rB4MZuB-.js +292 -0
  303. package/dist/ui/assets/gitGraph-4MIJSDKK-BOTWM8dY.js +1 -0
  304. package/dist/ui/assets/gitGraphDiagram-WWUBYQGX-eYscuhtP.js +106 -0
  305. package/dist/ui/assets/graphlib-DS17s2tU.js +1 -0
  306. package/dist/ui/assets/index-BACBfnga.css +1 -0
  307. package/dist/ui/assets/index-BOQ-ust6.js +26 -0
  308. package/dist/ui/assets/info-A6RAGUB7-_67wOORe.js +1 -0
  309. package/dist/ui/assets/infoDiagram-27XIBGKW-XVXbddTA.js +2 -0
  310. package/dist/ui/assets/init-C-OQMol4.js +1 -0
  311. package/dist/ui/assets/ishikawaDiagram-5VMMS53U-ClLNiEru.js +70 -0
  312. package/dist/ui/assets/journeyDiagram-3NMN7TZE-NRawjR6i.js +139 -0
  313. package/dist/ui/assets/kanban-definition-UXKFOSKX-BgSzBfIx.js +89 -0
  314. package/dist/ui/assets/katex-DolUETbr.js +257 -0
  315. package/dist/ui/assets/line-KAyfuul4.js +1 -0
  316. package/dist/ui/assets/linear-cCCPOpu7.js +1 -0
  317. package/dist/ui/assets/mermaid-parser.core-CgXZk1iN.js +7 -0
  318. package/dist/ui/assets/mermaid.core-BUUkChHW.js +44 -0
  319. package/dist/ui/assets/mindmap-definition-YA3MSWOX-Cpn7vW4r.js +96 -0
  320. package/dist/ui/assets/ordinal-BDEzSJ7C.js +1 -0
  321. package/dist/ui/assets/packet-AYTQ26CC-0lZwM-d6.js +1 -0
  322. package/dist/ui/assets/path-COt_16Va.js +1 -0
  323. package/dist/ui/assets/pegDiagram-XKGWAZYB-impy0inn.js +1 -0
  324. package/dist/ui/assets/pie-WAS4IAKB-CqfdGHYg.js +1 -0
  325. package/dist/ui/assets/pieDiagram-E7YTZNPT-CxjL4I1h.js +39 -0
  326. package/dist/ui/assets/quadrantDiagram-AXDQQJYC-Cjd1QMIe.js +7 -0
  327. package/dist/ui/assets/radar-RG4KPBEZ-CRkQIwiv.js +1 -0
  328. package/dist/ui/assets/railroad-74A4TZTK-i67XSWTB.js +1 -0
  329. package/dist/ui/assets/railroad-abnf-HS5TGJTU-Cv9m5aS3.js +1 -0
  330. package/dist/ui/assets/railroad-ebnf-LZEXJU2U-C5ZIn-Jh.js +1 -0
  331. package/dist/ui/assets/railroad-peg-WCYAUIDC-CGra9Oq-.js +1 -0
  332. package/dist/ui/assets/railroadDiagram-O6MQD6OU-_pK0qyaY.js +1 -0
  333. package/dist/ui/assets/releases-B0WNV-SY.js +1 -0
  334. package/dist/ui/assets/repoos-cto-square.webp +0 -0
  335. package/dist/ui/assets/repoos-helpful-pm-square.webp +0 -0
  336. package/dist/ui/assets/repoos-orchestrator-square.webp +0 -0
  337. package/dist/ui/assets/repoos-performance-agent-square.webp +0 -0
  338. package/dist/ui/assets/repoos-ross-from-friends-square.webp +0 -0
  339. package/dist/ui/assets/repoos-tech-debt-agent-square.webp +0 -0
  340. package/dist/ui/assets/requirementDiagram-BXWQKSXE-CRrFX-yw.js +84 -0
  341. package/dist/ui/assets/rough.esm-By172zw-.js +1 -0
  342. package/dist/ui/assets/sankeyDiagram-P5KCCOFB-X_xFRj9E.js +40 -0
  343. package/dist/ui/assets/sequenceDiagram-WJ2MYXX4-Byb49lKf.js +162 -0
  344. package/dist/ui/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
  345. package/dist/ui/assets/src-BH-TyZbA.js +1 -0
  346. package/dist/ui/assets/stateDiagram-D77RDMKH-CzaPrqYF.js +1 -0
  347. package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-BCXckz7K.js +1 -0
  348. package/dist/ui/assets/swimlanes-42K2YHIH-Cx_1lnqr.js +1 -0
  349. package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-54qsKi6A.js +8 -0
  350. package/dist/ui/assets/timeline-definition-24CTP7MA-B1IbZPQ-.js +120 -0
  351. package/dist/ui/assets/treeView-Q6P3EWNA-DFWhVE3d.js +1 -0
  352. package/dist/ui/assets/treemap-WGGIJYW6-BmbYPThn.js +1 -0
  353. package/dist/ui/assets/vendor-ui-DPm7zLxi.js +3 -0
  354. package/dist/ui/assets/vendor-vue-CobgyEJp.js +1 -0
  355. package/dist/ui/assets/vennDiagram-4TSXK5OY-Ae2AtRpt.js +34 -0
  356. package/dist/ui/assets/wardley-WFR3VGLG-BtD4X4tL.js +1 -0
  357. package/dist/ui/assets/wardleyDiagram-VM6X3IG4-B3YN2fTd.js +78 -0
  358. package/dist/ui/assets/xychartDiagram-S5SC5T6Z-CIzJfMKA.js +7 -0
  359. package/dist/ui/favicon.svg +11 -0
  360. package/dist/ui/index.html +49 -0
  361. package/dist/ui/sw.js +48 -0
  362. package/package.json +91 -0
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Remote Validation Runner (#RVR).
3
+ *
4
+ * The close-out gate's expensive half — `bun install` + `bun run build` +
5
+ * `bun run test` — is what makes MTD fail on the developer's machine under
6
+ * memory pressure (see docs/remote-validation.md, memory
7
+ * `repoos-check-flakes-under-memory-pressure`). This module runs that half on a
8
+ * disposable Hetzner VM instead:
9
+ *
10
+ * 1. ensure a runner VM exists (create from a prebuilt snapshot, or reuse a
11
+ * still-warm one), one at a time, never more than one
12
+ * 2. `git bundle` the already-merged candidate tree and scp it up
13
+ * 3. ssh in and run `/opt/repoos/validate.sh` — which checks out the exact
14
+ * candidate SHA and runs build + test inside the `repoos-ci` Docker image
15
+ * 4. stream combined output back to a per-task log file and the caller
16
+ * 5. pull any artifacts, then arm an idle-shutdown timer
17
+ *
18
+ * The result is shaped as a {@link CheckSummary} so it is a drop-in for
19
+ * `runCloseOutCheck` in the two close-out paths (done.ts, integration-
20
+ * orchestrator.ts). Infra failures (provisioning, ssh) come back with
21
+ * `transient: true` — the caller decides whether to fail retryably or fall
22
+ * back to a local run (`remoteValidation.fallbackToLocal`).
23
+ */
24
+ import type { RepoOSConfig } from "../core/types.js";
25
+ import type { Logger } from "../core/logger.js";
26
+ import type { CheckSummary } from "./done.js";
27
+ export type { CheckSummary } from "./done.js";
28
+ import { type HetznerClient } from "./hetzner.js";
29
+ export interface RemoteHost {
30
+ ip: string;
31
+ user: string;
32
+ /** Path to the private key matching the Hetzner-registered public key. */
33
+ keyPath: string;
34
+ }
35
+ export interface RemoteExecResult {
36
+ code: number | null;
37
+ output: string;
38
+ timedOut: boolean;
39
+ }
40
+ /**
41
+ * The IO surface of the runner, injected so tests can substitute fakes and
42
+ * never touch a real API, network, or subprocess.
43
+ */
44
+ export interface RemoteExecDeps {
45
+ /** `git bundle create <outPath> HEAD` in `cwd`. */
46
+ bundleRepo(cwd: string, outPath: string): Promise<{
47
+ ok: boolean;
48
+ detail?: string;
49
+ }>;
50
+ /** `scp <localPath> <host>:<remotePath>`. */
51
+ uploadFile(host: RemoteHost, localPath: string, remotePath: string): Promise<{
52
+ ok: boolean;
53
+ detail?: string;
54
+ }>;
55
+ /** `scp -r <host>:<remotePath> <localDir>` — best effort, never throws. */
56
+ downloadDir(host: RemoteHost, remotePath: string, localDir: string): Promise<void>;
57
+ /** `ssh <host> <command>`, streaming combined stdout+stderr through `onChunk`. */
58
+ runRemote(host: RemoteHost, command: string, onChunk: (chunk: string) => void, timeoutMs: number): Promise<RemoteExecResult>;
59
+ /** TCP connect probe (used to wait for sshd to come up). */
60
+ probeTcp(ip: string, port: number, timeoutMs: number): Promise<boolean>;
61
+ }
62
+ export interface ValidateOptions {
63
+ taskId: string;
64
+ /** The already-merged candidate worktree (or task worktree). */
65
+ worktreePath: string;
66
+ /** Expected `git rev-parse HEAD` of `worktreePath` — the VM asserts it matches. */
67
+ candidateSha: string;
68
+ /** Live output sink (SSE, status bar). The per-task log file is always written. */
69
+ onChunk?: (chunk: string) => void;
70
+ }
71
+ export interface RemoteValidator {
72
+ validate(opts: ValidateOptions): Promise<CheckSummary>;
73
+ /** Delete leaked runner VMs. Call at server boot (nothing is validating then). */
74
+ reconcile(): Promise<void>;
75
+ /** Tear down any warm VM and cancel timers. Call on server shutdown. */
76
+ dispose(): Promise<void>;
77
+ /** Absolute path of the per-task log file (may not exist yet). */
78
+ logPath(taskId: string): string;
79
+ }
80
+ /** Tunable time budgets, overridable in tests so the polling loops don't sleep for real. */
81
+ export interface RunnerTimings {
82
+ provisionPollMs: number;
83
+ provisionTimeoutMs: number;
84
+ sshProbeIntervalMs: number;
85
+ sshWaitTimeoutMs: number;
86
+ /** Outer cap on the remote build+test run. Vitest's own testTimeout fails a real hang faster. */
87
+ remoteRunTimeoutMs: number;
88
+ }
89
+ export declare function defaultRemoteExec(): RemoteExecDeps;
90
+ export declare class RemoteValidationRunner implements RemoteValidator {
91
+ private readonly config;
92
+ private readonly logger?;
93
+ private readonly hetzner;
94
+ private readonly exec;
95
+ private readonly timings;
96
+ private readonly keyPath;
97
+ private readonly sshUser;
98
+ private state;
99
+ /** In-flight provisioning, so concurrent validate() calls share one VM. */
100
+ private provisioning;
101
+ private idleTimer;
102
+ private lifetimeTimer;
103
+ private activeJobs;
104
+ constructor(config: RepoOSConfig, logger?: Logger | undefined, deps?: {
105
+ hetzner?: HetznerClient;
106
+ exec?: RemoteExecDeps;
107
+ timings?: Partial<RunnerTimings>;
108
+ });
109
+ logPath(taskId: string): string;
110
+ private stateFile;
111
+ private loadState;
112
+ private saveState;
113
+ private appendLog;
114
+ private maxLifetimeMs;
115
+ private idleMs;
116
+ /** Infra failure → transient CheckSummary the caller can retry or fall back on. */
117
+ private infraFail;
118
+ validate(opts: ValidateOptions): Promise<CheckSummary>;
119
+ /** Reuse a warm VM, or provision one. Serialised: never creates two. */
120
+ private ensureRunner;
121
+ private doEnsureRunner;
122
+ private waitForRunning;
123
+ private waitForSsh;
124
+ /** Delete every labelled runner VM except the one we currently track. */
125
+ private deleteLeaked;
126
+ reconcile(): Promise<void>;
127
+ dispose(): Promise<void>;
128
+ private armIdleTimer;
129
+ private clearIdleTimer;
130
+ private armLifetimeTimer;
131
+ private teardown;
132
+ }
@@ -0,0 +1,478 @@
1
+ /**
2
+ * Remote Validation Runner (#RVR).
3
+ *
4
+ * The close-out gate's expensive half — `bun install` + `bun run build` +
5
+ * `bun run test` — is what makes MTD fail on the developer's machine under
6
+ * memory pressure (see docs/remote-validation.md, memory
7
+ * `repoos-check-flakes-under-memory-pressure`). This module runs that half on a
8
+ * disposable Hetzner VM instead:
9
+ *
10
+ * 1. ensure a runner VM exists (create from a prebuilt snapshot, or reuse a
11
+ * still-warm one), one at a time, never more than one
12
+ * 2. `git bundle` the already-merged candidate tree and scp it up
13
+ * 3. ssh in and run `/opt/repoos/validate.sh` — which checks out the exact
14
+ * candidate SHA and runs build + test inside the `repoos-ci` Docker image
15
+ * 4. stream combined output back to a per-task log file and the caller
16
+ * 5. pull any artifacts, then arm an idle-shutdown timer
17
+ *
18
+ * The result is shaped as a {@link CheckSummary} so it is a drop-in for
19
+ * `runCloseOutCheck` in the two close-out paths (done.ts, integration-
20
+ * orchestrator.ts). Infra failures (provisioning, ssh) come back with
21
+ * `transient: true` — the caller decides whether to fail retryably or fall
22
+ * back to a local run (`remoteValidation.fallbackToLocal`).
23
+ */
24
+ import { spawn } from "node:child_process";
25
+ import { createConnection } from "node:net";
26
+ import { mkdtempSync, mkdirSync, rmSync, appendFileSync, existsSync, readFileSync, writeFileSync, } from "node:fs";
27
+ import { join } from "node:path";
28
+ import { tmpdir } from "node:os";
29
+ import { redactSecrets, stripAnsi } from "./done.js";
30
+ import { createHetznerClient } from "./hetzner.js";
31
+ /** Label every runner VM carries, so leaked ones are always findable. */
32
+ const RUNNER_LABEL_KEY = "repoos-ci";
33
+ const RUNNER_LABEL_SELECTOR = `${RUNNER_LABEL_KEY}=1`;
34
+ const SSH_PORT = 22;
35
+ const DEFAULT_TIMINGS = {
36
+ provisionPollMs: 4_000,
37
+ provisionTimeoutMs: 180_000,
38
+ sshProbeIntervalMs: 3_000,
39
+ sshWaitTimeoutMs: 120_000,
40
+ remoteRunTimeoutMs: 25 * 60_000,
41
+ };
42
+ /** Contention-shaped failure text — matches runDoneStep's heuristic in done.ts. */
43
+ function looksTransient(output) {
44
+ return /(?:timed out waiting for|test timed out|worker .*?(?:timeout|exited)|ETIMEDOUT|Killed|out of memory|Cannot allocate memory)/i.test(output);
45
+ }
46
+ /** Last few non-empty lines, redacted and de-ANSI'd, bounded. */
47
+ function tail(output, lines = 20, maxChars = 1200) {
48
+ const cleaned = redactSecrets(stripAnsi(output))
49
+ .split("\n")
50
+ .map((l) => l.trimEnd())
51
+ .filter((l) => l.trim());
52
+ let out = cleaned.slice(-lines).join("\n");
53
+ if (out.length > maxChars)
54
+ out = `…${out.slice(out.length - maxChars)}`;
55
+ return out || "no output";
56
+ }
57
+ // ── default IO implementation ────────────────────────────────────────────────
58
+ function sshArgs(host) {
59
+ return [
60
+ "-i",
61
+ host.keyPath,
62
+ "-o",
63
+ "BatchMode=yes",
64
+ "-o",
65
+ "StrictHostKeyChecking=accept-new",
66
+ "-o",
67
+ "ConnectTimeout=15",
68
+ "-o",
69
+ "UserKnownHostsFile=/dev/null",
70
+ "-o",
71
+ "LogLevel=ERROR",
72
+ ];
73
+ }
74
+ function runLocal(cmd, args, opts) {
75
+ return new Promise((resolve) => {
76
+ const child = spawn(cmd, args, { cwd: opts.cwd });
77
+ let output = "";
78
+ let timedOut = false;
79
+ let settled = false;
80
+ const done = (code) => {
81
+ if (settled)
82
+ return;
83
+ settled = true;
84
+ clearTimeout(timer);
85
+ resolve({ code, output, timedOut });
86
+ };
87
+ const onData = (b) => {
88
+ const s = b.toString("utf8");
89
+ output += s;
90
+ opts.onChunk?.(s);
91
+ };
92
+ child.stdout?.on("data", onData);
93
+ child.stderr?.on("data", onData);
94
+ child.on("error", (e) => {
95
+ output += `\n[could not spawn ${cmd}: ${e.message}]\n`;
96
+ done(null);
97
+ });
98
+ child.on("close", (code) => done(code));
99
+ const timer = setTimeout(() => {
100
+ timedOut = true;
101
+ child.kill("SIGKILL");
102
+ }, opts.timeoutMs);
103
+ });
104
+ }
105
+ export function defaultRemoteExec() {
106
+ return {
107
+ async bundleRepo(cwd, outPath) {
108
+ const res = await runLocal("git", ["bundle", "create", outPath, "HEAD"], {
109
+ cwd,
110
+ timeoutMs: 120_000,
111
+ });
112
+ return res.code === 0 ? { ok: true } : { ok: false, detail: tail(res.output) };
113
+ },
114
+ async uploadFile(host, localPath, remotePath) {
115
+ const res = await runLocal("scp", [...sshArgs(host), localPath, `${host.user}@${host.ip}:${remotePath}`], { timeoutMs: 120_000 });
116
+ return res.code === 0 ? { ok: true } : { ok: false, detail: tail(res.output) };
117
+ },
118
+ async downloadDir(host, remotePath, localDir) {
119
+ mkdirSync(localDir, { recursive: true });
120
+ await runLocal("scp", [...sshArgs(host), "-r", `${host.user}@${host.ip}:${remotePath}`, localDir], { timeoutMs: 120_000 }).catch(() => undefined);
121
+ },
122
+ async runRemote(host, command, onChunk, timeoutMs) {
123
+ return runLocal("ssh", [...sshArgs(host), `${host.user}@${host.ip}`, command], {
124
+ timeoutMs,
125
+ onChunk,
126
+ });
127
+ },
128
+ probeTcp(ip, port, timeoutMs) {
129
+ return new Promise((resolve) => {
130
+ const socket = createConnection({ host: ip, port });
131
+ const done = (ok) => {
132
+ socket.destroy();
133
+ resolve(ok);
134
+ };
135
+ socket.setTimeout(timeoutMs);
136
+ socket.once("connect", () => done(true));
137
+ socket.once("timeout", () => done(false));
138
+ socket.once("error", () => done(false));
139
+ });
140
+ },
141
+ };
142
+ }
143
+ // ── the runner ──────────────────────────────────────────────────────────────
144
+ export class RemoteValidationRunner {
145
+ config;
146
+ logger;
147
+ hetzner;
148
+ exec;
149
+ timings;
150
+ keyPath;
151
+ sshUser = "root";
152
+ state = null;
153
+ /** In-flight provisioning, so concurrent validate() calls share one VM. */
154
+ provisioning = null;
155
+ idleTimer = null;
156
+ lifetimeTimer = null;
157
+ activeJobs = 0;
158
+ constructor(config, logger, deps) {
159
+ this.config = config;
160
+ this.logger = logger;
161
+ const token = process.env.HETZNER_API_TOKEN ?? "";
162
+ this.hetzner = deps?.hetzner ?? createHetznerClient(token);
163
+ this.exec = deps?.exec ?? defaultRemoteExec();
164
+ this.timings = { ...DEFAULT_TIMINGS, ...deps?.timings };
165
+ this.keyPath = process.env.REPOOS_REMOTE_SSH_KEY ?? "";
166
+ this.loadState();
167
+ }
168
+ logPath(taskId) {
169
+ return join(this.config.root, ".repoos", "logs", "remote-validation", `${taskId}.log`);
170
+ }
171
+ stateFile() {
172
+ return join(this.config.root, ".repoos", "remote-runner.json");
173
+ }
174
+ loadState() {
175
+ try {
176
+ const raw = readFileSync(this.stateFile(), "utf8");
177
+ const parsed = JSON.parse(raw);
178
+ if (parsed && typeof parsed.serverId === "number")
179
+ this.state = parsed;
180
+ }
181
+ catch {
182
+ this.state = null;
183
+ }
184
+ }
185
+ saveState() {
186
+ try {
187
+ mkdirSync(join(this.config.root, ".repoos"), { recursive: true });
188
+ if (this.state)
189
+ writeFileSync(this.stateFile(), JSON.stringify(this.state, null, 2));
190
+ else if (existsSync(this.stateFile()))
191
+ rmSync(this.stateFile(), { force: true });
192
+ }
193
+ catch {
194
+ /* state file is a convenience, not a source of truth */
195
+ }
196
+ }
197
+ appendLog(taskId, text) {
198
+ try {
199
+ const p = this.logPath(taskId);
200
+ mkdirSync(join(this.config.root, ".repoos", "logs", "remote-validation"), {
201
+ recursive: true,
202
+ });
203
+ appendFileSync(p, redactSecrets(text));
204
+ }
205
+ catch {
206
+ /* best effort */
207
+ }
208
+ }
209
+ maxLifetimeMs() {
210
+ return (this.config.remoteValidation?.maxServerLifetimeMinutes ?? 120) * 60_000;
211
+ }
212
+ idleMs() {
213
+ return (this.config.remoteValidation?.idleShutdownMinutes ?? 8) * 60_000;
214
+ }
215
+ /** Infra failure → transient CheckSummary the caller can retry or fall back on. */
216
+ infraFail(detail) {
217
+ this.logger?.system("warn", `remote validation unavailable: ${detail}`);
218
+ return {
219
+ ok: false,
220
+ stage: "check",
221
+ transient: true,
222
+ detail: `remote validation unavailable: ${detail}`,
223
+ };
224
+ }
225
+ async validate(opts) {
226
+ const rv = this.config.remoteValidation ?? {};
227
+ if (!rv.enabled)
228
+ return this.infraFail("remote validation is disabled");
229
+ if (!process.env.HETZNER_API_TOKEN)
230
+ return this.infraFail("HETZNER_API_TOKEN is not set");
231
+ if (!this.keyPath || !existsSync(this.keyPath)) {
232
+ return this.infraFail("REPOOS_REMOTE_SSH_KEY is not set or the key file is missing");
233
+ }
234
+ if (!rv.snapshotId)
235
+ return this.infraFail("remoteValidation.snapshotId is not configured");
236
+ if (!rv.sshKeyName)
237
+ return this.infraFail("remoteValidation.sshKeyName is not configured");
238
+ this.activeJobs++;
239
+ this.clearIdleTimer();
240
+ const startedAt = Date.now();
241
+ const emit = (s) => {
242
+ this.appendLog(opts.taskId, s);
243
+ opts.onChunk?.(s);
244
+ };
245
+ emit(`\n── remote validation for #${opts.taskId} @ ${opts.candidateSha.slice(0, 12)} ──\n`);
246
+ let tmp = null;
247
+ try {
248
+ const host = await this.ensureRunner();
249
+ emit(`[runner ${host.ip} ready in ${Math.round((Date.now() - startedAt) / 1000)}s]\n`);
250
+ // 1. bundle the candidate tree
251
+ tmp = mkdtempSync(join(tmpdir(), "repoos-rvr-"));
252
+ const bundlePath = join(tmp, "candidate.bundle");
253
+ const bundle = await this.exec.bundleRepo(opts.worktreePath, bundlePath);
254
+ if (!bundle.ok)
255
+ return this.infraFail(`git bundle failed: ${bundle.detail ?? "unknown"}`);
256
+ // 2. upload
257
+ const remoteBundle = `/tmp/repoos-${opts.taskId}.bundle`;
258
+ const up = await this.exec.uploadFile(host, bundlePath, remoteBundle);
259
+ if (!up.ok)
260
+ return this.infraFail(`scp of candidate bundle failed: ${up.detail ?? "unknown"}`);
261
+ // 3. run build + test inside the container
262
+ emit(`[running build + test on ${host.ip}]\n`);
263
+ const cmd = `/opt/repoos/validate.sh ${remoteBundle} ${opts.candidateSha}`;
264
+ const run = await this.exec.runRemote(host, cmd, emit, this.timings.remoteRunTimeoutMs);
265
+ // 4. pull artifacts (best effort)
266
+ await this.exec.downloadDir(host, "/tmp/repoos-artifacts/*", join(this.config.root, ".repoos", "logs", "remote-validation", opts.taskId));
267
+ const elapsed = Math.round((Date.now() - startedAt) / 1000);
268
+ if (run.timedOut) {
269
+ emit(`\n[remote run SIGKILLed after ${Math.round(this.timings.remoteRunTimeoutMs / 60000)}m]\n`);
270
+ this.logger?.integration(opts.taskId, "warn", `remote validation timed out after ${elapsed}s`);
271
+ return {
272
+ ok: false,
273
+ stage: "check",
274
+ transient: true,
275
+ exitCode: null,
276
+ output: tail(run.output, 40, 4000),
277
+ detail: `remote validation timed out after ${elapsed}s (the runner VM may be overloaded) — retrying resumes from the check step`,
278
+ };
279
+ }
280
+ if (run.code === 0) {
281
+ emit(`\n[remote validation PASSED in ${elapsed}s]\n`);
282
+ this.logger?.integration(opts.taskId, "info", `remote validation passed in ${elapsed}s`);
283
+ return { ok: true, stage: "check" };
284
+ }
285
+ // Non-zero: the ssh transport itself could have dropped (code 255) — treat
286
+ // that as infra, not a real test failure.
287
+ if (run.code === 255 &&
288
+ /(?:Connection|ssh:|closed by remote host|Broken pipe)/i.test(run.output)) {
289
+ return this.infraFail(`ssh connection to the runner dropped mid-run: ${tail(run.output)}`);
290
+ }
291
+ const transient = looksTransient(run.output);
292
+ emit(`\n[remote validation FAILED (exit ${run.code}) in ${elapsed}s]\n`);
293
+ this.logger?.integration(opts.taskId, "warn", `remote validation failed (exit ${run.code})`, {
294
+ transient,
295
+ });
296
+ return {
297
+ ok: false,
298
+ stage: "check",
299
+ exitCode: run.code,
300
+ transient,
301
+ output: tail(run.output, 40, 4000),
302
+ detail: `remote validation failed (exit ${run.code}) — ${tail(run.output)}`,
303
+ };
304
+ }
305
+ catch (e) {
306
+ return this.infraFail(e.message);
307
+ }
308
+ finally {
309
+ if (tmp)
310
+ rmSync(tmp, { recursive: true, force: true });
311
+ this.activeJobs = Math.max(0, this.activeJobs - 1);
312
+ if (this.activeJobs === 0)
313
+ this.armIdleTimer();
314
+ }
315
+ }
316
+ // ── VM lifecycle ──────────────────────────────────────────────────────────
317
+ /** Reuse a warm VM, or provision one. Serialised: never creates two. */
318
+ async ensureRunner() {
319
+ if (this.provisioning)
320
+ return this.provisioning;
321
+ this.provisioning = this.doEnsureRunner().finally(() => {
322
+ this.provisioning = null;
323
+ });
324
+ return this.provisioning;
325
+ }
326
+ async doEnsureRunner() {
327
+ const rv = this.config.remoteValidation ?? {};
328
+ // Warm reuse: tracked server still running and within its lifetime cap.
329
+ if (this.state) {
330
+ const ageMs = Date.now() - new Date(this.state.createdAt).getTime();
331
+ if (ageMs < this.maxLifetimeMs()) {
332
+ const existing = await this.hetzner.getServer(this.state.serverId).catch(() => null);
333
+ if (existing && existing.status === "running" && existing.ipv4) {
334
+ return { ip: existing.ipv4, user: this.sshUser, keyPath: this.keyPath };
335
+ }
336
+ }
337
+ // Stale/gone/expired — drop it and clean up below.
338
+ await this.hetzner.deleteServer(this.state.serverId).catch(() => undefined);
339
+ this.state = null;
340
+ this.saveState();
341
+ }
342
+ // Single-server invariant: delete any other labelled server before creating.
343
+ await this.deleteLeaked();
344
+ const name = `repoos-ci-${Date.now().toString(36)}`;
345
+ this.logger?.system("info", `provisioning remote validation runner (${rv.serverType})`);
346
+ const server = await this.hetzner.createServer({
347
+ name,
348
+ serverType: rv.serverType ?? "cpx41",
349
+ location: rv.location ?? "hil",
350
+ image: String(rv.snapshotId),
351
+ sshKeyNames: [String(rv.sshKeyName)],
352
+ labels: { [RUNNER_LABEL_KEY]: "1" },
353
+ });
354
+ const ip = await this.waitForRunning(server);
355
+ this.state = { serverId: server.id, ip, createdAt: new Date().toISOString() };
356
+ this.saveState();
357
+ this.armLifetimeTimer();
358
+ const reachable = await this.waitForSsh(ip);
359
+ if (!reachable) {
360
+ await this.hetzner.deleteServer(server.id).catch(() => undefined);
361
+ this.state = null;
362
+ this.saveState();
363
+ if (this.lifetimeTimer)
364
+ clearTimeout(this.lifetimeTimer);
365
+ throw new Error(`runner ${ip} never accepted SSH within ${this.timings.sshWaitTimeoutMs / 1000}s`);
366
+ }
367
+ return { ip, user: this.sshUser, keyPath: this.keyPath };
368
+ }
369
+ async waitForRunning(server) {
370
+ const deadline = Date.now() + this.timings.provisionTimeoutMs;
371
+ let current = server;
372
+ while (Date.now() < deadline) {
373
+ if (current.status === "running" && current.ipv4)
374
+ return current.ipv4;
375
+ await new Promise((r) => setTimeout(r, this.timings.provisionPollMs));
376
+ const next = await this.hetzner.getServer(server.id);
377
+ if (!next)
378
+ throw new Error(`runner ${server.id} vanished during provisioning`);
379
+ current = next;
380
+ }
381
+ // Give up: delete the half-born server so it never leaks.
382
+ await this.hetzner.deleteServer(server.id).catch(() => undefined);
383
+ this.state = null;
384
+ this.saveState();
385
+ throw new Error(`runner ${server.id} did not reach "running" within ${this.timings.provisionTimeoutMs / 1000}s`);
386
+ }
387
+ async waitForSsh(ip) {
388
+ const deadline = Date.now() + this.timings.sshWaitTimeoutMs;
389
+ while (Date.now() < deadline) {
390
+ if (await this.exec.probeTcp(ip, SSH_PORT, 5_000))
391
+ return true;
392
+ await new Promise((r) => setTimeout(r, this.timings.sshProbeIntervalMs));
393
+ }
394
+ return false;
395
+ }
396
+ /** Delete every labelled runner VM except the one we currently track. */
397
+ async deleteLeaked() {
398
+ let servers;
399
+ try {
400
+ servers = await this.hetzner.listServers(RUNNER_LABEL_SELECTOR);
401
+ }
402
+ catch {
403
+ return; // API list failed — nothing safe to do here
404
+ }
405
+ for (const s of servers) {
406
+ if (this.state && s.id === this.state.serverId)
407
+ continue;
408
+ await this.hetzner.deleteServer(s.id).catch(() => undefined);
409
+ this.logger?.system("info", `deleted leaked remote validation runner ${s.id} (${s.name})`);
410
+ }
411
+ }
412
+ async reconcile() {
413
+ // Called at boot: nothing is validating, so every labelled server is a leak.
414
+ this.state = null;
415
+ let servers;
416
+ try {
417
+ servers = await this.hetzner.listServers(RUNNER_LABEL_SELECTOR);
418
+ }
419
+ catch (e) {
420
+ this.logger?.system("warn", `remote runner reconcile skipped: ${e.message}`);
421
+ return;
422
+ }
423
+ for (const s of servers) {
424
+ await this.hetzner.deleteServer(s.id).catch(() => undefined);
425
+ this.logger?.system("info", `reconcile: deleted stray remote validation runner ${s.id} (${s.name})`);
426
+ }
427
+ this.saveState();
428
+ }
429
+ async dispose() {
430
+ this.clearIdleTimer();
431
+ if (this.lifetimeTimer)
432
+ clearTimeout(this.lifetimeTimer);
433
+ if (this.state) {
434
+ await this.hetzner.deleteServer(this.state.serverId).catch(() => undefined);
435
+ this.state = null;
436
+ this.saveState();
437
+ }
438
+ }
439
+ armIdleTimer() {
440
+ this.clearIdleTimer();
441
+ if (!this.state)
442
+ return;
443
+ this.idleTimer = setTimeout(() => {
444
+ void this.teardown("idle");
445
+ }, this.idleMs());
446
+ this.idleTimer.unref?.();
447
+ }
448
+ clearIdleTimer() {
449
+ if (this.idleTimer) {
450
+ clearTimeout(this.idleTimer);
451
+ this.idleTimer = null;
452
+ }
453
+ }
454
+ armLifetimeTimer() {
455
+ if (this.lifetimeTimer)
456
+ clearTimeout(this.lifetimeTimer);
457
+ this.lifetimeTimer = setTimeout(() => {
458
+ void this.teardown("lifetime cap");
459
+ }, this.maxLifetimeMs());
460
+ this.lifetimeTimer.unref?.();
461
+ }
462
+ async teardown(why) {
463
+ if (!this.state)
464
+ return;
465
+ if (why === "idle" && this.activeJobs > 0) {
466
+ this.armIdleTimer();
467
+ return;
468
+ }
469
+ const id = this.state.serverId;
470
+ this.state = null;
471
+ this.saveState();
472
+ this.clearIdleTimer();
473
+ if (this.lifetimeTimer)
474
+ clearTimeout(this.lifetimeTimer);
475
+ await this.hetzner.deleteServer(id).catch(() => undefined);
476
+ this.logger?.system("info", `remote validation runner ${id} deleted (${why})`);
477
+ }
478
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Repository-level locks for mutual exclusion on the main checkout.
3
+ *
4
+ * The close-out lock serializes publication to main, and the root lock
5
+ * additionally prevents a hotfix from running while a close-out holds the
6
+ * main checkout. Both locks share a single file to keep acquisition atomic.
7
+ */
8
+ export interface RepositoryLock {
9
+ /** Acquire the lock. Returns true if successful, false if already held. */
10
+ acquire(taskId: string): boolean;
11
+ /** Release the lock if held by this task. */
12
+ release(taskId: string): boolean;
13
+ /** Check if the lock is currently held. */
14
+ isLocked(): boolean;
15
+ /** Get the ID of the task holding the lock, or null if not locked. */
16
+ getHolder(): string | null;
17
+ }
18
+ export declare function createRepositoryLock(root: string): RepositoryLock;
19
+ /**
20
+ * Root lock: prevents a hotfix and a close-out from holding the main checkout
21
+ * at the same time. Both must acquire this lock before touching root.
22
+ */
23
+ export interface RootLock {
24
+ /** Try to acquire the root lock for a task. Returns true on success. */
25
+ acquire(taskId: string, kind: "hotfix" | "close-out"): boolean;
26
+ /** Release the root lock if held by this task. */
27
+ release(taskId: string): boolean;
28
+ /** Whether the root lock is currently held. */
29
+ isLocked(): boolean;
30
+ /** Get the holder info, or null. */
31
+ getHolder(): {
32
+ taskId: string;
33
+ kind: "hotfix" | "close-out";
34
+ } | null;
35
+ }
36
+ export declare function createRootLock(root: string): RootLock;