@theodo/bernard 0.1.0-beta.0 → 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/README.md +13 -12
  2. package/core/README.md +1 -1
  3. package/core/agents/review-quality.md +13 -12
  4. package/core/agents/review-security-bugs.md +1 -1
  5. package/core/agents/review-tests.md +1 -1
  6. package/core/coder/template/main.tf +14 -1
  7. package/core/skills/pull-request/SKILL.md +17 -7
  8. package/core/skills/pull-request/ci-fixer.md +3 -3
  9. package/dist/bin.mjs +4979 -3176
  10. package/dist/{config-BawD0TwQ.mjs → config-DtCwJKWe.mjs} +70 -6
  11. package/dist/config.d.mts +72 -4
  12. package/dist/config.mjs +1 -1
  13. package/dist/docs/404.html +23 -0
  14. package/dist/docs/CLAUDE.html +30 -0
  15. package/dist/docs/CONTEXT.html +30 -0
  16. package/dist/docs/CONTRIBUTING.html +62 -0
  17. package/dist/docs/assets/CLAUDE.md.BLSB-5gK.js +1 -0
  18. package/dist/docs/assets/CLAUDE.md.BLSB-5gK.lean.js +1 -0
  19. package/dist/docs/assets/CONTEXT.md.BovnvMGK.js +1 -0
  20. package/dist/docs/assets/CONTEXT.md.BovnvMGK.lean.js +1 -0
  21. package/dist/docs/assets/CONTRIBUTING.md.DwJsKrz3.js +33 -0
  22. package/dist/docs/assets/CONTRIBUTING.md.DwJsKrz3.lean.js +1 -0
  23. package/dist/docs/assets/app.URmpjWHb.js +312 -0
  24. package/dist/docs/assets/bernard.RCgrvDdX.svg +94 -0
  25. package/dist/docs/assets/chunks/@localSearchIndexroot.Ylq685Cw.js +1 -0
  26. package/dist/docs/assets/chunks/VPLocalSearchBox.C8gcRVj0.js +9 -0
  27. package/dist/docs/assets/chunks/dagre-GXQ25YYZ.2qvw8AqH.js +4 -0
  28. package/dist/docs/assets/chunks/framework.hgjl6W6o.js +20 -0
  29. package/dist/docs/assets/chunks/sizeCapture-INFHLROL.BEqL2zFi.js +1 -0
  30. package/dist/docs/assets/chunks/stateDiagram-v2-MP3YSRHH.3X7wNXKX.js +231 -0
  31. package/dist/docs/assets/chunks/theme.Cksh_UJH.js +2 -0
  32. package/dist/docs/assets/chunks/virtual_mermaid-config.DDnGl6nM.js +1 -0
  33. package/dist/docs/assets/core_README.md.DyxYsikc.js +15 -0
  34. package/dist/docs/assets/core_README.md.DyxYsikc.lean.js +1 -0
  35. package/dist/docs/assets/demo_README.md.Cpi2pUU9.js +18 -0
  36. package/dist/docs/assets/demo_README.md.Cpi2pUU9.lean.js +1 -0
  37. package/dist/docs/assets/docs_adr_0001-what-the-core-is-built-for.md.BxOoMjST.js +1 -0
  38. package/dist/docs/assets/docs_adr_0001-what-the-core-is-built-for.md.BxOoMjST.lean.js +1 -0
  39. package/dist/docs/assets/docs_adr_0002-which-harness-a-project-runs-on.md.2GwF-nj0.js +1 -0
  40. package/dist/docs/assets/docs_adr_0002-which-harness-a-project-runs-on.md.2GwF-nj0.lean.js +1 -0
  41. package/dist/docs/assets/docs_adr_0003-how-core-content-reaches-each-harness.md.BMWE00Mf.js +1 -0
  42. package/dist/docs/assets/docs_adr_0003-how-core-content-reaches-each-harness.md.BMWE00Mf.lean.js +1 -0
  43. package/dist/docs/assets/docs_adr_0004-how-the-core-uses-external-tools.md.B7dtE7MB.js +1 -0
  44. package/dist/docs/assets/docs_adr_0004-how-the-core-uses-external-tools.md.B7dtE7MB.lean.js +1 -0
  45. package/dist/docs/assets/docs_adr_0005-what-drives-the-pipeline.md.DGn0CJeq.js +1 -0
  46. package/dist/docs/assets/docs_adr_0005-what-drives-the-pipeline.md.DGn0CJeq.lean.js +1 -0
  47. package/dist/docs/assets/docs_adr_0006-what-steps-hand-off-to-each-other.md.BflT5ugJ.js +1 -0
  48. package/dist/docs/assets/docs_adr_0006-what-steps-hand-off-to-each-other.md.BflT5ugJ.lean.js +1 -0
  49. package/dist/docs/assets/docs_adr_0007-how-humans-take-part-in-the-pipeline.md.CNX3gUD-.js +1 -0
  50. package/dist/docs/assets/docs_adr_0007-how-humans-take-part-in-the-pipeline.md.CNX3gUD-.lean.js +1 -0
  51. package/dist/docs/assets/docs_adr_0008-where-a-steps-reviewers-run.md.aGbaBOpB.js +1 -0
  52. package/dist/docs/assets/docs_adr_0008-where-a-steps-reviewers-run.md.aGbaBOpB.lean.js +1 -0
  53. package/dist/docs/assets/docs_adr_0009-where-pipeline-state-and-handoffs-live.md.C3tKmV-E.js +1 -0
  54. package/dist/docs/assets/docs_adr_0009-where-pipeline-state-and-handoffs-live.md.C3tKmV-E.lean.js +1 -0
  55. package/dist/docs/assets/docs_adr_0010-how-a-project-adapts-the-core.md.Bp7N6tUE.js +1 -0
  56. package/dist/docs/assets/docs_adr_0010-how-a-project-adapts-the-core.md.Bp7N6tUE.lean.js +1 -0
  57. package/dist/docs/assets/docs_adr_0011-how-the-factory-is-packaged-and-versioned.md.BBJIwnqa.js +1 -0
  58. package/dist/docs/assets/docs_adr_0011-how-the-factory-is-packaged-and-versioned.md.BBJIwnqa.lean.js +1 -0
  59. package/dist/docs/assets/docs_adr_0012-how-a-steps-failures-are-told-apart-and-routed.md.uCyrBfug.js +1 -0
  60. package/dist/docs/assets/docs_adr_0012-how-a-steps-failures-are-told-apart-and-routed.md.uCyrBfug.lean.js +1 -0
  61. package/dist/docs/assets/docs_adr_0013-when-code-lands.md.oXgiobfz.js +1 -0
  62. package/dist/docs/assets/docs_adr_0013-when-code-lands.md.oXgiobfz.lean.js +1 -0
  63. package/dist/docs/assets/docs_adr_0014-what-a-step-may-change.md.Zslg3bnJ.js +1 -0
  64. package/dist/docs/assets/docs_adr_0014-what-a-step-may-change.md.Zslg3bnJ.lean.js +1 -0
  65. package/dist/docs/assets/docs_adr_0015-how-a-project-shapes-its-ticket-flow.md.DNHMxwIT.js +1 -0
  66. package/dist/docs/assets/docs_adr_0015-how-a-project-shapes-its-ticket-flow.md.DNHMxwIT.lean.js +1 -0
  67. package/dist/docs/assets/docs_adr_0016-which-core-the-factory-repo-runs.md.CLN6LqBZ.js +1 -0
  68. package/dist/docs/assets/docs_adr_0016-which-core-the-factory-repo-runs.md.CLN6LqBZ.lean.js +1 -0
  69. package/dist/docs/assets/docs_adr_0017-where-a-tickets-record-lives.md.jjUeM-ex.js +1 -0
  70. package/dist/docs/assets/docs_adr_0017-where-a-tickets-record-lives.md.jjUeM-ex.lean.js +1 -0
  71. package/dist/docs/assets/docs_adr_0018-the-products-name.md.6xaX9vDs.js +1 -0
  72. package/dist/docs/assets/docs_adr_0018-the-products-name.md.6xaX9vDs.lean.js +1 -0
  73. package/dist/docs/assets/docs_adr_0019-how-a-finding-is-answered-and-judged.md.CKKoO9-3.js +1 -0
  74. package/dist/docs/assets/docs_adr_0019-how-a-finding-is-answered-and-judged.md.CKKoO9-3.lean.js +1 -0
  75. package/dist/docs/assets/docs_adr_0020-how-an-interrupted-run-resumes.md.DTIP-MBK.js +1 -0
  76. package/dist/docs/assets/docs_adr_0020-how-an-interrupted-run-resumes.md.DTIP-MBK.lean.js +1 -0
  77. package/dist/docs/assets/docs_adr_0021-how-a-step-runs-in-its-workspace.md.B2r3-FPg.js +1 -0
  78. package/dist/docs/assets/docs_adr_0021-how-a-step-runs-in-its-workspace.md.B2r3-FPg.lean.js +1 -0
  79. package/dist/docs/assets/docs_adr_0022-who-names-the-branches.md.DaVk8X0i.js +1 -0
  80. package/dist/docs/assets/docs_adr_0022-who-names-the-branches.md.DaVk8X0i.lean.js +1 -0
  81. package/dist/docs/assets/docs_adr_0023-how-a-version-ships-before-npm.md.C7-wuCbq.js +1 -0
  82. package/dist/docs/assets/docs_adr_0023-how-a-version-ships-before-npm.md.C7-wuCbq.lean.js +1 -0
  83. package/dist/docs/assets/docs_adr_0024-how-the-config-type-carries-its-docs.md.CQmwhWXs.js +1 -0
  84. package/dist/docs/assets/docs_adr_0024-how-the-config-type-carries-its-docs.md.CQmwhWXs.lean.js +1 -0
  85. package/dist/docs/assets/docs_adr_0025-how-a-steps-handoff-is-written-and-checked.md.CgXYdF2v.js +1 -0
  86. package/dist/docs/assets/docs_adr_0025-how-a-steps-handoff-is-written-and-checked.md.CgXYdF2v.lean.js +1 -0
  87. package/dist/docs/assets/docs_adr_0026-support-for-github-copilot.md.DJsawm-R.js +1 -0
  88. package/dist/docs/assets/docs_adr_0026-support-for-github-copilot.md.DJsawm-R.lean.js +1 -0
  89. package/dist/docs/assets/docs_adr_0027-which-forge-a-project-is-on.md.CY0qy9xh.js +1 -0
  90. package/dist/docs/assets/docs_adr_0027-which-forge-a-project-is-on.md.CY0qy9xh.lean.js +1 -0
  91. package/dist/docs/assets/docs_adr_0028-several-ticket-flows-in-one-pipeline.md.oxffo2qg.js +1 -0
  92. package/dist/docs/assets/docs_adr_0028-several-ticket-flows-in-one-pipeline.md.oxffo2qg.lean.js +1 -0
  93. package/dist/docs/assets/docs_adr_0029-the-branch-a-feature-works-on.md.CrgkP6YJ.js +1 -0
  94. package/dist/docs/assets/docs_adr_0029-the-branch-a-feature-works-on.md.CrgkP6YJ.lean.js +1 -0
  95. package/dist/docs/assets/docs_adr_0030-a-runner-without-coder.md.CW8sF7jK.js +1 -0
  96. package/dist/docs/assets/docs_adr_0030-a-runner-without-coder.md.CW8sF7jK.lean.js +1 -0
  97. package/dist/docs/assets/docs_adr_0031-how-the-run-viewer-reads-its-runs.md.opQYnVF2.js +1 -0
  98. package/dist/docs/assets/docs_adr_0031-how-the-run-viewer-reads-its-runs.md.opQYnVF2.lean.js +1 -0
  99. package/dist/docs/assets/docs_adr_0032-how-a-project-gets-the-viewer.md.DB9NjYJf.js +1 -0
  100. package/dist/docs/assets/docs_adr_0032-how-a-project-gets-the-viewer.md.DB9NjYJf.lean.js +1 -0
  101. package/dist/docs/assets/docs_adr_0033-the-repos-license.md.NPlkrPj8.js +1 -0
  102. package/dist/docs/assets/docs_adr_0033-the-repos-license.md.NPlkrPj8.lean.js +1 -0
  103. package/dist/docs/assets/docs_adr_0034-how-code-review-launches-its-reviewers.md.DSNpkfaL.js +1 -0
  104. package/dist/docs/assets/docs_adr_0034-how-code-review-launches-its-reviewers.md.DSNpkfaL.lean.js +1 -0
  105. package/dist/docs/assets/docs_adr_0035-how-the-package-is-published.md.C1f4B5eb.js +1 -0
  106. package/dist/docs/assets/docs_adr_0035-how-the-package-is-published.md.C1f4B5eb.lean.js +1 -0
  107. package/dist/docs/assets/docs_adr_0036-what-the-cores-pull-request-skill-does.md.op39HLAT.js +1 -0
  108. package/dist/docs/assets/docs_adr_0036-what-the-cores-pull-request-skill-does.md.op39HLAT.lean.js +1 -0
  109. package/dist/docs/assets/docs_adr_0037-how-a-team-reads-the-factorys-documentation.md.Dar_Xo4t.js +1 -0
  110. package/dist/docs/assets/docs_adr_0037-how-a-team-reads-the-factorys-documentation.md.Dar_Xo4t.lean.js +1 -0
  111. package/dist/docs/assets/docs_adr_0038-where-adapters-live.md.CkVD3riv.js +1 -0
  112. package/dist/docs/assets/docs_adr_0038-where-adapters-live.md.CkVD3riv.lean.js +1 -0
  113. package/dist/docs/assets/docs_adr_0039-the-repos-license.md.C_rraWpb.js +1 -0
  114. package/dist/docs/assets/docs_adr_0039-the-repos-license.md.C_rraWpb.lean.js +1 -0
  115. package/dist/docs/assets/docs_adr_0040-how-a-version-is-released.md.CCdwC07z.js +1 -0
  116. package/dist/docs/assets/docs_adr_0040-how-a-version-is-released.md.CCdwC07z.lean.js +1 -0
  117. package/dist/docs/assets/docs_adr_0041-who-opens-the-features-pull-request.md.qLu9WLaG.js +1 -0
  118. package/dist/docs/assets/docs_adr_0041-who-opens-the-features-pull-request.md.qLu9WLaG.lean.js +1 -0
  119. package/dist/docs/assets/docs_adr_0042-how-secrets-are-kept-out-of-committed-records.md.DJWazI4H.js +1 -0
  120. package/dist/docs/assets/docs_adr_0042-how-secrets-are-kept-out-of-committed-records.md.DJWazI4H.lean.js +1 -0
  121. package/dist/docs/assets/docs_getting-started.md.BpQFS8Gr.js +12 -0
  122. package/dist/docs/assets/docs_getting-started.md.BpQFS8Gr.lean.js +1 -0
  123. package/dist/docs/assets/docs_github.md.D4gkW71G.js +1 -0
  124. package/dist/docs/assets/docs_github.md.D4gkW71G.lean.js +1 -0
  125. package/dist/docs/assets/docs_overlay.md.DoWVb6OM.js +35 -0
  126. package/dist/docs/assets/docs_overlay.md.DoWVb6OM.lean.js +1 -0
  127. package/dist/docs/assets/docs_pipeline.md.DdHywWSk.js +1 -0
  128. package/dist/docs/assets/docs_pipeline.md.DdHywWSk.lean.js +1 -0
  129. package/dist/docs/assets/docs_recipes.md.8Xf6BsRA.js +45 -0
  130. package/dist/docs/assets/docs_recipes.md.8Xf6BsRA.lean.js +1 -0
  131. package/dist/docs/assets/fixtures_README.md.DzbQ0hft.js +1 -0
  132. package/dist/docs/assets/fixtures_README.md.DzbQ0hft.lean.js +1 -0
  133. package/dist/docs/assets/index.md.DHgL6CzS.js +28 -0
  134. package/dist/docs/assets/index.md.DHgL6CzS.lean.js +1 -0
  135. package/dist/docs/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
  136. package/dist/docs/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
  137. package/dist/docs/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
  138. package/dist/docs/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
  139. package/dist/docs/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
  140. package/dist/docs/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
  141. package/dist/docs/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
  142. package/dist/docs/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
  143. package/dist/docs/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
  144. package/dist/docs/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
  145. package/dist/docs/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
  146. package/dist/docs/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
  147. package/dist/docs/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
  148. package/dist/docs/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
  149. package/dist/docs/assets/packages_docs_README.md.PV1a27nC.js +3 -0
  150. package/dist/docs/assets/packages_docs_README.md.PV1a27nC.lean.js +1 -0
  151. package/dist/docs/assets/packages_viewer_README.md.BynKZSdN.js +3 -0
  152. package/dist/docs/assets/packages_viewer_README.md.BynKZSdN.lean.js +1 -0
  153. package/dist/docs/assets/style.C3E8Ltp6.css +1 -0
  154. package/dist/docs/bernard.svg +94 -0
  155. package/dist/docs/core/README.html +44 -0
  156. package/dist/docs/demo/README.html +47 -0
  157. package/dist/docs/docs/adr/0001-what-the-core-is-built-for.html +30 -0
  158. package/dist/docs/docs/adr/0002-which-harness-a-project-runs-on.html +30 -0
  159. package/dist/docs/docs/adr/0003-how-core-content-reaches-each-harness.html +30 -0
  160. package/dist/docs/docs/adr/0004-how-the-core-uses-external-tools.html +30 -0
  161. package/dist/docs/docs/adr/0005-what-drives-the-pipeline.html +30 -0
  162. package/dist/docs/docs/adr/0006-what-steps-hand-off-to-each-other.html +30 -0
  163. package/dist/docs/docs/adr/0007-how-humans-take-part-in-the-pipeline.html +30 -0
  164. package/dist/docs/docs/adr/0008-where-a-steps-reviewers-run.html +30 -0
  165. package/dist/docs/docs/adr/0009-where-pipeline-state-and-handoffs-live.html +30 -0
  166. package/dist/docs/docs/adr/0010-how-a-project-adapts-the-core.html +30 -0
  167. package/dist/docs/docs/adr/0011-how-the-factory-is-packaged-and-versioned.html +30 -0
  168. package/dist/docs/docs/adr/0012-how-a-steps-failures-are-told-apart-and-routed.html +30 -0
  169. package/dist/docs/docs/adr/0013-when-code-lands.html +30 -0
  170. package/dist/docs/docs/adr/0014-what-a-step-may-change.html +30 -0
  171. package/dist/docs/docs/adr/0015-how-a-project-shapes-its-ticket-flow.html +30 -0
  172. package/dist/docs/docs/adr/0016-which-core-the-factory-repo-runs.html +30 -0
  173. package/dist/docs/docs/adr/0017-where-a-tickets-record-lives.html +30 -0
  174. package/dist/docs/docs/adr/0018-the-products-name.html +30 -0
  175. package/dist/docs/docs/adr/0019-how-a-finding-is-answered-and-judged.html +30 -0
  176. package/dist/docs/docs/adr/0020-how-an-interrupted-run-resumes.html +30 -0
  177. package/dist/docs/docs/adr/0021-how-a-step-runs-in-its-workspace.html +30 -0
  178. package/dist/docs/docs/adr/0022-who-names-the-branches.html +30 -0
  179. package/dist/docs/docs/adr/0023-how-a-version-ships-before-npm.html +30 -0
  180. package/dist/docs/docs/adr/0024-how-the-config-type-carries-its-docs.html +30 -0
  181. package/dist/docs/docs/adr/0025-how-a-steps-handoff-is-written-and-checked.html +30 -0
  182. package/dist/docs/docs/adr/0026-support-for-github-copilot.html +30 -0
  183. package/dist/docs/docs/adr/0027-which-forge-a-project-is-on.html +30 -0
  184. package/dist/docs/docs/adr/0028-several-ticket-flows-in-one-pipeline.html +30 -0
  185. package/dist/docs/docs/adr/0029-the-branch-a-feature-works-on.html +30 -0
  186. package/dist/docs/docs/adr/0030-a-runner-without-coder.html +30 -0
  187. package/dist/docs/docs/adr/0031-how-the-run-viewer-reads-its-runs.html +30 -0
  188. package/dist/docs/docs/adr/0032-how-a-project-gets-the-viewer.html +30 -0
  189. package/dist/docs/docs/adr/0033-the-repos-license.html +30 -0
  190. package/dist/docs/docs/adr/0034-how-code-review-launches-its-reviewers.html +30 -0
  191. package/dist/docs/docs/adr/0035-how-the-package-is-published.html +30 -0
  192. package/dist/docs/docs/adr/0036-what-the-cores-pull-request-skill-does.html +30 -0
  193. package/dist/docs/docs/adr/0037-how-a-team-reads-the-factorys-documentation.html +30 -0
  194. package/dist/docs/docs/adr/0038-where-adapters-live.html +30 -0
  195. package/dist/docs/docs/adr/0039-the-repos-license.html +30 -0
  196. package/dist/docs/docs/adr/0040-how-a-version-is-released.html +30 -0
  197. package/dist/docs/docs/adr/0041-who-opens-the-features-pull-request.html +30 -0
  198. package/dist/docs/docs/adr/0042-how-secrets-are-kept-out-of-committed-records.html +30 -0
  199. package/dist/docs/docs/getting-started.html +41 -0
  200. package/dist/docs/docs/github.html +30 -0
  201. package/dist/docs/docs/overlay.html +64 -0
  202. package/dist/docs/docs/pipeline.html +30 -0
  203. package/dist/docs/docs/recipes.html +74 -0
  204. package/dist/docs/fixtures/README.html +30 -0
  205. package/dist/docs/hashmap.json +1 -0
  206. package/dist/docs/index.html +57 -0
  207. package/dist/docs/packages/docs/README.html +32 -0
  208. package/dist/docs/packages/viewer/README.html +32 -0
  209. package/dist/docs/vp-icons.css +0 -0
  210. package/dist/viewer/assets/Heading-C5Va5XMQ.js +1 -0
  211. package/dist/viewer/assets/SelectionIndicator-DxHiKGW_.js +1 -0
  212. package/dist/viewer/assets/alert-BI2omIo2.js +1 -0
  213. package/dist/viewer/assets/attempt-Dunc67N6.js +1 -0
  214. package/dist/viewer/assets/card-IsZ2ZhSn.js +1 -0
  215. package/dist/viewer/assets/{chunk-WQQL4CDK-DSpW9ZO8.js → chunk-WQQL4CDK-Bd4bkond.js} +1 -1
  216. package/dist/viewer/assets/context-CzP3x3DO.js +7 -0
  217. package/dist/viewer/assets/{dagre-NK4GIZVX-BzzW8pOj.js → dagre-NK4GIZVX-Bhq8pl9Y.js} +1 -1
  218. package/dist/viewer/assets/{index-CnzUhLCJ.css → index-B90XVeY-.css} +1 -1
  219. package/dist/viewer/assets/index-tH-KNwJL.js +10 -0
  220. package/dist/viewer/assets/{mermaid.core-D1FXi6mg.js → mermaid.core-I426eCuN.js} +4 -4
  221. package/dist/viewer/assets/pipeline-Hw_7tocN.js +1 -0
  222. package/dist/viewer/assets/routes-BOl7_JgK.js +1 -0
  223. package/dist/viewer/assets/runs._runId-D4q1mLCt.js +6 -0
  224. package/dist/viewer/assets/runs._runId.index-B-woIhKA.js +1 -0
  225. package/dist/viewer/assets/runs._runId.machines-SkWqrTI6.js +2 -0
  226. package/dist/viewer/assets/runs._runId.sessions-DBVWEbLY.js +1 -0
  227. package/dist/viewer/assets/{stateDiagram-v2-A6IESKWZ-J9KnYRFT.js → stateDiagram-v2-A6IESKWZ-DUwIje_A.js} +1 -1
  228. package/dist/viewer/assets/surface-B6LupC_E.js +1 -0
  229. package/dist/viewer/assets/table-B8t5lQz5.js +1 -0
  230. package/dist/viewer/assets/useEvent-DXj38gmC.js +1 -0
  231. package/dist/viewer/assets/useToggleState-D_NUC1vr.js +2 -0
  232. package/dist/viewer/index.html +5 -5
  233. package/fixtures/starters/azure-devops/.factory/factory.config.ts +2 -7
  234. package/fixtures/starters/azure-devops/.factory/factory.lock.json +7 -8
  235. package/fixtures/starters/github/.factory/factory.config.ts +2 -0
  236. package/fixtures/starters/github/.factory/factory.lock.json +6 -7
  237. package/fixtures/starters/gitlab/.factory/factory.config.ts +9 -0
  238. package/fixtures/starters/gitlab/.factory/factory.lock.json +19 -0
  239. package/fixtures/starters/gitlab/.factory/slots/checks.command.md +1 -0
  240. package/fixtures/starters/gitlab/.factory/slots/context.sources.md +3 -0
  241. package/fixtures/starters/gitlab/.factory/slots/review.detectors.md +5 -0
  242. package/package.json +9 -7
  243. package/core/skills/pull-request/scripts/pr-drive.sh +0 -208
  244. package/dist/viewer/assets/Heading-Dm1Q0gV2.js +0 -1
  245. package/dist/viewer/assets/api-C4lMOGdC.js +0 -7
  246. package/dist/viewer/assets/attempt-lbW5voCR.js +0 -1
  247. package/dist/viewer/assets/card-CtJKot3P.js +0 -1
  248. package/dist/viewer/assets/context-DEB-Y3Iz.js +0 -1
  249. package/dist/viewer/assets/index-Co9KwXRI.js +0 -10
  250. package/dist/viewer/assets/jsx-runtime-CkXRJ162.js +0 -1
  251. package/dist/viewer/assets/pipeline-B-myvM6K.js +0 -1
  252. package/dist/viewer/assets/routes-BzvVN6y-.js +0 -1
  253. package/dist/viewer/assets/runs._runId-BVT2VH_w.js +0 -6
  254. package/dist/viewer/assets/runs._runId.index-C3bbzwAC.js +0 -1
  255. package/dist/viewer/assets/runs._runId.machines-Cj21KdLd.js +0 -2
  256. package/dist/viewer/assets/runs._runId.sessions-Tg6Ou3_9.js +0 -1
  257. package/dist/viewer/assets/table-DzduWiYu.js +0 -1
  258. package/dist/viewer/assets/useEvent-CtTfOmE7.js +0 -1
  259. package/dist/viewer/assets/useToggleState-DfzJwOaN.js +0 -2
  260. package/fixtures/starters/azure-devops/.factory/skills/pull-request/SKILL.md +0 -29
