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
@@ -13,7 +13,9 @@ import {
13
13
  type SurfacePersistOptions,
14
14
  type SurfacePersistResult,
15
15
  } from './surface-persist.js';
16
+ import {parseWorkBranchRef} from './slug-namespace.js';
16
17
  import {workItemRel, type WorkFolderKey} from './work-layout.js';
18
+ import type {LockEntry} from './item-lock.js';
17
19
 
18
20
  /**
19
21
  * The **MERGE-QUESTION SURFACER** (spec `land-time-reverify-and-parallel-merge-ceiling`,
@@ -37,17 +39,30 @@ import {workItemRel, type WorkFolderKey} from './work-layout.js';
37
39
  *
38
40
  * Layered like the spec asks: the FLOOR (git-alone reachability) is the
39
41
  * authoritative enumerator — it works against a bare `--bare` arbiter with
40
- * `NoneProvider`. The CEILING (`gh pr list` PR metadata) is pure ENRICHMENT
41
- * layered on top when a GitHub host is configured; the surfacer functions
42
- * identically without it.
42
+ * `NoneProvider`. The CEILING (`gh pr list`) runs when a GitHub host is
43
+ * configured, and there it FILTERS (task
44
+ * `wire-merge-questions-into-the-advance-tick`, decision 1): an open PR already
45
+ * IS the human's land decision (merge or close it), so a branch with an open PR
46
+ * gets no merge question. Merge questions are for branches with no PR: the
47
+ * git-alone floor, or a branch whose PR was closed unmerged.
43
48
  *
44
- * OUT OF SCOPE for this task (covered by sibling tasks of the same spec):
49
+ * Which branches it asks about (decisions 1, 4 and 6 of that task), in order:
45
50
  *
46
- * - The APPLY rung dispatch that lands an answered `kind: merge` through the
47
- * land primitive (rebase → re-verify → advance). Task
48
- * `apply-rung-merge-disposition`.
49
- * - The GATE axis that decides whether this surfacer runs at all on a given
50
- * advance tick. Task `merge-questions-gate-axis`.
51
+ * - the task body rests in `tasks/ready/` or `tasks/backlog/` (a body in
52
+ * `done/` or `cancelled/` is terminal, which also covers a squash-merged PR
53
+ * whose branch was not deleted);
54
+ * - the sidecar carries no merge question yet (pending or answered);
55
+ * - no open PR (GitHub only; an unreadable PR list asks nothing);
56
+ * - the item's lock is not held, or is held WITH the `propose-pr` marker (a
57
+ * finished propose build keeps it until its work lands); a lock held without
58
+ * it is a live build, including the rebuild of a bounced task's kept branch;
59
+ * - the branch tip carries the done-move (`work/tasks/done/<slug>.md`), the
60
+ * state the answered-merge land (the committed-recovery tail) lands. A
61
+ * bounced build's kept work stops before its done-move, so it is not asked
62
+ * about.
63
+ *
64
+ * WIRED by `merge-question-tick.ts` (the laptop bare `advance` and the CI
65
+ * `surface-merge-questions` job), behind the `mergeQuestions: off | ask` gate.
51
66
  *
52
67
  * Tests inject the two seams ({@link listUnmergedWorkBranches},
53
68
  * {@link listOpenPullRequests}) so they NEVER hit real GitHub — the floor uses
@@ -55,11 +70,11 @@ import {workItemRel, type WorkFolderKey} from './work-layout.js';
55
70
  * `gh pr list` stub.
56
71
  */
57
72
 
