dorfl 0.1.2 → 0.2.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 (364) hide show
  1. package/dist/advance-drivers.d.ts +1 -1
  2. package/dist/advance-drivers.d.ts.map +1 -1
  3. package/dist/advance-drivers.js +15 -3
  4. package/dist/advance-drivers.js.map +1 -1
  5. package/dist/advance-lifecycle-template.d.ts.map +1 -1
  6. package/dist/advance-lifecycle-template.js +76 -4
  7. package/dist/advance-lifecycle-template.js.map +1 -1
  8. package/dist/advance-treeless-publish.d.ts +23 -0
  9. package/dist/advance-treeless-publish.d.ts.map +1 -1
  10. package/dist/advance-treeless-publish.js +41 -0
  11. package/dist/advance-treeless-publish.js.map +1 -1
  12. package/dist/advance.d.ts +59 -7
  13. package/dist/advance.d.ts.map +1 -1
  14. package/dist/advance.js +389 -82
  15. package/dist/advance.js.map +1 -1
  16. package/dist/advancing-lock.d.ts +30 -2
  17. package/dist/advancing-lock.d.ts.map +1 -1
  18. package/dist/advancing-lock.js +50 -0
  19. package/dist/advancing-lock.js.map +1 -1
  20. package/dist/agent-launch.d.ts +12 -0
  21. package/dist/agent-launch.d.ts.map +1 -1
  22. package/dist/agent-launch.js +22 -12
  23. package/dist/agent-launch.js.map +1 -1
  24. package/dist/agent-stop.d.ts +40 -2
  25. package/dist/agent-stop.d.ts.map +1 -1
  26. package/dist/agent-stop.js +30 -2
  27. package/dist/agent-stop.js.map +1 -1
  28. package/dist/apply-decide.d.ts +17 -3
  29. package/dist/apply-decide.d.ts.map +1 -1
  30. package/dist/apply-decide.js +36 -7
  31. package/dist/apply-decide.js.map +1 -1
  32. package/dist/apply-merge-action.d.ts +17 -5
  33. package/dist/apply-merge-action.d.ts.map +1 -1
  34. package/dist/apply-merge-action.js +43 -8
  35. package/dist/apply-merge-action.js.map +1 -1
  36. package/dist/apply-persist.d.ts +57 -26
  37. package/dist/apply-persist.d.ts.map +1 -1
  38. package/dist/apply-persist.js +166 -37
  39. package/dist/apply-persist.js.map +1 -1
  40. package/dist/apply-stuck-action.d.ts +151 -0
  41. package/dist/apply-stuck-action.d.ts.map +1 -0
  42. package/dist/apply-stuck-action.js +125 -0
  43. package/dist/apply-stuck-action.js.map +1 -0
  44. package/dist/brand.d.ts +12 -1
  45. package/dist/brand.d.ts.map +1 -1
  46. package/dist/brand.js +2 -1
  47. package/dist/brand.js.map +1 -1
  48. package/dist/cli-spinner.d.ts +1 -1
  49. package/dist/cli-spinner.js +1 -1
  50. package/dist/cli.d.ts +10 -0
  51. package/dist/cli.d.ts.map +1 -1
  52. package/dist/cli.js +320 -63
  53. package/dist/cli.js.map +1 -1
  54. package/dist/close-job-template.js +1 -1
  55. package/dist/complete.d.ts +6 -3
  56. package/dist/complete.d.ts.map +1 -1
  57. package/dist/complete.js +91 -37
  58. package/dist/complete.js.map +1 -1
  59. package/dist/config.d.ts +55 -9
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/config.js +56 -4
  62. package/dist/config.js.map +1 -1
  63. package/dist/continue-branch.d.ts.map +1 -1
  64. package/dist/continue-branch.js +22 -0
  65. package/dist/continue-branch.js.map +1 -1
  66. package/dist/decision-engine.d.ts +38 -10
  67. package/dist/decision-engine.d.ts.map +1 -1
  68. package/dist/decision-engine.js +10 -6
  69. package/dist/decision-engine.js.map +1 -1
  70. package/dist/do-autopick.d.ts.map +1 -1
  71. package/dist/do-autopick.js +11 -1
  72. package/dist/do-autopick.js.map +1 -1
  73. package/dist/do-config.d.ts +1 -1
  74. package/dist/do.d.ts +49 -56
  75. package/dist/do.d.ts.map +1 -1
  76. package/dist/do.js +419 -30
  77. package/dist/do.js.map +1 -1
  78. package/dist/env-config.d.ts.map +1 -1
  79. package/dist/env-config.js +12 -4
  80. package/dist/env-config.js.map +1 -1
  81. package/dist/failure-cause.d.ts +3 -2
  82. package/dist/failure-cause.d.ts.map +1 -1
  83. package/dist/failure-cause.js +28 -1
  84. package/dist/failure-cause.js.map +1 -1
  85. package/dist/format.d.ts +6 -6
  86. package/dist/format.d.ts.map +1 -1
  87. package/dist/format.js +10 -27
  88. package/dist/format.js.map +1 -1
  89. package/dist/frontmatter.d.ts +16 -4
  90. package/dist/frontmatter.d.ts.map +1 -1
  91. package/dist/frontmatter.js +27 -1
  92. package/dist/frontmatter.js.map +1 -1
  93. package/dist/gc.d.ts +65 -6
  94. package/dist/gc.d.ts.map +1 -1
  95. package/dist/gc.js +126 -9
  96. package/dist/gc.js.map +1 -1
  97. package/dist/github.d.ts +14 -0
  98. package/dist/github.d.ts.map +1 -1
  99. package/dist/github.js +73 -0
  100. package/dist/github.js.map +1 -1
  101. package/dist/harness.d.ts +25 -0
  102. package/dist/harness.d.ts.map +1 -1
  103. package/dist/harness.js.map +1 -1
  104. package/dist/index.d.ts +4 -1
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js +2 -1
  107. package/dist/index.js.map +1 -1
  108. package/dist/install-ci-branch-protection.d.ts +132 -38
  109. package/dist/install-ci-branch-protection.d.ts.map +1 -1
  110. package/dist/install-ci-branch-protection.js +190 -48
  111. package/dist/install-ci-branch-protection.js.map +1 -1
  112. package/dist/install-ci-core.d.ts +21 -1
  113. package/dist/install-ci-core.d.ts.map +1 -1
  114. package/dist/install-ci-core.js +10 -3
  115. package/dist/install-ci-core.js.map +1 -1
  116. package/dist/install-ci-github.d.ts +32 -0
  117. package/dist/install-ci-github.d.ts.map +1 -1
  118. package/dist/install-ci-github.js +73 -0
  119. package/dist/install-ci-github.js.map +1 -1
  120. package/dist/install-skills.d.ts +123 -0
  121. package/dist/install-skills.d.ts.map +1 -0
  122. package/dist/install-skills.js +100 -0
  123. package/dist/install-skills.js.map +1 -0
  124. package/dist/intake-trigger-template.js +1 -1
  125. package/dist/intake.d.ts +17 -4
  126. package/dist/intake.d.ts.map +1 -1
  127. package/dist/intake.js +23 -7
  128. package/dist/intake.js.map +1 -1
  129. package/dist/integration-core.d.ts +10 -7
  130. package/dist/integration-core.d.ts.map +1 -1
  131. package/dist/integration-core.js +174 -60
  132. package/dist/integration-core.js.map +1 -1
  133. package/dist/integrator.d.ts +1 -1
  134. package/dist/integrator.d.ts.map +1 -1
  135. package/dist/integrator.js +19 -3
  136. package/dist/integrator.js.map +1 -1
  137. package/dist/isolation.d.ts +2 -2
  138. package/dist/isolation.d.ts.map +1 -1
  139. package/dist/isolation.js +9 -0
  140. package/dist/isolation.js.map +1 -1
  141. package/dist/item-lock.d.ts +196 -129
  142. package/dist/item-lock.d.ts.map +1 -1
  143. package/dist/item-lock.js +329 -254
  144. package/dist/item-lock.js.map +1 -1
  145. package/dist/ledger-write.d.ts +31 -26
  146. package/dist/ledger-write.d.ts.map +1 -1
  147. package/dist/ledger-write.js +100 -122
  148. package/dist/ledger-write.js.map +1 -1
  149. package/dist/lifecycle-gather.d.ts +17 -0
  150. package/dist/lifecycle-gather.d.ts.map +1 -1
  151. package/dist/lifecycle-gather.js +7 -1
  152. package/dist/lifecycle-gather.js.map +1 -1
  153. package/dist/lifecycle-pools.d.ts +40 -3
  154. package/dist/lifecycle-pools.d.ts.map +1 -1
  155. package/dist/lifecycle-pools.js +26 -6
  156. package/dist/lifecycle-pools.js.map +1 -1
  157. package/dist/merge-question-surfacer.d.ts +15 -1
  158. package/dist/merge-question-surfacer.d.ts.map +1 -1
  159. package/dist/merge-question-surfacer.js +18 -3
  160. package/dist/merge-question-surfacer.js.map +1 -1
  161. package/dist/migrate-stuck-locks.d.ts +129 -0
  162. package/dist/migrate-stuck-locks.d.ts.map +1 -0
  163. package/dist/migrate-stuck-locks.js +355 -0
  164. package/dist/migrate-stuck-locks.js.map +1 -0
  165. package/dist/needs-attention.d.ts +293 -16
  166. package/dist/needs-attention.d.ts.map +1 -1
  167. package/dist/needs-attention.js +532 -57
  168. package/dist/needs-attention.js.map +1 -1
  169. package/dist/orphan-sidecar.d.ts +8 -4
  170. package/dist/orphan-sidecar.d.ts.map +1 -1
  171. package/dist/orphan-sidecar.js +35 -2
  172. package/dist/orphan-sidecar.js.map +1 -1
  173. package/dist/pi-harness.d.ts +16 -0
  174. package/dist/pi-harness.d.ts.map +1 -1
  175. package/dist/pi-harness.js +82 -2
  176. package/dist/pi-harness.js.map +1 -1
  177. package/dist/placement.d.ts +3 -3
  178. package/dist/placement.js +1 -1
  179. package/dist/prd-to-spec.d.ts.map +1 -1
  180. package/dist/prd-to-spec.js +9 -5
  181. package/dist/prd-to-spec.js.map +1 -1
  182. package/dist/protocol/CLAIM-PROTOCOL.md +17 -10
  183. package/dist/protocol/REVIEW-PROTOCOL.md +4 -1
  184. package/dist/protocol/SURFACE-PROTOCOL.md +16 -2
  185. package/dist/protocol/TASKING-PROTOCOL.md +3 -1
  186. package/dist/protocol/WORK-CONTRACT.md +22 -18
  187. package/dist/protocol/task-template.md +1 -1
  188. package/dist/reap-branches.d.ts +12 -9
  189. package/dist/reap-branches.d.ts.map +1 -1
  190. package/dist/reap-branches.js +25 -7
  191. package/dist/reap-branches.js.map +1 -1
  192. package/dist/recover-isolated.d.ts +12 -0
  193. package/dist/recover-isolated.d.ts.map +1 -1
  194. package/dist/recover-isolated.js +5 -0
  195. package/dist/recover-isolated.js.map +1 -1
  196. package/dist/repo-config.d.ts +23 -2
  197. package/dist/repo-config.d.ts.map +1 -1
  198. package/dist/repo-config.js +53 -7
  199. package/dist/repo-config.js.map +1 -1
  200. package/dist/repo-mirror.d.ts.map +1 -1
  201. package/dist/repo-mirror.js +18 -2
  202. package/dist/repo-mirror.js.map +1 -1
  203. package/dist/review-verdict.js +1 -1
  204. package/dist/review-verdict.js.map +1 -1
  205. package/dist/run.d.ts +1 -1
  206. package/dist/run.d.ts.map +1 -1
  207. package/dist/run.js +54 -19
  208. package/dist/run.js.map +1 -1
  209. package/dist/scan.d.ts +17 -8
  210. package/dist/scan.d.ts.map +1 -1
  211. package/dist/scan.js +22 -8
  212. package/dist/scan.js.map +1 -1
  213. package/dist/select-priority.d.ts +2 -2
  214. package/dist/select-priority.js +1 -1
  215. package/dist/select-priority.js.map +1 -1
  216. package/dist/sidecar-apply.js +1 -1
  217. package/dist/sidecar-apply.js.map +1 -1
  218. package/dist/sidecar.d.ts +45 -10
  219. package/dist/sidecar.d.ts.map +1 -1
  220. package/dist/sidecar.js +82 -2
  221. package/dist/sidecar.js.map +1 -1
  222. package/dist/skills/answer-questions/SKILL.md +89 -0
  223. package/dist/skills/capture-signal/SKILL.md +52 -0
  224. package/dist/skills/convert-from-prd-to-spec/SKILL.md +90 -0
  225. package/dist/skills/drive-tasks/SKILL.md +218 -0
  226. package/dist/skills/from-idea/SKILL.md +83 -0
  227. package/dist/skills/merge-prs/SKILL.md +70 -0
  228. package/dist/skills/orchestrate/SKILL.md +101 -0
  229. package/dist/skills/promote/SKILL.md +35 -0
  230. package/dist/skills/review/SKILL.md +16 -0
  231. package/dist/skills/setup/SKILL.md +258 -0
  232. package/dist/skills/setup/protocol/ADR-FORMAT.md +47 -0
  233. package/dist/skills/setup/protocol/CLAIM-PROTOCOL.md +224 -0
  234. package/dist/skills/setup/protocol/REVIEW-PROTOCOL.md +122 -0
  235. package/dist/skills/setup/protocol/SURFACE-PROTOCOL.md +135 -0
  236. package/dist/skills/setup/protocol/TASKING-PROTOCOL.md +124 -0
  237. package/dist/skills/setup/protocol/WORK-CONTRACT.md +280 -0
  238. package/dist/skills/setup/protocol/spec-template.md +71 -0
  239. package/dist/skills/setup/protocol/task-template.md +65 -0
  240. package/dist/skills/surface-questions/SKILL.md +16 -0
  241. package/dist/skills/to-spec/SKILL.md +34 -0
  242. package/dist/skills/to-task/SKILL.md +19 -0
  243. package/dist/skills/triage-observations/SKILL.md +78 -0
  244. package/dist/skills/work/SKILL.md +51 -0
  245. package/dist/slug-namespace.d.ts +5 -5
  246. package/dist/slug-namespace.js +5 -5
  247. package/dist/slug-namespace.js.map +1 -1
  248. package/dist/spec-complete.d.ts.map +1 -1
  249. package/dist/spec-complete.js +2 -3
  250. package/dist/spec-complete.js.map +1 -1
  251. package/dist/start.d.ts.map +1 -1
  252. package/dist/start.js +51 -57
  253. package/dist/start.js.map +1 -1
  254. package/dist/surface-gate.d.ts.map +1 -1
  255. package/dist/surface-gate.js +10 -3
  256. package/dist/surface-gate.js.map +1 -1
  257. package/dist/surface-persist.js +1 -1
  258. package/dist/surface-persist.js.map +1 -1
  259. package/dist/tasker-review-loop.d.ts +4 -3
  260. package/dist/tasker-review-loop.d.ts.map +1 -1
  261. package/dist/tasker-review-loop.js.map +1 -1
  262. package/dist/tasking-lock.d.ts +2 -2
  263. package/dist/tasking-lock.d.ts.map +1 -1
  264. package/dist/tasking-lock.js +53 -32
  265. package/dist/tasking-lock.js.map +1 -1
  266. package/dist/tasking.d.ts +40 -6
  267. package/dist/tasking.d.ts.map +1 -1
  268. package/dist/tasking.js +224 -12
  269. package/dist/tasking.js.map +1 -1
  270. package/dist/triage-persist.d.ts +10 -6
  271. package/dist/triage-persist.d.ts.map +1 -1
  272. package/dist/triage-persist.js +34 -4
  273. package/dist/triage-persist.js.map +1 -1
  274. package/dist/vendor/incur/agents.d.ts +58 -0
  275. package/dist/vendor/incur/agents.d.ts.map +1 -0
  276. package/dist/vendor/incur/agents.js +343 -0
  277. package/dist/vendor/incur/agents.js.map +1 -0
  278. package/dist/verify-workflow-template.js +1 -1
  279. package/dist/watch-session.d.ts +11 -3
  280. package/dist/watch-session.d.ts.map +1 -1
  281. package/dist/watch-session.js +94 -7
  282. package/dist/watch-session.js.map +1 -1
  283. package/dist/work-layout.d.ts +7 -3
  284. package/dist/work-layout.d.ts.map +1 -1
  285. package/dist/work-layout.js +6 -3
  286. package/dist/work-layout.js.map +1 -1
  287. package/dist/workspace.d.ts +1 -1
  288. package/package.json +2 -2
  289. package/src/advance-drivers.ts +16 -3
  290. package/src/advance-lifecycle-template.ts +89 -4
  291. package/src/advance-treeless-publish.ts +45 -0
  292. package/src/advance.ts +467 -87
  293. package/src/advancing-lock.ts +96 -1
  294. package/src/agent-launch.ts +37 -12
  295. package/src/agent-stop.ts +60 -2
  296. package/src/apply-decide.ts +36 -7
  297. package/src/apply-merge-action.ts +45 -7
  298. package/src/apply-persist.ts +228 -54
  299. package/src/apply-stuck-action.ts +260 -0
  300. package/src/brand.ts +14 -2
  301. package/src/cli-spinner.ts +1 -1
  302. package/src/cli.ts +401 -70
  303. package/src/close-job-template.ts +1 -1
  304. package/src/complete.ts +110 -52
  305. package/src/config.ts +107 -13
  306. package/src/continue-branch.ts +22 -0
  307. package/src/decision-engine.ts +55 -17
  308. package/src/do-autopick.ts +12 -1
  309. package/src/do-config.ts +1 -1
  310. package/src/do.ts +511 -38
  311. package/src/env-config.ts +12 -4
  312. package/src/failure-cause.ts +30 -1
  313. package/src/format.ts +10 -30
  314. package/src/frontmatter.ts +35 -5
  315. package/src/gc.ts +172 -9
  316. package/src/github.ts +78 -0
  317. package/src/harness.ts +25 -0
  318. package/src/index.ts +10 -0
  319. package/src/install-ci-branch-protection.ts +282 -57
  320. package/src/install-ci-core.ts +30 -3
  321. package/src/install-ci-github.ts +86 -0
  322. package/src/install-skills.ts +166 -0
  323. package/src/intake-trigger-template.ts +1 -1
  324. package/src/intake.ts +46 -11
  325. package/src/integration-core.ts +199 -65
  326. package/src/integrator.ts +22 -9
  327. package/src/isolation.ts +11 -2
  328. package/src/item-lock.ts +485 -341
  329. package/src/ledger-write.ts +135 -150
  330. package/src/lifecycle-gather.ts +29 -6
  331. package/src/lifecycle-pools.ts +67 -9
  332. package/src/merge-question-surfacer.ts +28 -4
  333. package/src/migrate-stuck-locks.ts +451 -0
  334. package/src/needs-attention.ts +857 -84
  335. package/src/orphan-sidecar.ts +47 -6
  336. package/src/pi-harness.ts +82 -2
  337. package/src/placement.ts +3 -3
  338. package/src/prd-to-spec.ts +13 -5
  339. package/src/reap-branches.ts +38 -14
  340. package/src/recover-isolated.ts +17 -0
  341. package/src/repo-config.ts +55 -6
  342. package/src/repo-mirror.ts +23 -1
  343. package/src/review-verdict.ts +1 -1
  344. package/src/run.ts +64 -18
  345. package/src/scan.ts +22 -8
  346. package/src/select-priority.ts +3 -3
  347. package/src/sidecar-apply.ts +1 -1
  348. package/src/sidecar.ts +138 -11
  349. package/src/slug-namespace.ts +7 -7
  350. package/src/spec-complete.ts +2 -3
  351. package/src/start.ts +53 -59
  352. package/src/surface-gate.ts +10 -3
  353. package/src/surface-persist.ts +1 -1
  354. package/src/tasker-review-loop.ts +4 -3
  355. package/src/tasking-lock.ts +52 -36
  356. package/src/tasking.ts +261 -17
  357. package/src/triage-persist.ts +56 -12
  358. package/src/vendor/incur/LICENSE +21 -0
  359. package/src/vendor/incur/README.md +19 -0
  360. package/src/vendor/incur/agents.ts +392 -0
  361. package/src/verify-workflow-template.ts +1 -1
  362. package/src/watch-session.ts +102 -7
  363. package/src/work-layout.ts +6 -3
  364. package/src/workspace.ts +1 -1
