@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,1288 @@
1
+ /**
2
+ * Config resolution. Zero-config by default; `repoos.toml` at the repo root
3
+ * can override any field. We parse only the flat subset of TOML we need, again
4
+ * to avoid a runtime dependency.
5
+ */
6
+ import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync, } from "node:fs";
7
+ import { cpus } from "node:os";
8
+ import { basename, dirname, join, resolve } from "node:path";
9
+ import { STATUSES } from "./types.js";
10
+ import { stripTomlComment, unquoteTomlString } from "./toml-line.js";
11
+ /** Coding agents an Agent can run under. */
12
+ export const AGENT_CLIS = [
13
+ "opencode",
14
+ "claude code",
15
+ "qwen code",
16
+ "kiro",
17
+ "codex",
18
+ "github copilot",
19
+ ];
20
+ /** Models an Agent can pin (or "default" for the coding agent's default). */
21
+ export const AGENT_MODELS = ["default", "big pickle", "deepseek v4"];
22
+ /** Built-in agents, seeded at runtime when the config has none. */
23
+ export const DEFAULT_AGENTS = [
24
+ {
25
+ name: "Ross",
26
+ cli: "opencode",
27
+ model: "big pickle",
28
+ enabled: true,
29
+ instructions: "You are Ross, a repository-aware assistant inspired by Ross Geller from Friends — warm, enthusiastic, and genuinely curious about code and context. You answer questions about this repository, RepoOS, tasks, statuses, issues, and code with clear, helpful explanations and occasional dry wit. Never edit files, change task state, or modify the repository.",
30
+ },
31
+ {
32
+ name: "engineer",
33
+ cli: "opencode",
34
+ model: "big pickle",
35
+ enabled: true,
36
+ instructions: "Implements tasks: reads the task file, writes clean code, runs `repoos check`, updates the task status.",
37
+ },
38
+ {
39
+ name: "reviewer",
40
+ cli: "opencode",
41
+ model: "big pickle",
42
+ enabled: true,
43
+ instructions: "Reviews a task the moment it lands in `review`: reads the diff in the task's worktree and reports bugs, edge cases, and suggestions for the human signing off. Never changes a task's status.",
44
+ },
45
+ {
46
+ name: "pm",
47
+ cli: "opencode",
48
+ model: "big pickle",
49
+ enabled: true,
50
+ instructions: "Owns the roadmap: moves tasks between statuses, writes activity entries, keeps the work board tidy.",
51
+ },
52
+ {
53
+ name: "cto",
54
+ cli: "opencode",
55
+ model: "big pickle",
56
+ enabled: false,
57
+ instructions: "You are the CTO: an always-on board monitor and un-sticker. Watch for stuck tasks, stale reviews, zombie processes, and broken builds. Report what you find, escalate when needed, and ask the human before taking action. Never move a task to done, merge branches, delete worktrees, change config, or spend money.",
58
+ },
59
+ ];
60
+ /** Default agent names — these are seeded and cannot be removed. */
61
+ export const DEFAULT_AGENT_NAMES = DEFAULT_AGENTS.map((a) => a.name);
62
+ const REPO_GUIDE_NAME = "Ross";
63
+ const REPO_GUIDE_LEGACY_NAME = "RepoOS Guide";
64
+ /**
65
+ * Add Ross to an existing stored agent list without replacing user edits.
66
+ * When the stored config has the legacy "RepoOS Guide" name, rename it to "Ross"
67
+ * so the chat continues working. The other defaults deliberately are not
68
+ * re-seeded: a user may have removed one of those roles from an older configuration.
69
+ */
70
+ export function agentsForConfig(config) {
71
+ const stored = Array.isArray(config.agents) ? config.agents : [];
72
+ if (!stored.length)
73
+ return DEFAULT_AGENTS.map((agent) => ({ ...agent }));
74
+ // Migrate legacy "RepoOS Guide" name to "Ross", preserving all other fields
75
+ const migrated = stored.map((agent) => agent.name.toLowerCase() === REPO_GUIDE_LEGACY_NAME.toLowerCase()
76
+ ? { ...agent, name: REPO_GUIDE_NAME }
77
+ : agent);
78
+ const names = new Set(migrated.map((agent) => agent.name.toLowerCase()));
79
+ const guide = DEFAULT_AGENTS.find((agent) => agent.name === REPO_GUIDE_NAME);
80
+ return [
81
+ ...migrated.map((agent) => ({ ...agent })),
82
+ ...(guide && !names.has(REPO_GUIDE_NAME.toLowerCase()) ? [{ ...guide }] : []),
83
+ ];
84
+ }
85
+ /**
86
+ * "Auto" default for `maxConcurrentAgents`. An agent's own test pool
87
+ * is capped separately (vite.config.ts `test.poolOptions.forks.maxForks: 2`),
88
+ * so one agent's worst-case footprint is bounded rather than "the whole
89
+ * machine" — this can size off total cores directly instead of dividing them
90
+ * away defensively. `cores / 2` leaves headroom for that ~2-worker pool plus
91
+ * the agent process's own overhead per concurrent agent; capped at 8 so a
92
+ * many-core desktop doesn't queue dozens of agents whose non-test work (tool
93
+ * calls, I/O) still contends over shared resources like the git index.
94
+ */
95
+ export function defaultMaxConcurrentAgents() {
96
+ return Math.max(2, Math.min(8, Math.floor(cpus().length / 2)));
97
+ }
98
+ export const DEFAULT_CONFIG = {
99
+ workDir: "work",
100
+ docsDir: "docs",
101
+ skillsDir: "skills",
102
+ inputsDir: "inputs",
103
+ taskExtensions: [".md"],
104
+ defaultStatus: "inbox",
105
+ defaultAssignee: "unassigned",
106
+ cacheDir: ".repoos",
107
+ theme: "system",
108
+ uiTheme: "classic",
109
+ defaultTaskMode: "freeform",
110
+ tunnelEnabled: false,
111
+ ntfyEnabled: false,
112
+ ntfyTopic: "",
113
+ ntfyBaseUrl: "https://ntfy.sh",
114
+ agents: [],
115
+ supervisor: {
116
+ enabled: false,
117
+ interval: 300,
118
+ mode: "observe",
119
+ },
120
+ watchdog: {
121
+ enabled: true,
122
+ stalenessMs: 5 * 60 * 1000,
123
+ autoTransition: true,
124
+ },
125
+ autoEngineeringMode: false,
126
+ maxActiveTasks: 3,
127
+ worktreeWarnThreshold: 20,
128
+ whisper: {
129
+ provider: "none",
130
+ apiKey: "",
131
+ },
132
+ auth: {
133
+ enabled: false,
134
+ sessionMaxAge: 2592000,
135
+ },
136
+ remoteValidation: {
137
+ enabled: false,
138
+ provider: "hetzner",
139
+ serverType: "cax31",
140
+ location: "hil",
141
+ idleShutdownMinutes: 8,
142
+ maxServerLifetimeMinutes: 120,
143
+ fallbackToLocal: false,
144
+ },
145
+ };
146
+ /**
147
+ * Sibling directory that holds per-task agent worktrees, e.g.
148
+ * `/path/to/repoos` -> `/path/to/repoos-worktrees/`. Kept OUTSIDE the repo
149
+ * root so a linked worktree can never dirty the main checkout or confuse
150
+ * `repoos check` staleness. `<branch>` path segments are kept so a worktree
151
+ * for `feat/123-x` lives at `<dir>/feat/123-x`.
152
+ */
153
+ export function worktreesDir(root) {
154
+ return join(dirname(root), `${basename(root)}-worktrees`);
155
+ }
156
+ /** The port range `deriveServePort` picks from — deliberately just above the
157
+ * classic 7171 so a derived port never collides with a repo that pins it. */
158
+ const DERIVED_PORT_BASE = 7200;
159
+ const DERIVED_PORT_SPAN = 800;
160
+ /**
161
+ * A stable default serve port for a repo, derived from its canonical root
162
+ * path (djb2 hash → `${DERIVED_PORT_BASE}`..`${DERIVED_PORT_BASE + DERIVED_PORT_SPAN - 1}`).
163
+ * Deterministic per checkout, so two repos on one machine don't both default
164
+ * to 7171 and reap each other. Exported for tests.
165
+ */
166
+ export function deriveServePort(root) {
167
+ let canonical = root;
168
+ try {
169
+ canonical = realpathSync(root);
170
+ }
171
+ catch {
172
+ /* not on disk (a test fixture path) — hash the literal string */
173
+ }
174
+ let hash = 5381;
175
+ for (let i = 0; i < canonical.length; i++) {
176
+ hash = ((hash << 5) + hash + canonical.charCodeAt(i)) >>> 0;
177
+ }
178
+ return DERIVED_PORT_BASE + (hash % DERIVED_PORT_SPAN);
179
+ }
180
+ /**
181
+ * Resolve the port `repoos serve` should bind: an explicit `--port` flag wins,
182
+ * then `servePort` in repoos.toml, then a stable per-repo derived port.
183
+ */
184
+ export function resolveServePort(root, config, portFlag) {
185
+ if (Number.isInteger(portFlag) && portFlag > 0)
186
+ return portFlag;
187
+ if (Number.isInteger(config.servePort) && config.servePort > 0) {
188
+ return config.servePort;
189
+ }
190
+ return deriveServePort(root);
191
+ }
192
+ /**
193
+ * Load `.env` from the repo root into `process.env`, if present. Zero
194
+ * runtime deps (no dotenv package) — a minimal `KEY=value` parser, one line
195
+ * per variable. Real env vars already set take precedence over the file, so
196
+ * e.g. a systemd unit's `Environment=` still wins. Safe to call more than
197
+ * once; safe when `.env` doesn't exist.
198
+ */
199
+ export function loadDotEnv(root = findRepoRoot()) {
200
+ const envPath = join(root, ".env");
201
+ if (!existsSync(envPath))
202
+ return;
203
+ const text = readFileSync(envPath, "utf8");
204
+ for (const rawLine of text.split("\n")) {
205
+ const line = rawLine.trim();
206
+ if (!line || line.startsWith("#"))
207
+ continue;
208
+ const eq = line.indexOf("=");
209
+ if (eq === -1)
210
+ continue;
211
+ const key = line.slice(0, eq).trim();
212
+ if (!key || key in process.env)
213
+ continue;
214
+ let value = line.slice(eq + 1).trim();
215
+ if ((value.startsWith('"') && value.endsWith('"')) ||
216
+ (value.startsWith("'") && value.endsWith("'"))) {
217
+ value = value.slice(1, -1);
218
+ }
219
+ process.env[key] = value;
220
+ }
221
+ }
222
+ /**
223
+ * Persist a local secret to the gitignored `.env` at the repo root, keeping
224
+ * the same env-var-secret convention `loadDotEnv` reads back (dev backdoor
225
+ * code, auth provider keys, model-provider API keys). An empty `value`
226
+ * removes the line entirely so a cleared secret can't linger as `KEY=`.
227
+ * Also updates `process.env` directly: `loadDotEnv` skips keys already
228
+ * present in the environment, so a re-`loadConfig` alone would never refresh
229
+ * a value the booting process had already seen.
230
+ *
231
+ * Only ever called with the fixed secret names this codebase owns (the
232
+ * model-provider keys today) — callers validate that before calling. The
233
+ * value is written raw, one `KEY=value` line, exactly what `loadDotEnv`
234
+ * parses back.
235
+ */
236
+ export function setDotEnvSecret(root, key, value) {
237
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
238
+ throw new Error(`Invalid env var name: ${JSON.stringify(key)}`);
239
+ }
240
+ if (value.includes("\n") || value.includes("\r")) {
241
+ throw new Error("Secret value must not contain newlines");
242
+ }
243
+ const envPath = join(root, ".env");
244
+ const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
245
+ const lines = existing.replace(/\r\n/g, "\n").split("\n");
246
+ const kept = [];
247
+ let found = false;
248
+ for (const rawLine of lines) {
249
+ const trimmed = rawLine.trim();
250
+ const eq = trimmed.indexOf("=");
251
+ const name = eq > 0 ? trimmed.slice(0, eq).trim() : "";
252
+ if (name === key) {
253
+ found = true;
254
+ continue;
255
+ }
256
+ kept.push(rawLine);
257
+ }
258
+ if (value)
259
+ kept.push(`${key}=${value}`);
260
+ if (!found && lines.length && kept[kept.length - 1] === "")
261
+ kept.pop();
262
+ writeFileSync(envPath, kept.join("\n") + "\n", "utf8");
263
+ if (value)
264
+ process.env[key] = value;
265
+ else
266
+ delete process.env[key];
267
+ }
268
+ /** Walk upward from `start` to find the repo root (nearest .git or repoos.toml). */
269
+ export function findRepoRoot(start = process.cwd()) {
270
+ let dir = resolve(start);
271
+ // eslint-disable-next-line no-constant-condition
272
+ while (true) {
273
+ if (existsSync(join(dir, ".git")) || existsSync(join(dir, "repoos.toml"))) {
274
+ return dir;
275
+ }
276
+ const parent = resolve(dir, "..");
277
+ if (parent === dir)
278
+ return resolve(start); // hit filesystem root; fall back
279
+ dir = parent;
280
+ }
281
+ }
282
+ /**
283
+ * True when `dir` is a LINKED WORKTREE root rather than a real repo root: a
284
+ * worktree's `.git` is a FILE reading `gitdir: …`, while a real root's `.git`
285
+ * is a directory. Cheap — one stat, no git subprocess.
286
+ *
287
+ * `findRepoRoot` does not make this distinction (it matches any `.git`), which
288
+ * is exactly why board reads run from inside a worktree used to silently
289
+ * resolve to the worktree's own copy of the task files instead of the live
290
+ * board's main checkout (the #0068 false-positive).
291
+ */
292
+ export function isLinkedWorktreeRoot(dir) {
293
+ const gitPath = join(dir, ".git");
294
+ if (!existsSync(gitPath))
295
+ return false;
296
+ try {
297
+ return statSync(gitPath).isFile();
298
+ }
299
+ catch {
300
+ return false;
301
+ }
302
+ }
303
+ /**
304
+ * The MAIN checkout root for a linked worktree, or null when `dir` is not a
305
+ * linked worktree root. A worktree's `.git` file points at
306
+ * `<main>/.git/worktrees/<name>` (absolute, or relative to the worktree); the
307
+ * main root is the first ancestor of that pointer whose own `.git` is a
308
+ * directory. Fail-soft: any anomaly yields null and callers fall back to the
309
+ * resolved root.
310
+ */
311
+ export function mainCheckoutRoot(dir) {
312
+ if (!isLinkedWorktreeRoot(dir))
313
+ return null;
314
+ let text;
315
+ try {
316
+ text = readFileSync(join(dir, ".git"), "utf8");
317
+ }
318
+ catch {
319
+ return null;
320
+ }
321
+ const m = text.match(/^gitdir:\s*(.+)$/m);
322
+ if (!m)
323
+ return null;
324
+ let probe = resolve(dir, m[1].trim());
325
+ // eslint-disable-next-line no-constant-condition
326
+ while (true) {
327
+ if (existsSync(join(probe, ".git")) && !isLinkedWorktreeRoot(probe)) {
328
+ return probe;
329
+ }
330
+ const parent = resolve(probe, "..");
331
+ if (parent === probe)
332
+ return null; // hit filesystem root — not a worktree
333
+ probe = parent;
334
+ }
335
+ }
336
+ /**
337
+ * The root LIVE-BOARD reads (`repoos show`/`list`/`index`) should resolve to:
338
+ * the MAIN checkout even when the CLI runs inside a task worktree, so a
339
+ * readback can never false-positive on the worktree's own copy. `fromWorktree`
340
+ * signals that resolution jumped the main checkout — callers surface it so the
341
+ * behavior is never silent. Mutating commands keep `findRepoRoot` semantics
342
+ * (they must act on the directory they run in).
343
+ */
344
+ export function boardRoot(start) {
345
+ const resolved = findRepoRoot(start);
346
+ const main = mainCheckoutRoot(resolved);
347
+ return main ? { root: main, fromWorktree: true } : { root: resolved, fromWorktree: false };
348
+ }
349
+ /** Extremely small flat-TOML reader: `key = value`, `[section]` headers, and `[[array-of-tables]]`. */
350
+ function parseFlatToml(text) {
351
+ const out = {};
352
+ let section = "";
353
+ let arrayTable = null;
354
+ let arrayTableKey = "";
355
+ for (const rawLine of text.replace(/\r\n/g, "\n").split("\n")) {
356
+ const line = stripTomlComment(rawLine).trim();
357
+ if (!line)
358
+ continue;
359
+ const arrSec = line.match(/^\[\[([^\]]+)\]\]$/);
360
+ if (arrSec) {
361
+ section = "";
362
+ arrayTableKey = arrSec[1];
363
+ const arr = out[arrayTableKey] ?? [];
364
+ arrayTable = {};
365
+ arr.push(arrayTable);
366
+ out[arrayTableKey] = arr;
367
+ continue;
368
+ }
369
+ const sec = line.match(/^\[([^\]]+)\]$/);
370
+ if (sec) {
371
+ section = sec[1];
372
+ arrayTable = null;
373
+ continue;
374
+ }
375
+ const kv = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/);
376
+ if (!kv)
377
+ continue;
378
+ const key = section ? `${section}.${kv[1]}` : kv[1];
379
+ let val = kv[2].trim();
380
+ const s = val;
381
+ if (s.startsWith("[") && s.endsWith("]")) {
382
+ val = s
383
+ .slice(1, -1)
384
+ .split(",")
385
+ .map((x) => unquoteTomlString(x.trim()))
386
+ .filter(Boolean);
387
+ }
388
+ else if (/^-?\d+$/.test(s)) {
389
+ val = Number(s);
390
+ }
391
+ else if (s === "true" || s === "false") {
392
+ val = s === "true";
393
+ }
394
+ else {
395
+ val = unquoteTomlString(s);
396
+ }
397
+ if (arrayTable && arrayTableKey) {
398
+ arrayTable[kv[1]] = val;
399
+ }
400
+ else {
401
+ out[key] = val;
402
+ }
403
+ }
404
+ return out;
405
+ }
406
+ /**
407
+ * Parse the `[preview]` section (plus `[[preview.targets]]` tables) from flat
408
+ * TOML. Exported for tests; `loadConfig` merges the result into the config.
409
+ * Rows without a usable `command` are dropped rather than poisoning resolution.
410
+ */
411
+ export function parsePreviewConfig(parsed) {
412
+ const preview = {};
413
+ const command = parsed["preview.command"];
414
+ if (typeof command === "string" && command.trim())
415
+ preview.command = command.trim();
416
+ const cwd = parsed["preview.cwd"];
417
+ if (typeof cwd === "string" && cwd.trim())
418
+ preview.cwd = cwd.trim();
419
+ const readyPath = normalizeReadyPath(parsed["preview.readyPath"]);
420
+ if (readyPath)
421
+ preview.readyPath = readyPath;
422
+ const readyTimeoutMs = normalizeReadyTimeoutMs(parsed["preview.readyTimeoutMs"]);
423
+ if (readyTimeoutMs)
424
+ preview.readyTimeoutMs = readyTimeoutMs;
425
+ if (Array.isArray(parsed["preview.targets"])) {
426
+ const targets = [];
427
+ // Target names are the pick/label key the UI and resolution use (#0379), so
428
+ // they must be unique. Auto-derived names (`areas.join("/")`) collide the
429
+ // moment two targets claim the same area, and explicit duplicates are just
430
+ // as easy to write; either would render two identical picker options and
431
+ // make `resolvePreviewTarget(name)` select the first silently. Disambiguate
432
+ // deterministically with a numeric suffix instead of dropping a target.
433
+ const usedNames = new Set();
434
+ for (const raw of parsed["preview.targets"]) {
435
+ if (typeof raw !== "object" || raw === null)
436
+ continue;
437
+ const r = raw;
438
+ const targetCommand = typeof r.command === "string" ? r.command.trim() : "";
439
+ if (!targetCommand)
440
+ continue;
441
+ const areasRaw = r.areas;
442
+ const areas = (Array.isArray(areasRaw) ? areasRaw : [areasRaw])
443
+ .map((a) => (typeof a === "string" ? a.trim() : ""))
444
+ .filter(Boolean);
445
+ const base = typeof r.name === "string" && r.name.trim() ? r.name.trim() : areas.join("/") || "target";
446
+ let name = base;
447
+ for (let n = 2; usedNames.has(name); n++)
448
+ name = `${base} (${n})`;
449
+ usedNames.add(name);
450
+ const target = { name, areas, command: targetCommand };
451
+ const targetCwd = typeof r.cwd === "string" ? r.cwd.trim() : "";
452
+ if (targetCwd)
453
+ target.cwd = targetCwd;
454
+ const targetReadyPath = normalizeReadyPath(r.ready_path ?? r.readyPath);
455
+ if (targetReadyPath)
456
+ target.readyPath = targetReadyPath;
457
+ const targetReadyTimeoutMs = normalizeReadyTimeoutMs(r.ready_timeout_ms ?? r.readyTimeoutMs);
458
+ if (targetReadyTimeoutMs)
459
+ target.readyTimeoutMs = targetReadyTimeoutMs;
460
+ targets.push(target);
461
+ }
462
+ if (targets.length)
463
+ preview.targets = targets;
464
+ }
465
+ const hasCustom = Boolean(preview.command) || Boolean(preview.targets?.length);
466
+ return hasCustom ? preview : undefined;
467
+ }
468
+ /** Normalize a configured readiness path to a leading-slash path, or undefined. */
469
+ function normalizeReadyPath(value) {
470
+ if (typeof value !== "string")
471
+ return undefined;
472
+ const trimmed = value.trim();
473
+ if (!trimmed)
474
+ return undefined;
475
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
476
+ }
477
+ /** Normalize a configured readiness timeout (ms) to a positive finite number, or undefined. */
478
+ function normalizeReadyTimeoutMs(value) {
479
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0)
480
+ return undefined;
481
+ return value;
482
+ }
483
+ export function loadConfig(rootArg) {
484
+ const root = rootArg ? resolve(rootArg) : findRepoRoot();
485
+ // Load .env before resolving [auth]/[whisper] secrets below, so every path
486
+ // that boots a real server (repoos serve, previews, the UI smoke test)
487
+ // sees the same env-sourced values consistently, not just the CLI's own
488
+ // entrypoint.
489
+ loadDotEnv(root);
490
+ const cfg = { root, ...DEFAULT_CONFIG };
491
+ const tomlPath = join(root, "repoos.toml");
492
+ if (existsSync(tomlPath)) {
493
+ const parsed = parseFlatToml(readFileSync(tomlPath, "utf8"));
494
+ const get = (k) => parsed[k] ?? parsed[`repoos.${k}`];
495
+ if (typeof get("workDir") === "string")
496
+ cfg.workDir = get("workDir");
497
+ if (typeof get("docsDir") === "string")
498
+ cfg.docsDir = get("docsDir");
499
+ if (typeof get("skillsDir") === "string")
500
+ cfg.skillsDir = get("skillsDir");
501
+ if (typeof get("inputsDir") === "string")
502
+ cfg.inputsDir = get("inputsDir");
503
+ if (Array.isArray(get("taskExtensions")))
504
+ cfg.taskExtensions = get("taskExtensions");
505
+ if (typeof get("defaultStatus") === "string")
506
+ cfg.defaultStatus = get("defaultStatus");
507
+ if (typeof get("defaultAssignee") === "string")
508
+ cfg.defaultAssignee = get("defaultAssignee");
509
+ if (typeof get("cacheDir") === "string")
510
+ cfg.cacheDir = get("cacheDir");
511
+ if (typeof get("strictBuild") === "boolean")
512
+ cfg.strictBuild = get("strictBuild");
513
+ const tunnelEnabled = parsed["tunnel.enabled"];
514
+ if (typeof tunnelEnabled === "boolean")
515
+ cfg.tunnelEnabled = tunnelEnabled;
516
+ // Optional release surface. No [release] block means no Releases navigation
517
+ // or API action, keeping this entirely dormant for other repositories.
518
+ const releaseEnabled = parsed["release.enabled"];
519
+ if (typeof releaseEnabled === "boolean") {
520
+ cfg.release = { ...cfg.release, enabled: releaseEnabled };
521
+ for (const key of [
522
+ "provider",
523
+ "name",
524
+ "branch",
525
+ "versionFile",
526
+ "tagPrefix",
527
+ "remote",
528
+ "repository",
529
+ "workflow",
530
+ ]) {
531
+ const value = parsed[`release.${key}`];
532
+ if (typeof value === "string")
533
+ cfg.release[key] = value;
534
+ }
535
+ }
536
+ // Optional deployments surface (#0340). A [[deployments]] array of tables —
537
+ // one per (service, branch) — is what turns the Deployments nav item and
538
+ // API on; no block at all means the feature is entirely dormant for this
539
+ // repository, exactly like [release] above. Rows with no name/branch are
540
+ // dropped rather than poisoning the page.
541
+ if (Array.isArray(parsed.deployments)) {
542
+ const rows = [];
543
+ for (const raw of parsed.deployments) {
544
+ if (typeof raw !== "object" || raw === null)
545
+ continue;
546
+ const r = raw;
547
+ if (typeof r.name !== "string" || !r.name.trim())
548
+ continue;
549
+ if (typeof r.branch !== "string" || !r.branch.trim())
550
+ continue;
551
+ const row = { name: r.name.trim(), branch: r.branch.trim() };
552
+ if (typeof r.service === "string" && r.service.trim())
553
+ row.service = r.service.trim();
554
+ if (typeof r.provider === "string" && r.provider.trim())
555
+ row.provider = r.provider.trim();
556
+ if (typeof r.url === "string" && r.url.trim())
557
+ row.url = r.url.trim();
558
+ // TOML spelling is `dashboard_url`; normalized to camelCase here.
559
+ if (typeof r.dashboard_url === "string" && r.dashboard_url.trim())
560
+ row.dashboardUrl = r.dashboard_url.trim();
561
+ if (typeof r.subdir === "string" && r.subdir.trim())
562
+ row.subdir = r.subdir.trim();
563
+ rows.push(row);
564
+ }
565
+ if (rows.length)
566
+ cfg.deployments = rows;
567
+ }
568
+ if (typeof get("ntfyEnabled") === "boolean")
569
+ cfg.ntfyEnabled = get("ntfyEnabled");
570
+ if (typeof get("ntfyTopic") === "string")
571
+ cfg.ntfyTopic = get("ntfyTopic");
572
+ if (typeof get("ntfyBaseUrl") === "string")
573
+ cfg.ntfyBaseUrl = get("ntfyBaseUrl");
574
+ const taskMode = get("defaultTaskMode");
575
+ if (taskMode === "freeform" || taskMode === "manual")
576
+ cfg.defaultTaskMode = taskMode;
577
+ if (Array.isArray(parsed.agents))
578
+ cfg.agents = parsed.agents;
579
+ if (typeof get("autoEngineeringMode") === "boolean")
580
+ cfg.autoEngineeringMode = get("autoEngineeringMode");
581
+ const maxActiveTasks = get("maxActiveTasks");
582
+ if (typeof maxActiveTasks === "number" && maxActiveTasks >= 1 && maxActiveTasks <= 20)
583
+ cfg.maxActiveTasks = maxActiveTasks;
584
+ const worktreeWarnThreshold = get("worktreeWarnThreshold");
585
+ if (typeof worktreeWarnThreshold === "number" && worktreeWarnThreshold >= 0)
586
+ cfg.worktreeWarnThreshold = Math.floor(worktreeWarnThreshold);
587
+ else if (typeof worktreeWarnThreshold === "string" && /^\d+$/.test(worktreeWarnThreshold))
588
+ cfg.worktreeWarnThreshold = Number(worktreeWarnThreshold);
589
+ const servePort = get("servePort");
590
+ const servePortNum = typeof servePort === "number"
591
+ ? servePort
592
+ : typeof servePort === "string" && /^\d+$/.test(servePort)
593
+ ? Number(servePort)
594
+ : NaN;
595
+ if (Number.isInteger(servePortNum) && servePortNum >= 1 && servePortNum <= 65535)
596
+ cfg.servePort = servePortNum;
597
+ // Older Settings builds wrote this select value as a quoted TOML string.
598
+ // Accept a strict integer string on load so existing repos immediately
599
+ // recover, while the API now writes new values as numbers.
600
+ if (typeof maxActiveTasks === "string" && /^(?:[1-9]|1\d|20)$/.test(maxActiveTasks))
601
+ cfg.maxActiveTasks = Number(maxActiveTasks);
602
+ const maxConcurrentAgents = get("maxConcurrentAgents");
603
+ if (typeof maxConcurrentAgents === "number" &&
604
+ maxConcurrentAgents >= 1 &&
605
+ maxConcurrentAgents <= 16)
606
+ cfg.maxConcurrentAgents = maxConcurrentAgents;
607
+ // [check] section (#0348) — opt-in commands for `repoos check` steps that
608
+ // are only meaningful for projects that declare them. A project can instead
609
+ // declare a `smoke` package.json script for a zero-config default; this
610
+ // config value overrides it when both are present. Both the `[check]` and
611
+ // `[checks]` spellings are accepted (the task's prose used `checks.uiSmoke`).
612
+ const checkUiSmoke = parsed["check.uiSmoke"] ?? parsed["checks.uiSmoke"];
613
+ if (typeof checkUiSmoke === "string" && checkUiSmoke.trim()) {
614
+ cfg.check = { ...cfg.check, uiSmoke: checkUiSmoke.trim() };
615
+ }
616
+ // [check] stylesheet guards (#0351) — the CSS-layering and theme-contrast
617
+ // steps read a project-declared stylesheet and token vocabulary instead of
618
+ // a RepoOS-hardcoded path and token names. A row missing its required keys
619
+ // is dropped rather than poisoning the guard.
620
+ const checkUiStylesheet = parsed["check.uiStylesheet"] ?? parsed["checks.uiStylesheet"];
621
+ if (typeof checkUiStylesheet === "string" && checkUiStylesheet.trim()) {
622
+ cfg.check = { ...cfg.check, uiStylesheet: checkUiStylesheet.trim() };
623
+ }
624
+ const checkThemeScopes = parsed["check.themeScopes"] ?? parsed["checks.themeScopes"];
625
+ if (Array.isArray(checkThemeScopes)) {
626
+ const scopes = [];
627
+ for (const raw of checkThemeScopes) {
628
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
629
+ continue;
630
+ const r = raw;
631
+ if (typeof r.selector !== "string" || !r.selector.trim())
632
+ continue;
633
+ if (typeof r.name !== "string" || !r.name.trim())
634
+ continue;
635
+ const scope = { selector: r.selector.trim(), name: r.name.trim() };
636
+ if (Array.isArray(r.inherits)) {
637
+ const inherits = r.inherits
638
+ .filter((v) => typeof v === "string" && v.trim() !== "")
639
+ .map((v) => v.trim());
640
+ if (inherits.length)
641
+ scope.inherits = inherits;
642
+ }
643
+ scopes.push(scope);
644
+ }
645
+ if (scopes.length)
646
+ cfg.check = { ...cfg.check, themeScopes: scopes };
647
+ }
648
+ const checkContrastPairs = parsed["check.contrastPairs"] ?? parsed["checks.contrastPairs"];
649
+ if (Array.isArray(checkContrastPairs)) {
650
+ const pairs = [];
651
+ for (const raw of checkContrastPairs) {
652
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
653
+ continue;
654
+ const r = raw;
655
+ if (typeof r.fg !== "string" || !r.fg.trim())
656
+ continue;
657
+ if (typeof r.bg !== "string" || !r.bg.trim())
658
+ continue;
659
+ pairs.push({ fg: r.fg.trim(), bg: r.bg.trim() });
660
+ }
661
+ if (pairs.length)
662
+ cfg.check = { ...cfg.check, contrastPairs: pairs };
663
+ }
664
+ const checkGradientTokens = parsed["check.gradientTokens"] ?? parsed["checks.gradientTokens"];
665
+ if (Array.isArray(checkGradientTokens)) {
666
+ const tokens = checkGradientTokens
667
+ .filter((v) => typeof v === "string" && v.trim() !== "")
668
+ .map((v) => v.trim());
669
+ if (tokens.length)
670
+ cfg.check = { ...cfg.check, gradientTokens: tokens };
671
+ }
672
+ const checkBackdropToken = parsed["check.backdropToken"] ?? parsed["checks.backdropToken"];
673
+ if (typeof checkBackdropToken === "string" && checkBackdropToken.trim()) {
674
+ cfg.check = { ...cfg.check, backdropToken: checkBackdropToken.trim() };
675
+ }
676
+ // [check] bare-require source roots (#0352). The guard is generic but the
677
+ // directories to scan are RepoOS-shaped, so a managed project declares its
678
+ // own; empty rows are dropped rather than poisoning the scan.
679
+ const checkBareRequireDirs = parsed["check.bareRequireDirs"] ?? parsed["checks.bareRequireDirs"];
680
+ if (Array.isArray(checkBareRequireDirs)) {
681
+ const dirs = checkBareRequireDirs
682
+ .filter((v) => typeof v === "string" && v.trim() !== "")
683
+ .map((v) => v.trim());
684
+ if (dirs.length)
685
+ cfg.check = { ...cfg.check, bareRequireDirs: dirs };
686
+ }
687
+ const checkBareRequireExcludes = parsed["check.bareRequireExcludes"] ?? parsed["checks.bareRequireExcludes"];
688
+ if (Array.isArray(checkBareRequireExcludes)) {
689
+ const excludes = checkBareRequireExcludes
690
+ .filter((v) => typeof v === "string" && v.trim() !== "")
691
+ .map((v) => v.trim());
692
+ if (excludes.length)
693
+ cfg.check = { ...cfg.check, bareRequireExcludes: excludes };
694
+ }
695
+ // [preview] section (#0362) — how to preview a task's worktree. The
696
+ // project's own command runs, selected by the task's `area`; when no
697
+ // section is declared there is no implicit default (#0370).
698
+ const preview = parsePreviewConfig(parsed);
699
+ if (preview)
700
+ cfg.preview = preview;
701
+ // [whisper] section — voice transcription for vibe-coding.
702
+ const whisperProvider = parsed["whisper.provider"];
703
+ if (typeof whisperProvider === "string" &&
704
+ ["groq", "openai", "none"].includes(whisperProvider)) {
705
+ cfg.whisper = { ...cfg.whisper, provider: whisperProvider };
706
+ }
707
+ const whisperApiKey = parsed["whisper.apiKey"];
708
+ if (typeof whisperApiKey === "string") {
709
+ cfg.whisper = { ...cfg.whisper, apiKey: whisperApiKey };
710
+ }
711
+ else {
712
+ // Env var fallbacks, provider-aware: a generic key wins, then the
713
+ // provider's own var — never send an OPENAI_API_KEY to Groq.
714
+ const provider = cfg.whisper?.provider ?? "none";
715
+ const envKey = process.env.REPOOS_WHISPER_KEY ??
716
+ (provider === "groq"
717
+ ? process.env.GROQ_API_KEY
718
+ : provider === "openai"
719
+ ? process.env.OPENAI_API_KEY
720
+ : (process.env.GROQ_API_KEY ?? process.env.OPENAI_API_KEY));
721
+ if (envKey) {
722
+ cfg.whisper = { ...cfg.whisper, apiKey: envKey };
723
+ }
724
+ }
725
+ // [watchdog] section (0180) — the task watchdog over active tasks.
726
+ const watchdogEnabled = parsed["watchdog.enabled"];
727
+ if (typeof watchdogEnabled === "boolean") {
728
+ cfg.watchdog = { ...cfg.watchdog, enabled: watchdogEnabled };
729
+ }
730
+ const watchdogStaleness = parsed["watchdog.stalenessMs"];
731
+ if (typeof watchdogStaleness === "number" && watchdogStaleness >= 60_000) {
732
+ cfg.watchdog = { ...cfg.watchdog, stalenessMs: watchdogStaleness };
733
+ }
734
+ const watchdogAutoTransition = parsed["watchdog.autoTransition"];
735
+ if (typeof watchdogAutoTransition === "boolean") {
736
+ cfg.watchdog = { ...cfg.watchdog, autoTransition: watchdogAutoTransition };
737
+ }
738
+ // [auth] section — authentication configuration.
739
+ const authEnabled = parsed["auth.enabled"];
740
+ if (typeof authEnabled === "boolean") {
741
+ cfg.auth = { ...cfg.auth, enabled: authEnabled };
742
+ }
743
+ // Secrets prefer an env var over the (git-tracked) config file, same
744
+ // fallback pattern as [whisper] above — env wins when both are set.
745
+ const authSessionSecret = parsed["auth.sessionSecret"] ?? process.env.REPOOS_AUTH_SESSION_SECRET;
746
+ if (typeof authSessionSecret === "string" && authSessionSecret) {
747
+ cfg.auth = { ...cfg.auth, sessionSecret: authSessionSecret };
748
+ }
749
+ const authSessionMaxAge = parsed["auth.sessionMaxAge"];
750
+ if (typeof authSessionMaxAge === "number" || typeof authSessionMaxAge === "string") {
751
+ const num = typeof authSessionMaxAge === "string" ? Number(authSessionMaxAge) : authSessionMaxAge;
752
+ if (Number.isFinite(num) && num > 0) {
753
+ // Accept values in days (1-1000) or seconds (>=300s). Assume values <300 are days.
754
+ const ageInSeconds = num < 300 ? num * 86400 : num;
755
+ if (ageInSeconds >= 300) {
756
+ cfg.auth = { ...cfg.auth, sessionMaxAge: ageInSeconds };
757
+ }
758
+ }
759
+ }
760
+ const authBootstrapAdmin = parsed["auth.bootstrapAdmin"];
761
+ if (typeof authBootstrapAdmin === "string") {
762
+ cfg.auth = { ...cfg.auth, bootstrapAdmin: authBootstrapAdmin };
763
+ }
764
+ // Dev backdoor OTP: env-var only, never a repoos.toml key, so it can
765
+ // never end up in a git-tracked config file. `verifyOtp` also refuses to
766
+ // honor it outside NODE_ENV !== "production" as a second guard.
767
+ const authDevBackdoorCode = process.env.REPOOS_AUTH_DEV_BACKDOOR_CODE;
768
+ if (typeof authDevBackdoorCode === "string" &&
769
+ authDevBackdoorCode &&
770
+ process.env.NODE_ENV !== "production") {
771
+ cfg.auth = { ...cfg.auth, devBackdoorCode: authDevBackdoorCode };
772
+ }
773
+ // Email provider — fromAddress isn't sensitive and stays config-only;
774
+ // apiKey may come from the config file or REPOOS_RESEND_API_KEY.
775
+ const emailProviderType = parsed["auth.emailProvider.type"];
776
+ if (typeof emailProviderType === "string" && emailProviderType === "resend") {
777
+ const emailApiKey = parsed["auth.emailProvider.apiKey"] ?? process.env.REPOOS_RESEND_API_KEY;
778
+ const emailFrom = parsed["auth.emailProvider.fromAddress"];
779
+ const emailFromName = parsed["auth.emailProvider.fromName"];
780
+ if (typeof emailApiKey === "string" && emailApiKey && typeof emailFrom === "string") {
781
+ cfg.auth = {
782
+ ...cfg.auth,
783
+ emailProvider: {
784
+ type: "resend",
785
+ apiKey: emailApiKey,
786
+ fromAddress: emailFrom,
787
+ ...(typeof emailFromName === "string" && emailFromName
788
+ ? { fromName: emailFromName }
789
+ : {}),
790
+ },
791
+ };
792
+ }
793
+ }
794
+ // Google OAuth — clientId isn't sensitive and stays config-only;
795
+ // clientSecret may come from the config file or REPOOS_GOOGLE_CLIENT_SECRET.
796
+ const googleClientId = parsed["auth.google.clientId"];
797
+ const googleClientSecret = parsed["auth.google.clientSecret"] ?? process.env.REPOOS_GOOGLE_CLIENT_SECRET;
798
+ if (typeof googleClientId === "string" &&
799
+ typeof googleClientSecret === "string" &&
800
+ googleClientSecret) {
801
+ cfg.auth = {
802
+ ...cfg.auth,
803
+ google: { clientId: googleClientId, clientSecret: googleClientSecret },
804
+ };
805
+ }
806
+ // [remoteValidation] section — run the close-out build+test on a cloud VM.
807
+ // The API token and SSH key path are env-only (never a git-tracked TOML
808
+ // key), same rule as the [auth] secrets above.
809
+ const rvEnabled = parsed["remoteValidation.enabled"];
810
+ if (typeof rvEnabled === "boolean") {
811
+ cfg.remoteValidation = { ...cfg.remoteValidation, enabled: rvEnabled };
812
+ }
813
+ const rvServerType = parsed["remoteValidation.serverType"];
814
+ if (typeof rvServerType === "string" && rvServerType) {
815
+ cfg.remoteValidation = { ...cfg.remoteValidation, serverType: rvServerType };
816
+ }
817
+ const rvLocation = parsed["remoteValidation.location"];
818
+ if (typeof rvLocation === "string" && rvLocation) {
819
+ cfg.remoteValidation = { ...cfg.remoteValidation, location: rvLocation };
820
+ }
821
+ const rvSnapshot = parsed["remoteValidation.snapshotId"];
822
+ if (typeof rvSnapshot === "string" && rvSnapshot) {
823
+ cfg.remoteValidation = { ...cfg.remoteValidation, snapshotId: rvSnapshot };
824
+ }
825
+ const rvSshKeyName = parsed["remoteValidation.sshKeyName"];
826
+ if (typeof rvSshKeyName === "string" && rvSshKeyName) {
827
+ cfg.remoteValidation = { ...cfg.remoteValidation, sshKeyName: rvSshKeyName };
828
+ }
829
+ const rvIdle = parsed["remoteValidation.idleShutdownMinutes"];
830
+ if (typeof rvIdle === "number" && rvIdle >= 0) {
831
+ cfg.remoteValidation = { ...cfg.remoteValidation, idleShutdownMinutes: rvIdle };
832
+ }
833
+ const rvMaxLife = parsed["remoteValidation.maxServerLifetimeMinutes"];
834
+ if (typeof rvMaxLife === "number" && rvMaxLife >= 10) {
835
+ cfg.remoteValidation = { ...cfg.remoteValidation, maxServerLifetimeMinutes: rvMaxLife };
836
+ }
837
+ const rvFallback = parsed["remoteValidation.fallbackToLocal"];
838
+ if (typeof rvFallback === "boolean") {
839
+ cfg.remoteValidation = { ...cfg.remoteValidation, fallbackToLocal: rvFallback };
840
+ }
841
+ const rvReleases = parsed["remoteValidation.useForReleases"];
842
+ if (typeof rvReleases === "boolean") {
843
+ cfg.remoteValidation = { ...cfg.remoteValidation, useForReleases: rvReleases };
844
+ }
845
+ }
846
+ // Model-provider API keys (0327): env-only, same rule as the [auth] secrets
847
+ // above — they must never land in the git-tracked repoos.toml. Read after
848
+ // the TOML block (env vars need no repoos.toml to exist), into the flat
849
+ // `modelProviders` config section the model-providers route consumes.
850
+ const modelProviders = {};
851
+ const openrouterApiKey = process.env.REPOOS_OPENROUTER_API_KEY;
852
+ if (openrouterApiKey)
853
+ modelProviders.openrouterApiKey = openrouterApiKey;
854
+ const opencodeGoApiKey = process.env.REPOOS_OPENCODE_GO_API_KEY;
855
+ if (opencodeGoApiKey)
856
+ modelProviders.opencodeGoApiKey = opencodeGoApiKey;
857
+ if (modelProviders.openrouterApiKey || modelProviders.opencodeGoApiKey) {
858
+ cfg.modelProviders = modelProviders;
859
+ }
860
+ cfg.builtInAgents = loadBuiltInAgentsConfig(root, cfg.cacheDir);
861
+ return cfg;
862
+ }
863
+ export function getConfigSchema() {
864
+ return [
865
+ {
866
+ key: "tunnelEnabled",
867
+ label: "Cloudflare Tunnel",
868
+ type: "boolean",
869
+ tier: "live",
870
+ restartRequired: false,
871
+ default: DEFAULT_CONFIG.tunnelEnabled,
872
+ description: "Publish local apps securely through Cloudflare Tunnel + Access",
873
+ },
874
+ {
875
+ key: "ntfyEnabled",
876
+ label: "ntfy notifications",
877
+ type: "boolean",
878
+ tier: "live",
879
+ restartRequired: false,
880
+ default: DEFAULT_CONFIG.ntfyEnabled,
881
+ description: "Send push notifications on task lifecycle events",
882
+ },
883
+ {
884
+ key: "ntfyTopic",
885
+ label: "ntfy topic",
886
+ type: "string",
887
+ tier: "live",
888
+ restartRequired: false,
889
+ default: DEFAULT_CONFIG.ntfyTopic,
890
+ description: "The ntfy topic RepoOS publishes events to (e.g. repoos_myproject)",
891
+ },
892
+ {
893
+ key: "ntfyBaseUrl",
894
+ label: "ntfy base URL",
895
+ type: "string",
896
+ tier: "guarded",
897
+ restartRequired: false,
898
+ default: DEFAULT_CONFIG.ntfyBaseUrl,
899
+ description: "Self-hosted ntfy server base URL (NTFY_BASE_URL env var overrides)",
900
+ },
901
+ {
902
+ key: "defaultStatus",
903
+ label: "Default status",
904
+ type: "select",
905
+ tier: "restart",
906
+ restartRequired: true,
907
+ default: DEFAULT_CONFIG.defaultStatus,
908
+ options: STATUSES.map((s) => ({ value: s, label: s.charAt(0).toUpperCase() + s.slice(1) })),
909
+ description: "Status assigned to new tasks",
910
+ },
911
+ {
912
+ key: "defaultTaskMode",
913
+ label: "New-task mode",
914
+ type: "select",
915
+ tier: "live",
916
+ restartRequired: false,
917
+ default: DEFAULT_CONFIG.defaultTaskMode,
918
+ options: [
919
+ { value: "freeform", label: "Freeform (AI writes the task)" },
920
+ { value: "manual", label: "Manual form" },
921
+ ],
922
+ description: "Which flow the New task drawer opens with",
923
+ },
924
+ {
925
+ key: "defaultAssignee",
926
+ label: "Default assignee",
927
+ type: "select",
928
+ tier: "restart",
929
+ restartRequired: true,
930
+ default: DEFAULT_CONFIG.defaultAssignee,
931
+ options: [
932
+ { value: "unassigned", label: "Unassigned" },
933
+ { value: "ai", label: "AI agent" },
934
+ { value: "human", label: "Human" },
935
+ ],
936
+ description: "Default assignee for new tasks",
937
+ },
938
+ {
939
+ key: "strictBuild",
940
+ label: "Strict build check",
941
+ type: "boolean",
942
+ tier: "restart",
943
+ restartRequired: true,
944
+ default: false,
945
+ description: "Exit with error on stale build instead of warning",
946
+ },
947
+ {
948
+ key: "workDir",
949
+ label: "Work directory",
950
+ type: "string",
951
+ tier: "guarded",
952
+ restartRequired: true,
953
+ default: DEFAULT_CONFIG.workDir,
954
+ description: "Directory holding task files (relative to repo root)",
955
+ },
956
+ {
957
+ key: "docsDir",
958
+ label: "Docs directory",
959
+ type: "string",
960
+ tier: "guarded",
961
+ restartRequired: true,
962
+ default: DEFAULT_CONFIG.docsDir,
963
+ description: "Directory holding context docs (relative to repo root)",
964
+ },
965
+ {
966
+ key: "skillsDir",
967
+ label: "Skills directory",
968
+ type: "string",
969
+ tier: "guarded",
970
+ restartRequired: true,
971
+ default: DEFAULT_CONFIG.skillsDir,
972
+ description: "Directory holding skills (relative to repo root)",
973
+ },
974
+ {
975
+ key: "taskExtensions",
976
+ label: "Task extensions",
977
+ type: "array",
978
+ tier: "guarded",
979
+ restartRequired: true,
980
+ default: DEFAULT_CONFIG.taskExtensions,
981
+ description: "File extensions treated as tasks (comma-separated)",
982
+ },
983
+ {
984
+ key: "cacheDir",
985
+ label: "Cache directory",
986
+ type: "string",
987
+ tier: "guarded",
988
+ restartRequired: true,
989
+ default: DEFAULT_CONFIG.cacheDir,
990
+ description: "Directory for derived index cache (relative to repo root)",
991
+ },
992
+ {
993
+ key: "autoEngineeringMode",
994
+ label: "Auto-engineering mode",
995
+ type: "boolean",
996
+ tier: "live",
997
+ restartRequired: false,
998
+ default: DEFAULT_CONFIG.autoEngineeringMode,
999
+ description: "Automatically select and start ready tasks up to the maximum",
1000
+ },
1001
+ {
1002
+ key: "maxActiveTasks",
1003
+ label: "Maximum active tasks",
1004
+ type: "select",
1005
+ tier: "live",
1006
+ restartRequired: false,
1007
+ default: DEFAULT_CONFIG.maxActiveTasks,
1008
+ options: Array.from({ length: 20 }, (_, i) => {
1009
+ const val = i + 1;
1010
+ return { value: String(val), label: String(val) };
1011
+ }),
1012
+ description: "Maximum number of simultaneously active tasks when auto-engineering mode is enabled (1-20)",
1013
+ },
1014
+ {
1015
+ key: "worktreeWarnThreshold",
1016
+ label: "Worktree warning threshold",
1017
+ type: "select",
1018
+ tier: "live",
1019
+ restartRequired: false,
1020
+ default: DEFAULT_CONFIG.worktreeWarnThreshold,
1021
+ options: [
1022
+ { value: "0", label: "Off" },
1023
+ ...[10, 15, 20, 25, 30, 40, 50].map((v) => ({ value: String(v), label: String(v) })),
1024
+ ],
1025
+ description: "Advisory ceiling on registered git worktrees. Above it, the Control page's Codebase card turns amber and the server logs a `repoos gc` reminder. Never blocks a task.",
1026
+ },
1027
+ {
1028
+ key: "maxConcurrentAgents",
1029
+ label: "Maximum concurrent agent processes",
1030
+ type: "select",
1031
+ tier: "live",
1032
+ restartRequired: false,
1033
+ default: defaultMaxConcurrentAgents(),
1034
+ options: Array.from({ length: 16 }, (_, i) => {
1035
+ const val = i + 1;
1036
+ return { value: String(val), label: String(val) };
1037
+ }),
1038
+ description: `How many agent CLI processes (start/send/chat) may run at once; extras queue. ` +
1039
+ `Default (${defaultMaxConcurrentAgents()}) is computed from this machine's CPU count.`,
1040
+ },
1041
+ {
1042
+ key: "whisper.provider",
1043
+ label: "Voice transcription provider",
1044
+ type: "select",
1045
+ tier: "live",
1046
+ group: "voice",
1047
+ restartRequired: false,
1048
+ default: DEFAULT_CONFIG.whisper?.provider ?? "none",
1049
+ options: [
1050
+ { value: "none", label: "Disabled" },
1051
+ { value: "groq", label: "Groq (whisper-large-v3)" },
1052
+ { value: "openai", label: "OpenAI (whisper-1)" },
1053
+ ],
1054
+ description: "Provider for voice-to-text transcription in text areas",
1055
+ },
1056
+ {
1057
+ key: "whisper.apiKey",
1058
+ label: "Voice transcription API key",
1059
+ type: "string",
1060
+ tier: "live",
1061
+ group: "voice",
1062
+ restartRequired: false,
1063
+ default: "",
1064
+ description: "API key for the selected provider (never sent to browser; stored in repoos.toml or REPOOS_WHISPER_KEY env var)",
1065
+ },
1066
+ {
1067
+ key: "auth.enabled",
1068
+ label: "Authentication",
1069
+ type: "boolean",
1070
+ tier: "restart",
1071
+ restartRequired: true,
1072
+ default: false,
1073
+ description: "Require login to access RepoOS (email OTP or Google OAuth)",
1074
+ },
1075
+ {
1076
+ key: "auth.sessionMaxAge",
1077
+ label: "Session duration (seconds)",
1078
+ type: "string",
1079
+ tier: "restart",
1080
+ restartRequired: true,
1081
+ default: "604800",
1082
+ description: "How long a login session lasts in seconds (default 604800 = 7 days)",
1083
+ },
1084
+ {
1085
+ key: "remoteValidation.enabled",
1086
+ label: "Remote validation runner",
1087
+ type: "boolean",
1088
+ tier: "restart",
1089
+ restartRequired: true,
1090
+ default: false,
1091
+ description: "Run the close-out build + test suite on a disposable Hetzner VM instead of this machine " +
1092
+ "(needs HETZNER_API_TOKEN + REPOOS_REMOTE_SSH_KEY env vars and [remoteValidation] snapshotId/sshKeyName in repoos.toml — see docs/remote-validation.md). Sends repo contents to a third-party host.",
1093
+ },
1094
+ {
1095
+ key: "remoteValidation.fallbackToLocal",
1096
+ label: "Remote validation: fall back to local",
1097
+ type: "boolean",
1098
+ tier: "restart",
1099
+ restartRequired: true,
1100
+ default: false,
1101
+ description: "When the remote runner is unreachable, run the full gate locally instead of keeping the task in review for retry.",
1102
+ },
1103
+ {
1104
+ key: "remoteValidation.useForReleases",
1105
+ label: "Remote validation: also validate releases",
1106
+ type: "boolean",
1107
+ tier: "restart",
1108
+ restartRequired: true,
1109
+ default: false,
1110
+ description: "Cut releases on the same Hetzner runner as close-outs (off by default — a release is watched live, so the provision delay reads as a regression; opt in per repo). Only applies when the runner is enabled.",
1111
+ },
1112
+ ];
1113
+ }
1114
+ function serializeTomlVal(val) {
1115
+ if (typeof val === "string")
1116
+ return JSON.stringify(val);
1117
+ if (typeof val === "number")
1118
+ return String(val);
1119
+ if (typeof val === "boolean")
1120
+ return val ? "true" : "false";
1121
+ if (Array.isArray(val)) {
1122
+ return `[${val.map((v) => JSON.stringify(v)).join(", ")}]`;
1123
+ }
1124
+ return String(val);
1125
+ }
1126
+ /** True when a value is an array of plain objects → serialized as [[tables]]. */
1127
+ function isTableArray(val) {
1128
+ return (Array.isArray(val) &&
1129
+ val.length > 0 &&
1130
+ val.every((v) => typeof v === "object" && v !== null && !Array.isArray(v)));
1131
+ }
1132
+ /** Serialize an array of plain objects as `[[key]]` table blocks. */
1133
+ function serializeTableArray(key, rows) {
1134
+ return rows
1135
+ .map((row) => {
1136
+ const body = Object.entries(row)
1137
+ .filter(([, v]) => v !== undefined)
1138
+ .map(([k, v]) => `${k} = ${serializeTomlVal(v)}`);
1139
+ return `[[${key}]]\n${body.join("\n")}`;
1140
+ })
1141
+ .join("\n");
1142
+ }
1143
+ /**
1144
+ * Patch a repoos.toml file with the given key-value pairs, preserving all
1145
+ * other lines (comments, formatting, unknown keys).
1146
+ */
1147
+ export function patchTomlConfig(tomlPath, patch) {
1148
+ if (!existsSync(tomlPath)) {
1149
+ writeFileSync(tomlPath, "", "utf8");
1150
+ }
1151
+ const text = readFileSync(tomlPath, "utf8");
1152
+ let result = text.replace(/\r\n/g, "\n").split("\n");
1153
+ let modified = false;
1154
+ // Array-of-tables keys ([[agents]]): drop the existing blocks and append the
1155
+ // freshly serialized ones at the end of the file.
1156
+ for (const [key, rawVal] of Object.entries(patch)) {
1157
+ if (!isTableArray(rawVal))
1158
+ continue;
1159
+ const blocks = serializeTableArray(key, rawVal);
1160
+ const kept = [];
1161
+ let i = 0;
1162
+ while (i < result.length) {
1163
+ if (stripTomlComment(result[i]).trim() === `[[${key}]]`) {
1164
+ i++;
1165
+ while (i < result.length) {
1166
+ const s = stripTomlComment(result[i]).trim();
1167
+ if (s.startsWith("["))
1168
+ break;
1169
+ i++;
1170
+ }
1171
+ continue;
1172
+ }
1173
+ kept.push(result[i]);
1174
+ i++;
1175
+ }
1176
+ while (kept.length && kept[kept.length - 1].trim() === "")
1177
+ kept.pop();
1178
+ kept.push(blocks);
1179
+ result = kept;
1180
+ modified = true;
1181
+ }
1182
+ // Scalars and plain arrays: in-place line-preserving patch.
1183
+ for (const [key, rawVal] of Object.entries(patch)) {
1184
+ if (isTableArray(rawVal))
1185
+ continue;
1186
+ const serialized = serializeTomlVal(rawVal);
1187
+ let found = false;
1188
+ for (let i = 0; i < result.length; i++) {
1189
+ const stripped = stripTomlComment(result[i]).trim();
1190
+ if (!stripped || stripped.startsWith("["))
1191
+ continue;
1192
+ const kv = stripped.match(/^([A-Za-z0-9_.-]+)\s*=\s*/);
1193
+ if (kv && kv[1] === key) {
1194
+ const indent = result[i].match(/^\s*/)?.[0] || "";
1195
+ result[i] = `${indent}${key} = ${serialized}`;
1196
+ modified = true;
1197
+ found = true;
1198
+ break;
1199
+ }
1200
+ }
1201
+ if (!found) {
1202
+ // A brand-new scalar must land at root scope. Appending at the very end
1203
+ // of the file is only safe when nothing after it is a `[section]` or
1204
+ // `[[array-of-tables]]` block — otherwise the line reads back as a
1205
+ // member of that table instead of the root config (the reader has no
1206
+ // way to know the table "ended" without a following header). Insert
1207
+ // before the first header line instead, so newly-saved keys are always
1208
+ // unambiguously root-level regardless of what tables follow.
1209
+ const firstHeaderIndex = result.findIndex((l) => stripTomlComment(l).trim().startsWith("["));
1210
+ if (firstHeaderIndex === -1) {
1211
+ result.push(`${key} = ${serialized}`);
1212
+ }
1213
+ else {
1214
+ result.splice(firstHeaderIndex, 0, `${key} = ${serialized}`);
1215
+ }
1216
+ modified = true;
1217
+ }
1218
+ }
1219
+ if (modified) {
1220
+ writeFileSync(tomlPath, result.join("\n") + "\n", "utf8");
1221
+ }
1222
+ }
1223
+ /**
1224
+ * Absolute path of the built-in agent state sidecar (a JSON file living next
1225
+ * to the cache dir). It holds runtime state — enabled/schedule/last run — that
1226
+ * the Tech Debt Agent's server-side scheduler reads and the Agents page
1227
+ * writes; it is deliberately NOT part of repoos.toml.
1228
+ */
1229
+ export function builtInAgentsPath(root, cacheDir) {
1230
+ return join(root, cacheDir ?? DEFAULT_CONFIG.cacheDir, "built-in-agents.json");
1231
+ }
1232
+ const BUILT_IN_SCHEDULES = ["daily", "weekly", "manual"];
1233
+ /** Coerce an unknown PATCH/read value into a sane BuiltInAgentConfig. */
1234
+ export function sanitizeBuiltInAgent(value) {
1235
+ if (typeof value !== "object" || value === null || Array.isArray(value))
1236
+ return null;
1237
+ const raw = value;
1238
+ const out = {};
1239
+ if (typeof raw.enabled === "boolean")
1240
+ out.enabled = raw.enabled;
1241
+ if (typeof raw.schedule === "string" && BUILT_IN_SCHEDULES.includes(raw.schedule)) {
1242
+ out.schedule = raw.schedule;
1243
+ }
1244
+ if (typeof raw.lastRunAt === "string" && !Number.isNaN(Date.parse(raw.lastRunAt))) {
1245
+ out.lastRunAt = raw.lastRunAt;
1246
+ }
1247
+ if (typeof raw.cli === "string")
1248
+ out.cli = raw.cli;
1249
+ if (typeof raw.model === "string")
1250
+ out.model = raw.model;
1251
+ return out;
1252
+ }
1253
+ /**
1254
+ * Coerce a whole record of built-in agent state (as read from the sidecar or
1255
+ * sent via PATCH) into a safe shape. Invalid entries are dropped; a valid
1256
+ * entry with no recognized fields is dropped too.
1257
+ */
1258
+ export function sanitizeBuiltInAgents(value) {
1259
+ if (typeof value !== "object" || value === null || Array.isArray(value))
1260
+ return {};
1261
+ const out = {};
1262
+ for (const [name, entry] of Object.entries(value)) {
1263
+ const clean = sanitizeBuiltInAgent(entry);
1264
+ if (clean && Object.keys(clean).length > 0)
1265
+ out[name] = clean;
1266
+ }
1267
+ return out;
1268
+ }
1269
+ /** Read the built-in agent state sidecar, or undefined when absent/unreadable. */
1270
+ export function loadBuiltInAgentsConfig(root, cacheDir) {
1271
+ const file = builtInAgentsPath(root, cacheDir);
1272
+ try {
1273
+ if (!existsSync(file))
1274
+ return undefined;
1275
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
1276
+ return sanitizeBuiltInAgents(parsed);
1277
+ }
1278
+ catch {
1279
+ // A corrupt sidecar must never block config loading — treat as empty.
1280
+ return {};
1281
+ }
1282
+ }
1283
+ /** Persist the built-in agent state sidecar, creating the cache dir as needed. */
1284
+ export function saveBuiltInAgentsConfig(root, state, cacheDir) {
1285
+ const file = builtInAgentsPath(root, cacheDir);
1286
+ mkdirSync(dirname(file), { recursive: true });
1287
+ writeFileSync(file, JSON.stringify(state, null, 2) + "\n", "utf8");
1288
+ }