58
- /** A `work/<slug>` branch whose tip is not reachable from `<base>`. */
73
+ /** A task build branch (`work/task-<slug>`) whose tip is not reachable from `<base>`. */
59
74
  export interface UnmergedWorkBranch {
60
- /** The full short ref, e.g. `work/foo`. */
75
+ /** The branch name as it is on the arbiter, e.g. `work/task-foo`. */
61
76
  ref: string;
62
- /** The bare slug (`work/` prefix stripped), e.g. `foo`. */
77
+ /** The bare task slug, parsed by `parseWorkBranchRef`, e.g. `foo`. */
63
78
  slug: string;
64
79
  /** Branch tip SHA (advisory). */
65
80
  sha?: string;
@@ -77,6 +92,12 @@ export interface MergeQuestionPullRequest {
77
92
  export interface ListUnmergedInput {
78
93
  cwd: string;
79
94
  base: string;
95
+ /**
96
+ * The arbiter's remote NAME in `cwd` (default `origin`). In a clone the
97
+ * arbiter's branches are read from `refs/remotes/<arbiter>/`; ignored in the
98
+ * hub mirror, whose local heads ARE the arbiter's branches.
99
+ */
100
+ arbiter?: string;
80
101
  env?: NodeJS.ProcessEnv;
81
102
  }
82
103
 
@@ -99,6 +120,12 @@ export interface SurfaceMergeQuestionsOptions {
99
120
  arbiterUrl?: string;
100
121
  /** The base branch reachability is checked against. Default `main`. */
101
122
  base?: string;
123
+ /**
124
+ * The arbiter's remote NAME in `cwd` (default `origin`), whose
125
+ * remote-tracking refs the FLOOR reads in a clone. See
126
+ * {@link listUnmergedWorkBranchesViaGit}.
127
+ */
128
+ arbiter?: string;
102
129
  /** The `gh` CLI binary to invoke for the ceiling. Default `gh` on PATH. */
103
130
  ghBin?: string;
104
131
  /** Environment for spawned git/gh processes. */
@@ -121,6 +148,12 @@ export interface SurfaceMergeQuestionsOptions {
121
148
  ) => Map<string, MergeQuestionPullRequest>;
122
149
  /** Seam: persist a single merge-question. Defaults to {@link persistSurfacedQuestions}. */
123
150
  persist?: (options: SurfacePersistOptions) => SurfacePersistResult;
151
+ /**
152
+ * The per-item locks held on the arbiter (`listItemLockEntries`), read by the
153
+ * caller. A task whose lock is held WITHOUT the `propose-pr` marker is a live
154
+ * build and is skipped (`lock-held`). Omitted ⇒ no lock is held.
155
+ */
156
+ locks?: readonly LockEntry[];
124
157
  }
125
158
 
126
159
  /** One surfaced merge-question (a row in the result). */
@@ -129,14 +162,12 @@ export interface MergeQuestionSurfaced {
129
162
  item: string;
130
163
  /** The bare slug. */
131
164
  slug: string;
132
- /** The `work/<slug>` ref the question is about. */
165
+ /** The `work/task-<slug>` branch the question is about. */
133
166
  ref: string;
134
167
  /** The sidecar path the persist touched (repo-relative). */
135
168
  sidecarPath: string;
136
169
  /** The persist commit (`undefined` if the persist was a no-op). */
137
170
  commit?: string;
138
- /** PR url when a GitHub ceiling matched the branch (advisory). */
139
- prUrl?: string;
140
171
  }
141
172
 
142
173
  /**
@@ -152,7 +183,22 @@ export interface MergeQuestionSkipped {
152
183
  ref: string;
153
184
  slug: string;
154
185
  /** PROVISIONAL vocabulary — see {@link MergeQuestionSkipped}. */
155
- reason: 'no-item-body' | 'already-pending-merge-question' | 'persist-nothing';
186
+ reason:
187
+ | 'no-item-body'
188
+ /** The body rests in `tasks/done/` or `tasks/cancelled/`. */
189
+ | 'terminal'
190
+ | 'already-pending-merge-question'
191
+ /** An answered merge question awaits its apply. */
192
+ | 'merge-question-answered'
193
+ /** The branch has an open PR (GitHub): the PR is the land decision. */
194
+ | 'open-pr'
195
+ /** GitHub, but the open PRs could not be listed: nothing is asked. */
196
+ | 'pr-state-unknown'
197
+ /** The item's lock is held without the `propose-pr` marker (a live build). */
198
+ | 'lock-held'
199
+ /** The branch tip does not carry the task's done-move. */
200
+ | 'no-done-move'
201
+ | 'persist-nothing';
156
202
  }
157
203
 
158
204
  /** Aggregate result of one surfacer pass. */
@@ -163,6 +209,14 @@ export interface SurfaceMergeQuestionsResult {
163
209
  surfaced: MergeQuestionSurfaced[];
164
210
  /** The branches that were considered but skipped, with the reason. */
165
211
  skipped: MergeQuestionSkipped[];
212
+ /**
213
+ * Set when the arbiter is GitHub-shaped but its open PRs could not be listed
214
+ * (`gh pr list` failed: no credential, no `gh`, an outage). The pass then
215
+ * asks about no branch (`pr-state-unknown`); a caller reports it as a FAILED
216
+ * pass rather than a quiet one (the `surface-merge-questions` command exits
217
+ * non-zero). Absent when the listing succeeded or was not needed.
218
+ */
219
+ prListingError?: string;
166
220
  }
167
221
 
168
222
  /** Raised for fatal usage errors (a missing repo, etc.). */
@@ -174,21 +228,18 @@ export class MergeQuestionSurfacerError extends Error {
174
228
  }
175
229
 
176
230
  /**
177
- * The lifecycle folders {@link findTaskItemPath} scans for a task body to flip
178
- * `needsAnswers:true` on. This set DELIBERATELY DIVERGES from `advance.ts`'s
179
- * `FOLDERS_FOR_TYPE.task` (`['tasks-backlog','tasks-ready','in-progress','done']`):
180
- *
181
- * - OMITS `in-progress` (and `needs-attention`): an unmerged `work/<slug>`
182
- * branch whose body is mid-build should NOT trigger a merge-question — the
183
- * build is still active, and surfacing a land-decision now would race the
184
- * builder. Such tasks fall through to the `no-item-body` skip.
185
- * - ADDS `cancelled`: a cancelled task with a lingering unmerged `work/*`
186
- * branch SHOULD surface a merge-question so the operator explicitly decides
187
- * whether to merge the branch or drop it.
231
+ * The lifecycle folders {@link findTaskItemPath} asks about (task
232
+ * `wire-merge-questions-into-the-advance-tick`, decision 6): the pool and
233
+ * staging. `in-progress` / `needs-attention` no longer exist as folders (a
234
+ * build in flight is a held lock, filtered separately).
188
235
  */
189
236
  const TASK_FOLDERS: readonly WorkFolderKey[] = [
190
237
  'tasks-ready',
191
238
  'tasks-backlog',
239
+ ] as const;
240
+
241
+ /** A body in one of these is TERMINAL: its branch is never asked about. */
242
+ const TERMINAL_TASK_FOLDERS: readonly WorkFolderKey[] = [
192
243
  'done',
193
244
  'cancelled',
194
245
  ] as const;
@@ -197,9 +248,10 @@ const TASK_FOLDERS: readonly WorkFolderKey[] = [
197
248
  * Surface merge-questions for every unmerged `work/*` branch in `cwd`.
198
249
  *
199
250
  * The FLOOR (git reachability) is authoritative; the CEILING (`gh pr list`)
200
- * runs only when {@link isGitHubArbiterUrl} accepts the arbiter URL, and is
201
- * pure enrichment (a missing/failed `gh` degrades silently — the floor still
202
- * surfaces the question).
251
+ * runs only when {@link isGitHubArbiterUrl} accepts the arbiter URL. On GitHub
252
+ * it is load-bearing (an open PR is the land decision), so a failed listing
253
+ * asks about NO branch and is reported on
254
+ * {@link SurfaceMergeQuestionsResult.prListingError}.
203
255
  *
204
256
  * Idempotency: a branch whose sidecar ALREADY carries a PENDING `kind: merge`
205
257
  * entry is SKIPPED (with `already-pending-merge-question`). A branch with no
@@ -219,11 +271,15 @@ export function surfaceMergeQuestions(
219
271
 
220
272
  const listBranches =
221
273
  options.listUnmergedWorkBranches ?? listUnmergedWorkBranchesViaGit;
222
- const branches = listBranches({cwd, base, env});
274
+ const branches = listBranches({cwd, base, arbiter: options.arbiter, env});
223
275
 
224
276
  // CEILING: only consult `gh pr list` when the arbiter is GitHub-shaped. A
225
277
  // bare / non-GitHub arbiter never spawns `gh` (the floor is sufficient).
226
- let prs: Map<string, MergeQuestionPullRequest> = new Map();
278
+ // On GitHub an open PR is the land decision, so a branch with one is not
279
+ // asked about; a failed listing asks about NOTHING this pass (a question on
280
+ // a branch with an open PR would land it behind the PR's back).
281
+ let prs: Map<string, MergeQuestionPullRequest> | undefined = new Map();
282
+ let prListingError: string | undefined;
227
283
  const ghEnabled =
228
284
  options.arbiterUrl !== undefined && isGitHubArbiterUrl(options.arbiterUrl);
229
285
  if (ghEnabled && branches.length > 0) {
@@ -236,52 +292,92 @@ export function surfaceMergeQuestions(
236
292
  env,
237
293
  });
238
294
  } catch (err) {
239
- // `gh` is the ENRICHMENT layer — a failure must NEVER suppress the
240
- // floor's surfacing. Note it and proceed with an empty PR map.
241
295
  const detail = err instanceof Error ? err.message : String(err);
242
296
  note(
243
- `merge-question surfacer: gh pr list failed (${detail}) — continuing with the git-only floor.`,
297
+ `merge-question surfacer: gh pr list failed (${detail}); asking about no branch this pass.`,
244
298
  );
245
- prs = new Map();
299
+ prs = undefined;
300
+ prListingError = detail;
301
+ }
302
+ }
303
+
304
+ // The item locks by task slug (`task-<slug>` entries only).
305
+ const locks = new Map<string, LockEntry>();
306
+ for (const lock of options.locks ?? []) {
307
+ if (lock.entry.startsWith('task-')) {
308
+ locks.set(lock.entry.slice('task-'.length), lock);
246
309
  }
247
310
  }
248
311
 
249
312
  const surfaced: MergeQuestionSurfaced[] = [];
250
313
  const skipped: MergeQuestionSkipped[] = [];
314
+ const skip = (
315
+ branch: UnmergedWorkBranch,
316
+ reason: MergeQuestionSkipped['reason'],
317
+ ): void => {
318
+ skipped.push({ref: branch.ref, slug: branch.slug, reason});
319
+ };
320
+
321
+ // The `main` every question of this pass is asked against: recorded on the
322
+ // entry so a `strictMergeApproval` apply can tell whether `main` moved since
323
+ // (task `strict-merge-approval-restale-check-runs-before-the-continue-rebase`).
324
+ // Unresolvable (a test seam over a non-repo) ⇒ no base recorded.
325
+ const askedAtMain =
326
+ branches.length > 0 ? resolveCommit(cwd, base, env) : undefined;
251
327
 
252
328
  for (const branch of branches) {
253
329
  const item = `task:${branch.slug}`;
254
330
 
255
- // Idempotency: if the sidecar already carries a PENDING `kind: merge`
256
- // entry, this surfacer must not append a duplicate. (An ANSWERED merge
257
- // entry is fine — apply will dispatch it; appending a new entry would
258
- // re-pause the sidecar unnecessarily.)
259
- if (alreadyHasPendingMergeQuestion(cwd, item)) {
260
- skipped.push({
261
- ref: branch.ref,
262
- slug: branch.slug,
263
- reason: 'already-pending-merge-question',
264
- });
265
- continue;
266
- }
267
-
268
331
  const itemPath = findTaskItemPath(cwd, branch.slug);
269
332
  if (itemPath === undefined) {
333
+ // A terminal body (done / cancelled): its branch is finished business
334
+ // (a squash-merged PR whose branch was not deleted rests here too).
335
+ if (isTerminalTask(cwd, branch.slug)) {
336
+ skip(branch, 'terminal');
337
+ continue;
338
+ }
270
339
  // The `branch:`/`ref:`-keyed sidecar identity (SPEC sidecar Q5-i, the
271
340
  // cross-cutting open question SHARED with the stuck-lock surfacer) is
272
- // OUT OF SCOPE for this task — without a body to flip `needsAnswers`
273
- // on, persist would tear the invariant. Skip with the reason so the
274
- // case is visible to the caller.
275
- skipped.push({
276
- ref: branch.ref,
277
- slug: branch.slug,
278
- reason: 'no-item-body',
279
- });
341
+ // OUT OF SCOPE: without a body to flip `needsAnswers` on, persist would
342
+ // tear the invariant. Skip with the reason so the case is visible.
343
+ skip(branch, 'no-item-body');
280
344
  continue;
281
345
  }
282
346
 
283
- const pr = prs.get(branch.ref);
284
- const question = buildMergeQuestion(branch, pr);
347
+ // Idempotency: never append a second merge question. A PENDING one is
348
+ // awaiting its answer; an ANSWERED one awaits its apply (appending would
349
+ // re-pause the sidecar and strand the answer).
350
+ const existing = existingMergeQuestion(cwd, item);
351
+ if (existing === 'pending') {
352
+ skip(branch, 'already-pending-merge-question');
353
+ continue;
354
+ }
355
+ if (existing === 'answered') {
356
+ skip(branch, 'merge-question-answered');
357
+ continue;
358
+ }
359
+
360
+ if (prs === undefined) {
361
+ skip(branch, 'pr-state-unknown');
362
+ continue;
363
+ }
364
+ if (prs.has(branch.ref)) {
365
+ skip(branch, 'open-pr');
366
+ continue;
367
+ }
368
+
369
+ const lock = locks.get(branch.slug);
370
+ if (lock !== undefined && lock.keptFor !== 'propose-pr') {
371
+ skip(branch, 'lock-held');
372
+ continue;
373
+ }
374
+
375
+ if (!tipHasDoneMove(cwd, branch, env)) {
376
+ skip(branch, 'no-done-move');
377
+ continue;
378
+ }
379
+
380
+ const question = buildMergeQuestion(branch, ghEnabled, askedAtMain);
285
381
 
286
382
  const result = persist({
287
383
  cwd,
@@ -292,11 +388,7 @@ export function surfaceMergeQuestions(
292
388
  note,
293
389
  });
294
390
  if (result.outcome === 'nothing') {
295
- skipped.push({
296
- ref: branch.ref,
297
- slug: branch.slug,
298
- reason: 'persist-nothing',
299
- });
391
+ skip(branch, 'persist-nothing');
300
392
  continue;
301
393
  }
302
394
 
@@ -306,11 +398,15 @@ export function surfaceMergeQuestions(
306
398
  ref: branch.ref,
307
399
  sidecarPath: result.sidecarPath,
308
400
  commit: result.commit,
309
- prUrl: pr?.url,
310
401
  });
311
402
  }
312
403
 
313
- return {considered: branches.length, surfaced, skipped};
404
+ return {
405
+ considered: branches.length,
406
+ surfaced,
407
+ skipped,
408
+ ...(prListingError === undefined ? {} : {prListingError}),
409
+ };
314
410
  }