@@ -34,28 +34,44 @@
34
34
  * change can flip it knowingly; the spec calls this "pick the cheaper of the
35
35
  * two and record the choice."
36
36
  *
37
- * DEADLOCK GUARDa NEVER-RUN required check would block every merge (GitHub
38
- * treats an unreported required check as pending). install-ci ORDERS the steps
39
- * to avoid that:
37
+ * DEFAULT BRANCHNOT hardcoded to `main`. The GitHub adapter resolves the
38
+ * repo's real default branch ({@link CIProviderContext.getDefaultBranch}, via
39
+ * `gh repo view --json defaultBranchRef`); the orchestrator threads it into
40
+ * BOTH the classic PUT and the printed `gh api` fallback so a `master`-defaulted
41
+ * (or otherwise-renamed) repo is protected on the correct branch instead of
42
+ * 404-ing a hardcoded `main`. The lookup falls back to {@link
43
+ * DEFAULT_PROTECTED_BRANCH} (`main`) only when it fails/empties, and that
44
+ * fallback is LOGGED so it is visible. An explicit `branch` option still wins
45
+ * over auto-detection (a repo whose protected branch is not its default).
40
46
  *
41
- * 1. write the verify workflow artifact (the `verify.yml` whose job is named
42
- * exactly {@link VERIFY_CHECK_CONTEXT}); the human commits + pushes it to
43
- * `main` so the workflow file lands BEFORE the next PR;
44
- * 2. THEN (this step) set the branch-protection rule.
47
+ * DEADLOCK GUARD a required check that has NEVER reported blocks every merge
48
+ * (GitHub treats an unreported required check as pending), so a PR opened before
49
+ * `verify.yml` first ran would deadlock. The guard is a RUNTIME MECHANISM, not a
50
+ * log line: the required-`verify` check is enforced through a branch RULESET
51
+ * ({@link buildBranchProtectionRuleset}, `POST /repos/{owner}/{repo}/rulesets`)
52
+ * whose `required_status_checks` rule carries `do_not_enforce_on_create: true` —
53
+ * GitHub's native "require this check, but do not fail refs created before the
54
+ * rule / before the check first ran" toggle. The ruleset targets the resolved
55
+ * default branch via `conditions.ref_name`.
45
56
  *