@@ -0,0 +1 @@
1
+ import{_ as t,o as r,c as a,a3 as s}from"./chunks/framework.hgjl6W6o.js";const p=JSON.parse('{"title":"How an interrupted run resumes","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0020-how-an-interrupted-run-resumes.md","filePath":"docs/adr/0020-how-an-interrupted-run-resumes.md"}'),n={name:"docs/adr/0020-how-an-interrupted-run-resumes.md"};function o(d,e,i,h,c,l){return r(),a("div",null,[...e[0]||(e[0]=[s('<h1 id="how-an-interrupted-run-resumes" tabindex="-1">How an interrupted run resumes <a class="header-anchor" href="#how-an-interrupted-run-resumes" aria-label="Permalink to &quot;How an interrupted run resumes&quot;">​</a></h1><p>Accepted, amended by ADR-0021 · 2026-10-21</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>ADR-0017 committed each ticket&#39;s record on its branch, but left resuming an interrupted run open (#31): nothing stores the machines&#39; state, and a stopped run starts again from scratch. <code>persist()</code> saved XState&#39;s snapshot of the whole feature machine, which depends on XState&#39;s internals and on the state ids, and nothing ever wrote it to a file.</p><h2 id="criteria" tabindex="-1">Criteria <a class="header-anchor" href="#criteria" aria-label="Permalink to &quot;Criteria&quot;">​</a></h2><ol><li><strong>Resumes an interrupted run</strong>: nothing stores the machines&#39; state, and a stopped run starts again from scratch.</li><li><strong>Independent of XState</strong>: a snapshot depends on XState&#39;s internals and on the state ids, ties the record to XState&#39;s version, and needs migrations.</li><li><strong>One source of truth</strong>: a state file repeats what the log already says, and the two can disagree.</li><li><strong>Resumes without a remote</strong>: a run without an upstream branch must still resume on the machine that ran it.</li></ol><h2 id="options" tabindex="-1">Options <a class="header-anchor" href="#options" aria-label="Permalink to &quot;Options&quot;">​</a></h2><ul><li><strong>XState&#39;s snapshot, split per actor.</strong> It ties the record to XState&#39;s version and to the state ids, and needs migrations (2).</li><li><strong>Replay the records.</strong> The records become the run&#39;s full event log, and <code>bernard resume &lt;feature&gt;</code> folds their lines into fresh machines, then carries on (1, 2). No state file is stored (3). It reads each record from the local branch first and from the remote branch when there is no local one (4).</li><li><strong>A state file the factory owns, written after each step.</strong> It repeats what the log already says, and the two can disagree (3).</li><li><strong>Read the records from the remote only.</strong> A run without an upstream branch could not be resumed on the machine that ran it (4).</li></ul><h2 id="comparison" tabindex="-1">Comparison <a class="header-anchor" href="#comparison" aria-label="Permalink to &quot;Comparison&quot;">​</a></h2><table tabindex="0"><thead><tr><th>Option</th><th>1. Resumes</th><th>2. Independent of XState</th><th>3. One source of truth</th><th>4. Without a remote</th></tr></thead><tbody><tr><td>XState&#39;s snapshot, per actor</td><td>🟢</td><td>🔴</td><td>⚪</td><td>⚪</td></tr><tr><td>Replay the records</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>A state file the factory owns</td><td>🟢</td><td>🟢</td><td>🔴</td><td>⚪</td></tr><tr><td>Records from the remote only</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🔴</td></tr></tbody></table><p>🟢 meets · 🟠 partly, or at a cost · 🔴 fails · ⚪ not discussed</p><h2 id="decision" tabindex="-1">Decision <a class="header-anchor" href="#decision" aria-label="Permalink to &quot;Decision&quot;">​</a></h2><p>Replay the records. The records become the run&#39;s full event log, and a run resumes by replaying them. No state file is stored.</p><ul><li>The record&#39;s <code>log.jsonl</code> also holds every event that moves a machine besides a step&#39;s result: gate decisions, <code>decide-and-log</code> ones included, and human events (<code>GATE.APPROVED</code>, <code>GATE.CHANGES_REQUESTED</code>, <code>PR.MERGED</code>, <code>RETRY</code>). The orchestrator appends them and the runner commits them like step results.</li><li>The first line of the <code>feature/</code> record holds the pipeline the run started with. A resumed feature replays against it, not against the current config, so it finishes on the graph it started with.</li><li><code>bernard resume &lt;feature&gt;</code> reads the <code>feature/</code> record and each ticket&#39;s record, from the local branch first and from the remote branch when there is no local one. It folds their lines into fresh machines, then carries on. It fails when the feature has no record.</li><li>A step that was running when the run stopped has no result in the record. It is logged as a crash, counts against <code>maxCrashes</code>, and runs again. Amended by ADR-0021: a runner that can find the step, still running or finished, follows it or reads it instead.</li></ul><p>Where gate pull requests point stays open: it belongs with the forge port (#26).</p><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li><code>persist()</code> and <code>restore()</code> are deleted.</li><li><code>bernard run</code> refuses a feature that has a record, and suggests <code>bernard resume</code>, or <code>bernard run --fresh</code> to start again from the base branch: each ticket&#39;s working copy goes and its branches are reset to the base, then the feature&#39;s branch is reset to the base and force-pushed, record included. Clearing the records alone is not enough: a branch left on an earlier run&#39;s commits brings back their code, and misses what the base gained since. Resuming and discarding are always explicit.</li><li>A ticket&#39;s record counts as the feature&#39;s, whether or not the feature&#39;s record lists the ticket: the runner finds the tickets&#39; records on the branches <code>ticket/&lt;feature&gt;-*</code>. A feature&#39;s record that lost its <code>tickets</code> line, to a rejected push for instance, can no longer leave a ticket&#39;s record behind for the next run to append to.</li><li>A ticket&#39;s branch carries a commit for each gate decision and human event, besides one per step.</li></ul>',17)])])}const m=t(n,[["render",o]]);export{p as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as r,c as a,a3 as s}from"./chunks/framework.hgjl6W6o.js";const p=JSON.parse('{"title":"How an interrupted run resumes","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0020-how-an-interrupted-run-resumes.md","filePath":"docs/adr/0020-how-an-interrupted-run-resumes.md"}'),n={name:"docs/adr/0020-how-an-interrupted-run-resumes.md"};function o(d,e,i,h,c,l){return r(),a("div",null,[...e[0]||(e[0]=[s("",17)])])}const m=t(n,[["render",o]]);export{p as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as s,c as o,a3 as a}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"How a step runs in its workspace","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0021-how-a-step-runs-in-its-workspace.md","filePath":"docs/adr/0021-how-a-step-runs-in-its-workspace.md"}'),r={name:"docs/adr/0021-how-a-step-runs-in-its-workspace.md"};function n(i,e,d,c,l,h){return s(),o("div",null,[...e[0]||(e[0]=[a('<h1 id="how-a-step-runs-in-its-workspace" tabindex="-1">How a step runs in its workspace <a class="header-anchor" href="#how-a-step-runs-in-its-workspace" aria-label="Permalink to &quot;How a step runs in its workspace&quot;">​</a></h1><p>Accepted · 2026-10-22</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>The Coder runner waits on each step through one <code>coder ssh</code> session, and reads the step&#39;s outcome from that session&#39;s stdout. A network cut, a sleeping laptop or a restart of the orchestrator&#39;s machine ends the session: the step counts as a crash and runs again, and the session&#39;s work is lost (#48).</p><h2 id="criteria" tabindex="-1">Criteria <a class="header-anchor" href="#criteria" aria-label="Permalink to &quot;Criteria&quot;">​</a></h2><ol><li><strong>Survives the caller</strong>: a network cut or an orchestrator restart doesn&#39;t kill the step, and its result can be read later.</li><li><strong>Fits the ticket workspace</strong>: one long-lived workspace per ticket, reused by its steps, with its database and branch.</li><li><strong>Simplicity</strong>: few moving parts, little to install, easy to debug.</li></ol><h2 id="options" tabindex="-1">Options <a class="header-anchor" href="#options" aria-label="Permalink to &quot;Options&quot;">​</a></h2><ul><li><strong><code>nohup</code>.</strong> As simple (3), but nobody can watch a running step live.</li><li><strong><code>tmux</code> session per step.</strong> The runner starts the driver in a <code>tmux</code> session of the ticket&#39;s workspace and follows it by polling: a dropped connection only delays a poll, and a resumed run picks the step up (1, 2). A human can <code>tmux attach</code> to watch it, and the workspace image ships <code>tmux</code> (3).</li><li><strong><code>systemd-run</code>.</strong> Workspace images often run without systemd (3).</li><li><strong>Coder Tasks.</strong> Ties the runner to a Coder version and its task model, which maps poorly onto the driver&#39;s stream (3).</li><li><strong>AFK.</strong> Each run is a fresh clone, with no workspace or database kept between a ticket&#39;s steps (2). It replaces Coder rather than detaching inside it; it belongs in its own runner adapter (#61).</li></ul><h2 id="comparison" tabindex="-1">Comparison <a class="header-anchor" href="#comparison" aria-label="Permalink to &quot;Comparison&quot;">​</a></h2><table tabindex="0"><thead><tr><th>Option</th><th>1. Survives the caller</th><th>2. Fits the ticket workspace</th><th>3. Simplicity</th></tr></thead><tbody><tr><td><code>nohup</code>, a stream file and a pid file</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td><code>tmux</code> session per step</td><td>🟢</td><td>🟢</td><td>🟠</td></tr><tr><td><code>systemd-run --user</code> transient unit</td><td>🟢</td><td>🟢</td><td>🔴</td></tr><tr><td>Coder Tasks / AgentAPI</td><td>🟢</td><td>🟢</td><td>🔴</td></tr><tr><td>AFK runs</td><td>🟢</td><td>🔴</td><td>🟠</td></tr></tbody></table><p>🟢 meets · 🟠 partly, or at a cost · 🔴 fails · ⚪ not discussed</p><h2 id="decision" tabindex="-1">Decision <a class="header-anchor" href="#decision" aria-label="Permalink to &quot;Decision&quot;">​</a></h2><p>A <code>tmux</code> session per step. A step runs detached, in a <code>tmux</code> session of its ticket&#39;s workspace, and the runner follows it by polling.</p><ul><li>The runner starts the driver in a <code>tmux</code> session named after the step and its attempt, writing its stream to a file in the workspace&#39;s scratch directory, and returns.</li><li>It polls the workspace with short <code>coder ssh</code> calls: it copies the stream&#39;s new lines into the local session log, and checks whether the <code>tmux</code> session still runs. A dropped connection only delays a poll.</li><li>Once the session ends, the outcome is read from the stream file, as it was from stdout. A session that ended without a valid outcome is a crash.</li><li>On <code>bernard resume</code> (ADR-0020), the runner looks for the step&#39;s <code>tmux</code> session and stream file first: a step still running is followed again, a step that finished is read. Only a step the runner cannot find runs again as a crash. This amends ADR-0020.</li><li>A human can <code>tmux attach</code> to a running step to watch it.</li></ul><p>AFK is rejected for this runner only.</p><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li>The workspace image ships <code>tmux</code>.</li><li>The orchestrator&#39;s machine can sleep or restart while steps run; a resumed run picks them up.</li><li>A step&#39;s stream lives in the workspace until the runner has copied it.</li></ul>',17)])])}const m=t(r,[["render",n]]);export{u as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as s,c as o,a3 as a}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"How a step runs in its workspace","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0021-how-a-step-runs-in-its-workspace.md","filePath":"docs/adr/0021-how-a-step-runs-in-its-workspace.md"}'),r={name:"docs/adr/0021-how-a-step-runs-in-its-workspace.md"};function n(i,e,d,c,l,h){return s(),o("div",null,[...e[0]||(e[0]=[a("",17)])])}const m=t(r,[["render",n]]);export{u as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as r,a3 as n}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"Who names the branches","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0022-who-names-the-branches.md","filePath":"docs/adr/0022-who-names-the-branches.md"}'),s={name:"docs/adr/0022-who-names-the-branches.md"};function o(i,e,c,h,d,l){return a(),r("div",null,[...e[0]||(e[0]=[n('<h1 id="who-names-the-branches" tabindex="-1">Who names the branches <a class="header-anchor" href="#who-names-the-branches" aria-label="Permalink to &quot;Who names the branches&quot;">​</a></h1><p>Accepted, amended by ADR-0029 and ADR-0041 · 2026-10-22</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>A ticket&#39;s branch was named by the runner adapter. The orchestrator passed <code>stackOn</code>, the id of the ticket a stacked ticket starts from, and the Coder adapter turned it into <code>ticket/&lt;feature&gt;-&lt;ticket&gt;</code>, with the base branch from its own settings (<code>adapters.runner.base</code>). A step learned neither: a pull request could not target its base branch, and the demo&#39;s <code>fix-ci</code> copied the naming convention into its skill, with <code>main</code> hard-coded. Each new runner adapter (#61, #36) and the forge (#26) would derive the same names again.</p><h2 id="criteria" tabindex="-1">Criteria <a class="header-anchor" href="#criteria" aria-label="Permalink to &quot;Criteria&quot;">​</a></h2><ol><li><strong>Named in one place</strong>: every runner adapter, the forge and every skill that needs a base branch would derive the convention again, while only the orchestrator knows which ticket stacks on which.</li><li><strong>Shown in the request</strong>: a step learned neither its branch nor its base branch, so a pull request could not target its base branch.</li><li><strong>Same on every runner</strong>: a step finds what it needs about git wherever it runs, while git&#39;s tracking setup depends on how each runner adapter creates the branch.</li><li><strong>Nothing without a consumer</strong>: no project asks for a configurable template for branch names (ADR-0013).</li></ol><h2 id="options" tabindex="-1">Options <a class="header-anchor" href="#options" aria-label="Permalink to &quot;Options&quot;">​</a></h2><ul><li><strong>The runner adapter names the branches, as before.</strong> Every adapter, the forge and every skill that needs a base branch derive the convention again, while only the orchestrator knows which ticket stacks on which (1).</li><li><strong>The orchestrator plans the branches, and every step request carries them.</strong> One function names a ticket&#39;s branch, and only the orchestrator knows which ticket stacks on which (1). Every step request carries <code>branch</code>, and a ticket&#39;s also <code>baseBranch</code> (2), so a step finds what it needs about git in its request, wherever it runs (3).</li><li><strong>The step reads its base branch from git, such as an upstream tracking branch or a git config entry.</strong> It depends on how each runner adapter creates the branch (<code>--no-track</code> on Coder, a fresh clone on AFK; 3), and the request does not show it (2).</li><li><strong>A configurable template for branch names.</strong> No project asks for one (4, ADR-0013). The names come from one function, so adding it later touches nothing else.</li></ul><h2 id="comparison" tabindex="-1">Comparison <a class="header-anchor" href="#comparison" aria-label="Permalink to &quot;Comparison&quot;">​</a></h2><table tabindex="0"><thead><tr><th>Option</th><th>1. One place</th><th>2. In the request</th><th>3. Every runner</th><th>4. A consumer</th></tr></thead><tbody><tr><td>The runner adapter names them</td><td>🔴</td><td>🔴</td><td>⚪</td><td>⚪</td></tr><tr><td>The orchestrator plans the branches</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>The step reads git</td><td>⚪</td><td>🔴</td><td>🔴</td><td>⚪</td></tr><tr><td>A configurable template</td><td>⚪</td><td>⚪</td><td>⚪</td><td>🔴</td></tr></tbody></table><p>🟢 meets · 🟠 partly, or at a cost · 🔴 fails · ⚪ not discussed</p><h2 id="decision" tabindex="-1">Decision <a class="header-anchor" href="#decision" aria-label="Permalink to &quot;Decision&quot;">​</a></h2><p>The orchestrator now plans the branches, and every step request carries them:</p><ul><li><code>branch</code>: the branch the step works on. A ticket&#39;s is <code>ticket/&lt;feature&gt;-&lt;ticket&gt;</code>; an upstream step works on the project&#39;s base branch.</li><li><code>baseBranch</code>, for a ticket: the branch its branch starts from and its pull request targets. That is the branch of the ticket it stacks on, or the project&#39;s base branch.</li><li>The project&#39;s base branch is <code>pipeline.baseBranch</code>, <code>main</code> by default, no longer a runner setting: every runner adapter and the forge need it. It is part of the pipeline a run records, so a resumed run keeps it (ADR-0020).</li><li>A ticket&#39;s base branch is set when the ticket starts, from what is merged then (ADR-0015), and stays for its whole flow.</li><li>A runner adapter works on <code>branch</code>, and creates it from <code>baseBranch</code> when the remote does not have it. It no longer knows that tickets stack.</li><li>One function names a ticket&#39;s branch. An adapter that needs it outside a request, such as the record reader of a resumed run, calls it.</li></ul><p>A step finds what it needs about git in its request, wherever it runs: a long-lived workspace, a fresh clone or a sandbox.</p><p>A configurable template for branch names is rejected for now.</p><p>This amends ADR-0015, whose stacked tickets carried <code>stackOn</code> for the runner adapter to map.</p><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li><code>stackOn</code> leaves the request, and <code>adapters.runner.base</code> leaves the config.</li><li>A skill that opens, syncs or checks a pull request takes its base branch from <code>baseBranch</code>. A stacked ticket&#39;s pull request targets the branch of the ticket it stacks on, so its diff is its own. Retargeting it once that ticket merges is the forge&#39;s (#26).</li><li>Upstream steps still work on the base branch, so their records are pushed to it, which a protected base branch refuses. A branch per feature, which tickets start from and target, would fix it by changing the plan alone.</li></ul><p>Amended by ADR-0029: a feature works on its own branch, which its tickets start from and target.</p>',20)])])}const b=t(s,[["render",o]]);export{u as __pageData,b as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as r,a3 as n}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"Who names the branches","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0022-who-names-the-branches.md","filePath":"docs/adr/0022-who-names-the-branches.md"}'),s={name:"docs/adr/0022-who-names-the-branches.md"};function o(i,e,c,h,d,l){return a(),r("div",null,[...e[0]||(e[0]=[n("",20)])])}const b=t(s,[["render",o]]);export{u as __pageData,b as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as a,a3 as s}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"How a version ships before npm","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0023-how-a-version-ships-before-npm.md","filePath":"docs/adr/0023-how-a-version-ships-before-npm.md"}'),i={name:"docs/adr/0023-how-a-version-ships-before-npm.md"};function n(r,e,d,l,c,h){return o(),a("div",null,[...e[0]||(e[0]=[s('<h1 id="how-a-version-ships-before-npm" tabindex="-1">How a version ships before npm <a class="header-anchor" href="#how-a-version-ships-before-npm" aria-label="Permalink to &quot;How a version ships before npm&quot;">​</a></h1><p>Superseded by ADR-0035 · 2026-10-22</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>ADR-0011 publishes the factory as one npm package. We cannot publish it yet: we have no rights on the <code>@theodo-group</code> organisation of npmjs.com. GitHub Packages, the registry ADR-0011 first named, needs a token even to install, and so would a restricted package on npmjs.com: a client&#39;s developers and its CI have neither.</p><h2 id="criteria" tabindex="-1">Criteria <a class="header-anchor" href="#criteria" aria-label="Permalink to &quot;Criteria&quot;">​</a></h2><ol><li><strong>Runs outside this repo now</strong>: until the package is on npm, nothing outside this repo can run the factory.</li><li><strong>Installs without a token</strong>: a client&#39;s developers and its CI have no token, nor access to this private repo.</li><li><strong>Dependencies installed as usual</strong>: a project installs them from npmjs.com, or from its client&#39;s mirror of it, as it does for any Node tool, rather than carrying them and their licences in the tarball.</li><li><strong>We control the releases</strong>: we decide when a version ships and what it is called, while releases are rare.</li></ol><h2 id="options" tabindex="-1">Options <a class="header-anchor" href="#options" aria-label="Permalink to &quot;Options&quot;">​</a></h2><ul><li><strong>Wait for publishing rights on npm.</strong> Nothing outside this repo can run the factory until then (1).</li><li><strong>A GitHub release of this repo.</strong> A project downloads the tarball, commits it and installs it: only the person who upgrades the factory needs access to this repo, and the project&#39;s developers and its CI install from the committed tarball (1, 2). The dependencies in <code>package.json</code> are installed with it, from npmjs.com (3). A release is manual, so that we decide when a version ships and what it is called (4). The tarball is committed in each project&#39;s history, once per upgrade, and a project upgrades by hand.</li><li><strong>GitHub Packages, as ADR-0011 first said.</strong> Every developer and every CI of a client project would need a GitHub token with access to it (2).</li><li><strong>A git dependency on a tag.</strong> The <code>workspace:*</code> dependencies don&#39;t install from git, so the build would have to be committed on a release branch. Installing would still need access to this private repo (2).</li><li><strong>Bundle the dependencies too, so that the tarball installs without a registry.</strong> The tarball would carry jiti, zod, xstate, handlebars, yaml and verkit, and their licences. A project installs them from npmjs.com, or from its client&#39;s mirror of it, as it does for any Node tool (3).</li><li><strong>Release from a merged release pull request, with release-please.</strong> It would derive the version from the Conventional Commits and write a changelog. We want to keep control of the releases while they are rare, and the manual release is two steps (4).</li></ul><h2 id="comparison" tabindex="-1">Comparison <a class="header-anchor" href="#comparison" aria-label="Permalink to &quot;Comparison&quot;">​</a></h2><table tabindex="0"><thead><tr><th>Option</th><th>1. Runs now</th><th>2. No token</th><th>3. Dependencies as usual</th><th>4. Control of releases</th></tr></thead><tbody><tr><td>Wait for npm</td><td>🔴</td><td>⚪</td><td>⚪</td><td>⚪</td></tr><tr><td>A GitHub release</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>GitHub Packages</td><td>⚪</td><td>🔴</td><td>⚪</td><td>⚪</td></tr><tr><td>A git dependency on a tag</td><td>⚪</td><td>🔴</td><td>⚪</td><td>⚪</td></tr><tr><td>Bundle the dependencies</td><td>⚪</td><td>⚪</td><td>🔴</td><td>⚪</td></tr><tr><td>release-please</td><td>⚪</td><td>⚪</td><td>⚪</td><td>🔴</td></tr></tbody></table><p>🟢 meets · 🟠 partly, or at a cost · 🔴 fails · ⚪ not discussed</p><h2 id="decision" tabindex="-1">Decision <a class="header-anchor" href="#decision" aria-label="Permalink to &quot;Decision&quot;">​</a></h2><p>A GitHub release of this repo. Until the package is on npm, a version is a GitHub release of this repo:</p><ul><li>The package is <code>@theodo-group/bernard</code>, the root of this repo. tsdown bundles the workspace packages into <code>dist/</code>: the <code>bernard</code> bin, and the <code>config</code> module a project&#39;s config imports <code>defineConfig</code> from, with its types. It ships <code>core/</code> and the starter overlay next to <code>dist/</code>. The dependencies in <code>package.json</code> are installed with it, from npmjs.com.</li><li>The package stays <code>private</code>, so that nothing publishes it by mistake. <code>pnpm pack</code> builds it all the same.</li><li>A release is manual, so that we decide when a version ships and what it is called. A pull request bumps <code>version</code> in <code>package.json</code>, along with the lock files that record it. Once it is merged, pushing a tag <code>vX.Y.Z</code> runs the Release workflow. It checks the package, then creates the release with the tarball attached and notes generated by GitHub. A version with a suffix, such as <code>0.1.0-alpha.0</code>, is a pre-release.</li><li>A project downloads the tarball into <code>.factory/</code>, commits it, and installs it with pnpm, which pins it in <code>.factory/package.json</code> as <code>file:theodo-group-bernard-&lt;version&gt;.tgz</code>. <code>.factory/</code> stays out of the project&#39;s own pnpm workspace, if it has one, so its lockfile is its own. Only the person who upgrades the factory needs access to this repo. The project&#39;s developers and its CI install from the committed tarball.</li><li><code>install.sh</code>, at the root of this repo, does all of that in one line, which <code>gh</code> fetches from the private repo. The same line upgrades the factory.</li><li><code>bernard</code> resolves the config&#39;s import of <code>@theodo-group/bernard/config</code> itself, so a config also loads where the package is not installed, such as in this repo.</li></ul><p>Bundling the dependencies is rejected for now. Releasing with release-please is rejected for now too: we revisit it when releasing by hand becomes a chore.</p><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li>The tarball, about 50 kB, is committed in each project&#39;s history, once per upgrade.</li><li>The factory&#39;s code is in the client&#39;s repository. The core&#39;s skills already were, rendered in <code>.claude/</code>.</li><li>A client that forbids binaries in git, or imposes its own registry, gets the same tarball pushed to that registry instead.</li><li>Renovate and Dependabot don&#39;t follow a file dependency: until the package is on npm, a project upgrades by hand.</li><li>Once we can publish to npm, a release publishes there too, and a project pins a version instead of a file, without touching its config. That switch remains in #23.</li></ul>',17)])])}const m=t(i,[["render",n]]);export{u as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as a,a3 as s}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"How a version ships before npm","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0023-how-a-version-ships-before-npm.md","filePath":"docs/adr/0023-how-a-version-ships-before-npm.md"}'),i={name:"docs/adr/0023-how-a-version-ships-before-npm.md"};function n(r,e,d,l,c,h){return o(),a("div",null,[...e[0]||(e[0]=[s("",17)])])}const m=t(i,[["render",n]]);export{u as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as i,a3 as r}from"./chunks/framework.hgjl6W6o.js";const f=JSON.parse('{"title":"How the config type carries its docs","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0024-how-the-config-type-carries-its-docs.md","filePath":"docs/adr/0024-how-the-config-type-carries-its-docs.md"}'),s={name:"docs/adr/0024-how-the-config-type-carries-its-docs.md"};function d(n,e,a,c,h,p){return o(),i("div",null,[...e[0]||(e[0]=[r('<h1 id="how-the-config-type-carries-its-docs" tabindex="-1">How the config type carries its docs <a class="header-anchor" href="#how-the-config-type-carries-its-docs" aria-label="Permalink to &quot;How the config type carries its docs&quot;">​</a></h1><p>Accepted · 2026-10-22</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>A project writes <code>.factory/factory.config.ts</code> in its editor, against the types of <code>@theodo/bernard/config</code>. Each field must explain itself there, on hover, without a trip to <a href="./../overlay.html">docs/overlay.md</a>.</p><p>The zod schema in <code>packages/contracts</code> parses the config, and a type inferred from it carries the docs written on the schema&#39;s fields. The published types lose them: tsgo, which generates <code>dist/config.d.mts</code>, drops the comments of inferred types, where TypeScript 5 kept them.</p><h2 id="criteria" tabindex="-1">Criteria <a class="header-anchor" href="#criteria" aria-label="Permalink to &quot;Criteria&quot;">​</a></h2><ol><li><strong>Docs on hover in a project</strong>: each field must explain itself in the project&#39;s editor, which reads <code>dist/</code>, without a trip to the docs.</li><li><strong>The repo&#39;s own compiler</strong>: a second TypeScript in the dependencies, pinned for one plugin by a pnpm override, works around the compiler the repo chose.</li><li><strong>Types the project can check</strong>: the project&#39;s TypeScript checks what we publish under its own options.</li><li><strong>Little to write</strong>: a type generator to write or depend on is a lot for a hundred lines.</li></ol><h2 id="options" tabindex="-1">Options <a class="header-anchor" href="#options" aria-label="Permalink to &quot;Options&quot;">​</a></h2><ul><li><strong>Docs on the schema, types inferred.</strong> They reach the editor in this repo, which reads the sources, but not in a project, which reads <code>dist/</code> (1).</li><li><strong><code>FactoryConfigInput</code> written out in interfaces.</strong> The type <code>defineConfig</code> takes carries a doc on every field, and the hover shows plain types (1). A config field is written twice, in the schema and in <code>FactoryConfigInput</code>, and a test names the one left behind (4).</li><li><strong>Generate the <code>.d.ts</code> with TypeScript 6, which keeps the comments.</strong> A second TypeScript in the dependencies, pinned for one plugin by a pnpm override, to work around the compiler the repo chose (2).</li><li><strong>Publish the sources of the config module as its types.</strong> The project&#39;s TypeScript would check our sources under its own options, and fail on some (3).</li><li><strong>Write the docs as zod descriptions and generate the type from them.</strong> It would also generate the tables of <code>docs/overlay.md</code>. A type generator to write or depend on, for a hundred lines (4).</li></ul><h2 id="comparison" tabindex="-1">Comparison <a class="header-anchor" href="#comparison" aria-label="Permalink to &quot;Comparison&quot;">​</a></h2><table tabindex="0"><thead><tr><th>Option</th><th>1. Docs on hover</th><th>2. The repo&#39;s compiler</th><th>3. Checkable types</th><th>4. Little to write</th></tr></thead><tbody><tr><td>Docs on the schema, types inferred</td><td>🔴</td><td>🟢</td><td>⚪</td><td>🟢</td></tr><tr><td><code>FactoryConfigInput</code> written out</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟠</td></tr><tr><td>TypeScript 6 for the <code>.d.ts</code></td><td>🟢</td><td>🔴</td><td>⚪</td><td>⚪</td></tr><tr><td>Publish the sources</td><td>🟢</td><td>⚪</td><td>🔴</td><td>⚪</td></tr><tr><td>Generate the type from zod descriptions</td><td>🟢</td><td>⚪</td><td>⚪</td><td>🔴</td></tr></tbody></table><p>🟢 meets · 🟠 partly, or at a cost · 🔴 fails · ⚪ not discussed</p><h2 id="decision" tabindex="-1">Decision <a class="header-anchor" href="#decision" aria-label="Permalink to &quot;Decision&quot;">​</a></h2><p><code>FactoryConfigInput</code> written out in interfaces. So <code>FactoryConfigInput</code>, the type <code>defineConfig</code> takes, is written out in interfaces, with a doc on every field. The schema keeps the validation and the defaults, and no docs. A test checks at type level that <code>FactoryConfigInput</code> equals the schema&#39;s input, so the typecheck fails when they drift apart.</p><p>Generating the type from zod descriptions is rejected for now.</p><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li>A config field is written twice: in the schema and in <code>FactoryConfigInput</code>. The test names the one left behind.</li><li>The hover shows plain types, such as <code>harness: &#39;claude-code&#39; | &#39;pi&#39;</code>, rather than zod&#39;s.</li><li>A field&#39;s default is in its doc, since a type cannot carry it.</li><li>The output type, <code>FactoryConfig</code>, stays inferred: only the CLI reads it.</li></ul>',17)])])}const u=t(s,[["render",d]]);export{f as __pageData,u as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as i,a3 as r}from"./chunks/framework.hgjl6W6o.js";const f=JSON.parse('{"title":"How the config type carries its docs","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0024-how-the-config-type-carries-its-docs.md","filePath":"docs/adr/0024-how-the-config-type-carries-its-docs.md"}'),s={name:"docs/adr/0024-how-the-config-type-carries-its-docs.md"};function d(n,e,a,c,h,p){return o(),i("div",null,[...e[0]||(e[0]=[r("",17)])])}const u=t(s,[["render",d]]);export{f as __pageData,u as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as s,a3 as a}from"./chunks/framework.hgjl6W6o.js";const p=JSON.parse(`{"title":"How a step's handoff is written and checked","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0025-how-a-steps-handoff-is-written-and-checked.md","filePath":"docs/adr/0025-how-a-steps-handoff-is-written-and-checked.md"}`),n={name:"docs/adr/0025-how-a-steps-handoff-is-written-and-checked.md"};function d(i,e,r,h,c,l){return o(),s("div",null,[...e[0]||(e[0]=[a('<h1 id="how-a-step-s-handoff-is-written-and-checked" tabindex="-1">How a step&#39;s handoff is written and checked <a class="header-anchor" href="#how-a-step-s-handoff-is-written-and-checked" aria-label="Permalink to &quot;How a step&#39;s handoff is written and checked&quot;">​</a></h1><p>Accepted · 2026-10-22</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>ADR-0006 typed the handoffs, but left them in the skills&#39; text: each skill wrote its own JSON file to its <code>handoffDir</code>, from an example in its instructions, and nothing checked it (#10). Changing a skill could change the format of its handoff as a side effect, and break the next step of the chain without anyone noticing.</p><h2 id="criteria" tabindex="-1">Criteria <a class="header-anchor" href="#criteria" aria-label="Permalink to &quot;Criteria&quot;">​</a></h2><ol><li><strong>The handoff&#39;s format is enforced</strong>: changing a skill could change the format of its handoff as a side effect, and break the next step of the chain without anyone noticing.</li><li><strong>A mismatch caught in the session</strong>: a mismatch found once the session is over costs a whole rerun.</li><li><strong>Each reader its part</strong>: the orchestrator and the next steps read different things, and the outcome&#39;s fields are the same for every skill.</li><li><strong>The contract follows the skill</strong>: the contract belongs to what the skill does, wherever the graph runs it.</li></ol><h2 id="options" tabindex="-1">Options <a class="header-anchor" href="#options" aria-label="Permalink to &quot;Options&quot;">​</a></h2><ul><li><strong>The skill writes its handoff file, checked after the session.</strong> A mismatch found once the session is over costs a whole rerun (2), and each skill&#39;s JSON example drifted from its schema (1).</li><li><strong>One answer constrained by the skill&#39;s schema, with an <code>outcome</code> and a <code>handoff</code>.</strong> A field the next step relies on is enforced in the session, so changing a skill can no longer change its handoff&#39;s format as a side effect (1, 2). Its two keys are for two readers: <code>outcome</code> for the orchestrator, <code>handoff</code> for the next steps (3). The schema is keyed on the skill, and the handoff&#39;s directory on the step (4).</li><li><strong>The outcome inside the handoff.</strong> The orchestrator and the next steps read different things, and the outcome&#39;s fields are the same for every skill (3).</li><li><strong>Schemas keyed on the step.</strong> The contract belongs to what the skill does, wherever the graph runs it (4).</li></ul><h2 id="comparison" tabindex="-1">Comparison <a class="header-anchor" href="#comparison" aria-label="Permalink to &quot;Comparison&quot;">​</a></h2><table tabindex="0"><thead><tr><th>Option</th><th>1. Format enforced</th><th>2. Caught in the session</th><th>3. Each reader its part</th><th>4. Follows the skill</th></tr></thead><tbody><tr><td>The skill writes its file, checked after</td><td>🟠</td><td>🔴</td><td>⚪</td><td>⚪</td></tr><tr><td>One constrained answer, <code>outcome</code> and <code>handoff</code></td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>The outcome inside the handoff</td><td>⚪</td><td>⚪</td><td>🔴</td><td>⚪</td></tr><tr><td>Schemas keyed on the step</td><td>⚪</td><td>⚪</td><td>⚪</td><td>🔴</td></tr></tbody></table><p>🟢 meets · 🟠 partly, or at a cost · 🔴 fails · ⚪ not discussed</p><h2 id="decision" tabindex="-1">Decision <a class="header-anchor" href="#decision" aria-label="Permalink to &quot;Decision&quot;">​</a></h2><p>One answer constrained by the skill&#39;s schema, with an <code>outcome</code> and a <code>handoff</code>. A step&#39;s session ends with one answer, constrained by its skill&#39;s schema, with two keys for two readers:</p><ul><li><code>outcome</code> is what the orchestrator branches on and logs: <code>result</code>, <code>failureClass</code>, <code>error</code>, <code>openQuestions</code>, <code>tickets</code>.</li><li><code>handoff</code> is what the next steps read: the fields of the skill&#39;s schema.</li></ul><p>The schema is keyed on the skill, and the handoff&#39;s directory on the step: two steps that run one skill share its contract and keep their handoffs apart. Every core skill a step runs declares its handoff in <code>@bernard/contracts</code>, one file per skill in <code>handoffs/&lt;skill&gt;.ts</code>: <code>implement</code>, <code>validate</code>, <code>code-review</code>, <code>apply-review</code>. <code>pull-request</code> declares by name the default handoff, <code>{ summary }</code>. An overlay adds fields to them or declares its own skills&#39; (ADR-0010). A skill no step runs has no handoff.</p><p>The schemas carry the rules that one handoff can prove, such as a <code>contested</code> verdict requiring its proof (ADR-0019). The rules that span handoffs stay in the skills&#39; text.</p><p>The runner writes the handoff to <code>handoff.json</code> in the step&#39;s <code>&lt;step&gt;-&lt;attempt&gt;/</code> directory of the ticket&#39;s record, and commits it with the record (ADR-0017). The skills never write it: they read the earlier ones there. They carry no JSON example either: the fields&#39; meanings are in the schemas&#39; <code>.describe()</code>, which the model reads in the schema it is constrained with, and each skill keeps only its rules. Run by hand, <code>code-review</code> prints its handoff.</p><p>An answer that does not match its schema is a crash, not a step result (ADR-0012): the run is retried up to <code>maxCrashes</code> and never logged as an attempt.</p><p>The schema is exported from Zod as draft-7 JSON Schema for both harnesses (Claude Code&#39;s <code>--json-schema</code> rejects the 2020-12 meta-schema):</p><ul><li>On Claude Code, the session runs with <code>--json-schema</code>.</li><li>On Pi, which has no driver yet (ADR-0002, ADR-0013), a Bernard extension loaded with <code>pi -ne -e</code> registers a final-answer tool whose parameters are the JSON Schema. The tool returns <code>terminate: true</code> and sets <code>constrainedSampling: { type: &#39;json_schema&#39;, strict: &#39;prefer&#39; }</code>. The answer is read from <code>tool_execution_start</code> in <code>pi --mode json</code>, then checked again with Zod. Pi&#39;s strict mode cannot constrain the verdicts&#39; <code>oneOf</code>: there, Pi checks the tool&#39;s arguments and the model retries in the same session, which is still cheaper than a full rerun.</li></ul><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li>Changing a skill can no longer change its handoff&#39;s format as a side effect: a field the next step relies on is enforced in the session.</li><li>Handoffs carry no <code>schemaVersion</code> until a project outside this repo consumes the factory (ADR-0009).</li><li>The reviewers&#39; reports, written by subagents, stay files the skill checks, with their own schema.</li></ul>',22)])])}const m=t(n,[["render",d]]);export{p as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as s,a3 as a}from"./chunks/framework.hgjl6W6o.js";const p=JSON.parse(`{"title":"How a step's handoff is written and checked","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0025-how-a-steps-handoff-is-written-and-checked.md","filePath":"docs/adr/0025-how-a-steps-handoff-is-written-and-checked.md"}`),n={name:"docs/adr/0025-how-a-steps-handoff-is-written-and-checked.md"};function d(i,e,r,h,c,l){return o(),s("div",null,[...e[0]||(e[0]=[a("",22)])])}const m=t(n,[["render",d]]);export{p as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as o,o as t,c as s,a3 as a}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"Support for GitHub Copilot","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0026-support-for-github-copilot.md","filePath":"docs/adr/0026-support-for-github-copilot.md"}'),r={name:"docs/adr/0026-support-for-github-copilot.md"};function i(n,e,d,c,l,h){return t(),s("div",null,[...e[0]||(e[0]=[a('<h1 id="support-for-github-copilot" tabindex="-1">Support for GitHub Copilot <a class="header-anchor" href="#support-for-github-copilot" aria-label="Permalink to &quot;Support for GitHub Copilot&quot;">​</a></h1><p>Accepted · 2026-10-26</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>Teams whose organisation standardises on GitHub Copilot cannot run the factory on Claude Code or Pi.</p><h2 id="criteria" tabindex="-1">Criteria <a class="header-anchor" href="#criteria" aria-label="Permalink to &quot;Criteria&quot;">​</a></h2><ol><li><strong>Teams on Copilot can run it</strong>: teams whose organisation standardises on GitHub Copilot cannot run the factory on Claude Code or Pi.</li><li><strong>Files for their harness</strong>: the agents&#39; format differs between harnesses, and a project should see which harness its files are for.</li><li><strong>A constrained answer</strong>: Copilot has no structured output, and an answer that does not match its schema costs a rerun, in premium requests.</li></ol><h2 id="options" tabindex="-1">Options <a class="header-anchor" href="#options" aria-label="Permalink to &quot;Options&quot;">​</a></h2><ul><li><strong>Render into <code>.claude/</code>, which Copilot also reads.</strong> The agents&#39; format differs, and a project would not see which harness its files are for (2).</li><li><strong>Render for the GitHub Copilot CLI, into <code>.github/</code>.</strong> GitHub Copilot becomes a third harness, <code>copilot</code> (1), and <code>bernard sync</code> renders the core into the folders the Copilot CLI reads in a project, <code>.github/skills/</code> and <code>.github/agents/</code> (2). Copilot has no structured output: the prompt carries the answer&#39;s JSON Schema and Zod checks it, so a Copilot answer is only as constrained as the model follows the schema, and the reruns on a mismatch cost premium requests (3).</li><li><strong>Wait for a structured-output flag.</strong> The schema in the prompt, checked after the session, costs a rerun only on a mismatch, as ADR-0025 accepts for Pi&#39;s loose cases (3).</li></ul><h2 id="comparison" tabindex="-1">Comparison <a class="header-anchor" href="#comparison" aria-label="Permalink to &quot;Comparison&quot;">​</a></h2><table tabindex="0"><thead><tr><th>Option</th><th>1. Runs on Copilot</th><th>2. Files for their harness</th><th>3. Constrained answer</th></tr></thead><tbody><tr><td>Render into <code>.claude/</code></td><td>⚪</td><td>🔴</td><td>⚪</td></tr><tr><td>Render for Copilot, into <code>.github/</code></td><td>🟢</td><td>🟢</td><td>🟠</td></tr><tr><td>Wait for a structured-output flag</td><td>🔴</td><td>⚪</td><td>🟢</td></tr></tbody></table><p>🟢 meets · 🟠 partly, or at a cost · 🔴 fails · ⚪ not discussed</p><h2 id="decision" tabindex="-1">Decision <a class="header-anchor" href="#decision" aria-label="Permalink to &quot;Decision&quot;">​</a></h2><p>Render for the GitHub Copilot CLI, into <code>.github/</code>. GitHub Copilot becomes a third harness, <code>copilot</code>, next to the two of ADR-0002, which still holds: one harness per project.</p><p><code>bernard sync</code> renders the core for the GitHub Copilot CLI, into the folders it reads in a project: skills in <code>.github/skills/&lt;name&gt;/SKILL.md</code>, agents as custom agents in <code>.github/agents/&lt;name&gt;.agent.md</code>. The context file is <code>AGENTS.md</code>, and a subagent is launched with the <code>task</code> tool. The neutral tools map to Copilot&#39;s aliases: <code>read</code>, <code>edit</code> for <code>write</code> and <code>edit</code>, <code>execute</code> for <code>bash</code>, <code>search</code> for <code>grep</code>, <code>find</code> and <code>ls</code>. A tier is emitted only when <code>models.copilot</code> maps it, as on Pi.</p><p><code>bernard run</code> drives a step as <code>copilot -p</code> (ADR-0005), with <code>--output-format json</code>, <code>--allow-all</code>, <code>--trust-folder</code> so that the project&#39;s skills and agents load, and <code>--no-ask-user</code>. Two differences with Claude Code shape the driver:</p><ul><li>In prompt mode, Copilot does not expand a skill&#39;s slash command. The prompt names the skill&#39;s file, <code>.github/skills/&lt;skill&gt;/SKILL.md</code>, for the session to read and follow.</li><li>Copilot has no structured output. The prompt carries the answer&#39;s JSON Schema (ADR-0025), the answer is the last ```json block of the session&#39;s last top-level message, and Zod checks it. An answer that does not match is a crash, as on the other harnesses: the step is rerun, never logged as an attempt.</li></ul><p>The session reads its token from <code>COPILOT_GITHUB_TOKEN</code>, <code>GH_TOKEN</code> or <code>GITHUB_TOKEN</code>, and the driver passes it alone to the runner. Copilot reports premium requests, not dollars: the run&#39;s log shows them in place of a cost.</p><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li>The core may only rely on what the three harnesses can express. A skill reads as plain instructions: it cannot count on being invoked by its slash command.</li><li>A Copilot answer is only as constrained as the model follows the schema: the reruns on a mismatch cost premium requests.</li><li><code>.github/</code> holds the project&#39;s workflows too: <code>bernard sync</code> only writes the paths its lock lists, and refuses to overwrite a file of the project&#39;s.</li></ul>',19)])])}const m=o(r,[["render",i]]);export{u as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as o,o as t,c as s,a3 as a}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"Support for GitHub Copilot","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0026-support-for-github-copilot.md","filePath":"docs/adr/0026-support-for-github-copilot.md"}'),r={name:"docs/adr/0026-support-for-github-copilot.md"};function i(n,e,d,c,l,h){return t(),s("div",null,[...e[0]||(e[0]=[a("",19)])])}const m=o(r,[["render",i]]);export{u as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as r,a3 as a}from"./chunks/framework.hgjl6W6o.js";const p=JSON.parse('{"title":"Which forge a project is on","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0027-which-forge-a-project-is-on.md","filePath":"docs/adr/0027-which-forge-a-project-is-on.md"}'),i={name:"docs/adr/0027-which-forge-a-project-is-on.md"};function s(d,e,n,c,h,l){return o(),r("div",null,[...e[0]||(e[0]=[a('<h1 id="which-forge-a-project-is-on" tabindex="-1">Which forge a project is on <a class="header-anchor" href="#which-forge-a-project-is-on" aria-label="Permalink to &quot;Which forge a project is on&quot;">​</a></h1><p>Accepted · 2026-10-26 · rewritten for the forge port (#26), proposed · 2026-11-02</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>The core needs a forge for two things: opening a ticket&#39;s pull request, and reading its checks. Both are in one skill, <code>pull-request</code> (amended: it began as an overlay skill, with <code>fix-ci</code> reading the checks through a <code>ci.checks</code> slot). The starter overlay wrote the skill for GitHub only, and teams on Azure DevOps rewrote it, then a starter per forge overrode it. A project on GitLab had no way at all.</p><h2 id="criteria" tabindex="-1">Criteria <a class="header-anchor" href="#criteria" aria-label="Permalink to &quot;Criteria&quot;">​</a></h2><ol><li><strong>The team&#39;s forge</strong>: GitHub, GitLab and Azure DevOps, the forges the projects in sight use.</li><li><strong>Explicit over implicit</strong>: a team on Azure DevOps would not notice it got the GitHub starter.</li><li><strong>Written once</strong>: the git half of a pull request, and the rule that calls its checks green, are the same on every forge.</li></ol><h2 id="options" tabindex="-1">Options <a class="header-anchor" href="#options" aria-label="Permalink to &quot;Options&quot;">​</a></h2><ul><li><strong>A required <code>--forge</code> at init, and a starter per forge that overrides the <code>pull-request</code> skill.</strong> The forge is not in the config, and a forge is added by adding a starter (1, 2). Each starter writes the pull request and the green rule again, in its own tool&#39;s prose (3).</li><li><strong>An optional <code>--forge</code>, defaulting to GitHub.</strong> A team on Azure DevOps would not notice it got the GitHub starter (2).</li><li><strong>A required <code>adapters.forge</code> in the config, behind the forge port, chosen by a required <code>--forge</code> at init.</strong> <code>bernard pull-request</code> runs the git half and the green rule once, and the forge&#39;s half through its adapter (1, 3). A config without a forge is refused (2).</li></ul><h2 id="comparison" tabindex="-1">Comparison <a class="header-anchor" href="#comparison" aria-label="Permalink to &quot;Comparison&quot;">​</a></h2><table tabindex="0"><thead><tr><th>Option</th><th>1. The team&#39;s forge</th><th>2. Explicit</th><th>3. Written once</th></tr></thead><tbody><tr><td>A starter per forge, overriding the skill</td><td>🟠</td><td>🟢</td><td>🔴</td></tr><tr><td>An optional <code>--forge</code></td><td>🟢</td><td>🔴</td><td>⚪</td></tr><tr><td><code>adapters.forge</code>, behind the forge port</td><td>🟢</td><td>🟢</td><td>🟢</td></tr></tbody></table><p>🟢 meets · 🟠 partly, or at a cost · 🔴 fails · ⚪ not discussed</p><h2 id="decision" tabindex="-1">Decision <a class="header-anchor" href="#decision" aria-label="Permalink to &quot;Decision&quot;">​</a></h2><p>A required <code>adapters.forge</code> in the config, behind the forge port, chosen by a required <code>--forge</code> at init. <code>adapters.forge</code> is <code>{ type: &#39;github&#39; }</code>, <code>{ type: &#39;gitlab&#39;, host? }</code> or <code>{ type: &#39;azure-devops&#39; }</code>, and a config without it is refused. <code>bernard init --forge github|gitlab|azure-devops</code> writes that forge&#39;s starter, from <code>fixtures/starters/&lt;forge&gt;/</code>, which sets it. No starter overrides the <code>pull-request</code> skill: the core&#39;s runs <code>bernard pull-request</code>, which drives the pull request on the forge the config names (ADR-0036). CI syncs every starter against the core.</p><p>The forge is not the tracker. Many teams on Azure DevOps track their tickets in Jira: the forge creates no work item, and the pull request&#39;s title starts with the ticket&#39;s key, which the tracker links by.</p><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li>A forge is added by an adapter of the forge port, one branch of <code>createForge</code> in <code>bin.ts</code> (ADR-0038), a starter and a type of <code>adapters.forge</code>.</li><li>Each adapter signs in as the developer, with the forge&#39;s own tool: <code>gh auth login</code>, <code>glab auth login</code>, <code>az login</code>. A GitLab token forwarded to a Coder workspace waits for a project that needs it.</li></ul>',16)])])}const u=t(i,[["render",s]]);export{p as __pageData,u as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as r,a3 as a}from"./chunks/framework.hgjl6W6o.js";const p=JSON.parse('{"title":"Which forge a project is on","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0027-which-forge-a-project-is-on.md","filePath":"docs/adr/0027-which-forge-a-project-is-on.md"}'),i={name:"docs/adr/0027-which-forge-a-project-is-on.md"};function s(d,e,n,c,h,l){return o(),r("div",null,[...e[0]||(e[0]=[a("",16)])])}const u=t(i,[["render",s]]);export{p as __pageData,u as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as o,a3 as r}from"./chunks/framework.hgjl6W6o.js";const g=JSON.parse('{"title":"Several ticket flows in one pipeline","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0028-several-ticket-flows-in-one-pipeline.md","filePath":"docs/adr/0028-several-ticket-flows-in-one-pipeline.md"}'),i={name:"docs/adr/0028-several-ticket-flows-in-one-pipeline.md"};function s(d,e,n,c,h,l){return a(),o("div",null,[...e[0]||(e[0]=[r('<h1 id="several-ticket-flows-in-one-pipeline" tabindex="-1">Several ticket flows in one pipeline <a class="header-anchor" href="#several-ticket-flows-in-one-pipeline" aria-label="Permalink to &quot;Several ticket flows in one pipeline&quot;">​</a></h1><p>Accepted · 2026-10-26</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>Every ticket went through the one <code>pipeline.delivery</code> graph (ADR-0015). Tickets of one feature need different flows: a fullstack feature splits into backend tickets and frontend tickets, which do not run the same steps.</p><h2 id="criteria" tabindex="-1">Criteria <a class="header-anchor" href="#criteria" aria-label="Permalink to &quot;Criteria&quot;">​</a></h2><ol><li><strong>A flow per kind of ticket</strong>: a fullstack feature splits into backend tickets and frontend tickets, which do not run the same steps.</li><li><strong>Explicit over implicit</strong>: a ticket goes through the graph the upstream step chose for it, never one it fell into, and the config shows every graph a ticket can take.</li><li><strong>Graph names apart from fields</strong>: a graph name must not collide with a field of <code>delivery</code>, such as <code>default</code>.</li><li><strong>Upstream keeps its shape</strong>: upstream is an ordered list of steps with modes, not a graph, and needs its own redesign.</li></ol><h2 id="options" tabindex="-1">Options <a class="header-anchor" href="#options" aria-label="Permalink to &quot;Options&quot;">​</a></h2><ul><li><strong>A <code>default</code> graph for the tickets that name none.</strong> Explicit over implicit. A ticket of a project with several graphs goes through the one the upstream step chose for it, never one it fell into (2). With a single graph there is nothing to choose, so no field is needed.</li><li><strong><code>pipeline.delivery.graphs</code>, every ticket naming its graph when there are several.</strong> It names the project&#39;s ticket graphs, each with its own <code>start</code>, <code>steps</code> and <code>maxFailures</code> (1). A ticket names its graph in its <code>graph</code> field, which the upstream step writes, and with several graphs the orchestrator fails the feature on a ticket that names none (2). The upstream step&#39;s skill must know the graph names.</li><li><strong>The graphs at the root of <code>delivery</code>, next to <code>default</code>.</strong> A graph could not be named <code>default</code>, and graph names would share a namespace with fields (3).</li><li><strong>The upstream steps as a <code>feature</code> graph in <code>graphs</code>.</strong> Upstream is an ordered list of steps with modes, not a graph, and needs its own redesign (4).</li><li><strong>The core&#39;s graph always available, next to the project&#39;s.</strong> Setting <code>graphs</code> replaces it, as setting <code>steps</code> replaced the default graph, so the config shows every graph a ticket can take (2).</li></ul><h2 id="comparison" tabindex="-1">Comparison <a class="header-anchor" href="#comparison" aria-label="Permalink to &quot;Comparison&quot;">​</a></h2><table tabindex="0"><thead><tr><th>Option</th><th>1. A flow per kind</th><th>2. Explicit</th><th>3. Names apart from fields</th><th>4. Upstream&#39;s shape</th></tr></thead><tbody><tr><td>A <code>default</code> graph</td><td>🟢</td><td>🔴</td><td>⚪</td><td>⚪</td></tr><tr><td><code>delivery.graphs</code>, named per ticket</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>The graphs at the root of <code>delivery</code></td><td>🟢</td><td>⚪</td><td>🔴</td><td>⚪</td></tr><tr><td>Upstream as a <code>feature</code> graph</td><td>🟢</td><td>⚪</td><td>⚪</td><td>🔴</td></tr><tr><td>The core&#39;s graph always available</td><td>🟢</td><td>🔴</td><td>⚪</td><td>⚪</td></tr></tbody></table><p>🟢 meets · 🟠 partly, or at a cost · 🔴 fails · ⚪ not discussed</p><h2 id="decision" tabindex="-1">Decision <a class="header-anchor" href="#decision" aria-label="Permalink to &quot;Decision&quot;">​</a></h2><p><code>pipeline.delivery.graphs</code>, every ticket naming its graph when there are several. <code>pipeline.delivery.graphs</code> now names the project&#39;s ticket graphs, each with its own <code>start</code>, <code>steps</code> and <code>maxFailures</code>. Without <code>delivery</code>, the pipeline has one graph, <code>ticket</code>, the core&#39;s default.</p><ul><li>A ticket names the graph it goes through in its <code>graph</code> field, which the upstream step that reports the tickets writes.</li><li>With one graph, a ticket that names none goes through it: the core&#39;s <code>tickets</code> skill keeps reporting tickets without a <code>graph</code>.</li><li>With several graphs, every ticket names one. At handoff, the orchestrator fails the feature on a ticket that names none or names an unknown graph, and the error lists the declared graphs.</li><li>The orchestrator compiles one ticket machine per graph, and spawns each ticket on its graph&#39;s machine.</li><li>The config load checks every graph as ADR-0015 checks the one; <code>bernard sync</code> checks the skills of every graph&#39;s steps.</li><li><code>bernard pipeline</code> draws every graph.</li></ul><p>This amends ADR-0015: <code>delivery.start</code>, <code>delivery.steps</code> and <code>delivery.maxFailures</code> move into each graph of <code>delivery.graphs</code>. It is the first of three steps toward several flows in one repo, before segments shared between graphs and several named pipelines.</p><p>The upstream steps as a <code>feature</code> graph are deferred to the named pipelines.</p><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li>Choosing a ticket&#39;s graph is the job of the upstream step that splits the feature: its skill must know the graph names.</li><li>The ticket machines are registered under one actor key per graph. XState types actors by literal key, so they are all typed as one.</li><li><code>machines.json</code>, which <code>bernard run</code> writes, holds one ticket machine per graph, under <code>tickets</code>.</li></ul>',18)])])}const f=t(i,[["render",s]]);export{g as __pageData,f as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as o,a3 as r}from"./chunks/framework.hgjl6W6o.js";const g=JSON.parse('{"title":"Several ticket flows in one pipeline","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0028-several-ticket-flows-in-one-pipeline.md","filePath":"docs/adr/0028-several-ticket-flows-in-one-pipeline.md"}'),i={name:"docs/adr/0028-several-ticket-flows-in-one-pipeline.md"};function s(d,e,n,c,h,l){return a(),o("div",null,[...e[0]||(e[0]=[r("",18)])])}const f=t(i,[["render",s]]);export{g as __pageData,f as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as r,a3 as s}from"./chunks/framework.hgjl6W6o.js";const m=JSON.parse('{"title":"The branch a feature works on","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0029-the-branch-a-feature-works-on.md","filePath":"docs/adr/0029-the-branch-a-feature-works-on.md"}'),n={name:"docs/adr/0029-the-branch-a-feature-works-on.md"};function o(d,e,h,i,c,u){return a(),r("div",null,[...e[0]||(e[0]=[s('<h1 id="the-branch-a-feature-works-on" tabindex="-1">The branch a feature works on <a class="header-anchor" href="#the-branch-a-feature-works-on" aria-label="Permalink to &quot;The branch a feature works on&quot;">​</a></h1><p>Accepted, amended by ADR-0041 · 2026-10-27</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>A feature&#39;s upstream steps worked on the project&#39;s base branch (ADR-0022). The runner committed the feature&#39;s record there and pushed it, so <code>bernard run</code> wrote straight to <code>main</code>, which a protected base branch refuses, and a run that stopped at its first step left a commit on <code>main</code> all the same. An upstream step&#39;s documents, such as a plan, were not committed at all: a ticket branch cut from the base branch never saw them.</p><h2 id="criteria" tabindex="-1">Criteria <a class="header-anchor" href="#criteria" aria-label="Permalink to &quot;Criteria&quot;">​</a></h2><ol><li><strong>Base branch untouched</strong>: <code>bernard run</code> wrote straight to <code>main</code>, which a protected base branch refuses, and a run that stopped at its first step left a commit on <code>main</code> all the same.</li><li><strong>Documents reach the tickets, once</strong>: a ticket branch cut from the base branch never saw an upstream step&#39;s documents, and a ticket&#39;s pull request to the base branch would carry the feature&#39;s documents and record again.</li><li><strong>No half-done remote</strong>: a rename on the remote is a push and a delete, and a run stopped between them leaves two branches.</li><li><strong>A readable name, given once</strong>: a truncated title reads badly, and a slug given on the command line would have to be repeated on every run and every resume.</li><li><strong>Explicit, no new dependency</strong>: explicit over implicit, so no hidden session outside the records and the write scopes, and no runner that depends on a forge before the forge port (#26) exists.</li></ol><h2 id="options" tabindex="-1">Options <a class="header-anchor" href="#options" aria-label="Permalink to &quot;Options&quot;">​</a></h2><ul><li><strong>The feature branch named from the issue title.</strong> A truncated title reads badly (4), and the runner would depend on a forge before the forge port (#26) exists (5).</li><li><strong>The feature branch named by its first step&#39;s slug.</strong> The runner commits the feature&#39;s folder, with the step&#39;s documents, on the feature&#39;s own branch (1). Until a step returns a slug, the branch is <code>feat/&lt;feature&gt;</code> and has no upstream, so nothing is pushed (3). A ticket starts from the feature branch, and its pull request targets it (2). The slug is a short kebab-case name the feature&#39;s first step returns in its outcome, and a named feature branch keeps its name for good (4). A feature ends with one more merge, the feature branch into the base branch, which the team does.</li><li><strong>The slug given on the command line.</strong> Every run and every resume would have to repeat it (4).</li><li><strong>A slug asked of a model before the first step, outside the pipeline.</strong> Explicit over implicit. A hidden session would run outside the records and the write scopes (5).</li><li><strong>Tickets that start from the feature branch, with pull requests to the base branch.</strong> Each ticket&#39;s pull request would carry the feature&#39;s documents and record again (2).</li><li><strong>The branch pushed from the start, renamed on the remote once the slug is known.</strong> A rename on the remote is a push and a delete, and a run stopped between them leaves two branches (3).</li></ul><h2 id="comparison" tabindex="-1">Comparison <a class="header-anchor" href="#comparison" aria-label="Permalink to &quot;Comparison&quot;">​</a></h2><table tabindex="0"><thead><tr><th>Option</th><th>1. Base untouched</th><th>2. Documents once</th><th>3. Clean remote</th><th>4. Readable name</th><th>5. Explicit</th></tr></thead><tbody><tr><td>Issue title</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🔴</td><td>🔴</td></tr><tr><td>Slug from the first step</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>Slug on the command line</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🔴</td><td>🟢</td></tr><tr><td>Slug from a hidden model</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🔴</td></tr><tr><td>Pull requests to the base branch</td><td>🟢</td><td>🟠</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>Pushed, then renamed</td><td>🟢</td><td>🟢</td><td>🔴</td><td>🟢</td><td>🟢</td></tr></tbody></table><p>🟢 meets · 🟠 partly, or at a cost · 🔴 fails · ⚪ not discussed</p><h2 id="decision" tabindex="-1">Decision <a class="header-anchor" href="#decision" aria-label="Permalink to &quot;Decision&quot;">​</a></h2><p>The feature branch named by its first step&#39;s slug. A feature now has its own branch, the feature branch:</p><ul><li>Its name is <code>feat/&lt;feature&gt;-&lt;slug&gt;</code>. The slug is a short kebab-case name the feature&#39;s first step returns in its outcome, as <code>slug</code>. Only a <code>passed</code> result names the branch: a <code>failed</code> or <code>blocked</code> attempt&#39;s slug is ignored, since the runner publishes passed steps only. The orchestrator keeps the first slug a passed step returns, and ignores later ones. The orchestrator and the runner apply this one rule, <code>slugOf</code> in <code>packages/orchestrator/src/branch.ts</code>.</li><li>Until a step returns a slug, the feature branch is <code>feat/&lt;feature&gt;</code>. Its workspace creates it from the base branch, and it has no upstream, so nothing is pushed: a run that stops before its first step is done leaves nothing on the remote.</li><li>When an upstream step is done, by a session or by a human, the runner commits the feature&#39;s folder, which holds the step&#39;s documents and its record line, names the branch with the slug if a step returned one and the branch has none yet, and pushes it with its upstream.</li><li>A ticket starts from the feature branch, or from the branch of the ticket it stacks on, and its pull request targets it. The team merges the feature branch into the base branch once its tickets are in.</li><li>Every step request carries <code>branch</code>, and a ticket&#39;s also <code>baseBranch</code>, as ADR-0022 plans. <code>stackOn</code> leaves the request. The orchestrator names every branch, with the one function per kind in <code>packages/orchestrator/src/branch.ts</code>.</li><li>A feature&#39;s workspace created again, as a resumed run does, starts on <code>feat/&lt;feature&gt;</code> and checks out the remote&#39;s <code>feat/&lt;feature&gt;-*</code> if there is one.</li><li>A named feature branch keeps its name for good, across <code>bernard run --fresh</code> too: deleting or renaming it on the remote would close the pull requests that target it. A fresh run resets it to the base branch instead. A run asks the runner for the slug of the branch the feature&#39;s working copy is on, and writes it in the <code>start</code> line of the feature&#39;s record. The orchestrator starts with that slug, from the <code>start</code> line again on resume, and ignores the slugs its steps return.</li><li><code>bernard status</code> reads a feature&#39;s record on its feature branch, and each ticket&#39;s on the ticket&#39;s branch. Once the team merged a branch and deleted it, the record is where it was merged: a merged feature&#39;s records are read on the base branch, and a merged ticket&#39;s on the branch its feature&#39;s record is read on.</li></ul><p>This amends ADR-0022, whose upstream steps worked on the base branch, and settles its last consequence.</p><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li>The first upstream step of a project&#39;s pipeline should return a slug; one that does not leaves the feature branch as <code>feat/&lt;feature&gt;</code>.</li><li>A failed upstream step&#39;s leftovers stay uncommitted in the feature&#39;s workspace: only a step that is done is published, and only its feature&#39;s folder.</li><li>A feature ends with one more merge, the feature branch into the base branch, which the team does.</li><li>Feature ids that are prefixes of each other with a hyphen, such as <code>49</code> and <code>49-x</code>, can match each other&#39;s branches on resume: the runner then refuses to pick one.</li></ul>',17)])])}const p=t(n,[["render",o]]);export{m as __pageData,p as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as r,a3 as s}from"./chunks/framework.hgjl6W6o.js";const m=JSON.parse('{"title":"The branch a feature works on","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0029-the-branch-a-feature-works-on.md","filePath":"docs/adr/0029-the-branch-a-feature-works-on.md"}'),n={name:"docs/adr/0029-the-branch-a-feature-works-on.md"};function o(d,e,h,i,c,u){return a(),r("div",null,[...e[0]||(e[0]=[s("",17)])])}const p=t(n,[["render",o]]);export{m as __pageData,p as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as r,c as o,a3 as s}from"./chunks/framework.hgjl6W6o.js";const p=JSON.parse('{"title":"A runner without Coder","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0030-a-runner-without-coder.md","filePath":"docs/adr/0030-a-runner-without-coder.md"}'),a={name:"docs/adr/0030-a-runner-without-coder.md"};function n(i,e,d,h,c,l){return r(),o("div",null,[...e[0]||(e[0]=[s('<h1 id="a-runner-without-coder" tabindex="-1">A runner without Coder <a class="header-anchor" href="#a-runner-without-coder" aria-label="Permalink to &quot;A runner without Coder&quot;">​</a></h1><p>Accepted · 2026-10-28</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p><code>bernard run</code> needed a runner, and the only one was Coder (#95). A developer without a Coder deployment, such as one working on this repo, could not run a feature. The orchestrator only knows the <code>Runner</code> port (ADR-0004): Coder is one adapter among others.</p><h2 id="criteria" tabindex="-1">Criteria <a class="header-anchor" href="#criteria" aria-label="Permalink to &quot;Criteria&quot;">​</a></h2><ol><li><strong>The checkout left as it is</strong>: the feature&#39;s steps in the checkout would hold it for the whole run, and a <code>git switch</code> mid-run would commit the feature&#39;s records on another branch; symlinks to its ignored files would let a ticket change the developer&#39;s own <code>.env</code> and settings, and every other ticket&#39;s.</li><li><strong>Cheap working copies</strong>: a clone is slower to make, heavier on disk, and its branches would need pushing to reach the checkout.</li><li><strong>The project&#39;s own setup</strong>: the dependencies already have their file, the project&#39;s lockfile, and each project starts its services its own way.</li><li><strong>A fresh run keeps the review</strong>: deleting a remote branch closes its pull request and its review, and refusing the fresh run leaves the developer a manual step for every ticket.</li></ol><h2 id="options" tabindex="-1">Options <a class="header-anchor" href="#options" aria-label="Permalink to &quot;Options&quot;">​</a></h2><ul><li><strong>A clone per ticket.</strong> Slower to make, heavier on disk, and its branches would need pushing to reach the checkout (2). Worktrees share the repo&#39;s objects and branches.</li><li><strong>Git worktrees of the project&#39;s checkout.</strong> Every step runs in a worktree, and the checkout itself is left as it is (1). <code>worktree.copy</code> copies the checkout&#39;s ignored files, such as <code>.env</code>, into each new worktree (1), and <code>worktree.setup</code>, the project&#39;s own command, installs the dependencies and makes a database (3). On <code>bernard run --fresh</code>, the branch on the remote is reset to the base, so the pull request open on it stays (4). A session runs with the developer&#39;s tools, credentials and files, and worktrees in parallel share the machine&#39;s local services and ports.</li><li><strong>The feature&#39;s steps in the developer&#39;s checkout, switched to the feature&#39;s branch.</strong> It holds the checkout for the whole run, the feature&#39;s records go on being committed there while the tickets run, and a <code>git switch</code> mid-run would commit them on another branch (1). A worktree gives the feature what a Coder workspace gives it, and the developer reads its documents there or on the pushed branch.</li><li><strong>A setup file of its own in the overlay.</strong> The dependencies already have their file, the project&#39;s lockfile, which every worktree has; only the ignored files and the commands are missing, and the config says both (3).</li><li><strong>Symlinks to the checkout&#39;s ignored files.</strong> A ticket would change the developer&#39;s own <code>.env</code> and settings, and every other ticket&#39;s (1).</li><li><strong>Delete the pushed branch on a fresh run.</strong> Discarding is local, and deleting a remote branch closes its pull request and its review (4).</li><li><strong>Refuse the fresh run while the branch is on the remote.</strong> It leaves the developer a manual step for every ticket (4).</li><li><strong>Services started per ticket by the runner.</strong> Each project starts its services its own way (3). <code>setup</code> lets it give each worktree its own, such as a database per worktree, as the demo does.</li></ul><h2 id="comparison" tabindex="-1">Comparison <a class="header-anchor" href="#comparison" aria-label="Permalink to &quot;Comparison&quot;">​</a></h2><table tabindex="0"><thead><tr><th>Option</th><th>1. Checkout as is</th><th>2. Cheap copies</th><th>3. Project&#39;s setup</th><th>4. Fresh run keeps review</th></tr></thead><tbody><tr><td>A clone per ticket</td><td>🟢</td><td>🔴</td><td>🟢</td><td>🟢</td></tr><tr><td>Worktrees of the checkout</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>The feature&#39;s steps in the checkout</td><td>🔴</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>A setup file in the overlay</td><td>🟢</td><td>🟢</td><td>🔴</td><td>🟢</td></tr><tr><td>Symlinks to ignored files</td><td>🔴</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>Delete the pushed branch</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🔴</td></tr><tr><td>Refuse the fresh run</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟠</td></tr><tr><td>Services started by the runner</td><td>🟢</td><td>🟢</td><td>🔴</td><td>🟢</td></tr></tbody></table><p>🟢 meets · 🟠 partly, or at a cost · 🔴 fails · ⚪ not discussed</p><h2 id="decision" tabindex="-1">Decision <a class="header-anchor" href="#decision" aria-label="Permalink to &quot;Decision&quot;">​</a></h2><p>Git worktrees of the project&#39;s checkout. <code>adapters.runner: { type: &#39;local&#39; }</code> runs every step on the developer&#39;s machine, in git worktrees of the project&#39;s checkout, the folder <code>bernard run --cwd</code> points to. The checkout itself is left as it is.</p><ul><li>The feature&#39;s steps run in a worktree of their own, <code>&lt;scratch&gt;/worktrees/&lt;feature&gt;</code>, on the feature&#39;s branch (ADR-0029). A new feature branch starts from the local branch <code>base</code>, <code>main</code> by default. A worktree made again, as a resumed run does, takes the <code>feat/&lt;feature&gt;-*</code> branch a step named, the checkout&#39;s or the remote&#39;s, which another checkout pushed, and refuses to pick between several.</li><li>Each ticket runs in its own worktree, <code>&lt;scratch&gt;/worktrees/&lt;feature&gt;-&lt;ticket&gt;</code>, on the branch its request names, started from the request&#39;s <code>baseBranch</code> when the branch is new (ADR-0022).</li><li>The records are committed and pushed as on Coder: the feature&#39;s on its branch once a step names it, each ticket&#39;s on its branch from its first line. <code>bernard status</code> reads them on the remote, whatever the runner.</li><li>A worktree only has the files its branch tracks. <code>worktree.copy</code> lists globs of the checkout&#39;s files that git ignores, such as <code>.env</code>, copied into each new worktree; a tracked file is never copied, so the worktree keeps its branch&#39;s.</li><li><code>worktree.setup</code>, an optional shell command, runs in each worktree before its first step of a run, after the copies: it installs the dependencies, makes a database. A resumed run sets up again the worktrees it reuses, whose services may be gone, so <code>setup</code> must be safe to run twice. It runs with a session&#39;s environment, the machine&#39;s basic variables without the factory&#39;s own, since it runs scripts a session may have written, such as a package&#39;s install hook. A failed setup keeps the worktree, with its work, and the next attempt runs it again.</li><li><code>release</code> removes a <code>DONE</code> ticket&#39;s worktree and keeps its branch. A worktree that holds uncommitted work is kept, for the developer to look at. A ticket without a worktree has its record read from its local branch.</li><li><code>bernard run --fresh</code> discards each ticket of the feature: its worktree, even with uncommitted work, and its local branch, so that the ticket starts again from its base. The branch on the remote is reset to the base, so the pull request open on it stays. The Coder runner does the same, deleting the ticket&#39;s workspace instead of a worktree: a workspace is created on the remote branch when there is one.</li><li>Checkpoints, restores, handoffs and records are the git scripts the Coder runner runs, shared in one git runner over a small port each adapter implements: where the working copies are, how to make one ready and run a script in it, and whether a ticket&#39;s branch is read locally or from the remote.</li><li>The runner refuses to start when the checkout has no branch <code>base</code>, or when git does not ignore the scratch folder, where every checkpoint of a worktree would take in the others.</li></ul><p>Services started per ticket by the runner are rejected for now.</p><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li>No isolation from the developer&#39;s machine: a session runs with the developer&#39;s tools, credentials and files, as the developer would run it. Its environment variables are the exception: a session gets the machine&#39;s basic ones, the variables the forges&#39; tools sign in with (<code>GH_TOKEN</code>, <code>GITLAB_TOKEN</code>, <code>AZURE_DEVOPS_EXT_PAT</code> and their config folders) and the harness credentials, not the rest of the factory&#39;s environment, such as a tracker&#39;s token.</li><li>Worktrees in parallel share the machine&#39;s local services, such as one database server, and its ports. Keeping them apart is the project&#39;s business, through <code>worktree.setup</code>.</li><li>A feature branch starts from the local <code>base</code>, so the developer pulls it before a run. Git refuses one branch in two worktrees: the developer checks a feature or ticket branch out in the checkout only once its worktree is removed.</li></ul>',17)])])}const k=t(a,[["render",n]]);export{p as __pageData,k as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as r,c as o,a3 as s}from"./chunks/framework.hgjl6W6o.js";const p=JSON.parse('{"title":"A runner without Coder","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0030-a-runner-without-coder.md","filePath":"docs/adr/0030-a-runner-without-coder.md"}'),a={name:"docs/adr/0030-a-runner-without-coder.md"};function n(i,e,d,h,c,l){return r(),o("div",null,[...e[0]||(e[0]=[s("",17)])])}const k=t(a,[["render",n]]);export{p as __pageData,k as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as r,a3 as s}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"How the run viewer reads its runs","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0031-how-the-run-viewer-reads-its-runs.md","filePath":"docs/adr/0031-how-the-run-viewer-reads-its-runs.md"}'),a={name:"docs/adr/0031-how-the-run-viewer-reads-its-runs.md"};function d(n,e,i,h,c,l){return o(),r("div",null,[...e[0]||(e[0]=[s('<h1 id="how-the-run-viewer-reads-its-runs" tabindex="-1">How the run viewer reads its runs <a class="header-anchor" href="#how-the-run-viewer-reads-its-runs" aria-label="Permalink to &quot;How the run viewer reads its runs&quot;">​</a></h1><p>Accepted, amended by ADR-0032 and ADR-0035 · 2026-10-28</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>The run viewer lived in <code>demo/viewer</code>, a project of its own, and read the demo&#39;s files: the trace <code>bernard run --out</code> wrote, the local Coder&#39;s workspaces and the app&#39;s bare repository. No other project could watch its runs, and a viewer that reads files works only next to the orchestrator that writes them, which breaks once the orchestrator runs remotely (#60).</p><h2 id="criteria" tabindex="-1">Criteria <a class="header-anchor" href="#criteria" aria-label="Permalink to &quot;Criteria&quot;">​</a></h2><ol><li><strong>Works with a remote orchestrator</strong>: a viewer that reads files works only next to the orchestrator that writes them, which breaks once the orchestrator runs remotely (#60).</li><li><strong>Any project can watch its runs</strong>: the viewer read the demo&#39;s files, so no other project could watch its runs.</li><li><strong>Holds what the viewer shows</strong>: the sessions&#39; logs and the machines&#39; snapshots make most of the viewer.</li><li><strong>No import of the CLI</strong>: the viewer&#39;s server reads through the port, and the viewer must not import the CLI that bundles it.</li></ol><h2 id="options" tabindex="-1">Options <a class="header-anchor" href="#options" aria-label="Permalink to &quot;Options&quot;">​</a></h2><ul><li><strong>Keep reading the run&#39;s files, from a folder passed to <code>bernard view</code>.</strong> It ties the viewer to the machine that runs the orchestrator, the very thing #60 removes (1).</li><li><strong>The viewer in the core, reading through a telemetry port in <code>packages/telemetry</code>.</strong> <code>bernard run</code> writes a run through the port, each actor&#39;s latest snapshot and each session&#39;s log included (3), and <code>bernard view</code> reads it back through the same port, so nothing in the viewer names a file. A project&#39;s <code>adapters.telemetry</code> says where its runs go (2), and an adapter that sends them to a store the remote orchestrator writes to implements the same port, which <code>bernard view</code> follows without a change (1). The port lives in <code>packages/telemetry</code>, not in the CLI that bundles the viewer (4).</li><li><strong>Read the runs from the records committed on each ticket&#39;s branch (ADR-0017).</strong> The records hold every attempt and its handoff, but not the sessions&#39; logs nor the machines&#39; snapshots, which make most of the viewer (3).</li><li><strong>The port in <code>packages/cli</code>, next to the runner&#39;s.</strong> The viewer&#39;s server reads through it, and the viewer must not import the CLI that bundles it (4).</li><li><strong>Keep the Coder workspaces and the app&#39;s history in the viewer.</strong> They are the demo&#39;s, read with its own Coder sign-in and bare repository. A project&#39;s viewer would show another project&#39;s Coder, or nothing (2).</li></ul><h2 id="comparison" tabindex="-1">Comparison <a class="header-anchor" href="#comparison" aria-label="Permalink to &quot;Comparison&quot;">​</a></h2><table tabindex="0"><thead><tr><th>Option</th><th>1. Remote orchestrator</th><th>2. Any project</th><th>3. Logs and snapshots</th><th>4. No CLI import</th></tr></thead><tbody><tr><td>Read files from a folder</td><td>🔴</td><td>⚪</td><td>🟢</td><td>⚪</td></tr><tr><td>Telemetry port</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>Read the records</td><td>🟢</td><td>🟢</td><td>🔴</td><td>⚪</td></tr><tr><td>The port in <code>packages/cli</code></td><td>🟢</td><td>🟢</td><td>🟢</td><td>🔴</td></tr><tr><td>Keep Coder and the app&#39;s history</td><td>⚪</td><td>🔴</td><td>🟢</td><td>🟢</td></tr></tbody></table><p>🟢 meets · 🟠 partly, or at a cost · 🔴 fails · ⚪ not discussed</p><h2 id="decision" tabindex="-1">Decision <a class="header-anchor" href="#decision" aria-label="Permalink to &quot;Decision&quot;">​</a></h2><p>The viewer in the core, reading through a telemetry port in <code>packages/telemetry</code>. The viewer moves into the core as <code>packages/viewer</code>, and the telemetry port of ADR-0004 gets its first shape, in <code>packages/telemetry</code>:</p><ul><li><code>bernard run</code> and <code>bernard resume</code> write a run through the port: its trace, its machines, each actor&#39;s latest snapshot, and each session&#39;s readable log and raw output. <code>--out</code> is gone: the project&#39;s <code>adapters.telemetry</code> says where runs go.</li><li><code>bernard view</code> serves the viewer&#39;s built page and its API, and the API reads the runs back through the same port: the runs, a stream&#39;s lines so far, a stream followed live, the machines and the snapshots. Nothing in the viewer names a file.</li><li>The default adapter, <code>jsonl</code>, keeps each run in its own folder of <code>dir</code>, by default <code>runs/</code> in the scratch folder that <code>bernard init</code> git-ignores, named after the time it started.</li><li>A session is named <code>&lt;where&gt;/&lt;name&gt;</code> in the trace and the port, not by the path of its log on the machine that ran it.</li><li>The viewer shows only what the telemetry holds. The live Coder workspaces give way to the environments the trace records, and the app&#39;s git history and ticket documents leave the viewer: they belong to the forge and the records, not to a run.</li><li><code>pnpm build</code> builds the page into <code>dist/viewer/</code> next to the CLI. The package ships it (ADR-0035). Its React packages are build inputs, <code>devDependencies</code> of <code>packages/viewer</code>, never installed by a project.</li></ul><p>The records committed on each ticket&#39;s branch are not read for now: a telemetry adapter may read them later.</p><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li>An adapter that sends the runs elsewhere, such as an observability backend or a store the remote orchestrator writes to, implements the same port, and <code>bernard view</code> follows it without a change.</li><li>The trace&#39;s <code>step.start</code> carries <code>session</code>, not <code>log</code>. A run written before has no <code>session</code>: the viewer lists it, but cannot open its sessions&#39; logs.</li><li><code>machines.json</code>, the snapshots and the session logs are the <code>jsonl</code> adapter&#39;s layout, not a contract: ADR-0028&#39;s <code>machines.json</code> is now the machines the port stores.</li><li>The demo points its app&#39;s telemetry at <code>demo/runs/</code>, and <code>pnpm demo:viewer</code> runs <code>bernard view</code> from the package the replay installed, on the viewer that package ships (ADR-0035).</li></ul>',17)])])}const m=t(a,[["render",d]]);export{u as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as r,a3 as s}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"How the run viewer reads its runs","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0031-how-the-run-viewer-reads-its-runs.md","filePath":"docs/adr/0031-how-the-run-viewer-reads-its-runs.md"}'),a={name:"docs/adr/0031-how-the-run-viewer-reads-its-runs.md"};function d(n,e,i,h,c,l){return o(),r("div",null,[...e[0]||(e[0]=[s("",17)])])}const m=t(a,[["render",d]]);export{u as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as o,a3 as i}from"./chunks/framework.hgjl6W6o.js";const g=JSON.parse('{"title":"How a project gets the viewer","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0032-how-a-project-gets-the-viewer.md","filePath":"docs/adr/0032-how-a-project-gets-the-viewer.md"}'),r={name:"docs/adr/0032-how-a-project-gets-the-viewer.md"};function d(s,e,c,n,h,l){return a(),o("div",null,[...e[0]||(e[0]=[i('<h1 id="how-a-project-gets-the-viewer" tabindex="-1">How a project gets the viewer <a class="header-anchor" href="#how-a-project-gets-the-viewer" aria-label="Permalink to &quot;How a project gets the viewer&quot;">​</a></h1><p>Superseded by ADR-0035 · 2026-10-28</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>ADR-0031 built the run viewer&#39;s page into <code>dist/viewer/</code> and shipped it in the package. The page weighed 6.35 MB unpacked, 1.77 MB gzipped, 96% of the package. A project commits the package&#39;s tarball in <code>.factory/</code> (ADR-0023), so each upgrade added nearly 2 MB to its git history, for a page most of its team never opens.</p><h2 id="criteria" tabindex="-1">Criteria <a class="header-anchor" href="#criteria" aria-label="Permalink to &quot;Criteria&quot;">​</a></h2><ol><li><strong>Light project history</strong>: a project commits the package&#39;s tarball (ADR-0023), so each upgrade added nearly 2 MB to its git history, for a page most of its team never opens.</li><li><strong>One place a version lives</strong>: until the package is on npm, a release of this repo is the one place a version lives (ADR-0023), and <code>gh</code> already reads it.</li><li><strong>Explicit about which viewer runs</strong>: a silent fallback hides which viewer runs.</li></ol><h2 id="options" tabindex="-1">Options <a class="header-anchor" href="#options" aria-label="Permalink to &quot;Options&quot;">​</a></h2><ul><li><strong>Keep shipping the page, only trimmed.</strong> Half a megabyte per upgrade is still history a project keeps for a tool it can fetch (1).</li><li><strong>The page downloaded from the release on demand.</strong> The package leaves the page out (1), and the first <code>bernard view</code> of a version downloads it from the release with <code>gh</code>, as <code>install.sh</code> downloads the package (2). Which viewer is served is decided by where <code>bernard</code> runs, and neither falls back on the other (3). It needs <code>gh</code>, signed in, once per version.</li><li><strong>Fetch the page from a CDN or npm at view time.</strong> Until the package is on npm, a release of this repo is the one place a version lives (ADR-0023), and <code>gh</code> already reads it (2).</li><li><strong>Serve the source&#39;s build when the release has no archive.</strong> A silent fallback hides which viewer runs (3). <code>--viewer</code> makes the choice explicit.</li></ul><h2 id="comparison" tabindex="-1">Comparison <a class="header-anchor" href="#comparison" aria-label="Permalink to &quot;Comparison&quot;">​</a></h2><table tabindex="0"><thead><tr><th>Option</th><th>1. Light history</th><th>2. One place</th><th>3. Explicit viewer</th></tr></thead><tbody><tr><td>Ship the trimmed page</td><td>🟠</td><td>🟢</td><td>🟢</td></tr><tr><td>Download from the release</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>Fetch from a CDN or npm</td><td>🟢</td><td>🔴</td><td>⚪</td></tr><tr><td>Fall back on the source&#39;s build</td><td>🟢</td><td>🟢</td><td>🔴</td></tr></tbody></table><p>🟢 meets · 🟠 partly, or at a cost · 🔴 fails · ⚪ not discussed</p><h2 id="decision" tabindex="-1">Decision <a class="header-anchor" href="#decision" aria-label="Permalink to &quot;Decision&quot;">​</a></h2><p>The page downloaded from the release on demand. This amends the packaging consequence of ADR-0031:</p><ul><li>The package leaves <code>dist/viewer/</code> out. <code>pnpm pack</code> archives the page as <code>bernard-viewer-&lt;version&gt;.tgz</code> (<code>scripts/pack-viewer.sh</code>), which the Release workflow attaches to the release next to the package, and the package ships the archive&#39;s sha256 in <code>dist/viewer.sha256</code>.</li><li>The first <code>bernard view</code> of an installed version downloads that archive with <code>gh release download</code>, as <code>install.sh</code> downloads the package, and keeps it in the user&#39;s cache, <code>$XDG_CACHE_HOME/bernard/viewer/&lt;version&gt;/</code> or <code>~/.cache/bernard/viewer/&lt;version&gt;/</code>, for every project on that version. It caches the archive only when its sha256 is the one the package ships, it holds only files and folders, and it has an <code>index.html</code>: a release asset can be replaced, the committed package cannot. The next <code>bernard view</code> serves the cache and needs no network.</li><li>Which viewer is served is decided by where <code>bernard</code> runs, as <code>packages/cli/src/package.ts</code> decides the rest: from this repo&#39;s source, the <code>dist/viewer/</code> that <code>pnpm build</code> builds; from the package, the release&#39;s. Neither falls back on the other. <code>--viewer &lt;dir&gt;</code> serves the page in a folder instead, for a package of a version not released yet, such as the demo&#39;s.</li><li><code>bernard view</code> fails with what to do when <code>gh</code> is missing, signed out, or the release has no viewer archive.</li><li>The build bundles only the kind of diagram the orchestrator draws, <code>MERMAID_DIAGRAM</code> in <code>packages/viewer/mermaid-diagram.ts</code>, which a test holds to what <code>packages/orchestrator/src/mermaid.ts</code> draws: Mermaid&#39;s other diagram types, its other layout engines and KaTeX are left out of the page, which brings it to about 2 MB unpacked, 0.5 MB gzipped.</li></ul><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li><code>bernard view</code> needs <code>gh</code>, signed in, once per version, where the rest of <code>bernard</code> needs it only to install. A team and its CI still install without <code>gh</code>, but each machine that views a version downloads it once.</li><li>A release created without the archive cannot be viewed: <code>scripts/check-package.sh</code> fails if the package ships <code>dist/viewer/</code>, and the Release workflow builds the archive in the same job as the package.</li><li><code>pnpm demo:viewer</code> passes <code>--viewer</code> the <code>dist/viewer/</code> that the replay&#39;s <code>pnpm pack</code> built.</li><li>Drawing another kind of Mermaid diagram in the viewer means allowing it in <code>packages/viewer/vite.config.ts</code>.</li></ul>',16)])])}const w=t(r,[["render",d]]);export{g as __pageData,w as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as o,a3 as i}from"./chunks/framework.hgjl6W6o.js";const g=JSON.parse('{"title":"How a project gets the viewer","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0032-how-a-project-gets-the-viewer.md","filePath":"docs/adr/0032-how-a-project-gets-the-viewer.md"}'),r={name:"docs/adr/0032-how-a-project-gets-the-viewer.md"};function d(s,e,c,n,h,l){return a(),o("div",null,[...e[0]||(e[0]=[i("",16)])])}const w=t(r,[["render",d]]);export{g as __pageData,w as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as a,a3 as i}from"./chunks/framework.hgjl6W6o.js";const p=JSON.parse(`{"title":"The repo's license","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0033-the-repos-license.md","filePath":"docs/adr/0033-the-repos-license.md"}`),s={name:"docs/adr/0033-the-repos-license.md"};function n(r,e,l,d,c,h){return o(),a("div",null,[...e[0]||(e[0]=[i('<h1 id="the-repo-s-license" tabindex="-1">The repo&#39;s license <a class="header-anchor" href="#the-repo-s-license" aria-label="Permalink to &quot;The repo&#39;s license&quot;">​</a></h1><p>Accepted · 2026-10-29</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>This supersedes the MIT license of ADR-0039. Under MIT, anyone could take the factory and sell it against us: a competing consultancy could sell migrations run with Bernard. We want our clients to keep using and changing the factory freely, without handing it to our competitors, and to choose ourselves when, if ever, it goes fully open.</p><h2 id="criteria" tabindex="-1">Criteria <a class="header-anchor" href="#criteria" aria-label="Permalink to &quot;Criteria&quot;">​</a></h2><ol><li><strong>No resale by competitors</strong>: under MIT, a competing consultancy could sell migrations run with Bernard.</li><li><strong>Free for our clients</strong>: we want our clients to keep using and changing the factory freely.</li><li><strong>We choose when it opens</strong>: we want to choose ourselves when, if ever, it goes fully open.</li><li><strong>Legal clearance</strong>: a client&#39;s legal team clears a standard text faster than one we write.</li></ol><h2 id="options" tabindex="-1">Options <a class="header-anchor" href="#options" aria-label="Permalink to &quot;Options&quot;">​</a></h2><ul><li><strong>Keep MIT.</strong> It lets a competitor sell the factory, or services built on it, against us (1).</li><li><strong>The PolyForm Shield License 1.0.0.</strong> A standard text (4): anyone may use, change and redistribute the factory, as long as they don&#39;t compete with Theodo Group with it (1, 2), and Theodo Group, which holds the copyright, can release it under an open-source license later (3).</li><li><strong>The Functional Source License (FSL).</strong> It allows any use but a competing one, and turns into MIT two years after each release. It explicitly allows professional services, so a competing consultancy could sell migrations with Bernard (1); and its change to MIT is automatic, where we want to open the factory when we are ready (3).</li><li><strong>A copyleft license, such as AGPL-3.0.</strong> For the reasons of ADR-0039: obligations in client repositories (2), and it does not stop a competitor (1).</li><li><strong>A custom license.</strong> A client&#39;s legal team clears a standard text faster than one we write (4).</li></ul><h2 id="comparison" tabindex="-1">Comparison <a class="header-anchor" href="#comparison" aria-label="Permalink to &quot;Comparison&quot;">​</a></h2><table tabindex="0"><thead><tr><th>Option</th><th>1. No resale</th><th>2. Free for clients</th><th>3. We choose</th><th>4. Legal clearance</th></tr></thead><tbody><tr><td>Keep MIT</td><td>🔴</td><td>🟢</td><td>⚪</td><td>🟢</td></tr><tr><td>PolyForm Shield 1.0.0</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>FSL</td><td>🔴</td><td>🟢</td><td>🔴</td><td>🟢</td></tr><tr><td>AGPL-3.0</td><td>🔴</td><td>🔴</td><td>⚪</td><td>🟠</td></tr><tr><td>A custom license</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🔴</td></tr></tbody></table><p>🟢 meets · 🟠 partly, or at a cost · 🔴 fails · ⚪ not discussed</p><h2 id="decision" tabindex="-1">Decision <a class="header-anchor" href="#decision" aria-label="Permalink to &quot;Decision&quot;">​</a></h2><p>The PolyForm Shield License 1.0.0. So everything in this repo is under it:</p><ul><li>One license for the whole repo, as ADR-0039 settled: the code in <code>packages/</code>, <code>core/</code>, the docs, the fixtures and the demo.</li><li>The <code>LICENSE</code> file at the root holds the license&#39;s standard text, unchanged, followed by its required notice, <code>Copyright (c) 2026 Theodo Group</code>. <code>pnpm pack</code> adds it to the package&#39;s tarball, so the terms come with the factory into every project that installs it.</li><li>The <code>license</code> field of every <code>package.json</code> says <code>PolyForm-Shield-1.0.0</code>, its SPDX identifier.</li><li>Still no CLA and no DCO: a contribution comes in under the license the repo goes out under, and its author keeps its copyright.</li></ul><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li>Our clients, and anyone else, may use, change and redistribute the factory, as long as they don&#39;t compete with Theodo Group with it, and pass on the license&#39;s terms and its required notice.</li><li>Using the factory to provide a product or a service that competes with ours, migrations sold by another consultancy included, is not allowed.</li><li>The factory is source-available, not open-source in the OSI&#39;s sense. Theodo Group, which holds the copyright, can release it under an open-source license later.</li><li>What a project writes in its overlay stays its own. The rendered files carry no notice of their own: the package that renders them carries the license.</li></ul>',16)])])}const g=t(s,[["render",n]]);export{p as __pageData,g as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as a,a3 as i}from"./chunks/framework.hgjl6W6o.js";const p=JSON.parse(`{"title":"The repo's license","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0033-the-repos-license.md","filePath":"docs/adr/0033-the-repos-license.md"}`),s={name:"docs/adr/0033-the-repos-license.md"};function n(r,e,l,d,c,h){return o(),a("div",null,[...e[0]||(e[0]=[i("",16)])])}const g=t(s,[["render",n]]);export{p as __pageData,g as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as r,c as i,a3 as o}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"How code review launches its reviewers","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0034-how-code-review-launches-its-reviewers.md","filePath":"docs/adr/0034-how-code-review-launches-its-reviewers.md"}'),a={name:"docs/adr/0034-how-code-review-launches-its-reviewers.md"};function s(n,e,d,c,h,l){return r(),i("div",null,[...e[0]||(e[0]=[o('<h1 id="how-code-review-launches-its-reviewers" tabindex="-1">How code review launches its reviewers <a class="header-anchor" href="#how-code-review-launches-its-reviewers" aria-label="Permalink to &quot;How code review launches its reviewers&quot;">​</a></h1><p>Accepted · 2026-10-29</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p><code>code-review</code> ran one reviewer per dimension, five in all, and relaunched each one up to three times: when it found a <code>blocking</code> or <code>major</code> finding, when it covered less than the bundle, and when it wrote no valid report. A ticket&#39;s graph already re-reviews the code: <code>code-review</code> sends its findings to <code>apply-review</code>, which sends the fixed code back to <code>code-review</code> (ADR-0015). The relaunches inside the step repeated that loop, for up to 15 launches per review.</p><h2 id="criteria" tabindex="-1">Criteria <a class="header-anchor" href="#criteria" aria-label="Permalink to &quot;Criteria&quot;">​</a></h2><ol><li><strong>One re-review loop</strong>: a ticket&#39;s graph already re-reviews the code (ADR-0015), and relaunches inside the step repeated that loop.</li><li><strong>Few launches</strong>: relaunching each reviewer cost up to 15 launches per review.</li><li><strong>Simple over complex</strong>: a lost reviewer already shows in the handoff and the recap.</li></ol><h2 id="options" tabindex="-1">Options <a class="header-anchor" href="#options" aria-label="Permalink to &quot;Options&quot;">​</a></h2><ul><li><strong>Keep a reviewer per dimension, relaunched until it finds nothing new.</strong> It repeats the graph&#39;s loop inside the step (1), and costs up to 15 launches (2).</li><li><strong>Three reviewers, each launched once.</strong> They are launched in one message (2), and the re-review is the graph&#39;s <code>code-review ⇄ apply-review</code> loop (1). A lost reviewer is not retried: it is handed off with what it reviewed (3).</li><li><strong>Relaunch only a lost reviewer.</strong> Simple over complex (3). A lost reviewer already shows in the handoff and the recap.</li></ul><h2 id="comparison" tabindex="-1">Comparison <a class="header-anchor" href="#comparison" aria-label="Permalink to &quot;Comparison&quot;">​</a></h2><table tabindex="0"><thead><tr><th>Option</th><th>1. One loop</th><th>2. Few launches</th><th>3. Simple</th></tr></thead><tbody><tr><td>A reviewer per dimension, relaunched</td><td>🔴</td><td>🔴</td><td>⚪</td></tr><tr><td>Three reviewers, once each</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>Relaunch only a lost reviewer</td><td>🟢</td><td>🟠</td><td>🔴</td></tr></tbody></table><p>🟢 meets · 🟠 partly, or at a cost · 🔴 fails · ⚪ not discussed</p><h2 id="decision" tabindex="-1">Decision <a class="header-anchor" href="#decision" aria-label="Permalink to &quot;Decision&quot;">​</a></h2><p>Three reviewers, each launched once. <code>code-review</code> now runs three reviewers, each launched once, in one message:</p><ul><li><code>review-quality</code>: the shape, the letter of the code style and the names, which are one question, does the code look like the codebase it lives in, at three altitudes. It merges the architecture and naming reviewers.</li><li><code>review-security-bugs</code>: will the change leak, misbehave or be slow. It merges the security reviewer and the bug and performance reviewer.</li><li><code>review-tests</code>: whether the tests are worth anything.</li></ul><p>Each merged reviewer keeps every criterion of the two it replaces, in subsections of one criteria slot. The re-review is the graph&#39;s <code>code-review ⇄ apply-review</code> loop, bounded by the step&#39;s failure budget. Relaunching only a lost reviewer is left out for now.</p><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li>A reviewer that writes no valid report, or covers less than the bundle, is handed off with what it reviewed, <code>0</code> when it was lost, and the review is not called clean. It is not retried, and the step still passes unless a <code>blocking</code> or <code>major</code> finding was kept, even when the lost reviewer is <code>review-security-bugs</code>: a pass can reach the pull request with no security review. The pull request&#39;s description names each such reviewer as a gap in the review, so the people who review it know which dimension they review alone.</li><li>The code-review handoff lists the three reviewers, each once, a lost one included: its schema rejects a handoff that leaves one out.</li><li>The reviewer reports and the code-review handoff count no relaunches.</li><li>The criteria slots are <code>review.quality.criteria</code>, <code>review.security-bugs.criteria</code> and <code>review.tests.criteria</code>. A project that overrode an old slot moves its criteria into the new one.</li></ul>',17)])])}const p=t(a,[["render",s]]);export{u as __pageData,p as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as r,c as i,a3 as o}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"How code review launches its reviewers","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0034-how-code-review-launches-its-reviewers.md","filePath":"docs/adr/0034-how-code-review-launches-its-reviewers.md"}'),a={name:"docs/adr/0034-how-code-review-launches-its-reviewers.md"};function s(n,e,d,c,h,l){return r(),i("div",null,[...e[0]||(e[0]=[o("",17)])])}const p=t(a,[["render",s]]);export{u as __pageData,p as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as a,a3 as i}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"How the package is published","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0035-how-the-package-is-published.md","filePath":"docs/adr/0035-how-the-package-is-published.md"}'),d={name:"docs/adr/0035-how-the-package-is-published.md"};function s(n,e,r,c,l,h){return o(),a("div",null,[...e[0]||(e[0]=[i('<h1 id="how-the-package-is-published" tabindex="-1">How the package is published <a class="header-anchor" href="#how-the-package-is-published" aria-label="Permalink to &quot;How the package is published&quot;">​</a></h1><p>Accepted · 2026-10-30</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>ADR-0023 made each version a GitHub release of this repo, which a project downloaded with <code>gh</code> and committed in <code>.factory/</code>, until we had rights on an organisation of npmjs.com. We have them now on <code>theodo</code>.</p><h2 id="criteria" tabindex="-1">Criteria <a class="header-anchor" href="#criteria" aria-label="Permalink to &quot;Criteria&quot;">​</a></h2><ol><li><strong>No token for client projects</strong>: a restricted package would need an npm token for each developer and each CI of a client project, as ADR-0023 found for GitHub Packages.</li><li><strong>One place a version lives</strong>: a version published in two places, or in two packages, has to be kept in step.</li><li><strong><code>bernard view</code> just works</strong>: a second package makes it fail until the project adds it, and a download on demand adds a cache at view time.</li><li><strong>Install size</strong>: the viewer adds 2 MB unpacked to an install of about 28 MB.</li></ol><h2 id="options" tabindex="-1">Options <a class="header-anchor" href="#options" aria-label="Permalink to &quot;Options&quot;">​</a></h2><ul><li><strong>The GitHub release as the source of a version, mirrored on npm.</strong> A version would live in two places (2).</li><li><strong>A public package on npmjs.com, with the viewer.</strong> A project&#39;s developers and its CI install it from npmjs.com, or from their client&#39;s mirror of it, without access to this repo or <code>gh</code> (1). The GitHub release holds only the notes and the tag (2). <code>bernard view</code> serves the viewer the package ships, with no download, no <code>gh</code> and no cache (3), and the page adds 2 MB unpacked to an install of about 28 MB (4).</li><li><strong>A restricted package.</strong> Each developer and each CI of a client project would need an npm token, as ADR-0023 found for GitHub Packages (1).</li><li><strong>The viewer in a second package, <code>@theodo/bernard-viewer</code>, that a project adds when it wants it.</strong> It saves 7% of an install (4), for a second package to publish, a version to keep in step with the first (2), and a <code>bernard view</code> that fails until the project adds it (3).</li><li><strong>The viewer downloaded from npm on demand, as ADR-0032 did from the release.</strong> The same 7% (4), for a download and a cache at view time (3).</li></ul><h2 id="comparison" tabindex="-1">Comparison <a class="header-anchor" href="#comparison" aria-label="Permalink to &quot;Comparison&quot;">​</a></h2><table tabindex="0"><thead><tr><th>Option</th><th>1. No token</th><th>2. One place</th><th>3. <code>bernard view</code> works</th><th>4. Install size</th></tr></thead><tbody><tr><td>The GitHub release, mirrored on npm</td><td>🟢</td><td>🔴</td><td>🟢</td><td>🟠</td></tr><tr><td>A public package, with the viewer</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟠</td></tr><tr><td>A restricted package</td><td>🔴</td><td>🟢</td><td>🟢</td><td>🟠</td></tr><tr><td>The viewer in a second package</td><td>🟢</td><td>🟠</td><td>🔴</td><td>🟢</td></tr><tr><td>The viewer downloaded on demand</td><td>🟢</td><td>🟢</td><td>🟠</td><td>🟢</td></tr></tbody></table><p>🟢 meets · 🟠 partly, or at a cost · 🔴 fails · ⚪ not discussed</p><h2 id="decision" tabindex="-1">Decision <a class="header-anchor" href="#decision" aria-label="Permalink to &quot;Decision&quot;">​</a></h2><p>A public package on npmjs.com, with the viewer. This supersedes ADR-0023, and ADR-0032 with it:</p><ul><li><code>@theodo/bernard</code> is a public package on npmjs.com. A project installs it in <code>.factory/</code> with <code>pnpm --dir .factory --ignore-workspace add -E @theodo/bernard@&lt;version&gt;</code>, pinned to that exact version, and commits <code>.factory/package.json</code> and its lockfile, not the package. Its developers and its CI install it from npmjs.com, or from their client&#39;s mirror of it, without access to this repo or <code>gh</code>.</li><li>A release is still the pull request that bumps the version (ADR-0040). Once it is merged, the Release workflow publishes the tarball <code>pnpm pack</code> writes, since only pnpm replaces the <code>catalog:</code> versions of the dependencies. It publishes with npm&#39;s trusted publishing: npmjs.com accepts this repo&#39;s <code>release.yml</code> and refuses tokens, so no npm token is stored anywhere. It then creates the GitHub release, which holds the notes and the tag, with no file attached.</li><li>A version with a suffix, such as <code>0.1.0-beta.0</code>, is published under the dist-tag <code>next</code>, any other under <code>latest</code>. While Bernard is in beta, each version is a pre-release.</li><li>The package ships the run viewer again, in <code>dist/viewer/</code>. ADR-0032 left it out because a project committed the tarball of each version, which it no longer does. The page adds 2 MB unpacked to an install of about 28 MB, most of it the dependencies. <code>bernard view</code> serves it, with no download, no <code>gh</code> and no cache, and its <code>--viewer</code> flag goes.</li><li><code>install.sh</code> goes. <code>bernard init</code> makes <code>.factory/package.json</code> an ES module, as <code>install.sh</code> did.</li><li>The first version was published by hand: npm sets a trusted publisher only on a package that exists. The workflow publishes every later one.</li></ul><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li>The package is public: anyone can read the bundled code, the core and the README on npmjs.com, under the PolyForm Shield License (ADR-0033), while this repo stays private.</li><li>npm generates no provenance for a package published from a private repository.</li><li>Renovate and Dependabot follow a project&#39;s pin in <code>.factory/</code>, as ADR-0011 meant them to.</li><li>A version is published once: npm refuses it again, even unpublished. A release that went wrong is fixed by the next version.</li><li>A pull request that changes <code>version</code> by mistake publishes it on merge, as ADR-0040 already warned for the release.</li></ul>',16)])])}const m=t(d,[["render",s]]);export{u as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as a,a3 as i}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"How the package is published","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0035-how-the-package-is-published.md","filePath":"docs/adr/0035-how-the-package-is-published.md"}'),d={name:"docs/adr/0035-how-the-package-is-published.md"};function s(n,e,r,c,l,h){return o(),a("div",null,[...e[0]||(e[0]=[i("",16)])])}const m=t(d,[["render",s]]);export{u as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as s,a3 as r}from"./chunks/framework.hgjl6W6o.js";const p=JSON.parse(`{"title":"What the core's pull-request skill does","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0036-what-the-cores-pull-request-skill-does.md","filePath":"docs/adr/0036-what-the-cores-pull-request-skill-does.md"}`),a={name:"docs/adr/0036-what-the-cores-pull-request-skill-does.md"};function i(n,e,d,h,l,c){return o(),s("div",null,[...e[0]||(e[0]=[r('<h1 id="what-the-core-s-pull-request-skill-does" tabindex="-1">What the core&#39;s pull-request skill does <a class="header-anchor" href="#what-the-core-s-pull-request-skill-does" aria-label="Permalink to &quot;What the core&#39;s pull-request skill does&quot;">​</a></h1><p>Accepted, amended by ADR-0041 · 2026-10-29 · rewritten for the forge port (#26), proposed · 2026-11-02</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>ADR-0015 left merging, resolving conflicts with the base and applying a reviewer&#39;s comments to the projects, and ADR-0004 keeps external tools out of the core. Folding <code>fix-ci</code> into <code>pull-request</code> (ADR-0015, ADR-0027) gave the core a pull-request skill that does more than open the pull request: it needs the branch up to date with its base to get a meaningful green, and it needs a forge, which the forge port gives it (ADR-0027).</p><h2 id="criteria" tabindex="-1">Criteria <a class="header-anchor" href="#criteria" aria-label="Permalink to &quot;Criteria&quot;">​</a></h2><ol><li><strong>A meaningful green</strong>: the pull request needs the branch up to date with its base to get a meaningful green.</li><li><strong>History kept</strong>: the orchestrator commits the ticket&#39;s record on the branch and a stacked base moves under it, so other commits sit on its history.</li><li><strong>Atomic commits</strong>: the ticket&#39;s record and scratch files stay out of the session&#39;s commits.</li><li><strong>Written once</strong>: every project would otherwise write the same skill, for its forge.</li><li><strong>Nothing without a consumer</strong>: nothing consumes the draft state (ADR-0013).</li></ol><h2 id="options" tabindex="-1">Options <a class="header-anchor" href="#options" aria-label="Permalink to &quot;Options&quot;">​</a></h2><ul><li><strong>Leave the sync and the CI to each project&#39;s own skill, as before.</strong> Every project would write the same skill (4).</li><li><strong>A core <code>pull-request</code> skill that merges the base into the branch, through <code>bernard pull-request</code>.</strong> The command merges <code>origin/&lt;baseBranch&gt;</code> into the ticket&#39;s branch before pushing (1), and hands the session the conflicts to resolve when the merge has some. It opens the pull request and reads its checks through the forge port, so the skill names no forge tool (ADR-0004).</li><li><strong>Rebase onto the base and force-push, as the driver it was built from did.</strong> The orchestrator commits the ticket&#39;s record on the branch and a stacked base moves under it, so a force push rewrites history other commits sit on (2).</li><li><strong>Commit everything left in the working copy (<code>git add -A</code>) from the command.</strong> It would stage the ticket&#39;s record and scratch files (3). The command halts on uncommitted work and the session commits it in atomic commits.</li><li><strong>Open the pull request as a draft.</strong> Nothing consumes the draft state (5).</li></ul><h2 id="comparison" tabindex="-1">Comparison <a class="header-anchor" href="#comparison" aria-label="Permalink to &quot;Comparison&quot;">​</a></h2><table tabindex="0"><thead><tr><th>Option</th><th>1. Meaningful green</th><th>2. History kept</th><th>3. Atomic commits</th><th>4. Written once</th><th>5. A consumer</th></tr></thead><tbody><tr><td>Each project&#39;s own skill</td><td>⚪</td><td>⚪</td><td>⚪</td><td>🔴</td><td>⚪</td></tr><tr><td>Merge the base, in the core</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>Rebase and force-push</td><td>🟢</td><td>🔴</td><td>⚪</td><td>🟢</td><td>⚪</td></tr><tr><td>Commit everything from the command</td><td>🟢</td><td>🟢</td><td>🔴</td><td>🟢</td><td>⚪</td></tr><tr><td>A draft pull request</td><td>🟢</td><td>🟢</td><td>⚪</td><td>🟢</td><td>🔴</td></tr></tbody></table><p>🟢 meets · 🟠 partly, or at a cost · 🔴 fails · ⚪ not discussed</p><h2 id="decision" tabindex="-1">Decision <a class="header-anchor" href="#decision" aria-label="Permalink to &quot;Decision&quot;">​</a></h2><p>A core <code>pull-request</code> skill that merges the base into the branch, through <code>bernard pull-request</code>. The skill writes the pull request&#39;s title and body in its handoff folder, then runs <code>bernard pull-request --base &lt;baseBranch&gt; --handoff-dir &lt;handoffDir&gt;</code>. The command&#39;s git half is the same on every forge: it fetches, merges <code>origin/&lt;baseBranch&gt;</code> into the ticket&#39;s branch, and pushes it. It hands the session the conflicts to resolve when the merge has some. That is the only place the core resolves a conflict with the base: it amends the paragraph of ADR-0015 that leaves it to the projects, for the sync of a ticket&#39;s branch before its pull request. The forge&#39;s half, opening or updating the pull request, reading its checks and the log of a failed one, goes through the forge port, to the adapter <code>adapters.forge</code> names (ADR-0027). The skill names no forge tool. Merging the pull request, and applying a reviewer&#39;s comments, stay the team&#39;s.</p><p>ADR-0041 adds a merge mode, where the command lands the ticket on the feature&#39;s branch instead of opening its pull request, and the feature&#39;s pull request, which it keeps in both modes. Both go through the same port: finding, opening and updating a pull request, and reading its description.</p><p>A project without continuous integration, or that wants no sync, overrides the skill (ADR-0010). A pull request with no check registered stops the command with <code>CI_NO_CHECKS</code>: the skill reports <code>blocked</code>, since a project without continuous integration declares it by overriding the skill, not by a judgement at each run. The one exception is a change that no pipeline&#39;s path filter takes in: no check applies to it, and the skill reports <code>passed</code>, saying so.</p><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li>Amends ADR-0015 for the pull-request skill.</li><li>The green rule is written once, in the command, and fail-closed: once the 90 seconds every check has to register are over, two reads of the pull request&#39;s checks, a poll apart, give the same set, every one passed, under a 9-minute cap. Each adapter maps its forge&#39;s states to <code>passed</code>, <code>failed</code>, <code>pending</code> and <code>approval</code>: GitHub counts a skipped or neutral check as passing, GitLab a job allowed to fail, and Azure DevOps reads only its build and status policies, the others being the team&#39;s review. A check that registers after the window is not waited for.</li><li>Azure DevOps has no state for a run that waits on a person: such a build reads as pending, and ends in <code>CI_PENDING</code>, then <code>blocked</code> after three runs.</li><li>The sessions run the <code>bernard</code> the project installs in <code>.factory/</code>: the local runner puts the checkout&#39;s <code>.factory/node_modules/.bin</code> first on a session&#39;s PATH, since a worktree has only the files its branch tracks, and the reference Coder template installs <code>.factory/</code> and does the same in each workspace.</li><li>Azure DevOps cannot serve a build&#39;s log through <code>az</code>: <code>bernard pull-request --failed-log</code> gives the build&#39;s link, and the session reads the cause from the project&#39;s checks run locally.</li></ul>',17)])])}const g=t(a,[["render",i]]);export{p as __pageData,g as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as s,a3 as r}from"./chunks/framework.hgjl6W6o.js";const p=JSON.parse(`{"title":"What the core's pull-request skill does","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0036-what-the-cores-pull-request-skill-does.md","filePath":"docs/adr/0036-what-the-cores-pull-request-skill-does.md"}`),a={name:"docs/adr/0036-what-the-cores-pull-request-skill-does.md"};function i(n,e,d,h,l,c){return o(),s("div",null,[...e[0]||(e[0]=[r("",17)])])}const g=t(a,[["render",i]]);export{p as __pageData,g as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as a,a3 as i}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse(`{"title":"How a team reads the factory's documentation","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0037-how-a-team-reads-the-factorys-documentation.md","filePath":"docs/adr/0037-how-a-team-reads-the-factorys-documentation.md"}`),s={name:"docs/adr/0037-how-a-team-reads-the-factorys-documentation.md"};function n(r,e,d,h,c,l){return o(),a("div",null,[...e[0]||(e[0]=[i('<h1 id="how-a-team-reads-the-factory-s-documentation" tabindex="-1">How a team reads the factory&#39;s documentation <a class="header-anchor" href="#how-a-team-reads-the-factory-s-documentation" aria-label="Permalink to &quot;How a team reads the factory&#39;s documentation&quot;">​</a></h1><p>Accepted · 2026-10-30</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>A project&#39;s team could read the factory&#39;s documentation only on GitHub, where the repo is private, and never for the version its project pins.</p><h2 id="criteria" tabindex="-1">Criteria <a class="header-anchor" href="#criteria" aria-label="Permalink to &quot;Criteria&quot;">​</a></h2><ol><li><strong>The pinned version, for the team</strong>: the team reads the documentation of the version it pins, which GitHub never shows it.</li><li><strong>Plain markdown, one copy</strong>: the pages stay the repo&#39;s markdown, written for GitHub, and a second copy would leave GitHub showing the stale one.</li><li><strong>Tooling the team knows</strong>: the team already runs VitePress on pro-backend.</li><li><strong>A core for every project</strong>: a rule written for this repo stays out of the core.</li></ol><h2 id="options" tabindex="-1">Options <a class="header-anchor" href="#options" aria-label="Permalink to &quot;Options&quot;">​</a></h2><ul><li><strong>Docusaurus, or Starlight on Astro.</strong> Unlike VitePress, which builds plain markdown with no change to it (2), and which the team already runs on pro-backend (3).</li><li><strong>A VitePress site, shipped in the package and served by <code>bernard view</code>.</strong> A team reads the documentation of the version it pins, offline (1). The pages stay the repo&#39;s markdown, written for GitHub (2), and the site is built as BioGroup&#39;s pro-backend builds its own (3). This repo&#39;s overlay makes the documentation part of every change, and the core is unchanged (4). The package grows by about 5 MB unpacked.</li><li><strong>The pages copied into <code>packages/docs/</code>.</strong> Two copies of each guide, and GitHub would show the stale one (2).</li><li><strong>A site hosted online only.</strong> The repo&#39;s GitHub plan has no Pages for it, and an online site shows <code>main</code>, not the pinned version (1).</li><li><strong>The directive in the core&#39;s <code>implement</code> skill, behind a new slot.</strong> Every project would get a rule written for this repo (4).</li></ul><h2 id="comparison" tabindex="-1">Comparison <a class="header-anchor" href="#comparison" aria-label="Permalink to &quot;Comparison&quot;">​</a></h2><table tabindex="0"><thead><tr><th>Option</th><th>1. Pinned version</th><th>2. Plain markdown</th><th>3. Known tooling</th><th>4. Core for every project</th></tr></thead><tbody><tr><td>Docusaurus, or Starlight</td><td>🟢</td><td>🟠</td><td>🔴</td><td>🟢</td></tr><tr><td>A VitePress site in the package</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>The pages copied</td><td>🟢</td><td>🔴</td><td>🟢</td><td>🟢</td></tr><tr><td>A site hosted online only</td><td>🔴</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>The directive in the core</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🔴</td></tr></tbody></table><p>🟢 meets · 🟠 partly, or at a cost · 🔴 fails · ⚪ not discussed</p><h2 id="decision" tabindex="-1">Decision <a class="header-anchor" href="#decision" aria-label="Permalink to &quot;Decision&quot;">​</a></h2><p>A VitePress site, shipped in the package and served by <code>bernard view</code>. The documentation becomes a website, built with VitePress, as BioGroup&#39;s pro-backend builds its own:</p><ul><li><code>packages/docs</code> holds the site&#39;s configuration and theme, not the pages: the pages stay the repo&#39;s markdown, written for GitHub. A link to a page stays on the site; a link to any other file opens it on GitHub at the version&#39;s tag; a link to nothing fails the build.</li><li><code>SECTIONS</code> in <code>packages/docs/.vitepress/config.ts</code> is the sidebar, the nav and the pages; the ADRs come from their folder. <code>NOT_PAGES</code> names the markdown that is never a page: the build fails on a markdown file in neither.</li><li>The pages never use Vue: a <code>{{</code> stays text.</li><li><code>pnpm build</code> builds the site into <code>dist/docs/</code> and the package ships it. <code>bernard view</code> serves it under <code>/docs/</code>, each linking to the other. A team reads the documentation of the version it pins, offline.</li><li><code>pnpm check:docs</code> builds the site and fails on a dead link. CI runs it.</li><li>The site draws only the orchestrator&#39;s kind of Mermaid diagram, with the viewer&#39;s plugin (<code>packages/viewer/mermaid-diagram.ts</code>): the other types would add 2.8 MB.</li><li>This repo&#39;s overlay makes the documentation part of every change, through its <code>context.sources</code> slot. The core is unchanged: another project keeps its documentation its own way.</li></ul><p>Hosting the site online as well stays open.</p><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li>The package grows by about 5 MB unpacked: 2 MB of pages, the rest search index, framework and fonts.</li><li>Every page ships in the public package, the ADRs and CONTRIBUTING with them, while this repo stays private (ADR-0035).</li><li>A link to a file that is not a page leads to GitHub, which a reader outside the organisation cannot open. Built from a branch, it leads to the last release&#39;s tag.</li></ul>',17)])])}const m=t(s,[["render",n]]);export{u as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as a,a3 as i}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse(`{"title":"How a team reads the factory's documentation","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0037-how-a-team-reads-the-factorys-documentation.md","filePath":"docs/adr/0037-how-a-team-reads-the-factorys-documentation.md"}`),s={name:"docs/adr/0037-how-a-team-reads-the-factorys-documentation.md"};function n(r,e,d,h,c,l){return o(),a("div",null,[...e[0]||(e[0]=[i("",17)])])}const m=t(s,[["render",n]]);export{u as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as r,a3 as a}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"Where adapters live","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0038-where-adapters-live.md","filePath":"docs/adr/0038-where-adapters-live.md"}'),d={name:"docs/adr/0038-where-adapters-live.md"};function s(c,e,n,i,l,h){return o(),r("div",null,[...e[0]||(e[0]=[a('<h1 id="where-adapters-live" tabindex="-1">Where adapters live <a class="header-anchor" href="#where-adapters-live" aria-label="Permalink to &quot;Where adapters live&quot;">​</a></h1><p>Accepted · 2026-11-02</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>Code tied to one outside tool (Coder, the local runner, Jira, <code>gh</code>, git) sits next to what the factory does (<code>init</code>, <code>sync</code>, <code>run</code>, <code>status</code>). <code>packages/cli/src/runners/</code> holds the <code>runner</code> port (<code>runner.ts</code>) and the logic every runner shares (<code>git.ts</code>) next to the adapters for Coder (<code>coder*.ts</code>) and the local runner (<code>local.ts</code>, ADR-0030), and the port wires those adapters itself: <code>createRunner</code> imports <code>createCoderRunner</code> and <code>createLocalRunner</code>. <code>packages/cli/src/trackers/</code> does the same for the tracker: <code>createTracker</code> in <code>trackers/tracker.ts</code> imports the Jira adapter (ADR-0035). The orchestrator&#39;s <code>types.ts</code>, <code>nodes.ts</code> and <code>record.ts</code> each hold several concepts. The ports of ADR-0004 keep tool names out of the core, but the layout does not show which file names a tool, so nothing stops a capability from importing an adapter.</p><h2 id="criteria" tabindex="-1">Criteria <a class="header-anchor" href="#criteria" aria-label="Permalink to &quot;Criteria&quot;">​</a></h2><ol><li><strong>The tool&#39;s code shows</strong>: the layout does not show which file names a tool, so nothing tells a capability from a tool.</li><li><strong>An enforced boundary</strong>: nothing stops a capability from importing an adapter, and without a lint rule the boundary would erode with the first such import.</li><li><strong>The port wires no adapter</strong>: <code>createRunner</code> and <code>createTracker</code> import their adapters, so the port wires the adapters itself.</li><li><strong>Light structure</strong>: a package per adapter is too heavy for files the CLI alone uses.</li></ol><h2 id="options" tabindex="-1">Options <a class="header-anchor" href="#options" aria-label="Permalink to &quot;Options&quot;">​</a></h2><ul><li><strong>Adapters in the folder of their port, as <code>runners/coder.ts</code>.</strong> The port then wires the adapters itself (3), and nothing tells a capability from a tool (1).</li><li><strong>A folder per outside tool, <code>&lt;package&gt;/src/adapters/&lt;tool&gt;/</code>, with a lint rule.</strong> Each outside tool gets a folder of its own (1), a <code>noRestrictedImports</code> override in <code>biome.json</code> enforces the boundary (2), and only the entry point, <code>bin.ts</code>, wires adapters (3). Adding an adapter is a new folder plus one case in <code>bin.ts</code> (4).</li><li><strong>One package per adapter.</strong> Too heavy for files the CLI alone uses (4). A package per harness already exists (ADR-0002), and each harness is a whole product of its own.</li><li><strong>A convention without a lint rule.</strong> Nothing would catch a capability importing an adapter, and the boundary would erode with the first such import (2).</li></ul><h2 id="comparison" tabindex="-1">Comparison <a class="header-anchor" href="#comparison" aria-label="Permalink to &quot;Comparison&quot;">​</a></h2><table tabindex="0"><thead><tr><th>Option</th><th>1. Tool shows</th><th>2. Enforced</th><th>3. Port wires none</th><th>4. Light</th></tr></thead><tbody><tr><td>Adapters in the port&#39;s folder</td><td>🔴</td><td>⚪</td><td>🔴</td><td>🟢</td></tr><tr><td><code>adapters/&lt;tool&gt;/</code> with a lint rule</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>One package per adapter</td><td>🟢</td><td>🟢</td><td>⚪</td><td>🔴</td></tr><tr><td>A convention without a lint rule</td><td>🟢</td><td>🔴</td><td>🟢</td><td>🟢</td></tr></tbody></table><p>🟢 meets · 🟠 partly, or at a cost · 🔴 fails · ⚪ not discussed</p><h2 id="decision" tabindex="-1">Decision <a class="header-anchor" href="#decision" aria-label="Permalink to &quot;Decision&quot;">​</a></h2><p>A folder per outside tool, <code>&lt;package&gt;/src/adapters/&lt;tool&gt;/</code>, with a lint rule.</p><ul><li><strong>An adapter lives in <code>&lt;package&gt;/src/adapters/&lt;tool&gt;/</code>, one folder per outside tool.</strong> The CLI has <code>adapters/coder/</code> (<code>runner.ts</code>, <code>agent.ts</code>, <code>cli.ts</code>, <code>setup.ts</code>, <code>local-server.ts</code>: the local Coder server is Coder tooling), <code>adapters/jira/</code> (<code>tracker.ts</code>, <code>adf.ts</code>: ADF is Jira&#39;s document format), <code>adapters/local/runner.ts</code>, the local runner (ADR-0030), and the forges&#39; <code>adapters/github/forge.ts</code>, <code>adapters/gitlab/forge.ts</code> and <code>adapters/azure-devops/forge.ts</code> (ADR-0027).</li><li><strong>A capability never imports an adapter.</strong> The capabilities are the code of what the factory does (<code>init</code>, <code>sync</code>, <code>run</code>, <code>status</code>, the orchestrator) and the ports with the logic every adapter shares: <code>runners/runner.ts</code>, <code>runners/git.ts</code>, <code>trackers/tracker.ts</code>, <code>trackers/mirror.ts</code>, <code>forges/forge.ts</code>.</li><li><strong>An adapter never imports another adapter.</strong></li><li><strong>Only the entry point wires adapters.</strong> <code>packages/cli/src/bin.ts</code> holds <code>createRunner</code> and <code>createTracker</code> and passes them to <code>run</code>, and <code>createForge</code>, whose forge it passes to <code>pull-request</code>.</li><li><strong>Tests may import an adapter to test it.</strong> The rule binds production source.</li><li><strong>A <code>noRestrictedImports</code> override in <code>biome.json</code> enforces the boundary</strong>, as it already does between packages.</li><li><strong>One concept per file.</strong> A file holds one concept, named by the file: its type, its schema and the functions that act on it. Utils files (small helpers, shared types) are exempt. A file that needs a section comment to separate two ideas is two files. The <code>review-quality</code> agent flags an export whose noun does not match its file&#39;s, and such a file.</li></ul><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li>Adding an adapter is a new folder plus one case in <code>bin.ts</code>.</li><li><code>run</code> takes the factories as options, so a test can pass a fake runner or tracker.</li><li>Files move with <code>git mv</code>, so their history follows.</li><li>The orchestrator has more, smaller files.</li></ul>',16)])])}const g=t(d,[["render",s]]);export{u as __pageData,g as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as r,a3 as a}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"Where adapters live","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0038-where-adapters-live.md","filePath":"docs/adr/0038-where-adapters-live.md"}'),d={name:"docs/adr/0038-where-adapters-live.md"};function s(c,e,n,i,l,h){return o(),r("div",null,[...e[0]||(e[0]=[a("",16)])])}const g=t(d,[["render",s]]);export{u as __pageData,g as default};