dorfl 0.14.2 → 0.15.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 (418) hide show
  1. package/dist/advance-ci-template.d.ts +17 -16
  2. package/dist/advance-ci-template.d.ts.map +1 -1
  3. package/dist/advance-ci-template.js +42 -53
  4. package/dist/advance-ci-template.js.map +1 -1
  5. package/dist/advance-drivers.d.ts +8 -0
  6. package/dist/advance-drivers.d.ts.map +1 -1
  7. package/dist/advance-drivers.js +28 -4
  8. package/dist/advance-drivers.js.map +1 -1
  9. package/dist/advance-isolated.d.ts.map +1 -1
  10. package/dist/advance-isolated.js +3 -2
  11. package/dist/advance-isolated.js.map +1 -1
  12. package/dist/advance-lifecycle-template.d.ts +41 -40
  13. package/dist/advance-lifecycle-template.d.ts.map +1 -1
  14. package/dist/advance-lifecycle-template.js +309 -489
  15. package/dist/advance-lifecycle-template.js.map +1 -1
  16. package/dist/advance-loop-driver.d.ts.map +1 -1
  17. package/dist/advance-loop-driver.js +3 -2
  18. package/dist/advance-loop-driver.js.map +1 -1
  19. package/dist/advance-treeless-publish.d.ts +31 -0
  20. package/dist/advance-treeless-publish.d.ts.map +1 -1
  21. package/dist/advance-treeless-publish.js +40 -0
  22. package/dist/advance-treeless-publish.js.map +1 -1
  23. package/dist/advance.d.ts +63 -3
  24. package/dist/advance.d.ts.map +1 -1
  25. package/dist/advance.js +80 -12
  26. package/dist/advance.js.map +1 -1
  27. package/dist/advancing-lock.d.ts +9 -0
  28. package/dist/advancing-lock.d.ts.map +1 -1
  29. package/dist/advancing-lock.js +21 -1
  30. package/dist/advancing-lock.js.map +1 -1
  31. package/dist/agent-env.d.ts +61 -0
  32. package/dist/agent-env.d.ts.map +1 -0
  33. package/dist/agent-env.js +144 -0
  34. package/dist/agent-env.js.map +1 -0
  35. package/dist/agent-stop.d.ts +35 -0
  36. package/dist/agent-stop.d.ts.map +1 -1
  37. package/dist/agent-stop.js +62 -0
  38. package/dist/agent-stop.js.map +1 -1
  39. package/dist/apply-merge-action.d.ts +76 -34
  40. package/dist/apply-merge-action.d.ts.map +1 -1
  41. package/dist/apply-merge-action.js +339 -129
  42. package/dist/apply-merge-action.js.map +1 -1
  43. package/dist/apply-stuck-action.js +2 -2
  44. package/dist/apply-stuck-action.js.map +1 -1
  45. package/dist/ci-agent-result.d.ts +151 -0
  46. package/dist/ci-agent-result.d.ts.map +1 -0
  47. package/dist/ci-agent-result.js +314 -0
  48. package/dist/ci-agent-result.js.map +1 -0
  49. package/dist/ci-handoff-apply.d.ts +162 -0
  50. package/dist/ci-handoff-apply.d.ts.map +1 -0
  51. package/dist/ci-handoff-apply.js +497 -0
  52. package/dist/ci-handoff-apply.js.map +1 -0
  53. package/dist/ci-handoff-format.d.ts +369 -0
  54. package/dist/ci-handoff-format.d.ts.map +1 -0
  55. package/dist/ci-handoff-format.js +646 -0
  56. package/dist/ci-handoff-format.js.map +1 -0
  57. package/dist/ci-handoff-lfs.d.ts +146 -0
  58. package/dist/ci-handoff-lfs.d.ts.map +1 -0
  59. package/dist/ci-handoff-lfs.js +397 -0
  60. package/dist/ci-handoff-lfs.js.map +1 -0
  61. package/dist/ci-handoff.d.ts +99 -0
  62. package/dist/ci-handoff.d.ts.map +1 -0
  63. package/dist/ci-handoff.js +277 -0
  64. package/dist/ci-handoff.js.map +1 -0
  65. package/dist/ci-lock-outputs.d.ts +93 -0
  66. package/dist/ci-lock-outputs.d.ts.map +1 -0
  67. package/dist/ci-lock-outputs.js +223 -0
  68. package/dist/ci-lock-outputs.js.map +1 -0
  69. package/dist/ci-phase-build.d.ts +178 -0
  70. package/dist/ci-phase-build.d.ts.map +1 -0
  71. package/dist/ci-phase-build.js +989 -0
  72. package/dist/ci-phase-build.js.map +1 -0
  73. package/dist/ci-phase-driver.d.ts +247 -0
  74. package/dist/ci-phase-driver.d.ts.map +1 -0
  75. package/dist/ci-phase-driver.js +370 -0
  76. package/dist/ci-phase-driver.js.map +1 -0
  77. package/dist/ci-phase-intake.d.ts +172 -0
  78. package/dist/ci-phase-intake.d.ts.map +1 -0
  79. package/dist/ci-phase-intake.js +555 -0
  80. package/dist/ci-phase-intake.js.map +1 -0
  81. package/dist/ci-phase-tasking.d.ts +205 -0
  82. package/dist/ci-phase-tasking.d.ts.map +1 -0
  83. package/dist/ci-phase-tasking.js +906 -0
  84. package/dist/ci-phase-tasking.js.map +1 -0
  85. package/dist/ci-phase-treeless.d.ts +224 -0
  86. package/dist/ci-phase-treeless.d.ts.map +1 -0
  87. package/dist/ci-phase-treeless.js +1482 -0
  88. package/dist/ci-phase-treeless.js.map +1 -0
  89. package/dist/ci-read-token.d.ts +47 -0
  90. package/dist/ci-read-token.d.ts.map +1 -0
  91. package/dist/ci-read-token.js +80 -0
  92. package/dist/ci-read-token.js.map +1 -0
  93. package/dist/cli.d.ts.map +1 -1
  94. package/dist/cli.js +447 -22
  95. package/dist/cli.js.map +1 -1
  96. package/dist/close-job-template.d.ts +10 -1
  97. package/dist/close-job-template.d.ts.map +1 -1
  98. package/dist/close-job-template.js +46 -14
  99. package/dist/close-job-template.js.map +1 -1
  100. package/dist/close-job.d.ts +20 -5
  101. package/dist/close-job.d.ts.map +1 -1
  102. package/dist/close-job.js +65 -12
  103. package/dist/close-job.js.map +1 -1
  104. package/dist/complete.d.ts.map +1 -1
  105. package/dist/complete.js +20 -1
  106. package/dist/complete.js.map +1 -1
  107. package/dist/config.d.ts +41 -44
  108. package/dist/config.d.ts.map +1 -1
  109. package/dist/config.js +2 -4
  110. package/dist/config.js.map +1 -1
  111. package/dist/continue-branch.d.ts +63 -15
  112. package/dist/continue-branch.d.ts.map +1 -1
  113. package/dist/continue-branch.js +75 -8
  114. package/dist/continue-branch.js.map +1 -1
  115. package/dist/crashed-run-save.d.ts +73 -0
  116. package/dist/crashed-run-save.d.ts.map +1 -0
  117. package/dist/crashed-run-save.js +86 -0
  118. package/dist/crashed-run-save.js.map +1 -0
  119. package/dist/cwd-section.d.ts.map +1 -1
  120. package/dist/cwd-section.js +10 -2
  121. package/dist/cwd-section.js.map +1 -1
  122. package/dist/decision-engine.d.ts +10 -0
  123. package/dist/decision-engine.d.ts.map +1 -1
  124. package/dist/decision-engine.js.map +1 -1
  125. package/dist/do-config.d.ts +2 -24
  126. package/dist/do-config.d.ts.map +1 -1
  127. package/dist/do-config.js +1 -33
  128. package/dist/do-config.js.map +1 -1
  129. package/dist/do.d.ts +149 -1
  130. package/dist/do.d.ts.map +1 -1
  131. package/dist/do.js +115 -25
  132. package/dist/do.js.map +1 -1
  133. package/dist/dorfl-item-template.d.ts +98 -0
  134. package/dist/dorfl-item-template.d.ts.map +1 -0
  135. package/dist/dorfl-item-template.js +504 -0
  136. package/dist/dorfl-item-template.js.map +1 -0
  137. package/dist/env-config.d.ts.map +1 -1
  138. package/dist/env-config.js +7 -5
  139. package/dist/env-config.js.map +1 -1
  140. package/dist/frontmatter.d.ts +52 -0
  141. package/dist/frontmatter.d.ts.map +1 -1
  142. package/dist/frontmatter.js +136 -2
  143. package/dist/frontmatter.js.map +1 -1
  144. package/dist/gc.d.ts +13 -0
  145. package/dist/gc.d.ts.map +1 -1
  146. package/dist/gc.js +16 -2
  147. package/dist/gc.js.map +1 -1
  148. package/dist/git.d.ts +18 -1
  149. package/dist/git.d.ts.map +1 -1
  150. package/dist/git.js +70 -5
  151. package/dist/git.js.map +1 -1
  152. package/dist/harness.d.ts +44 -5
  153. package/dist/harness.d.ts.map +1 -1
  154. package/dist/harness.js +24 -1
  155. package/dist/harness.js.map +1 -1
  156. package/dist/index.d.ts +11 -4
  157. package/dist/index.d.ts.map +1 -1
  158. package/dist/index.js +7 -3
  159. package/dist/index.js.map +1 -1
  160. package/dist/install-ci-action-pins.d.ts +123 -0
  161. package/dist/install-ci-action-pins.d.ts.map +1 -0
  162. package/dist/install-ci-action-pins.js +190 -0
  163. package/dist/install-ci-action-pins.js.map +1 -0
  164. package/dist/install-ci-actions-pr-creation.d.ts +70 -0
  165. package/dist/install-ci-actions-pr-creation.d.ts.map +1 -0
  166. package/dist/install-ci-actions-pr-creation.js +106 -0
  167. package/dist/install-ci-actions-pr-creation.js.map +1 -0
  168. package/dist/install-ci-capabilities/dorfl-item.d.ts +12 -0
  169. package/dist/install-ci-capabilities/dorfl-item.d.ts.map +1 -0
  170. package/dist/install-ci-capabilities/dorfl-item.js +26 -0
  171. package/dist/install-ci-capabilities/dorfl-item.js.map +1 -0
  172. package/dist/install-ci-core.d.ts +155 -37
  173. package/dist/install-ci-core.d.ts.map +1 -1
  174. package/dist/install-ci-core.js +304 -200
  175. package/dist/install-ci-core.js.map +1 -1
  176. package/dist/install-ci-github.d.ts +47 -2
  177. package/dist/install-ci-github.d.ts.map +1 -1
  178. package/dist/install-ci-github.js +113 -1
  179. package/dist/install-ci-github.js.map +1 -1
  180. package/dist/install-ci.d.ts +25 -6
  181. package/dist/install-ci.d.ts.map +1 -1
  182. package/dist/install-ci.js +72 -36
  183. package/dist/install-ci.js.map +1 -1
  184. package/dist/intake-marker.d.ts +6 -1
  185. package/dist/intake-marker.d.ts.map +1 -1
  186. package/dist/intake-marker.js.map +1 -1
  187. package/dist/intake-triage.d.ts.map +1 -1
  188. package/dist/intake-triage.js +6 -2
  189. package/dist/intake-triage.js.map +1 -1
  190. package/dist/intake-trigger-template.d.ts +31 -25
  191. package/dist/intake-trigger-template.d.ts.map +1 -1
  192. package/dist/intake-trigger-template.js +107 -292
  193. package/dist/intake-trigger-template.js.map +1 -1
  194. package/dist/intake.d.ts +186 -4
  195. package/dist/intake.d.ts.map +1 -1
  196. package/dist/intake.js +533 -416
  197. package/dist/intake.js.map +1 -1
  198. package/dist/integration-core.d.ts +154 -1
  199. package/dist/integration-core.d.ts.map +1 -1
  200. package/dist/integration-core.js +759 -486
  201. package/dist/integration-core.js.map +1 -1
  202. package/dist/integrator.d.ts +12 -0
  203. package/dist/integrator.d.ts.map +1 -1
  204. package/dist/integrator.js +21 -0
  205. package/dist/integrator.js.map +1 -1
  206. package/dist/isolation.d.ts +12 -2
  207. package/dist/isolation.d.ts.map +1 -1
  208. package/dist/isolation.js +19 -8
  209. package/dist/isolation.js.map +1 -1
  210. package/dist/item-lock.d.ts +93 -11
  211. package/dist/item-lock.d.ts.map +1 -1
  212. package/dist/item-lock.js +233 -65
  213. package/dist/item-lock.js.map +1 -1
  214. package/dist/land-question-residue.d.ts +58 -0
  215. package/dist/land-question-residue.d.ts.map +1 -0
  216. package/dist/land-question-residue.js +95 -0
  217. package/dist/land-question-residue.js.map +1 -0
  218. package/dist/lifecycle-gather.d.ts +16 -0
  219. package/dist/lifecycle-gather.d.ts.map +1 -1
  220. package/dist/lifecycle-gather.js +54 -6
  221. package/dist/lifecycle-gather.js.map +1 -1
  222. package/dist/merge-answer.d.ts +41 -0
  223. package/dist/merge-answer.d.ts.map +1 -0
  224. package/dist/merge-answer.js +60 -0
  225. package/dist/merge-answer.js.map +1 -0
  226. package/dist/merge-question-surfacer.d.ts +105 -30
  227. package/dist/merge-question-surfacer.d.ts.map +1 -1
  228. package/dist/merge-question-surfacer.js +202 -91
  229. package/dist/merge-question-surfacer.js.map +1 -1
  230. package/dist/merge-question-tick.d.ts +64 -0
  231. package/dist/merge-question-tick.d.ts.map +1 -0
  232. package/dist/merge-question-tick.js +96 -0
  233. package/dist/merge-question-tick.js.map +1 -0
  234. package/dist/mint-adr.d.ts.map +1 -1
  235. package/dist/mint-adr.js +20 -2
  236. package/dist/mint-adr.js.map +1 -1
  237. package/dist/needs-attention.d.ts +72 -2
  238. package/dist/needs-attention.d.ts.map +1 -1
  239. package/dist/needs-attention.js +275 -38
  240. package/dist/needs-attention.js.map +1 -1
  241. package/dist/phase-recorder.d.ts +168 -0
  242. package/dist/phase-recorder.d.ts.map +1 -0
  243. package/dist/phase-recorder.js +218 -0
  244. package/dist/phase-recorder.js.map +1 -0
  245. package/dist/phase.d.ts +94 -0
  246. package/dist/phase.d.ts.map +1 -0
  247. package/dist/phase.js +148 -0
  248. package/dist/phase.js.map +1 -0
  249. package/dist/pi-harness.d.ts.map +1 -1
  250. package/dist/pi-harness.js +19 -6
  251. package/dist/pi-harness.js.map +1 -1
  252. package/dist/prepare.d.ts.map +1 -1
  253. package/dist/prepare.js +4 -0
  254. package/dist/prepare.js.map +1 -1
  255. package/dist/prompt.d.ts +4 -0
  256. package/dist/prompt.d.ts.map +1 -1
  257. package/dist/prompt.js +48 -1
  258. package/dist/prompt.js.map +1 -1
  259. package/dist/protocol/CLAIM-PROTOCOL.md +3 -1
  260. package/dist/protocol/WORK-CONTRACT.md +1 -1
  261. package/dist/reap-branches.d.ts +48 -0
  262. package/dist/reap-branches.d.ts.map +1 -1
  263. package/dist/reap-branches.js +64 -0
  264. package/dist/reap-branches.js.map +1 -1
  265. package/dist/ref-write.d.ts +198 -0
  266. package/dist/ref-write.d.ts.map +1 -0
  267. package/dist/ref-write.js +79 -0
  268. package/dist/ref-write.js.map +1 -0
  269. package/dist/repo-config.js +8 -8
  270. package/dist/repo-config.js.map +1 -1
  271. package/dist/repo-mirror.d.ts +36 -0
  272. package/dist/repo-mirror.d.ts.map +1 -1
  273. package/dist/repo-mirror.js +91 -1
  274. package/dist/repo-mirror.js.map +1 -1
  275. package/dist/review-verdict.d.ts +1 -1
  276. package/dist/review-verdict.js +1 -1
  277. package/dist/run.d.ts +7 -1
  278. package/dist/run.d.ts.map +1 -1
  279. package/dist/run.js +28 -4
  280. package/dist/run.js.map +1 -1
  281. package/dist/scan.d.ts +19 -1
  282. package/dist/scan.d.ts.map +1 -1
  283. package/dist/scan.js +21 -8
  284. package/dist/scan.js.map +1 -1
  285. package/dist/sidecar.d.ts +14 -0
  286. package/dist/sidecar.d.ts.map +1 -1
  287. package/dist/sidecar.js +21 -0
  288. package/dist/sidecar.js.map +1 -1
  289. package/dist/single-job-warning.d.ts +63 -0
  290. package/dist/single-job-warning.d.ts.map +1 -0
  291. package/dist/single-job-warning.js +110 -0
  292. package/dist/single-job-warning.js.map +1 -0
  293. package/dist/skills/setup/protocol/CLAIM-PROTOCOL.md +3 -1
  294. package/dist/skills/setup/protocol/WORK-CONTRACT.md +1 -1
  295. package/dist/spec-complete.d.ts +13 -6
  296. package/dist/spec-complete.d.ts.map +1 -1
  297. package/dist/spec-complete.js +23 -11
  298. package/dist/spec-complete.js.map +1 -1
  299. package/dist/start.d.ts.map +1 -1
  300. package/dist/start.js +47 -14
  301. package/dist/start.js.map +1 -1
  302. package/dist/status.d.ts +12 -0
  303. package/dist/status.d.ts.map +1 -1
  304. package/dist/status.js +11 -3
  305. package/dist/status.js.map +1 -1
  306. package/dist/tasking.d.ts +246 -1
  307. package/dist/tasking.d.ts.map +1 -1
  308. package/dist/tasking.js +452 -358
  309. package/dist/tasking.js.map +1 -1
  310. package/dist/triage-persist.d.ts.map +1 -1
  311. package/dist/triage-persist.js +21 -2
  312. package/dist/triage-persist.js.map +1 -1
  313. package/dist/verify-workflow-template.d.ts.map +1 -1
  314. package/dist/verify-workflow-template.js +8 -4
  315. package/dist/verify-workflow-template.js.map +1 -1
  316. package/dist/verify.d.ts.map +1 -1
  317. package/dist/verify.js +4 -0
  318. package/dist/verify.js.map +1 -1
  319. package/dist/watch-session.d.ts +42 -13
  320. package/dist/watch-session.d.ts.map +1 -1
  321. package/dist/watch-session.js +52 -23
  322. package/dist/watch-session.js.map +1 -1
  323. package/dist/work-layout.d.ts +10 -4
  324. package/dist/work-layout.d.ts.map +1 -1
  325. package/dist/work-layout.js +11 -3
  326. package/dist/work-layout.js.map +1 -1
  327. package/dist/workspace.d.ts +65 -4
  328. package/dist/workspace.d.ts.map +1 -1
  329. package/dist/workspace.js +159 -11
  330. package/dist/workspace.js.map +1 -1
  331. package/package.json +1 -1
  332. package/src/advance-ci-template.ts +80 -81
  333. package/src/advance-drivers.ts +45 -6
  334. package/src/advance-isolated.ts +3 -5
  335. package/src/advance-lifecycle-template.ts +375 -538
  336. package/src/advance-loop-driver.ts +3 -5
  337. package/src/advance-treeless-publish.ts +61 -0
  338. package/src/advance.ts +145 -15
  339. package/src/advancing-lock.ts +36 -1
  340. package/src/agent-env.ts +174 -0
  341. package/src/agent-stop.ts +81 -0
  342. package/src/apply-merge-action.ts +401 -154
  343. package/src/apply-stuck-action.ts +2 -2
  344. package/src/ci-agent-result.ts +421 -0
  345. package/src/ci-handoff-apply.ts +737 -0
  346. package/src/ci-handoff-format.ts +926 -0
  347. package/src/ci-handoff-lfs.ts +519 -0
  348. package/src/ci-handoff.ts +392 -0
  349. package/src/ci-lock-outputs.ts +273 -0
  350. package/src/ci-phase-build.ts +1328 -0
  351. package/src/ci-phase-driver.ts +548 -0
  352. package/src/ci-phase-intake.ts +861 -0
  353. package/src/ci-phase-tasking.ts +1255 -0
  354. package/src/ci-phase-treeless.ts +2028 -0
  355. package/src/ci-read-token.ts +90 -0
  356. package/src/cli.ts +591 -29
  357. package/src/close-job-template.ts +52 -15
  358. package/src/close-job.ts +77 -12
  359. package/src/complete.ts +19 -1
  360. package/src/config.ts +43 -48
  361. package/src/continue-branch.ts +121 -16
  362. package/src/crashed-run-save.ts +162 -0
  363. package/src/cwd-section.ts +10 -0
  364. package/src/decision-engine.ts +10 -0
  365. package/src/do-config.ts +2 -57
  366. package/src/do.ts +170 -28
  367. package/src/dorfl-item-template.ts +556 -0
  368. package/src/env-config.ts +7 -5
  369. package/src/frontmatter.ts +155 -2
  370. package/src/gc.ts +27 -2
  371. package/src/git.ts +83 -5
  372. package/src/harness.ts +58 -6
  373. package/src/index.ts +50 -1
  374. package/src/install-ci-action-pins.ts +209 -0
  375. package/src/install-ci-actions-pr-creation.ts +186 -0
  376. package/src/install-ci-capabilities/dorfl-item.ts +33 -0
  377. package/src/install-ci-core.ts +388 -233
  378. package/src/install-ci-github.ts +149 -1
  379. package/src/install-ci.ts +83 -37
  380. package/src/intake-marker.ts +6 -1
  381. package/src/intake-triage.ts +7 -3
  382. package/src/intake-trigger-template.ts +126 -327
  383. package/src/intake.ts +725 -544
  384. package/src/integration-core.ts +975 -523
  385. package/src/integrator.ts +28 -0
  386. package/src/isolation.ts +29 -10
  387. package/src/item-lock.ts +308 -92
  388. package/src/land-question-residue.ts +122 -0
  389. package/src/lifecycle-gather.ts +82 -10
  390. package/src/merge-answer.ts +82 -0
  391. package/src/merge-question-surfacer.ts +320 -112
  392. package/src/merge-question-tick.ts +182 -0
  393. package/src/mint-adr.ts +28 -2
  394. package/src/needs-attention.ts +350 -42
  395. package/src/phase-recorder.ts +350 -0
  396. package/src/phase.ts +177 -0
  397. package/src/pi-harness.ts +25 -5
  398. package/src/prepare.ts +4 -0
  399. package/src/prompt.ts +68 -1
  400. package/src/reap-branches.ts +107 -0
  401. package/src/ref-write.ts +324 -0
  402. package/src/repo-config.ts +8 -8
  403. package/src/repo-mirror.ts +107 -1
  404. package/src/review-verdict.ts +1 -1
  405. package/src/run.ts +47 -6
  406. package/src/scan.ts +37 -7
  407. package/src/sidecar.ts +36 -0
  408. package/src/single-job-warning.ts +135 -0
  409. package/src/spec-complete.ts +36 -14
  410. package/src/start.ts +55 -16
  411. package/src/status.ts +29 -3
  412. package/src/tasking.ts +593 -378
  413. package/src/triage-persist.ts +24 -2
  414. package/src/verify-workflow-template.ts +8 -4
  415. package/src/verify.ts +4 -0
  416. package/src/watch-session.ts +86 -28
  417. package/src/work-layout.ts +11 -3
  418. package/src/workspace.ts +210 -14