46
- * Because the verify workflow uses `on: pull_request`, every PR opened AFTER
47
- * `verify.yml` is on `main` triggers the workflow and produces the required
48
- * `verify` check the protection never deadlocks new PRs. The only edge case
49
- * is an IN-FLIGHT PR opened BEFORE `verify.yml` landed on `main`: it has no
50
- * `verify` check waiting; installCI prints exactly that re-trigger remediation
51
- * in the success log. We chose this "run-the-check-once-by-construction-of-the-
52
- * trigger" ordering over the ruleset `do_not_enforce_on_create` toggle because
53
- * (a) one PUT against the existing branch-protection endpoint is simpler than
54
- * minting a ruleset shape this spec otherwise does not need, AND (b) the
55
- * `merge_queue` Tier-2 forward seam already lives in the verify workflow's
56
- * `on:` trigger (so the ruleset seam is not needed here either it is the
57
- * follow-on spec's lever). Trade-off RECORDED so a future merge-queue task can
58
- * flip to rulesets knowingly without re-deciding why we did not now.
57
+ * WHY RULESET FOR THE GUARD BUT CLASSIC PUT FOR THE BASE (nit-a preserved).
58
+ * The base protection stays a single classic PUT
59
+ * ({@link buildBranchProtectionSpec}: `strict: true` = "require branches up to
60
+ * date before merging") because that endpoint is the simplest way to express
61
+ * up-to-date-ness and nit-a ratified the classic-PUT choice. ONLY the deadlock-
62
+ * prone piece — the required-status-check gate moves to a ruleset, because the
63
+ * `do_not_enforce_on_create` create-exemption exists ONLY on rulesets, not on
64
+ * the classic `required_status_checks` object. Keeping the required check in the
65
+ * classic PUT AND adding the ruleset would double-enforce it and NULLIFY the
66
+ * exemption (the classic copy has no exemption), so the required check lives in
67
+ * the ruleset ALONE; the classic PUT keeps `strict` with an EMPTY `checks`
68
+ * array (valid: "require up to date", no named required check at the classic
69
+ * layer). This is exactly "only the deadlock-guard aspect needs the ruleset"
70
+ * not a full base-protection migration. Chosen over the pre-run mechanism
71
+ * (`gh workflow run` + wait) because `writeArtifacts` writes `verify.yml`
72
+ * LOCALLY and does NOT push, so at install time the workflow is not yet on the
73
+ * default branch and a pre-run has nothing to dispatch. The ruleset toggle needs
74
+ * no pushed workflow and no wait, so it is the only viable runtime guard here.
59
75
  */
60
76
 
61
77
  import {VERIFY_CHECK_CONTEXT} from './verify-workflow-template.js';
@@ -64,15 +80,17 @@ import type {CIProviderContext} from './install-ci-core.js';
64
80
  export {VERIFY_CHECK_CONTEXT};
65
81
 
66
82
  /**
67
- * The branch-protection request body the GitHub adapter PUTs at
83
+ * The BASE branch-protection request body the GitHub adapter PUTs at
68
84
  * `/repos/{owner}/{repo}/branches/{branch}/protection`. Shape mirrors the REST
69
85
  * docs (the GitHub PUT REQUIRES `enforce_admins`, `required_pull_request_reviews`,
70
86
  * and `restrictions` to be present; `null` for "not configured"). We set only
71
- * the two fields the spec names the required `${VERIFY_CHECK_CONTEXT}` check
72
- * AND `strict: true` (= "require branches up to date before merging") and
73
- * leave the others `null` so install-ci does not silently impose unrelated
74
- * gates (reviews, admin enforcement, push restrictions) the user did not opt
75
- * into.
87
+ * `strict: true` (= "require branches up to date before merging") with an EMPTY
88
+ * `checks` array the required `${VERIFY_CHECK_CONTEXT}` check is NOT here; it
89
+ * lives in the deadlock-guard RULESET ({@link buildBranchProtectionRuleset}) so
90
+ * its `do_not_enforce_on_create` create-exemption is meaningful (see module
91
+ * header). The other required fields stay `null` so install-ci does not silently
92
+ * impose unrelated gates (reviews, admin enforcement, push restrictions) the
93
+ * user did not opt into.
76
94
  */
77
95
  export interface BranchProtectionSpec {
78
96
  required_status_checks: {
@@ -85,16 +103,15 @@ export interface BranchProtectionSpec {
85
103
  }
86
104
 
87
105
  /**
88
- * Build the Tier-1 branch-protection spec the GitHub adapter PUTs. The required
89
- * `context` is the SAME constant the emitted verify workflow names its job
90
- * after ({@link VERIFY_CHECK_CONTEXT}) by construction, not by hand-typed
91
- * agreement, so the two cannot drift.
106
+ * Build the Tier-1 BASE branch-protection spec the GitHub adapter PUTs
107
+ * (`strict: true`, no named required check — the required check is the ruleset's
108
+ * job, see {@link buildBranchProtectionRuleset} and the module header).
92
109
  */
93
110
  export function buildBranchProtectionSpec(): BranchProtectionSpec {
94
111
  return {
95
112
  required_status_checks: {
96
113
  strict: true,
97
- checks: [{context: VERIFY_CHECK_CONTEXT}],
114
+ checks: [],
98
115
  },
99
116
  enforce_admins: null,
100
117
  required_pull_request_reviews: null,
@@ -102,6 +119,70 @@ export function buildBranchProtectionSpec(): BranchProtectionSpec {
102
119
  };
103
120
  }
104
121
 
122
+ /** The name the deadlock-guard ruleset is created under (stable/idempotent-ish). */
123
+ export const BRANCH_PROTECTION_RULESET_NAME = 'dorfl-verify-required';
124
+
125
+ /**
126
+ * The deadlock-guard RULESET body the GitHub adapter POSTs at
127
+ * `/repos/{owner}/{repo}/rulesets`. It carries the required-`${VERIFY_CHECK_CONTEXT}`
128
+ * status-check rule with `do_not_enforce_on_create: true` — GitHub's native
129
+ * "require this check, but do not fail refs that predate the rule / the check's
130
+ * first run" toggle (the runtime deadlock guard). `enforcement: 'active'` +
131
+ * `conditions.ref_name` scope it to the resolved default branch. The required
132
+ * `context` is the SAME {@link VERIFY_CHECK_CONTEXT} constant the emitted verify
133
+ * workflow names its job after — by construction, so the two cannot drift.
134
+ */
135
+ export interface BranchProtectionRuleset {
136
+ name: string;
137
+ target: 'branch';
138
+ enforcement: 'active';
139
+ conditions: {
140
+ ref_name: {
141
+ include: string[];
142
+ exclude: string[];
143
+ };
144
+ };
145
+ rules: {
146
+ type: 'required_status_checks';
147
+ parameters: {
148
+ strict_required_status_checks_policy: boolean;
149
+ do_not_enforce_on_create: true;
150
+ required_status_checks: {context: string}[];
151
+ };
152
+ }[];
153
+ }
154
+
155
+ /**
156
+ * Build the deadlock-guard ruleset for `branch` (the resolved default branch).
157
+ * The `ref_name` include uses the exact-ref form (`refs/heads/<branch>`) so the
158
+ * rule targets only that branch. `do_not_enforce_on_create: true` is the guard.
159
+ */
160
+ export function buildBranchProtectionRuleset(
161
+ branch: string = DEFAULT_PROTECTED_BRANCH,
162
+ ): BranchProtectionRuleset {
163
+ return {
164
+ name: BRANCH_PROTECTION_RULESET_NAME,
165
+ target: 'branch',
166
+ enforcement: 'active',
167
+ conditions: {
168
+ ref_name: {
169
+ include: [`refs/heads/${branch}`],
170
+ exclude: [],
171
+ },
172
+ },
173
+ rules: [
174
+ {
175
+ type: 'required_status_checks',
176
+ parameters: {
177
+ strict_required_status_checks_policy: true,
178
+ do_not_enforce_on_create: true,
179
+ required_status_checks: [{context: VERIFY_CHECK_CONTEXT}],
180
+ },
181
+ },
182
+ ],
183
+ };
184
+ }
185
+
105
186
  /**
106
187
  * The repo's default branch the protection rule targets (the rule the spec
107
188
  * names: protect `main` so a propose-mode PR cannot be merged stale). Exposed
@@ -132,6 +213,26 @@ export function formatManualBranchProtectionCommand(
132
213
  );
133
214
  }
134
215
 
216
+ /**
217
+ * The exact `gh api` command the user runs themselves to create the deadlock-
218
+ * guard RULESET when install-ci cannot auto-configure (no admin scope). Quoted
219
+ * body is the same {@link BranchProtectionRuleset} the auto path POSTs, so
220
+ * reproducing the call by copy-paste is byte-equivalent to a successful auto
221
+ * run. Pure string, no I/O.
222
+ */
223
+ export function formatManualBranchRulesetCommand(
224
+ repo: string,
225
+ branch: string = DEFAULT_PROTECTED_BRANCH,
226
+ ): string {
227
+ const body = JSON.stringify(buildBranchProtectionRuleset(branch));
228
+ return (
229
+ `echo '${body}' | gh api -X POST ` +
230
+ `-H "Accept: application/vnd.github+json" ` +
231
+ `repos/${repo}/rulesets ` +
232
+ `--input -`
233
+ );
234
+ }
235
+
135
236
  /** Options for {@link installCIBranchProtectionStep}. */
136
237
  export interface BranchProtectionStepOptions {
137
238
  /** The CI-provider seam (GitHub adapter in production; stub in tests). */
@@ -140,7 +241,13 @@ export interface BranchProtectionStepOptions {
140
241
  fake?: boolean;
141
242
  /** Sink for human-facing progress lines. */
142
243
  log: (line: string) => void;
143
- /** The branch the rule targets (default `main`). */
244
+ /**
245
+ * Explicitly override the branch the rule targets. When OMITTED, the step
246
+ * auto-detects the repo's default branch via
247
+ * {@link CIProviderContext.getDefaultBranch}, falling back to
248
+ * {@link DEFAULT_PROTECTED_BRANCH} (`main`) and logging the fallback. An
249
+ * explicit value always wins over auto-detection.
250
+ */
144
251
  branch?: string;
145
252
  }
146
253
 
@@ -168,41 +275,105 @@ export interface BranchProtectionStepResult {
168
275
  error?: string;
169
276
  /** The branch the step targeted (echoed for tests / logs). */
170
277
  branch: string;
278
+ /**
279
+ * Whether the branch was AUTO-DETECTED (`true`) or came from an explicit
280
+ * `branch` option / the `main` fallback (`false`). Echoed for tests / logs.
281
+ */
282
+ branchAutoDetected?: boolean;
283
+ /**
284
+ * The deadlock-guard RULESET outcome (only on the admin auto path):
285
+ * - `set` — the ruleset POST succeeded;
286
+ * - `failed` — the ruleset POST was rejected (detail in `rulesetError`);
287
+ * - `skipped-no-seam` — the provider does not implement the ruleset seam.
288
+ * Absent on the non-admin / skipped paths.
289
+ */
290
+ rulesetStatus?: 'set' | 'failed' | 'skipped-no-seam';
291
+ /** The ruleset failure detail when `rulesetStatus === 'failed'`. */
292
+ rulesetError?: string;
293
+ }
294
+
295
+ /**
296
+ * Resolve the branch the protection targets. An EXPLICIT `branch` option always
297
+ * wins; otherwise auto-detect the repo's default branch via
298
+ * {@link CIProviderContext.getDefaultBranch}, falling back to `main` (LOGGED)
299
+ * when the lookup fails/empties. Returns the branch + whether it was
300
+ * auto-detected (for the result / log lines).
301
+ */
302
+ async function resolveTargetBranch(
303
+ ctx: CIProviderContext,
304
+ explicit: string | undefined,
305
+ log: (line: string) => void,
306
+ ): Promise<{branch: string; autoDetected: boolean}> {
307
+ if (explicit !== undefined) {
308
+ return {branch: explicit, autoDetected: false};
309
+ }
310
+ const detected = ctx.getDefaultBranch
311
+ ? await ctx.getDefaultBranch().catch(() => undefined)
312
+ : undefined;
313
+ if (detected) {
314
+ return {branch: detected, autoDetected: true};
315
+ }
316
+ log(
317
+ `branch protection: could not detect the default branch; ` +
318
+ `falling back to \`${DEFAULT_PROTECTED_BRANCH}\`.`,
319
+ );
320
+ return {branch: DEFAULT_PROTECTED_BRANCH, autoDetected: false};
171
321
  }
172
322
 
173
323
  /**
174
324
  * The orchestrator install-ci calls AFTER artifacts are written + secrets are
175
- * orchestrated: detect admin scope (cheapest path: {@link
176
- * CIProviderContext.getRepoAdminScope}), then EITHER call
177
- * {@link CIProviderContext.setBranchProtection} (admin) OR print the exact
178
- * ready-to-run `gh api` command + the one-liner manual fallback (non-admin).
325
+ * orchestrated:
326
+ *
327
+ * 1. RESOLVE the target branch (explicit option > auto-detected default
328
+ * branch > `main` fallback), so the PUT + printed fallback target the real
329
+ * default branch (see module header, "DEFAULT BRANCH");
330
+ * 2. detect admin scope (cheapest path: {@link
331
+ * CIProviderContext.getRepoAdminScope});
332
+ * 3. ADMIN: PUT the base protection ({@link
333
+ * CIProviderContext.setBranchProtection}) AND create the deadlock-guard
334
+ * ruleset ({@link CIProviderContext.setBranchRuleset}); NON-ADMIN: print
335
+ * the exact ready-to-run `gh api` commands (PUT + ruleset POST) + the
336
+ * manual UI fallback.
179
337
  *
180
- * Never throws — a `setBranchProtection` rejection is reported as `failed` so
181
- * install-ci's later steps still run, and the user sees both the failure detail
182
- * AND the manual command to retry by hand.
338
+ * Never throws — a `setBranchProtection` rejection is reported as `failed`, a
339
+ * `setBranchRuleset` rejection as `rulesetStatus: 'failed'`, so install-ci's
340
+ * later steps still run and the user sees the failure detail AND the manual
341
+ * command to retry by hand.
183
342
  */
184
343
  export async function installCIBranchProtectionStep(
185
344
  options: BranchProtectionStepOptions,
186
345
  ): Promise<BranchProtectionStepResult> {
187
- const branch = options.branch ?? DEFAULT_PROTECTED_BRANCH;
188
346
  const {ctx, log, fake} = options;
189
347
 
190
348
  if (fake) {
191
- log(`branch protection: skipped (--fake; would have targeted ${branch})`);
192
- return {status: 'skipped-fake', branch};
349
+ const target = options.branch ?? DEFAULT_PROTECTED_BRANCH;
350
+ log(`branch protection: skipped (--fake; would have targeted ${target})`);
351
+ return {status: 'skipped-fake', branch: target};
193
352
  }
194
353
  if (!ctx.setBranchProtection) {
195
354
  log(
196
355
  'branch protection: skipped (provider does not implement the seam — non-GitHub host).',
197
356
  );
198
- return {status: 'skipped-no-seam', branch};
357
+ return {
358
+ status: 'skipped-no-seam',
359
+ branch: options.branch ?? DEFAULT_PROTECTED_BRANCH,
360
+ };
199
361
  }
200
362
  if (!ctx.repo) {
201
363
  log('branch protection: skipped (repo unknown — pass --repo to enable).');
202
- return {status: 'skipped-no-repo', branch};
364
+ return {
365
+ status: 'skipped-no-repo',
366
+ branch: options.branch ?? DEFAULT_PROTECTED_BRANCH,
367
+ };
203
368
  }
204
369
 
205
370
  const repo = ctx.repo;
371
+ const {branch, autoDetected} = await resolveTargetBranch(
372
+ ctx,
373
+ options.branch,
374
+ log,
375
+ );
376
+
206
377
  // Cheapest scope-detection: one repo-permissions read (see module header).
207
378
  // `undefined` ⇒ unknown ⇒ treat as non-admin (instruct, never silently
208
379
  // attempt a call that would fail with 403 anyway).
@@ -213,20 +384,30 @@ export async function installCIBranchProtectionStep(
213
384
  if (adminScope !== true) {
214
385
  log('branch protection: no admin-scoped credential detected.');
215
386
  log(
216
- ` install-ci will NOT call the GitHub API. Run this yourself (admin token required):`,
387
+ ` install-ci will NOT call the GitHub API. Run these yourself (admin token required):`,
217
388
  );
218
389
  log(` ${formatManualBranchProtectionCommand(repo, branch)}`);
390
+ log(
391
+ ` Then create the deadlock-guard ruleset (required check \`${VERIFY_CHECK_CONTEXT}\`, ` +
392
+ `do_not_enforce_on_create):`,
393
+ );
394
+ log(` ${formatManualBranchRulesetCommand(repo, branch)}`);
219
395
  log(
220
396
  ` Manual UI fallback: https://github.com/${repo}/settings/branches ` +
221
397
  `→ "Add branch protection rule" → branch name pattern \`${branch}\` ` +
222
- `→ enable "Require status checks to pass" + "Require branches to be up to date" ` +
223
- `→ add required check \`${VERIFY_CHECK_CONTEXT}\`.`,
398
+ `→ enable "Require branches to be up to date", then add a RULESET ` +
399
+ `requiring \`${VERIFY_CHECK_CONTEXT}\` with "Do not require ... on creation".`,
224
400
  );
225
401
  log(
226
402
  ` (The ambient CI \`GITHUB_TOKEN\` is NEVER admin; a human admin's \`gh\` ` +
227
403
  `or a configured PAT/App token with "Administration: write" is required.)`,
228
404
  );
229
- return {status: 'instructed', adminScope, branch};
405
+ return {
406
+ status: 'instructed',
407
+ adminScope,
408
+ branch,
409
+ branchAutoDetected: autoDetected,
410
+ };
230
411
  }
231
412
 
232
413
  const spec = buildBranchProtectionSpec();
@@ -234,15 +415,8 @@ export async function installCIBranchProtectionStep(
234
415
  await ctx.setBranchProtection(branch, spec);
235
416
  log(
236
417
  `branch protection: set on ${repo}@${branch} ` +
237
- `(required check \`${VERIFY_CHECK_CONTEXT}\`, strict: true).`,
418
+ `(strict: true = require branches up to date).`,
238
419
  );
239
- log(
240
- ` NOTE: PRs opened BEFORE \`verify.yml\` lands on \`${branch}\` have ` +
241
- `no \`${VERIFY_CHECK_CONTEXT}\` check waiting. Push/merge \`verify.yml\` ` +
242
- `to \`${branch}\` first; existing in-flight PRs may need a no-op push ` +
243
- `(or close/reopen) to re-trigger the check.`,
244
- );
245
- return {status: 'set', adminScope, branch};
246
420
  } catch (err) {
247
421
  const error = err instanceof Error ? err.message : String(err);
248
422
  log(`branch protection: FAILED (${error})`);
@@ -250,6 +424,57 @@ export async function installCIBranchProtectionStep(
250
424
  ` Retry by hand (admin token required):\n` +
251
425
  ` ${formatManualBranchProtectionCommand(repo, branch)}`,
252
426
  );
253
- return {status: 'failed', adminScope, error, branch};
427
+ return {
428
+ status: 'failed',
429
+ adminScope,
430
+ error,
431
+ branch,
432
+ branchAutoDetected: autoDetected,
433
+ };
254
434
  }
435
+
436
+ // The deadlock guard: the required `verify` check lives in a RULESET with
437
+ // `do_not_enforce_on_create: true` (see module header), NOT in the classic
438
+ // PUT above — so a ref that predates the rule / the check's first run is not
439
+ // deadlocked by a never-reported required check.
440
+ let rulesetStatus: 'set' | 'failed' | 'skipped-no-seam';
441
+ let rulesetError: string | undefined;
442
+ if (!ctx.setBranchRuleset) {
443
+ rulesetStatus = 'skipped-no-seam';
444
+ log(
445
+ ` deadlock guard: provider does not implement the ruleset seam; ` +
446
+ `required check \`${VERIFY_CHECK_CONTEXT}\` NOT enforced.`,
447
+ );
448
+ } else {
449
+ try {
450
+ await ctx.setBranchRuleset(buildBranchProtectionRuleset(branch));
451
+ rulesetStatus = 'set';
452
+ log(
453
+ `deadlock-guard ruleset: created on ${repo}@${branch} ` +
454
+ `(required check \`${VERIFY_CHECK_CONTEXT}\`, do_not_enforce_on_create: true).`,
455
+ );
456
+ log(
457
+ ` NOTE: still push/merge \`verify.yml\` to \`${branch}\` so the check ` +
458
+ `can report on new PRs; the ruleset's create-exemption keeps existing ` +
459
+ `in-flight refs from deadlocking on the not-yet-run check.`,
460
+ );
461
+ } catch (err) {
462
+ rulesetError = err instanceof Error ? err.message : String(err);
463
+ rulesetStatus = 'failed';
464
+ log(`deadlock-guard ruleset: FAILED (${rulesetError})`);
465
+ log(
466
+ ` Retry by hand (admin token required):\n` +
467
+ ` ${formatManualBranchRulesetCommand(repo, branch)}`,
468
+ );
469
+ }
470
+ }
471
+
472
+ return {
473
+ status: 'set',
474
+ adminScope,
475
+ branch,
476
+ branchAutoDetected: autoDetected,
477
+ rulesetStatus,
478
+ rulesetError,
479
+ };
255
480
  }
@@ -152,7 +152,7 @@ export const DEFAULT_HARNESS: HarnessAdapter = 'pi';
152
152
  * draining steadily rather than stampeding the model-provider rate limit + the
153
153
  * arbiter CAS. Tunable via `install-ci --max-parallel <n>` / the config file.
154
154
  */
155
- export const DEFAULT_MAX_PARALLEL = 4;
155
+ export const DEFAULT_MAX_PARALLEL = 2;
156
156
 
157
157
  /** The default install source: the published CLI via `npm install -g`. */
158
158
  export const DEFAULT_INSTALL_SOURCE: InstallSource = 'registry';
@@ -212,6 +212,26 @@ export interface CIProviderContext {
212
212
  * Throws on failure (caller catches + logs).
213
213
  */
214
214
  setBranchProtection?(branch: string, spec: unknown): Promise<void>;
215
+ /**
216
+ * OPTIONALLY report the repo's DEFAULT BRANCH name (GitHub:
217
+ * `gh repo view --json defaultBranchRef`). Used by the Tier-1 branch-
218
+ * protection step so the PUT and the printed `gh api` fallback both target
219
+ * the repo's real default branch (a `master`-defaulted repo would 404 a
220
+ * hardcoded `main`). Returns `undefined` when the lookup fails / is empty
221
+ * (caller falls back to `main` and logs it). Absent on a provider that has
222
+ * no default-branch concept.
223
+ */
224
+ getDefaultBranch?(): Promise<string | undefined>;
225
+ /**
226
+ * OPTIONALLY create/replace a branch RULESET (GitHub:
227
+ * `POST /repos/{owner}/{repo}/rulesets`) carrying the deadlock-guard
228
+ * `required_status_checks` rule with `do_not_enforce_on_create: true`. The
229
+ * `ruleset` body (`BranchProtectionRuleset` in
230
+ * `install-ci-branch-protection.ts`) is typed as `unknown` here to keep this
231
+ * core seam free of the branch-protection module's import. Absent on a non-
232
+ * GitHub provider. Throws on failure (caller catches + logs).
233
+ */
234
+ setBranchRuleset?(ruleset: unknown): Promise<void>;
215
235
  /**
216
236
  * OPTIONALLY render the user's project-setup payload (the value of
217
237
  * {@link CIConfigFile.projectSetup}[providerId]) to the NATIVE-SYNTAX
@@ -787,7 +807,7 @@ ${indent(modelsJsonStr, 8)}
787
807
  if (config.installSource === 'workspace') {
788
808
  installSteps = `\
789
809
  - name: Setup pnpm
790
- uses: pnpm/action-setup@v4
810
+ uses: pnpm/action-setup@v5
791
811
 
792
812
  - name: Add pnpm global bin to PATH
793
813
  shell: bash
@@ -903,9 +923,16 @@ runs:
903
923
  using: composite
904
924
  steps:
905
925
  ${projectSetupBlock} - name: Setup Node.js
906
- uses: actions/setup-node@v4
926
+ uses: actions/setup-node@v5
907
927
  with:
908
928
  node-version: '22'
929
+ # setup-node@v5 auto-enables package-manager caching, which probes the
930
+ # project package manager DURING its own run when a lockfile is present,
931
+ # before that manager is on PATH, so the probe crashes ("Unable to
932
+ # locate executable file"). dorfl provisions ONLY its own runtime and
933
+ # leaves the project package-manager cache to the project (toolchain
934
+ # boundary), so disable it explicitly.
935
+ package-manager-cache: false
909
936
 
910
937
  - name: Configure git identity
911
938
  shell: bash
@@ -239,6 +239,66 @@ export class GitHubCIContext implements CIProviderContext {
239
239
  return undefined;
240
240
  }
241
241
 
242
+ /**
243
+ * Detect the repo's DEFAULT BRANCH via
244
+ * `gh repo view --json defaultBranchRef --jq .defaultBranchRef.name`. Returns
245
+ * `undefined` when `gh` is unavailable / the repo is unknown / the call fails
246
+ * / the name is empty — the caller falls back to `main` (and logs it) so a
247
+ * `master`-defaulted repo is never silently protected on a hardcoded `main`.
248
+ */
249
+ async getDefaultBranch(): Promise<string | undefined> {
250
+ if (!this.repo) {
251
+ return undefined;
252
+ }
253
+ const result = this.runGh([
254
+ 'repo',
255
+ 'view',
256
+ '--json',
257
+ 'defaultBranchRef',
258
+ '--jq',
259
+ '.defaultBranchRef.name',
260
+ ]);
261
+ if (result === undefined || result.status !== 0) {
262
+ return undefined;
263
+ }
264
+ const name = result.stdout.trim();
265
+ return name === '' ? undefined : name;
266
+ }
267
+
268
+ /**
269
+ * Create the deadlock-guard RULESET via
270
+ * `POST /repos/{owner}/{repo}/rulesets`. The `ruleset` body is JSON-piped on
271
+ * stdin (`gh api --input -`) so the nested rule shape carries cleanly. Throws
272
+ * on a non-zero exit; caller logs + falls back. See the module header of
273
+ * `install-ci-branch-protection.ts` for why the required-check guard uses a
274
+ * ruleset while base protection stays a classic PUT.
275
+ */
276
+ async setBranchRuleset(ruleset: unknown): Promise<void> {
277
+ if (!this.repo) {
278
+ throw new Error('cannot create branch ruleset: repo unknown');
279
+ }
280
+ const args = [
281
+ 'api',
282
+ '-X',
283
+ 'POST',
284
+ '-H',
285
+ 'Accept: application/vnd.github+json',
286
+ `repos/${this.repo}/rulesets`,
287
+ '--input',
288
+ '-',
289
+ ];
290
+ const result = this.runGhWithInput(args, JSON.stringify(ruleset));
291
+ if (result === undefined) {
292
+ throw new Error('gh not available; cannot create branch ruleset');
293
+ }
294
+ if (result.status !== 0) {
295
+ throw new Error(
296
+ `gh api POST repos/${this.repo}/rulesets failed: ` +
297
+ (result.stderr.trim() || 'unknown error'),
298
+ );
299
+ }
300
+ }
301
+
242
302
  /**
243
303
  * Configure Tier-1 branch protection via
244
304
  * `PUT /repos/{owner}/{repo}/branches/{branch}/protection`. The `spec` body
@@ -322,10 +382,16 @@ export class MemoryCIProviderContext implements CIProviderContext {
322
382
  readonly repoSettings = new Map<string, boolean>();
323
383
  /** Branch-protection spec recorded in memory (branch → spec). */
324
384
  readonly branchProtections = new Map<string, unknown>();
385
+ /** Deadlock-guard rulesets recorded in memory (append-only). */
386
+ readonly rulesets: unknown[] = [];
325
387
  /** What `getRepoAdminScope()` should return (tests configure). */
326
388
  private readonly adminScope: boolean | undefined;
389
+ /** What `getDefaultBranch()` should return (tests configure). */
390
+ private readonly defaultBranch: string | undefined;
327
391
  /** Whether `setBranchProtection` should throw (tests configure). */
328
392
  private readonly branchProtectionError: string | undefined;
393
+ /** Whether `setBranchRuleset` should throw (tests configure). */
394
+ private readonly branchRulesetError: string | undefined;
329
395
 
330
396
  constructor(options: {
331
397
  workDir: string;
@@ -333,8 +399,15 @@ export class MemoryCIProviderContext implements CIProviderContext {
333
399
  ghAvailable?: boolean;
334
400
  /** Fixture admin-scope verdict; `undefined` ⇒ unknown. */
335
401
  adminScope?: boolean;
402
+ /**
403
+ * Fixture default-branch name; `undefined` ⇒ lookup "fails" so the caller
404
+ * exercises its `main` fallback (models a `gh` that could not resolve it).
405
+ */
406
+ defaultBranch?: string;
336
407
  /** When set, `setBranchProtection` throws with this message (failure path). */
337
408
  branchProtectionError?: string;
409
+ /** When set, `setBranchRuleset` throws with this message (failure path). */
410
+ branchRulesetError?: string;
338
411
  /**
339
412
  * The provider id this stub claims (default `github`). Tests of the
340
413
  * project-setup seam may override to assert the orchestrator's
@@ -347,7 +420,9 @@ export class MemoryCIProviderContext implements CIProviderContext {
347
420
  this.repo = options.repo;
348
421
  this.ghAvailable = options.ghAvailable ?? false;
349
422
  this.adminScope = options.adminScope;
423
+ this.defaultBranch = options.defaultBranch;
350
424
  this.branchProtectionError = options.branchProtectionError;
425
+ this.branchRulesetError = options.branchRulesetError;
351
426
  this.providerId = options.providerId ?? GITHUB_PROVIDER_ID;
352
427
  }
353
428
 
@@ -367,10 +442,21 @@ export class MemoryCIProviderContext implements CIProviderContext {
367
442
  return this.adminScope;
368
443
  }
369
444
 
445
+ async getDefaultBranch(): Promise<string | undefined> {
446
+ return this.defaultBranch;
447
+ }
448
+
370
449
  async setBranchProtection(branch: string, spec: unknown): Promise<void> {
371
450
  if (this.branchProtectionError !== undefined) {
372
451
  throw new Error(this.branchProtectionError);
373
452
  }
374
453
  this.branchProtections.set(branch, spec);
375
454
  }
455
+
456
+ async setBranchRuleset(ruleset: unknown): Promise<void> {
457
+ if (this.branchRulesetError !== undefined) {
458
+ throw new Error(this.branchRulesetError);
459
+ }
460
+ this.rulesets.push(ruleset);
461
+ }
376
462
  }