@wascartech/devbot 0.0.0-stage → 2.1.0

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 (376) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +638 -2
  3. package/dist/access-grant-store.js +99 -0
  4. package/dist/access-grant-store.js.map +1 -0
  5. package/dist/access-resolve.js +11 -0
  6. package/dist/access-resolve.js.map +1 -0
  7. package/dist/access-rule-store.js +99 -0
  8. package/dist/access-rule-store.js.map +1 -0
  9. package/dist/admin/access-routes.js +73 -0
  10. package/dist/admin/access-routes.js.map +1 -0
  11. package/dist/admin/access-rule-routes.js +107 -0
  12. package/dist/admin/access-rule-routes.js.map +1 -0
  13. package/dist/admin/custom-repos-routes.js +93 -0
  14. package/dist/admin/custom-repos-routes.js.map +1 -0
  15. package/dist/admin/env-routes.js +74 -0
  16. package/dist/admin/env-routes.js.map +1 -0
  17. package/dist/admin/global-settings-routes.js +254 -0
  18. package/dist/admin/global-settings-routes.js.map +1 -0
  19. package/dist/admin/mcp-cleanup.js +12 -0
  20. package/dist/admin/mcp-cleanup.js.map +1 -0
  21. package/dist/admin/mcp-routes.js +169 -0
  22. package/dist/admin/mcp-routes.js.map +1 -0
  23. package/dist/admin/query-params.js +80 -0
  24. package/dist/admin/query-params.js.map +1 -0
  25. package/dist/admin/routes.js +150 -0
  26. package/dist/admin/routes.js.map +1 -0
  27. package/dist/admin/run-history-routes.js +44 -0
  28. package/dist/admin/run-history-routes.js.map +1 -0
  29. package/dist/admin/run-routes.js +99 -0
  30. package/dist/admin/run-routes.js.map +1 -0
  31. package/dist/admin/settings-routes.js +119 -0
  32. package/dist/admin/settings-routes.js.map +1 -0
  33. package/dist/admin/stats-routes.js +51 -0
  34. package/dist/admin/stats-routes.js.map +1 -0
  35. package/dist/admin/task-routes.js +213 -0
  36. package/dist/admin/task-routes.js.map +1 -0
  37. package/dist/admin/test-routes.js +38 -0
  38. package/dist/admin/test-routes.js.map +1 -0
  39. package/dist/admin/volume-routes.js +82 -0
  40. package/dist/admin/volume-routes.js.map +1 -0
  41. package/dist/admin/webhook-routes.js +174 -0
  42. package/dist/admin/webhook-routes.js.map +1 -0
  43. package/dist/agents.js +48 -0
  44. package/dist/agents.js.map +1 -0
  45. package/dist/assignee-value.js +6 -0
  46. package/dist/assignee-value.js.map +1 -0
  47. package/dist/attachments.js +76 -0
  48. package/dist/attachments.js.map +1 -0
  49. package/dist/auto-commands.js +88 -0
  50. package/dist/auto-commands.js.map +1 -0
  51. package/dist/auto-features.js +32 -0
  52. package/dist/auto-features.js.map +1 -0
  53. package/dist/base-branch-store.js +40 -0
  54. package/dist/base-branch-store.js.map +1 -0
  55. package/dist/base-branches.js +9 -0
  56. package/dist/base-branches.js.map +1 -0
  57. package/dist/build-workflow.js +116 -0
  58. package/dist/build-workflow.js.map +1 -0
  59. package/dist/change-request-lifecycle.js +16 -0
  60. package/dist/change-request-lifecycle.js.map +1 -0
  61. package/dist/change-request-store.js +72 -0
  62. package/dist/change-request-store.js.map +1 -0
  63. package/dist/ci-failure.js +80 -0
  64. package/dist/ci-failure.js.map +1 -0
  65. package/dist/cli.js +3 -0
  66. package/dist/cli.js.map +1 -0
  67. package/dist/config.js +17 -0
  68. package/dist/config.js.map +1 -0
  69. package/dist/conflict-fix.js +178 -0
  70. package/dist/conflict-fix.js.map +1 -0
  71. package/dist/cookie.js +70 -0
  72. package/dist/cookie.js.map +1 -0
  73. package/dist/crypto-util.js +40 -0
  74. package/dist/crypto-util.js.map +1 -0
  75. package/dist/custom-repos-store.js +90 -0
  76. package/dist/custom-repos-store.js.map +1 -0
  77. package/dist/custom-repos-sync.js +118 -0
  78. package/dist/custom-repos-sync.js.map +1 -0
  79. package/dist/db.js +12 -0
  80. package/dist/db.js.map +1 -0
  81. package/dist/devbot-scripts.js +18 -0
  82. package/dist/devbot-scripts.js.map +1 -0
  83. package/dist/devcontainer.js +141 -0
  84. package/dist/devcontainer.js.map +1 -0
  85. package/dist/env-config-store.js +132 -0
  86. package/dist/env-config-store.js.map +1 -0
  87. package/dist/error-log-store.js +193 -0
  88. package/dist/error-log-store.js.map +1 -0
  89. package/dist/git.js +605 -0
  90. package/dist/git.js.map +1 -0
  91. package/dist/global-queue.js +127 -0
  92. package/dist/global-queue.js.map +1 -0
  93. package/dist/global-settings-store.js +307 -0
  94. package/dist/global-settings-store.js.map +1 -0
  95. package/dist/help.js +41 -0
  96. package/dist/help.js.map +1 -0
  97. package/dist/http-csp.js +15 -0
  98. package/dist/http-csp.js.map +1 -0
  99. package/dist/http-util.js +40 -0
  100. package/dist/http-util.js.map +1 -0
  101. package/dist/idempotency.js +50 -0
  102. package/dist/idempotency.js.map +1 -0
  103. package/dist/lifecycle.js +234 -0
  104. package/dist/lifecycle.js.map +1 -0
  105. package/dist/logger.js +33 -0
  106. package/dist/logger.js.map +1 -0
  107. package/dist/lsp-servers.js +81 -0
  108. package/dist/lsp-servers.js.map +1 -0
  109. package/dist/mcp/auth.js +62 -0
  110. package/dist/mcp/auth.js.map +1 -0
  111. package/dist/mcp/effective-mcp.js +42 -0
  112. package/dist/mcp/effective-mcp.js.map +1 -0
  113. package/dist/mcp/oauth-refresh.js +138 -0
  114. package/dist/mcp/oauth-refresh.js.map +1 -0
  115. package/dist/mcp/peer-sync.js +109 -0
  116. package/dist/mcp/peer-sync.js.map +1 -0
  117. package/dist/mcp/registry.js +54 -0
  118. package/dist/mcp/registry.js.map +1 -0
  119. package/dist/mcp/runner-bridge.js +93 -0
  120. package/dist/mcp/runner-bridge.js.map +1 -0
  121. package/dist/mcp/server.js +173 -0
  122. package/dist/mcp/server.js.map +1 -0
  123. package/dist/mcp/tools/_helpers.js +88 -0
  124. package/dist/mcp/tools/_helpers.js.map +1 -0
  125. package/dist/mcp/tools/attachments.js +117 -0
  126. package/dist/mcp/tools/attachments.js.map +1 -0
  127. package/dist/mcp/tools/cicd.js +105 -0
  128. package/dist/mcp/tools/cicd.js.map +1 -0
  129. package/dist/mcp/tools/discovery.js +34 -0
  130. package/dist/mcp/tools/discovery.js.map +1 -0
  131. package/dist/mcp/tools/issues.js +56 -0
  132. package/dist/mcp/tools/issues.js.map +1 -0
  133. package/dist/mcp/tools/merge_requests.js +67 -0
  134. package/dist/mcp/tools/merge_requests.js.map +1 -0
  135. package/dist/mcp/tools/metadata.js +127 -0
  136. package/dist/mcp/tools/metadata.js.map +1 -0
  137. package/dist/mcp/tools/peer_sync.js +43 -0
  138. package/dist/mcp/tools/peer_sync.js.map +1 -0
  139. package/dist/mcp/tools/repository.js +178 -0
  140. package/dist/mcp/tools/repository.js.map +1 -0
  141. package/dist/mcp/tools/work_tracking.js +125 -0
  142. package/dist/mcp/tools/work_tracking.js.map +1 -0
  143. package/dist/mcp/tools/workitems.js +30 -0
  144. package/dist/mcp/tools/workitems.js.map +1 -0
  145. package/dist/mcp/volume-files.js +187 -0
  146. package/dist/mcp/volume-files.js.map +1 -0
  147. package/dist/mcp-auth-store.js +134 -0
  148. package/dist/mcp-auth-store.js.map +1 -0
  149. package/dist/mcp-config-store.js +83 -0
  150. package/dist/mcp-config-store.js.map +1 -0
  151. package/dist/mention.js +512 -0
  152. package/dist/mention.js.map +1 -0
  153. package/dist/migrate.js +73 -0
  154. package/dist/migrate.js.map +1 -0
  155. package/dist/mr-assignee.js +71 -0
  156. package/dist/mr-assignee.js.map +1 -0
  157. package/dist/mr-content.js +90 -0
  158. package/dist/mr-content.js.map +1 -0
  159. package/dist/oauth-routes.js +54 -0
  160. package/dist/oauth-routes.js.map +1 -0
  161. package/dist/onboarding-allowlist.js +32 -0
  162. package/dist/onboarding-allowlist.js.map +1 -0
  163. package/dist/opencode.js +291 -0
  164. package/dist/opencode.js.map +1 -0
  165. package/dist/outbound-webhook-store.js +229 -0
  166. package/dist/outbound-webhook-store.js.map +1 -0
  167. package/dist/outbound-webhook.js +314 -0
  168. package/dist/outbound-webhook.js.map +1 -0
  169. package/dist/path-sandbox.js +200 -0
  170. package/dist/path-sandbox.js.map +1 -0
  171. package/dist/platform/git-auth.js +34 -0
  172. package/dist/platform/git-auth.js.map +1 -0
  173. package/dist/platform/gitealike/attachments.js +62 -0
  174. package/dist/platform/gitealike/attachments.js.map +1 -0
  175. package/dist/platform/gitealike/base.js +935 -0
  176. package/dist/platform/gitealike/base.js.map +1 -0
  177. package/dist/platform/gitealike/forgejo.js +16 -0
  178. package/dist/platform/gitealike/forgejo.js.map +1 -0
  179. package/dist/platform/gitealike/gitea.js +16 -0
  180. package/dist/platform/gitealike/gitea.js.map +1 -0
  181. package/dist/platform/gitealike/http.js +152 -0
  182. package/dist/platform/gitealike/http.js.map +1 -0
  183. package/dist/platform/gitealike/schemas.js +284 -0
  184. package/dist/platform/gitealike/schemas.js.map +1 -0
  185. package/dist/platform/github/attachments.js +52 -0
  186. package/dist/platform/github/attachments.js.map +1 -0
  187. package/dist/platform/github/http.js +153 -0
  188. package/dist/platform/github/http.js.map +1 -0
  189. package/dist/platform/github/index.js +936 -0
  190. package/dist/platform/github/index.js.map +1 -0
  191. package/dist/platform/github/schemas.js +308 -0
  192. package/dist/platform/github/schemas.js.map +1 -0
  193. package/dist/platform/github/zip.js +85 -0
  194. package/dist/platform/github/zip.js.map +1 -0
  195. package/dist/platform/gitlab/admin.js +105 -0
  196. package/dist/platform/gitlab/admin.js.map +1 -0
  197. package/dist/platform/gitlab/api.js +229 -0
  198. package/dist/platform/gitlab/api.js.map +1 -0
  199. package/dist/platform/gitlab/attachments.js +79 -0
  200. package/dist/platform/gitlab/attachments.js.map +1 -0
  201. package/dist/platform/gitlab/http.js +165 -0
  202. package/dist/platform/gitlab/http.js.map +1 -0
  203. package/dist/platform/gitlab/index.js +630 -0
  204. package/dist/platform/gitlab/index.js.map +1 -0
  205. package/dist/platform/gitlab/oauth.js +58 -0
  206. package/dist/platform/gitlab/oauth.js.map +1 -0
  207. package/dist/platform/gitlab/read.js +182 -0
  208. package/dist/platform/gitlab/read.js.map +1 -0
  209. package/dist/platform/gitlab/schemas-read.js +274 -0
  210. package/dist/platform/gitlab/schemas-read.js.map +1 -0
  211. package/dist/platform/gitlab/schemas.js +251 -0
  212. package/dist/platform/gitlab/schemas.js.map +1 -0
  213. package/dist/platform/gitlab/signature.js +52 -0
  214. package/dist/platform/gitlab/signature.js.map +1 -0
  215. package/dist/platform/index.js +38 -0
  216. package/dist/platform/index.js.map +1 -0
  217. package/dist/platform/port.js +26 -0
  218. package/dist/platform/port.js.map +1 -0
  219. package/dist/platform/types.js +80 -0
  220. package/dist/platform/types.js.map +1 -0
  221. package/dist/podman.js +119 -0
  222. package/dist/podman.js.map +1 -0
  223. package/dist/project-sync.js +69 -0
  224. package/dist/project-sync.js.map +1 -0
  225. package/dist/prompt.js +47 -0
  226. package/dist/prompt.js.map +1 -0
  227. package/dist/queue-store.js +236 -0
  228. package/dist/queue-store.js.map +1 -0
  229. package/dist/queue.js +129 -0
  230. package/dist/queue.js.map +1 -0
  231. package/dist/redact.js +23 -0
  232. package/dist/redact.js.map +1 -0
  233. package/dist/run-files.js +297 -0
  234. package/dist/run-files.js.map +1 -0
  235. package/dist/run-filters.js +54 -0
  236. package/dist/run-filters.js.map +1 -0
  237. package/dist/run-history-store.js +229 -0
  238. package/dist/run-history-store.js.map +1 -0
  239. package/dist/run-log-sink.js +270 -0
  240. package/dist/run-log-sink.js.map +1 -0
  241. package/dist/run-log.js +97 -0
  242. package/dist/run-log.js.map +1 -0
  243. package/dist/run-usage.js +29 -0
  244. package/dist/run-usage.js.map +1 -0
  245. package/dist/run-view.js +112 -0
  246. package/dist/run-view.js.map +1 -0
  247. package/dist/runner.js +1094 -0
  248. package/dist/runner.js.map +1 -0
  249. package/dist/runtime-apply.js +26 -0
  250. package/dist/runtime-apply.js.map +1 -0
  251. package/dist/runtime-settings.js +71 -0
  252. package/dist/runtime-settings.js.map +1 -0
  253. package/dist/runtime-state.js +19 -0
  254. package/dist/runtime-state.js.map +1 -0
  255. package/dist/server.js +119 -0
  256. package/dist/server.js.map +1 -0
  257. package/dist/settings-store.js +320 -0
  258. package/dist/settings-store.js.map +1 -0
  259. package/dist/status.js +121 -0
  260. package/dist/status.js.map +1 -0
  261. package/dist/stop.js +56 -0
  262. package/dist/stop.js.map +1 -0
  263. package/dist/sweeps.js +87 -0
  264. package/dist/sweeps.js.map +1 -0
  265. package/dist/task-log.js +279 -0
  266. package/dist/task-log.js.map +1 -0
  267. package/dist/task-view.js +62 -0
  268. package/dist/task-view.js.map +1 -0
  269. package/dist/token-store.js +68 -0
  270. package/dist/token-store.js.map +1 -0
  271. package/dist/types/access.js +74 -0
  272. package/dist/types/access.js.map +1 -0
  273. package/dist/types/custom-repos.js +28 -0
  274. package/dist/types/custom-repos.js.map +1 -0
  275. package/dist/types/env-config.js +25 -0
  276. package/dist/types/env-config.js.map +1 -0
  277. package/dist/types/env.js +190 -0
  278. package/dist/types/env.js.map +1 -0
  279. package/dist/types/global-settings.js +178 -0
  280. package/dist/types/global-settings.js.map +1 -0
  281. package/dist/types/lsp-servers.js +145 -0
  282. package/dist/types/lsp-servers.js.map +1 -0
  283. package/dist/types/mcp-config.js +77 -0
  284. package/dist/types/mcp-config.js.map +1 -0
  285. package/dist/types/opencode.js +59 -0
  286. package/dist/types/opencode.js.map +1 -0
  287. package/dist/types/outbound-webhook.js +56 -0
  288. package/dist/types/outbound-webhook.js.map +1 -0
  289. package/dist/types/project-settings.js +114 -0
  290. package/dist/types/project-settings.js.map +1 -0
  291. package/dist/types/provider-registry.js +37 -0
  292. package/dist/types/provider-registry.js.map +1 -0
  293. package/dist/types/runtime-settings.js +78 -0
  294. package/dist/types/runtime-settings.js.map +1 -0
  295. package/dist/usage-stats.js +159 -0
  296. package/dist/usage-stats.js.map +1 -0
  297. package/dist/version.js +19 -0
  298. package/dist/version.js.map +1 -0
  299. package/dist/volume-gc.js +193 -0
  300. package/dist/volume-gc.js.map +1 -0
  301. package/dist/volume-manage.js +191 -0
  302. package/dist/volume-manage.js.map +1 -0
  303. package/dist/volume-store.js +122 -0
  304. package/dist/volume-store.js.map +1 -0
  305. package/dist/webhook.js +1106 -0
  306. package/dist/webhook.js.map +1 -0
  307. package/dist/work-item-auto-store.js +75 -0
  308. package/dist/work-item-auto-store.js.map +1 -0
  309. package/dist/work-item-branch-store.js +37 -0
  310. package/dist/work-item-branch-store.js.map +1 -0
  311. package/migrations/0001_initial.sql +59 -0
  312. package/migrations/0002_queue_status.sql +15 -0
  313. package/migrations/0003_work_item_base_branch.sql +14 -0
  314. package/migrations/0004_project_settings.sql +15 -0
  315. package/migrations/0005_global_settings.sql +34 -0
  316. package/migrations/0006_model_variants.sql +8 -0
  317. package/migrations/0007_container_error_logs.sql +21 -0
  318. package/migrations/0008_project_access_grants.sql +16 -0
  319. package/migrations/0009_drop_imported_legacy.sql +4 -0
  320. package/migrations/0010_container_error_log_model.sql +6 -0
  321. package/migrations/0011_container_error_log_title.sql +5 -0
  322. package/migrations/0012_pending_task_cancel.sql +6 -0
  323. package/migrations/0013_builder_image.sql +6 -0
  324. package/migrations/0014_build_workflow.sql +7 -0
  325. package/migrations/0015_access_rules.sql +24 -0
  326. package/migrations/0016_trigger_policy.sql +17 -0
  327. package/migrations/0017_attachment_upload.sql +8 -0
  328. package/migrations/0018_work_item_branch.sql +11 -0
  329. package/migrations/0019_auto_push_mr.sql +18 -0
  330. package/migrations/0020_outbound_webhooks.sql +47 -0
  331. package/migrations/0021_runtime_settings.sql +12 -0
  332. package/migrations/0022_auto_resolve_conflicts.sql +17 -0
  333. package/migrations/0023_ci_fail_auto_report.sql +27 -0
  334. package/migrations/0024_work_item_auto_override.sql +22 -0
  335. package/migrations/0025_run_history.sql +37 -0
  336. package/migrations/0026_run_history_task_seq.sql +6 -0
  337. package/migrations/0027_custom_repos.sql +11 -0
  338. package/migrations/0028_run_usage.sql +14 -0
  339. package/migrations/0029_drop_ocr_langs.sql +6 -0
  340. package/migrations/0030_mr_assignee.sql +9 -0
  341. package/migrations/0031_lsp_servers.sql +14 -0
  342. package/opencode-config/managed/devbot-agent-policy.md +61 -0
  343. package/opencode-config/managed/opencode.json +77 -0
  344. package/opencode-config/opencode.json +211 -0
  345. package/opencode-config/plugins/devbot-path-guard.js +224 -0
  346. package/package.json +157 -4
  347. package/public/assets/DashboardSidebarCollapse-rIuzCmVm.js +1 -0
  348. package/public/assets/DashboardToolbar-DFCUqZ4Z.js +1 -0
  349. package/public/assets/ErrorLogModal.vue_vue_type_script_setup_true_lang-BCyNIf1E.js +1 -0
  350. package/public/assets/ErrorLogsPage-DyYK2Rop.js +1 -0
  351. package/public/assets/LogSlideover.vue_vue_type_script_setup_true_lang-33xie7TE.js +1 -0
  352. package/public/assets/OverviewPage-CFfa0uwg.js +1 -0
  353. package/public/assets/PageGrid-CBROIU87.js +1 -0
  354. package/public/assets/PeriodRangePicker.vue_vue_type_script_setup_true_lang-DiFUNem7.js +1 -0
  355. package/public/assets/ProjectDetailPage-CreyvUit.js +1 -0
  356. package/public/assets/ProjectsPage-CFx8-elJ.js +1 -0
  357. package/public/assets/RunDetailSlideover.vue_vue_type_script_setup_true_lang-DopYYmCT.js +1 -0
  358. package/public/assets/RunsPage-CRnnXU4P.js +1 -0
  359. package/public/assets/Select-Bvx32Diz.js +1 -0
  360. package/public/assets/SelectMenu-BjMxc4VB.js +1 -0
  361. package/public/assets/SettingsPage-Bb6LM3wO.js +60 -0
  362. package/public/assets/TimeCell.vue_vue_type_script_setup_true_lang-DW_UB0Fu.js +1 -0
  363. package/public/assets/UsagePage-BLMDEsv-.js +332 -0
  364. package/public/assets/UsagePage-DodEeAjB.css +1 -0
  365. package/public/assets/VolumesPage-CxcLc3L2.js +1 -0
  366. package/public/assets/WebhookConfigPanel.vue_vue_type_script_setup_true_lang-B2V2GF6b.js +1 -0
  367. package/public/assets/WorkItemCell.vue_vue_type_script_setup_true_lang-BQn6vTjd.js +1 -0
  368. package/public/assets/index-BMbMibyb.js +4 -0
  369. package/public/assets/index-C5RBBrPj.js +30 -0
  370. package/public/assets/index-CgX29UgC.css +1 -0
  371. package/public/assets/usage-format-jsb31_O8.js +1 -0
  372. package/public/assets/useManagedProjects-8rtjRqVJ.js +1 -0
  373. package/public/assets/useRouteTab-DfwrcMic.js +1 -0
  374. package/public/favicon.ico +0 -0
  375. package/public/favicon.png +0 -0
  376. package/public/index.html +15 -0
