@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,1227 @@
1
+ /**
2
+ * `repoos check` — the definition-of-done gate.
3
+ *
4
+ * Runs, in sequence: build staleness check, formatting & lint guard
5
+ * (fmt:check + lint, since nothing else enforces them — deliberately BEFORE
6
+ * the build: both are pure source-level checks with no build dependency, and
7
+ * running them first means a formatting fix never has to pay for a second
8
+ * full build in the same `repoos check` invocation — see the "Formatting &
9
+ * lint guard" comment below), full build (tsc + asset copy), CSS layering
10
+ * guard, theme contrast guard (button-gradient validity + WCAG contrast on
11
+ * every theme's fg/bg token pairs), test suite (if present), and a headless
12
+ * browser smoke test. The UI smoke step is per-project and opt-in (#0348): a
13
+ * project declares a command via a `smoke` package.json script or `[check]
14
+ * uiSmoke` in repoos.toml, and skips cleanly when it declares neither. RepoOS
15
+ * itself opts in the same way (its `smoke` script runs src/commands/ui-smoke.ts).
16
+ * The stylesheet guards are likewise opt-in (#0351): `[check] uiStylesheet`
17
+ * plus a `themeScopes`/`contrastPairs`/`gradientTokens` vocabulary, so neither
18
+ * guard carries a RepoOS-only path or token name.
19
+ *
20
+ * Exits non-zero on any failure. Designed for CI gates and agent pre-review.
21
+ */
22
+ import { execFileSync, execSync } from "node:child_process";
23
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
24
+ import { cpus, totalmem } from "node:os";
25
+ import { isAbsolute, join, relative, sep } from "node:path";
26
+ import { c } from "../cli/colors.js";
27
+ import { checkBuildForRoot } from "../core/build.js";
28
+ import { findRepoRoot, loadConfig } from "../core/config.js";
29
+ import { availableMemBytes } from "../core/sysmem.js";
30
+ import { preferBunForDevTasks } from "../core/runtime.js";
31
+ function pass(name, detail) {
32
+ return { name, ok: true, detail };
33
+ }
34
+ function fail(name, detail) {
35
+ return { name, ok: false, detail };
36
+ }
37
+ /**
38
+ * Split a CSS selector list on top-level commas (respecting parentheses,
39
+ * so `:not(.a,.b)` stays one group).
40
+ */
41
+ function selectorGroups(sel) {
42
+ const parts = [];
43
+ let depth = 0;
44
+ let cur = "";
45
+ for (const ch of sel) {
46
+ if (ch === "(") {
47
+ depth++;
48
+ cur += ch;
49
+ }
50
+ else if (ch === ")") {
51
+ depth--;
52
+ cur += ch;
53
+ }
54
+ else if (ch === "," && depth === 0) {
55
+ parts.push(cur);
56
+ cur = "";
57
+ }
58
+ else {
59
+ cur += ch;
60
+ }
61
+ }
62
+ parts.push(cur);
63
+ return parts.map((p) => p.trim()).filter(Boolean);
64
+ }
65
+ /** True for a selector that targets every element of a kind: `*` or a bare tag. */
66
+ function isBroadSelector(g) {
67
+ if (g === "*")
68
+ return true;
69
+ return /^[a-z][a-z0-9-]*([\s.#:\[>~+]|$)/.test(g);
70
+ }
71
+ /** Recursively list `.ts` files under `dir`, skipping node_modules/dist/dotdirs. */
72
+ function walkTsFiles(dir, acc = []) {
73
+ let entries;
74
+ try {
75
+ entries = readdirSync(dir, { withFileTypes: true });
76
+ }
77
+ catch {
78
+ return acc;
79
+ }
80
+ for (const e of entries) {
81
+ if (e.name.startsWith(".") || e.name === "node_modules" || e.name === "dist")
82
+ continue;
83
+ const full = join(dir, e.name);
84
+ if (e.isDirectory())
85
+ walkTsFiles(full, acc);
86
+ else if (e.name.endsWith(".ts"))
87
+ acc.push(full);
88
+ }
89
+ return acc;
90
+ }
91
+ /**
92
+ * Bare `require(...)` calls guard (#0271 follow-up): this package is
93
+ * `"type": "module"` (ESM) — a bare, un-shadowed `require` is undefined at
94
+ * runtime in the compiled `dist/` output, throwing `ReferenceError`.
95
+ * Confirmed live: `integration-job.ts`'s `removeJob()` used bare
96
+ * `require("fs")` inside a try/catch that silently swallowed the
97
+ * `ReferenceError`, so a moot close-out job was never actually deleted from
98
+ * disk — it kept getting re-picked-up and re-processed in a tight infinite
99
+ * loop, live, in production (task #0258), until someone noticed and asked
100
+ * why it was stuck.
101
+ *
102
+ * vitest transpiles TypeScript on the fly with its own module loader, which
103
+ * DOES provide a working `require()` even in these ESM-flagged files — so
104
+ * this bug is invisible to every unit test that imports the affected module,
105
+ * no matter how thorough. Only the actual compiled `dist/` output, run under
106
+ * Node's real ESM semantics, exposes it. This static, textual guard is the
107
+ * only check in the whole gate that runs against the real failure mode.
108
+ *
109
+ * `createRequire(...)` (a real, valid way to get a working `require` in ESM
110
+ * — see `ui-harness.ts`) shadows the global; a file that uses it anywhere is
111
+ * exempted file-wide rather than precisely scoping which calls are shadowed
112
+ * and which aren't — simpler, and a false negative here just means a
113
+ * legitimately-shadowed file's OTHER bare-require bugs (if any) go
114
+ * uncaught, never that a broken bare require ships silently.
115
+ */
116
+ export function bareRequireOffenders(roots, opts = {}) {
117
+ const offenders = [];
118
+ const repoRoot = opts.repoRoot ?? ".";
119
+ const excludes = (opts.excludes ?? []).map(normalizeGuardDir).filter(Boolean);
120
+ for (const root of roots) {
121
+ const rootRel = root === "." ? "." : normalizeGuardDir(root);
122
+ if (!rootRel)
123
+ continue;
124
+ const absRoot = rootRel === "." ? repoRoot : join(repoRoot, rootRel);
125
+ for (const absPath of walkTsFiles(absRoot)) {
126
+ const relPath = relative(repoRoot, absPath).split(sep).join("/");
127
+ if (relPath.endsWith(".test.ts"))
128
+ continue; // vitest supplies its own require shim
129
+ if (isExcludedPath(relPath, excludes))
130
+ continue;
131
+ let content;
132
+ try {
133
+ content = readFileSync(absPath, "utf8");
134
+ }
135
+ catch {
136
+ continue;
137
+ }
138
+ if (content.includes("createRequire"))
139
+ continue; // legitimately shadowed file-wide
140
+ content.split("\n").forEach((line, i) => {
141
+ const trimmed = line.trim();
142
+ if (trimmed.startsWith("*") || trimmed.startsWith("//"))
143
+ return;
144
+ if (/\brequire\(/.test(line))
145
+ offenders.push(`${relPath}:${i + 1}`);
146
+ });
147
+ }
148
+ }
149
+ return offenders;
150
+ }
151
+ /**
152
+ * Strip `//` and `/* *​/` comments and trailing commas from a tsconfig so it
153
+ * parses as JSON (tsconfig.json is JSONC, which `JSON.parse` rejects). Both
154
+ * transformations are string-aware: a `//` or `,]` inside a double-quoted
155
+ * string value is left untouched, and only a comma whose next significant
156
+ * token is `}` or `]` is dropped.
157
+ */
158
+ function stripJsonComments(text) {
159
+ let out = "";
160
+ let i = 0;
161
+ let inStr = false;
162
+ while (i < text.length) {
163
+ const ch = text[i];
164
+ if (inStr) {
165
+ out += ch;
166
+ if (ch === "\\") {
167
+ out += text[i + 1] ?? "";
168
+ i += 2;
169
+ continue;
170
+ }
171
+ if (ch === '"')
172
+ inStr = false;
173
+ i++;
174
+ continue;
175
+ }
176
+ if (ch === '"') {
177
+ inStr = true;
178
+ out += ch;
179
+ i++;
180
+ continue;
181
+ }
182
+ if (ch === "/" && text[i + 1] === "/") {
183
+ while (i < text.length && text[i] !== "\n")
184
+ i++;
185
+ continue;
186
+ }
187
+ if (ch === "/" && text[i + 1] === "*") {
188
+ i += 2;
189
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/"))
190
+ i++;
191
+ i += 2;
192
+ continue;
193
+ }
194
+ if (ch === ",") {
195
+ const next = nextSignificantChar(text, i + 1);
196
+ if (next === "}" || next === "]") {
197
+ i++; // trailing comma
198
+ continue;
199
+ }
200
+ }
201
+ out += ch;
202
+ i++;
203
+ }
204
+ return out;
205
+ }
206
+ /** The next non-whitespace, non-comment character at or after `from`, or null. */
207
+ function nextSignificantChar(text, from) {
208
+ let i = from;
209
+ while (i < text.length) {
210
+ const ch = text[i];
211
+ if (/\s/.test(ch)) {
212
+ i++;
213
+ continue;
214
+ }
215
+ if (ch === "/" && text[i + 1] === "/") {
216
+ while (i < text.length && text[i] !== "\n")
217
+ i++;
218
+ continue;
219
+ }
220
+ if (ch === "/" && text[i + 1] === "*") {
221
+ i += 2;
222
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/"))
223
+ i++;
224
+ i += 2;
225
+ continue;
226
+ }
227
+ return ch;
228
+ }
229
+ return null;
230
+ }
231
+ /** Parse a `tsconfig.json` (JSONC) and read the fields the guard needs, or null. */
232
+ export function readTsconfig(repoRoot) {
233
+ const path = join(repoRoot, "tsconfig.json");
234
+ if (!existsSync(path))
235
+ return null;
236
+ try {
237
+ return JSON.parse(stripJsonComments(readFileSync(path, "utf8")));
238
+ }
239
+ catch {
240
+ return null;
241
+ }
242
+ }
243
+ /**
244
+ * Turn one tsconfig glob (`src/**​/*.ts`, `packages/app/src`, `lib/index.ts`)
245
+ * into a repo-relative directory to walk. Returns `"."` for a whole-repo glob
246
+ * (`**​/*.ts`), "" for anything absolute, home-relative, or negated.
247
+ */
248
+ function globToScanRoot(glob) {
249
+ let g = glob.replace(/\\/g, "/");
250
+ while (g.startsWith("./"))
251
+ g = g.slice(2);
252
+ if (g.startsWith("!") || g.startsWith("/") || g.startsWith("~"))
253
+ return "";
254
+ const globAt = g.search(/[*?[{]/);
255
+ let base = globAt === -1 ? g : g.slice(0, globAt);
256
+ if (/\.(ts|tsx|mts|cts)$/.test(base))
257
+ base = base.replace(/\/[^/]*$/, "");
258
+ if (base === "" || base === ".")
259
+ return ".";
260
+ return normalizeGuardDir(base);
261
+ }
262
+ function escapeRegExp(s) {
263
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
264
+ }
265
+ /** Compile a tsconfig-style glob (`src/**​/*.ts`, `**​/*.spec.ts`) to a regex. */
266
+ function globToRegExp(pattern) {
267
+ let re = "";
268
+ for (let i = 0; i < pattern.length; i++) {
269
+ const ch = pattern[i];
270
+ if (ch === "*") {
271
+ if (pattern[i + 1] === "*") {
272
+ if (pattern[i + 2] === "/") {
273
+ re += "(?:.*/)?"; // `**​/` — any number of leading directories
274
+ i += 2;
275
+ }
276
+ else {
277
+ re += ".*";
278
+ i += 1;
279
+ }
280
+ }
281
+ else {
282
+ re += "[^/]*";
283
+ }
284
+ }
285
+ else if (ch === "?") {
286
+ re += "[^/]";
287
+ }
288
+ else if (ch === "{") {
289
+ const end = pattern.indexOf("}", i);
290
+ if (end === -1) {
291
+ re += "\\{";
292
+ }
293
+ else {
294
+ const alts = pattern
295
+ .slice(i + 1, end)
296
+ .split(",")
297
+ .map(escapeRegExp);
298
+ re += `(?:${alts.join("|")})`;
299
+ i = end;
300
+ }
301
+ }
302
+ else {
303
+ re += escapeRegExp(ch);
304
+ }
305
+ }
306
+ return new RegExp(`^${re}$`);
307
+ }
308
+ /**
309
+ * Whether a repo-relative path is excluded. A literal path excludes that file
310
+ * exactly, or — for a directory — everything beneath it. A pattern containing
311
+ * glob metacharacters is compiled and matched against the whole path. Keeping
312
+ * these distinct matters: `globToScanRoot("src/legacy.ts")` would collapse a
313
+ * single-file exclude to `src` and wipe out the whole tree (#0352 review).
314
+ */
315
+ function isExcludedPath(relPath, patterns) {
316
+ for (const p of patterns) {
317
+ if (!/[*?[{]/.test(p)) {
318
+ if (relPath === p || relPath.startsWith(`${p}/`))
319
+ return true;
320
+ }
321
+ else if (globToRegExp(p).test(relPath)) {
322
+ return true;
323
+ }
324
+ }
325
+ return false;
326
+ }
327
+ /**
328
+ * Decide which source roots the bare-`require()` guard scans (#0352). Explicit
329
+ * `[check] bareRequireDirs` wins; otherwise the repo's tsconfig `include` (plus
330
+ * `files`) is mapped to directories, minus its `exclude` list. Pure so the
331
+ * resolution is unit-testable; if neither yields a root the guard skips rather
332
+ * than passing vacuously.
333
+ */
334
+ export function resolveBareRequireRoots(configured, tsconfig, configuredExcludes) {
335
+ const normalizePatterns = (v) => [
336
+ ...new Set((v ?? []).map(normalizeGuardDir).filter(Boolean)),
337
+ ];
338
+ const configRoots = dedupeRoots((configured ?? []).map((d) => (d === "." ? "." : normalizeGuardDir(d))).filter(Boolean));
339
+ if (configRoots.length) {
340
+ return {
341
+ roots: configRoots,
342
+ excludes: normalizePatterns(configuredExcludes),
343
+ source: "config",
344
+ };
345
+ }
346
+ const patterns = [
347
+ ...(Array.isArray(tsconfig?.include) ? tsconfig.include : []),
348
+ ...(Array.isArray(tsconfig?.files) ? tsconfig.files : []),
349
+ ].filter((v) => typeof v === "string" && v.trim() !== "");
350
+ const roots = dedupeRoots(patterns.map(globToScanRoot).filter(Boolean));
351
+ const excludes = normalizePatterns((Array.isArray(tsconfig?.exclude) ? tsconfig.exclude : []).filter((v) => typeof v === "string" && v.trim() !== ""));
352
+ return { roots, excludes, source: roots.length ? "tsconfig" : "none" };
353
+ }
354
+ /** Drop any root that is a descendant of another, and collapse to `["."]` if present. */
355
+ function dedupeRoots(roots) {
356
+ const set = [...new Set(roots)];
357
+ if (set.includes("."))
358
+ return ["."];
359
+ return set.filter((r) => !set.some((o) => o !== r && r.startsWith(`${o}/`)));
360
+ }
361
+ /**
362
+ * Binary attachments (screenshots, PDFs) under the task/input folders must
363
+ * never enter git history: they are uploaded through the UI, written to
364
+ * `<dir>/.attachments/`, and served back by the running server from disk — the
365
+ * committed record is the task/input `.md`, not the pixels. Binaries in
366
+ * history bloat the repo irreversibly (this repo once carried ~250 MiB of
367
+ * stale screenshot churn). Product image assets — UI, icons, logos, docs —
368
+ * live outside those folders and stay tracked.
369
+ *
370
+ * The folder names come from `repoos.toml` (`workDir`/`inputsDir`) so a managed
371
+ * repo that renames them still gets the guard; they default to `work`/`inputs`.
372
+ *
373
+ * Pure (takes the tracked-file list) so it is unit-testable; the check block
374
+ * feeds it `git ls-files`.
375
+ */
376
+ /**
377
+ * Normalize a configured guard directory (`workDir`/`inputsDir`) for prefix
378
+ * matching against `git ls-files` output, which is always repo-root-relative:
379
+ * strip any leading `./` and trailing slashes. Returns empty for anything that
380
+ * can't be a repo-relative directory — an explicit `""`, `"."`/`"./"`, an
381
+ * absolute path, a home-relative `~/…`, or a path containing a `..` segment.
382
+ * Those would all silently match nothing (or, for `""`, fatal git's pathspec),
383
+ * so callers must treat empty as unusable rather than as "match everything".
384
+ */
385
+ export function normalizeGuardDir(d) {
386
+ let trimmed = d;
387
+ while (trimmed.startsWith("./"))
388
+ trimmed = trimmed.slice(2);
389
+ trimmed = trimmed.replace(/\/+$/, "");
390
+ if (trimmed === "" || trimmed === ".")
391
+ return "";
392
+ if (isAbsolute(trimmed) || trimmed.startsWith("~") || trimmed.split("/").includes("..")) {
393
+ return "";
394
+ }
395
+ return trimmed;
396
+ }
397
+ export function taskAssetOffenders(trackedPaths, dirs = {}) {
398
+ const IMG = /\.(png|jpe?g|gif|webp|avif|bmp|svg|ico|pdf)$/i;
399
+ const prefixes = [dirs.workDir ?? "work", dirs.inputsDir ?? "inputs"]
400
+ .map(normalizeGuardDir)
401
+ .filter(Boolean)
402
+ .map((d) => `${d}/`);
403
+ return trackedPaths.filter((p) => prefixes.some((pre) => p.startsWith(pre)) && IMG.test(p));
404
+ }
405
+ /**
406
+ * Scan a stylesheet for UNLAYERED universal/bare-element selectors.
407
+ * With Tailwind v4 everything lives in cascade layers; an unlayered `*`
408
+ * or tag rule silently beats every utility (unlayered > @layer), which
409
+ * already collapsed all shadcn padding once. Scoped class/ID/attribute
410
+ * rules are intentional legacy overrides and stay allowed.
411
+ */
412
+ export function cssLayeringOffenders(css) {
413
+ const out = [];
414
+ const src = css.replace(/\/\*[\s\S]*?\*\//g, "");
415
+ const scopes = [];
416
+ const inLayer = () => scopes.includes("layer");
417
+ const inKeyframes = () => scopes.includes("keyframes");
418
+ src.split("\n").forEach((line, li) => {
419
+ let seg = "";
420
+ for (let i = 0; i < line.length; i++) {
421
+ const ch = line[i];
422
+ if (ch === "{") {
423
+ const sel = seg.trim();
424
+ if (sel.startsWith("@")) {
425
+ if (/^@keyframes\b/.test(sel))
426
+ scopes.push("keyframes");
427
+ else if (/^@layer\b/.test(sel))
428
+ scopes.push("layer");
429
+ else
430
+ scopes.push("");
431
+ }
432
+ else {
433
+ if (!inLayer() && !inKeyframes()) {
434
+ for (const g of selectorGroups(sel)) {
435
+ if (isBroadSelector(g))
436
+ out.push(`${li + 1}: ${g}`);
437
+ }
438
+ }
439
+ scopes.push("");
440
+ }
441
+ seg = "";
442
+ }
443
+ else if (ch === "}") {
444
+ scopes.pop();
445
+ seg = "";
446
+ }
447
+ else if (ch === ";") {
448
+ seg = "";
449
+ }
450
+ else {
451
+ seg += ch;
452
+ }
453
+ }
454
+ });
455
+ return out;
456
+ }
457
+ function heading(label) {
458
+ console.log(c.bold(c.cyan(`\n ◆ ${label}`)));
459
+ }
460
+ const MIN_CONTRAST = 3.0;
461
+ function parseThemeBlocks(css, scopeNames) {
462
+ const blocks = [];
463
+ let cur = null;
464
+ let buf = "";
465
+ for (const raw of css.split("\n")) {
466
+ const t = raw.trim();
467
+ if (cur === null) {
468
+ if (t.endsWith("{") && !t.startsWith("@")) {
469
+ const sel = t.slice(0, -1).trim();
470
+ cur = scopeNames.get(sel) ?? "";
471
+ buf = "";
472
+ }
473
+ }
474
+ else if (t === "}") {
475
+ if (cur) {
476
+ const decls = {};
477
+ for (const pair of buf.split(";")) {
478
+ const idx = pair.indexOf(":");
479
+ if (idx > 0) {
480
+ const k = pair.slice(0, idx).trim();
481
+ const v = pair.slice(idx + 1).trim();
482
+ if (k.startsWith("--"))
483
+ decls[k] = v;
484
+ }
485
+ }
486
+ blocks.push({ variant: cur, decls });
487
+ }
488
+ cur = null;
489
+ }
490
+ else if (cur) {
491
+ buf += t;
492
+ }
493
+ }
494
+ return blocks;
495
+ }
496
+ function resolveVar(value, map, depth = 0) {
497
+ if (depth > 6)
498
+ return value;
499
+ const m = value.trim().match(/^var\(--([a-z0-9-]+)\)$/i);
500
+ if (!m)
501
+ return value;
502
+ const next = map[`--${m[1]}`];
503
+ return next !== undefined ? resolveVar(next, map, depth + 1) : value;
504
+ }
505
+ function parseColor(v) {
506
+ const s = v.trim().toLowerCase();
507
+ if (!s || s === "none")
508
+ return null;
509
+ if (s === "transparent")
510
+ return { r: 0, g: 0, b: 0, a: 0 };
511
+ if (s.startsWith("#")) {
512
+ const h = s.slice(1);
513
+ if (h.length === 3 || h.length === 4) {
514
+ const full = h
515
+ .split("")
516
+ .map((ch) => ch + ch)
517
+ .join("");
518
+ return parseColor("#" + full);
519
+ }
520
+ if (h.length === 6 || h.length === 8) {
521
+ const n = parseInt(h.slice(0, 6), 16);
522
+ const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1;
523
+ return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255, a };
524
+ }
525
+ return null;
526
+ }
527
+ const m = s.match(/^rgba?\(([\d.]+),([\d.]+),([\d.]+)(?:,([\d.]+))?\)$/);
528
+ if (m) {
529
+ return {
530
+ r: parseInt(m[1], 10),
531
+ g: parseInt(m[2], 10),
532
+ b: parseInt(m[3], 10),
533
+ a: m[4] !== undefined ? parseFloat(m[4]) : 1,
534
+ };
535
+ }
536
+ return null;
537
+ }
538
+ function composite(fg, bg) {
539
+ const a = fg.a;
540
+ return {
541
+ r: fg.r * a + bg.r * (1 - a),
542
+ g: fg.g * a + bg.g * (1 - a),
543
+ b: fg.b * a + bg.b * (1 - a),
544
+ };
545
+ }
546
+ function channelLum(ch) {
547
+ const c = ch / 255;
548
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
549
+ }
550
+ function luminance(c) {
551
+ return 0.2126 * channelLum(c.r) + 0.7152 * channelLum(c.g) + 0.0722 * channelLum(c.b);
552
+ }
553
+ function contrastRatio(a, b) {
554
+ const [hi, lo] = a >= b ? [a, b] : [b, a];
555
+ return (hi + 0.05) / (lo + 0.05);
556
+ }
557
+ /** Resolve a token value to concrete colors (composited over `bg`). */
558
+ function colorCandidates(raw, map, bg) {
559
+ const resolved = resolveVar(raw, map);
560
+ const out = [];
561
+ if (resolved.includes("gradient(")) {
562
+ const re = /#[0-9a-fA-F]{3,8}|rgba?\([^)]*\)|transparent/g;
563
+ let m;
564
+ while ((m = re.exec(resolved))) {
565
+ const c = parseColor(m[0]);
566
+ if (c)
567
+ out.push(composite(c, bg));
568
+ }
569
+ }
570
+ else {
571
+ const c = parseColor(resolved);
572
+ if (c)
573
+ out.push(composite(c, bg));
574
+ }
575
+ return out;
576
+ }
577
+ /**
578
+ * An opaque backdrop to composite semi-transparent colors over before their
579
+ * luminance is meaningful. Prefer the configured `backdropToken` (the page
580
+ * background); otherwise fall back to the pair's own background token when that
581
+ * resolves to a solid color; only then to white. A missing backdrop never skips
582
+ * a pair — at worst it approximates an alpha channel.
583
+ */
584
+ function resolveBackdrop(map, bgToken, backdropToken) {
585
+ const configured = backdropToken ? map[backdropToken] : undefined;
586
+ for (const raw of [configured, map[bgToken]]) {
587
+ if (raw === undefined)
588
+ continue;
589
+ const solid = parseColor(resolveVar(raw, map));
590
+ if (solid)
591
+ return composite(solid, solid);
592
+ }
593
+ return { r: 255, g: 255, b: 255 };
594
+ }
595
+ /**
596
+ * True when at least one configured scope selector opens a block in `css`.
597
+ * The theme-contrast step uses this to skip a stylesheet with no theme blocks
598
+ * rather than keying off a hardcoded RepoOS selector like `:root`.
599
+ */
600
+ export function hasThemeBlocks(css, scopes) {
601
+ return parseThemeBlocks(css, new Map(scopes.map((s) => [s.selector, s.name]))).length > 0;
602
+ }
603
+ /**
604
+ * Advisory config-shape warnings for declared theme scopes: a typo'd
605
+ * `inherits` target or a duplicated selector/name otherwise collapses
606
+ * silently (empty declarations / last-wins). Returned rather than thrown so
607
+ * `repoos check` can surface them without failing the gate on a config typo.
608
+ */
609
+ export function themeScopeConfigWarnings(scopes) {
610
+ const warnings = [];
611
+ const names = new Set(scopes.map((s) => s.name));
612
+ const seenNames = new Set();
613
+ const seenSelectors = new Set();
614
+ for (const s of scopes) {
615
+ if (seenNames.has(s.name))
616
+ warnings.push(`[check] themeScopes: duplicate name "${s.name}"`);
617
+ seenNames.add(s.name);
618
+ if (seenSelectors.has(s.selector)) {
619
+ warnings.push(`[check] themeScopes: duplicate selector "${s.selector}"`);
620
+ }
621
+ seenSelectors.add(s.selector);
622
+ for (const base of s.inherits ?? []) {
623
+ if (!names.has(base)) {
624
+ warnings.push(`[check] themeScopes: "${s.name}" inherits unknown scope "${base}"`);
625
+ }
626
+ }
627
+ }
628
+ return warnings;
629
+ }
630
+ export function themeContrastOffenders(css, config) {
631
+ const { scopes, pairs, gradientTokens, backdropToken } = config;
632
+ const blocks = parseThemeBlocks(css, new Map(scopes.map((s) => [s.selector, s.name])));
633
+ if (!blocks.length)
634
+ return [];
635
+ const byVariant = {};
636
+ for (const b of blocks)
637
+ byVariant[b.variant] = b;
638
+ const out = [];
639
+ for (const scope of scopes) {
640
+ const map = {};
641
+ for (const base of scope.inherits?.length ? scope.inherits : [scope.name]) {
642
+ Object.assign(map, byVariant[base]?.decls ?? {});
643
+ }
644
+ for (const tk of gradientTokens) {
645
+ const raw = map[tk];
646
+ if (raw !== undefined && !resolveVar(raw, map).includes("gradient(")) {
647
+ out.push(`${scope.name} · ${tk} must be a gradient — it is consumed via background-image`);
648
+ }
649
+ }
650
+ for (const { fg, bg: bgK } of pairs) {
651
+ if (map[fg] === undefined || map[bgK] === undefined)
652
+ continue;
653
+ const backdrop = resolveBackdrop(map, bgK, backdropToken);
654
+ const fgs = colorCandidates(map[fg], map, backdrop);
655
+ const bgs = colorCandidates(map[bgK], map, backdrop);
656
+ if (!fgs.length || !bgs.length)
657
+ continue;
658
+ let worst = Infinity;
659
+ for (const f of fgs)
660
+ for (const b of bgs) {
661
+ const c = contrastRatio(luminance(f), luminance(b));
662
+ if (c < worst)
663
+ worst = c;
664
+ }
665
+ if (worst < MIN_CONTRAST) {
666
+ out.push(`${scope.name} · ${fg} on ${bgK} → ${worst.toFixed(2)} (need ≥${MIN_CONTRAST})`);
667
+ }
668
+ }
669
+ }
670
+ return out;
671
+ }
672
+ /**
673
+ * Which git ref (if any) to scope the Tests step to, via Vitest's own
674
+ * `--changed <ref>` (git-diff + module graph, only runs tests that could
675
+ * actually be affected). Unset for a standalone `repoos check` — that path
676
+ * is the full definition-of-done gate and must never narrow coverage.
677
+ *
678
+ * Only the two per-branch pre-merge checks set this: the engineer's own
679
+ * self-check before requesting handoff, and the server's handoff-finalize
680
+ * re-verification (both re-checking the SAME isolated branch, just diffed
681
+ * against its own base). The close-out gate that validates the actual merge
682
+ * onto main (integration-orchestrator.ts's validateCandidate) deliberately
683
+ * never sets it — that check is about interaction with whatever else has
684
+ * landed on main since, which a per-branch diff can't see.
685
+ */
686
+ export function changedTestRef(env) {
687
+ const ref = env.REPOOS_CHECK_CHANGED;
688
+ return ref && ref.trim() ? ref.trim() : undefined;
689
+ }
690
+ /**
691
+ * The `package.json` script name a project uses to opt into `repoos check`'s
692
+ * UI smoke step with no configuration — the same zero-config convention
693
+ * `build`/`test`/`lint` already follow elsewhere in this gate.
694
+ */
695
+ export const UI_SMOKE_SCRIPT = "smoke";
696
+ /**
697
+ * Resolve which UI smoke command (if any) `repoos check` should run for the
698
+ * current project (#0348). Precedence: `repoos.toml` `[check] uiSmoke` wins,
699
+ * then a `smoke` package.json script, else null — the caller then skips the
700
+ * step cleanly. RepoOS's own repo resolves through this same path (its `smoke`
701
+ * script); there is no RepoOS-only fallback.
702
+ */
703
+ export function resolveSmokeCommand(configured, pkgScripts) {
704
+ const fromConfig = configured?.trim();
705
+ if (fromConfig)
706
+ return { source: "config", command: fromConfig };
707
+ if (pkgScripts?.[UI_SMOKE_SCRIPT])
708
+ return { source: "script", script: UI_SMOKE_SCRIPT };
709
+ return null;
710
+ }
711
+ /**
712
+ * Vitest worker-pool size for the Tests step. `vite.config.ts` pins a
713
+ * conservative floor (`maxWorkers: 2`) because several agent-driven check runs
714
+ * routinely overlap across worktrees and each pool otherwise multiplies against
715
+ * the others. But a solo run on an idle machine with memory to spare leaves
716
+ * most of the box idle — the subprocess-heavy suites (agent-review,
717
+ * done-reliability, task-watchdog) are I/O-bound on `waitFor` polls, so more
718
+ * workers is close to linear there.
719
+ *
720
+ * Scale up only with real headroom, gated on BOTH cpu count and reclaimable
721
+ * memory (budget ~900MB per worker + its fixture subprocesses). An explicit
722
+ * `REPOOS_TEST_WORKERS` always wins; anything else falls back to the config
723
+ * floor via `undefined`.
724
+ */
725
+ export function testPoolSize(env) {
726
+ const pinned = Number(env.REPOOS_TEST_WORKERS);
727
+ if (Number.isFinite(pinned) && pinned >= 1)
728
+ return Math.floor(pinned);
729
+ const avail = availableMemBytes();
730
+ // Scale up only with genuine headroom. `availableMem` counts reclaimable
731
+ // inactive/cache, but on a box that has been thrashing that memory is slow
732
+ // and expensive to actually reclaim — so also require it to be a solid
733
+ // fraction of total before trusting it. A starved machine stays at the
734
+ // config floor (2).
735
+ if (avail < totalmem() * 0.45)
736
+ return undefined;
737
+ const byCpu = Math.floor(cpus().length * 0.75);
738
+ const byMem = Math.floor(avail / (900 * 1024 * 1024));
739
+ const n = Math.min(byCpu, byMem, 8);
740
+ return n >= 3 ? n : undefined;
741
+ }
742
+ export async function cmdCheck() {
743
+ let exitCode = 0;
744
+ const results = [];
745
+ // ── 1. Build staleness ──────────────────────────────────────────────
746
+ heading("Build staleness check");
747
+ // `repoos check` validates the checkout it was invoked in. In particular,
748
+ // a global/dev-linked CLI may be running from the main checkout while cwd
749
+ // is a task worktree with its own source and build marker.
750
+ const stale = checkBuildForRoot(findRepoRoot());
751
+ if (stale.stale && stale.applicable) {
752
+ console.log(c.yellow(` ⚠ ${stale.message}`));
753
+ results.push(fail("staleness", stale.message ?? "build is stale"));
754
+ exitCode = 1;
755
+ }
756
+ else if (stale.code === "fresh") {
757
+ console.log(c.green(" ✔ Build is fresh"));
758
+ results.push(pass("staleness"));
759
+ }
760
+ else {
761
+ // Not a RepoOS-style build (no dist/ or no marker) — degrade to a skip
762
+ // rather than failing a project whose pipeline simply isn't ours. See
763
+ // checkBuildForRoot: `applicable` is false exactly when no marker exists.
764
+ console.log(c.dim(` · ${stale.message ?? stale.code}`));
765
+ results.push(pass("staleness", stale.message ?? stale.code));
766
+ }
767
+ // ── 1b. Lockfile sync check ─────────────────────────────────────────
768
+ // A dependency bump in package.json without a regenerated bun.lock passes
769
+ // every other check here (node_modules is already installed) but breaks
770
+ // `bun install --frozen-lockfile` for every fresh worktree bootstrap
771
+ // (bootstrap.ts installDeps) — silently, since bootstrap runs on a
772
+ // different checkout than the one that merged the drift. Dry-run makes
773
+ // the frozen install fail loudly instead, before the merge lands.
774
+ heading("Lockfile sync check");
775
+ if (!existsSync("bun.lock")) {
776
+ console.log(c.dim(" · No bun.lock — skipping"));
777
+ results.push(pass("lockfile-sync", "skipped — no bun.lock"));
778
+ }
779
+ else {
780
+ try {
781
+ execSync("bun install --frozen-lockfile --dry-run", { stdio: "pipe", timeout: 60_000 });
782
+ console.log(c.green(" ✔ bun.lock matches package.json"));
783
+ results.push(pass("lockfile-sync"));
784
+ }
785
+ catch {
786
+ const msg = "bun.lock is out of sync with package.json — run `bun install` and commit the updated lockfile";
787
+ console.log(c.red(" ✗ " + msg));
788
+ results.push(fail("lockfile-sync", msg));
789
+ exitCode = 1;
790
+ }
791
+ }
792
+ const pkg = JSON.parse(existsSync("package.json") ? readFileSync("package.json", "utf8") : "{}");
793
+ // Per-project step config (#0348) — `[check] uiSmoke` from repoos.toml. Read
794
+ // once here so the UI smoke step below can resolve its command without a
795
+ // second config load; loadConfig is cheap and repoos.toml is git-tracked.
796
+ const cfg = loadConfig(findRepoRoot());
797
+ // ── 1c. Zero-runtime-dependencies guard ─────────────────────────────
798
+ // "Zero runtime dependencies" is a hard design constraint (AGENTS.md),
799
+ // publicly claimed on the landing page (landing/src/App.vue), and it has
800
+ // already silently drifted from `package.json` once (#0343: mermaid ended
801
+ // up in `dependencies` instead of `devDependencies`, discovered only by a
802
+ // manual audit). This makes the claim self-enforcing instead of
803
+ // convention-only.
804
+ //
805
+ // Scoped to RepoOS's own package.json rather than running for every
806
+ // managed project: this constraint is specific to this repo, not a
807
+ // general rule `repoos check` should impose on projects it manages (a
808
+ // typical managed project has legitimate runtime deps). #0348 tracks
809
+ // making check steps like this declarable per-project instead of
810
+ // hardcoded here; do not widen this scope without that mechanism.
811
+ //
812
+ // Matches the unscoped OR scoped package name (`repoos` / `@.../repoos`):
813
+ // the package was renamed to `@repo-os/repoos` after this guard was
814
+ // written with a literal `=== "repoos"` check, which silently disabled
815
+ // it — the exact "constraint silently stops being enforced" failure mode
816
+ // #0343 exists to catch, now recurring in the guard meant to catch it.
817
+ heading("Zero runtime dependencies guard");
818
+ const pkgBaseName = typeof pkg.name === "string" ? pkg.name.split("/").pop() : undefined;
819
+ if (pkgBaseName !== "repoos") {
820
+ results.push(pass("zero-runtime-deps", "skipped — not RepoOS's own package.json"));
821
+ }
822
+ else {
823
+ const deps = Object.keys(pkg.dependencies ?? {});
824
+ if (deps.length > 0) {
825
+ const msg = `package.json "dependencies" must be empty (zero runtime dependencies is a hard ` +
826
+ `design constraint — AGENTS.md). Found: ${deps.join(", ")}. Move build-time-only ` +
827
+ `packages to devDependencies, or if the constraint no longer holds, update AGENTS.md ` +
828
+ `and landing/src/App.vue's "Zero runtime dependencies" claim instead (see #0343).`;
829
+ console.log(c.red(" ✗ " + msg));
830
+ results.push(fail("zero-runtime-deps", msg));
831
+ exitCode = 1;
832
+ }
833
+ else {
834
+ console.log(c.green(" ✔ package.json has no runtime dependencies"));
835
+ results.push(pass("zero-runtime-deps"));
836
+ }
837
+ }
838
+ // ── 1d. Formatting & lint guard ──────────────────────────────────────
839
+ // Nothing else runs the formatter/linter — no git hook, no CI job — so
840
+ // without this step `dist/` builds fine while the source silently drifts out
841
+ // of the house style (that's how it accumulated ~10 unformatted files). Both
842
+ // are sub-second with oxfmt/oxlint. Gated on the scripts existing so a plain
843
+ // `repoos init` repo without them just skips.
844
+ // Runs BEFORE the build (not after, as it originally did): fmt/lint are
845
+ // pure source-level checks, no build dependency either way, but a fix
846
+ // (`bun run fmt`) rewrites files — if that happened after a build already
847
+ // ran, the next `repoos check` would see source newer than the build
848
+ // marker and pay for a second full rebuild of nothing but whitespace
849
+ // changes. Checking first means the build only ever runs once source is
850
+ // already clean.
851
+ heading("Formatting & lint guard");
852
+ // Gates the (expensive) Full build and UI smoke test below: a fix here
853
+ // (`bun run fmt`) rewrites source, so a build against the still-unformatted
854
+ // tree is immediately invalidated the moment that fix lands — building it
855
+ // anyway just burns a full tsc pass for nothing. This doesn't weaken the
856
+ // gate: it already fails this run (exitCode is set below) either way, so
857
+ // there is no scenario where skipping the build here lets a broken one
858
+ // slip through unnoticed — it only defers verifying the build to the
859
+ // rerun once source is actually clean.
860
+ let fmtLintFailed = false;
861
+ for (const [label, script, hint] of [
862
+ ["Formatting", "fmt:check", "run `bun run fmt` to fix"],
863
+ ["Lint", "lint", "fix the reported errors"],
864
+ ]) {
865
+ if (!pkg.scripts?.[script]) {
866
+ console.log(c.dim(` · No \`${script}\` script — skipping ${label.toLowerCase()}`));
867
+ results.push(pass(`check-${script}`, `skipped — no ${script} script`));
868
+ continue;
869
+ }
870
+ try {
871
+ execSync(`${preferBunForDevTasks() ? "bun run" : "npm run"} ${script}`, {
872
+ stdio: "pipe",
873
+ timeout: 120_000,
874
+ });
875
+ console.log(c.green(` ✔ ${label} clean`));
876
+ results.push(pass(`check-${script}`));
877
+ }
878
+ catch (e) {
879
+ const out = [e.stdout, e.stderr]
880
+ .map((b) => b?.toString().trim())
881
+ .filter(Boolean)
882
+ .join("\n");
883
+ const msg = `${label} check failed — ${hint}:\n${out || e.message}`;
884
+ console.log(c.red(` ✗ ${label} check failed — ${hint}`));
885
+ results.push(fail(`check-${script}`, msg));
886
+ exitCode = 1;
887
+ fmtLintFailed = true;
888
+ }
889
+ }
890
+ // ── 2. Full build ───────────────────────────────────────────────────
891
+ // Always run `bun run build`; it is staleness-aware now (scripts/build.mjs,
892
+ // #0377) and skips in ~0.1s when src/ is unchanged since the marker was
893
+ // written. That replaces the old REPOOS_SKIP_BUILD opt-in: the close-out
894
+ // pipeline (src/server/done.ts, integration-orchestrator.ts, release.ts) runs
895
+ // `bun run build` just before invoking `repoos check`, and this step now
896
+ // detects the fresh marker on its own instead of via a private env flag.
897
+ //
898
+ // The staleness step above still runs and reports FIRST (#0276): a genuinely
899
+ // stale build is surfaced there, then this step repairs it within the same
900
+ // invocation. The build's skip is safe here precisely because step 1 already
901
+ // verified the marker matches src/ — a stale or missing marker never skips.
902
+ heading("Full build");
903
+ if (fmtLintFailed) {
904
+ console.log(c.dim(" · Skipped — formatting/lint failed above; fix that first, dist would be stale " +
905
+ "the moment you do, and this run already fails regardless"));
906
+ results.push(pass("build", "skipped — formatting/lint failed, fix and rerun"));
907
+ }
908
+ else {
909
+ try {
910
+ execSync("bun run build", { stdio: "inherit", timeout: 120_000 });
911
+ console.log(c.green(" ✔ Build succeeded"));
912
+ results.push(pass("build"));
913
+ }
914
+ catch (e) {
915
+ const msg = e.message;
916
+ console.log(c.red(" ✗ Build failed"));
917
+ results.push(fail("build", msg));
918
+ exitCode = 1;
919
+ }
920
+ }
921
+ // ── 2b. CSS layering guard ──────────────────────────────────────────
922
+ // Both stylesheet guards below read `[check] uiStylesheet` (#0351): there is
923
+ // no RepoOS-shaped default path, so a project that declares neither a
924
+ // stylesheet nor a token vocabulary skips both cleanly. The path is resolved
925
+ // against the repo root (not cwd) and read once here; a configured path that
926
+ // doesn't exist is a misconfiguration worth a warning, not a silent skip.
927
+ heading("CSS layering guard");
928
+ const cssRelPath = cfg.check?.uiStylesheet;
929
+ const repoRoot = findRepoRoot();
930
+ const cssPath = cssRelPath && !isAbsolute(cssRelPath) ? join(repoRoot, cssRelPath) : cssRelPath;
931
+ const cssExists = Boolean(cssPath && existsSync(cssPath));
932
+ if (cssRelPath && !cssExists) {
933
+ console.log(c.yellow(` ⚠ [check] uiStylesheet "${cssRelPath}" does not exist in this repo — stylesheet guards will skip`));
934
+ }
935
+ const cssSrc = cssPath && cssExists ? readFileSync(cssPath, "utf8") : "";
936
+ if (!cssRelPath) {
937
+ console.log(c.dim(" · No [check] uiStylesheet configured — skipping"));
938
+ results.push(pass("css-layers", "skipped — no [check] uiStylesheet configured"));
939
+ }
940
+ else if (!cssExists) {
941
+ console.log(c.dim(` · ${cssRelPath} does not exist — skipping`));
942
+ results.push(pass("css-layers", `skipped — ${cssRelPath} not found`));
943
+ }
944
+ else if (!cssSrc.includes('@import "tailwindcss"')) {
945
+ console.log(c.dim(` · ${cssPath} is not a Tailwind v4 stylesheet — skipping`));
946
+ results.push(pass("css-layers", `skipped — ${cssPath} has no Tailwind v4 import`));
947
+ }
948
+ else {
949
+ const offenders = cssLayeringOffenders(cssSrc);
950
+ if (offenders.length > 0) {
951
+ const msg = "Unlayered universal/bare-element selectors (they silently beat all Tailwind utilities):\n " +
952
+ offenders.slice(0, 8).join("\n ") +
953
+ "\n Wrap them in @layer base or scope them to a class/id.";
954
+ console.log(c.red(" ✗ " + msg.split("\n")[0]));
955
+ results.push(fail("css-layers", msg));
956
+ exitCode = 1;
957
+ }
958
+ else {
959
+ console.log(c.green(" ✔ No unlayered universal/bare-element selectors"));
960
+ results.push(pass("css-layers"));
961
+ }
962
+ }
963
+ // ── 2c. Theme contrast guard ────────────────────────────────────────
964
+ heading("Theme contrast guard");
965
+ const themeScopes = cfg.check?.themeScopes ?? [];
966
+ for (const w of themeScopeConfigWarnings(themeScopes))
967
+ console.log(c.yellow(` ⚠ ${w}`));
968
+ if (!cssRelPath) {
969
+ console.log(c.dim(" · No [check] uiStylesheet configured — skipping"));
970
+ results.push(pass("theme-contrast", "skipped — no [check] uiStylesheet configured"));
971
+ }
972
+ else if (!cssExists) {
973
+ console.log(c.dim(` · ${cssRelPath} does not exist — skipping`));
974
+ results.push(pass("theme-contrast", `skipped — ${cssRelPath} not found`));
975
+ }
976
+ else if (!themeScopes.length) {
977
+ console.log(c.dim(" · No [check] themeScopes configured — skipping"));
978
+ results.push(pass("theme-contrast", "skipped — no [check] themeScopes configured"));
979
+ }
980
+ else if (!hasThemeBlocks(cssSrc, themeScopes)) {
981
+ console.log(c.dim(` · No configured theme scope matched a block in ${cssRelPath} — skipping`));
982
+ results.push(pass("theme-contrast", "skipped — no configured theme block found"));
983
+ }
984
+ else {
985
+ const offenders = themeContrastOffenders(cssSrc, {
986
+ scopes: themeScopes,
987
+ pairs: cfg.check?.contrastPairs ?? [],
988
+ gradientTokens: cfg.check?.gradientTokens ?? [],
989
+ backdropToken: cfg.check?.backdropToken,
990
+ });
991
+ if (offenders.length > 0) {
992
+ const msg = "Low-contrast or invalid theme tokens (invisible text risk):\n " +
993
+ offenders.slice(0, 10).join("\n ");
994
+ console.log(c.red(" ✗ " + msg.split("\n")[0]));
995
+ results.push(fail("theme-contrast", msg));
996
+ exitCode = 1;
997
+ }
998
+ else {
999
+ console.log(c.green(" ✔ Theme tokens have valid button gradients and ≥3:1 contrast"));
1000
+ results.push(pass("theme-contrast"));
1001
+ }
1002
+ }
1003
+ // ── 2d. Bare require() guard ─────────────────────────────────────────
1004
+ // The bug this guards is specific to `"type": "module"` packages (a bare
1005
+ // `require` is valid in CJS), and the directories to scan are per-project
1006
+ // (#0352): `[check] bareRequireDirs`, else the tsconfig include list.
1007
+ heading("Bare require() guard");
1008
+ {
1009
+ const resolved = resolveBareRequireRoots(cfg.check?.bareRequireDirs, readTsconfig(repoRoot), cfg.check?.bareRequireExcludes);
1010
+ if (pkg.type !== "module") {
1011
+ console.log(c.dim(' · package.json is not "type": "module" — skipping (bare require() is valid in CJS)'));
1012
+ results.push(pass("bare-require", 'skipped — package.json is not "type": "module"'));
1013
+ }
1014
+ else if (resolved.roots.length === 0) {
1015
+ console.log(c.dim(" · No source roots to scan — set [check] bareRequireDirs or a tsconfig include — skipping"));
1016
+ results.push(pass("bare-require", "skipped — no source roots configured"));
1017
+ }
1018
+ else {
1019
+ const offenders = bareRequireOffenders(resolved.roots, {
1020
+ repoRoot,
1021
+ excludes: resolved.excludes,
1022
+ });
1023
+ if (offenders.length > 0) {
1024
+ const msg = 'Bare require() in ESM source (this package is "type": "module" — a bare require throws ' +
1025
+ "ReferenceError at runtime in dist/, silently if caught):\n " +
1026
+ offenders.slice(0, 10).join("\n ") +
1027
+ '\n Import from "node:..." normally, or use createRequire(import.meta.url) if you ' +
1028
+ "genuinely need CJS interop (see ui-harness.ts).";
1029
+ console.log(c.red(" ✗ " + msg.split("\n")[0]));
1030
+ results.push(fail("bare-require", msg));
1031
+ exitCode = 1;
1032
+ }
1033
+ else {
1034
+ const from = resolved.source === "config" ? "[check] bareRequireDirs" : "tsconfig include";
1035
+ console.log(c.green(` ✔ No bare require() calls in ESM source (${from})`));
1036
+ results.push(pass("bare-require"));
1037
+ }
1038
+ }
1039
+ }
1040
+ // ── 2d′. Task / input asset guard ───────────────────────────────────
1041
+ heading("Task asset guard");
1042
+ {
1043
+ // Folder names are configurable (`workDir`/`inputsDir` in repoos.toml); a
1044
+ // managed repo that renames them must still be guarded, so read them rather
1045
+ // than assuming the default `work`/`inputs`.
1046
+ const config = loadConfig();
1047
+ const inputsDirRaw = config.inputsDir ?? "inputs";
1048
+ const guardDirs = [
1049
+ { label: "workDir", raw: config.workDir, norm: normalizeGuardDir(config.workDir) },
1050
+ { label: "inputsDir", raw: inputsDirRaw, norm: normalizeGuardDir(inputsDirRaw) },
1051
+ ];
1052
+ for (const { label, raw, norm } of guardDirs) {
1053
+ if (!norm) {
1054
+ console.log(c.yellow(` ⚠ repoos.toml's ${label} ("${raw}") doesn't resolve to a repo-relative ` +
1055
+ "directory — the task-asset guard can't be scoped to it. Fix the config."));
1056
+ }
1057
+ }
1058
+ // Only usable, repo-relative dirs go into the pathspec. An empty one is
1059
+ // dropped, not passed to git (`git ls-files -- ""` exits fatally, and the
1060
+ // catch below would turn that into a silent green). If BOTH are unusable
1061
+ // the guard can't run at all — fail loudly rather than pass with nothing
1062
+ // to check.
1063
+ const usableDirs = guardDirs.map((d) => d.norm).filter(Boolean);
1064
+ if (usableDirs.length === 0) {
1065
+ const msg = "task-asset guard is disabled — neither workDir nor inputsDir in repoos.toml resolves " +
1066
+ "to a repo-relative directory, so no task/input folder can be checked. Fix the config.";
1067
+ console.log(c.red(" ✗ " + msg));
1068
+ results.push(fail("task-assets", msg));
1069
+ exitCode = 1;
1070
+ }
1071
+ else {
1072
+ let tracked = [];
1073
+ try {
1074
+ // Pathspecs are pre-validated (non-empty, repo-relative) above, so a
1075
+ // failure here means no git repo / git unavailable — not bad config.
1076
+ tracked = execFileSync("git", ["ls-files", "--", ...usableDirs], {
1077
+ encoding: "utf8",
1078
+ maxBuffer: 16 * 1024 * 1024,
1079
+ })
1080
+ .split("\n")
1081
+ .filter(Boolean);
1082
+ }
1083
+ catch {
1084
+ /* not a git repo / git unavailable — nothing to guard */
1085
+ }
1086
+ const offenders = taskAssetOffenders(tracked, {
1087
+ workDir: config.workDir,
1088
+ inputsDir: inputsDirRaw,
1089
+ });
1090
+ const labels = usableDirs.map((d) => `${d}/`).join(" or ");
1091
+ if (offenders.length > 0) {
1092
+ const msg = `Binary attachments committed under ${labels} — these are served by ` +
1093
+ "the running server from disk and must never enter git history (it bloats the repo " +
1094
+ "irreversibly). Run `git rm --cached` on them (they stay on disk) and let " +
1095
+ "`.gitignore` keep them out:\n " +
1096
+ offenders.slice(0, 15).join("\n ") +
1097
+ (offenders.length > 15 ? `\n …and ${offenders.length - 15} more` : "");
1098
+ console.log(c.red(" ✗ " + msg.split("\n")[0]));
1099
+ results.push(fail("task-assets", msg));
1100
+ exitCode = 1;
1101
+ }
1102
+ else {
1103
+ console.log(c.green(` ✔ No committed binaries under ${labels}`));
1104
+ results.push(pass("task-assets"));
1105
+ }
1106
+ }
1107
+ }
1108
+ // ── 3. Tests (if any) ───────────────────────────────────────────────
1109
+ heading("Tests");
1110
+ const hasTestScript = Boolean(pkg.scripts && pkg.scripts.test);
1111
+ const hasTestFiles = existsSync("test") || existsSync("__tests__") || existsSync("tests");
1112
+ // The close-out pipeline can hand the test suite to the Remote Validation
1113
+ // Runner (docs/remote-validation.md) — a Hetzner VM runs `bun run build` +
1114
+ // `bun run test` off this machine — and then invoke the LOCAL `repoos check`
1115
+ // with REPOOS_SKIP_TESTS=1 for only the cheap static guards + UI smoke.
1116
+ // Standalone `repoos check` never sets it, so the CLI gate is unchanged.
1117
+ if (process.env.REPOOS_SKIP_TESTS === "1") {
1118
+ console.log(c.dim(" · Skipped — test suite ran on the remote validation runner (REPOOS_SKIP_TESTS=1)"));
1119
+ results.push(pass("tests", "skipped — ran on the remote validation runner"));
1120
+ }
1121
+ else if (hasTestScript || hasTestFiles) {
1122
+ // Only the vitest-backed `bun run test` script understands `--changed`;
1123
+ // the bare `bun test` fallback (no package.json test script) is left
1124
+ // unscoped. `--bun` forces vitest's `#!/usr/bin/env node` shebang onto
1125
+ // Bun for a Bun-native repo (has bun.lock) unless pinned to Node — ~5x
1126
+ // faster and it stops the swap-thrash flake on a loaded machine.
1127
+ // preferBunForDevTasks() checks the lockfile so a managed repo whose
1128
+ // tests want Node is never switched.
1129
+ const changedRef = hasTestScript ? changedTestRef(process.env) : undefined;
1130
+ const runScript = preferBunForDevTasks() ? "bun run --bun test" : "bun run test";
1131
+ const cmd = hasTestScript
1132
+ ? changedRef
1133
+ ? `${runScript} -- --changed ${changedRef}`
1134
+ : runScript
1135
+ : "bun test";
1136
+ if (changedRef) {
1137
+ console.log(c.dim(` · Scoped to files changed vs ${changedRef} (--changed)`));
1138
+ }
1139
+ // Worker pool: `undefined` keeps the config floor (2). A solo run with
1140
+ // headroom scales up — passed through env, read back in vite.config.ts.
1141
+ const workers = hasTestScript ? testPoolSize(process.env) : undefined;
1142
+ const testEnv = workers
1143
+ ? { ...process.env, REPOOS_TEST_WORKERS: String(workers) }
1144
+ : process.env;
1145
+ if (workers && !changedRef) {
1146
+ const availGiB = (availableMemBytes() / 1024 ** 3).toFixed(1);
1147
+ console.log(c.dim(` · Full suite · ${workers} workers (${availGiB} GiB reclaimable)`));
1148
+ }
1149
+ // A full unscoped run is ~10min even healthy and can legitimately reach
1150
+ // ~25min on a slow box; a too-tight cap SIGTERMs `bun run test` mid-run
1151
+ // (exit 143) even when every test is green. Vitest's own per-test
1152
+ // `testTimeout` fails a genuine hang fast; this is only the outer backstop.
1153
+ const timeoutMs = changedRef ? 300_000 : 1_500_000;
1154
+ try {
1155
+ execSync(cmd, { stdio: "inherit", timeout: timeoutMs, env: testEnv });
1156
+ console.log(c.green(" ✔ Tests passed"));
1157
+ results.push(pass("tests", changedRef ? `scoped to changed vs ${changedRef}` : undefined));
1158
+ }
1159
+ catch (e) {
1160
+ console.log(c.red(" ✗ Tests failed"));
1161
+ results.push(fail("tests", e.message));
1162
+ exitCode = 1;
1163
+ }
1164
+ }
1165
+ else {
1166
+ console.log(c.dim(" · No test suite found — skipping"));
1167
+ results.push(pass("tests", "skipped — no test suite"));
1168
+ }
1169
+ // ── 4. UI smoke test ────────────────────────────────────────────────
1170
+ // Per-project and opt-in (#0348). `repoos check` is the generic gate every
1171
+ // managed project runs, so it must not boot RepoOS's own dashboard for a
1172
+ // project that never declared a smoke command — that either silently tested
1173
+ // RepoOS's UI or failed on RepoOS-only infrastructure. A project opts in
1174
+ // with a `smoke` package.json script (zero-config default) or `[check]
1175
+ // uiSmoke` in repoos.toml (overrides the script). With neither, the step
1176
+ // skips cleanly. RepoOS dogfoods this: its own dashboard assertions live in
1177
+ // src/commands/ui-smoke.ts behind a `smoke` script (scripts/ui-smoke.mjs),
1178
+ // so the declaration path is exercised on every RepoOS check.
1179
+ heading("UI smoke test");
1180
+ const smokeCommand = resolveSmokeCommand(cfg.check?.uiSmoke, pkg.scripts);
1181
+ if (fmtLintFailed) {
1182
+ // Build was skipped above, so dist reflects whatever the last successful
1183
+ // build was (possibly stale, possibly absent) — never a build of the
1184
+ // current, still-unformatted source. Probing it here would be testing the
1185
+ // wrong thing — the same reason the build step above is gated on it.
1186
+ console.log(c.dim(" · Skipped — formatting/lint failed above, so build was skipped too"));
1187
+ results.push(pass("ui-smoke", "skipped — formatting/lint failed, fix and rerun"));
1188
+ }
1189
+ else if (smokeCommand) {
1190
+ const runner = preferBunForDevTasks() ? "bun run" : "npm run";
1191
+ const cmd = smokeCommand.source === "config" ? smokeCommand.command : `${runner} ${smokeCommand.script}`;
1192
+ const origin = smokeCommand.source === "config"
1193
+ ? "repoos.toml [check] uiSmoke"
1194
+ : "package.json smoke script";
1195
+ console.log(c.dim(` · Running ${origin}: ${cmd}`));
1196
+ try {
1197
+ execSync(cmd, { stdio: "inherit", timeout: 300_000 });
1198
+ console.log(c.green(" ✔ Smoke command passed"));
1199
+ results.push(pass("ui-smoke", `ran ${origin}`));
1200
+ }
1201
+ catch (e) {
1202
+ console.log(c.red(" ✗ Smoke command failed"));
1203
+ results.push(fail("ui-smoke", e.message));
1204
+ exitCode = 1;
1205
+ }
1206
+ }
1207
+ else {
1208
+ console.log(c.dim(" · No smoke command configured — skipping"));
1209
+ console.log(c.dim(' Declare one with a `smoke` package.json script or [check] uiSmoke = "bun run smoke" in repoos.toml'));
1210
+ results.push(pass("ui-smoke", "skipped — no smoke command configured"));
1211
+ }
1212
+ // ── Summary ─────────────────────────────────────────────────────────
1213
+ const failed = results.filter((r) => !r.ok);
1214
+ console.log(c.bold(c.cyan("\n ── Results ──")));
1215
+ for (const r of results) {
1216
+ const icon = r.ok ? c.green("✔") : c.red("✗");
1217
+ const detail = r.detail ? c.dim(` — ${r.detail}`) : "";
1218
+ console.log(` ${icon} ${r.name}${detail}`);
1219
+ }
1220
+ if (failed.length === 0) {
1221
+ console.log(c.bold(c.green("\n All checks passed.\n")));
1222
+ }
1223
+ else {
1224
+ console.log(c.bold(c.red(`\n ${failed.length} check(s) failed.\n`)));
1225
+ }
1226
+ process.exit(exitCode);
1227
+ }