315
411
 
316
412
  /**
@@ -328,23 +424,15 @@ export function surfaceMergeQuestions(
328
424
  */
329
425
  function buildMergeQuestion(
330
426
  branch: UnmergedWorkBranch,
331
- pr: MergeQuestionPullRequest | undefined,
427
+ ghEnabled: boolean,
428
+ askedAtMain: string | undefined,
332
429
  ): NewQuestion {
333
430
  const contextLines: string[] = [
334
431
  `The branch \`${branch.ref}\` is not reachable from \`main\` — it carries pushed work that has not yet landed.`,
432
+ ghEnabled
433
+ ? 'It has no open PR (none was opened, or its PR was closed without merging).'
434
+ : 'No host PR metadata available (git-alone floor: the branch is the source of truth).',
335
435
  ];
336
- if (pr !== undefined) {
337
- const titleSuffix = pr.title ? `: ${pr.title}` : '';
338
- const urlSuffix = pr.url ? ` (${pr.url})` : '';
339
- const stateSuffix = pr.state ? ` [${pr.state}]` : '';
340
- contextLines.push(
341
- `Open PR #${pr.number}${stateSuffix}${titleSuffix}${urlSuffix}.`,
342
- );
343
- } else {
344
- contextLines.push(
345
- `No host PR metadata available (git-alone floor — the branch is the source of truth).`,
346
- );
347
- }
348
436
  return {
349
437
  question: `Land \`${branch.ref}\`? An unmerged \`work/*\` branch is awaiting an integration decision.`,
350
438
  context: contextLines.join('\n'),
@@ -352,26 +440,83 @@ function buildMergeQuestion(
352
440
  default: 'merge | hold | drop',
353
441
  // MACHINE dispatch signal — the apply layer routes on this, not on `default`.
354
442
  kind: 'merge',
443
+ ...(askedAtMain === undefined ? {} : {askedAtMain}),
355
444
  };
356
445
  }
357
446
 
358
- /** Probe the sidecar for an existing pending `kind: merge` entry. */
359
- function alreadyHasPendingMergeQuestion(cwd: string, item: string): boolean {
447
+ /** The full sha `rev` names in `cwd`, or `undefined` when it does not resolve. */
448
+ function resolveCommit(
449
+ cwd: string,
450
+ rev: string,
451
+ env: NodeJS.ProcessEnv | undefined,
452
+ ): string | undefined {
453
+ let res: RunResult;
454
+ try {
455
+ res = gitSoft(
456
+ ['rev-parse', '--verify', '--quiet', `${rev}^{commit}`],
457
+ cwd,
458
+ env,
459
+ );
460
+ } catch {
461
+ return undefined;
462
+ }
463
+ if (res.status !== 0) return undefined;
464
+ const sha = res.stdout.trim();
465
+ return sha === '' ? undefined : sha;
466
+ }
467
+
468
+ /**
469
+ * Probe the sidecar for an existing `kind: merge` entry: `pending` when one is
470
+ * unanswered, `answered` when every one is answered, `undefined` when there is
471
+ * none.
472
+ */
473
+ function existingMergeQuestion(
474
+ cwd: string,
475
+ item: string,
476
+ ): 'pending' | 'answered' | undefined {
360
477
  const rel = sidecarPathFor(item);
361
478
  const abs = join(cwd, rel);
362
479
  if (!existsSync(abs)) {
363
- return false;
480
+ return undefined;
364
481
  }
365
482
  try {
366
- const model = parseSidecar(readFileSync(abs, 'utf8'));
367
- return model.entries.some((e) => e.kind === 'merge' && !isEntryAnswered(e));
483
+ const merge = parseSidecar(readFileSync(abs, 'utf8')).entries.filter(
484
+ (e) => e.kind === 'merge',
485
+ );
486
+ if (merge.length === 0) return undefined;
487
+ return merge.some((e) => !isEntryAnswered(e)) ? 'pending' : 'answered';
368
488
  } catch {
369
489
  // A malformed sidecar is not THIS surfacer's problem — pretend there is
370
- // no pending merge-question and let the persist write surface the issue.
371
- return false;
490
+ // no merge-question and let the persist write surface the issue.
491
+ return undefined;
372
492
  }
373
493
  }
374
494
 
495
+ /** Does the body of `slug` rest in a terminal task folder? */
496
+ function isTerminalTask(cwd: string, slug: string): boolean {
497
+ return TERMINAL_TASK_FOLDERS.some((folder) =>
498
+ existsSync(join(cwd, workItemRel(folder, `${slug}.md`))),
499
+ );
500
+ }
501
+
502
+ /**
503
+ * Does the branch tip carry the task's done-move (its body at
504
+ * `work/tasks/done/<slug>.md`)? The answered-merge land is the
505
+ * committed-recovery tail, which presupposes it. Read at the tip sha the
506
+ * listing gave; a branch listed without one is not asked about.
507
+ */
508
+ function tipHasDoneMove(
509
+ cwd: string,
510
+ branch: UnmergedWorkBranch,
511
+ env: NodeJS.ProcessEnv | undefined,
512
+ ): boolean {
513
+ if (branch.sha === undefined) return false;
514
+ const done = workItemRel('done', `${branch.slug}.md`);
515
+ return (
516
+ gitSoft(['cat-file', '-e', `${branch.sha}:${done}`], cwd, env).status === 0
517
+ );
518
+ }
519
+
375
520
  /** Locate the task body `work/<folder>/<slug>.md` across lifecycle folders. */
376
521
  function findTaskItemPath(cwd: string, slug: string): string | undefined {
377
522
  for (const folder of TASK_FOLDERS) {
@@ -394,19 +539,50 @@ function gitSoft(
394
539
  }
395
540
 
396
541
  /**
397
- * Production FLOOR: enumerate every `refs/heads/work/*` whose tip is NOT
398
- * reachable from `<base>`. A repo with no `<base>` (e.g. a fresh init before
399
- * the first commit) yields the empty list — the surfacer is a no-op until
400
- * `main` exists.
542
+ * Production FLOOR: enumerate the ARBITER's task build branches
543
+ * (`work/task-<slug>`, the {@link workBranchRef} form) whose tip is NOT
544
+ * reachable from the arbiter's `<base>`.
545
+ *
546
+ * WHERE the arbiter's refs live depends on the repo `cwd` is:
547
+ *
548
+ * - A **clone** (non-bare, with an `<arbiter>` remote): the arbiter's branches
549
+ * are the remote-tracking refs `refs/remotes/<arbiter>/work/*`, checked
550
+ * against `refs/remotes/<arbiter>/<base>`. Local `refs/heads/work/*` in a
551
+ * clone are this machine's own (possibly unpushed or stale) heads, not the
552
+ * arbiter's, so they are NOT listed.
553
+ * - The **hub mirror** (a `--bare` clone, or a worktree added from one, where
554
+ * `core.bare` reads `true`) or a repo with no `<arbiter>` remote (the repo IS
555
+ * the arbiter's ref store): the local heads `refs/heads/work/*`, checked
556
+ * against the local `<base>`.
557
+ *
558
+ * The item slug comes from {@link parseWorkBranchRef}, never from slicing the
559
+ * ref. Only the plain BUILD branch of a task (`namespace: 'task'`, no producer)
560
+ * is listed: an intake branch (`work/intake-*`) creates an item rather than
561
+ * building one, a spec branch (`work/spec-*`) carries tasking, and an
562
+ * un-namespaced `work/<slug>` is a pre-cutover ref `parseWorkBranchRef` refuses
563
+ * (the clean-break stance). None of those is what a `kind: merge` answer lands.
564
+ *
565
+ * Purely local: it reads refs already in `cwd` (no fetch, no `ls-remote`); the
566
+ * caller refreshes them. A repo whose `<base>` does not resolve (e.g. a fresh
567
+ * init before the first commit, or a clone that never fetched `<base>`) yields
568
+ * the empty list: without a base, "unmerged" is meaningless and every branch
569
+ * would over-surface.
401
570
  */
402
571
  export function listUnmergedWorkBranchesViaGit(
403
572
  input: ListUnmergedInput,
404
573
  ): UnmergedWorkBranch[] {
405
574
  const {cwd, base, env} = input;
406
- // Bail if `<base>` does not resolve — without a base, "unmerged" is
407
- // meaningless and we'd over-surface every branch as unmerged.
575
+ const arbiter = input.arbiter ?? 'origin';
576
+ const source = arbiterRefSource(cwd, arbiter, env);
577
+ const baseRef =
578
+ source === 'remote-tracking'
579
+ ? `refs/remotes/${arbiter}/${base}`
580
+ : `refs/heads/${base}`;
581
+ const branchPrefix =
582
+ source === 'remote-tracking' ? `refs/remotes/${arbiter}/` : 'refs/heads/';
583
+
408
584
  const haveBase = gitSoft(
409
- ['rev-parse', '--verify', '--quiet', base],
585
+ ['rev-parse', '--verify', '--quiet', `${baseRef}^{commit}`],
410
586
  cwd,
411
587
  env,
412
588
  );
@@ -416,8 +592,8 @@ export function listUnmergedWorkBranchesViaGit(
416
592
  const refs = gitSoft(
417
593
  [
418
594
  'for-each-ref',
419
- '--format=%(refname:short) %(objectname)',
420
- 'refs/heads/work/',
595
+ '--format=%(refname) %(objectname)',
596
+ `${branchPrefix}work/`,
421
597
  ],
422
598
  cwd,
423
599
  env,
@@ -435,35 +611,65 @@ export function listUnmergedWorkBranchesViaGit(
435
611
  if (space === -1) {
436
612
  continue;
437
613
  }
438
- const ref = line.slice(0, space);
614
+ const fullRef = line.slice(0, space);
439
615
  const sha = line.slice(space + 1).trim();
440
- if (!ref.startsWith('work/')) {
616
+ if (!fullRef.startsWith(branchPrefix)) {
441
617
  continue;
442
618
  }
443
- const slug = ref.slice('work/'.length);
444
- if (slug === '') {
619
+ // The branch name AS IT IS ON THE ARBITER (`work/task-<slug>`), whichever
620
+ // local namespace it was read from.
621
+ const ref = fullRef.slice(branchPrefix.length);
622
+ const parsed = parseWorkBranchRef(ref);
623
+ if (
624
+ parsed === undefined ||
625
+ parsed.namespace !== 'task' ||
626
+ parsed.producer !== undefined
627
+ ) {
445
628
  continue;
446
629
  }
447
- // `git merge-base --is-ancestor <sha> <base>` — exit 0 ⇒ reachable
630
+ // `git merge-base --is-ancestor <sha> <base>`: exit 0 ⇒ reachable
448
631
  // (merged), exit 1 ⇒ unmerged.
449
- const reach = gitSoft(['merge-base', '--is-ancestor', sha, base], cwd, env);
632
+ const reach = gitSoft(
633
+ ['merge-base', '--is-ancestor', sha, baseRef],
634
+ cwd,
635
+ env,
636
+ );
450
637
  if (reach.status !== 0) {
451
- unmerged.push({ref, slug, sha});
638
+ unmerged.push({ref, slug: parsed.slug, sha});
452
639
  }
453
640
  }
454
641
  return unmerged;
455
642
  }
456
643
 
644
+ /**
645
+ * Which local ref namespace holds the arbiter's branches in `cwd`: the local
646
+ * heads for the bare hub mirror (and its worktrees, which share its
647
+ * `core.bare = true` config) or for a repo with no `<arbiter>` remote; the
648
+ * remote-tracking refs for an ordinary clone.
649
+ */
650
+ function arbiterRefSource(
651
+ cwd: string,
652
+ arbiter: string,
653
+ env: NodeJS.ProcessEnv | undefined,
654
+ ): 'local-heads' | 'remote-tracking' {
655
+ const bare = gitSoft(['config', '--bool', 'core.bare'], cwd, env);
656
+ if (bare.status === 0 && bare.stdout.trim() === 'true') {
657
+ return 'local-heads';
658
+ }
659
+ const remote = gitSoft(['remote', 'get-url', arbiter], cwd, env);
660
+ return remote.status === 0 ? 'remote-tracking' : 'local-heads';
661
+ }
662
+
457
663
  /**
458
664
  * Production CEILING: shell `gh pr list --state open --json …` and index the
459
- * results by their `headRefName` so the surfacer can enrich the matching
460
- * branch's question. A non-zero / missing `gh` is treated as "no host
461
- * metadata available" — the floor still surfaces every unmerged branch.
665
+ * results by their `headRefName`, so the surfacer can skip a branch with an
666
+ * open PR. A non-zero `gh` (or output that is not a JSON array) THROWS: the
667
+ * surfacer then asks about no branch this pass, rather than asking about
668
+ * branches whose PR it cannot see.
462
669
  *
463
- * Best-effort enrichment. The git-reachability FLOOR is authoritative; the
464
- * `--state open`, `--base <base>`, and `--limit 200` arguments are DELIBERATE
465
- * ceilings — a PR targeting a non-`main` base (e.g. a stacked PR) or the case
466
- * of >200 open PRs degrades to floor-only output, never corrupts it.
670
+ * The `--state open`, `--base <base>`, and `--limit 200` arguments are
671
+ * DELIBERATE ceilings: a PR targeting a non-`main` base (e.g. a stacked PR) or
672
+ * the case of >200 open PRs is not seen, so its branch may still be asked about.
467
673
  */
468
674
  export function listOpenPullRequestsViaGh(
469
675
  input: ListPullRequestsInput,
@@ -487,16 +693,18 @@ export function listOpenPullRequestsViaGh(
487
693
  {env},
488
694
  );
489
695
  if (result.status !== 0) {
490
- return new Map();
696
+ throw new Error(
697
+ `${ghBin} pr list exited ${result.status}: ${result.stderr.trim().slice(0, 300)}`,
698
+ );
491
699
  }
492
700
  let parsed: unknown;
493
701
  try {
494
702
  parsed = JSON.parse(result.stdout);
495
703
  } catch {
496
- return new Map();
704
+ throw new Error(`${ghBin} pr list printed no JSON`);
497
705
  }
498
706
  if (!Array.isArray(parsed)) {
499
- return new Map();
707
+ throw new Error(`${ghBin} pr list printed no JSON array`);
500
708
  }
501
709
  const out = new Map<string, MergeQuestionPullRequest>();
502
710
  for (const raw of parsed) {