package/README.md CHANGED
@@ -1,3 +1,639 @@
1
- # Temporary Holding Version
1
+ # DevBot
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ NodeJS orchestrator that runs [OpenCode](https://opencode.ai) agents in Podman containers, triggered by forge webhooks when the bot is mentioned in a work item (issue, task, etc.).
4
+
5
+ DevBot is **forge-agnostic**: a platform abstraction layer supports **GitLab**, **ForgeJo**, **Gitea**, and **GitHub**. One bot instance talks to exactly one forge instance, selected with `PLATFORM` (`gitlab|forgejo|gitea|github`). GitLab is the default. ForgeJo and Gitea share a common `/api/v1` driver; GitHub targets github.com (not Enterprise Server).
6
+
7
+ ## Workflow
8
+
9
+ 1. The forge sends a webhook when the bot account is `@mentioned` in a work item (comment on the work item, or work item creation with a mention in the description).
10
+ 2. The mention is parsed: `@devbot /build <prompt>` triggers **build mode**; anything else triggers plan mode (the default).
11
+ 3. DevBot starts a Podman container with a per-work-item named volume (`devbot-wi-<id>`), bind-mounting the OpenCode config read-only.
12
+ 4. The container clones the repo (first run) or fetches (subsequent runs). On first run it clones with a fallback chain: `wi-<number>` (prior WIP branch if it exists remotely) → the `/base` target branch if given, else the first existing branch from the global base-branches list tried **top-to-bottom** (default `dev,master` → tries `dev` then `master`, set in the WebUI). A `/branch <name>` mention overrides that work-branch name (stored per work item, first naming only; a name that already exists remotely is rejected). In build mode it then force-checks-out the work branch off whatever HEAD landed on; plan mode stays on the successful clone's branch. If neither the work branch nor any configured branch exists remotely (e.g. a brand-new repo with no branches), the run aborts with an actionable `⚠️ No base branch found` note — no bare-clone fall-through. On subsequent runs it fetches and checks out the work branch (build mode pulls --rebase; plan mode tolerates a missing branch). With `GIT_SUBMODULES_ENABLED` (default `true`), clone and each fetch also run `git submodule update --init --recursive` (best-effort; see [Git submodules](#git-submodules)).
13
+ 5. OpenCode runs with `--continue` (one session per work item, resumed across mentions) and `--format json`. The agent is `plan` or `build` per the mode.
14
+ 6. **Build mode only**: the container commits and pushes to the branch. Plan mode makes no git changes.
15
+ 7. DevBot parses the JSON output and extracts assistant `text` parts only. In build mode it also extracts the agent's ` ```mr ` content block (title + description) from the reply, stripping it from the comment text.
16
+ 8. DevBot posts the result as a top-level comment on the work item.
17
+ 9. **Build mode only**: DevBot creates a merge/pull request targeting the base branch if none exists for the branch, using the agent-authored title + structured-Markdown description (ending with `Resolves #<number>` so the forge auto-closes the work item on merge); if a request already exists, it updates the title + description with the agent's latest content.
18
+ 10. **CI failure auto report (opt-in)**: when enabled, a failed pipeline on an MR DevBot itself created automatically starts a **plan run** on the linked work item with the failing CI job logs (tails) as context. See [CI failure auto report](#ci-failure-auto-report).
19
+
20
+ ## Modes
21
+
22
+ - **Plan mode** (default): the agent analyzes and replies with a plan/note. No branch, commit, push, or MR. Bash is allowed for read-only inspection (`git log`, `grep`, `find`) but destructive/mutating commands (`rm`, `git push/commit/checkout/reset/rebase/merge/cherry-pick`) are denied. File edits are denied.
23
+ - **Build mode** (`/build`): the agent implements changes on branch `wi-<iid>` and commits. Publishing is opt-in: the branch is pushed and the MR opened only when **Settings → Runtime → Build output** enables auto-push / auto-MR (both off by default), or per run with the `/push` and `/mr` tokens. Add `/force-push` to make the host-side push use `--force-with-lease` (see [Safety](#safety)). On its own, `/force-push` is a **push-only command** — no agent run; it force-pushes the work item's existing local branch. `/push` and `/mr` also work standalone as push-only / publish-and-open tasks (see below). When **auto-resolve conflicts** is enabled, DevBot checks the branch against its MR target before opening the MR and, on conflicts, runs a direct build run to rebase/merge the target in; if it cannot resolve them, the branch is pushed but the MR is skipped with an actionable note. `/fix-conflicts` re-runs that conflict fix on demand (e.g. after an MR's target branch moved) without publishing or touching the MR. The host-side publish is rewrite-aware: it force-pushes (SHA-pinned lease) only when the remote still equals the run's starting tip, so a rebase never errors with git's raw fast-forward hint.
24
+
25
+ ## Subagents
26
+
27
+ Subagents are supplied by admin-registered git repositories rather than a vendored bundle (work item #63): register one or more repositories of opencode agent markdown under **Settings → Skills & Agents**, and every `/agent <name>` mention routes to an installed agent. The bot injects `@<name>` into the prompt and OpenCode routes to that subagent. Multiple `/agent` calls can appear in a single mention, on any lines, anywhere in the message — each gets its own prompt segment and all run in one OpenCode session. An unknown name is reported in-thread after the repositories sync. OpenCode's built-in `general`, `explore`, and `scout` subagents are always available.
28
+
29
+ The full command reference — every token, its parsing rules, combinations, and precedence — lives in [docs/commands.md](./docs/commands.md). The table below is the quick summary.
30
+
31
+ | Token | Effect |
32
+ |---|---|
33
+ | `/build` | build mode (existing) |
34
+ | `/base <branch>` | set the base/target branch for this work item's first run. Overrides the global base-branch walk entirely (hard per-work-item override) and retargets the MR (build mode). First-run only: on subsequent runs it's stripped and a `⚠️ /base '<branch>' ignored — work item already initialized.` note is posted. Rejects the whole mention if the branch is missing on the remote or the name has unsafe characters. Persists per work item (first-write-wins); cleared by `/reset` and work-item close. May appear anywhere, like `/build`; duplicate `/base` in one mention → last wins. |
35
+ | `/branch <name>` | name this work item's work branch for every run (default `wi-<number>`). Honored on the **first naming only**: the name is stored per work item (first-write-wins) and reused on later runs, including after `/reset` and work-item close. Rejects the whole mention if the name has unsafe characters, or if the branch already exists on the remote (fail-closed — never renames or clobbers). A later, different `/branch` on an already-named work item is ignored with a `⚠️` note. May appear anywhere, like `/build`; duplicate `/branch` in one mention → last wins. |
36
+ | `/agent <name> <segment>` | route `<segment>` to subagent `<name>` (case-insensitive, `.md` suffix optional). May appear multiple times in one mention, on any lines. `<segment>` is the rest of the line after the `/agent <name>` token. |
37
+ | `/force-push` | standalone push-only command (no agent run) **or** a build modifier. Alone, it dispatches a push task that force-pushes the work item's existing local `wi-<iid>` branch with `--force-with-lease` — no opencode run, no clone, no commit; it fails with an actionable note if the workspace or branch doesn't exist. Combined with `/build`, it keeps the original meaning: run the agent, then force-push its result. Stripped from the prompt wherever it appears, like `/build`. |
38
+ | `/push` | standalone push-only command (no agent run) **or** a build modifier. Alone, it publishes the work item's existing local branch with a plain fast-forward push (no force); if nothing changed it posts `ℹ️ Nothing to push`. Combined with `/build`, it forces the run's host-side push even when the global/project auto-push setting is off. Stripped from the prompt wherever it appears. |
39
+ | `/mr` | standalone publish-and-open command (no agent run) **or** a build modifier. Alone, it publishes the existing local branch (skipping the push when already up to date) then creates/updates its MR with the fallback title/description. Combined with `/build`, it forces both the push and the MR even when auto-push/auto-MR are off. `/mr` implies a push. Stripped from the prompt wherever it appears. |
40
+ | `/fix-conflicts` | standalone conflict-fix command (no agent run) **or** a build modifier. Alone, it re-runs the conflict resolver on the work item's existing branch against the current target of its open MR (falling back to the `/base` target), then re-pushes the resolved branch — no publish-first and no MR create/update. Combined with `/build`, it forces the post-build conflict check for that run. Bypasses the auto-resolve-conflicts setting. Stripped from the prompt wherever it appears. |
41
+ | `/reset` | destroy this work item's volumes (`devbot-wi-<id>` and its `-podman` / `-xdg` siblings) and clear any `/base` choice. Force-stops any in-flight session container first, then removes the volumes. Volume-only: the `wi-<iid>` branch and any open MR are untouched. Posts a confirmation note back. Short-circuits over `/build`, `/base`, `/force-push`, and `/agent`. |
42
+ | `/stop` | cancel this work item's running task and drop its queued backlog. Kills the phase containers (`-clone`/`-sync`/`-run`/`-push`/…); the run unwinds cleanly — no `⚠️` error reply, no error-log row, no push/MR. The volume is untouched (`/reset` discards it). Short-circuits over `/build`, `/base`, `/force-push`, and `/agent`; `/reset` wins if both appear. |
43
+ | `/help` | reply-only command: posts the list of command tokens with one-line descriptions and dispatches no run. Short-circuits over every token except `/reset` and `/stop` (both still win). Sits behind the usual trigger policy and needs no provider/model config. |
44
+ | `/status` | reply-only command: posts a local snapshot of this work item's running/queued tasks, global gate depth, branch/MR, retries, and effective auto-* settings, and dispatches no run. Insensitive to `QUEUE_FEEDBACK`; allowed for plan-only subjects. `/help` wins if both appear. |
45
+ | `/auto-push`, `/auto-mr`, `/auto-resolve-conflicts`, `/auto-ci-report` | contextual auto-* commands (work item #58). Bare token queries the feature's effective state for this work item; `… off` disables it here (wins over a project-on setting) and `… on` re-enables it. A non-admin cannot enable a feature the project resolves **off** — that requires an allowlisted DevBot admin (`ONBOARDING_USERS`), and the enable is for this work item only. Applied and acknowledged before any run in the same mention is dispatched; a mention with only these tokens dispatches nothing. `/reset` and `/stop` still win. |
46
+ | `/auto` | query all four auto-* features at once; `/auto off` disables all four for this work item; `/auto on` enables all four (per-feature admin check on project-off features). |
47
+ | `/ask` | alias of `/auto off` — disable all four auto-* features for this work item. |
48
+
49
+ A token is a command only when it stands as its own word: preceded by the start of a line or whitespace, and not followed by a word character or `/`. A token embedded in a path (`(some/foo/bar)`, `/build/output`, `https://host/x/build/y`) is literal prose and preserved verbatim in the prompt; so is a token inside inline code, a fenced block, `"…"` / `'…'` quotes, `(…)` parentheses, or `**…**` bold (parentheses nesting-aware). Sentence-final punctuation is fine (`/build.` still builds).
50
+
51
+ `/build` is a global flag — order-independent, stripped wherever it appears. `/agent <name>` is line-scoped: one `/agent` per line, segment is the rest of that line. `/base <branch>` and `/branch <name>` are global flags like `/build`, stripped wherever they appear (`/branch` names the work branch, `/base` its MR target). `/force-push`, `/push`, and `/mr` are global commands/modifiers like `/build`; standalone they dispatch their own task, and with `/build` they force the corresponding build-output step. Standalone they short-circuit over `/base` and `/agent` (they neither clone nor run the agent) but, unlike `/reset`, are still queued behind any in-flight run for the same work item. `/reset` short-circuits entirely: it wins over every other token and no run is dispatched. `/stop` short-circuits over `/build`/`/agent` too; `/reset` still wins if both appear. Non-`/agent` prose (before, between, after `/agent` lines) becomes a shared preamble prepended once to the combined OpenCode prompt; the main `plan`/`build` agent reads it as background, then dispatches each `@agent` block in one session.
52
+
53
+ ### Single-agent examples
54
+
55
+ All equivalent (segment folds the pre-`/agent` text into the agent's prompt):
56
+
57
+ ```
58
+ @devbot /build /agent python-pro review types here
59
+ @devbot /agent python-pro /build review types here
60
+ @devbot /build review types here /agent python-pro
61
+ ```
62
+
63
+ Result: `mode=build`, prompt sent to opencode = `@python-pro review types here`.
64
+
65
+ ### Multi-agent examples
66
+
67
+ Each `/agent` line carries its own segment; other lines are shared context:
68
+
69
+ ```
70
+ @devbot looking at this bug.
71
+
72
+ /agent python-pro review the models
73
+
74
+ Also check the FE here.
75
+
76
+ /agent typescript-pro fix the typing
77
+ ```
78
+
79
+ Result: `mode=plan`, prompt sent to opencode:
80
+
81
+ ```
82
+ looking at this bug.
83
+
84
+ Also check the FE here.
85
+
86
+ @python-pro review the models
87
+
88
+ @typescript-pro fix the typing
89
+ ```
90
+
91
+ One OpenCode run, one combined note posted back, build mode (if set) commits once at the end.
92
+
93
+ ### Errors (no run dispatched)
94
+
95
+ - `@devbot /agent nope …` → `⚠️ Unknown agent: nope.` (validated after the configured agent repositories sync)
96
+ - `@devbot /agent python-pro` (no segment on the line) → `⚠️ No prompt given for /agent python-pro.`
97
+ - `@devbot /agent python-pro x\n/agent python-pro y` → `⚠️ Duplicate agent: python-pro.`
98
+
99
+ Step-0 error replies (unknown agent, authorization denial, `/reset` failure) are posted **in the same discussion thread** as the triggering mention when one exists (Note Hook), or as a **top-level note** on the work-item-creation path (Issue Hook, where there's no triggering note to reply to).
100
+
101
+ ## Prompt construction
102
+
103
+ - **First run** on a work item (volume `devbot-wi-<id>` doesn't exist yet): the prompt is `Work item #<iid>: <title>\n\n<description>\n\n---\n\n<mention text>`, seeding the OpenCode session with full work item context. If the description is empty, it's omitted. If the mention text is empty, the separator is omitted.
104
+ - **Subsequent runs** (volume exists → session has prior context): the prompt is the cleaned mention text only (work item title + description are already in the session history).
105
+
106
+ ## Requirements
107
+
108
+ - Node.js 22+
109
+ - Podman on the host PATH
110
+ - A forge bot account (numeric user ID + access token) — GitLab, ForgeJo, Gitea, or GitHub
111
+ - An LLM provider API key (Anthropic, OpenAI, etc.)
112
+ - **Attachments (optional)**: DevBot does not read or OCR attachment content itself. The work item's attachment links are passed to the agent, which downloads the whole files on demand (`download_work_item_attachment`) and handles them with its own tools. OCR/PDF text extraction runs inside the builder image (`docker/Dockerfile.builder` installs `tesseract`, `tesseract-langpack-all`, and `poppler-utils`), so the agent can OCR an image or extract PDF text itself. The only attachment knob is `ATTACHMENT_MAX_BYTES` (the download/upload size cap).
113
+
114
+ ## Global settings
115
+
116
+ Provider credentials, global plan/build models (with optional variants), OpenCode plugins, base branches, the post-build auto-push / auto-MR / conflict auto-fix switches, the CI-failure auto-report switch, and the global LSP server selection are configured from the web panel's **Settings** page (sidebar → Settings), with global MCP servers on the **Settings → MCP** tab; all are stored in `devbot.db` — not `.env`. The AI provider list is pulled by the browser from [models.dev](https://models.dev/api.json) when the page opens; a custom OpenAI-compatible provider can be added manually. Legacy `LLM_*`/`OPENCODE_PLUGINS`/`BASE_BRANCHES`/`ATTACHMENT_OCR_LANG` env vars (and `GITLAB_BOT_URL`) are removed and ignored — there is no auto-import, so configure everything in the WebUI. See [docs/global-settings.md](./docs/global-settings.md). Until a provider with a key and both global models are set, mentions are blocked with an actionable note.
117
+
118
+ ## CI failure auto report
119
+
120
+ Opt-in (**Settings → Runtime → CI failure reports**, plus a per-project override, both off by default). When on, a failed CI pipeline on a merge/pull request that DevBot **created** starts an automatic plan run on the linked work item, with the failing job logs (tails — the head is dropped on long logs) injected as read-only context. Detection uses forge webhooks (GitLab Pipeline Hook, GitHub/ForgeJo/Gitea `workflow_run`), so it is near-realtime.
121
+
122
+ "Created by DevBot" is matched against a durable registry of MRs the runner opened — not a title or author heuristic. The plan run is skipped when the work item is closed, the bot is unconfigured, or the same pipeline was already reported (dedup by pipeline id/sha). Enabling the toggle attempts to subscribe the project's forge webhook to CI events using the signed-in user's OAuth token (the bot token cannot manage hooks); if that fails, re-onboard the project. Turning the toggle off leaves the subscription in place — the server ignores CI events while it is off. See [docs/global-settings.md](./docs/global-settings.md#ci-failure-reports) and [docs/platforms.md](./docs/platforms.md#ci-failure-webhooks).
123
+
124
+ ## Runs
125
+
126
+ The sidebar's **Runs** page is one table over every run, with a **status filter** (`All runs` · `Active` · `Errored` · `Cancelled` · `Settled`, stored in the URL as `?status=` so it is deep-linkable). **Active** covers queued and running tasks; **Errored** shows failed runs; **Cancelled** shows stopped runs; **Settled** covers every finished run (ok, failed, or stopped). Alongside the status filter the header toolbar offers the same filters as the Usage page — period (7/14/30/90 days, all time, or a custom range), project, model, and mode — plus a debounced **work item title search**. Rows show the work item (`#iid title`, linked to the forge when the project is managed), project, mode, the resolved **model** (`provider/model (variant)`, variant omitted when unset), state, phase, queued and finished times, duration, and retry attempts; the table never exposes the agent prompt or work item description. The concurrency quota (`active / max` running, `waiting` on the gate) is shown for the **All runs** and **Active** filters. The old `/history` URL redirects to `/runs?status=settled`.
127
+
128
+ The listing is served by `GET /api/runs?status=all|active|errored|cancelled|settled&days=&projectId=&model=&mode=&q=&limit=&offset=` (a custom range uses `from=YYYY-MM-DD&to=YYYY-MM-DD` instead of `days`), which merges the in-flight queue and the settled run history on their shared task `seq` (newest first); the response also carries the `filterOptions` and `projectPaths` the dropdowns need. The queue and history sources are also still available directly at `GET /api/tasks` and `GET /api/run-history/:id`.
129
+
130
+ Each row has a **Logs** button. For an active task it opens a live view of the **currently-active phase** container output — clone, sync, auth, agent run, push, and so on — not just the agent's `-run` container, polled every couple of seconds while the slideover is open (log bodies are redacted). The viewer names the active phase (e.g. "Cloning repository") and shows its output as it arrives; when the active phase has produced no output yet it says so explicitly. Every line is prefixed with a server-local `HH:MM:SS` clock derived from `podman logs --timestamps` (opencode event timestamps are used as a fallback; shell-only lines with neither source stay unprefixed). Because phase containers are short-lived (`--rm`), live logs are only available while a phase is in flight; phase containers pin the `k8s-file` log driver so `podman logs` can serve a running container on hosts whose default driver (e.g. `journald`) would otherwise expose nothing until exit. For a settled run the same button reads the captured run-log file instead; a run that captured nothing has it disabled. If a task finishes while you are watching it, the live poll switches to the archived file automatically.
131
+
132
+ Each active row also has a **Stop** button (two-step inline confirm): it cancels that one task — a queued row is removed, a running row is flagged and its phase containers are killed, and the stop is posted to the work item thread. Stopping via the button is task-scoped; `/stop` in a work-item thread is work-item-scoped and also drops the queued backlog.
133
+
134
+ Log rendering is verbose: every opencode part type is shown. Tool calls print their title, compact input, and indented output (or `error:` on failure); `step-finish` lines carry token counts and cost; `file`, `subtask`, and `retry` parts carry their detail; unknown part types dump their JSON body. Tool input/output and unknown-part bodies are capped per event (`…[truncated]`) so one huge result can't crowd out the rest.
135
+
136
+ ### Run-log capture and retention
137
+
138
+ Capture is in-process: every phase container (clone, sync, auth, detect, agent run, push, conflict check, tip, and MCP-triggered peer/attachment operations) has its stdout/stderr tee'd into one file per run at `RUN_LOG_DIR` (default `run-logs/`, or `${INSTANCE_ID}-run-<seq>.log` when `INSTANCE_ID` is set). Output is redacted before it touches disk (line-buffered so a secret split across stream chunks is redacted whole) and capped at 2 MB per file (`log_truncated` is then set). `run_history` in `devbot.db` stores only metadata plus the filename — never the body. Reads use the same formatter as the live and error-log views, so NDJSON becomes readable lines with a server-local `HH:MM:SS` clock.
139
+
140
+ Log files are retained for `RUN_LOG_TTL_HOURS` (default **48**; `0` keeps them forever), configurable in **Settings → Runtime → Maintenance** or via the env var as the fallback. Expiry unlinks the **files** only: the history row survives and the detail view says "logs expired". This is separate from `ERROR_LOG_RETENTION_DAYS`, which governs error-**row** retention. In a container deployment, mount a volume at `RUN_LOG_DIR` so the files survive container recreation. This is why no `journalctl` integration exists: phase containers pin the `k8s-file` log driver and are `--rm`, so host journald has nothing to read.
141
+
142
+ Below the Runs page, an **Error logs** card lists persisted failures from every phase (attachment, prepare/clone/sync, run, push, MR) with the final message, the exact model the run used, and redacted output. Only the final failure is stored — retries collapse to one row. The header toolbar filters by the same period/project/model/mode as Usage and searches the work item title (`GET /api/error-logs?days=&projectId=&model=&mode=&q=&limit=&offset=`). Rows older than `ERROR_LOG_RETENTION_DAYS` (default 30; `0` keeps them forever) are purged at startup and by a daily sweep; see the configuration table.
143
+
144
+ ## Usage
145
+
146
+ The sidebar's **Usage** page aggregates token consumption and cost across settled runs. Every run's opencode output already carries per-step token counts and the USD cost opencode computed; DevBot sums those `step-finish` parts while the container streams, accumulates them per work item (the main agent run and any conflict-resolver runs), and stores the total on the run's history row when the task settles. Retries reuse the same task row, so all attempts sum into one entry. In-flight runs are not counted — the page covers **settled** runs only, and rows recorded before this feature read as zero.
147
+
148
+ The page is filterable by period (7/14/30/90 days, or all time), project, model, and mode, and shows stat cards (runs, tokens in/out, cache, total, cost) with period-over-period deltas, a stacked token or line-cost chart, full-width by-model/by-project/by-mode breakdown rows, and the ten most recent matching runs. Every breakdown and the recent-runs table splits tokens into **In / Out / Cache** columns (Out counts reasoning, Cache counts read + write). The token chart has a color legend (input, output, cache read, cache write). Per-run tokens and cost are also shown in the Runs table and the run detail slideover.
149
+
150
+ The period select also offers **Custom**, which reveals a date-range picker (two months side by side, bounded to the last 365 days). Selecting a range filters every card, the chart, and the tables to those calendar days (inclusive at both ends); the deltas compare against the immediately preceding, equally long window.
151
+
152
+ The data is served by `GET /api/stats/usage?days=&projectId=&model=&mode=` (session-gated), which reads `run_history` and aggregates in a pure module (local-calendar-day buckets). A custom range is requested with `from=YYYY-MM-DD&to=YYYY-MM-DD` instead of `days`.
153
+
154
+ ### Custom-model prices
155
+
156
+ OpenCode computes `cost` from a model's price table, and a price in the generated `opencode.json` overrides the models.dev catalog. Custom providers added in **Settings → Providers** therefore have optional per-model price fields (**input**, **output**, **cache read**, **cache write**) in **USD per 1M tokens** — the same unit models.dev and opencode use. When any field is non-zero the provider's model is emitted with a `cost` block in the per-run config; leave all fields empty to use the provider default (or report no cost). Registry providers take their prices from models.dev and are not editable here.
157
+
158
+ ## Volumes
159
+
160
+ The sidebar's **Volumes** page lists the per-work-item volumes and their GC state, and lets an admin delete one. A header toolbar filters by GC state and project and searches the work item title. It merges the volume ledger (`work_item_volumes`) with `podman volume ls`, so a volume that exists in podman but has no ledger row (e.g. volume GC never reconciled because `VOLUME_GC_ENABLED=false`) still shows up as **Untracked** and is deletable. Each row shows the work item (`#iid` + title, linked when the project is managed), the sibling volumes present (`workspace`, `podman` for the nested-Podman store, `xdg`), created/last-used times, and a GC-state badge: **Purges in ~Xd** / **Purge eligible** (TTL-based, computed from `last_run_at` + `VOLUME_GC_TTL_MS`), **Missing** (the ledger volume is gone from podman), **Untracked** (podman-only), and **Unmapped** (a ledger orphan with no resolvable work item). GC state is derived from the ledger and the live runtime TTL only, so loading the page makes **no per-row forge call**; the sweep's liveness pass (closed/deleted work items purged before TTL) is shown as a note, not evaluated per row.
161
+
162
+ Deleting a volume force-stops any in-flight phase container, removes the workspace volume and its `-podman`/`-xdg` siblings, and drops the ledger row — the same teardown as `/reset`. When the ledger row maps to a work item that is still **open**, a `🧹 Volume … removed from the WebUI by <admin>. Mention @devbot to resume.` note is posted to it; closed items, deleted items (forge 404), and untracked volumes are removed silently. The work branch (`wi-<iid>`) and any open merge request are untouched, and the stored `/base` choice is **not** cleared (still `/reset` semantics). Deleting a volume mid-run behaves like `/reset`: the killed run posts its own error reply. The page degrades to a ledger-only list with a warning when `podman volume ls` fails.
163
+
164
+ The listing is served by `GET /api/volumes?state=&projectId=&q=&limit=&offset=` (session-gated, `{ volumes, total, gcEnabled, ttlMs, podmanAvailable, filterOptions, projectPaths }`); deletion is `DELETE /api/volumes/:workItemId`.
165
+
166
+ ## Installation
167
+
168
+ ```bash
169
+ # Configure the scoped registry once
170
+ npm config set @wascartech:registry https://git.wascar.tech/api/v4/projects/37/packages/npm/
171
+
172
+ # Install globally
173
+ npm install -g @wascartech/devbot
174
+
175
+ # Run
176
+ devbot
177
+ ```
178
+
179
+ ### Dev channel
180
+
181
+ Every push to `dev` publishes a prerelease under the `dev` dist-tag, so
182
+ `latest` keeps pointing at the last stable release. Install the newest dev
183
+ build with:
184
+
185
+ ```bash
186
+ npm install -g @wascartech/devbot@dev
187
+ ```
188
+
189
+ Dev versions are stamped `2.1.0-dev.<UTC timestamp>.<short sha>` (the base
190
+ version tracks the upcoming release line).
191
+
192
+ For private projects, authenticate with a GitLab deploy token or personal access token in `~/.npmrc`:
193
+ ```
194
+ //git.wascar.tech/api/v4/projects/37/packages/npm/:_authToken=<your-token>
195
+ ```
196
+
197
+ ## Builder image
198
+
199
+ The builder image is based on Fedora 44 and bundles:
200
+
201
+ - **Node.js** 24 (Fedora `nodejs24` package)
202
+ - **PHP** 8.5 + composer (extensions: mbstring, xml, curl, gd, pdo, pdo_mysql, zip, pgsql, pdo_pgsql, redis)
203
+ - **Python** 3.14
204
+ - **Chromium**: headless browser for the bundled `chrome-devtools-mcp` MCP server (browser automation, performance tracing, screenshots). Available on both amd64 and arm64.
205
+ - **chrome-devtools-mcp**: globally installed npm package providing Chrome DevTools tools to agents via MCP
206
+ - **Android SDK**: API 34 + build-tools 34.0.0, platform-tools (no NDK). **amd64 only** — Google ships no native arm64 Linux Android SDK, so the arm64 variant of the image omits it.
207
+ - **JDK**: Temurin (Adoptium) OpenJDK 17 (default `JAVA_HOME`)
208
+ - **OpenCode** (latest)
209
+
210
+ Published to the GitLab Container Registry on every push to `main` (`:main` tag) and on version tags (`:vX.Y.Z` + `:latest`) as a **multi-arch manifest** (amd64 + arm64):
211
+ ```bash
212
+ podman pull registry.wascar.tech/opensource/devbot:main
213
+ ```
214
+
215
+ For a custom image, clone this repo and build from `docker/Dockerfile.builder` (native arch, or `--platform linux/amd64,linux/arm64` for a multi-arch manifest):
216
+ ```bash
217
+ podman build -t devbot-builder -f docker/Dockerfile.builder .
218
+ ```
219
+
220
+ The image used for a run is resolved per project: **project setting → global setting → `BUILDER_IMAGE` env var**. Set a global builder image in **Global settings → Runtime → Builder image**, and override it per project in **Project settings → Runtime → Builder image** (a per-project value applies to every container spawned for that project: clone/sync, the agent run, peer sync, push, and the nested-podman store init). An empty field inherits the next level, so unset deployments keep using `BUILDER_IMAGE`. A custom image must bundle the same toolchain the default does (`git`, `sh`, `opencode`).
221
+
222
+ To run the DevBot process itself in a container, `npm run build` then build `docker/Dockerfile.devbot`:
223
+ ```bash
224
+ podman build -t devbot -f docker/Dockerfile.devbot .
225
+ ```
226
+
227
+ ## Setup
228
+
229
+ ### 1. Configure environment
230
+
231
+ ```bash
232
+ cp .env.example .env
233
+ # edit .env with your GitLab URL, token, bot user ID, model, API keys
234
+ ```
235
+
236
+ ### 2. Install and run
237
+
238
+ ```bash
239
+ npm install
240
+ npm run build
241
+ npm start
242
+ ```
243
+
244
+ Dev mode with auto-reload:
245
+
246
+ ```bash
247
+ npm run dev
248
+ ```
249
+
250
+ ### 4. Configure the forge webhook
251
+
252
+ DevBot configures webhooks per project via a web onboarding flow — no manual webhook setup. See [docs/gitlab-setup.md](./docs/gitlab-setup.md) (GitLab) and [docs/platforms.md](./docs/platforms.md) (ForgeJo/Gitea/GitHub). In short:
253
+
254
+ 1. Create an OAuth app with redirect URI `${PUBLIC_URL}/oauth/callback`. GitLab: User Settings → Applications (scope `api`). ForgeJo/Gitea: an app at `/admin/applications`. GitHub: Settings → Developer settings → OAuth Apps (the `repo` scope is requested).
255
+ 2. Start DevBot, visit `${PUBLIC_URL}/onboarding`, and sign in with an account whose username is listed in `ONBOARDING_USERS` (comma-separated, case-insensitive, **fails closed** when empty). No forge admin status is required.
256
+ 3. Select the projects to onboard. DevBot adds the bot, creates/updates the webhook with a per-project HMAC signing token, and stores the token locally in `devbot.db`.
257
+ 4. Mention the bot in a work item: `@devbot /build <prompt>` for build mode, `@devbot <prompt>` for plan mode.
258
+
259
+ To revoke the bot from a project later, visit `${PUBLIC_URL}/onboarding/managed` (the **Projects** page lists the onboarded projects with a path search; `GET /api/managed?q=&limit=&offset=`). If you rename or transfer an onboarded project on the forge, DevBot reconciles the stored path automatically (on the managed-list load plus a startup/daily sweep), so the displayed name, work-item links, MCP path selectors and clone URLs stay correct.
260
+
261
+ ## MCP servers
262
+
263
+ DevBot lets an admin attach external MCP servers (Sentry, Context7, Grep, custom local commands, etc.) to each project; OpenCode connects to them during runs. The bot ships two internal, always-active MCP servers that are never user-configurable: `devbot-forge` (forge tools — issues, merge/pull requests, work items, repository files/commits/branches/tags, CI/CD runs/jobs, labels, milestones, members, releases, wikis, scoped search, plus `download_work_item_attachment` (pull a work-item attachment into the workspace) and `upload_work_item_attachment` for sending files back) and `chrome-devtools` (headless Chrome DevTools automation via `chrome-devtools-mcp`). The browser only launches when an agent actually invokes a browser tool, so the idle cost is just a Node stdio process. By default `devbot-forge` is scoped to the run's own project; an admin can grant a project access to another enrolled project from the project row's **Access** button, letting runs read a sibling project and clone its code into a git-excluded `.devbot-peers/` folder. See [docs/mcp-config.md](./docs/mcp-config.md) for the server shapes, the web-panel workflow, the OAuth token paste flow, the REST API, and the storage model, and [docs/project-access.md](./docs/project-access.md) for cross-project grants.
264
+
265
+ ### Reading attachments and sending files back
266
+
267
+ Work-item attachments are not pre-read; their Markdown links appear inline in the prompt. To inspect one, the agent calls `download_work_item_attachment` with the link exactly as it appears; the tool fetches the whole file into `.devbot-out/incoming/` (git-excluded) and returns its local path. Image/PDF OCR is available on the downloaded file (`tesseract`, `pdftotext`, `pdftoppm` are installed in the builder image).
268
+
269
+ The agent can share screenshots, test logs, or generated artifacts with `upload_work_item_attachment`: it uploads a file from the run workspace and posts it as a work-item comment (with an optional Markdown caption). Agent scratch output belongs in `.devbot-out/` at the repo root — git-excluded like `.devbot-peers/`, so it can never be committed. Files must live under `/workspace` (the tool rejects traversal and symlink escapes). Upload is native on GitLab (`POST /projects/:id/uploads`) and ForgeJo/Gitea (`POST /repos/:o/:r/issues/:n/assets`); GitHub has no public REST API to attach a file, so the tool reports "not supported on this platform" there.
270
+
271
+ ## Outbound webhooks
272
+
273
+ Besides receiving forge webhooks, DevBot can **send** signed JSON events to URLs an admin configures — a CI runner, chat bridge, or deployment hook that should react when a run finishes. Configure endpoints per project (**project → Webhooks** tab) or bot-wide (**Settings → Webhooks** tab); a global endpoint fires for every project. Events: `run.started`, `run.succeeded`, `run.failed`, `run.cancelled`, `mr.created`, `mr.updated`.
274
+
275
+ Deliveries are POSTed with `X-DevBot-Event`, a unique `X-DevBot-Delivery` id, and `X-DevBot-Signature-256: sha256=HMAC-SHA256(body)` keyed by a per-endpoint secret (revealable/rotatable in the panel). They are persisted and retried with a linear backoff (never blocking a run), with a per-endpoint delivery log and manual replay. Custom static headers are supported for receivers that don't implement HMAC. Loopback/private targets are **blocked by default** (`WEBHOOK_ALLOW_PRIVATE_HOSTS=true` to allow internal endpoints). See [docs/outbound-webhooks.md](./docs/outbound-webhooks.md).
276
+
277
+ ## Dev containers
278
+
279
+ When `DEVCONTAINER_ENABLED=true`, **build-mode** runs get a private rootless
280
+ Podman runtime *inside* the builder container, so an agent can `podman build`,
281
+ run service images, and create private networks for integration tests. Each work
282
+ item has its own runtime store (`devbot-wi-<id>-podman`), so task A cannot see or
283
+ affect task B's containers, images, or networks. Plan mode never gets a runtime,
284
+ and the feature is off by default.
285
+
286
+ - The runtime is **nested**, not shared: no host Podman socket is mounted.
287
+ - The store lives in a separate volume, never `/workspace`, so container
288
+ artifacts cannot enter the git diff or be committed.
289
+ - The store is created on the first dev-container run and removed with the work
290
+ item (`/reset`, work-item close, or the volume GC), exactly like the workspace
291
+ volume.
292
+ - **Enabling this broadens the builder container's privileges** (nested user
293
+ namespaces + mount + mknod) versus the default unprivileged launch; hence the
294
+ off-by-default global toggle.
295
+
296
+ See [docs/dev-containers.md](./docs/dev-containers.md) for the workflow, the
297
+ storage-driver choice, the isolation guarantees, and the host smoke-test
298
+ runbook.
299
+
300
+ ## Git submodules
301
+
302
+ When `GIT_SUBMODULES_ENABLED=true` (default), DevBot initializes and updates
303
+ submodules on **first-run clone** and on **every subsequent fetch** by running
304
+ `git submodule update --init --recursive`. This keeps the agent's view of the
305
+ repo complete and picks up new submodules or updated gitlinks between runs.
306
+
307
+ - **Best-effort.** A submodule that cannot be fetched does **not** abort the run;
308
+ the affected paths are listed in an informational `ℹ️ Some git submodules could
309
+ not be fetched: …` note and the run continues without them. This makes the
310
+ default safe for repos with a private/inaccessible submodule.
311
+ - **Token scoping.** The bot token is attached only to the configured
312
+ `PLATFORM_URL` host (`http.<PLATFORM_URL>/.extraHeader`), so a submodule on an
313
+ external host (e.g. `github.com`) is fetched anonymously instead of leaking the
314
+ token. Public external submodules work; private external ones don't (no
315
+ per-submodule credentials).
316
+ - **SSH → HTTPS rewrite.** Same-host submodule URLs written as
317
+ `git@<PLATFORM_URL-host>:…` or `ssh://git@<PLATFORM_URL-host>/…` are rewritten
318
+ to `PLATFORM_URL` over HTTPS, so they inherit the scoped token and work
319
+ without an SSH key in the container. If your forge's SSH listens on a
320
+ non-default port (e.g. `ssh://git@git.example.com:2222/…`), set
321
+ `PLATFORM_SSH_PORT=2222` so the port-bearing URL is rewritten too. Submodules
322
+ on other SSH hosts are not rewritten.
323
+ - The submodule step runs after the clone/checkout and before `.devbot/init.sh`,
324
+ so `init.sh` sees populated submodules.
325
+
326
+ Set `GIT_SUBMODULES_ENABLED=false` (or `0`) to skip submodules entirely.
327
+
328
+ ## ENV vars & `.devbot/init.sh`
329
+
330
+ DevBot lets an admin configure per-project environment variables via the web panel. These are injected into every builder container run (so the agent, MCP servers, and `.devbot/init.sh` all see them). A project may include an optional `.devbot/init.sh` script that runs once after the initial `git clone` (e.g. to install deps, run migrations). Non-zero exit aborts the run. See [docs/env-config.md](./docs/env-config.md) for the web-panel workflow, the REST API, the `.devbot/init.sh` contract, and the storage model.
331
+
332
+ Per-project model, model variant, plugin, and base-branch overrides live in the same web panel (open a project row → **Settings** tab) and default to the global values set in **Settings**. See [docs/project-settings.md](./docs/project-settings.md) and [docs/global-settings.md](./docs/global-settings.md).
333
+
334
+ ## Configuration
335
+
336
+ Infrastructure and identity variables only — provider, model, plugin, base-branch, and runtime (timeout/queue/attachment/maintenance/dev-container) config moved to the WebUI ([docs/global-settings.md](./docs/global-settings.md)). The runtime vars below remain as the **fallback** an empty WebUI field inherits.
337
+
338
+ | Variable | Description | Default |
339
+ |---|---|---|
340
+ | `PLATFORM` | Forge backend: `gitlab`, `forgejo`, `gitea`, or `github`. One instance ↔ one forge. | `gitlab` |
341
+ | `PLATFORM_URL` | Forge base URL | — |
342
+ | `PLATFORM_TOKEN` | Bot access token (api scope; Gitea/ForgeJo: write:repository + write:issue; GitHub: repo scope) | — |
343
+ | `PLATFORM_SSH_PORT` | Forge SSH port for the same-host submodule SSH→HTTPS rewrite (set when SSH is not on 22) | `22` |
344
+ | `PUBLIC_URL` | Public base URL of the bot (webhook URL + OAuth redirect) | — |
345
+ | `PLATFORM_OAUTH_CLIENT_ID` | OAuth app client id (for the onboarding flow) | — |
346
+ | `PLATFORM_OAUTH_CLIENT_SECRET` | OAuth app secret | — |
347
+ | `ONBOARDING_USERS` | Comma-separated forge usernames allowed to sign in to the WebUI onboarding flow (all platforms). **Fails closed** when empty. | `""` |
348
+ | `GITHUB_API_URL` | GitHub only: API base (defaults to github.com) | `https://api.github.com` |
349
+ | `SESSION_KEY` | 32+ char key for encrypting the session cookie (AES-GCM) | — |
350
+ | `BOT_USERNAME` | Bot account username (for mention regex) | — |
351
+ | `BOT_USER_ID` | Bot account numeric ID (for self-loop guard + project invite) | — |
352
+ | `BOT_GIT_NAME` | Git commit author name | `DevBot` |
353
+ | `BOT_GIT_EMAIL` | Git commit author email (use the bot's verified email for profile linking) | — |
354
+ | `BUILDER_IMAGE` | Podman builder image tag (fallback when unset in the WebUI; global + per-project overrides live in settings) | `devbot-builder:latest` |
355
+ | `PLAN_TIMEOUT_MS` | Per-run timeout for plan mode (ms) (WebUI Runtime override) | `600000` |
356
+ | `BUILD_TIMEOUT_MS` | Per-run timeout for build mode (ms) (WebUI Runtime override) | `1800000` |
357
+ | `RUN_STALL_TIMEOUT_MS` | Kill an agent run that produces no output for this long (ms); bounds a wedged or looping agent before the total phase timeout. 0 disables (env-only) | `900000` |
358
+ | `MAX_QUEUE_PER_WORK_ITEM` | Max pending runs per work item (overflow dropped) (WebUI Runtime override) | `3` |
359
+ | `MAX_CONCURRENT_RUNS` | Max simultaneous podman builder runs across all work items (WebUI Runtime override) | `4` |
360
+ | `VOLUME_GC_TTL_MS` | Inactivity period after which a per-work-item volume is auto-purged (WebUI Runtime override) | `604800000` (1 week) |
361
+ | `VOLUME_GC_INTERVAL_MS` | How often the GC sweep runs (WebUI Runtime override; live re-arm) | `86400000` (daily) |
362
+ | `VOLUME_GC_ENABLED` | Enable the volume GC (startup + periodic sweep) (WebUI Runtime override) | `true` |
363
+ | `ERROR_LOG_RETENTION_DAYS` | Days to keep persisted container/run error logs (startup + daily purge; 0 = keep forever) (WebUI Runtime override) | `30` |
364
+ | `RUN_LOG_TTL_HOURS` | Hours to keep per-run log files (startup + hourly purge; 0 = keep forever). Governs files only — history rows persist (WebUI Runtime override) | `48` |
365
+ | `RUN_LOG_DIR` | Directory for the per-run log files (relative to the process cwd unless absolute). Mount a volume here in a container deployment (env-only) | `run-logs` |
366
+ | `PROJECT_SYNC_ENABLED` | Reconcile enrolled project paths from the forge after a rename/transfer (startup + periodic sweep; WebUI list always refreshes) (WebUI Runtime override) | `true` |
367
+ | `PROJECT_SYNC_INTERVAL_MS` | How often the project-path sweep runs (WebUI Runtime override; live re-arm) | `86400000` (daily) |
368
+ | `MAX_GLOBAL_QUEUE` | Max pending tasks in the global FIFO (0 = unbounded; overflow dropped with ⚠️) (WebUI Runtime override) | `50` |
369
+ | `QUEUE_POLL_INTERVAL_MS` | Worker poll cadence (ms) for the DB-backed queue | `250` |
370
+ | `MAX_RETRIES` | App-level retry on transient failures (podman spawn, git network). 0 = disabled (WebUI Runtime override) | `2` |
371
+ | `RETRY_BACKOFF_MS` | Linear backoff base for retry: `RETRY_BACKOFF_MS * attempts` (WebUI Runtime override) | `5000` |
372
+ | `QUEUE_FEEDBACK` | Post `⏳`/`⚠️`/`📨`/`▶️`/`🔁` queue notes to the work item (WebUI Runtime override) | `true` |
373
+ | `PORT` | HTTP listen port | `3000` |
374
+ | `DRY_RUN` | If true, skip podman/opencode; log prompt only | `0` |
375
+ | `ATTACHMENT_MAX_BYTES` | Attachment size cap for agent downloads and outbound uploads (larger files are rejected) (WebUI Runtime override) | `10000000` |
376
+ | `DEVCONTAINER_ENABLED` | Enable nested rootless Podman for build-mode runs (private runtime per work item). Broadens the builder container's privileges — see [Dev containers](#dev-containers). (WebUI Runtime override) | `false` |
377
+ | `DEVCONTAINER_STORAGE_DRIVER` | Inner runtime storage driver: `auto` (probe `/dev/fuse` → `fuse-overlayfs`, else `vfs`), `fuse-overlayfs`, or `vfs`. (WebUI Runtime override) | `auto` |
378
+ | `GIT_SUBMODULES_ENABLED` | On first-run clone and every fetch, run `git submodule update --init --recursive` (best-effort: failures are named in an `ℹ️` note and don't abort the run). Bot token is scoped to `PLATFORM_URL`; same-host `git@host:`/`ssh://git@host/` submodule URLs are rewritten to HTTPS (see `PLATFORM_SSH_PORT`). (WebUI Runtime override) | `true` |
379
+ | `WEBHOOK_ALLOW_PRIVATE_HOSTS` | Allow outbound webhook endpoints on loopback/private/CGNAT/ULA addresses (blocked by default, SSRF hardening). See [Outbound webhooks](#outbound-webhooks). | `false` |
380
+ | `WEBHOOK_DELIVERY_TIMEOUT_MS` | Per-attempt HTTP timeout for an outbound webhook delivery | `10000` |
381
+ | `WEBHOOK_RETRY_MAX` | Total attempts per delivery before it is marked terminally failed | `5` |
382
+ | `WEBHOOK_RETRY_BACKOFF_MS` | Linear backoff base for retries (`base * attempts`) | `5000` |
383
+ | `WEBHOOK_POLL_INTERVAL_MS` | Outbound delivery worker poll cadence | `1000` |
384
+ | `WEBHOOK_RETENTION_DAYS` | Days to keep outbound delivery rows (startup + daily purge; 0 = keep forever) | `30` |
385
+
386
+ > **Migration (global settings)**: `LLM_MODEL`, `LLM_API_KEY`, `LLM_BASE_URL`, `PLAN_LLM_MODEL`, `BUILD_LLM_MODEL`, `OPENCODE_PLUGINS`, `BASE_BRANCHES`, `ATTACHMENT_OCR_LANG`, and `GITLAB_BOT_URL` are removed from the schema and ignored — there is no auto-import. Configure everything in **Settings**.
387
+ >
388
+ > **Runtime settings**: timeouts, queue caps/retries, attachment limits, volume GC, project sync, dev containers, and git submodules are now editable in **Settings → Runtime**. Their env vars remain as the fallback: an empty WebUI field inherits the env value (shown as the placeholder), which itself falls back to the built-in default. `DRY_RUN` and `LOG_LEVEL` stay env-only.
389
+
390
+ > **Migration (from `MR_TARGET_BRANCH`)**: the former single-value `MR_TARGET_BRANCH` env var is removed. Set the base-branch list in the WebUI global settings (default `dev,master`, top entry tried first).
391
+
392
+ ## Safety
393
+
394
+ - **Self-loop guard**: notes authored by the bot never re-trigger.
395
+ - **Trigger authorization**: a mention triggers the bot only if it passes the configured trigger policy. The baseline has two gates, each a global default with a per-project override: **require author/assignee** (default on) — the mentioning user must be the work item's author or assignee; and **minimum project role** (default Developer, i.e. access_level ≥ 30). The denial note names the resolved policy. Mention failures to resolve the actor's role or the work item fail closed (deny + note). See [docs/global-settings.md](./docs/global-settings.md#trigger-policy) and [docs/project-settings.md](./docs/project-settings.md#permissions).
396
+ - **Per-project access rules** (optional): an admin can refine the role gate per project by adding a rule for a **role** (normalized permission: none/read/write/admin/owner) or a specific **user** (resolved by username to a stable numeric id). Each rule sets a level — **Full** (plan + build), **Plan only**, or **Denied**. A rule replaces the role gate for the matched subject (it can broaden or narrow), but cannot bypass the author/assignee gate. An exact user rule beats a role rule. Denied users are ignored silently (204, no reply, no run); plan-only users are ignored on build/force-push/push/mr mentions. Rules are read live, so changes take effect immediately.
397
+ - **Access mode + per-project whitelist**: the baseline authorization above is always enforced. The global **Access mode** switch (Global settings) selects **Role based** (baseline only) or **Whitelist mode**; each project can override that mode from its own Settings panel. In whitelist mode an additional per-project list (forge usernames, resolved to stable numeric ids) must contain the mentioning user. A user who passes the baseline rule but is not listed is **silently ignored** (204, no reply, no run); a user who fails the baseline rule is still denied with the usual `🚫` note — the whitelist narrows access, never grants it. Entries are read live, so adding/removing one takes effect immediately. See [docs/project-settings.md](./docs/project-settings.md#access-mode--whitelist).
398
+ - **Per-work-item FIFO queue**: concurrent webhooks for the same work item are queued (max `MAX_QUEUE_PER_WORK_ITEM` pending). The queue is DB-backed (`pending_tasks` table); a restart resets orphaned running rows to queued and replays them. Position > 0 gets a `⏳ Queued (position N)` note; overflow gets `⚠️ Queue full, retry later.`. Different work items run in parallel up to `MAX_CONCURRENT_RUNS` simultaneous runs; beyond that, tasks wait in a global FIFO (also DB-backed). The thread is notified at three milestones: `⏳ Queued globally (position N).` at enqueue, `⏳ You're next.` when it reaches the head of the queue, and one wait-aware start note. The start note is posted whenever the task waited in its own work-item queue or for a global slot, measures the wait from the row's `enqueued_at`, and names how many tasks are waiting plus which one is next. Prompt excerpts are rendered as inline code so user text cannot mention users or break the note layout. Order is preserved so each mention resumes the same OpenCode session and sees prior context. A global overflow cap (`MAX_GLOBAL_QUEUE`) drops excess with `⚠️ Global queue full, retry later.`
399
+ - **App-level retry**: transient failures (podman spawn, git clone/fetch network blip) are retried up to `MAX_RETRIES` times with linear backoff (`RETRY_BACKOFF_MS * attempts`). The same row is requeued (preserving its `seq` so FIFO order is unchanged). Terminal failures (timeout, branch divergence, agent errors) are not retried. A `🔁 Retrying (attempt N/M).` note is posted on each retry; `⚠️ Gave up after M attempts.` on exhaustion.
400
+ - **Run timeout**: runaway agents are killed after `PLAN_TIMEOUT_MS` (plan mode) or `BUILD_TIMEOUT_MS` (build mode). The podman container is `rm -f`'d on timeout, not just the client process. A run that goes completely silent for `RUN_STALL_TIMEOUT_MS` is killed earlier with a distinct "stalled" note (env-only; `0` disables) — this bounds a wedged opencode process or a model looping on a sandbox-blocked tool call.
401
+ - **In-flight stop**: `/stop` (work-item scope) or the WebUI row **Stop** button (task scope) sets a durable `cancel_requested` flag on the queued/running row and `rm -f`s the work item's phase containers. The runner checks the flag at each phase boundary (post-attachment, post-clone/sync, post-run, pre-push, pre-MR) and unwinds as a clean stop: no `⚠️` error reply, no error-log row, and no push/MR. Authorization is the same as any mention (Developer+ AND author/assignee); a stop works even when the bot is unconfigured. The flag is durable so a crash mid-stop does not replay the task on restart (`purgeCancelled` runs before replay). The per-work-item volume is untouched — use `/reset` to discard it.
402
+ - **No silent rebase loss**: if `git pull --rebase` fails on a diverged branch, the run aborts with a note rather than discarding local commits.
403
+ - **Force push is opt-in and leased**: `/force-push` overwrites the `wi-<iid>` remote branch using `--force-with-lease` (never plain `--force`). The lease refuses the push if the remote moved since the run's fetch, so a concurrent/upstream change is not clobbered. It only ever touches the work item branch — the MR target branch is untouched. With `/build` the host-side HEAD guard and fallback-commit rescue still apply. Standalone (no `/build`), `/force-push` runs no agent: it just fetches and force-pushes the existing local `wi-<iid>` branch (a repair action after a normal push was refused); if the workspace or the local branch doesn't exist it posts an actionable note and pushes nothing.
404
+ - **Branch naming is fail-closed**: `/branch <name>` names the work branch on the first naming only, after verifying the name does not already exist on the remote. A collision, an unsafe name, or a forge lookup failure rejects the mention and runs nothing. The chosen name is stored per work item and reused on every later run (including after `/reset` and close); it is never auto-renamed.
405
+ - **Rewrite-aware publish**: the post-build host-side push detects a rewritten branch history (e.g. after a rebase or a conflict auto-fix) and, when the remote still equals the run's **starting** tip, publishes with a SHA-pinned `--force-with-lease` instead of failing with git's raw fast-forward hint. If the remote moved during the run, the push is refused with a clean "re-run" note — it never clobbers commits it did not produce. The fallback-commit rescue also refuses to commit a working tree that still contains conflict markers.
406
+ - **Conflict auto-fix is opt-in and fails closed**: when enabled, DevBot checks the branch against its MR target and delegates resolution to a direct build run (rebase-first, merge fallback; the resolver never pushes). It only opens/updates the MR after a clean re-check; an unresolved conflict leaves the branch pushed but opens no MR and posts the conflicting paths. With the switch off there is no check and no behavior change.
407
+ - **Per-run auth**: the provider `auth.json`, the per-run `opencode.json` (carrying the scoped MCP bearer token), and pasted `mcp-auth.json` are written at mode 0600 into the per-work-item **xdg volume** (`devbot-wi-<id>-xdg`, mounted at `/home/devbot/.xdg`) — outside the cloned repository, so the agent's `git add -A` can never commit them. No keys are baked into the image.
408
+
409
+ ## Known ceilings (documented, not built)
410
+
411
+ - **In-process idempotency**: a restart mid-run + a webhook retry can double-run. The dedup table uses `INSERT OR IGNORE` for atomicity, but it's still single-host (no shared store). Acceptable at this scale; upgrade path is a shared Redis SETNX if HA lands.
412
+ - **Volumes auto-purged after inactivity or on close/deletion**: per-work-item volumes (`devbot-wi-<id>` and its `-podman` / `-xdg` siblings, tracked as one ledger row) are deleted after `VOLUME_GC_TTL_MS` (default 1 week) of no run completing, even if the work item stays open. The sweep additionally runs a **liveness pass**: a volume whose work item is closed or deleted (forge 404) is purged immediately rather than waiting out the TTL — a deleted item can never run again, and a closed one matches the close-hook behavior. A `🧹 Volume purged …` note is posted to the work item on purge (skipped for deleted items and for un-mappable orphans, which have no thread); mention `@devbot` to resume (the next run re-clones into a fresh volume). Volumes created before the tracking table existed are reconciled at startup, stamped with their **real** `podman` creation time (so old orphans are TTL-eligible at once), and purged silently. The sweep runs on startup and every `VOLUME_GC_INTERVAL_MS` (default daily); set `VOLUME_GC_ENABLED=false` to disable. Close and `/reset` still remove the volumes immediately and clean the ledger row.
413
+ - **Attachment content is agent-handled**: DevBot does not extract or OCR attachments host-side. The agent must download each one it needs with `download_work_item_attachment`; a file the agent never downloads is never read. Content is treated as untrusted data.
414
+ - **No attachment cache across runs**: a downloaded attachment lives in the disposable workspace volume and is re-downloaded after a close/reopen cycle; the tool does not dedupe across runs. Within a run, a repeat download of the same reference is suffixed (`-1`, `-2`) rather than returned from cache.
415
+ - **GitHub attachment upload unsupported**: GitHub has no public REST API to attach a file to an issue or comment, so `upload_work_item_attachment` reports "not supported on this platform" on `PLATFORM=github`. GitLab and ForgeJo/Gitea upload natively.
416
+ - **Private external submodules unsupported**: submodules are fetched anonymously on external hosts (the bot token is scoped to `PLATFORM_URL` only). A private submodule on another host needs a per-project credential store, not built. Same-host submodules (HTTPS, relative, or SSH-to-HTTPS-rewritten) work with the bot token.
417
+ - **Submodule state not updated remotely**: `git submodule update` (no `--remote`) checks out the gitlink commit recorded by the superproject, matching git's default. Advancing submodules to their remote branch tip is not done.
418
+ - **Stop mid-clone can leave a partial volume**: stopping during the first-run clone or `.devbot/init.sh` kills the container mid-setup, so the per-work-item volume can be partially initialized. The next mention re-syncs (and re-runs `init.sh` only on a detected empty workspace), but if the volume is wedged use `/reset` to discard and rebuild it. Stop never removes the volume itself.
419
+ - **ForgeJo/Gitea webhook secret is create-only**: both forges honor `config.secret` when the hook is created but silently ignore it on `PATCH` (`editHook`), and the API never returns the stored secret. DevBot sends the secret in the create payload, so fresh onboarding works; an already-onboarded hook with an empty secret must be fixed in the forge UI (paste `project_tokens.signing_token`) or by revoke + re-onboard. See [docs/platforms.md](./docs/platforms.md).
420
+
421
+ ## Development
422
+
423
+ ```bash
424
+ npm run typecheck # strict tsc --noEmit
425
+ npm run build # tsc → dist/ + tailwindcss → public/app.css
426
+ npm run test:signature # HMAC webhook-signature self-check
427
+ npm run test:mention # mention parser self-check (multi-agent, error cases)
428
+ npm run test:global-queue # global concurrency gate self-check
429
+ npm run test:queue # DB-backed per-work-item queue self-check
430
+ npm run test:queue-store # queue store (claim/requeue/markDone) self-check
431
+ npm run test:idempotency # webhook-id dedup self-check
432
+ npm run test:migrate # migration runner self-check
433
+ npm run test:dispatch # dispatch integration self-check (retry, overflow, replay)
434
+ npm run test:mr-content # agent-authored MR block parser self-check
435
+ npm run test:attachments # attachment discovery + download self-check
436
+ npm run test:base-branch # /base branch store self-check
437
+ npm run test:work-item-branch # /branch work-branch store self-check
438
+ npm run test:branch-naming # /branch Step-0 gate self-check
439
+ npm run test:volume-manage # WebUI volume list + delete self-check
440
+ npm run test:global-settings # global settings + provider store self-check
441
+ npm run test:runtime-settings # ENV -> WebUI runtime resolver + live apply self-check
442
+ npm run test:provider-registry # models.dev normalization self-check
443
+ npm run test:mcp-download-tool # download_work_item_attachment handler self-check
444
+ npm run test:config-generation # auth.json + provider block self-check
445
+ npm run test:ui-e2e # browser E2E for the whole WebUI (Chrome DevTools MCP; skips if no browser)
446
+ npm run test:path-sandbox # path policy + guard plugin parity self-check
447
+ npm run test:shell-sandbox # managed config + bwrap shell wrapper self-check
448
+ npm run test:podman-volume # real-Podman volume + readFileFromVolume deny/symlink self-check
449
+ npm run test:podman-run # real-Podman runPodmanCapture + logs self-check
450
+ npm run test:nested-podman # nested rootless Podman smoke (nightly; needs builder image)
451
+ npm run test:devcontainer # nested-podman flag/driver builder self-check
452
+ npm run test:devcontainer-wiring # store volume lifecycle self-check
453
+ npm run test:platform-webhooks # platform webhook parse + signature self-check
454
+ npm run test:platform-drivers # Gitea/ForgeJo driver contract self-check
455
+ npm run test:ci-adapter # GitLab + Actions CI normalization self-check
456
+ npm run test:github-webhooks # GitHub webhook signature + event parsing self-check
457
+ npm run test:github-driver # GitHub driver contract + ZIP log self-check
458
+ npm run test:onboarding-allowlist # ONBOARDING_USERS gate self-check (fail closed)
459
+ npm run test:outbound-webhooks # outbound endpoint store + delivery worker + signing/SSRF self-check
460
+ npm test # (none — see acceptance checks below)
461
+ npm run test:custom-repos # custom skills/agents repo store + sync self-check
462
+ ```
463
+
464
+ ### Browser E2E (`test:ui-e2e`)
465
+
466
+ `scripts/check-ui-e2e.ts` drives the built WebUI through the bundled
467
+ `chrome-devtools-mcp` server (the same one runs use) against a real server +
468
+ temp SQLite DB and a mock GitLab. It covers the sign-in gate, managed list,
469
+ Settings (registry + custom providers, models), ENV, MCP,
470
+ per-project Settings, and Remove project — asserting both rendered DOM and
471
+ server persistence.
472
+
473
+ It **skips** (exit 0) when `chrome-devtools-mcp` or a Chromium binary is absent,
474
+ so `npm test` stays green without a browser. Override with
475
+ `CHROME_DEVTOOLS_MCP_BIN`, `CHROMIUM_BIN`, or force-skip with
476
+ `DEVBOT_E2E_SKIP=1`. The harness starts the server with
477
+ `DEVBOT_TEST_AUTH=1` to sign a session without GitLab OAuth — that flag
478
+ must never be set in production. It always rebuilds `public/` first, so a
479
+ stale SPA build from an earlier run cannot silently serve old code.
480
+
481
+ ### Test profiles (`DEVBOT_TEST_PROFILE`)
482
+
483
+ `scripts/run-all-tests.mjs` auto-discovers every `test:*` script. CI splits them
484
+ by runtime requirement:
485
+
486
+ | Profile | Suites | Where |
487
+ | --- | --- | --- |
488
+ | `hermetic` (default for the `test` job) | everything except the tooling suites | slim `node:22-slim` |
489
+ | `integration` | `test:podman-*`, `test:ui-e2e`, `test:shell-sandbox` | privileged runner (`test:integration`) |
490
+ | `nested` | `test:nested-podman` | nightly (`test:nested-podman`) |
491
+ | `all` | every suite (default with no env var) | local `npm test` |
492
+
493
+ The integration suites self-skip when Podman/Chromium/bubblewrap is absent, but
494
+ the CI integration and nightly jobs set `DEVBOT_REQUIRE_PODMAN=1`, which turns a
495
+ failed Podman probe (or a non-working nested run) into a hard failure with the
496
+ captured reason rather than a silent skip — so missing coverage cannot hide. On
497
+ a dev host without the tooling the suites skip gracefully.
498
+
499
+ For a manual interactive pass, `node --experimental-sqlite --import tsx
500
+ scripts/e2e-serve.ts` seeds a temp DB and serves the built app with the same
501
+ test-login route, printing the URL to open in a browser.
502
+
503
+ ### Acceptance checks (DRY_RUN)
504
+
505
+ ```bash
506
+ DRY_RUN=1 npm start
507
+ # missing token → 401
508
+ # wrong token → 401
509
+ # bot-authored note → 204 (self-loop guard)
510
+ # non-issue note → 204
511
+ # no @bot mention → 204
512
+ # valid note hook → 202 + prompt logged
513
+ # valid work item hook (open + mention in description) → 202 + prompt logged
514
+ ```
515
+
516
+ ### Skills & agents
517
+
518
+ Custom skills and subagents come from git repositories an admin registers in **Settings → Skills & Agents** (globally, with optional per-project overrides). DevBot shallow-clones each repo once per work item and fetches thereafter; skill dirs are added to OpenCode's `skills.paths`, and subagent markdown is flattened into the config dir OpenCode scans. See [docs/custom-repos.md](./docs/custom-repos.md).
519
+
520
+ ## Layout
521
+
522
+ ```
523
+ src/
524
+ config.ts env validation (zod)
525
+ logger.ts tiny console logger
526
+ migrate.ts versioned migration runner (schema_migrations + migrations/*.sql)
527
+ platform/ forge abstraction: Platform port + drivers
528
+ types.ts neutral domain types + NormalizedWebhookEvent + errors + attachment types
529
+ port.ts aggregate Platform interface (capability sub-interfaces) + CAPABILITY_METHODS
530
+ index.ts getPlatform(): driver selected by PLATFORM
531
+ git-auth.ts Basic-auth git extraHeader helper
532
+ gitlab/ GitLab driver (all GitLab code lives here)
533
+ http.ts schema-validated fetch, token auth, pagination, raw bytes
534
+ schemas.ts GitLab wire schemas (API + webhooks)
535
+ schemas-read.ts GitLab read-endpoint schemas
536
+ api.ts typed GitLab REST client (notes, MRs, members, …)
537
+ read.ts read-only cross-project resources (MCP)
538
+ admin.ts OAuth-token REST calls (onboarding)
539
+ oauth.ts OAuth authorize/token/user helpers
540
+ signature.ts Standard Webhooks HMAC verification
541
+ attachments.ts GitLab upload discovery + fetch
542
+ index.ts capability assembly
543
+ gitealike/ shared Gitea/ForgeJo /api/v1 driver
544
+ http.ts schema-validated fetch, token auth, pagination, raw bytes
545
+ schemas.ts Gitea/ForgeJo wire schemas
546
+ base.ts capability implementations
547
+ forgejo.ts ForgeJo overrides (X-Forgejo-* headers, OAuth paths)
548
+ gitea.ts Gitea overrides (X-Gitea-* headers)
549
+ attachments.ts Gitea/ForgeJo attachment discovery + fetch
550
+ github/ GitHub (github.com) driver
551
+ http.ts schema-validated fetch, bearer auth, pagination, raw bytes
552
+ schemas.ts GitHub REST wire schemas
553
+ zip.ts minimal ZIP reader for Actions log archives
554
+ attachments.ts GitHub attachment discovery + fetch
555
+ index.ts capability implementations
556
+ onboarding-allowlist.ts ONBOARDING_USERS gate for all drivers (fails closed)
557
+ queue.ts per-issue DB-backed FIFO queue (worker-per-key, claim/markDone/retry)
558
+ global-queue.ts global concurrency gate (DB backlog + in-memory active counter)
559
+ queue-store.ts SQLite store for pending_tasks (claim/requeue/markDone/rank/housekeeping)
560
+ mention.ts mention parser + subagent resolver (pure, testable)
561
+ mr-content.ts agent-authored MR/PR block parser (pure, testable)
562
+ types/ zod schemas: env, opencode, outbound-webhook, mcp-config, access
563
+ cookie.ts AES-GCM session cookie encrypt/decrypt
564
+ token-store.ts SQLite store for per-project whsec_ signing tokens (node:sqlite)
565
+ access-grant-store.ts SQLite store for directed cross-project MCP access grants
566
+ mcp-auth-store.ts SQLite store for per-project/global MCP OAuth entries (encrypted)
567
+ env-config-store.ts SQLite store for per-project env vars (encrypted values)
568
+ global-settings-store.ts SQLite store for global settings + provider registry
569
+ types/provider-registry.ts models.dev registry types + normalizer (pure, shared with the browser)
570
+ base-branches.ts base-branch list parser (pure)
571
+ devbot-scripts.ts .devbot/ script phase builders (init.sh post-clone today)
572
+ base-branch-store.ts SQLite store for per-work-item /base branch choice (node:sqlite)
573
+ work-item-branch-store.ts SQLite store for per-work-item /branch work branch (node:sqlite)
574
+ change-request-store.ts SQLite registry of MRs DevBot created (CI-failure correlation + dedup)
575
+ ci-failure.ts CI-failure context: failing job log tails for the auto-report plan run
576
+ outbound-webhook.ts outbound event emitter + signed delivery worker
577
+ outbound-webhook-store.ts SQLite store for outbound endpoints + delivery queue
578
+ idempotency.ts atomic webhook-id dedup (INSERT OR IGNORE, TTL window)
579
+ views.ts server-rendered HTML for the onboarding flow (Tailwind)
580
+ styles/ tailwind entry (compiled to public/app.css at build time)
581
+ opencode.ts spawn podman, parse NDJSON, extract text parts
582
+ devcontainer.ts nested rootless Podman flag/driver/storage helpers (pure)
583
+ attachments.ts forge-free attachment discovery/filename helpers
584
+ runner.ts volume lifecycle, clone/pull, run, commit/push (returns RunOutcome)
585
+ server.ts Express webhook receiver + onboarding routes + filter chain
586
+ mcp/
587
+ peer-sync.ts on-demand peer clone/fetch into the git-excluded .devbot-peers/ dir
588
+ volume-files.ts read agent files from the run volume + git-excluded .devbot-out/ staging
589
+ tools/ internal devbot-forge MCP tool handlers (incl. cross-project reads + attachment upload)
590
+ AGENTS.md agent workflow + platform-parity rules
591
+ migrations/
592
+ 0001_initial.sql baseline: all current tables (idempotent)
593
+ 0002_queue_status.sql pending_tasks status/attempts/claimed_at/enqueued_at/last_error
594
+ 0005_global_settings.sql global_settings + global_providers; drops project_settings.base_model
595
+ 0006_project_access_grants.sql directed cross-project access grants
596
+ 0022_auto_resolve_conflicts.sql conflict auto-fix column on both settings tables
597
+ 0023_ci_fail_auto_report.sql ci_fail_auto_report columns + change_requests registry
598
+ scripts/
599
+ check-platform-webhooks.ts platform webhook parse + signature self-check
600
+ check-platform-drivers.ts Gitea/ForgeJo driver contract self-check
601
+ check-platform-parity.ts port coverage + no-leak parity self-check
602
+ check-ci-adapter.ts GitLab + Actions CI normalization self-check
603
+ check-ci-fail-report.ts CI-failure registry + log context + resolver self-check
604
+ check-github-webhooks.ts GitHub webhook signature + event parsing self-check
605
+ check-github-driver.ts GitHub driver contract + ZIP log self-check
606
+ check-onboarding-allowlist.ts ONBOARDING_USERS gate self-check (fail closed)
607
+ check-signature.ts HMAC webhook-signature self-check (npm run test:signature)
608
+ check-mention.ts mention parser self-check (npm run test:mention)
609
+ check-attachments.ts attachment discovery + download self-check (npm run test:attachments)
610
+ check-upload-attachments.ts outbound upload (multipart, path validation, volume read) self-check
611
+ check-mcp-upload-tool.ts upload_work_item_attachment handler self-check
612
+ check-mcp-download-tool.ts download_work_item_attachment handler self-check
613
+ check-base-branch-store.ts /base store self-check (npm run test:base-branch)
614
+ check-work-item-branch-store.ts /branch work-branch store self-check (npm run test:work-item-branch)
615
+ check-branch-naming.ts /branch Step-0 gate self-check (npm run test:branch-naming)
616
+ check-devcontainer.ts nested-podman helpers self-check (npm run test:devcontainer)
617
+ check-devcontainer-wiring.ts store volume lifecycle self-check (npm run test:devcontainer-wiring)
618
+ check-queue.ts DB-backed queue self-check (npm run test:queue)
619
+ check-global-queue.ts global gate self-check (npm run test:global-queue)
620
+ check-queue-store.ts queue store self-check (npm run test:queue-store)
621
+ check-idempotency.ts idempotency self-check (npm run test:idempotency)
622
+ check-migrate.ts migration runner self-check (npm run test:migrate)
623
+ check-dispatch.ts dispatch integration self-check (npm run test:dispatch)
624
+ check-access-grants.ts cross-project grant store self-check (npm run test:access-grants)
625
+ check-mcp-scope.ts MCP cross-project scope resolver self-check (npm run test:mcp-scope)
626
+ check-peer-sync.ts peer clone/fetch command builder self-check (npm run test:peer-sync)
627
+ check-outbound-webhooks.ts outbound endpoint store + delivery worker + signing/SSRF self-check
628
+ tailwind.config.cjs Tailwind CLI config (v3, CommonJS)
629
+ opencode-config/ written/mounted into the run container
630
+ opencode.json base config (per-run config is generated into the xdg volume)
631
+ plugins/ path-guard plugin source
632
+ managed/ root-owned managed config baked into the builder image
633
+ Dockerfile.builder node + git + build tools + opencode-ai + tesseract/poppler (attachment OCR)
634
+ Dockerfile.devbot DevBot process image (node)
635
+ ```
636
+
637
+ ## License
638
+
639
+ Apache License 2.0 — see [LICENSE](./LICENSE).