@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,1838 @@
1
+ /**
2
+ * Built-in agents: pre-configured agents like Tech Debt Agent that extend RepoOS.
3
+ * These agents are triggered on-demand or on a schedule.
4
+ */
5
+ import { readdirSync, readFileSync, writeFileSync, statSync, accessSync, existsSync, mkdirSync, } from "node:fs";
6
+ import { writeFile } from "node:fs/promises";
7
+ import { join, extname, basename } from "node:path";
8
+ import { saveBuiltInAgentsConfig } from "../core/config.js";
9
+ import { commitTaskFile } from "../core/git.js";
10
+ /** Raised when the Tech Debt Agent cannot do its job at all (e.g. missing work dir). */
11
+ export class TechDebtError extends Error {
12
+ }
13
+ /** Raised when the Performance Agent cannot do its job at all (e.g. missing work dir). */
14
+ export class PerformanceError extends Error {
15
+ }
16
+ export class ArchitectureError extends Error {
17
+ }
18
+ export class DesignError extends Error {
19
+ }
20
+ const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".vue"]);
21
+ const IGNORED_DIRS = new Set(["node_modules", ".git", "dist", ".next", ".nuxt", ".repoos"]);
22
+ /** Scan is bounded so a huge repo can never stall the server. */
23
+ const MAX_SCAN_FILES = 400;
24
+ const MAX_FILE_BYTES = 400_000;
25
+ const HIGH_COMPLEXITY_LINES = 500;
26
+ const DUPLICATION_WINDOW = 6;
27
+ const MAX_DUPLICATION_ISSUES = 20;
28
+ const MAX_UNUSED_ISSUES = 10;
29
+ const MIN_EXPORT_NAME_LENGTH = 3;
30
+ /** Registry probes are bounded and best-effort: offline is never fatal. */
31
+ const MAX_REGISTRY_PROBES = 12;
32
+ const REGISTRY_CONCURRENCY = 4;
33
+ const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
34
+ /** Performance thresholds */
35
+ const SLOW_FUNCTION_LINES = 300;
36
+ const NESTED_LOOP_DEPTH = 3;
37
+ const MAX_PERF_SCAN_FILES = 500;
38
+ const MAX_PERF_ISSUES = 30;
39
+ /** Docs Debt Agent bounds (#0354): a periodic sweep, never an unbounded crawl. */
40
+ const DOC_ROOTS = ["AGENTS.md", "docs", "user-docs"];
41
+ const MAX_DOCS = 120;
42
+ const MAX_DOC_BYTES = 300_000;
43
+ const MAX_REPO_INDEX_FILES = 2_000;
44
+ /** At most this many mechanical doc fixes land per run; the rest become a task. */
45
+ export const MAX_TRIVIAL_FIXES_PER_RUN = 5;
46
+ /** Phrases that assert a hard constraint we can verify against package.json. */
47
+ const ZERO_RUNTIME_DEPS_RE = /\b(?:zero|no)\s+runtime\s+dependenc/i;
48
+ /** Backtick-quoted commands whose `<script>` must exist in package.json. */
49
+ const SCRIPT_CLAIM_RE = /^(?:bun|npm|pnpm|yarn)\s+run\s+([A-Za-z0-9:_-]+)$/;
50
+ const TEST_CLAIM_RE = /^(?:bun|npm|pnpm|yarn)\s+test$/;
51
+ /** Root-relative prefixes and files a doc may cite as a concrete path claim. */
52
+ const KNOWN_PATH_PREFIXES = [
53
+ "src/",
54
+ "docs/",
55
+ "user-docs/",
56
+ "work/",
57
+ "scripts/",
58
+ "tests/",
59
+ "inputs/",
60
+ "bin/",
61
+ ".github/",
62
+ ];
63
+ const KNOWN_ROOT_FILES = new Set([
64
+ "package.json",
65
+ "repoos.toml",
66
+ "AGENTS.md",
67
+ "CLAUDE.md",
68
+ "README.md",
69
+ "bunfig.toml",
70
+ ".env.example",
71
+ ".oxfmtrc.json",
72
+ "tsconfig.json",
73
+ "LICENSE.md",
74
+ ]);
75
+ /** Platform globals that look like camelCase but are not repo symbols. */
76
+ const NON_REPO_SYMBOLS = new Set(["localStorage", "sessionStorage", "indexedDB"]);
77
+ /** Walk the repo for scannable source files, bounded in count and size. */
78
+ function collectSourceFiles(root) {
79
+ const files = [];
80
+ const walk = (dir, relPrefix) => {
81
+ let entries;
82
+ try {
83
+ entries = readdirSync(dir);
84
+ }
85
+ catch {
86
+ return;
87
+ }
88
+ for (const name of entries) {
89
+ if (files.length >= MAX_SCAN_FILES)
90
+ return;
91
+ const abs = join(dir, name);
92
+ let st;
93
+ try {
94
+ st = statSync(abs);
95
+ }
96
+ catch {
97
+ continue;
98
+ }
99
+ if (st.isDirectory()) {
100
+ if (IGNORED_DIRS.has(name))
101
+ continue;
102
+ walk(abs, relPrefix ? `${relPrefix}/${name}` : name);
103
+ }
104
+ else if (st.isFile() && SOURCE_EXTS.has(extname(name)) && st.size <= MAX_FILE_BYTES) {
105
+ files.push(abs);
106
+ }
107
+ }
108
+ };
109
+ walk(root, "");
110
+ return files;
111
+ }
112
+ function readScannedFiles(root, files) {
113
+ const out = [];
114
+ for (const abs of files) {
115
+ try {
116
+ const content = readFileSync(abs, "utf8");
117
+ out.push({
118
+ rel: abs.slice(root.length).replace(/^[/\\]/, "") || abs,
119
+ content,
120
+ lineCount: content.split("\n").length,
121
+ });
122
+ }
123
+ catch {
124
+ // Skip unreadable files
125
+ }
126
+ }
127
+ return out;
128
+ }
129
+ function findLineAt(content, index) {
130
+ let line = 1;
131
+ const end = Math.min(index, content.length);
132
+ for (let i = 0; i < end; i++) {
133
+ if (content.charCodeAt(i) === 10 /* \n */)
134
+ line++;
135
+ }
136
+ return line;
137
+ }
138
+ /**
139
+ * Replace comments and string literals with whitespace (preserving newlines)
140
+ * so later regexes never match code that only mentions a pattern. The returned
141
+ * string is the same length as the input, so indexes map 1:1 to the original.
142
+ */
143
+ export function stripCommentsAndStrings(source) {
144
+ const chars = source.split("");
145
+ const n = chars.length;
146
+ const blank = (start, end) => {
147
+ for (let j = start; j < end && j < n; j++) {
148
+ chars[j] = chars[j] === "\n" ? "\n" : " ";
149
+ }
150
+ };
151
+ let i = 0;
152
+ while (i < n) {
153
+ const c = chars[i];
154
+ if (c === "/" && chars[i + 1] === "/") {
155
+ let end = i + 2;
156
+ while (end < n && chars[end] !== "\n")
157
+ end++;
158
+ blank(i, end);
159
+ i = end;
160
+ continue;
161
+ }
162
+ if (c === "/" && chars[i + 1] === "*") {
163
+ let end = source.indexOf("*/", i + 2);
164
+ end = end === -1 ? n : end + 2;
165
+ blank(i, end);
166
+ i = end;
167
+ continue;
168
+ }
169
+ if (c === '"' || c === "'" || c === "`") {
170
+ const quote = c;
171
+ let end = i + 1;
172
+ while (end < n && chars[end] !== quote) {
173
+ if (chars[end] === "\\")
174
+ end++;
175
+ end++;
176
+ }
177
+ end = end < n ? end + 1 : end;
178
+ blank(i, end);
179
+ i = end;
180
+ continue;
181
+ }
182
+ i++;
183
+ }
184
+ return chars.join("");
185
+ }
186
+ /** Map a "1.2.3", "^1.2.3", "~1.2.3", ">=1.0.0"… specifier to its major number. */
187
+ function parseMajor(spec) {
188
+ const match = spec.match(/(\d+)/);
189
+ if (!match)
190
+ return null;
191
+ const major = Number(match[1]);
192
+ return Number.isInteger(major) ? major : null;
193
+ }
194
+ async function fetchLatestVersion(name, fetchImpl) {
195
+ try {
196
+ const res = await fetchImpl(`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`, {
197
+ signal: AbortSignal.timeout(5000),
198
+ });
199
+ if (!res.ok)
200
+ return null;
201
+ const body = (await res.json());
202
+ return typeof body.version === "string" ? body.version : null;
203
+ }
204
+ catch {
205
+ return null;
206
+ }
207
+ }
208
+ async function mapLimit(items, limit, fn) {
209
+ const out = new Array(items.length);
210
+ let next = 0;
211
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
212
+ while (true) {
213
+ const idx = next++;
214
+ if (idx >= items.length)
215
+ return;
216
+ out[idx] = await fn(items[idx]);
217
+ }
218
+ });
219
+ await Promise.all(workers);
220
+ return out;
221
+ }
222
+ async function checkOutdatedDependencies(config, fetchImpl) {
223
+ const issues = [];
224
+ const packageJsonPath = join(config.root, "package.json");
225
+ let pkg;
226
+ try {
227
+ pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
228
+ }
229
+ catch {
230
+ // File doesn't exist or isn't valid JSON — nothing to check.
231
+ return { issues, checked: 0 };
232
+ }
233
+ const raw = {
234
+ ...(typeof pkg.dependencies === "object" && pkg.dependencies !== null
235
+ ? pkg.dependencies
236
+ : {}),
237
+ ...(typeof pkg.devDependencies === "object" && pkg.devDependencies !== null
238
+ ? pkg.devDependencies
239
+ : {}),
240
+ };
241
+ // Pre-release/wildcard pins are always suspicious, independent of the registry.
242
+ for (const [name, version] of Object.entries(raw)) {
243
+ const versionStr = String(version);
244
+ if (versionStr.includes("alpha") || versionStr.includes("beta") || versionStr.includes("*")) {
245
+ issues.push({
246
+ type: "outdated-dependency",
247
+ file: "package.json",
248
+ description: `Dependency "${name}" uses pre-release or wildcard version: ${versionStr}`,
249
+ severity: "medium",
250
+ });
251
+ }
252
+ }
253
+ // Compare exact/range pins against the registry's latest, best-effort and
254
+ // bounded (offline registries or slow networks must never block the scan).
255
+ const candidates = Object.entries(raw)
256
+ .filter(([, version]) => {
257
+ const s = String(version);
258
+ return (!s.includes("alpha") && !s.includes("beta") && !s.includes("*") && parseMajor(s) !== null);
259
+ })
260
+ .slice(0, MAX_REGISTRY_PROBES);
261
+ const results = await mapLimit(candidates, REGISTRY_CONCURRENCY, async ([name, version]) => {
262
+ const latest = await fetchLatestVersion(name, fetchImpl);
263
+ return { name, version: String(version), latest };
264
+ });
265
+ let checked = 0;
266
+ for (const { name, version, latest } of results) {
267
+ if (!latest)
268
+ continue;
269
+ checked++;
270
+ const installed = parseMajor(version);
271
+ const remote = parseMajor(latest);
272
+ if (installed === null || remote === null || remote <= installed)
273
+ continue;
274
+ issues.push({
275
+ type: "outdated-dependency",
276
+ file: "package.json",
277
+ description: `Dependency "${name}" is outdated: installed ${version}, latest is ${latest}`,
278
+ severity: remote - installed >= 2 ? "high" : "medium",
279
+ });
280
+ }
281
+ return { issues, checked };
282
+ }
283
+ /**
284
+ * Scan the repository for tech debt patterns.
285
+ * Returns the identified issues plus scan bounds, so callers can tell a
286
+ * successful-but-empty scan apart from a failed one.
287
+ */
288
+ export async function scanForTechDebt(config, options = {}) {
289
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
290
+ const issues = [];
291
+ const depCheck = await checkOutdatedDependencies(config, fetchImpl);
292
+ issues.push(...depCheck.issues);
293
+ const files = collectSourceFiles(config.root);
294
+ const scanned = readScannedFiles(config.root, files);
295
+ const windowMap = new Map();
296
+ const exported = [];
297
+ const EXPORT_PATTERNS = [
298
+ /\bexport\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/g,
299
+ /\bexport\s+(?:declare\s+)?const\s+([A-Za-z_$][\w$]*)/g,
300
+ /\bexport\s+(?:declare\s+)?let\s+([A-Za-z_$][\w$]*)/g,
301
+ /\bexport\s+(?:declare\s+)?class\s+([A-Za-z_$][\w$]*)/g,
302
+ /\bexport\s+(?:declare\s+)?interface\s+([A-Za-z_$][\w$]*)/g,
303
+ /\bexport\s+type\s+([A-Za-z_$][\w$]*)/g,
304
+ ];
305
+ for (const file of scanned) {
306
+ // 1. High-complexity (simple heuristic: very long files).
307
+ if (file.lineCount > HIGH_COMPLEXITY_LINES) {
308
+ issues.push({
309
+ type: "high-complexity",
310
+ file: file.rel,
311
+ line: 1,
312
+ description: `File has ${file.lineCount} lines of code — consider breaking it into smaller modules`,
313
+ severity: "medium",
314
+ });
315
+ }
316
+ // 2. Deprecated patterns: 'var' declarations, matched on comment/string-free code.
317
+ const cleaned = stripCommentsAndStrings(file.content);
318
+ const varRe = /\bvar\s+[A-Za-z_$][\w$]*/g;
319
+ let m;
320
+ while ((m = varRe.exec(cleaned)) !== null) {
321
+ issues.push({
322
+ type: "deprecated-api",
323
+ file: file.rel,
324
+ line: findLineAt(file.content, m.index),
325
+ description: "File uses 'var' declarations — modernize to 'const' or 'let'",
326
+ severity: "low",
327
+ });
328
+ }
329
+ // 3. Duplication windows: identical blocks of >=6 non-blank lines.
330
+ const lines = file.content.split("\n");
331
+ const blocks = [];
332
+ for (let i = 0; i < lines.length; i++) {
333
+ const text = lines[i].trim();
334
+ if (text)
335
+ blocks.push({ text, line: i + 1 });
336
+ }
337
+ if (blocks.length >= DUPLICATION_WINDOW) {
338
+ for (let i = 0; i + DUPLICATION_WINDOW <= blocks.length; i++) {
339
+ let key = "";
340
+ for (let j = 0; j < DUPLICATION_WINDOW; j++) {
341
+ key += (j === 0 ? "" : "\u0000") + blocks[i + j].text;
342
+ }
343
+ const locs = windowMap.get(key);
344
+ const loc = { file: file.rel, line: blocks[i].line };
345
+ if (locs)
346
+ locs.push(loc);
347
+ else
348
+ windowMap.set(key, [loc]);
349
+ }
350
+ }
351
+ // 4. Exported identifiers (for the unused-code check).
352
+ for (const re of EXPORT_PATTERNS) {
353
+ let em;
354
+ while ((em = re.exec(file.content)) !== null) {
355
+ exported.push({ name: em[1], file: file.rel, line: findLineAt(file.content, em.index) });
356
+ }
357
+ }
358
+ }
359
+ // Cross-file duplication: a block that appears in >=2 different files.
360
+ let dupCount = 0;
361
+ for (const [key, locs] of windowMap) {
362
+ if (dupCount >= MAX_DUPLICATION_ISSUES)
363
+ break;
364
+ if (locs.length < 2)
365
+ continue;
366
+ const filesInvolved = new Set(locs.map((l) => l.file));
367
+ if (filesInvolved.size < 2)
368
+ continue;
369
+ const primary = locs[0];
370
+ const duplicatedIn = locs
371
+ .slice(1)
372
+ .filter((l) => l.file !== primary.file)
373
+ .map((l) => `\`${l.file}\`:${l.line}`)
374
+ .slice(0, 3)
375
+ .join(", ");
376
+ const parts = key.split("\u0000");
377
+ const snippet = parts.slice(0, 3).join(" | ");
378
+ issues.push({
379
+ type: "code-duplication",
380
+ file: primary.file,
381
+ line: primary.line,
382
+ description: `Identical ${DUPLICATION_WINDOW}-line block also found in ${duplicatedIn} — extract it into a shared helper ("${snippet}${parts.length > 3 ? "…" : ""}")`,
383
+ severity: "low",
384
+ });
385
+ dupCount++;
386
+ }
387
+ // Unused exports: an exported identifier that no other file references.
388
+ let unusedCount = 0;
389
+ for (const decl of exported) {
390
+ if (unusedCount >= MAX_UNUSED_ISSUES)
391
+ break;
392
+ if (decl.name.length < MIN_EXPORT_NAME_LENGTH)
393
+ continue;
394
+ const nameRe = new RegExp(`\\b${decl.name}\\b`);
395
+ const referencedElsewhere = scanned.some((f) => f.rel !== decl.file && nameRe.test(f.content));
396
+ if (referencedElsewhere)
397
+ continue;
398
+ issues.push({
399
+ type: "unused-code",
400
+ file: decl.file,
401
+ line: decl.line,
402
+ description: `Exported "${decl.name}" is never referenced by any other file — consider removing it`,
403
+ severity: "low",
404
+ });
405
+ unusedCount++;
406
+ }
407
+ return { issues, scannedFiles: scanned.length, checkedDependencies: depCheck.checked };
408
+ }
409
+ /**
410
+ * Create tasks in the inbox for tech debt issues.
411
+ * Returns counts for created and failed writes, so callers never see a
412
+ * silently-truncated task list. Throws TechDebtError when the work dir itself
413
+ * is unusable (missing or read-only) — the caller surfaces that to the user.
414
+ */
415
+ export async function createTechDebtTasks(config, issues) {
416
+ const workDir = join(config.root, config.workDir);
417
+ let workDirStats;
418
+ try {
419
+ workDirStats = statSync(workDir);
420
+ }
421
+ catch {
422
+ throw new TechDebtError(`Task directory "${config.workDir}" does not exist — create it (or fix workDir) before running the Tech Debt Agent`);
423
+ }
424
+ if (!workDirStats.isDirectory()) {
425
+ throw new TechDebtError(`Task directory "${config.workDir}" is not a directory`);
426
+ }
427
+ try {
428
+ accessSync(workDir, 0o2 /* W_OK */);
429
+ }
430
+ catch {
431
+ throw new TechDebtError(`Task directory "${config.workDir}" is not writable`);
432
+ }
433
+ const result = { created: 0, failed: 0, errors: [] };
434
+ if (issues.length === 0)
435
+ return result;
436
+ // Group issues by type for cleaner task creation.
437
+ const grouped = new Map();
438
+ for (const issue of issues) {
439
+ if (!grouped.has(issue.type))
440
+ grouped.set(issue.type, []);
441
+ grouped.get(issue.type).push(issue);
442
+ }
443
+ const now = new Date().toISOString();
444
+ for (const [type, typeIssues] of grouped) {
445
+ const title = getTitleForIssueType(type);
446
+ const body = formatIssuesForTask(typeIssues);
447
+ const taskId = findNextTaskId(workDir);
448
+ const taskPath = join(workDir, `${taskId}-${slugify(title)}.md`);
449
+ // The title is JSON-stringified: it always survives YAML parsing, even with
450
+ // colons, quotes, or other metacharacters.
451
+ const frontmatter = `---
452
+ id: "${taskId}"
453
+ title: ${JSON.stringify(title)}
454
+ type: chore
455
+ status: inbox
456
+ priority: p2
457
+ area: tech-debt
458
+ assigned_to: unassigned
459
+ created_by: tech-debt-agent
460
+ created_at: "${now}"
461
+ updated_at: "${now}"
462
+ ---`;
463
+ const taskContent = `${frontmatter}\n${body}`;
464
+ try {
465
+ await writeFile(taskPath, taskContent, "utf8");
466
+ result.created++;
467
+ }
468
+ catch (err) {
469
+ result.failed++;
470
+ result.errors.push(`${taskPath}: ${err instanceof Error ? err.message : String(err)}`);
471
+ }
472
+ }
473
+ return result;
474
+ }
475
+ /**
476
+ * Whether a built-in agent is due for a scheduled run right now. Manual-only
477
+ * agents never auto-run; a never-run enabled agent is due immediately.
478
+ */
479
+ export function isDueForScheduledRun(state, now = new Date()) {
480
+ if (!state?.enabled)
481
+ return false;
482
+ if (state.schedule === "manual" || state.schedule === undefined)
483
+ return false;
484
+ if (!state.lastRunAt)
485
+ return true;
486
+ const last = Date.parse(state.lastRunAt);
487
+ if (Number.isNaN(last))
488
+ return true;
489
+ if (state.schedule === "daily") {
490
+ const lastD = new Date(last);
491
+ return (lastD.getUTCFullYear() !== now.getUTCFullYear() ||
492
+ lastD.getUTCMonth() !== now.getUTCMonth() ||
493
+ lastD.getUTCDate() !== now.getUTCDate());
494
+ }
495
+ return now.getTime() - last >= WEEK_MS;
496
+ }
497
+ /**
498
+ * Scan the repository for performance issues.
499
+ * Returns the identified issues plus scan bounds, so callers can tell a
500
+ * successful-but-empty scan apart from a failed one.
501
+ */
502
+ export async function scanForPerformanceIssues(config) {
503
+ const issues = [];
504
+ const files = collectSourceFiles(config.root);
505
+ const scanned = readScannedFiles(config.root, files);
506
+ let perfIssuesCount = 0;
507
+ for (const file of scanned) {
508
+ if (perfIssuesCount >= MAX_PERF_ISSUES)
509
+ break;
510
+ const lines = file.content.split("\n");
511
+ // 1. Functions that are too long (likely doing too much).
512
+ if (file.lineCount > SLOW_FUNCTION_LINES) {
513
+ issues.push({
514
+ type: "slow-function",
515
+ file: file.rel,
516
+ line: 1,
517
+ description: `Function/file is ${file.lineCount} lines long — consider breaking it into smaller functions for better performance and readability`,
518
+ severity: "medium",
519
+ });
520
+ perfIssuesCount++;
521
+ }
522
+ // 2. Detect deeply nested loops (N² or worse performance).
523
+ const cleaned = stripCommentsAndStrings(file.content);
524
+ let maxDepth = 0;
525
+ let currentDepth = 0;
526
+ let lineNum = 1;
527
+ let maxDepthLine = 1;
528
+ for (let i = 0; i < cleaned.length; i++) {
529
+ const char = cleaned[i];
530
+ if (char === "\n")
531
+ lineNum++;
532
+ if (char === "{" &&
533
+ cleaned.substring(Math.max(0, i - 50), i).match(/\b(?:for|while|forEach)\s*[\(\{]/)) {
534
+ currentDepth++;
535
+ if (currentDepth > maxDepth) {
536
+ maxDepth = currentDepth;
537
+ maxDepthLine = lineNum;
538
+ }
539
+ }
540
+ if (char === "}")
541
+ currentDepth = Math.max(0, currentDepth - 1);
542
+ }
543
+ if (maxDepth >= NESTED_LOOP_DEPTH) {
544
+ issues.push({
545
+ type: "blocking-operation",
546
+ file: file.rel,
547
+ line: maxDepthLine,
548
+ description: `Nested loops detected (depth ${maxDepth}) — this could cause O(n²) or worse performance; consider refactoring`,
549
+ severity: maxDepth > 4 ? "high" : "medium",
550
+ });
551
+ perfIssuesCount++;
552
+ }
553
+ // 3. Look for synchronous operations that should be async.
554
+ const syncPatterns = [
555
+ { pattern: /\bfs\.readFileSync\b/, desc: "Synchronous file read blocks the event loop" },
556
+ { pattern: /\bfs\.writeFileSync\b/, desc: "Synchronous file write blocks the event loop" },
557
+ {
558
+ pattern: /\bJSON\.stringify\(.*\)\s*;/,
559
+ desc: "Large object serialization could block; consider streaming",
560
+ },
561
+ ];
562
+ for (const { pattern, desc } of syncPatterns) {
563
+ if (pattern.test(cleaned)) {
564
+ let m;
565
+ const globalPattern = new RegExp(pattern.source, "g");
566
+ while ((m = globalPattern.exec(cleaned)) !== null) {
567
+ if (perfIssuesCount >= MAX_PERF_ISSUES)
568
+ break;
569
+ issues.push({
570
+ type: "blocking-operation",
571
+ file: file.rel,
572
+ line: findLineAt(file.content, m.index),
573
+ description: `${desc}`,
574
+ severity: "high",
575
+ });
576
+ perfIssuesCount++;
577
+ }
578
+ }
579
+ }
580
+ // 4. Detect potential unbounded growth (array/object accumulation without cleanup).
581
+ const unboundedPatterns = [
582
+ { pattern: /\w+\.push\s*\(/g, label: "array push" },
583
+ { pattern: /Map\s*\(/g, label: "Map construction" },
584
+ ];
585
+ for (const { pattern, label } of unboundedPatterns) {
586
+ if (perfIssuesCount >= MAX_PERF_ISSUES)
587
+ break;
588
+ const matches = (cleaned.match(pattern) || []).length;
589
+ if (matches > 10) {
590
+ issues.push({
591
+ type: "unbounded-growth",
592
+ file: file.rel,
593
+ line: 1,
594
+ description: `File uses "${label}" frequently (${matches} times) — ensure proper cleanup to prevent memory leaks`,
595
+ severity: "low",
596
+ });
597
+ perfIssuesCount++;
598
+ break;
599
+ }
600
+ }
601
+ // 5. Detect duplicate computations (expensive operations inside loops).
602
+ const lines_trimmed = lines.map((l) => l.trim()).filter((l) => l);
603
+ for (let i = 0; i < lines_trimmed.length - 2; i++) {
604
+ if (perfIssuesCount >= MAX_PERF_ISSUES)
605
+ break;
606
+ const line = lines_trimmed[i];
607
+ // Match loop constructs: for(...), while(...), do...while
608
+ const loopMatch = /^\s*(for|while|do)\s*[\(\{]/.test(line);
609
+ if (loopMatch) {
610
+ // Look for expensive operations in the next few lines
611
+ const loopBody = lines_trimmed.slice(i + 1, Math.min(i + 5)).join(" ");
612
+ if (loopBody.includes("JSON.parse") ||
613
+ loopBody.includes("JSON.stringify") ||
614
+ loopBody.includes("fetch") ||
615
+ loopBody.includes("database") ||
616
+ loopBody.includes("query")) {
617
+ // Find the actual line number by searching for the loop start from position i
618
+ // Count lines up to this point
619
+ let lineNum = 1;
620
+ let charIndex = 0;
621
+ for (let j = 0; j < lines.length; j++) {
622
+ const currentLine = lines[j].trim();
623
+ if (currentLine === line) {
624
+ lineNum = j + 1;
625
+ break;
626
+ }
627
+ }
628
+ issues.push({
629
+ type: "duplicated-computation",
630
+ file: file.rel,
631
+ line: lineNum,
632
+ description: `Potentially expensive operation detected inside loop — move it outside the loop if possible`,
633
+ severity: "medium",
634
+ });
635
+ perfIssuesCount++;
636
+ }
637
+ }
638
+ }
639
+ }
640
+ return { issues, scannedFiles: scanned.length };
641
+ }
642
+ /**
643
+ * Create tasks in the inbox for performance issues.
644
+ * Returns counts for created and failed writes, so callers never see a
645
+ * silently-truncated task list. Throws PerformanceError when the work dir itself
646
+ * is unusable (missing or read-only).
647
+ */
648
+ export async function createPerformanceTasks(config, issues) {
649
+ const workDir = join(config.root, config.workDir);
650
+ let workDirStats;
651
+ try {
652
+ workDirStats = statSync(workDir);
653
+ }
654
+ catch {
655
+ throw new PerformanceError(`Task directory "${config.workDir}" does not exist — create it (or fix workDir) before running the Performance Agent`);
656
+ }
657
+ if (!workDirStats.isDirectory()) {
658
+ throw new PerformanceError(`Task directory "${config.workDir}" is not a directory`);
659
+ }
660
+ try {
661
+ accessSync(workDir, 0o2 /* W_OK */);
662
+ }
663
+ catch {
664
+ throw new PerformanceError(`Task directory "${config.workDir}" is not writable`);
665
+ }
666
+ const result = { created: 0, failed: 0, errors: [] };
667
+ if (issues.length === 0)
668
+ return result;
669
+ // Group issues by type for cleaner task creation.
670
+ const grouped = new Map();
671
+ for (const issue of issues) {
672
+ if (!grouped.has(issue.type))
673
+ grouped.set(issue.type, []);
674
+ grouped.get(issue.type).push(issue);
675
+ }
676
+ const now = new Date().toISOString();
677
+ for (const [type, typeIssues] of grouped) {
678
+ const title = getTitleForPerformanceIssueType(type);
679
+ const body = formatPerformanceIssuesForTask(typeIssues);
680
+ const taskId = findNextTaskId(workDir);
681
+ const taskPath = join(workDir, `${taskId}-${slugify(title)}.md`);
682
+ const frontmatter = `---
683
+ id: "${taskId}"
684
+ title: ${JSON.stringify(title)}
685
+ type: chore
686
+ status: inbox
687
+ priority: p2
688
+ area: performance
689
+ assigned_to: unassigned
690
+ created_by: performance-agent
691
+ created_at: "${now}"
692
+ updated_at: "${now}"
693
+ ---`;
694
+ const taskContent = `${frontmatter}\n${body}`;
695
+ try {
696
+ await writeFile(taskPath, taskContent, "utf8");
697
+ result.created++;
698
+ }
699
+ catch (err) {
700
+ result.failed++;
701
+ result.errors.push(`${taskPath}: ${err instanceof Error ? err.message : String(err)}`);
702
+ }
703
+ }
704
+ return result;
705
+ }
706
+ /**
707
+ * Run the Tech Debt Agent end to end: scan, create tasks, record lastRunAt.
708
+ * The caller owns overlap protection (a single in-flight guard in server.ts).
709
+ */
710
+ export async function runTechDebtAgent(config, options = {}, logger) {
711
+ logger?.agent("tech-debt", "info", "Tech Debt Agent scan started");
712
+ const scan = await scanForTechDebt(config, options);
713
+ logger?.agent("tech-debt", "info", `Tech Debt scan completed`, {
714
+ issuesFound: scan.issues.length,
715
+ scannedFiles: scan.scannedFiles,
716
+ });
717
+ const created = await createTechDebtTasks(config, scan.issues);
718
+ if (created.failed > 0) {
719
+ logger?.agent("tech-debt", "error", `Failed to create ${created.failed} tech debt tasks`, {
720
+ errors: created.errors,
721
+ });
722
+ }
723
+ if (created.created > 0) {
724
+ logger?.agent("tech-debt", "info", `Created ${created.created} tech debt tasks`);
725
+ }
726
+ const agents = { ...(config.builtInAgents ?? {}) };
727
+ agents["tech-debt"] = { ...(agents["tech-debt"] ?? {}), lastRunAt: new Date().toISOString() };
728
+ saveBuiltInAgentsConfig(config.root, agents, config.cacheDir);
729
+ config.builtInAgents = agents;
730
+ logger?.agent("tech-debt", "info", "Tech Debt Agent run completed", {
731
+ created: created.created,
732
+ failed: created.failed,
733
+ });
734
+ return {
735
+ issuesFound: scan.issues.length,
736
+ scannedFiles: scan.scannedFiles,
737
+ ...created,
738
+ };
739
+ }
740
+ /**
741
+ * Run the Performance Agent end to end: scan, create tasks, record lastRunAt.
742
+ * The caller owns overlap protection (a single in-flight guard in server.ts).
743
+ */
744
+ export async function runPerformanceAgent(config) {
745
+ const scan = await scanForPerformanceIssues(config);
746
+ const created = await createPerformanceTasks(config, scan.issues);
747
+ const agents = { ...(config.builtInAgents ?? {}) };
748
+ agents["performance"] = { ...(agents["performance"] ?? {}), lastRunAt: new Date().toISOString() };
749
+ saveBuiltInAgentsConfig(config.root, agents, config.cacheDir);
750
+ config.builtInAgents = agents;
751
+ return {
752
+ issuesFound: scan.issues.length,
753
+ scannedFiles: scan.scannedFiles,
754
+ ...created,
755
+ };
756
+ }
757
+ /**
758
+ * Scan the repository for architectural issues and opportunities.
759
+ */
760
+ export async function scanForArchitectureIssues(config) {
761
+ const issues = [];
762
+ const insights = [];
763
+ const files = collectSourceFiles(config.root);
764
+ const scanned = readScannedFiles(config.root, files);
765
+ const dirCounts = new Map();
766
+ for (const file of scanned) {
767
+ const parts = file.rel.split("/");
768
+ if (parts.length > 1) {
769
+ const dir = parts[0];
770
+ dirCounts.set(dir, (dirCounts.get(dir) ?? 0) + 1);
771
+ }
772
+ }
773
+ const smallDirs = Array.from(dirCounts.entries()).filter(([, count]) => count > 20);
774
+ if (smallDirs.length > 0) {
775
+ insights.push(`Found ${smallDirs.length} directories with >20 files each. Consider consolidating or restructuring for better maintainability.`);
776
+ }
777
+ const importPattern = /(?:import|from)\s+['"](\.\.?\/[^'"]+)['"]/g;
778
+ let maxDependencies = 0;
779
+ let maxDepFile = "";
780
+ for (const file of scanned) {
781
+ const cleaned = stripCommentsAndStrings(file.content);
782
+ const fileImports = new Set();
783
+ let m;
784
+ while ((m = importPattern.exec(cleaned)) !== null) {
785
+ fileImports.add(m[1]);
786
+ }
787
+ if (fileImports.size > maxDependencies) {
788
+ maxDependencies = fileImports.size;
789
+ maxDepFile = file.rel;
790
+ }
791
+ }
792
+ if (maxDependencies > 8) {
793
+ issues.push({
794
+ type: "tight-coupling",
795
+ file: maxDepFile,
796
+ description: `File has ${maxDependencies} internal dependencies — consider refactoring to reduce coupling`,
797
+ severity: "medium",
798
+ recommendation: "Extract common functionality into shared utilities and use dependency injection.",
799
+ });
800
+ }
801
+ const patternSignatures = [/\binterface\s+\w+/g, /\btype\s+\w+\s*=/g, /\bclass\s+\w+/g];
802
+ const patterns = new Map();
803
+ for (const file of scanned) {
804
+ const cleaned = stripCommentsAndStrings(file.content);
805
+ for (const re of patternSignatures) {
806
+ let m;
807
+ while ((m = re.exec(cleaned)) !== null) {
808
+ const sig = m[0];
809
+ if (!patterns.has(sig))
810
+ patterns.set(sig, { pattern: sig, files: [] });
811
+ patterns.get(sig).files.push(file.rel);
812
+ }
813
+ }
814
+ }
815
+ for (const [, match] of patterns) {
816
+ if (match.files.length >= 5 && match.files.length <= 10) {
817
+ issues.push({
818
+ type: "missing-abstraction",
819
+ description: `Pattern "${match.pattern}" repeated across ${match.files.length} files — consider extracting into a shared abstraction`,
820
+ severity: "low",
821
+ recommendation: "Review the pattern and create a reusable base type or utility.",
822
+ });
823
+ break;
824
+ }
825
+ }
826
+ const complexCount = scanned.filter((f) => f.content.includes("abstract") || f.content.includes("decorator")).length;
827
+ if (complexCount > 5) {
828
+ issues.push({
829
+ type: "over-engineering",
830
+ description: `Repository uses advanced patterns in ${complexCount} files — ensure they justify the complexity`,
831
+ severity: "low",
832
+ recommendation: "Review whether all abstractions add value or could be simplified.",
833
+ });
834
+ }
835
+ const largeFiles = scanned.filter((f) => f.lineCount > 1000);
836
+ if (largeFiles.length > 3) {
837
+ issues.push({
838
+ type: "scalability-risk",
839
+ description: `${largeFiles.length} files exceed 1000 lines — these may be bottlenecks as the system scales`,
840
+ severity: "medium",
841
+ recommendation: "Consider breaking large files into smaller modules with clear responsibilities.",
842
+ });
843
+ }
844
+ if (scanned.length > 0) {
845
+ insights.push(`Analyzed ${scanned.length} source files across ${dirCounts.size} directories.`);
846
+ }
847
+ const workDir = join(config.root, config.workDir);
848
+ let taskCount = 0;
849
+ let activeArchTasks = 0;
850
+ try {
851
+ const taskFiles = readdirSync(workDir);
852
+ taskCount = taskFiles.filter((f) => f.endsWith(".md")).length;
853
+ for (const taskFile of taskFiles) {
854
+ if (!taskFile.endsWith(".md"))
855
+ continue;
856
+ try {
857
+ const content = readFileSync(join(workDir, taskFile), "utf8");
858
+ if (content.includes("architecture") ||
859
+ content.includes("design") ||
860
+ content.includes("refactor")) {
861
+ activeArchTasks++;
862
+ }
863
+ }
864
+ catch {
865
+ /* skip unreadable files */
866
+ }
867
+ }
868
+ }
869
+ catch {
870
+ /* work directory might not exist */
871
+ }
872
+ if (activeArchTasks > 0) {
873
+ insights.push(`Found ${activeArchTasks} active tasks related to architecture and design decisions.`);
874
+ }
875
+ return { issues, scannedFiles: scanned.length, taskCount, insights };
876
+ }
877
+ /**
878
+ * Generate a markdown architecture report and save it with a timestamp.
879
+ */
880
+ export async function generateArchitectureReport(config, scan) {
881
+ const reportDir = join(config.root, "docs", "agents", "Architect");
882
+ mkdirSync(reportDir, { recursive: true });
883
+ const now = new Date();
884
+ const ts = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`;
885
+ const fileName = `Architect_report_${ts}.md`;
886
+ const reportPath = join(reportDir, fileName);
887
+ let report = `# Architecture Review Report\n\n`;
888
+ report += `**Generated**: ${now.toISOString()}\n\n`;
889
+ report += `## Executive Summary\n\n`;
890
+ report += `- **Files Scanned**: ${scan.scannedFiles}\n`;
891
+ report += `- **Tasks in Backlog**: ${scan.taskCount}\n`;
892
+ report += `- **Issues Identified**: ${scan.issues.length}\n\n`;
893
+ if (scan.insights.length > 0) {
894
+ report += `## Key Insights\n\n`;
895
+ for (const insight of scan.insights)
896
+ report += `- ${insight}\n`;
897
+ report += `\n`;
898
+ }
899
+ if (scan.issues.length > 0) {
900
+ report += `## Architecture Issues & Risks\n\n`;
901
+ for (const sev of ["high", "medium", "low"]) {
902
+ const filtered = scan.issues.filter((i) => i.severity === sev);
903
+ if (filtered.length === 0)
904
+ continue;
905
+ report += `### ${sev.charAt(0).toUpperCase() + sev.slice(1)} Severity\n\n`;
906
+ for (const issue of filtered) {
907
+ report += `**${issue.type}**: ${issue.description}\n`;
908
+ if (issue.file)
909
+ report += `- File: \`${issue.file}\`\n`;
910
+ if (issue.line)
911
+ report += `- Line: ${issue.line}\n`;
912
+ if (issue.recommendation)
913
+ report += `- **Recommendation**: ${issue.recommendation}\n`;
914
+ report += `\n`;
915
+ }
916
+ }
917
+ }
918
+ else {
919
+ report += `## Architecture Assessment\n\n`;
920
+ report += `No significant architectural issues detected.\n\n`;
921
+ }
922
+ report += `## Recommendations\n\n`;
923
+ report += `1. Schedule periodic architecture reviews (quarterly) to track progress.\n`;
924
+ report += `2. Maintain an up-to-date architecture document reflecting actual system design.\n`;
925
+ if (scan.issues.some((i) => i.severity === "high"))
926
+ report += `3. Address high-severity issues first.\n`;
927
+ if (scan.issues.some((i) => i.type === "tight-coupling"))
928
+ report += `4. Implement dependency injection and clear module boundaries to reduce tight coupling.\n`;
929
+ if (scan.issues.some((i) => i.type === "scalability-risk"))
930
+ report += `5. Plan refactoring for large modules that may become bottlenecks.\n`;
931
+ report += `\n## Next Steps\n\n`;
932
+ report += `- Review this report with the team\n`;
933
+ report += `- Create tasks for addressing identified issues\n`;
934
+ report += `- Track progress through subsequent reports\n`;
935
+ await writeFile(reportPath, report, "utf8");
936
+ return { reportPath, fileName };
937
+ }
938
+ /**
939
+ * Run the Architect Agent end to end: scan, generate report, record lastRunAt.
940
+ */
941
+ export async function runArchitectAgent(config) {
942
+ const scan = await scanForArchitectureIssues(config);
943
+ const report = await generateArchitectureReport(config, scan);
944
+ const agents = { ...(config.builtInAgents ?? {}) };
945
+ agents["architect"] = { ...(agents["architect"] ?? {}), lastRunAt: new Date().toISOString() };
946
+ saveBuiltInAgentsConfig(config.root, agents, config.cacheDir);
947
+ config.builtInAgents = agents;
948
+ return {
949
+ reportPath: report.reportPath,
950
+ fileName: report.fileName,
951
+ issuesFound: scan.issues.length,
952
+ scannedFiles: scan.scannedFiles,
953
+ taskCount: scan.taskCount,
954
+ created: 0,
955
+ failed: 0,
956
+ errors: [],
957
+ };
958
+ }
959
+ /**
960
+ * Scan the web UI (src/ui-app/) for UI bugs, UX friction, and design
961
+ * improvements. Each finding is grounded in a best practice with a concrete,
962
+ * actionable recommendation that references the file/component involved.
963
+ * The scan is heuristic, flagging signal-not-noise patterns rather than trying
964
+ * to be a fully automated accessibility audit — it never edits UI source.
965
+ */
966
+ export async function scanForDesignIssues(config) {
967
+ const findings = [];
968
+ const insights = [];
969
+ const uiRoot = join(config.root, "src", "ui-app", "src");
970
+ let files = [];
971
+ try {
972
+ files = collectSourceFiles(uiRoot);
973
+ }
974
+ catch {
975
+ files = [];
976
+ }
977
+ const scanned = readScannedFiles(uiRoot, files);
978
+ const components = scanned.filter((f) => f.rel.startsWith("components/") && f.rel.endsWith(".vue"));
979
+ const views = scanned.filter((f) => f.rel.startsWith("views/") && f.rel.endsWith(".vue"));
980
+ insights.push(`Analyzed ${scanned.length} files under \`src/ui-app/src/\` (${components.length} components, ${views.length} views).`);
981
+ if (scanned.length === 0) {
982
+ insights.push("No web UI source found — the scan only looks under `src/ui-app/src/`.");
983
+ return { findings, scannedFiles: 0, insights };
984
+ }
985
+ // 1. Inline `style="..."` attributes in templates: they break the design
986
+ // system by bypassing CSS variables/classes and make dark-mode theming drift.
987
+ const INLINE_STYLE_RE = /\sstyle\s*=\s*["']([^"']+)["']/g;
988
+ for (const file of scanned) {
989
+ let m;
990
+ while ((m = INLINE_STYLE_RE.exec(file.content)) !== null) {
991
+ const value = m[1];
992
+ // Skip Tailwind-style dynamic bindings (:style) — the static style attr
993
+ // is the theme-unsafe one.
994
+ if (value.includes("{") || value.length === 0)
995
+ continue;
996
+ findings.push({
997
+ category: value.includes("color") || value.includes("background") || value.includes("border")
998
+ ? "ui-bug"
999
+ : "design-recommendation",
1000
+ file: file.rel,
1001
+ line: findLineAt(file.content, m.index),
1002
+ description: `Hardcoded inline style "${value}" bypasses the shared design system.`,
1003
+ rationale: "Inline styles ignore the centralized CSS variables and can drift from the theme, especially across dark mode.",
1004
+ recommendation: `Move this styling into a scoped class or a shared utility so it inherits the app's theme tokens (see how neighboring \`src/ui-app/src/components/*.vue\` components style via CSS variables).`,
1005
+ severity: "medium",
1006
+ });
1007
+ }
1008
+ }
1009
+ // 2. Hardcoded hex colors in templates/styles: they can't respond to theme.
1010
+ const HEX_COLOR_RE = /#[0-9a-fA-F]{3,8}\b/g;
1011
+ for (const file of scanned) {
1012
+ const cleaned = file.content.replace(/style\s*=\s*["'][^"']*["']/g, "");
1013
+ let m;
1014
+ while ((m = HEX_COLOR_RE.exec(cleaned)) !== null) {
1015
+ findings.push({
1016
+ category: "design-recommendation",
1017
+ file: file.rel,
1018
+ line: findLineAt(cleaned, m.index),
1019
+ description: `Hardcoded hex color ${m[0]} used instead of a theme variable.`,
1020
+ rationale: "Hardcoded colors do not adapt to the app's light/dark theme and make palette changes require editing many files.",
1021
+ recommendation: "Replace with a CSS variable (e.g. `var(--text-primary)`, `var(--border)`) so it follows the active theme.",
1022
+ severity: "low",
1023
+ });
1024
+ }
1025
+ }
1026
+ // 3. Interactive elements without an accessible name: buttons with only an
1027
+ // icon or empty labels are invisible to screen readers.
1028
+ for (const file of scanned) {
1029
+ if (!/\.vue$/.test(file.rel))
1030
+ continue;
1031
+ const BUTTON_RE = /<button\b([^>]*)>/g;
1032
+ let m;
1033
+ while ((m = BUTTON_RE.exec(file.content)) !== null) {
1034
+ const attrs = m[1];
1035
+ // A button already has an accessible name via aria-label/title, or a
1036
+ // closing tag on the same line means it stays open for visible content.
1037
+ if (/aria-label\s*=|aria-labelledby\s*=|title\s*=/.test(attrs))
1038
+ continue;
1039
+ const after = file.content.slice(m.index + m[0].length);
1040
+ const lineEnd = after.search(/\n/);
1041
+ const restOfLine = (lineEnd === -1 ? after : after.slice(0, lineEnd)).trim();
1042
+ // Only flag unmistakable cases: an icon/expression or an immediately-
1043
+ // closed button with no accessible name.
1044
+ const iconOnly = /^\{[^}]*\}/.test(restOfLine) || /^<\/button>/.test(restOfLine);
1045
+ if (!iconOnly)
1046
+ continue;
1047
+ findings.push({
1048
+ category: "ux-friction",
1049
+ file: file.rel,
1050
+ line: findLineAt(file.content, m.index),
1051
+ description: "A button appears to have no visible label or `aria-label`.",
1052
+ rationale: "Icon-only or label-less buttons are inaccessible to screen readers and confusing to users.",
1053
+ recommendation: "Add a visible label or an `aria-label` describing the action.",
1054
+ severity: "medium",
1055
+ });
1056
+ }
1057
+ }
1058
+ // 4. click handlers on non-interactive elements (div/span/li without
1059
+ // role="button" or a tabindex) — a common keyboard-inaccessibility bug.
1060
+ for (const file of scanned) {
1061
+ if (!/\.vue$/.test(file.rel))
1062
+ continue;
1063
+ const NONINT_RE = /<(div|span|li)\b([^>]*)\s@click\s*=/g;
1064
+ let m;
1065
+ while ((m = NONINT_RE.exec(file.content)) !== null) {
1066
+ const attrs = m[2] ?? "";
1067
+ const isButtonRole = /role\s*=\s*["']button["']/.test(attrs) || /tabindex\s*=/.test(attrs);
1068
+ if (isButtonRole)
1069
+ continue;
1070
+ findings.push({
1071
+ category: "ux-friction",
1072
+ file: file.rel,
1073
+ line: findLineAt(file.content, m.index),
1074
+ description: `A <${m[1]}> element carries a @click handler but no role="button" or tabindex.`,
1075
+ rationale: "Click-only handlers on non-interactive elements are unreachable by keyboard and screen readers don't announce them as actionable.",
1076
+ recommendation: `Add role="button" and tabindex="0" (plus Enter/Space handling) or use a real <button> in \`${file.rel}\`.`,
1077
+ severity: "medium",
1078
+ });
1079
+ }
1080
+ }
1081
+ // 5. v-html usage: unsanitized HTML injection risk and hard to theme/style consistently.
1082
+ const V_HTML_RE = /\bv-html\s*=/g;
1083
+ for (const file of scanned) {
1084
+ let m;
1085
+ while ((m = V_HTML_RE.exec(file.content)) !== null) {
1086
+ findings.push({
1087
+ category: "ui-bug",
1088
+ file: file.rel,
1089
+ line: findLineAt(file.content, m.index),
1090
+ description: "Uses `v-html`, which injects raw HTML.",
1091
+ rationale: "v-html can render unsanitized HTML (XSS risk) and makes styling/consistency harder to control.",
1092
+ recommendation: "Prefer Vue interpolation or a dedicated render approach; if v-html is required, ensure the source is trusted and sanitized.",
1093
+ severity: "high",
1094
+ });
1095
+ }
1096
+ }
1097
+ // 6. Form inputs without an associated label (no <label> nearby or aria-label).
1098
+ for (const file of scanned) {
1099
+ if (!/\.vue$/.test(file.rel))
1100
+ continue;
1101
+ const INPUT_RE = /<input\b([^>]*)\/?>/gi;
1102
+ let m;
1103
+ while ((m = INPUT_RE.exec(file.content)) !== null) {
1104
+ const attrs = m[1];
1105
+ if (/type\s*=\s*["'](?:hidden|checkbox|radio)["']/i.test(attrs))
1106
+ continue;
1107
+ const hasName = /aria-label\s*=|aria-labelledby\s*=|id\s*=|placeholder\s*=|v-model\s*/.test(attrs);
1108
+ if (hasName)
1109
+ continue;
1110
+ const before = file.content.slice(Math.max(0, m.index - 80), m.index);
1111
+ if (/<label\b/.test(before))
1112
+ continue;
1113
+ findings.push({
1114
+ category: "ux-friction",
1115
+ file: file.rel,
1116
+ line: findLineAt(file.content, m.index),
1117
+ description: "An <input> has no explicit label, aria-label, or labelled-by association.",
1118
+ rationale: "Inputs without accessible labels are hard to fill out for screen-reader users and can be ambiguous for everyone.",
1119
+ recommendation: "Wrap or associate the input with a <label>, or add aria-label/aria-labelledby.",
1120
+ severity: "medium",
1121
+ });
1122
+ }
1123
+ }
1124
+ // 7. Very large component files — a maintainability and consistency concern.
1125
+ for (const file of scanned) {
1126
+ if (!/\.vue$/.test(file.rel))
1127
+ continue;
1128
+ if (file.lineCount > 600) {
1129
+ findings.push({
1130
+ category: "design-recommendation",
1131
+ file: file.rel,
1132
+ line: 1,
1133
+ description: `Component file is ${file.lineCount} lines long.`,
1134
+ rationale: "Very large single-file components are hard to maintain and tend to accumulate inconsistent, copy-pasted styling.",
1135
+ recommendation: "Break the component into smaller focused components and extract repeated markup/styling into shared primitives.",
1136
+ severity: "low",
1137
+ });
1138
+ }
1139
+ }
1140
+ // Keep the report focused: cap the number of findings per category.
1141
+ const MAX_FINDINGS_PER_CATEGORY = 8;
1142
+ const capped = [];
1143
+ const counts = {
1144
+ "ui-bug": 0,
1145
+ "ux-friction": 0,
1146
+ "design-recommendation": 0,
1147
+ };
1148
+ for (const finding of findings) {
1149
+ if (counts[finding.category] >= MAX_FINDINGS_PER_CATEGORY)
1150
+ continue;
1151
+ counts[finding.category]++;
1152
+ capped.push(finding);
1153
+ }
1154
+ return { findings: capped, scannedFiles: scanned.length, insights };
1155
+ }
1156
+ /**
1157
+ * Generate a markdown UI/UX design report and save it with a timestamp.
1158
+ * Fallback content guarantees the report reads correctly even when the scan
1159
+ * found nothing to flag.
1160
+ */
1161
+ export async function generateDesignReport(config, scan) {
1162
+ const reportDir = join(config.root, "docs", "agents", "Design");
1163
+ mkdirSync(reportDir, { recursive: true });
1164
+ const now = new Date();
1165
+ const ts = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`;
1166
+ const fileName = `Design_report_${ts}.md`;
1167
+ const reportPath = join(reportDir, fileName);
1168
+ let report = `# UI/UX Design Review Report\n\n`;
1169
+ report += `**Generated**: ${now.toISOString()}\n\n`;
1170
+ report += `## Executive Summary\n\n`;
1171
+ report += `- **Files Scanned**: ${scan.scannedFiles}\n`;
1172
+ report += `- **Findings Identified**: ${scan.findings.length}\n`;
1173
+ const byCat = { "ui-bug": 0, "ux-friction": 0, "design-recommendation": 0 };
1174
+ for (const f of scan.findings)
1175
+ byCat[f.category]++;
1176
+ report += `- **UI bugs**: ${byCat["ui-bug"]}\n`;
1177
+ report += `- **UX frictions**: ${byCat["ux-friction"]}\n`;
1178
+ report += `- **Design recommendations**: ${byCat["design-recommendation"]}\n\n`;
1179
+ if (scan.insights.length > 0) {
1180
+ report += `## Scan Overview\n\n`;
1181
+ for (const insight of scan.insights)
1182
+ report += `- ${insight}\n`;
1183
+ report += `\n`;
1184
+ }
1185
+ if (scan.findings.length > 0) {
1186
+ const labels = {
1187
+ "ui-bug": { title: "UI Bugs", heading: "UI Bugs" },
1188
+ "ux-friction": { title: "UX Friction", heading: "UX Friction" },
1189
+ "design-recommendation": {
1190
+ title: "Design Recommendations",
1191
+ heading: "Proposed Updates, Fixes, and New Designs",
1192
+ },
1193
+ };
1194
+ const order = ["ui-bug", "ux-friction", "design-recommendation"];
1195
+ for (const cat of order) {
1196
+ const items = scan.findings.filter((f) => f.category === cat);
1197
+ if (items.length === 0)
1198
+ continue;
1199
+ report += `## ${labels[cat].heading}\n\n`;
1200
+ for (const sev of ["high", "medium", "low"]) {
1201
+ const filtered = items.filter((i) => i.severity === sev);
1202
+ if (filtered.length === 0)
1203
+ continue;
1204
+ report += `### ${sev.charAt(0).toUpperCase() + sev.slice(1)} Severity\n\n`;
1205
+ for (const f of filtered) {
1206
+ report += `**${f.description}**\n`;
1207
+ report += `- File: \`${f.file}\`${f.line ? `:${f.line}` : ""}\n`;
1208
+ report += `- **Rationale**: ${f.rationale}\n`;
1209
+ report += `- **Suggested fix**: ${f.recommendation}\n\n`;
1210
+ }
1211
+ }
1212
+ }
1213
+ }
1214
+ else {
1215
+ report += `## UI/UX Assessment\n\n`;
1216
+ report += `No significant UI/UX issues detected in the current web UI.\n\n`;
1217
+ }
1218
+ report += `## Next Steps\n\n`;
1219
+ report += `- Review the findings and confirm each is worth addressing.\n`;
1220
+ report += `- Create follow-up tasks for the agreed-upon fixes/redesigns (this agent reports only; it does not edit UI source).\n`;
1221
+ report += `- Track progress through subsequent reports under \`docs/agents/Design/\`.\n`;
1222
+ await writeFile(reportPath, report, "utf8");
1223
+ return { reportPath, fileName };
1224
+ }
1225
+ /**
1226
+ * Run the Design Agent end to end: scan the web UI, generate a markdown
1227
+ * report saved to docs/agents/Design/, and record lastRunAt. Like the
1228
+ * Architect agent it only reports — it never edits UI source or creates tasks.
1229
+ */
1230
+ export async function runDesignAgent(config) {
1231
+ const scan = await scanForDesignIssues(config);
1232
+ const report = await generateDesignReport(config, scan);
1233
+ const agents = { ...(config.builtInAgents ?? {}) };
1234
+ agents["design"] = { ...(agents["design"] ?? {}), lastRunAt: new Date().toISOString() };
1235
+ saveBuiltInAgentsConfig(config.root, agents, config.cacheDir);
1236
+ config.builtInAgents = agents;
1237
+ return {
1238
+ reportPath: report.reportPath,
1239
+ fileName: report.fileName,
1240
+ findingsFound: scan.findings.length,
1241
+ scannedFiles: scan.scannedFiles,
1242
+ created: 0,
1243
+ failed: 0,
1244
+ errors: [],
1245
+ };
1246
+ }
1247
+ /** Raised when the Docs Debt Agent cannot do its job at all (e.g. missing work dir). */
1248
+ export class DocsDebtError extends Error {
1249
+ }
1250
+ /** Collect the docs this agent may read (and, for trivial fixes, edit). */
1251
+ function collectDocFiles(root) {
1252
+ const out = [];
1253
+ const addFile = (abs) => {
1254
+ if (out.length >= MAX_DOCS)
1255
+ return;
1256
+ let st;
1257
+ try {
1258
+ st = statSync(abs);
1259
+ }
1260
+ catch {
1261
+ return;
1262
+ }
1263
+ if (st.isFile() && st.size <= MAX_DOC_BYTES)
1264
+ out.push(abs);
1265
+ };
1266
+ addFile(join(root, "AGENTS.md"));
1267
+ const walk = (dir) => {
1268
+ if (out.length >= MAX_DOCS)
1269
+ return;
1270
+ let entries;
1271
+ try {
1272
+ entries = readdirSync(dir);
1273
+ }
1274
+ catch {
1275
+ return;
1276
+ }
1277
+ for (const name of entries) {
1278
+ if (out.length >= MAX_DOCS)
1279
+ return;
1280
+ const abs = join(dir, name);
1281
+ let st;
1282
+ try {
1283
+ st = statSync(abs);
1284
+ }
1285
+ catch {
1286
+ continue;
1287
+ }
1288
+ if (st.isDirectory()) {
1289
+ if (IGNORED_DIRS.has(name) || name.startsWith("."))
1290
+ continue;
1291
+ walk(abs);
1292
+ }
1293
+ else if (st.isFile() && name.endsWith(".md") && st.size <= MAX_DOC_BYTES) {
1294
+ out.push(abs);
1295
+ }
1296
+ }
1297
+ };
1298
+ for (const dir of ["docs", "user-docs"])
1299
+ walk(join(root, dir));
1300
+ return out;
1301
+ }
1302
+ /** Bounded index of every repo file, used to find a unique replacement path. */
1303
+ function collectRepoFileIndex(root) {
1304
+ const out = [];
1305
+ const walk = (dir, relPrefix) => {
1306
+ if (out.length >= MAX_REPO_INDEX_FILES)
1307
+ return;
1308
+ let entries;
1309
+ try {
1310
+ entries = readdirSync(dir);
1311
+ }
1312
+ catch {
1313
+ return;
1314
+ }
1315
+ for (const name of entries) {
1316
+ if (out.length >= MAX_REPO_INDEX_FILES)
1317
+ return;
1318
+ if (name === ".github") {
1319
+ walk(join(dir, name), relPrefix ? `${relPrefix}/${name}` : name);
1320
+ continue;
1321
+ }
1322
+ if (IGNORED_DIRS.has(name) || name.startsWith("."))
1323
+ continue;
1324
+ const abs = join(dir, name);
1325
+ let st;
1326
+ try {
1327
+ st = statSync(abs);
1328
+ }
1329
+ catch {
1330
+ continue;
1331
+ }
1332
+ const rel = relPrefix ? `${relPrefix}/${name}` : name;
1333
+ if (st.isDirectory())
1334
+ walk(abs, rel);
1335
+ else if (st.isFile())
1336
+ out.push({ rel, base: name });
1337
+ }
1338
+ };
1339
+ walk(root, "");
1340
+ return out;
1341
+ }
1342
+ /**
1343
+ * Every identifier appearing in real `src/` code (comments and string literals
1344
+ * are stripped first). A doc claim naming a symbol absent from this set points
1345
+ * at something the code no longer has — the #0343 class of drift, one level up.
1346
+ */
1347
+ function collectSourceWords(root) {
1348
+ const words = new Set();
1349
+ let files = [];
1350
+ try {
1351
+ files = collectSourceFiles(join(root, "src"));
1352
+ }
1353
+ catch {
1354
+ files = [];
1355
+ }
1356
+ const scanned = readScannedFiles(join(root, "src"), files);
1357
+ const WORD_RE = /[A-Za-z_$][\w$]*/g;
1358
+ for (const file of scanned) {
1359
+ const cleaned = stripCommentsAndStrings(file.content);
1360
+ let m;
1361
+ while ((m = WORD_RE.exec(cleaned)) !== null)
1362
+ words.add(m[0]);
1363
+ }
1364
+ return words;
1365
+ }
1366
+ function readPackageScripts(root) {
1367
+ try {
1368
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
1369
+ const scripts = pkg.scripts;
1370
+ if (typeof scripts === "object" && scripts !== null) {
1371
+ return new Set(Object.keys(scripts));
1372
+ }
1373
+ }
1374
+ catch {
1375
+ /* no package.json — no script claims to verify */
1376
+ }
1377
+ return new Set();
1378
+ }
1379
+ function readRuntimeDependencies(root) {
1380
+ try {
1381
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
1382
+ const deps = pkg.dependencies;
1383
+ if (typeof deps === "object" && deps !== null) {
1384
+ return Object.keys(deps);
1385
+ }
1386
+ }
1387
+ catch {
1388
+ /* no package.json — no dependency constraint to verify */
1389
+ }
1390
+ return [];
1391
+ }
1392
+ function looksLikeRepoPath(value) {
1393
+ if (value.startsWith("dist/") || value.startsWith(".repoos/"))
1394
+ return false;
1395
+ if (value.endsWith("/")) {
1396
+ return KNOWN_PATH_PREFIXES.some((prefix) => value === prefix);
1397
+ }
1398
+ if (KNOWN_ROOT_FILES.has(value))
1399
+ return true;
1400
+ return KNOWN_PATH_PREFIXES.some((prefix) => value.startsWith(prefix));
1401
+ }
1402
+ /** Camel-case identifiers read as function/const references, not prose words. */
1403
+ function isSymbolClaim(value) {
1404
+ return /^[a-z][A-Za-z0-9_$]*[A-Z][A-Za-z0-9_$]*$/.test(value) && !NON_REPO_SYMBOLS.has(value);
1405
+ }
1406
+ /**
1407
+ * Decide what kind of concrete claim a backtick span makes. Returns null for
1408
+ * prose, globs, URLs, function calls with arguments, and anything not worth
1409
+ * verifying — the scan prefers signal over breadth.
1410
+ */
1411
+ function classifyBacktickSpan(span) {
1412
+ const raw = span.trim();
1413
+ if (!raw || raw.includes("://"))
1414
+ return null;
1415
+ // Script claims are distinctive and deliberately contain a space.
1416
+ const script = raw.match(SCRIPT_CLAIM_RE)?.[1] ?? (TEST_CLAIM_RE.test(raw) ? "test" : undefined);
1417
+ if (script)
1418
+ return { script };
1419
+ if (/[\s<>{}#|="'[\],;]/.test(raw))
1420
+ return null;
1421
+ const core = raw.endsWith("()") ? raw.slice(0, -2) : raw;
1422
+ if (/[()*]/.test(core))
1423
+ return null;
1424
+ const lineMatch = core.match(/^(.*?):(\d+)(?:-\d+)?$/);
1425
+ const pathPart = lineMatch ? lineMatch[1] : core;
1426
+ if (looksLikeRepoPath(pathPart)) {
1427
+ return { path: { path: pathPart, suffix: lineMatch ? core.slice(pathPart.length) : "" } };
1428
+ }
1429
+ if (isSymbolClaim(pathPart))
1430
+ return { symbol: pathPart };
1431
+ return null;
1432
+ }
1433
+ /**
1434
+ * Scan `AGENTS.md`/`docs/`/`user-docs/` for concrete, checkable claims and
1435
+ * verify each against the real repo: file paths are tested for existence,
1436
+ * symbols against identifiers present in `src/`, `bun run <script>` against
1437
+ * package.json, and "zero runtime dependencies" against `dependencies`.
1438
+ * Bounded in doc count and size so a periodic sweep can never stall the server.
1439
+ */
1440
+ export async function scanForDocsDebt(config) {
1441
+ const trivialFixes = [];
1442
+ const needsHuman = [];
1443
+ let claimsChecked = 0;
1444
+ const docs = collectDocFiles(config.root);
1445
+ const sourceWords = collectSourceWords(config.root);
1446
+ const scripts = readPackageScripts(config.root);
1447
+ const runtimeDeps = readRuntimeDependencies(config.root);
1448
+ let repoIndex = null;
1449
+ const fileIndex = () => (repoIndex ??= collectRepoFileIndex(config.root));
1450
+ const seen = new Set();
1451
+ for (const abs of docs) {
1452
+ const doc = abs.slice(config.root.length).replace(/^[/\\]/, "") || abs;
1453
+ let content;
1454
+ try {
1455
+ content = readFileSync(abs, "utf8");
1456
+ }
1457
+ catch {
1458
+ continue;
1459
+ }
1460
+ const lines = content.split("\n");
1461
+ let zeroDepsLine = null;
1462
+ for (let i = 0; i < lines.length; i++) {
1463
+ const line = lines[i];
1464
+ if (zeroDepsLine === null && ZERO_RUNTIME_DEPS_RE.test(line))
1465
+ zeroDepsLine = i + 1;
1466
+ const BACKTICK_RE = /`([^`\n]+)`/g;
1467
+ let m;
1468
+ while ((m = BACKTICK_RE.exec(line)) !== null) {
1469
+ const span = m[1];
1470
+ const classified = classifyBacktickSpan(span);
1471
+ if (!classified)
1472
+ continue;
1473
+ claimsChecked++;
1474
+ if (classified.script !== undefined) {
1475
+ const key = `${doc}\u0000script:${classified.script}`;
1476
+ if (seen.has(key))
1477
+ continue;
1478
+ seen.add(key);
1479
+ if (!scripts.has(classified.script)) {
1480
+ needsHuman.push({
1481
+ kind: "missing-script",
1482
+ doc,
1483
+ line: i + 1,
1484
+ claim: span.trim(),
1485
+ evidence: `package.json has no \`${classified.script}\` script`,
1486
+ severity: "medium",
1487
+ recommendation: `Update the command or add the \`${classified.script}\` script.`,
1488
+ });
1489
+ }
1490
+ continue;
1491
+ }
1492
+ if (classified.path) {
1493
+ const { path: pathPart, suffix } = classified.path;
1494
+ const key = `${doc}\u0000path:${pathPart}`;
1495
+ if (seen.has(key))
1496
+ continue;
1497
+ seen.add(key);
1498
+ if (existsSync(join(config.root, pathPart)))
1499
+ continue;
1500
+ const base = basename(pathPart);
1501
+ const candidates = pathPart.endsWith("/")
1502
+ ? []
1503
+ : fileIndex().filter((f) => f.base === base);
1504
+ if (candidates.length === 1 && candidates[0].rel !== pathPart) {
1505
+ trivialFixes.push({
1506
+ kind: "renamed-path",
1507
+ doc,
1508
+ line: i + 1,
1509
+ from: span.trim(),
1510
+ to: `${candidates[0].rel}${suffix}`,
1511
+ evidence: `\`${pathPart}\` does not exist; the only file named \`${base}\` is \`${candidates[0].rel}\``,
1512
+ });
1513
+ }
1514
+ else {
1515
+ needsHuman.push({
1516
+ kind: "missing-path",
1517
+ doc,
1518
+ line: i + 1,
1519
+ claim: span.trim(),
1520
+ evidence: `\`${pathPart}\` is referenced but does not exist in the repo`,
1521
+ severity: "medium",
1522
+ recommendation: "Update the reference or restore the path.",
1523
+ });
1524
+ }
1525
+ continue;
1526
+ }
1527
+ if (classified.symbol && !sourceWords.has(classified.symbol)) {
1528
+ const key = `${doc}\u0000symbol:${classified.symbol}`;
1529
+ if (seen.has(key))
1530
+ continue;
1531
+ seen.add(key);
1532
+ needsHuman.push({
1533
+ kind: "missing-symbol",
1534
+ doc,
1535
+ line: i + 1,
1536
+ claim: span.trim(),
1537
+ evidence: `\`${classified.symbol}\` does not appear anywhere under \`src/\``,
1538
+ severity: "medium",
1539
+ recommendation: "Confirm the symbol was renamed or removed, then update the doc.",
1540
+ });
1541
+ }
1542
+ }
1543
+ }
1544
+ if (zeroDepsLine !== null && runtimeDeps.length > 0) {
1545
+ const key = `${doc}\u0000deps`;
1546
+ if (!seen.has(key)) {
1547
+ seen.add(key);
1548
+ const shown = runtimeDeps.slice(0, 5).join(", ");
1549
+ needsHuman.push({
1550
+ kind: "false-constraint",
1551
+ doc,
1552
+ line: zeroDepsLine,
1553
+ claim: "zero runtime dependencies",
1554
+ evidence: `This doc claims zero runtime dependencies, but package.json declares ${runtimeDeps.length}: ${shown}${runtimeDeps.length > 5 ? ", …" : ""}`,
1555
+ severity: "high",
1556
+ recommendation: "Either remove the runtime dependency (zero-deps is a hard constraint) or correct the doc.",
1557
+ });
1558
+ }
1559
+ }
1560
+ }
1561
+ return { trivialFixes, needsHuman, scannedDocs: docs.length, claimsChecked };
1562
+ }
1563
+ /**
1564
+ * Apply the mechanical doc fixes, capped per run, committing each with its
1565
+ * evidence. Fail-soft: a missing git identity or non-git checkout never throws;
1566
+ * the doc edit still lands and the caller can see it was not committed. Never
1567
+ * touches `src/` — the fix list only names doc paths by construction.
1568
+ */
1569
+ export async function applyDocsDebtFixes(config, fixes) {
1570
+ const result = { applied: 0, skipped: 0, committed: 0, errors: [] };
1571
+ const toApply = fixes.slice(0, MAX_TRIVIAL_FIXES_PER_RUN);
1572
+ result.skipped = fixes.length - toApply.length;
1573
+ for (const fix of toApply) {
1574
+ const abs = join(config.root, fix.doc);
1575
+ try {
1576
+ const content = readFileSync(abs, "utf8");
1577
+ const updated = content.split(`\`${fix.from}\``).join(`\`${fix.to}\``);
1578
+ if (updated === content) {
1579
+ result.errors.push(`${fix.doc}: nothing to replace for \`${fix.from}\``);
1580
+ result.skipped++;
1581
+ continue;
1582
+ }
1583
+ writeFileSync(abs, updated, "utf8");
1584
+ const message = [
1585
+ `docs: fix stale reference in ${fix.doc}`,
1586
+ "",
1587
+ fix.evidence,
1588
+ "Verified by the Docs Debt Agent.",
1589
+ ].join("\n");
1590
+ if (commitTaskFile(config.root, abs, message))
1591
+ result.committed++;
1592
+ result.applied++;
1593
+ }
1594
+ catch (err) {
1595
+ result.skipped++;
1596
+ result.errors.push(`${fix.doc}: ${err instanceof Error ? err.message : String(err)}`);
1597
+ }
1598
+ }
1599
+ return result;
1600
+ }
1601
+ /** Cap-downgraded fixes become needs-human findings so nothing is dropped. */
1602
+ function fixesToFindings(fixes) {
1603
+ return fixes.map((fix) => ({
1604
+ kind: "missing-path",
1605
+ doc: fix.doc,
1606
+ line: fix.line,
1607
+ claim: fix.from,
1608
+ evidence: `${fix.evidence} (not auto-fixed: the per-run cap of ${MAX_TRIVIAL_FIXES_PER_RUN} was reached)`,
1609
+ severity: "low",
1610
+ recommendation: `Update \`${fix.from}\` to \`${fix.to}\`.`,
1611
+ }));
1612
+ }
1613
+ /**
1614
+ * Create at most ONE task bundling every needs-human finding from a run. This
1615
+ * is the deliberate departure from the other built-ins: a per-finding task
1616
+ * would flood the inbox. Zero findings means zero tasks — never an empty task.
1617
+ */
1618
+ export async function createDocsDebtTask(config, findings) {
1619
+ const workDir = join(config.root, config.workDir);
1620
+ let workDirStats;
1621
+ try {
1622
+ workDirStats = statSync(workDir);
1623
+ }
1624
+ catch {
1625
+ throw new DocsDebtError(`Task directory "${config.workDir}" does not exist — create it (or fix workDir) before running the Docs Debt Agent`);
1626
+ }
1627
+ if (!workDirStats.isDirectory()) {
1628
+ throw new DocsDebtError(`Task directory "${config.workDir}" is not a directory`);
1629
+ }
1630
+ try {
1631
+ accessSync(workDir, 0o2 /* W_OK */);
1632
+ }
1633
+ catch {
1634
+ throw new DocsDebtError(`Task directory "${config.workDir}" is not writable`);
1635
+ }
1636
+ const result = { created: 0, failed: 0, errors: [] };
1637
+ if (findings.length === 0)
1638
+ return result;
1639
+ const title = "Docs debt: stale claims in AGENTS.md, docs/, and user-docs/";
1640
+ const now = new Date().toISOString();
1641
+ const taskId = findNextTaskId(workDir);
1642
+ const taskPath = join(workDir, `${taskId}-${slugify(title)}.md`);
1643
+ let body = `## Docs Debt Findings\n\n`;
1644
+ body += `The Docs Debt Agent verified concrete claims in \`AGENTS.md\`/\`docs/\`/\`user-docs/\` against the actual repo and found ${findings.length} that need a human decision.\n\n`;
1645
+ findings.forEach((finding, index) => {
1646
+ body += `### ${index + 1}. ${finding.claim}\n`;
1647
+ body += `- **Doc**: \`${finding.doc}\`:${finding.line}\n`;
1648
+ body += `- **Kind**: ${finding.kind}\n`;
1649
+ body += `- **Severity**: ${finding.severity}\n`;
1650
+ body += `- **Evidence**: ${finding.evidence}\n`;
1651
+ if (finding.recommendation)
1652
+ body += `- **Suggested fix**: ${finding.recommendation}\n`;
1653
+ body += `\n`;
1654
+ });
1655
+ body += `## Next Steps\n\n`;
1656
+ body += `1. Confirm each finding is real drift and not a deliberate, documented difference.\n`;
1657
+ body += `2. Update the doc(s) or the code so the two agree.\n`;
1658
+ body += `3. Move this task to done when complete.\n`;
1659
+ const frontmatter = `---
1660
+ id: "${taskId}"
1661
+ title: ${JSON.stringify(title)}
1662
+ type: chore
1663
+ status: inbox
1664
+ priority: p2
1665
+ area: docs-debt
1666
+ assigned_to: unassigned
1667
+ created_by: docs-debt-agent
1668
+ created_at: "${now}"
1669
+ updated_at: "${now}"
1670
+ ---`;
1671
+ try {
1672
+ await writeFile(taskPath, `${frontmatter}\n${body}`, "utf8");
1673
+ result.created++;
1674
+ }
1675
+ catch (err) {
1676
+ result.failed++;
1677
+ result.errors.push(`${taskPath}: ${err instanceof Error ? err.message : String(err)}`);
1678
+ }
1679
+ return result;
1680
+ }
1681
+ /**
1682
+ * Run the Docs Debt Agent end to end: scan, apply capped trivial fixes, file
1683
+ * one bundled task for whatever needs a human, and record lastRunAt. The
1684
+ * caller owns overlap protection (a single in-flight guard in server.ts).
1685
+ */
1686
+ export async function runDocsDebtAgent(config, logger) {
1687
+ logger?.agent("docs-debt", "info", "Docs Debt Agent scan started");
1688
+ const scan = await scanForDocsDebt(config);
1689
+ logger?.agent("docs-debt", "info", "Docs Debt scan completed", {
1690
+ scannedDocs: scan.scannedDocs,
1691
+ claimsChecked: scan.claimsChecked,
1692
+ trivialFixes: scan.trivialFixes.length,
1693
+ needsHuman: scan.needsHuman.length,
1694
+ });
1695
+ const apply = await applyDocsDebtFixes(config, scan.trivialFixes);
1696
+ if (apply.applied > 0) {
1697
+ logger?.agent("docs-debt", "info", `Applied ${apply.applied} trivial doc fix(es)`, {
1698
+ committed: apply.committed,
1699
+ });
1700
+ }
1701
+ const cappedFixes = fixesToFindings(scan.trivialFixes.slice(MAX_TRIVIAL_FIXES_PER_RUN));
1702
+ const findings = [...scan.needsHuman, ...cappedFixes];
1703
+ const task = await createDocsDebtTask(config, findings);
1704
+ if (task.failed > 0) {
1705
+ logger?.agent("docs-debt", "error", `Failed to create docs debt task`, {
1706
+ errors: task.errors,
1707
+ });
1708
+ }
1709
+ if (task.created > 0) {
1710
+ logger?.agent("docs-debt", "info", `Created ${task.created} docs debt task`);
1711
+ }
1712
+ const agents = { ...(config.builtInAgents ?? {}) };
1713
+ agents["docs-debt"] = { ...(agents["docs-debt"] ?? {}), lastRunAt: new Date().toISOString() };
1714
+ saveBuiltInAgentsConfig(config.root, agents, config.cacheDir);
1715
+ config.builtInAgents = agents;
1716
+ logger?.agent("docs-debt", "info", "Docs Debt Agent run completed", {
1717
+ trivialFixesApplied: apply.applied,
1718
+ findingsFound: findings.length,
1719
+ taskCreated: task.created,
1720
+ });
1721
+ return {
1722
+ scannedDocs: scan.scannedDocs,
1723
+ scannedFiles: scan.scannedDocs,
1724
+ claimsChecked: scan.claimsChecked,
1725
+ trivialFixesApplied: apply.applied,
1726
+ findingsFound: findings.length,
1727
+ taskCreated: task.created,
1728
+ created: task.created,
1729
+ failed: task.failed,
1730
+ errors: [...apply.errors, ...task.errors],
1731
+ };
1732
+ }
1733
+ /** Dispatch to the appropriate built-in agent by name. */
1734
+ export async function runBuiltInAgent(name, config, logger) {
1735
+ if (name === "tech-debt") {
1736
+ return runTechDebtAgent(config, {}, logger);
1737
+ }
1738
+ if (name === "performance") {
1739
+ return runPerformanceAgent(config);
1740
+ }
1741
+ if (name === "architect") {
1742
+ return runArchitectAgent(config);
1743
+ }
1744
+ if (name === "design") {
1745
+ return runDesignAgent(config);
1746
+ }
1747
+ if (name === "docs-debt") {
1748
+ return runDocsDebtAgent(config, logger);
1749
+ }
1750
+ return null;
1751
+ }
1752
+ function getTitleForIssueType(type) {
1753
+ switch (type) {
1754
+ case "outdated-dependency":
1755
+ return "Update outdated dependencies";
1756
+ case "code-duplication":
1757
+ return "Refactor duplicated code";
1758
+ case "high-complexity":
1759
+ return "Reduce file complexity";
1760
+ case "unused-code":
1761
+ return "Remove unused code";
1762
+ case "deprecated-api":
1763
+ return "Modernize deprecated patterns";
1764
+ default:
1765
+ return "Address tech debt";
1766
+ }
1767
+ }
1768
+ function getTitleForPerformanceIssueType(type) {
1769
+ switch (type) {
1770
+ case "slow-function":
1771
+ return "Optimize function performance";
1772
+ case "blocking-operation":
1773
+ return "Fix blocking operations";
1774
+ case "unbounded-growth":
1775
+ return "Prevent unbounded memory growth";
1776
+ case "duplicated-computation":
1777
+ return "Eliminate duplicate computations";
1778
+ default:
1779
+ return "Improve performance";
1780
+ }
1781
+ }
1782
+ function formatIssuesForTask(issues) {
1783
+ let body = "## Issues Identified\n\n";
1784
+ for (const issue of issues) {
1785
+ body += `### ${issue.description}\n`;
1786
+ body += `- **File**: \`${issue.file}\`\n`;
1787
+ if (issue.line)
1788
+ body += `- **Line**: ${issue.line}\n`;
1789
+ body += `- **Severity**: ${issue.severity}\n\n`;
1790
+ }
1791
+ body += "## Next Steps\n\n";
1792
+ body += "1. Review each issue in the files listed above\n";
1793
+ body += "2. Make the suggested improvements\n";
1794
+ body += "3. Test the changes thoroughly\n";
1795
+ body += "4. Move this task to done when complete\n";
1796
+ return body;
1797
+ }
1798
+ function formatPerformanceIssuesForTask(issues) {
1799
+ let body = "## Performance Issues Identified\n\n";
1800
+ for (const issue of issues) {
1801
+ body += `### ${issue.description}\n`;
1802
+ body += `- **File**: \`${issue.file}\`\n`;
1803
+ if (issue.line)
1804
+ body += `- **Line**: ${issue.line}\n`;
1805
+ body += `- **Severity**: ${issue.severity}\n`;
1806
+ body += `- **Type**: ${issue.type}\n\n`;
1807
+ }
1808
+ body += "## Next Steps\n\n";
1809
+ body += "1. Profile the identified performance issues with real-world data\n";
1810
+ body += "2. Optimize the code using appropriate techniques (async, streaming, caching, etc.)\n";
1811
+ body += "3. Measure the improvement with benchmarks\n";
1812
+ body += "4. Test thoroughly to ensure no regressions\n";
1813
+ body += "5. Move this task to done when optimized\n";
1814
+ return body;
1815
+ }
1816
+ function slugify(title) {
1817
+ return title
1818
+ .toLowerCase()
1819
+ .replace(/[^a-z0-9]+/g, "-")
1820
+ .replace(/^-|-$/g, "")
1821
+ .slice(0, 40);
1822
+ }
1823
+ function findNextTaskId(workDir) {
1824
+ try {
1825
+ const files = readdirSync(workDir);
1826
+ const ids = files
1827
+ .map((f) => {
1828
+ const match = f.match(/^(\d+)-/);
1829
+ return match ? parseInt(match[1], 10) : 0;
1830
+ })
1831
+ .filter((n) => n > 0);
1832
+ const maxId = Math.max(...ids, 0);
1833
+ return String(maxId + 1).padStart(4, "0");
1834
+ }
1835
+ catch {
1836
+ return "0001";
1837
+ }
1838
+ }