@wascartech/devbot 0.0.0-stage → 2.1.1

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 +150 -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 +438 -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 +158 -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/dist/git.js ADDED
@@ -0,0 +1,605 @@
1
+ import { config } from "./config.js";
2
+ import { runtime } from "./runtime-state.js";
3
+ import { log } from "./logger.js";
4
+ import { shq, redact } from "./redact.js";
5
+ import { podman } from "./podman.js";
6
+ import { buildPostCloneScript } from "./devbot-scripts.js";
7
+ import { getPlatform } from "./platform/index.js";
8
+ import { conflictCheckScript, parseConflictCheckResult, publishScript, parsePublishResult, parseBranchTip, resolverPrompt, parseResolverResult } from "./conflict-fix.js";
9
+ // Volume GC lives in src/volume-gc.ts: a startup reconcile + periodic sweep
10
+ // remove devbot-wi-* volumes inactive for VOLUME_GC_TTL_MS (default 1 week).
11
+ // Build the `--env NAME=VALUE` podman args for a set of per-project env vars.
12
+ // Spread into any podman `run` args array so every container (clone, sync,
13
+ // auth-write, opencode, push) sees the same env. Values are injected as-is —
14
+ // they're already decrypted by the store. podman --env passes them into the
15
+ // container's environment; no shell interpolation occurs.
16
+ export function envArgs(vars) {
17
+ const out = [];
18
+ for (const v of vars) {
19
+ out.push("--env", `${v.name}=${v.value}`);
20
+ }
21
+ return out;
22
+ }
23
+ export function volumeName(workItemId) {
24
+ const prefix = config.INSTANCE_ID ? `${config.INSTANCE_ID}-` : "";
25
+ return `${prefix}devbot-wi-${workItemId}`;
26
+ }
27
+ // Every container that mounts a work item's named volumes must use the same
28
+ // SELinux MCS pair. Podman assigns each container a random pair, and `:Z`
29
+ // relabels the whole volume to the mounting container's pair — so a helper
30
+ // spawned while the agent is running (attachment download/read, peer sync)
31
+ // would revoke the agent's access to /workspace and make the files it writes
32
+ // unreadable to the agent (EACCES). Deriving one pair from the work item id
33
+ // keeps every container of that work item compatible while still separating
34
+ // different work items. No-op where SELinux is disabled (InitLabels returns
35
+ // early), so it is safe to pass unconditionally.
36
+ export function volumeSelinuxLevel(workItemId) {
37
+ const id = Math.abs(Math.trunc(workItemId));
38
+ const a = id % 1024;
39
+ const b = (a + 1 + (id % 1023)) % 1024;
40
+ return `s0:c${a},c${b}`;
41
+ }
42
+ export function volumeSelinuxArgs(workItemId) {
43
+ return ["--security-opt", `label=level:${volumeSelinuxLevel(workItemId)}`];
44
+ }
45
+ // Name of the opencode `run` container for a work item. Single-sourced so the
46
+ // runner and the logs route can never drift (see runner.ts and task-log.ts).
47
+ export function runContainerName(workItemId) {
48
+ return `devbot-wi-${workItemId}-run`;
49
+ }
50
+ // Prefix shared by every phase container for a work item. The live-log viewer
51
+ // discovers the active phase by listing running containers with this prefix.
52
+ export function workItemContainerPrefix(workItemId) {
53
+ return `devbot-wi-${workItemId}-`;
54
+ }
55
+ // Pin the container log driver so `podman logs` can serve a RUNNING container's
56
+ // output. Some hosts default to the journald driver, which returns nothing for a
57
+ // live container (and may have no journal backend at all); k8s-file writes each
58
+ // container's stdout/stderr to a file that `podman logs` can read/tail
59
+ // immediately. Every phase container launch spreads this in.
60
+ export function logDriverArgs() {
61
+ return ["--log-driver", "k8s-file"];
62
+ }
63
+ // Name of a concrete phase container for a work item. Single-sourced so stop/
64
+ // reset, the launch sites, and the live-log phase discovery can never drift.
65
+ // `runContainerName` stays separate because it is referenced widely; every other
66
+ // phase goes through here.
67
+ export function workItemContainerName(workItemId, phase) {
68
+ return `${workItemContainerPrefix(workItemId)}${phase}`;
69
+ }
70
+ // Every phase container for a work item, in one place so stop/reset and the
71
+ // launch sites can never drift. These are the exact `--name`s used when the
72
+ // containers are created (git clone/sync/push, auth/podman-init, peer sync,
73
+ // opencode run). `podman rm -f` tolerates absent names.
74
+ export function workItemContainerNames(workItemId) {
75
+ return [
76
+ workItemContainerName(workItemId, "detect"),
77
+ workItemContainerName(workItemId, "clone"),
78
+ workItemContainerName(workItemId, "sync"),
79
+ workItemContainerName(workItemId, "auth"),
80
+ workItemContainerName(workItemId, "podman-init"),
81
+ workItemContainerName(workItemId, "peerinit"),
82
+ workItemContainerName(workItemId, "peersync"),
83
+ workItemContainerName(workItemId, "gitexclude"),
84
+ workItemContainerName(workItemId, "attachread"),
85
+ workItemContainerName(workItemId, "attachdl"),
86
+ runContainerName(workItemId),
87
+ workItemContainerName(workItemId, "push"),
88
+ workItemContainerName(workItemId, "forcepush"),
89
+ workItemContainerName(workItemId, "conflictcheck"),
90
+ workItemContainerName(workItemId, "tip"),
91
+ workItemContainerName(workItemId, "resolve"),
92
+ ];
93
+ }
94
+ // Human-facing phase labels for the log viewer, keyed by the container-name
95
+ // suffix. A phase with no label falls back to the raw suffix.
96
+ const PHASE_LABELS = {
97
+ detect: "Detecting workspace",
98
+ clone: "Cloning repository",
99
+ sync: "Syncing repository",
100
+ auth: "Writing run files",
101
+ "podman-init": "Preparing nested Podman",
102
+ peerinit: "Preparing peer projects",
103
+ peersync: "Syncing peer project",
104
+ gitexclude: "Updating git excludes",
105
+ attachread: "Reading attachment",
106
+ attachdl: "Downloading attachment",
107
+ run: "Running agent",
108
+ push: "Pushing branch",
109
+ forcepush: "Force-pushing branch",
110
+ conflictcheck: "Checking for conflicts",
111
+ tip: "Reading branch tip",
112
+ resolve: "Resolving conflicts",
113
+ };
114
+ export function phaseLabel(phase) {
115
+ return PHASE_LABELS[phase] ?? phase;
116
+ }
117
+ // Private nested-Podman store for a work item. Paired 1:1 with the workspace
118
+ // volume; created only when DEVCONTAINER_ENABLED, removed alongside it. The
119
+ // `-podman` suffix is deliberately outside the `(\d+)$` workspace reconciler
120
+ // regex so the two are never confused.
121
+ export function podmanVolumeName(workItemId) {
122
+ const prefix = config.INSTANCE_ID ? `${config.INSTANCE_ID}-` : "";
123
+ return `${prefix}devbot-wi-${workItemId}-podman`;
124
+ }
125
+ // Mount point for the dedicated per-work-item XDG volume. Deliberately outside
126
+ // /workspace (the cloned target repo's working tree) so opencode's config,
127
+ // auth, and session data — including the per-run MCP bearer token and the
128
+ // provider API keys — can never be staged by the agent's `git add -A`.
129
+ export const XDG_HOME_DIR = "/home/devbot/.xdg";
130
+ // Dedicated per-work-item XDG volume: opencode's config + data live OUTSIDE the
131
+ // work tree. Sibling of the workspace and nested-Podman store volumes; all
132
+ // three share one ledger row and are reclaimed together by reset/GC. The
133
+ // `-xdg` suffix is recognized by the volume reconciler (volume-gc.ts).
134
+ export function xdgVolumeName(workItemId) {
135
+ const prefix = config.INSTANCE_ID ? `${config.INSTANCE_ID}-` : "";
136
+ return `${prefix}devbot-wi-${workItemId}-xdg`;
137
+ }
138
+ export function branchName(workItemNumber) {
139
+ return `wi-${workItemNumber}`;
140
+ }
141
+ // Resolve the branch a work item runs on: the stored /branch choice when set,
142
+ // otherwise the default wi-<number>. Pure so the resolution rule is unit
143
+ // testable without a DB; the store read happens in runner.ts.
144
+ export function resolveWorkBranch(stored, workItemNumber) {
145
+ return stored ?? branchName(workItemNumber);
146
+ }
147
+ export function timeoutFor(mode) {
148
+ return mode === "build" ? runtime().buildTimeoutMs : runtime().planTimeoutMs;
149
+ }
150
+ // Strip trailing slashes from PLATFORM_URL so URL-scoped git config keys stay
151
+ // well-formed (http.<url>/.extraHeader, not http.<url>//.extraHeader).
152
+ export function normalizeForgeUrl(url) {
153
+ return url.replace(/\/+$/, "");
154
+ }
155
+ // Hostname (no port) of PLATFORM_URL, for the SSH→HTTPS insteadOf rewrite. Uses
156
+ // the URL parser so a path on PLATFORM_URL (rare) doesn't leak into the rewrite
157
+ // key, and strips any HTTPS port so it never contaminates an SSH rewrite. The
158
+ // SSH host is assumed to equal the PLATFORM_URL hostname.
159
+ export function forgeHostname(url) {
160
+ return new URL(normalizeForgeUrl(url)).hostname;
161
+ }
162
+ // Git auth for the forge's smart-HTTP endpoint. The driver supplies the header
163
+ // (URL-scoped to the configured forge host for GitLab, so git only sends the
164
+ // bot token there — a submodule on an external host is fetched without the
165
+ // token instead of leaking it). The `-c` config propagates to the child git
166
+ // processes `submodule update` spawns.
167
+ export function gitExtraHeader() {
168
+ return getPlatform().gitAuth.gitExtraHeader();
169
+ }
170
+ // Same-host SSH→HTTPS URL rewriting for submodules. A submodule whose
171
+ // .gitmodules URL is `git@<forge-host>:…` or `ssh://git@<forge-host>/…`
172
+ // would otherwise try SSH (no key in the container) and fail. Rewriting it to
173
+ // the forge HTTPS base lets it inherit the scoped http extraHeader above.
174
+ // External SSH submodules are deliberately NOT rewritten.
175
+ // git's `insteadOf` is a literal PREFIX match, so a URL with an explicit SSH
176
+ // port (`ssh://git@host:2222/…`) only matches a key that carries that same
177
+ // `:2222`. The port can't be derived from the HTTPS PLATFORM_URL, so it comes
178
+ // from PLATFORM_SSH_PORT; when set, an extra port-bearing ssh:// key is emitted.
179
+ // Only the ssh:// form has a port suffix — the scp-like `git@host:path` form
180
+ // has no port slot, so a single scp key suffices.
181
+ export function submoduleGitConfig(extraHeader, sshPort = config.PLATFORM_SSH_PORT) {
182
+ const base = normalizeForgeUrl(config.PLATFORM_URL);
183
+ const host = forgeHostname(config.PLATFORM_URL);
184
+ const scp = `url.${base}/.insteadOf=${shq(`git@${host}:`)}`;
185
+ const ssh = `url.${base}/.insteadOf=${shq(`ssh://git@${host}/`)}`;
186
+ const parts = [extraHeader, `-c ${scp}`, `-c ${ssh}`];
187
+ if (sshPort) {
188
+ parts.push(`-c url.${base}/.insteadOf=${shq(`ssh://git@${host}:${sshPort}/`)}`);
189
+ }
190
+ return parts.join(" ");
191
+ }
192
+ // Marker printed (one per path) when a submodule can't be initialized. The
193
+ // group always exits 0 so the submodule step stays best-effort and can never
194
+ // mask an earlier clone/fetch failure.
195
+ export const SUBMODULE_FAILED_MARKER = "DEVBOT_SUBMODULE_FAILED";
196
+ // Max submodule paths named in the warning note; the rest are summarized.
197
+ export const SUBMODULE_WARNING_CAP = 10;
198
+ // Compose the submodule init/update step. Recursive, best-effort, and grouped
199
+ // in braces so its `||` applies to the submodule command alone (a bare
200
+ // `a && b || c` would mask an earlier clone/fetch failure). On failure,
201
+ // `git submodule status --recursive` lists the uninitialized paths (leading
202
+ // '-' in column 1) — the failing one plus any never reached.
203
+ export function buildSubmoduleStep(submoduleCfg, enabled = true) {
204
+ if (!enabled || !submoduleCfg)
205
+ return "";
206
+ const update = `${submoduleCfg} submodule update --init --recursive`;
207
+ const report = `git submodule status --recursive | awk 'substr($1,1,1)=="-" {print "${SUBMODULE_FAILED_MARKER}:" $2}'`;
208
+ return `{ ${update} || ${report}; }`;
209
+ }
210
+ // Extract the failing submodule paths from a submodule-step stdout. Each failed
211
+ // path is emitted on its own marker line; order is preserved.
212
+ export function parseSubmoduleFailures(stdout) {
213
+ const out = [];
214
+ const re = new RegExp(`^${SUBMODULE_FAILED_MARKER}:(.+)$`, "gm");
215
+ let m;
216
+ while ((m = re.exec(stdout)) !== null) {
217
+ const path = m[1]?.trim();
218
+ if (path)
219
+ out.push(path);
220
+ }
221
+ return out;
222
+ }
223
+ // Build the informational note for submodule fetch failures. Returns null when
224
+ // there's nothing to report. Paths are repo-controlled: strip newlines/CR and
225
+ // backticks before wrapping in an inline-code span (a stray backtick would
226
+ // break the Markdown and could inject a mention). The list is capped.
227
+ export function formatSubmoduleWarning(paths) {
228
+ if (paths.length === 0)
229
+ return null;
230
+ const sanitize = (p) => p.replace(/[`\r\n]/g, "").trim();
231
+ const shown = paths.slice(0, SUBMODULE_WARNING_CAP).map((p) => `\`${sanitize(p)}\``);
232
+ const overflow = paths.length - shown.length;
233
+ const list = shown.join(", ") + (overflow > 0 ? ` and ${overflow} more` : "");
234
+ return `ℹ️ Some git submodules could not be fetched: ${list}. Continuing without them.`;
235
+ }
236
+ export async function volumeExists(vol) {
237
+ const r = await podman(["volume", "inspect", vol]);
238
+ return r.code === 0;
239
+ }
240
+ // Best-effort removal of the legacy in-repo XDG dirs written by DevBot before
241
+ // the xdg volume existed. They now live on /home/devbot/.xdg, but a volume
242
+ // created by an older release still contains /workspace/.xdg-data and
243
+ // /workspace/.xdg-config with a stale MCP token and provider API keys — which
244
+ // the agent's `git add -A` could commit into a target repo that doesn't
245
+ // ignore them. `|| true` so a permissions hiccup can never block a sync.
246
+ export function legacyXdgCleanup() {
247
+ return "rm -rf /workspace/.xdg-data /workspace/.xdg-config || true";
248
+ }
249
+ // Compose the host-side sync script for an existing volume. It MUST fetch
250
+ // origin before every run so the agent sees the latest remote refs — the
251
+ // orchestrator runs this before spawning opencode. Branch checkout is
252
+ // host-side and deterministic in both modes: relying on the agent to checkout
253
+ // left HEAD on the clone target (e.g. dev) whenever the LLM skipped the step,
254
+ // which the HEAD guard then had to refuse. Rebase onto origin/<branch> stays
255
+ // agent-owned in build mode (it is in the prompt); only branch selection is
256
+ // enforced here. Exported as a pure helper so scripts/check-git-sync.ts can
257
+ // pin the fetch guarantee. submoduleStep (when non-empty) runs last, after
258
+ // fetch/checkout, best-effort.
259
+ // Checkout chain (first match wins):
260
+ // 1. local branch exists → git checkout <branch>
261
+ // 2. remote branch exists → git checkout -b <branch> origin/<branch>
262
+ // 3. neither (fresh work item) → git checkout -B <branch> (off clone target HEAD)
263
+ // `mode` is kept in the signature for shape stability (caller + tests); the
264
+ // mode split was dropped when branch selection became orchestrator-owned.
265
+ export function syncScript(branch, extraHeader, _mode, submoduleStep = "") {
266
+ const b = shq(branch);
267
+ const checkout = `git checkout ${b} 2>/dev/null` +
268
+ ` || git checkout -b ${b} origin/${b} 2>/dev/null` +
269
+ ` || git checkout -B ${b}`;
270
+ // umask 002 + group-writable tree: the bash tool runs as the unprivileged
271
+ // `agent` UID (gid devbot) and must be able to edit/commit alongside opencode
272
+ // (devbot). chmod -R keeps pre-existing files (from older volumes) editable.
273
+ // Braced so its `|| true` applies to the chmod alone: a fetch/checkout failure
274
+ // must still abort the sync, while a chmod EPERM is harmless. After the UID
275
+ // split (#81) `/workspace` can hold files devbot doesn't own — the `agent` UID
276
+ // (1001) and nested-Podman inner-root (subuid) — and chmod then fails
277
+ // "Operation not permitted" (#91). A bare chmod here turned that into a fatal
278
+ // sync failure misreported as a rebase conflict, blocking every re-run.
279
+ const lines = ["umask 002", "cd /workspace", legacyXdgCleanup(), `${extraHeader} fetch origin`, checkout];
280
+ if (submoduleStep)
281
+ lines.push(submoduleStep);
282
+ lines.push("{ chmod -R g+rwX /workspace 2>/dev/null || true; }");
283
+ lines.push(`echo "TIP:$(git rev-parse origin/${b} 2>/dev/null || true)"`);
284
+ return lines.join(" && ");
285
+ }
286
+ export async function ensureVolumeAndWorkspace(input, mode, envVars = [], branches = [], builderImage = config.BUILDER_IMAGE) {
287
+ const vol = volumeName(input.workItemId);
288
+ const exists = await volumeExists(vol);
289
+ if (!exists) {
290
+ const create = await podman(["volume", "create", vol]);
291
+ if (create.code !== 0) {
292
+ return { ok: false, reason: `volume create failed: ${redact(create.stderr.slice(-256))}`, isFirstRun: true, logs: redact(create.stderr) };
293
+ }
294
+ }
295
+ const cloneUrl = input.repoHttpUrl;
296
+ const branch = resolveWorkBranch(input.workBranch ?? null, input.workItemNumber);
297
+ const extraHeader = gitExtraHeader();
298
+ // Submodule init/update runs on first clone and every sync (picks up new
299
+ // submodules + updated gitlinks). Best-effort and never fails the run; the
300
+ // scoped header + same-host SSH rewrite apply because the `-c` config
301
+ // propagates to the child git processes.
302
+ const submoduleStep = buildSubmoduleStep(submoduleGitConfig(extraHeader), runtime().gitSubmodulesEnabled);
303
+ // Detect whether the workspace already has a git repo.
304
+ const detect = await podman([
305
+ "run", "--rm", ...logDriverArgs(), "--name", `devbot-wi-${input.workItemId}-detect`,
306
+ "-v", `${vol}:/workspace:Z`,
307
+ ...envArgs(envVars),
308
+ builderImage,
309
+ "sh", "-c", "cd /workspace 2>/dev/null && git rev-parse --is-inside-work-tree 2>/dev/null && echo OK || echo EMPTY",
310
+ ], { timeoutMs: timeoutFor(mode) });
311
+ const isInit = detect.stdout.includes("OK");
312
+ if (!isInit) {
313
+ // First run: clone with a fallback chain.
314
+ // 1. wi-<iid> — prior WIP branch for this work item, if it exists remotely
315
+ // 2. targetBranch — /base <branch> if given, else the first existing branch
316
+ // from BASE_BRANCHES (tried top-to-bottom, e.g. default
317
+ // "dev,master" tries dev then master); the MR target.
318
+ // Every mode force-checks-out wi-<iid> after the clone, so the local work
319
+ // branch exists even after a fallback clone. Plan mode creates it as a local
320
+ // ref alongside the target; build mode makes it HEAD before the agent runs.
321
+ // /base is a hard per-work-item override that bypasses the env list entirely.
322
+ // If neither /base nor any BASE_BRANCHES branch exists remotely, this is an
323
+ // explicit error (no bare-clone fall-through) — the runner posts an
324
+ // actionable note. targetBranch is always a verified-existing branch by the
325
+ // time the clone runs, so the chain is 2 links (no repo-default bare clone
326
+ // slot).
327
+ log.info("first-run clone", { workItemId: input.workItemId, repo: input.repoHttpUrl, mode, baseBranch: input.baseBranch });
328
+ let targetBranch;
329
+ if (input.baseBranch) {
330
+ targetBranch = input.baseBranch;
331
+ }
332
+ else {
333
+ let hit;
334
+ try {
335
+ hit = await getPlatform().repository.firstExistingBranch(input.projectId, branches);
336
+ }
337
+ catch (e) {
338
+ return { ok: false, reason: `base branch existence check failed: ${e instanceof Error ? e.message : String(e)}`, isFirstRun: true };
339
+ }
340
+ if (!hit) {
341
+ return { ok: false, reason: `no base branch found (tried: ${branches.join(", ")})`, isFirstRun: true };
342
+ }
343
+ targetBranch = hit;
344
+ }
345
+ const gitConfig = `git config user.name ${shq(config.BOT_GIT_NAME)} && git config user.email ${shq(config.BOT_GIT_EMAIL)}`;
346
+ const cloneChain = [
347
+ `${extraHeader} clone --branch ${shq(branch)} ${shq(cloneUrl)} /workspace`,
348
+ `${extraHeader} clone --branch ${shq(targetBranch)} ${shq(cloneUrl)} /workspace`,
349
+ ].join(" || ");
350
+ const checkoutBranch = ` && git checkout -B ${shq(branch)}`;
351
+ // Run .devbot/init.sh if present (after clone, before opencode). Env vars
352
+ // are already exported into this container via --env flags. Non-zero exit
353
+ // aborts with exit 8 + DEVBOT_INIT_FAILED marker for an actionable error.
354
+ const postClone = buildPostCloneScript();
355
+ // Submodule init/update runs after clone + checkout + git config, before
356
+ // .devbot/init.sh (so init.sh sees populated submodules). The step is
357
+ // best-effort: its brace group exits 0 even on failure, then we surface
358
+ // the affected paths as a non-fatal warning below.
359
+ const submoduleSegment = submoduleStep ? ` && ${submoduleStep}` : "";
360
+ // umask 002 + a final group-writable pass: the bash tool runs as the
361
+ // unprivileged `agent` UID (gid devbot) and must edit/commit the tree.
362
+ const cloneCmd = `umask 002 && ${cloneChain} && cd /workspace && ${gitConfig}${checkoutBranch}${submoduleSegment} && ${postClone} && ${legacyXdgCleanup()} && chmod -R g+rwX /workspace && echo "TIP:$(git rev-parse origin/${shq(branch)} 2>/dev/null || true)"`;
363
+ const r = await podman([
364
+ "run", "--rm", ...logDriverArgs(), "--name", `devbot-wi-${input.workItemId}-clone`,
365
+ "-v", `${vol}:/workspace:Z`,
366
+ ...envArgs(envVars),
367
+ builderImage,
368
+ "sh", "-c", cloneCmd,
369
+ ], { timeoutMs: timeoutFor(mode) });
370
+ if (r.code !== 0) {
371
+ // init.sh failure (exit 8) → actionable, developer-fixable error.
372
+ if (r.code === 8 && r.stdout.includes("DEVBOT_INIT_FAILED")) {
373
+ const exitCode = r.stdout.split("DEVBOT_INIT_FAILED:")[1]?.split("\n")[0]?.trim() ?? "unknown";
374
+ return { ok: false, reason: `.devbot/init.sh failed (exit ${exitCode}) — check the script`, isFirstRun: true, logs: redact(r.stdout + r.stderr) };
375
+ }
376
+ return { ok: false, reason: `clone failed: ${redact(r.stderr.slice(-512))}`, isFirstRun: true, logs: redact(r.stdout + r.stderr) };
377
+ }
378
+ return { ok: true, isFirstRun: true, submoduleWarnings: parseSubmoduleFailures(r.stdout), branchTip: parseBranchTip(r.stdout) };
379
+ }
380
+ // Existing volume: fetch + checkout. The branch transition is host-side and
381
+ // deterministic (see syncScript); the agent's build-mode prompt still owns
382
+ // the rebase step. Plan and build modes now share the same checkout chain —
383
+ // a plan-mode run no longer leaves HEAD on the clone target.
384
+ log.info("existing volume, syncing", { workItemId: input.workItemId, mode });
385
+ const syncCmd = syncScript(branch, extraHeader, mode, submoduleStep);
386
+ const r = await podman([
387
+ "run", "--rm", ...logDriverArgs(), "--name", `devbot-wi-${input.workItemId}-sync`,
388
+ "-v", `${vol}:/workspace:Z`,
389
+ ...envArgs(envVars),
390
+ builderImage,
391
+ "sh", "-c", syncCmd,
392
+ ], { timeoutMs: timeoutFor(mode) });
393
+ if (r.code !== 0) {
394
+ // fix #4: do not discard local work via fallback. Report and abort.
395
+ // The sync chain ends in an unconditional `checkout -B`, so a non-zero
396
+ // exit is a fetch/upstream failure or a pathological ref state — infra,
397
+ // not a rebase conflict (the rebase is agent-owned and never runs here).
398
+ // The stderr check distinguishes a missing work-item branch (plan mode,
399
+ // developer-actionable) from a fetch/upstream error.
400
+ const stderr = redact(r.stderr.slice(-512));
401
+ const missing = /pathspec|is not a commit/.test(stderr);
402
+ const reason = missing
403
+ ? `work item branch '${branch}' not found locally or on origin (merged/deleted upstream?)`
404
+ : `workspace sync failed: ${stderr}`;
405
+ return { ok: false, reason, isFirstRun: false, logs: redact(r.stdout + r.stderr) };
406
+ }
407
+ return { ok: true, isFirstRun: false, submoduleWarnings: parseSubmoduleFailures(r.stdout), branchTip: parseBranchTip(r.stdout) };
408
+ }
409
+ // Push the work item branch. The agent has already committed its own work
410
+ // (Conventional Commits, #<iid> in subject — see the build-mode prompt suffix
411
+ // in run()), so this pushes directly. Push is always host-side — the
412
+ // container has no token; the extraHeader injects Basic auth for this one
413
+ // git call only.
414
+ // The host-side sync (syncScript) now places HEAD on the work branch
415
+ // deterministically before the agent runs; this function still carries a
416
+ // HEAD guard as the last-resort check: if HEAD is somehow elsewhere (agent
417
+ // ran git checkout itself, devcontainer init raced, etc.), abort the push
418
+ // and post an actionable note. This is the one check the agent cannot skip —
419
+ // it runs host-side before the push touches the remote. Exit 7 distinguishes
420
+ // a wrong-HEAD from a push failure.
421
+ // Compose the host-side publish script for a work item branch. Thin delegate to
422
+ // publishScript (src/conflict-fix.ts), kept for scripts/check-push-fallback.ts
423
+ // and any other pure-shape callers. Rewrite-aware: see publishScript.
424
+ export function pushScript(workItemNumber, branch, extraHeader, force = false) {
425
+ return publishScript(workItemNumber, branch, extraHeader, "", force);
426
+ }
427
+ // Compose the host-side script for a standalone `/force-push` (no /build, no
428
+ // agent run): force-push the work item's existing local branch to origin
429
+ // using --force-with-lease. Unlike pushScript there is no HEAD guard (the
430
+ // named ref is pushed, not HEAD) and no fallback commit (we are pushing, not
431
+ // committing). Exported as a pure helper for scripts/check-push-fallback.ts.
432
+ // Order is load-bearing:
433
+ // 1. branch guard — abort if the local wi-<iid> branch doesn't exist (exit 6,
434
+ // NO_BRANCH); there is nothing to push.
435
+ // 2. fetch — refresh origin/<branch> so the lease compares against
436
+ // the latest remote, not a stale tracking ref.
437
+ // 3. force-push — overwrite the remote branch iff it hasn't moved since
438
+ // the fetch (--force-with-lease, never plain --force).
439
+ export function forcePushStandaloneScript(branch, extraHeader) {
440
+ return [
441
+ "cd /workspace",
442
+ `git rev-parse --verify --quiet refs/heads/${shq(branch)} >/dev/null || { echo "NO_BRANCH:${branch}"; exit 6; }`,
443
+ `${extraHeader} fetch origin`,
444
+ `${extraHeader} push origin ${shq(branch)} --force-with-lease && echo PUSHED`,
445
+ ].join(" && ");
446
+ }
447
+ // Compose the host-side script for a standalone `/push` (no /build, no agent
448
+ // run): publish the work item's existing local branch to origin without
449
+ // force. Never emits git's raw fast-forward hint: a branch whose remote is not
450
+ // an ancestor of HEAD (diverged, e.g. after a rebase) reports REWRITTEN and
451
+ // exits 9 so the caller can tell the user to use `/force-push`. Order:
452
+ // 1. branch guard — abort if the local branch doesn't exist (exit 6,
453
+ // NO_BRANCH).
454
+ // 2. fetch — refresh origin/<branch> for the ancestor/ahead tests.
455
+ // 3. unborn → plain push.
456
+ // ancestor → ahead ? plain push : NO_COMMITS.
457
+ // not ancestor → REWRITTEN, exit 9 (no clobber; use /force-push).
458
+ // Exported as a pure helper for scripts/check-push-fallback.ts.
459
+ export function pushStandaloneScript(branch, extraHeader) {
460
+ const b = shq(branch);
461
+ return [
462
+ "cd /workspace",
463
+ `git rev-parse --verify --quiet refs/heads/${b} >/dev/null || { echo "NO_BRANCH:${branch}"; exit 6; }`,
464
+ `${extraHeader} fetch origin`,
465
+ `if ! git rev-parse --verify --quiet origin/${b} >/dev/null; then ${extraHeader} push origin ${b} && echo PUSHED;`,
466
+ `elif git merge-base --is-ancestor origin/${b} HEAD; then if test -n "$(git log --format=%H origin/${b}..HEAD)"; then ${extraHeader} push origin ${b} && echo PUSHED; else echo NO_COMMITS; fi;`,
467
+ `else echo "REWRITTEN:${branch}"; exit 9; fi`,
468
+ ].join(" && ");
469
+ }
470
+ export async function forcePushStandalone(input, envVars = [], builderImage = config.BUILDER_IMAGE) {
471
+ const vol = volumeName(input.workItemId);
472
+ // Precondition: the workspace must already exist. A standalone force-push
473
+ // has nothing to do without it (no clone, no local commits) — fail with an
474
+ // actionable reason instead of letting podman create an empty volume and
475
+ // the script hit NO_BRANCH.
476
+ if (!(await volumeExists(vol))) {
477
+ return { ok: false, reason: `no workspace for this work item (volume ${vol} not found)`, missingVolume: true };
478
+ }
479
+ const branch = resolveWorkBranch(input.workBranch ?? null, input.workItemNumber);
480
+ const extraHeader = gitExtraHeader();
481
+ const r = await podman([
482
+ "run", "--rm", ...logDriverArgs(), "--name", workItemContainerName(input.workItemId, "forcepush"),
483
+ "-v", `${vol}:/workspace:Z`,
484
+ "-w", "/workspace",
485
+ ...envArgs(envVars),
486
+ builderImage,
487
+ "sh", "-c",
488
+ forcePushStandaloneScript(branch, extraHeader),
489
+ ], { timeoutMs: runtime().buildTimeoutMs });
490
+ if (r.code !== 0) {
491
+ // exit 6 + NO_BRANCH = the work item never produced a local branch.
492
+ if (r.code === 6 && r.stdout.includes("NO_BRANCH")) {
493
+ return { ok: false, reason: `no local branch '${branch}' to force-push`, logs: redact(r.stdout + r.stderr), missingBranch: true };
494
+ }
495
+ return { ok: false, reason: redact(r.stderr.slice(-256)), logs: redact(r.stdout + r.stderr) };
496
+ }
497
+ return { ok: true, pushed: r.stdout.includes("PUSHED") };
498
+ }
499
+ // Publish-the-existing-branch counterpart of forcePushStandalone for a
500
+ // standalone `/push`. Same preconditions and error taxonomy; the push itself
501
+ // is a plain fast-forward (pushStandaloneScript). `pushed` is false when the
502
+ // remote already has the branch at HEAD (NO_COMMITS) — an idempotent no-op.
503
+ export async function pushStandalone(input, envVars = [], builderImage = config.BUILDER_IMAGE) {
504
+ const vol = volumeName(input.workItemId);
505
+ if (!(await volumeExists(vol))) {
506
+ return { ok: false, reason: `no workspace for this work item (volume ${vol} not found)`, missingVolume: true };
507
+ }
508
+ const branch = resolveWorkBranch(input.workBranch ?? null, input.workItemNumber);
509
+ const extraHeader = gitExtraHeader();
510
+ const r = await podman([
511
+ "run", "--rm", ...logDriverArgs(), "--name", workItemContainerName(input.workItemId, "push"),
512
+ "-v", `${vol}:/workspace:Z`,
513
+ "-w", "/workspace",
514
+ ...envArgs(envVars),
515
+ builderImage,
516
+ "sh", "-c",
517
+ pushStandaloneScript(branch, extraHeader),
518
+ ], { timeoutMs: runtime().buildTimeoutMs });
519
+ if (r.code !== 0) {
520
+ if (r.code === 6 && r.stdout.includes("NO_BRANCH")) {
521
+ return { ok: false, reason: `no local branch '${branch}' to push`, logs: redact(r.stdout + r.stderr), missingBranch: true };
522
+ }
523
+ if (r.code === 9 && r.stdout.includes("REWRITTEN")) {
524
+ return { ok: false, rewritten: true, reason: `branch '${branch}' has a rewritten history; use \`/force-push\` to publish it`, logs: redact(r.stdout + r.stderr) };
525
+ }
526
+ return { ok: false, reason: redact(r.stderr.slice(-256)), logs: redact(r.stdout + r.stderr) };
527
+ }
528
+ return { ok: true, pushed: r.stdout.includes("PUSHED") };
529
+ }
530
+ export async function pushBranch(input, envVars = [], builderImage = config.BUILDER_IMAGE) {
531
+ const vol = volumeName(input.workItemId);
532
+ const branch = resolveWorkBranch(input.workBranch ?? null, input.workItemNumber);
533
+ const extraHeader = gitExtraHeader();
534
+ const r = await podman([
535
+ "run", "--rm", ...logDriverArgs(), "--name", `devbot-wi-${input.workItemId}-push`,
536
+ "-v", `${vol}:/workspace:Z`,
537
+ "-w", "/workspace",
538
+ ...envArgs(envVars),
539
+ builderImage,
540
+ "sh", "-c",
541
+ publishScript(input.workItemNumber, branch, extraHeader, input.preRunTip ?? "", input.forcePush),
542
+ ], { timeoutMs: runtime().buildTimeoutMs });
543
+ const parsed = parsePublishResult(r.stdout);
544
+ if (r.code === 7 && r.stdout.includes("WRONG_HEAD")) {
545
+ const onBranch = r.stdout.split("WRONG_HEAD:")[1]?.split("\n")[0]?.trim() ?? "unknown";
546
+ return { ok: false, reason: `agent left HEAD on '${onBranch}'; push skipped to avoid corrupting the MR`, logs: redact(r.stdout + r.stderr) };
547
+ }
548
+ if (r.code === 10 && r.stdout.includes("CONFLICT_MARKERS")) {
549
+ return { ok: false, reason: "the work tree still contains conflict markers; refusing to commit them", logs: redact(r.stdout + r.stderr) };
550
+ }
551
+ if (r.code === 9 && parsed.remoteMoved) {
552
+ return { ok: false, remoteMoved: true, reason: "the remote branch moved during the run; re-run to fetch and retry", logs: redact(r.stdout + r.stderr) };
553
+ }
554
+ if (r.code !== 0) {
555
+ return { ok: false, reason: redact(r.stderr.slice(-256)), logs: redact(r.stdout + r.stderr) };
556
+ }
557
+ return { ok: true, pushed: parsed.pushed, fallbackCommitted: parsed.fallbackCommitted, forced: parsed.forced, noCommits: parsed.noCommits };
558
+ }
559
+ // Run the conflict check in the builder container. Returns null on an infra
560
+ // failure (missing target, fetch error) so the caller can fail closed.
561
+ export async function checkConflicts(input, envVars = [], builderImage = config.BUILDER_IMAGE) {
562
+ const vol = volumeName(input.workItemId);
563
+ const branch = resolveWorkBranch(input.workBranch ?? null, input.workItemNumber);
564
+ const extraHeader = gitExtraHeader();
565
+ const r = await podman([
566
+ "run", "--rm", ...logDriverArgs(), "--name", `devbot-wi-${input.workItemId}-conflictcheck`,
567
+ "-v", `${vol}:/workspace:Z`,
568
+ "-w", "/workspace",
569
+ ...envArgs(envVars),
570
+ builderImage,
571
+ "sh", "-c",
572
+ conflictCheckScript(input.target, branch, extraHeader),
573
+ ], { timeoutMs: runtime().buildTimeoutMs });
574
+ const parsed = parseConflictCheckResult(r.code, r.stdout);
575
+ if (!parsed) {
576
+ log.warn("conflict check failed", { workItemId: input.workItemId, code: r.code, stderr: redact(r.stderr.slice(-256)) });
577
+ }
578
+ return parsed;
579
+ }
580
+ // Read the remote-tracking tip of the work branch (`origin/<branch>`) without
581
+ // fetching — used to pin the force-with-lease after the resolver rewrites
582
+ // history. The conflict check that runs just before this already fetched, so
583
+ // the tracking ref is current and no auth is needed here (a bare `fetch` in a
584
+ // private repo would fail without the token). Returns null when absent.
585
+ export async function readRemoteBranchTip(input, envVars = [], builderImage = config.BUILDER_IMAGE) {
586
+ const vol = volumeName(input.workItemId);
587
+ const branch = resolveWorkBranch(input.workBranch ?? null, input.workItemNumber);
588
+ const r = await podman([
589
+ "run", "--rm", ...logDriverArgs(), "--name", `devbot-wi-${input.workItemId}-tip`,
590
+ "-v", `${vol}:/workspace:Z`,
591
+ "-w", "/workspace",
592
+ ...envArgs(envVars),
593
+ builderImage,
594
+ "sh", "-c",
595
+ `cd /workspace && git rev-parse origin/${shq(branch)} 2>/dev/null || true`,
596
+ ], { timeoutMs: runtime().buildTimeoutMs });
597
+ const sha = r.stdout.trim();
598
+ return /^[0-9a-f]{7,40}$/.test(sha) ? sha : null;
599
+ }
600
+ // The resolver prompt for the direct build run that resolves conflicts.
601
+ export function conflictResolverPrompt(branch, target) {
602
+ return resolverPrompt(branch, target);
603
+ }
604
+ export { parseResolverResult };
605
+ //# sourceMappingURL=git.js.map