@@ -0,0 +1,519 @@
1
+ /**
2
+ * **Git LFS objects in the CI handoff** (spec `ci-agent-job-without-write-token`,
3
+ * ADR `ci-agent-job-holds-no-write-token` decision 6, task
4
+ * `ci-split-handoff-lfs-objects`).
5
+ *
6
+ * The agent job commits LFS-tracked paths as pointers (its checkout runs
7
+ * `git lfs install --local`), with the objects under `.git/lfs/objects/`. It
8
+ * holds no write token, so it cannot upload them: the handoff carries them as
9
+ * `lfs/<oid>`, and the apply job pushes them to the arbiter's LFS store BEFORE
10
+ * any ref, so a ref never lands pointing at a missing object.
11
+ *
12
+ * Pointers are found by scanning the blobs the new commits add or change (a
13
+ * blob that parses as a pointer counts, whatever `.gitattributes` says), never
14
+ * by trusting `git lfs ls-files`. Accepted consequence: a legitimate text file
15
+ * that happens to parse as a pointer is treated as one, and its object must be
16
+ * present or the handoff is rejected.
17
+ *
18
+ * - agent side: {@link writeLfsObjects} copies every referenced object from
19
+ * the local store into `lfs/<oid>`, including one the agent copied from
20
+ * elsewhere in the repository;
21
+ * - apply side: {@link checkLfsObjects} runs the strict parser's verdicts
22
+ * against the artifact's objects (present, a regular file, exactly `size`
23
+ * bytes, hashing to `oid`, none unreferenced, within the size limit), and
24
+ * {@link pushLfsObjects} stages the validated objects into the apply
25
+ * checkout's own store (re-hashing every byte it copies) and pushes them with
26
+ * `git lfs push --object-id <arbiter> <oids>` from that checkout of the
27
+ * trusted base, so `.lfsconfig` and the endpoint come from `main` (the
28
+ * bundle cannot change `.lfsconfig`: it is a protected path).
29
+ *
30
+ * Content addressing means a hostile object can only be itself: it cannot
31
+ * overwrite another object.
32
+ */
33
+
34
+ import {createHash} from 'node:crypto';
35
+ import {
36
+ closeSync,
37
+ constants as fsConstants,
38
+ copyFileSync,
39
+ existsSync,
40
+ fstatSync,
41
+ mkdirSync,
42
+ openSync,
43
+ readSync,
44
+ renameSync,
45
+ rmSync,
46
+ statSync,
47
+ writeSync,
48
+ } from 'node:fs';
49
+ import {dirname, isAbsolute, join, resolve} from 'node:path';
50
+ import {
51
+ handoffByteLimits,
52
+ rejectHandoff as reject,
53
+ } from './ci-handoff-format.js';
54
+ import {git, gitBytes, run} from './git.js';
55
+ import {refWrite} from './ref-write.js';
56
+
57
+ /** git-lfs's blob size cutoff: a larger blob is never read as a pointer. */
58
+ export const LFS_POINTER_MAX_BYTES = 1024;
59
+
60
+ /**
61
+ * The directory of the artifact that holds the objects (`lfs/<oid>`): the
62
+ * same name as `HANDOFF_LFS_DIR` in `ci-handoff.ts`, which imports this module.
63
+ */
64
+ const LFS_ARTIFACT_DIR = 'lfs';
65
+
66
+ /** The one pointer form accepted: spec v1, keys in order, nothing else. */
67
+ const STRICT_POINTER =
68
+ /^version https:\/\/git-lfs\.github\.com\/spec\/v1\noid sha256:([0-9a-f]{64})\nsize (0|[1-9][0-9]*)\n$/;
69
+
70
+ /**
71
+ * What git-lfs itself could read as a pointer: a first key `version` naming an
72
+ * LFS spec (current or the legacy `hawser` one), after leading whitespace.
73
+ */
74
+ const POINTER_LIKE =
75
+ /^[ \t\r\n]*version https:\/\/(?:git-lfs|hawser)\.github\.com\/spec\//;
76
+
77
+ const MODE_FILE = '100644';
78
+ const MODE_EXEC = '100755';
79
+
80
+ /** A parsed LFS pointer. */
81
+ export interface LfsPointer {
82
+ /** The sha256 of the object, 64 lower-case hex. */
83
+ oid: string;
84
+ /** The object's size in bytes. */
85
+ size: number;
86
+ }
87
+
88
+ /**
89
+ * Parse a blob as a spec v1 LFS pointer, STRICTLY: at most
90
+ * {@link LFS_POINTER_MAX_BYTES} bytes, exactly
91
+ * `version https://git-lfs.github.com/spec/v1`, `oid sha256:<64 hex>`,
92
+ * `size <n>` (no leading zero, a safe integer), each ending in `\n`, in that
93
+ * order, nothing else. `undefined` when the blob is not such a pointer.
94
+ */
95
+ export function parseLfsPointer(content: Buffer): LfsPointer | undefined {
96
+ if (content.length > LFS_POINTER_MAX_BYTES) return undefined;
97
+ const m = STRICT_POINTER.exec(content.toString('latin1'));
98
+ if (m === null) return undefined;
99
+ const size = Number(m[2]);
100
+ if (!Number.isSafeInteger(size)) return undefined;
101
+ return {oid: m[1], size};
102
+ }
103
+
104
+ /**
105
+ * Whether git-lfs could read a blob as a pointer (see {@link POINTER_LIKE}). A
106
+ * blob that looks like one but does not parse strictly is a MALFORMED pointer,
107
+ * which the apply job rejects rather than treating as an ordinary file.
108
+ */
109
+ export function looksLikeLfsPointer(content: Buffer): boolean {
110
+ if (content.length === 0 || content.length > LFS_POINTER_MAX_BYTES) {
111
+ return false;
112
+ }
113
+ return POINTER_LIKE.test(content.toString('latin1'));
114
+ }
115
+
116
+ /** A regular-file blob a new commit adds or changes, with one path it has. */
117
+ export interface ChangedBlob {
118
+ oid: string;
119
+ path: string;
120
+ }
121
+
122
+ /** A pointer found in the new commits, with a path that carries it. */
123
+ export interface FoundPointer extends LfsPointer {
124
+ path: string;
125
+ }
126
+
127
+ /** What {@link scanLfsPointers} found. */
128
+ export interface LfsScan {
129
+ pointers: FoundPointer[];
130
+ /** Paths of blobs that look like pointers but do not parse strictly. */
131
+ malformed: string[];
132
+ }
133
+
134
+ /** Whether a tree entry mode is a regular file (LFS applies to nothing else). */
135
+ export function isRegularFileMode(mode: string): boolean {
136
+ return mode === MODE_FILE || mode === MODE_EXEC;
137
+ }
138
+
139
+ /**
140
+ * Scan `blobs` (read in `cwd`) for LFS pointers: every blob of at most
141
+ * {@link LFS_POINTER_MAX_BYTES} bytes is read, in one `cat-file --batch`.
142
+ */
143
+ export function scanLfsPointers(params: {
144
+ cwd: string;
145
+ blobs: readonly ChangedBlob[];
146
+ env?: NodeJS.ProcessEnv;
147
+ }): LfsScan {
148
+ const {cwd, env} = params;
149
+ const pathOf = new Map<string, string>();
150
+ for (const b of params.blobs) {
151
+ if (!pathOf.has(b.oid)) pathOf.set(b.oid, b.path);
152
+ }
153
+ if (pathOf.size === 0) return {pointers: [], malformed: []};
154
+
155
+ const small: string[] = [];
156
+ const checks = git(
157
+ ['cat-file', '--batch-check=%(objectname) %(objecttype) %(objectsize)'],
158
+ cwd,
159
+ {env, input: [...pathOf.keys()].join('\n') + '\n'},
160
+ )
161
+ .trim()
162
+ .split('\n');
163
+ for (const line of checks) {
164
+ const [oid, type, size] = line.split(' ');
165
+ const n = Number(size);
166
+ if (type === 'blob' && n > 0 && n <= LFS_POINTER_MAX_BYTES) small.push(oid);
167
+ }
168
+ if (small.length === 0) return {pointers: [], malformed: []};
169
+
170
+ const out = gitBytes(['cat-file', '--batch'], cwd, {
171
+ env,
172
+ input: small.join('\n') + '\n',
173
+ });
174
+ const pointers: FoundPointer[] = [];
175
+ const malformed: string[] = [];
176
+ let at = 0;
177
+ for (let i = 0; i < small.length; i++) {
178
+ const eol = out.indexOf(0x0a, at);
179
+ if (eol === -1) throw new Error('git cat-file --batch: truncated output');
180
+ const [oid, type, size] = out
181
+ .subarray(at, eol)
182
+ .toString('latin1')
183
+ .split(' ');
184
+ const n = Number(size);
185
+ if (type !== 'blob' || !Number.isSafeInteger(n)) {
186
+ throw new Error(`git cat-file --batch: unexpected header for ${oid}`);
187
+ }
188
+ const content = out.subarray(eol + 1, eol + 1 + n);
189
+ at = eol + 1 + n + 1;
190
+ const path = pathOf.get(oid) ?? oid;
191
+ const pointer = parseLfsPointer(content);
192
+ if (pointer !== undefined) {
193
+ pointers.push({...pointer, path});
194
+ } else if (looksLikeLfsPointer(content)) {
195
+ malformed.push(path);
196
+ }
197
+ }
198
+ return {pointers, malformed};
199
+ }
200
+
201
+ /**
202
+ * The regular-file blobs `commits` add or change against their first parent
203
+ * (a root commit against the empty tree). Deletions carry no blob.
204
+ */
205
+ export function changedFileBlobs(
206
+ cwd: string,
207
+ commits: readonly string[],
208
+ env?: NodeJS.ProcessEnv,
209
+ ): ChangedBlob[] {
210
+ if (commits.length === 0) return [];
211
+ const out = git(
212
+ [
213
+ '-c',
214
+ 'diff.renames=false',
215
+ 'diff-tree',
216
+ '--stdin',
217
+ '--root',
218
+ '-r',
219
+ '-z',
220
+ '--raw',
221
+ '--no-renames',
222
+ '--no-commit-id',
223
+ ],
224
+ cwd,
225
+ {env, input: commits.join('\n') + '\n'},
226
+ );
227
+ const fields = out.split('\0');
228
+ const blobs: ChangedBlob[] = [];
229
+ for (let i = 0; i + 1 < fields.length; i += 2) {
230
+ const meta = fields[i];
231
+ if (!meta.startsWith(':')) {
232
+ // `--stdin` may echo a commit id line; skip it.
233
+ i -= 1;
234
+ continue;
235
+ }
236
+ const [, newMode, , newOid, status] = meta.slice(1).split(' ');
237
+ if (status === 'D' || !isRegularFileMode(newMode)) continue;
238
+ blobs.push({oid: newOid, path: fields[i + 1]});
239
+ }
240
+ return blobs;
241
+ }
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // The local object store
245
+ // ---------------------------------------------------------------------------
246
+
247
+ /**
248
+ * The repository's local LFS object directory: `<storage>/objects`, where the
249
+ * storage is `lfs.storage` (relative to the git directory) or `<git dir>/lfs`.
250
+ */
251
+ export function localLfsObjectsDir(
252
+ repo: string,
253
+ env?: NodeJS.ProcessEnv,
254
+ ): string {
255
+ const common = git(
256
+ ['rev-parse', '--path-format=absolute', '--git-common-dir'],
257
+ repo,
258
+ {env},
259
+ ).trim();
260
+ const custom = run('git', ['config', '--get', 'lfs.storage'], repo, {
261
+ env,
262
+ }).stdout.trim();
263
+ const storage =
264
+ custom === ''
265
+ ? join(common, 'lfs')
266
+ : isAbsolute(custom)
267
+ ? custom
268
+ : resolve(common, custom);
269
+ return join(storage, 'objects');
270
+ }
271
+
272
+ /** Where the store keeps `oid`: `<objects>/<oid[0:2]>/<oid[2:4]>/<oid>`. */
273
+ export function lfsObjectPath(objectsDir: string, oid: string): string {
274
+ return join(objectsDir, oid.slice(0, 2), oid.slice(2, 4), oid);
275
+ }
276
+
277
+ // ---------------------------------------------------------------------------
278
+ // Agent side
279
+ // ---------------------------------------------------------------------------
280
+
281
+ /**
282
+ * Copy into `<dir>/lfs/<oid>` the object of every pointer the commits
283
+ * `<ref> ^<baseSha>` add or change, from `repo`'s local store. An object the
284
+ * store lacks is left out: the apply job then rejects the handoff naming it
285
+ * (that is also the fate of a text file that parses as a pointer). Refuses
286
+ * ({@link HandoffRejected} `size`) objects over the LFS limit together.
287
+ * Returns the oids left out.
288
+ */
289
+ export function writeLfsObjects(params: {
290
+ dir: string;
291
+ repo: string;
292
+ ref: string;
293
+ baseSha: string;
294
+ env?: NodeJS.ProcessEnv;
295
+ }): {missing: string[]} {
296
+ const {dir, repo, env} = params;
297
+ const commits = git(['rev-list', params.ref, `^${params.baseSha}`], repo, {
298
+ env,
299
+ })
300
+ .split('\n')
301
+ .filter((l) => l !== '');
302
+ const {pointers} = scanLfsPointers({
303
+ cwd: repo,
304
+ blobs: changedFileBlobs(repo, commits, env),
305
+ env,
306
+ });
307
+ const oids = [...new Set(pointers.map((p) => p.oid))].sort();
308
+ if (oids.length === 0) return {missing: []};
309
+
310
+ const store = localLfsObjectsDir(repo, env);
311
+ const present: string[] = [];
312
+ const missing: string[] = [];
313
+ let total = 0;
314
+ for (const oid of oids) {
315
+ const src = lfsObjectPath(store, oid);
316
+ if (!existsSync(src) || !statSync(src).isFile()) {
317
+ missing.push(oid);
318
+ continue;
319
+ }
320
+ total += statSync(src).size;
321
+ present.push(oid);
322
+ }
323
+ const limit = handoffByteLimits().lfsBytes;
324
+ if (total > limit) {
325
+ reject('size', `the LFS objects are over ${limit} bytes`);
326
+ }
327
+ if (present.length > 0) {
328
+ mkdirSync(join(dir, LFS_ARTIFACT_DIR), {recursive: true});
329
+ for (const oid of present) {
330
+ copyFileSync(lfsObjectPath(store, oid), join(dir, LFS_ARTIFACT_DIR, oid));
331
+ }
332
+ }
333
+ return {missing};
334
+ }
335
+
336
+ // ---------------------------------------------------------------------------
337
+ // Apply side
338
+ // ---------------------------------------------------------------------------
339
+
340
+ /** An object file of the artifact (`lfs/<oid>`, already a named regular file). */
341
+ export interface LfsArtifactObject {
342
+ oid: string;
343
+ path: string;
344
+ size: number;
345
+ }
346
+
347
+ function q(path: string): string {
348
+ return JSON.stringify(path.slice(0, 300));
349
+ }
350
+
351
+ /**
352
+ * Stream a regular file (refusing a symlink at open time) through sha256,
353
+ * reading at most `max + 1` bytes; optionally copy every byte read to `outFd`.
354
+ */
355
+ function hashFile(
356
+ path: string,
357
+ max: number,
358
+ outFd?: number,
359
+ ): {sha256: string; size: number} {
360
+ let fd: number;
361
+ try {
362
+ fd = openSync(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
363
+ } catch {
364
+ reject('symlink', `${q(path)} could not be opened as a regular file`);
365
+ }
366
+ try {
367
+ if (!fstatSync(fd).isFile()) {
368
+ reject('layout', `${q(path)} is not a regular file`);
369
+ }
370
+ const hash = createHash('sha256');
371
+ const buf = Buffer.alloc(1024 * 1024);
372
+ let size = 0;
373
+ while (size <= max) {
374
+ const n = readSync(fd, buf, 0, buf.length, null);
375
+ if (n === 0) break;
376
+ hash.update(buf.subarray(0, n));
377
+ if (outFd !== undefined) writeSync(outFd, buf, 0, n);
378
+ size += n;
379
+ }
380
+ return {sha256: hash.digest('hex'), size};
381
+ } finally {
382
+ closeSync(fd);
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Check the artifact's LFS objects against the pointers the new commits carry
388
+ * (decision 6). Rejects ({@link HandoffRejected}): a blob that looks like a
389
+ * pointer but does not parse strictly (`lfs-pointer`); two pointers giving one
390
+ * oid different sizes, or an object whose size or hash differs from its
391
+ * pointer (`lfs-mismatch`); a pointer whose object is not in `lfs/`
392
+ * (`lfs-missing`); an object no pointer references (`lfs-extra`); a pointer, or
393
+ * all of them together, over the LFS limit (`size`). Returns the referenced
394
+ * objects, sorted by oid.
395
+ */
396
+ export function checkLfsObjects(
397
+ scan: LfsScan,
398
+ objects: readonly LfsArtifactObject[],
399
+ ): LfsArtifactObject[] {
400
+ if (scan.malformed.length > 0) {
401
+ reject(
402
+ 'lfs-pointer',
403
+ `the file ${q(scan.malformed[0])} looks like a Git LFS pointer but is not a ` +
404
+ 'strict spec v1 pointer (version, oid sha256:<64 hex>, size, in order, nothing else)',
405
+ );
406
+ }
407
+ const limit = handoffByteLimits().lfsBytes;
408
+ const want = new Map<string, FoundPointer>();
409
+ let total = 0;
410
+ for (const p of scan.pointers) {
411
+ if (p.size > limit) {
412
+ reject(
413
+ 'size',
414
+ `the LFS pointer ${q(p.path)} names a ${p.size}-byte object, over the ` +
415
+ `LFS limit of ${limit} bytes`,
416
+ );
417
+ }
418
+ const seen = want.get(p.oid);
419
+ if (seen !== undefined) {
420
+ if (seen.size !== p.size) {
421
+ reject(
422
+ 'lfs-mismatch',
423
+ `the LFS pointers ${q(seen.path)} and ${q(p.path)} give the object ` +
424
+ `${p.oid} different sizes`,
425
+ );
426
+ }
427
+ continue;
428
+ }
429
+ want.set(p.oid, p);
430
+ total += p.size;
431
+ }
432
+ if (total > limit) {
433
+ reject('size', `the LFS objects are over ${limit} bytes`);
434
+ }
435
+
436
+ const byOid = new Map(objects.map((o) => [o.oid, o]));
437
+ for (const o of objects) {
438
+ if (!want.has(o.oid)) {
439
+ reject(
440
+ 'lfs-extra',
441
+ `lfs/${o.oid} is referenced by no LFS pointer of the new commits`,
442
+ );
443
+ }
444
+ }
445
+ const referenced: LfsArtifactObject[] = [];
446
+ for (const [oid, p] of [...want].sort(([a], [b]) => a.localeCompare(b))) {
447
+ const obj = byOid.get(oid);
448
+ if (obj === undefined) {
449
+ reject(
450
+ 'lfs-missing',
451
+ `the LFS object ${oid} of ${q(p.path)} is not in lfs/ (a text file ` +
452
+ 'that parses as an LFS pointer counts as one)',
453
+ );
454
+ }
455
+ const got = hashFile(obj.path, p.size);
456
+ if (got.size !== p.size) {
457
+ reject(
458
+ 'lfs-mismatch',
459
+ `lfs/${oid} is ${got.size > p.size ? 'over ' : ''}${got.size} bytes, but ` +
460
+ `its pointer ${q(p.path)} says ${p.size} bytes`,
461
+ );
462
+ }
463
+ if (got.sha256 !== oid) {
464
+ reject(
465
+ 'lfs-mismatch',
466
+ `lfs/${oid} does not hash to its oid (its sha256 hash is ${got.sha256})`,
467
+ );
468
+ }
469
+ referenced.push(obj);
470
+ }
471
+ return referenced;
472
+ }
473
+
474
+ /**
475
+ * Stage the validated objects into `cwd`'s own LFS store and push them to
476
+ * `arbiter` with `git lfs push --object-id`, BEFORE any ref is pushed. `cwd`
477
+ * is the apply job's checkout of the trusted base, so `.lfsconfig` and the
478
+ * endpoint come from `main`. Every byte staged is hashed again as it is copied
479
+ * (a file that changed since validation rejects, `lfs-mismatch`); an object
480
+ * the store already holds is not copied. The upload itself is the write seam
481
+ * `refWrite.pushLfsObjects`. Returns the push's status and stderr.
482
+ */
483
+ export async function pushLfsObjects(params: {
484
+ cwd: string;
485
+ arbiter: string;
486
+ objects: readonly LfsArtifactObject[];
487
+ env?: NodeJS.ProcessEnv;
488
+ }): Promise<{status: number; stderr: string}> {
489
+ const {cwd, arbiter, objects, env} = params;
490
+ if (objects.length === 0) return {status: 0, stderr: ''};
491
+ const store = localLfsObjectsDir(cwd, env);
492
+ for (const o of objects) stageObject(store, o);
493
+ const r = await refWrite.pushLfsObjects({
494
+ arbiter,
495
+ oids: objects.map((o) => o.oid),
496
+ cwd,
497
+ env,
498
+ });
499
+ return {status: r.status, stderr: r.stderr};
500
+ }
501
+
502
+ function stageObject(store: string, o: LfsArtifactObject): void {
503
+ const dest = lfsObjectPath(store, o.oid);
504
+ if (existsSync(dest)) return;
505
+ mkdirSync(dirname(dest), {recursive: true});
506
+ const tmp = `${dest}.dorfl-${process.pid}.tmp`;
507
+ const out = openSync(tmp, 'w', 0o644);
508
+ let got: {sha256: string; size: number};
509
+ try {
510
+ got = hashFile(o.path, o.size, out);
511
+ } finally {
512
+ closeSync(out);
513
+ }
514
+ if (got.size !== o.size || got.sha256 !== o.oid) {
515
+ rmSync(tmp, {force: true});
516
+ reject('lfs-mismatch', `lfs/${o.oid} changed after it was validated`);
517
+ }
518
+ renameSync(tmp, dest);
519
+ }