@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,18 @@
1
+ import{_ as t,o as s,c as o,a3 as a}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"Demo: the factory end to end, on a local Coder or the local runner","description":"","frontmatter":{},"headers":[],"relativePath":"demo/README.md","filePath":"demo/README.md"}'),r={name:"demo/README.md"};function n(i,e,d,c,h,l){return s(),o("div",null,[...e[0]||(e[0]=[a(`<h1 id="demo-the-factory-end-to-end-on-a-local-coder-or-the-local-runner" tabindex="-1">Demo: the factory end to end, on a local Coder or the local runner <a class="header-anchor" href="#demo-the-factory-end-to-end-on-a-local-coder-or-the-local-runner" aria-label="Permalink to &quot;Demo: the factory end to end, on a local Coder or the local runner&quot;">​</a></h1><p>A replayable run of the factory on a small app: <code>bernard run</code> takes the <strong>expense reports</strong> feature from its validated migration plan to the feature&#39;s pull request, each step a real Claude Code or GitHub Copilot session in a Coder workspace on your machine, or, with <code>--local</code>, in a git worktree of the app&#39;s checkout.</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>bernard run (on your machine)</span></span>
2
+ <span class="line"><span>└─ feature machine</span></span>
3
+ <span class="line"><span> ├─ migration-plan done by the dev beforehand: decided and logged</span></span>
4
+ <span class="line"><span> ├─ tickets splits the plan into tickets workspace expense-reports-plan (no database)</span></span>
5
+ <span class="line"><span> └─ tickets 01, then 02 and 03 in parallel, workspace expense-reports-&lt;ticket&gt;, Postgres of its own</span></span>
6
+ <span class="line"><span> 02 and 03 start from the feature branch, which holds 01</span></span>
7
+ <span class="line"><span> 02, 03 (api): implement → validate → code-review ⇄ apply-review → pull-request → human-review → DONE (auto-approved)</span></span>
8
+ <span class="line"><span> 01 (storage): implement → validate → pull-request → DONE (no gate: lands on the feature branch)</span></span></code></pre></div><h2 id="what-it-shows" tabindex="-1">What it shows <a class="header-anchor" href="#what-it-shows" aria-label="Permalink to &quot;What it shows&quot;">​</a></h2><ul><li><strong>The orchestrator drives</strong>, not a conversation: the XState machines route every result, count the failures and start the tickets in dependency order (ADR-0005, ADR-0015).</li><li><strong>Several ticket graphs</strong> (ADR-0028): the app&#39;s config declares two, <code>api</code> and <code>storage</code>, which skips the code review. The demo&#39;s <code>tickets</code> skill sends ticket 01, which adds no route, through <code>storage</code>, and the others through <code>api</code>. <code>bernard pipeline --cwd demo/app</code> draws both, and the viewer draws each ticket&#39;s lane on its own graph.</li><li><strong>A branch per feature</strong> (ADR-0029): the run never commits on <code>main</code>. The feature&#39;s record lands on <code>feat/expense-reports</code>, pushed once its upstream step is done, and each ticket starts from it. The demo&#39;s migration plan is validated beforehand, so no step returns a slug and the branch keeps its unnamed form; this repo&#39;s own <code>plan</code> step names it, as <code>feat/&lt;issue&gt;-&lt;slug&gt;</code>.</li><li><strong>One graph with a gate, one without</strong> (ADR-0041): <code>storage</code> has no gate after <code>pull-request</code>, so ticket 01 merges itself: its <code>pull-request</code> step lands its commits on <code>feat/expense-reports</code> by a fast-forward push, with no pull request of its own, then the runner lands its record there and deletes <code>ticket/&lt;feature&gt;-01</code>. <code>api</code> keeps its <code>human-review</code> gate, so tickets 02 and 03 keep a pull request to the feature branch, merged by nobody here. Both write their section of the feature&#39;s pull request, <code>feature-section.md</code>, which the demo, with no forge, keeps in the ticket&#39;s record.</li><li><strong>Stacking.</strong> A ticket that merged itself counts as merged, so 02 and 03 start from the feature branch, which holds 01, and never stack on it. A ticket that depends on an <code>api</code> ticket, done but not merged, starts on its branch: the Coder adapter creates <code>ticket/&lt;feature&gt;-03</code> from <code>ticket/&lt;feature&gt;-02</code>. The demo&#39;s <code>tickets</code> skill gives each ticket one dependency at most, since a ticket cannot stack on two.</li><li><strong>Each step is a fresh <code>claude -p &quot;/&lt;skill&gt;&quot;</code> session.</strong> The skill comes from the project&#39;s rendered <code>.claude/</code>, the request and the outcome contract from the driver, the outcome as structured output.</li><li><strong>GitHub Copilot as the harness</strong> (ADR-0026): <code>pnpm demo:replay --harness copilot</code> renders the app for GitHub Copilot, into <code>.github/skills/</code> and <code>.github/agents/</code>, with its instructions in <code>AGENTS.md</code>, and runs each step as a <code>copilot -p</code> session. The session reads its skill&#39;s file, launches the reviewers with the <code>task</code> tool, and ends on a JSON answer the driver checks against the skill&#39;s schema.</li><li><strong>The runner port.</strong> <code>bernard run</code> knows nothing of Coder: the app&#39;s <code>.factory/factory.config.ts</code> names the runner in <code>adapters.runner</code>. The Coder adapter gives each ticket in flight its own workspace and database, and stops it once the ticket is <code>DONE</code>.</li><li><strong>The local Coder.</strong> <code>pnpm demo:up</code> starts it with <code>bernard coder up</code>: the <code>bernard-coder</code> container on Docker, its admin, and the Coder CLI signed in to it. The demo&#39;s scripts and viewer keep that sign-in in <code>demo/.state/coder</code> (<code>CODER_URL</code> and <code>CODER_CONFIG_DIR</code>, exported by <code>demo/scripts/env.sh</code>), so they never touch your own Coder CLI sign-in and only ever delete workspaces on the local Coder. A project whose runner sets <code>local: true</code>, such as this repo, gets the same from <code>bernard run</code>.</li><li><strong>The local runner</strong> (ADR-0030): <code>pnpm demo:replay --local</code> delivers the same feature without Coder. The feature&#39;s steps run in a git worktree on its branch, and each ticket&#39;s in a worktree of its own, all under <code>tmp/factory/worktrees/</code> of the app&#39;s checkout, which stays on <code>main</code>. One Postgres serves every worktree, with a database each. Each new worktree gets a copy of the checkout&#39;s <code>.env</code>, which git ignores (<code>worktree.copy</code>), and the project&#39;s <code>worktree.setup</code> points it at the worktree&#39;s own database (<a href="https://github.com/theodo-group/bernard/blob/v0.1.0-beta.1/demo/scripts/local-setup.sh" target="_blank" rel="noreferrer">scripts/local-setup.sh</a>): the runner shares the machine&#39;s services between worktrees in parallel, and keeping them apart is the project&#39;s business.</li><li><strong>Resuming a stopped run</strong> (ADR-0020): interrupt <code>pnpm demo:replay</code>, then <code>pnpm demo:replay --resume</code> replays the records into fresh machines and carries on. No finished step runs again; each step the run stopped in runs again, as a crash. The viewer shows the resumed run as the continuation of the stopped one, not as a new flow: one page with every attempt of both runs, a <code>↻</code> where a step carried on after the resume, the <em>Reprises</em> card on what each run ended on and what <code>bernard resume</code> sent, and the stopped run&#39;s URL leading to the resumed one.</li><li><strong>Answering a waiting ticket</strong> (ADR-0007): <code>pnpm demo:replay --gates</code> makes the <code>human-review</code> gates block: ticket 01 has none and lands, then the run stalls at ticket 02&#39;s pull request and prints the <code>bernard resume</code> command that approves it. <code>pnpm demo:replay --resume --ticket 02 --approve</code> sends the approval, recorded in ticket 02&#39;s record, and the run carries on, ticket 03 stalling at its own gate. <code>--request-changes</code>, <code>--retry</code> and <code>--answer</code> go through the same way, refused when the ticket waits on something else.</li><li><strong>Status from the records</strong>: at the end of each replay, <code>bernard status</code> lists the features from the records on the app&#39;s remote, then shows where each ticket of expense reports stands. It runs nothing and opens no workspace. In a terminal, <code>bernard status --cwd demo/.state/app</code> lets you pick a feature from the list.</li><li><strong>The package</strong> (ADR-0035): the replay packs the factory and installs the tarball in the app&#39;s <code>.factory/</code> with <code>pnpm add</code>, as a project installs it from npm. <code>bernard sync</code> and <code>bernard run</code> run from that install, not from this repo&#39;s source, and the replay names its version.</li><li><strong>Handoffs the overlay extends</strong> (ADR-0010, ADR-0025): <code>handoffs</code> in the app&#39;s config adds <code>routes</code> to the core&#39;s <code>validate</code> handoff, and <code>baseBranch</code>, <code>hadConflicts</code> and <code>fixRounds</code> to the overridden <code>pull-request</code>&#39;s: an override replaces a skill&#39;s text, not its contract. The sessions fill the new fields from their <code>.describe()</code> alone, without a word in the skills. <code>pnpm demo:replay --handoffs</code> prints each step&#39;s handoff under its result line; the viewer shows it in the session&#39;s <em>Handoff</em> tab, and the ticket&#39;s record keeps it as <code>handoff.json</code>.</li><li><strong>The forge port</strong> (ADR-0027, ADR-0036): <code>pnpm demo:starters</code> runs <code>bernard init --forge github</code>, <code>--forge gitlab</code> and <code>--forge azure-devops</code> in scratch projects, syncs them on GitHub Copilot (<code>pnpm demo:starters claude-code</code> for Claude Code), and prints the <code>adapters.forge</code> each config sets, the files rendered, and the one command every <code>pull-request</code> skill runs, <code>bernard pull-request</code>, whatever the forge. No starter overrides the skill. It needs neither Coder nor a token. GitLab and Azure DevOps themselves are shown by the tests of <code>bernard pull-request</code>, which run on a fake <code>glab</code> and a fake <code>az</code> (<a href="https://github.com/theodo-group/bernard/blob/v0.1.0-beta.1/packages/cli/test/pull-request.test.ts" target="_blank" rel="noreferrer">packages/cli/test/pull-request.test.ts</a>).</li><li><strong>Secrets kept out of the records</strong> (ADR-0042): <code>demo/scripts/secrets.sh</code> runs the local runner&#39;s scenarios of <code>packages/cli/test/runner-secrets.test.ts</code> and prints each one&#39;s result: a record line and a handoff that leak a secret variable&#39;s value and a GitHub key are committed with both <code>[REDACTED:…]</code>, and a file of the record that holds an AWS key or a secret variable&#39;s value, or is named after one, and a slug that holds one, are refused, naming the rule and never the value, with nothing committed or pushed. It then runs <code>bernard run</code> on a Jira project, whose token no overlay names, and shows it redacted from the record: the factory knows the secrets of the variables it reads. It needs neither Coder nor a token.</li><li><strong>The telemetry port</strong> (ADR-0031): the app&#39;s config sends its runs to <code>demo/runs/</code> in <code>adapters.telemetry</code>. <code>bernard run</code> writes each run&#39;s trace, machines and session logs through the port, and <code>pnpm demo:viewer</code> runs <code>bernard view</code>, which reads them back through it. While the replay runs, Bernard works in the viewer&#39;s header and the live chips ripple; once it ends, the page holds still. The viewer&#39;s home page, <a href="http://localhost:4321/" target="_blank" rel="noreferrer">http://localhost:4321/</a>, sums up the runs in flight: where each actor stands and the session it runs now. On the pipeline, a step opens the session of its last attempt.</li><li><strong>The viewer in the package</strong> (ADR-0035): <code>pnpm demo:viewer</code> serves the viewer that the package the replay installed ships, with no download. The Machines tab loads Mermaid only when opened, built for state diagrams alone.</li><li><strong>The documentation in the package</strong> (ADR-0037): <code>pnpm demo:viewer</code> also serves, under <a href="http://localhost:4321/docs/" target="_blank" rel="noreferrer">http://localhost:4321/docs/</a>, the documentation site of the version the replay installed. The viewer&#39;s <em>Documentation</em> button opens it, and the site&#39;s <em>Run viewer</em> button comes back to the run.</li><li><strong>Reviewers judge what a linter cannot</strong> (ADR-0034): the replay&#39;s <code>code-review</code> step launches the core&#39;s three reviewers once each, <code>review-quality</code>, <code>review-security-bugs</code> and <code>review-tests</code>, which show as three subagents in the <code>code-review</code> session in the viewer. The re-review comes from the <code>code-review ⇄ apply-review</code> loop, not from a relaunch. Their findings on vague identifiers, test titles and tests covering several branches show in the session&#39;s <em>Handoff</em> tab of the viewer, and in the ticket&#39;s record.</li><li><strong>The tracker port</strong>: <code>pnpm demo:replay --tracker jira</code> runs the feature from the issue <code>EXP-1</code> of a local Jira stub, <a href="https://github.com/theodo-group/bernard/blob/v0.1.0-beta.1/demo/scripts/jira-stub.mjs" target="_blank" rel="noreferrer">scripts/jira-stub.mjs</a>, whose <code>EXP-2</code> to <code>EXP-4</code> are its tickets. Each issue moves to <code>In Progress</code>, <code>In Review</code> and <code>Done</code> as its ticket goes, each pull request&#39;s title carries its ticket&#39;s key, and the replay ends on the history of the moves.</li><li><strong>Write scopes</strong> (ADR-0014): <code>implement</code> may only touch <code>src/</code>, <code>test/</code> and <code>db/migrations/</code>; <code>validate</code> and <code>code-review</code> touch nothing. A change outside is restored and fails the step.</li></ul><h2 id="what-it-simplifies" tabindex="-1">What it simplifies <a class="header-anchor" href="#what-it-simplifies" aria-label="Permalink to &quot;What it simplifies&quot;">​</a></h2><ul><li><strong>No forge.</strong> The app&#39;s repository is a bare repository on your disk, mounted in every workspace. A pull request is a pushed branch plus a description in the ticket&#39;s record, so the replay does not run <code>bernard pull-request</code>: its config names <code>github</code> only because a config names a forge. The demo overrides <code>pull-request</code> to push the branch and play the continuous integration: <code>pnpm check</code> on the branch merged with its base, in a worktree, without pushing to the base.</li><li><strong>No human.</strong> The migration plan counts as validated, and the review gate approves under <code>decide-and-log</code>, logged in the trace, unless <code>--gates</code> makes it wait on you. Nobody merges an <code>api</code> ticket&#39;s pull request: the run ends with the feature branch holding ticket 01, and one pull request per <code>api</code> ticket to it.</li><li><strong>The runner waits on each session through <code>coder ssh</code>.</strong> A network cut or a restart of your machine kills the step, which the machine reruns as a crash.</li></ul><h2 id="prerequisites" tabindex="-1">Prerequisites <a class="header-anchor" href="#prerequisites" aria-label="Permalink to &quot;Prerequisites&quot;">​</a></h2><ul><li><p>Docker (OrbStack or Docker Desktop), the <code>coder</code> CLI, Node 24 and pnpm. With <code>--local</code>, no <code>coder</code> CLI: Docker runs the worktrees&#39; Postgres.</p></li><li><p>A Claude Code token for the sessions, created once in your own terminal and kept out of the repository:</p><div class="language-sh vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">sh</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">mkdir</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> -p</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> ~/.config/bernard-poc</span></span>
9
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">claude</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> setup-token</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # copy the sk-ant-oat01-… token it prints, then:</span></span>
10
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">pbpaste</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &gt;</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> ~/.config/bernard-poc/claude-oauth-token</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &amp;&amp; </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">chmod</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 600</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> ~/.config/bernard-poc/claude-oauth-token</span></span></code></pre></div><p>The runs use your subscription. <code>CLAUDE_TOKEN_FILE</code> points elsewhere if you need.</p></li><li><p>For <code>--harness copilot</code>, a GitHub token with access to Copilot instead: a fine-grained personal access token with the <em>Copilot Requests</em> permission, saved the same way in <code>~/.config/bernard-poc/copilot-token</code> (or <code>COPILOT_TOKEN_FILE</code>). The runs use your Copilot premium requests.</p></li></ul><h2 id="run-it" tabindex="-1">Run it <a class="header-anchor" href="#run-it" aria-label="Permalink to &quot;Run it&quot;">​</a></h2><div class="language-sh vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">sh</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> demo:up</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # starts the local Coder (\`bernard coder up\`), builds the workspace image, pushes the template</span></span>
11
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> demo:replay</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # resets the app and runs the feature; prints each step as it goes (--handoffs: and its handoff)</span></span>
12
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # --harness copilot: on GitHub Copilot instead of Claude Code</span></span>
13
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # --tracker jira: starts the feature from the Jira issue EXP-1 of a local Jira REST stub</span></span>
14
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> demo:viewer</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # bernard view: follows the run in the browser, on http://localhost:4321</span></span>
15
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> demo:down</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # deletes the demo&#39;s workspaces and stops Coder (--purge also removes its volume and state)</span></span></code></pre></div><p>On the local runner, no <code>pnpm demo:up</code>:</p><div class="language-sh vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">sh</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> demo:replay</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> --local</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # starts a fresh Postgres on localhost:54329, resets the app and runs the feature</span></span>
16
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> demo:replay</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> --resume</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # resumes it on the runner it started on: the worktrees stay, and setup runs again in each</span></span>
17
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> demo:down</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # also stops the Postgres</span></span></code></pre></div><p>The demo runs on the same local Coder as <code>bernard run</code> with <code>local: true</code>: the <code>bernard-coder</code> container, which the demo signs in to with its own Coder CLI config in <code>demo/.state/coder</code>. Its web UI is at <a href="http://localhost:7080" target="_blank" rel="noreferrer">http://localhost:7080</a>, as <code>admin@bernard.local</code> with the password in <code>~/.config/bernard/local-coder-password</code>.</p><p>A run takes from 30 minutes to over an hour, depending on how many rounds of validation and review the tickets take. <code>FACTORY_MODEL=sonnet pnpm demo:replay</code> makes it cheaper; on GitHub Copilot, a Copilot model name such as <code>FACTORY_MODEL=claude-sonnet-4.5</code>. A Copilot run reports premium requests, not dollars, and the viewer splits only Claude Code sessions by subagent.</p><p>Without Coder, <code>pnpm demo:starters</code> shows what <code>bernard init</code> writes for each forge, and <code>demo/scripts/secrets.sh</code> how a record keeps secrets out.</p><h3 id="interrupt-and-resume" tabindex="-1">Interrupt and resume <a class="header-anchor" href="#interrupt-and-resume" aria-label="Permalink to &quot;Interrupt and resume&quot;">​</a></h3><p>Stop the replay while tickets are in flight, with Ctrl-C or by closing the laptop, then resume it:</p><div class="language-sh vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">sh</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> demo:replay</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> --resume</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # resumes expense-reports from its records, in a new demo/runs/&lt;date&gt;/</span></span></code></pre></div><p>The resumed run reads each record from the ticket&#39;s workspace, or from its branch in <code>demo/.state/git/app.git</code> when the workspace is gone, and replays it: the steps already done do not run again, and the summary is titled <em>Resumed run</em>.</p><p>A replay started with <code>--gates</code> stalls at each <code>api</code> ticket&#39;s <code>human-review</code> gate. Answer it the same way, the event&#39;s flags after <code>--resume</code>:</p><div class="language-sh vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">sh</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> demo:replay</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> --gates</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ticket 01 lands; stalls at ticket 02&#39;s pull request, and prints the command</span></span>
18
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> demo:replay</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> --resume</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> --ticket</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 02</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> --approve</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # approves it; the run carries on</span></span></code></pre></div><p>Without <code>--resume</code>, <code>pnpm demo:replay</code> starts over: it resets the app&#39;s repository and deletes the workspaces.</p><h2 id="follow-a-run" tabindex="-1">Follow a run <a class="header-anchor" href="#follow-a-run" aria-label="Permalink to &quot;Follow a run&quot;">​</a></h2><p><code>bernard view</code> follows the runs live in the browser: each ticket&#39;s graph of steps with every attempt, the sessions&#39; logs as they are written, the machines&#39; states and the environments the runner worked in. It comes in the factory&#39;s package (ADR-0035) and reads the runs through the project&#39;s telemetry, never from the demo&#39;s files (ADR-0031).</p><div class="language-sh vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">sh</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">pnpm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> demo:viewer</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # bernard view on the app, from the package the replay installed, on http://localhost:4321</span></span></code></pre></div><p>The app&#39;s config points its telemetry at <code>demo/runs/</code>: each run writes <code>demo/runs/&lt;date&gt;/</code>:</p><ul><li><code>events.jsonl</code>: the trace: every state of every machine, every step&#39;s request, outcome and handoff, every decision and environment.</li><li><code>sessions/&lt;workspace&gt;/&lt;NN&gt;-&lt;step&gt;-&lt;attempt&gt;.log</code>: what the session did, readable (<code>tail -f</code> it while it runs); <code>.jsonl</code> next to it is the raw stream.</li><li><code>summary.md</code>, at the end: the timeline of the steps, the decisions, and the tickets&#39; branches.</li></ul><p>Each ticket&#39;s record lands in the app&#39;s repository, on the ticket&#39;s branch: <code>docs/features/expense-reports/.pipeline/&lt;ticket&gt;/</code>. It holds <code>log.jsonl</code>, one line per step result, crash, gate decision and human event, and one directory per step attempt with what the step wrote: the code review&#39;s reports and handoff, the validation&#39;s report, the pull request&#39;s description. <code>git --git-dir demo/.state/git/app.git log --stat ticket/expense-reports-02 -- docs/features</code> shows it for ticket 02; ticket 01 merged itself, so its record is on <code>feat/expense-reports</code>.</p><p>On the local runner, the feature&#39;s worktree is <code>demo/.state/app/tmp/factory/worktrees/expense-reports/</code>, and each ticket&#39;s <code>expense-reports-&lt;ticket&gt;/</code>, with its <code>.env</code>, until the ticket is <code>DONE</code>. To look inside a workspace while it runs, or after: <code>source demo/scripts/env.sh &amp;&amp; coder ssh expense-reports-01</code>. The app&#39;s repository is <code>demo/.state/git/app.git</code>: <code>git --git-dir demo/.state/git/app.git log --graph --all --oneline</code>.</p><h2 id="layout" tabindex="-1">Layout <a class="header-anchor" href="#layout" aria-label="Permalink to &quot;Layout&quot;">​</a></h2><ul><li><code>app/</code>: the app before the feature: the API, the legacy PHP, the validated PRD and migration plan, and its <code>.factory/</code> overlay with the project&#39;s step skills.</li><li><code>coder/</code>: the workspace image and the Coder template, with one Postgres per workspace.</li><li><code>scripts/</code>: <code>up</code>, <code>replay</code>, <code>viewer</code>, <code>down</code>, the local runner&#39;s <code>local-setup</code>, the Jira stub, and the run&#39;s summary.</li></ul>`,32)])])}const k=t(r,[["render",n]]);export{u as __pageData,k 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":"Demo: the factory end to end, on a local Coder or the local runner","description":"","frontmatter":{},"headers":[],"relativePath":"demo/README.md","filePath":"demo/README.md"}'),r={name:"demo/README.md"};function n(i,e,d,c,h,l){return s(),o("div",null,[...e[0]||(e[0]=[a("",32)])])}const k=t(r,[["render",n]]);export{u as __pageData,k as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as o,a3 as i}from"./chunks/framework.hgjl6W6o.js";const p=JSON.parse('{"title":"What the core is built for","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0001-what-the-core-is-built-for.md","filePath":"docs/adr/0001-what-the-core-is-built-for.md"}'),r={name:"docs/adr/0001-what-the-core-is-built-for.md"};function n(s,e,l,c,h,d){return a(),o("div",null,[...e[0]||(e[0]=[i('<h1 id="what-the-core-is-built-for" tabindex="-1">What the core is built for <a class="header-anchor" href="#what-the-core-is-built-for" aria-label="Permalink to &quot;What the core is built for&quot;">​</a></h1><p>Accepted · 2026-10-05</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>The practice&#39;s projects migrate legacy applications.</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>Built for migrations</strong>: the practice&#39;s projects migrate legacy applications.</li><li><strong>No indirection without a user</strong>: a project without a legacy, a new build, runs the same core without the legacy analysis.</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 generic delivery core plus a &quot;migration pack&quot;.</strong> A project without a legacy, a new build, runs the same core without the legacy analysis, so the pack would be a layer of indirection with no user (2).</li><li><strong>The core as the migration factory.</strong> The core implements the migration flow end to end, for the practice&#39;s projects, which migrate legacy applications (1). A new build runs the same core without the legacy analysis, and leaves the legacy slots to their defaults (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. Built for migrations</th><th>2. No indirection</th></tr></thead><tbody><tr><td>A generic core plus a migration pack</td><td>🟢</td><td>🔴</td></tr><tr><td>The core as the migration factory</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 core as the migration factory. So the core implements the migration flow end to end: legacy analysis, PRD, migration plan, then automated delivery up to the merge. Anything tied to one legacy technology (COBOL, a 4GL, a legacy Java or PHP stack…) lives in adapters, and anything tied to one project lives in its overlay. If a build ever needs the core without its migration content, that content can be extracted into a pack then, through the same slots overlays use.</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 names no client, no project and no Theodo-internal tool (see ADR-0004), so a client could one day keep running the factory under their own name.</li><li>The core is written in English. The language of deliverables (PRD, plan, PR) is a project parameter.</li><li>A new build chooses its own steps before the tickets, without the legacy analysis, and leaves the legacy slots to their defaults.</li></ul>',15)])])}const g=t(r,[["render",n]]);export{p as __pageData,g as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as o,a3 as i}from"./chunks/framework.hgjl6W6o.js";const p=JSON.parse('{"title":"What the core is built for","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0001-what-the-core-is-built-for.md","filePath":"docs/adr/0001-what-the-core-is-built-for.md"}'),r={name:"docs/adr/0001-what-the-core-is-built-for.md"};function n(s,e,l,c,h,d){return a(),o("div",null,[...e[0]||(e[0]=[i("",15)])])}const g=t(r,[["render",n]]);export{p as __pageData,g as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as o,a3 as r}from"./chunks/framework.hgjl6W6o.js";const p=JSON.parse('{"title":"Which harness a project runs on","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0002-which-harness-a-project-runs-on.md","filePath":"docs/adr/0002-which-harness-a-project-runs-on.md"}'),n={name:"docs/adr/0002-which-harness-a-project-runs-on.md"};function s(i,e,h,d,c,l){return a(),o("div",null,[...e[0]||(e[0]=[r('<h1 id="which-harness-a-project-runs-on" tabindex="-1">Which harness a project runs on <a class="header-anchor" href="#which-harness-a-project-runs-on" aria-label="Permalink to &quot;Which harness a project runs on&quot;">​</a></h1><p>Accepted, amended by ADR-0026 · 2026-10-05</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 runs on both Claude Code and Pi, so teams can use the coding agent they prefer.</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&#39; choice of agent</strong>: teams can use the coding agent they prefer, and Pi is a requirement.</li><li><strong>Reproducible autonomous runs</strong>: autonomous runs stay reproducible.</li><li><strong>One overlay to maintain</strong>: overlays don&#39;t have to be maintained twice, nor work on both harnesses.</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>Each developer picks their harness within a repo.</strong> Every overlay would have to work on both harnesses (3).</li><li><strong>Both harnesses, one per project.</strong> The core runs on both Claude Code and Pi, so teams can use the coding agent they prefer (1). Each project&#39;s overlay targets only its harness: autonomous runs stay reproducible (2), and overlays don&#39;t have to be maintained twice (3).</li><li><strong>Claude Code only.</strong> Pi is a requirement (1).</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. Teams&#39; choice</th><th>2. Reproducible runs</th><th>3. One overlay</th></tr></thead><tbody><tr><td>Each developer picks</td><td>🟢</td><td>🔴</td><td>🔴</td></tr><tr><td>Both harnesses, one per project</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>Claude Code only</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>Both harnesses, one per project. Each project picks one harness, and its overlay targets only that one: autonomous runs stay reproducible, and overlays don&#39;t have to be maintained twice.</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 both harnesses can express (see ADR-0003, ADR-0005, ADR-0008). Amended by ADR-0026: GitHub Copilot is a third harness, still one per project.</li><li>Pi support is designed in from the start but will only be validated once a pilot project adopts it. The first pilots run on Claude Code.</li></ul>',15)])])}const m=t(n,[["render",s]]);export{p as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as o,a3 as r}from"./chunks/framework.hgjl6W6o.js";const p=JSON.parse('{"title":"Which harness a project runs on","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0002-which-harness-a-project-runs-on.md","filePath":"docs/adr/0002-which-harness-a-project-runs-on.md"}'),n={name:"docs/adr/0002-which-harness-a-project-runs-on.md"};function s(i,e,h,d,c,l){return a(),o("div",null,[...e[0]||(e[0]=[r("",15)])])}const m=t(n,[["render",s]]);export{p as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as o,a3 as r}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"How core content reaches each harness","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0003-how-core-content-reaches-each-harness.md","filePath":"docs/adr/0003-how-core-content-reaches-each-harness.md"}'),n={name:"docs/adr/0003-how-core-content-reaches-each-harness.md"};function s(i,e,d,c,h,l){return a(),o("div",null,[...e[0]||(e[0]=[r('<h1 id="how-core-content-reaches-each-harness" tabindex="-1">How core content reaches each harness <a class="header-anchor" href="#how-core-content-reaches-each-harness" aria-label="Permalink to &quot;How core content reaches each harness&quot;">​</a></h1><p>Accepted · 2026-10-05</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>Core content (skills, agents, hook scripts) is written once in a harness-neutral format.</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>Native on Pi</strong>: slots resolved at runtime through <code>!`cmd`</code> injection don&#39;t exist in Pi, and Pi would otherwise depend on a young third-party bridge.</li><li><strong>No duplication</strong>: two native trees maintained by hand duplicate the core.</li><li><strong>Reviewable upgrades</strong>: reviewers see what a core upgrade changes in the prompts.</li><li><strong>No install step</strong>: otherwise every machine and every CI job needs an install step.</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 native Claude Code plugin, loaded into Pi through a community bridge.</strong> Slots resolved at runtime through <code>!`cmd`</code> injection don&#39;t exist in Pi, and Pi would depend on a young third-party bridge (1).</li><li><strong>A neutral source, rendered per harness, output committed.</strong> Core content is written once in a harness-neutral format (2). <code>bernard sync</code> fills the slots and writes the native files of the project&#39;s harness (1). The rendered files are committed (4), so a core upgrade arrives as a PR showing the exact diff of the project&#39;s prompts (3).</li><li><strong>Two native trees maintained by hand.</strong> Duplication (2).</li><li><strong>Render at install time and gitignore the output, as one existing factory does.</strong> Every machine and every CI job then needs an install step (4), and reviewers can&#39;t see what a core upgrade changes in the prompts (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. Native on Pi</th><th>2. No duplication</th><th>3. Reviewable upgrades</th><th>4. No install step</th></tr></thead><tbody><tr><td>Claude Code plugin through a bridge</td><td>🔴</td><td>🟢</td><td>⚪</td><td>⚪</td></tr><tr><td>Neutral source, rendered, committed</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>Two native trees by hand</td><td>🟢</td><td>🔴</td><td>⚪</td><td>⚪</td></tr><tr><td>Render at install time</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 neutral source, rendered per harness, output committed. <code>bernard sync</code> assembles the core, the adapters and the project&#39;s overlay, fills the slots, and writes the native files of the project&#39;s harness (<code>.claude/</code> or <code>.pi/</code>). The rendered files are committed, and CI runs <code>bernard sync --check</code> to reject hand edits. Sync overwrites or deletes only the files its lock lists, unchanged since the last sync. It refuses any other file at a path it generates or deletes, and then writes nothing.</p><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li>We don&#39;t use the Claude Code plugin marketplace.</li><li>A core upgrade arrives as a PR showing the exact diff of the project&#39;s prompts.</li></ul>',15)])])}const m=t(n,[["render",s]]);export{u as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as o,a3 as r}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"How core content reaches each harness","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0003-how-core-content-reaches-each-harness.md","filePath":"docs/adr/0003-how-core-content-reaches-each-harness.md"}'),n={name:"docs/adr/0003-how-core-content-reaches-each-harness.md"};function s(i,e,d,c,h,l){return a(),o("div",null,[...e[0]||(e[0]=[r("",15)])])}const m=t(n,[["render",s]]);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":"How the core uses external tools","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0004-how-the-core-uses-external-tools.md","filePath":"docs/adr/0004-how-the-core-uses-external-tools.md"}'),s={name:"docs/adr/0004-how-the-core-uses-external-tools.md"};function i(n,e,l,d,c,h){return o(),r("div",null,[...e[0]||(e[0]=[a('<h1 id="how-the-core-uses-external-tools" tabindex="-1">How the core uses external tools <a class="header-anchor" href="#how-the-core-uses-external-tools" aria-label="Permalink to &quot;How the core uses external tools&quot;">​</a></h1><p>Accepted · 2026-10-05 · forge adapters 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 two existing factories already use different tools (Bitbucket vs GitHub, different Jira setups), and keeping Theodo-internal infrastructure in adapters is what keeps the core white-label.</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>Different tools per project</strong>: the two existing factories already use different tools (Bitbucket vs GitHub, different Jira setups).</li><li><strong>White-label core</strong>: keeping Theodo-internal infrastructure in adapters is what keeps the core white-label.</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>External tools behind ports.</strong> The core calls ports, and adapters implement them: Jira, GitHub, Bitbucket, Notion, Coder… (1). Keeping Theodo-internal infrastructure in adapters is what keeps the core white-label (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. Different tools</th><th>2. White-label</th></tr></thead><tbody><tr><td>External tools behind ports</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>External tools behind ports. The core never names an external tool. It calls ports (<code>tracker</code>, <code>forge</code>, <code>docs</code>, <code>runner</code>, <code>telemetry</code>, <code>legacy-tracer</code>), and adapters implement them: Jira, GitHub, GitLab, Azure DevOps, Notion, Coder, one tracer per legacy technology…</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 v1 adapters are the ones the existing factories need: Jira, Notion, Coder, and the forges.</li><li>The <code>forge</code> port exists, with GitHub, GitLab and Azure DevOps as its adapters, each over the forge&#39;s own command-line tool (<code>gh</code>, <code>glab</code>, <code>az</code>): <code>bernard pull-request</code> opens a ticket&#39;s pull request and reads its checks through it (ADR-0036). Bitbucket waits for a project that needs it (ADR-0013).</li><li>Telemetry is a port from v1. The default adapter writes local JSONL.</li><li>The <code>tracker</code> port exists, with Jira Cloud as its first adapter: a feature starts from an issue, and the issues of the feature and its tickets follow the run.</li><li>The <code>runner</code> port also reads and restores the working copy where a step ran, so write scopes are checked in the same environment (ADR-0014).</li><li>No secrets live in the core or in an overlay. Adapters read them from the environment or from the harness&#39;s secret store.</li></ul>',15)])])}const f=t(s,[["render",i]]);export{u as __pageData,f 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":"How the core uses external tools","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0004-how-the-core-uses-external-tools.md","filePath":"docs/adr/0004-how-the-core-uses-external-tools.md"}'),s={name:"docs/adr/0004-how-the-core-uses-external-tools.md"};function i(n,e,l,d,c,h){return o(),r("div",null,[...e[0]||(e[0]=[a("",15)])])}const f=t(s,[["render",i]]);export{u as __pageData,f as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as i,a3 as o}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"What drives the pipeline","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0005-what-drives-the-pipeline.md","filePath":"docs/adr/0005-what-drives-the-pipeline.md"}'),n={name:"docs/adr/0005-what-drives-the-pipeline.md"};function r(s,e,h,l,d,c){return a(),i("div",null,[...e[0]||(e[0]=[o('<h1 id="what-drives-the-pipeline" tabindex="-1">What drives the pipeline <a class="header-anchor" href="#what-drives-the-pipeline" aria-label="Permalink to &quot;What drives the pipeline&quot;">​</a></h1><p>Accepted · 2026-10-05</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>Both existing factories moved their loops out of agent conversations after measuring the cost: in one of them, a single orchestrating agent ran 152 turns and 43.7M tokens. Pi has no equivalent of Claude Code&#39;s Workflow scripts, so the orchestrator can&#39;t be one either.</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>Cost</strong>: in one existing factory, a single orchestrating agent ran 152 turns and 43.7M tokens.</li><li><strong>Runs on Pi</strong>: Pi has no equivalent of Claude Code&#39;s Workflow scripts.</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>An external orchestrator process.</strong> It takes the loops out of agent conversations, as both existing factories did after measuring the cost (1). A state machine in TypeScript, it launches each step as a top-level harness session through one driver per harness (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. Cost</th><th>2. Runs on Pi</th></tr></thead><tbody><tr><td>An external orchestrator process</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>An external orchestrator process. A state machine, written in TypeScript and versioned with the core, runs a declarative pipeline definition. It launches each step as a top-level harness session (<code>claude -p</code>, <code>pi --mode json</code>) through one driver per harness. Skills implement steps; they don&#39;t chain to each other.</p><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li>Every step can still be run by hand.</li><li>An overlay can add, replace or remove steps in the pipeline definition.</li><li>The orchestrator&#39;s owner decides the engine, the format of the pipeline definition, and the retry and resume semantics.</li></ul>',15)])])}const m=t(n,[["render",r]]);export{u as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as i,a3 as o}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"What drives the pipeline","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0005-what-drives-the-pipeline.md","filePath":"docs/adr/0005-what-drives-the-pipeline.md"}'),n={name:"docs/adr/0005-what-drives-the-pipeline.md"};function r(s,e,h,l,d,c){return a(),i("div",null,[...e[0]||(e[0]=[o("",15)])])}const m=t(n,[["render",r]]);export{u as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as a,o as e,c as o,a3 as s}from"./chunks/framework.hgjl6W6o.js";const p=JSON.parse('{"title":"What steps hand off to each other","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0006-what-steps-hand-off-to-each-other.md","filePath":"docs/adr/0006-what-steps-hand-off-to-each-other.md"}'),r={name:"docs/adr/0006-what-steps-hand-off-to-each-other.md"};function d(n,t,h,i,c,l){return e(),o("div",null,[...t[0]||(t[0]=[s('<h1 id="what-steps-hand-off-to-each-other" tabindex="-1">What steps hand off to each other <a class="header-anchor" href="#what-steps-hand-off-to-each-other" aria-label="Permalink to &quot;What steps hand off to each other&quot;">​</a></h1><p>Accepted · 2026-10-05</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>Each step produces documents for humans (Markdown, with validated frontmatter) and a handoff: an object validated by a Zod schema, carrying everything the orchestrator and later steps act on (ids of stories, tickets and acceptance criteria, dependencies, results, open questions, document paths).</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>Reliable facts for the machine</strong>: parsing Markdown that a model wrote and a human edited is brittle.</li><li><strong>Humans edit Markdown</strong>: with the PRD as typed data, PMs would have to edit YAML.</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>Data first: the PRD as typed data, Markdown as a generated view.</strong> PMs would have to edit YAML (2).</li><li><strong>Typed handoffs alongside the documents.</strong> Each step produces documents for humans in Markdown (2), and a handoff validated by a Zod schema, carrying everything the orchestrator and later steps act on (1).</li><li><strong>Strictly structured Markdown, parsed with Zod.</strong> Parsing Markdown that a model wrote and a human edited is brittle (1).</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. Reliable facts</th><th>2. Humans edit Markdown</th></tr></thead><tbody><tr><td>Data first</td><td>🟢</td><td>🔴</td></tr><tr><td>Typed handoffs alongside the documents</td><td>🟢</td><td>🟢</td></tr><tr><td>Strictly structured Markdown</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>Typed handoffs alongside the documents. A fact the machine acts on lives only in the handoff. When a human edits a document at a gate, the handoff is re-extracted and validated again.</p><p>The same schema, exported as JSON Schema, constrains the model&#39;s output on both harnesses. ADR-0025 settles how: a session ends with one answer, its outcome for the orchestrator and its handoff for the next steps.</p>',14)])])}const u=a(r,[["render",d]]);export{p as __pageData,u as default};
@@ -0,0 +1 @@
1
+ import{_ as a,o as e,c as o,a3 as s}from"./chunks/framework.hgjl6W6o.js";const p=JSON.parse('{"title":"What steps hand off to each other","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0006-what-steps-hand-off-to-each-other.md","filePath":"docs/adr/0006-what-steps-hand-off-to-each-other.md"}'),r={name:"docs/adr/0006-what-steps-hand-off-to-each-other.md"};function d(n,t,h,i,c,l){return e(),o("div",null,[...t[0]||(t[0]=[s("",14)])])}const u=a(r,[["render",d]]);export{p as __pageData,u as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as i,a3 as o}from"./chunks/framework.hgjl6W6o.js";const c=JSON.parse('{"title":"How humans take part in the pipeline","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0007-how-humans-take-part-in-the-pipeline.md","filePath":"docs/adr/0007-how-humans-take-part-in-the-pipeline.md"}'),n={name:"docs/adr/0007-how-humans-take-part-in-the-pipeline.md"};function s(r,e,h,l,d,p){return a(),i("div",null,[...e[0]||(e[0]=[o('<h1 id="how-humans-take-part-in-the-pipeline" tabindex="-1">How humans take part in the pipeline <a class="header-anchor" href="#how-humans-take-part-in-the-pipeline" aria-label="Permalink to &quot;How humans take part in the pipeline&quot;">​</a></h1><p>Accepted, amended by ADR-0015 · 2026-10-05</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>Pi has no UI in headless mode, and in-skill questions are what one existing factory had to override with a dedicated rules prompt for its autonomous runs.</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>Headless on Pi</strong>: Pi has no UI in headless mode.</li><li><strong>No override prompt</strong>: one existing factory had to override in-skill questions with a dedicated rules prompt for its autonomous 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>Gates as orchestrator states, reviews in pull requests.</strong> Skills never ask a human anything during an autonomous run (1, 2): a gate is a pipeline state the orchestrator pauses on, and reviews happen in pull requests.</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. Headless on Pi</th><th>2. No override prompt</th></tr></thead><tbody><tr><td>Gates as orchestrator states, reviews in PRs</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>Gates as orchestrator states, reviews in pull requests. Skills never ask a human anything during an autonomous run. Instead:</p><ul><li>A gate is a pipeline state the orchestrator pauses on.</li><li>An autonomous step returns open questions in its handoff, and the orchestrator turns them into a gate.</li><li>A conversational step, such as building the migration plan with the dev, is an interactive step a human runs in their harness. The orchestrator waits for its handoff.</li><li>Each gate&#39;s policy is a project setting: <code>block</code>, or <code>decide-and-log</code>.</li></ul><p>Reviews of documents and of code use a single gate type, <code>pr-review</code>, on the <code>forge</code> port. The step opens a PR, the reviewer comments, &quot;request changes&quot; triggers an automatic fix step, and &quot;approve&quot; passes the gate.</p><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li>By default there are two human gates: the PRD (the PM reviews a pre-generated PRD in a PR) and the migration plan (built interactively with the dev, who validates it). Everything after that is automatic up to the merge, including applying the humans&#39; review of the final PR.</li><li>Legacy analysis runs automatically. The PM validates it indirectly, by reviewing the PRD built from it.</li></ul>',17)])])}const m=t(n,[["render",s]]);export{c as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as i,a3 as o}from"./chunks/framework.hgjl6W6o.js";const c=JSON.parse('{"title":"How humans take part in the pipeline","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0007-how-humans-take-part-in-the-pipeline.md","filePath":"docs/adr/0007-how-humans-take-part-in-the-pipeline.md"}'),n={name:"docs/adr/0007-how-humans-take-part-in-the-pipeline.md"};function s(r,e,h,l,d,p){return a(),i("div",null,[...e[0]||(e[0]=[o("",17)])])}const m=t(n,[["render",s]]);export{c as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as s,c as a,a3 as n}from"./chunks/framework.hgjl6W6o.js";const p=JSON.parse(`{"title":"Where a step's reviewers run","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0008-where-a-steps-reviewers-run.md","filePath":"docs/adr/0008-where-a-steps-reviewers-run.md"}`),o={name:"docs/adr/0008-where-a-steps-reviewers-run.md"};function r(i,e,d,h,l,c){return s(),a("div",null,[...e[0]||(e[0]=[n('<h1 id="where-a-step-s-reviewers-run" tabindex="-1">Where a step&#39;s reviewers run <a class="header-anchor" href="#where-a-step-s-reviewers-run" aria-label="Permalink to &quot;Where a step&#39;s reviewers run&quot;">​</a></h1><p>Accepted · 2026-10-15</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>In one existing factory, Workflow phase agents were themselves subagents, and a subagent can&#39;t spawn subagents: its review phase ended up reviewing alone and signing for all its reviewers. Claude Code&#39;s headless mode runs subagents in the background by default: a review then answered before its reviewers were done, and answered again once they were.</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>Auditable review</strong>: a branch that returned nothing, or covered less than its share, must show instead of passing for a clean one.</li><li><strong>Simple over complex</strong>: running a step&#39;s fan-out in the orchestrator took a parallel state, relaunch counters, a preparation step and a merge step, and agents started as sessions, which each harness supports in its own way.</li><li><strong>Answers once its reviewers are done</strong>: with background subagents, the session answers before its subagents finish.</li><li><strong>Context economy</strong>: steps would lose the context economy subagents give.</li><li><strong>Maintained on Pi</strong>: Pi&#39;s official <code>subagent</code> extension is an example, not a maintained package.</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 orchestrator runs a step&#39;s fan-out, one session per branch.</strong> The previous version of this ADR, built and run end to end before being dropped. It gave each reviewer an exact cost and a crash retry of its own, for a parallel state, relaunch counters, a preparation step and a merge step in the orchestrator, and agents started as sessions, which each harness supports in its own way (2). The reports already make the review auditable: simple over complex.</li><li><strong>The step&#39;s own fan-out, in foreground subagents.</strong> The step&#39;s skill launches each branch as a subagent, waits for all of them, and merges what they return into the step&#39;s handoff; the orchestrator runs steps, not the branches inside one (2). Each reviewer writes its report, and the merged handoff says what each one covered (1). The subagents run in the foreground, so the session ends once it has the step&#39;s outcome (3), and they save the step&#39;s context (4). On Pi, they come from the <code>pi-subagents</code> extension, at a version pinned by each core release (5).</li><li><strong>No subagents.</strong> Steps would lose the context economy subagents give (4).</li><li><strong>Background subagents.</strong> The session answers before its subagents finish (3).</li><li><strong>Pi&#39;s official <code>subagent</code> example extension.</strong> It is an example, not a maintained package (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. Auditable</th><th>2. Simple</th><th>3. Answers when done</th><th>4. Context economy</th><th>5. Maintained on Pi</th></tr></thead><tbody><tr><td>The orchestrator runs the fan-out</td><td>🟢</td><td>🔴</td><td>⚪</td><td>⚪</td><td>⚪</td></tr><tr><td>Foreground subagents in the step</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>No subagents</td><td>⚪</td><td>⚪</td><td>🟢</td><td>🔴</td><td>🟢</td></tr><tr><td>Background subagents</td><td>⚪</td><td>⚪</td><td>🔴</td><td>🟢</td><td>⚪</td></tr><tr><td>Pi&#39;s <code>subagent</code> example extension</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 step&#39;s own fan-out, in foreground subagents. A fan-out that belongs to one step, such as the code reviewers, runs inside that step&#39;s session: its skill launches each branch as a subagent, waits for all of them, and merges what they return into the step&#39;s handoff. The orchestrator runs steps, not the branches inside one. The loops between steps, such as validate back to implement or code-review back to apply-review, stay in its graph (ADR-0015). Skills also use subagents to save context when delegating is a judgment call: exploring code, digging into the legacy, summarizing a large document. On Pi, subagents come from the <code>pi-subagents</code> extension, at a version pinned by each core release.</p><p>The subagents run in the foreground, so the session ends once it has the step&#39;s outcome. The Claude Code driver turns background tasks off (<code>CLAUDE_CODE_DISABLE_BACKGROUND_TASKS</code>).</p><p>What makes a review auditable is its files, not its sessions: each reviewer writes its report, and the merged handoff says what each one covered, so a branch that returned nothing, or covered less than its share, shows instead of passing for a clean one. The session&#39;s stream also gives each subagent&#39;s status, tokens and duration.</p><p>Our steps are top-level sessions (ADR-0005), so their skills can spawn subagents.</p><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li>One step is one session, one cost and one log; its subagents&#39; work is in them.</li><li>A step&#39;s skill launches its subagents once; re-running a review is a loop of the graph, bounded by the step&#39;s failure budget (ADR-0015). See ADR-0034.</li><li>Revisit if a harness runs subagents unreliably, or if a project needs each reviewer&#39;s exact cost.</li></ul>',18)])])}const g=t(o,[["render",r]]);export{p as __pageData,g as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as s,c as a,a3 as n}from"./chunks/framework.hgjl6W6o.js";const p=JSON.parse(`{"title":"Where a step's reviewers run","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0008-where-a-steps-reviewers-run.md","filePath":"docs/adr/0008-where-a-steps-reviewers-run.md"}`),o={name:"docs/adr/0008-where-a-steps-reviewers-run.md"};function r(i,e,d,h,l,c){return s(),a("div",null,[...e[0]||(e[0]=[n("",18)])])}const g=t(o,[["render",r]]);export{p as __pageData,g as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as i,a3 as o}from"./chunks/framework.hgjl6W6o.js";const f=JSON.parse('{"title":"Where pipeline state and handoffs live","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0009-where-pipeline-state-and-handoffs-live.md","filePath":"docs/adr/0009-where-pipeline-state-and-handoffs-live.md"}'),r={name:"docs/adr/0009-where-pipeline-state-and-handoffs-live.md"};function n(s,e,d,l,h,c){return a(),i("div",null,[...e[0]||(e[0]=[o('<h1 id="where-pipeline-state-and-handoffs-live" tabindex="-1">Where pipeline state and handoffs live <a class="header-anchor" href="#where-pipeline-state-and-handoffs-live" aria-label="Permalink to &quot;Where pipeline state and handoffs live&quot;">​</a></h1><p>Accepted · 2026-10-05</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>Pipeline state and handoffs are files in the project repo, in each feature&#39;s folder, not in a database. This needs no infrastructure, can be reviewed in PRs, and matches how both existing factories already work.</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 infrastructure</strong>: files in the project repo need no infrastructure.</li><li><strong>Reviewable in PRs</strong>: pipeline state and handoffs can be reviewed in PRs.</li><li><strong>Like the existing factories</strong>: both existing factories already work this way.</li><li><strong>Upgrades with a feature in flight</strong>: a project always has a feature in flight.</li><li><strong>Paths without configuration</strong>: with a layout configured per project, every skill and every step would have to resolve paths through configuration, for a cosmetic gain.</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>No migrations: finish in-flight features before a major upgrade.</strong> A project always has a feature in flight (4).</li><li><strong>Files in the project repo, in a canonical layout, migrated by <code>bernard sync</code>.</strong> Files in each feature&#39;s folder, not in a database, need no infrastructure (1), can be reviewed in PRs (2), and match how both existing factories already work (3). A breaking contract change ships with a migration that <code>bernard sync</code> applies in place (4), and the core defines the layout (5).</li><li><strong>A layout configured per project.</strong> Every skill and every step would have to resolve paths through configuration, for a cosmetic gain (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. No infrastructure</th><th>2. Reviewable</th><th>3. Like existing</th><th>4. Upgrades in flight</th><th>5. Paths without config</th></tr></thead><tbody><tr><td>No migrations</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🔴</td><td>🟢</td></tr><tr><td>Files in the repo, canonical layout, migrated</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>A layout configured per project</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>Files in the project repo, in a canonical layout, migrated by <code>bernard sync</code>. The core defines a canonical layout (with a configurable root): a feature has one PRD and one migration plan, the plan is split into tickets, and a PR delivers one or more tickets.</p><p>Every state file carries a <code>schemaVersion</code>. A breaking contract change ships in a major version, together with a migration that <code>bernard sync</code> applies in place. Handoffs carry none until a project outside this repo consumes the factory: nothing reads it, and git history versions their schemas (ADR-0025).</p>',14)])])}const u=t(r,[["render",n]]);export{f as __pageData,u as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as i,a3 as o}from"./chunks/framework.hgjl6W6o.js";const f=JSON.parse('{"title":"Where pipeline state and handoffs live","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0009-where-pipeline-state-and-handoffs-live.md","filePath":"docs/adr/0009-where-pipeline-state-and-handoffs-live.md"}'),r={name:"docs/adr/0009-where-pipeline-state-and-handoffs-live.md"};function n(s,e,d,l,h,c){return a(),i("div",null,[...e[0]||(e[0]=[o("",14)])])}const u=t(r,[["render",n]]);export{f as __pageData,u as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as a,a3 as r}from"./chunks/framework.hgjl6W6o.js";const f=JSON.parse('{"title":"How a project adapts the core","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0010-how-a-project-adapts-the-core.md","filePath":"docs/adr/0010-how-a-project-adapts-the-core.md"}'),s={name:"docs/adr/0010-how-a-project-adapts-the-core.md"};function i(n,e,d,l,c,h){return o(),a("div",null,[...e[0]||(e[0]=[r('<h1 id="how-a-project-adapts-the-core" tabindex="-1">How a project adapts the core <a class="header-anchor" href="#how-a-project-adapts-the-core" aria-label="Permalink to &quot;How a project adapts the core&quot;">​</a></h1><p>Accepted · 2026-10-05</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 customization lives in <code>.factory/</code>, configured by a typed <code>factory.config.ts</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 silent copies</strong>: teams would otherwise copy core content silently, which is how one existing factory&#39;s copied skills drifted away from their upstream.</li><li><strong>No home-made parser</strong>: slots as Markdown comment markers need a home-made parser for the same result.</li><li><strong>Config in TypeScript</strong>: steps are written in TypeScript anyway.</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>Forbidding overrides.</strong> Teams would copy core content silently. That is how one existing factory&#39;s copied skills drifted away from their upstream (1).</li><li><strong>An overlay with Handlebars slots, declared overrides and a TypeScript config.</strong> Replacing a whole core skill or agent is allowed only as a declared <code>override</code> with a reason, and <code>bernard sync</code> flags it whenever the core version of that item changes (1). Templates use Handlebars (2). The overlay is configured by a typed <code>factory.config.ts</code> (3).</li><li><strong>Slots as Markdown comment markers.</strong> A home-made parser for the same result (2).</li><li><strong>A YAML config.</strong> Steps are written in TypeScript anyway (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 silent copies</th><th>2. No home-made parser</th><th>3. Config in TypeScript</th></tr></thead><tbody><tr><td>Forbidding overrides</td><td>🔴</td><td>⚪</td><td>⚪</td></tr><tr><td>Overlay with declared overrides</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>Markdown comment slots</td><td>⚪</td><td>🔴</td><td>⚪</td></tr><tr><td>A YAML config</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>An overlay with Handlebars slots, declared overrides and a TypeScript config. An overlay can:</p><ul><li>fill slots;</li><li>set parameters: deliverable language, artifact root, model per step, gate policies;</li><li>add skills, agents and steps;</li><li>edit the pipeline.</li></ul><p>A step&#39;s handoff is extended in <code>handoffs</code>, keyed on the skill, with the <code>z</code> that <code>@theodo/bernard/config</code> re-exports: the project&#39;s schemas and the core&#39;s come from one Zod, the one the factory&#39;s version pins (ADR-0011), so they compose into one schema:</p><ul><li>for a core skill, overridden or not, the shape adds fields to the core&#39;s handoff: an overlay cannot redeclare a core field, so it can neither remove nor loosen one;</li><li>for an overlay skill, the shape is its handoff;</li><li>an overlay skill a step runs without an entry hands off the default <code>{ summary }</code>, and <code>bernard sync</code> warns that the next steps get only prose.</li></ul><p><code>bernard sync</code> fails on an entry for a skill that neither the core nor the overlay has, and loading the config fails on a core field declared again or on a shape that does not export to draft-7 JSON Schema (ADR-0025).</p><p>Replacing a whole core skill or agent is allowed only as a declared <code>override</code> with a reason. <code>bernard sync</code> flags it whenever the core version of that item changes.</p><p>Templates use Handlebars in strict mode: <code>{{var}}</code>, <code>{{#slot &quot;name&quot;}}default{{/slot}}</code> and <code>{{&gt; fragment}}</code>. An unknown slot or variable fails the render.</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 overlay tightens a core step&#39;s contract without overriding its skill: a field it adds is constrained in the session.</li><li>The vocabulary is core, adapter and overlay. We avoid &quot;plugin&quot;, which means a Claude Code distribution unit.</li></ul>',21)])])}const m=t(s,[["render",i]]);export{f as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as a,a3 as r}from"./chunks/framework.hgjl6W6o.js";const f=JSON.parse('{"title":"How a project adapts the core","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0010-how-a-project-adapts-the-core.md","filePath":"docs/adr/0010-how-a-project-adapts-the-core.md"}'),s={name:"docs/adr/0010-how-a-project-adapts-the-core.md"};function i(n,e,d,l,c,h){return o(),a("div",null,[...e[0]||(e[0]=[r("",21)])])}const m=t(s,[["render",i]]);export{f as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as a,o as t,c as o,a3 as n}from"./chunks/framework.hgjl6W6o.js";const g=JSON.parse('{"title":"How the factory is packaged and versioned","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0011-how-the-factory-is-packaged-and-versioned.md","filePath":"docs/adr/0011-how-the-factory-is-packaged-and-versioned.md"}'),r={name:"docs/adr/0011-how-the-factory-is-packaged-and-versioned.md"};function d(s,e,i,c,h,p){return t(),o("div",null,[...e[0]||(e[0]=[n('<h1 id="how-the-factory-is-packaged-and-versioned" tabindex="-1">How the factory is packaged and versioned <a class="header-anchor" href="#how-the-factory-is-packaged-and-versioned" aria-label="Permalink to &quot;How the factory is packaged and versioned&quot;">​</a></h1><p>Accepted · 2026-10-05</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>This repo is a pnpm monorepo, published as a single npm package, <code>@theodo/bernard</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 compatibility matrix</strong>: independent versions would make us maintain one.</li><li><strong>Bundled for Pi</strong>: Pi has no dependency resolution between packages, so bundling is needed anyway.</li><li><strong>Standard tooling</strong>: npm already provides semver, lockfiles and upgrade bots.</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>Independent versions per package.</strong> We would have to maintain a compatibility matrix (1). Pi also has no dependency resolution between packages, so bundling is needed anyway (2).</li><li><strong>One npm package, one version.</strong> A single npm package, <code>@theodo/bernard</code>: the core, the orchestrator, the adapters and the harness support share one semver and one changelog (1, 2). Projects pin the version in <code>.factory/package.json</code> and receive upgrades through Renovate or Dependabot (3).</li><li><strong>Git tags fetched by a custom CLI, as one existing factory does.</strong> npm already provides semver, lockfiles and upgrade bots (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 matrix</th><th>2. Bundled for Pi</th><th>3. Standard tooling</th></tr></thead><tbody><tr><td>Independent versions</td><td>🔴</td><td>🔴</td><td>🟢</td></tr><tr><td>One package, one version</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>Git tags and a custom CLI</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 npm package, one version. The core, the orchestrator, the adapters and the harness support share one semver and one changelog. Projects pin the version in <code>.factory/package.json</code>, which keeps non-JS projects clean, and receive upgrades through Renovate or Dependabot.</p><p>Each version is published on npmjs.com (ADR-0035).</p>',14)])])}const m=a(r,[["render",d]]);export{g as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as a,o as t,c as o,a3 as n}from"./chunks/framework.hgjl6W6o.js";const g=JSON.parse('{"title":"How the factory is packaged and versioned","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0011-how-the-factory-is-packaged-and-versioned.md","filePath":"docs/adr/0011-how-the-factory-is-packaged-and-versioned.md"}'),r={name:"docs/adr/0011-how-the-factory-is-packaged-and-versioned.md"};function d(s,e,i,c,h,p){return t(),o("div",null,[...e[0]||(e[0]=[n("",14)])])}const m=a(r,[["render",d]]);export{g as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as r,a3 as o}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse(`{"title":"How a step's failures are told apart and routed","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0012-how-a-steps-failures-are-told-apart-and-routed.md","filePath":"docs/adr/0012-how-a-steps-failures-are-told-apart-and-routed.md"}`),s={name:"docs/adr/0012-how-a-steps-failures-are-told-apart-and-routed.md"};function n(i,e,d,l,c,h){return a(),r("div",null,[...e[0]||(e[0]=[o('<h1 id="how-a-step-s-failures-are-told-apart-and-routed" tabindex="-1">How a step&#39;s failures are told apart and routed <a class="header-anchor" href="#how-a-step-s-failures-are-told-apart-and-routed" aria-label="Permalink to &quot;How a step&#39;s failures are told apart and routed&quot;">​</a></h1><p>Accepted, amended by ADR-0015 · 2026-10-07</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>A step returns <code>passed</code>, <code>failed</code> or <code>blocked</code>. <code>failed</code> means the step needs another try. <code>blocked</code> means another try is useless until something outside the step changes: an access, an environment, an input or a decision. A crashed harness run is an error of the factory itself, not a step result. These three need different handling, and a pass/fail verdict cannot tell them apart.</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>Failures, blockers and crashes told apart</strong>: these three need different handling: rework cannot fix a missing access, and it would burn the round budget.</li><li><strong>Routes stay in the machine</strong>: the machine owns the routes (ADR-0005), and with them retries, budgets and the drawn pipeline.</li><li><strong>Classes fit the project</strong>: each legacy technology fails in its own ways, and a core vocabulary would either be too coarse or carry one project&#39;s terms.</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>Let the step name the next step.</strong> The route would leave the machine, and with it retries, budgets and the drawn pipeline (2).</li><li><strong>Failure classes defined by the project, routed by the overlay.</strong> A failed or blocked result can carry a failure class, an open string the project defines (3). The overlay maps classes to the ticket step a failure goes back to, and the machine keeps owning the routes (2). A blocked step waits for a human and never follows a failure route, and a crash is retried automatically and not logged as an attempt (1).</li><li><strong>Failure classes defined by the core.</strong> Each legacy technology fails in its own ways, and a core vocabulary would either be too coarse or carry one project&#39;s terms (3).</li><li><strong>Treat a blocker as a failure.</strong> Rework cannot fix a missing access, and it would burn the round budget (1).</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. Told apart</th><th>2. Routes in the machine</th><th>3. Classes fit the project</th></tr></thead><tbody><tr><td>The step names the next step</td><td>⚪</td><td>🔴</td><td>⚪</td></tr><tr><td>Project classes, overlay routes</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>Core failure classes</td><td>⚪</td><td>⚪</td><td>🔴</td></tr><tr><td>A blocker as a failure</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>Failure classes defined by the project, routed by the overlay. A failed or blocked result can carry a failure class, an open string the project defines. The overlay maps classes to the ticket step a failure goes back to, for the steps that already loop (<code>validate</code>, <code>code-review</code>). An unmapped class takes the core route. The machine keeps owning the routes (ADR-0005); the step only names what went wrong.</p><p>Each step result is appended to an attempt log in the pipeline state (ADR-0009).</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 blocked step waits for a human to retry it, and never follows a failure route.</li><li>A crash is retried automatically, then waits for a human. It is not logged as an attempt.</li><li>Every reroute out of a step spends that step&#39;s round budget, whatever its target.</li><li>A step receives the failure classes of its previous attempts, so it does not repeat a failed approach.</li><li>A step sent back by a failure receives that <code>failure</code>: the failed step and its attempt, the failure class and error, and <code>failure.handoffDir</code>, which holds that attempt&#39;s handoff in the ticket&#39;s record (ADR-0017, ADR-0025). The step reads the findings there (ADR-0019).</li></ul>',16)])])}const f=t(s,[["render",n]]);export{u as __pageData,f as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as r,a3 as o}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse(`{"title":"How a step's failures are told apart and routed","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0012-how-a-steps-failures-are-told-apart-and-routed.md","filePath":"docs/adr/0012-how-a-steps-failures-are-told-apart-and-routed.md"}`),s={name:"docs/adr/0012-how-a-steps-failures-are-told-apart-and-routed.md"};function n(i,e,d,l,c,h){return a(),r("div",null,[...e[0]||(e[0]=[o("",16)])])}const f=t(s,[["render",n]]);export{u as __pageData,f as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as n,a3 as a}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"When code lands","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0013-when-code-lands.md","filePath":"docs/adr/0013-when-code-lands.md"}'),i={name:"docs/adr/0013-when-code-lands.md"};function s(r,e,d,c,l,h){return o(),n("div",null,[...e[0]||(e[0]=[a('<h1 id="when-code-lands" tabindex="-1">When code lands <a class="header-anchor" href="#when-code-lands" aria-label="Permalink to &quot;When code lands&quot;">​</a></h1><p>Accepted · 2026-10-07</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>A lesson learned on one project&#39;s factory comes in with the code that applies it, not ahead of it.</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>Designed against a consumer</strong>: a contract with no consumer is designed blind.</li><li><strong>Projects keep their terms open</strong>: an early contract fixes vocabulary and policy that projects should keep open.</li><li><strong>Checked by a machine</strong>: an unused export is cheap to add and easy to miss in review.</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>Publish target contracts early so projects converge on them.</strong> A contract with no consumer is designed blind (1), and it fixes vocabulary and policy that projects should keep open (2).</li><li><strong>Code only with a production consumer, checked by knip in CI.</strong> A lesson learned on one project&#39;s factory comes in with the code that applies it, not ahead of it (1, 2). <code>pnpm check:consumers</code> runs knip in CI and fails on an unused file, export or dependency, but the baseline ignores whole files, so a new unused export in a listed file is not caught until the file leaves the list (3).</li><li><strong>Review the rule by hand.</strong> An unused export is cheap to add and easy to miss in review (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. Against a consumer</th><th>2. Terms kept open</th><th>3. Machine-checked</th></tr></thead><tbody><tr><td>Publish contracts early</td><td>🔴</td><td>🔴</td><td>⚪</td></tr><tr><td>A consumer, checked by knip</td><td>🟢</td><td>🟢</td><td>🟠</td></tr><tr><td>Review the rule by hand</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 only with a production consumer, checked by knip in CI. A contract, schema or helper enters the framework only when production source uses it: a step, a driver, the orchestrator or the CLI. Tests do not count.</p><p><code>pnpm check:consumers</code> runs knip in production mode in CI and fails on an unused file, export or dependency. Exports that predate the rule are listed in <code>knip.json</code>; the list only shrinks, and <code>pnpm check:knip-baseline</code> fails a change that adds to it.</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 brick from one project&#39;s factory ships with its first consumer, or waits.</li><li>The public overlay API, such as <code>defineConfig</code>, is used by project config files outside the repo, so it stays in the baseline.</li><li>The baseline ignores whole files, so a new unused export in a listed file is not caught until the file leaves the list.</li></ul>',16)])])}const m=t(i,[["render",s]]);export{u as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as n,a3 as a}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse('{"title":"When code lands","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0013-when-code-lands.md","filePath":"docs/adr/0013-when-code-lands.md"}'),i={name:"docs/adr/0013-when-code-lands.md"};function s(r,e,d,c,l,h){return o(),n("div",null,[...e[0]||(e[0]=[a("",16)])])}const m=t(i,[["render",s]]);export{u as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as s,a3 as o}from"./chunks/framework.hgjl6W6o.js";const g=JSON.parse('{"title":"What a step may change","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0014-what-a-step-may-change.md","filePath":"docs/adr/0014-what-a-step-may-change.md"}'),r={name:"docs/adr/0014-what-a-step-may-change.md"};function i(n,e,h,d,c,l){return a(),s("div",null,[...e[0]||(e[0]=[o('<h1 id="what-a-step-may-change" tabindex="-1">What a step may change <a class="header-anchor" href="#what-a-step-may-change" aria-label="Permalink to &quot;What a step may change&quot;">​</a></h1><p>Accepted · 2026-10-07</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>The overlay can give a step a write scope: the globs it may change. This keeps an agent from reaching a green result by changing what proves it, such as the non-regression tests during <code>implement</code>, without freezing files at a gate.</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>Cannot be gone around</strong>: an agent must not reach a green result by changing what proves it, and a shell command goes around harness permission rules.</li><li><strong>No freeze moment</strong>: hashing approved files at a gate needs a freeze moment and stored hashes.</li><li><strong>Says which step may touch what</strong>: a hash at a gate says nothing about it.</li><li><strong>Both harnesses</strong>: the core may only rely on what both harnesses can express (ADR-0002).</li><li><strong>Another try can fix it</strong>: the step made the mistake, and another try with the failure class can fix 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>Hash the approved files at a gate and refuse a pass if they changed.</strong> It needs a freeze moment and stored hashes (2), and says nothing about which step may touch what (3).</li><li><strong>A write scope per step, checked after the step, which fails it.</strong> The overlay gives a step the globs it may change, which keeps an agent from reaching a green result by changing what proves it, without freezing files at a gate (1, 2, 3). After the step, the orchestrator lists the files it changed; a file outside the scope is restored, and the step fails with the class <code>out-of-scope-write</code> (5).</li><li><strong>Harness permission rules only.</strong> A shell command goes around them (1), and the core may only rely on what both harnesses can express (4, ADR-0002).</li><li><strong>Block the step instead of failing it.</strong> The step made the mistake, and another try with the failure class can fix it (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. Not gone around</th><th>2. No freeze</th><th>3. Which step</th><th>4. Both harnesses</th><th>5. Another try</th></tr></thead><tbody><tr><td>Hash at a gate</td><td>🟢</td><td>🔴</td><td>🔴</td><td>⚪</td><td>⚪</td></tr><tr><td>Scope checked after the step</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>Harness permission rules only</td><td>🔴</td><td>⚪</td><td>⚪</td><td>🔴</td><td>⚪</td></tr><tr><td>Block instead of fail</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 write scope per step, checked after the step, which fails it. The orchestrator records the commit the step starts from, and after the step lists the files it changed. A file outside the scope is restored to that commit, and the step fails with the class <code>out-of-scope-write</code>; the restored paths go in the error. The step also receives its scope in its request. Harness permission rules can come later as a first line of defence.</p><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li>Scopes are opt-in per step. A step without one is not checked.</li><li>A ticket&#39;s record (<code>.pipeline/</code>) is outside every scope: the runner writes each step&#39;s handoff there, and a step adds the files it produces for review (ADR-0017, ADR-0025).</li><li>The runner adapter reads and restores the working copy, since it knows where the step ran. A pipeline with scopes refuses a runner that cannot.</li><li>A blocked result is not checked: its writes stay for the human who unblocks it. A crashed run is checked before it is retried.</li><li>The check covers the steps the orchestrator runs, not interactive steps a human runs.</li><li>Out-of-scope work is lost; in-scope work is kept.</li><li>Read isolation, such as keeping the analysis away from target code, is not covered: a read leaves no trace to check.</li></ul>',15)])])}const u=t(r,[["render",i]]);export{g as __pageData,u as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as a,c as s,a3 as o}from"./chunks/framework.hgjl6W6o.js";const g=JSON.parse('{"title":"What a step may change","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0014-what-a-step-may-change.md","filePath":"docs/adr/0014-what-a-step-may-change.md"}'),r={name:"docs/adr/0014-what-a-step-may-change.md"};function i(n,e,h,d,c,l){return a(),s("div",null,[...e[0]||(e[0]=[o("",15)])])}const u=t(r,[["render",i]]);export{g as __pageData,u as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as a,a3 as s}from"./chunks/framework.hgjl6W6o.js";const g=JSON.parse('{"title":"How a project shapes its ticket flow","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0015-how-a-project-shapes-its-ticket-flow.md","filePath":"docs/adr/0015-how-a-project-shapes-its-ticket-flow.md"}'),r={name:"docs/adr/0015-how-a-project-shapes-its-ticket-flow.md"};function d(i,e,n,c,h,l){return o(),a("div",null,[...e[0]||(e[0]=[s('<h1 id="how-a-project-shapes-its-ticket-flow" tabindex="-1">How a project shapes its ticket flow <a class="header-anchor" href="#how-a-project-shapes-its-ticket-flow" aria-label="Permalink to &quot;How a project shapes its ticket flow&quot;">​</a></h1><p>Accepted, amended by ADR-0022, ADR-0028, ADR-0036 and ADR-0041 · 2026-10-13</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 delivery was hard-coded in the orchestrator: implement, validate, review, PR, then a human review that ignored gate policies and passed on the merge. A project could not remove a gate as it gained confidence, nor add a step.</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>Projects shape their flow</strong>: a project could not remove a gate nor add a step, and in code every new shape of flow needs a core change.</li><li><strong>The merge is the team&#39;s</strong>: merging is the human&#39;s act at the review, and whether a ticket is merged is a fact of the forge, which the orchestrator observes, whoever merged.</li><li><strong>Only the orchestrator routes</strong>: a skill the model could invoke would let one step chain into another without the orchestrator (ADR-0005), with no budget, cause or write scope.</li><li><strong>Readable config</strong>: a patch is hard to read against a default that moves, and a <code>RETRY</code> in <code>onFail</code> would say what a missing target already means.</li><li><strong>Code needs a consumer</strong>: the core ships only what it consumes (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>Keep the flow in code, with switches per gate.</strong> Every new shape of flow needs a core change, and the switches multiply (1).</li><li><strong>A graph of steps in <code>pipeline.delivery</code>, written whole by the project.</strong> The ticket flow becomes data, which the orchestrator compiles into the ticket machine: removing a gate means deleting it and reconnecting its edges, and a project adds its own steps (1). The default graph stops at the human review, and a merge is a human act the orchestrator observes (2). Only the orchestrator starts a step, so every skill a step runs sets <code>disable-model-invocation: true</code> (3). A project that sets <code>delivery.steps</code> writes the whole graph, and <code>bernard sync</code> warns when the core&#39;s default graph changed under it (4). Merging, resolving conflicts and applying a reviewer&#39;s comments are project-specific, so a project that wants the factory to do them adds its own steps (5).</li><li><strong>A <code>RETRY</code> keyword in <code>onFail</code>.</strong> Running again is what a missing target already means, and <code>RETRY</code> stays the human event (4).</li><li><strong>Overlay patches on the default graph.</strong> A patch is hard to read against a default that moves (4). The project writes its whole graph, and sync flags a changed default.</li><li><strong>A core <code>merge</code> step, with <code>resolve-conflict</code> and <code>apply-pr-feedback</code>.</strong> Merging is the human&#39;s act at the review, and acting on a reviewer&#39;s comments needs that reviewer, to sort the fixes from the replies and to ask when a comment is ambiguous (2). Neither has a consumer in the core (5, ADR-0013).</li><li><strong>Steps that report a merge.</strong> Whether a ticket is merged is a fact of the forge, which the orchestrator observes, whoever merged (2).</li><li><strong>Gates that merge.</strong> A merge can fail, and a failure needs routes and a budget, which only a step has. A gate only observes the merge (2).</li><li><strong>Step skills the model can invoke.</strong> The orchestrator does not need it, and the model would route the ticket in its place (3). Every session would also carry the descriptions of the steps it is not running.</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. Projects shape</th><th>2. Merge is the team&#39;s</th><th>3. Orchestrator routes</th><th>4. Readable config</th><th>5. Consumer</th></tr></thead><tbody><tr><td>Flow in code, with switches</td><td>🔴</td><td>⚪</td><td>⚪</td><td>⚪</td><td>⚪</td></tr><tr><td>A graph of steps, written whole</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td><code>RETRY</code> in <code>onFail</code></td><td>🟢</td><td>⚪</td><td>⚪</td><td>🔴</td><td>⚪</td></tr><tr><td>Overlay patches</td><td>🟢</td><td>⚪</td><td>⚪</td><td>🔴</td><td>⚪</td></tr><tr><td>A core <code>merge</code> step</td><td>⚪</td><td>🔴</td><td>⚪</td><td>⚪</td><td>🔴</td></tr><tr><td>Steps that report a merge</td><td>⚪</td><td>🔴</td><td>⚪</td><td>⚪</td><td>⚪</td></tr><tr><td>Gates that merge</td><td>⚪</td><td>🔴</td><td>⚪</td><td>⚪</td><td>⚪</td></tr><tr><td>Model-invocable step skills</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 graph of steps in <code>pipeline.delivery</code>, written whole by the project. The ticket flow is now data: <code>pipeline.delivery</code> holds a graph of steps, which the orchestrator compiles into the ticket machine. The core ships a default graph; a project that sets <code>delivery.steps</code> writes the whole graph. Amended by ADR-0028: <code>delivery.graphs</code> declares several graphs, each with its own <code>start</code>, <code>steps</code> and <code>maxFailures</code>, and a ticket names the one it goes through.</p><p>The default graph stops at the human review: <code>implement</code>, <code>validate</code>, <code>code-review</code> and <code>apply-review</code>, then <code>pull-request</code>, which opens the pull request and drives its CI green so that it is green before the team reviews, then the <code>human-review</code> gate, then <code>DONE</code>. What follows the review is the team&#39;s: it acts on its own review, then approves or merges the pull request. Merging the pull request, resolving conflicts with the base and applying a reviewer&#39;s comments are project-specific (#13), so a project that wants the factory to do them adds its own steps. Amended by ADR-0036: the core&#39;s <code>pull-request</code> skill merges the base into the ticket&#39;s branch, and resolves the conflicts, before the pull request. Upstream steps keep their <code>auto</code>, <code>generate</code> and <code>interactive</code> modes.</p><ul><li>A <strong>step</strong> runs one skill in a harness session: the skill with its name, or the one its <code>skill</code> field names. A <strong>gate</strong> (<code>gate: true</code>) runs no skill and waits on a human. There are no other kinds.</li><li><code>onPass</code> names the next step, or <code>DONE</code>, which ends the ticket.</li><li><code>onFail</code> names the step a <code>failed</code> result goes to, or one per failure class with an optional <code>default</code>. When it says nothing, the step runs again.</li><li><code>maxFailures</code> counts every <code>failed</code> result of a step, whatever its target. Once it is spent, the step leaves through <code>onExhausted</code>: a step, <code>DONE</code>, or <code>BLOCK</code> (the default). <code>BLOCK</code> waits for a human <code>RETRY</code>, which resumes where the last failure was sent. The budget refills when the step passes, on that <code>RETRY</code>, and when the step leaves through <code>onExhausted</code> to a step or <code>DONE</code>.</li><li>A gate leaves through <code>onPass</code> when the team approves the pull request (<code>GATE.APPROVED</code>) or merges it (<code>PR.MERGED</code>). With <code>onChanges</code>, a change request (<code>GATE.CHANGES_REQUESTED</code>) sends the ticket to that step with the reviewer&#39;s feedback; only that step receives it, every time it runs, until it passes or the gate decides again. Without <code>onChanges</code>, the team acts on the review itself and the gate waits. A gate has a policy, <code>block</code> or <code>decide-and-log</code>, which approves without merging.</li><li>The step a failure is sent to receives that <code>failure</code>: the step and its attempt, the failure class and error.</li><li>A merge is a human act the orchestrator observes, never a step&#39;s report: the gate records it when <code>PR.MERGED</code> arrives. Today a human or the CLI sends that event; a forge adapter watching the pull requests will (#160). A project step that merges by itself leaves its ticket unmerged in the orchestrator&#39;s eyes.</li><li>Crashed runs are not failures: they re-run the same step on a separate budget, <code>pipeline.maxCrashes</code>, shared with upstream steps.</li></ul><p><code>DONE</code> and <code>BLOCK</code> are uppercase, so they never collide with a step name. The config load rejects a target that is neither a step nor an allowed keyword, a <code>start</code> that is missing or a gate, a cycle made only of <code>onPass</code> edges, and a step unreachable from <code>start</code>. <code>bernard sync</code> fails on a step whose skill neither the core nor the overlay has, or whose skill the model can invoke, and warns when the core&#39;s default graph changed under a project that wrote its own.</p><p>Only the orchestrator starts a step, so every skill a step runs sets <code>disable-model-invocation: true</code>. The orchestrator passes <code>/&lt;skill&gt;</code> as the session&#39;s prompt, which the flag does not block. A skill the model could invoke would let one step chain into another without the orchestrator (ADR-0005): <code>implement</code> seeing a red CI and running <code>pull-request</code> itself, with no budget, cause or write scope.</p><p>This amends ADR-0007, whose ticket gate now has a policy and ends the factory&#39;s work on the ticket: the factory no longer applies the humans&#39; review of the final PR, nor merges it. It also amends ADR-0012, whose failure routes now live on each step and whose round budgets become per-step failure budgets.</p><h2 id="consequences" tabindex="-1">Consequences <a class="header-anchor" href="#consequences" aria-label="Permalink to &quot;Consequences&quot;">​</a></h2><ul><li>Removing a gate means deleting it and reconnecting its edges.</li><li>The core ships <code>implement</code>, <code>validate</code>, <code>code-review</code>, <code>apply-review</code> and <code>pull-request</code>, which opens the pull request on the project&#39;s forge through the forge port (ADR-0036); a project with its own way to open a pull request overrides it (ADR-0010). A separate <code>fix-ci</code> step was folded into it: opening a pull request and getting it green are one job, with one budget and one <code>BLOCK</code>.</li><li>Write scopes stay keyed by skill, so two steps that run one skill share its scope.</li><li>A ticket reaching <code>DONE</code> lets its dependents start, once the team has finished with it, so none stacks on a branch still under review. The dependents of a merged ticket branch off main; those of a ticket approved without a merge stack on its branch (<code>stackOn</code> in their requests, which the runner adapter maps to that ticket&#39;s branch); a ticket with more than one unmerged dependency is blocked with an error. Amended by ADR-0022: the orchestrator names the branches, and a ticket&#39;s requests carry its <code>branch</code> and <code>baseBranch</code> instead of <code>stackOn</code>.</li><li>A feature in flight finishes on the graph it started with: <code>restore</code> rebuilds the machine from the pipeline saved in its state, so a config change applies to the next feature.</li><li>A step no longer receives the failure classes of all its previous attempts, only the cause that sent it there.</li></ul>',20)])])}const u=t(r,[["render",d]]);export{g as __pageData,u as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as a,a3 as s}from"./chunks/framework.hgjl6W6o.js";const g=JSON.parse('{"title":"How a project shapes its ticket flow","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0015-how-a-project-shapes-its-ticket-flow.md","filePath":"docs/adr/0015-how-a-project-shapes-its-ticket-flow.md"}'),r={name:"docs/adr/0015-how-a-project-shapes-its-ticket-flow.md"};function d(i,e,n,c,h,l){return o(),a("div",null,[...e[0]||(e[0]=[s("",20)])])}const u=t(r,[["render",d]]);export{g 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 core the factory repo runs","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0016-which-core-the-factory-repo-runs.md","filePath":"docs/adr/0016-which-core-the-factory-repo-runs.md"}'),i={name:"docs/adr/0016-which-core-the-factory-repo-runs.md"};function s(n,e,c,d,h,l){return o(),r("div",null,[...e[0]||(e[0]=[a('<h1 id="which-core-the-factory-repo-runs" tabindex="-1">Which core the factory repo runs <a class="header-anchor" href="#which-core-the-factory-repo-runs" aria-label="Permalink to &quot;Which core the factory repo runs&quot;">​</a></h1><p>Accepted · 2026-10-14</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>This repo is also a project of the factory.</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>Built with the core it ships</strong>: with a published version, we would build with yesterday&#39;s core and learn about a regression only after releasing it.</li><li><strong>Committed like any project&#39;s</strong>: a project commits what <code>sync</code> generates (ADR-0003), and a fresh clone would otherwise have no review until someone syncs.</li><li><strong>Tried on real changes</strong>: the fixtures check the rendering, not whether the skills help on real changes.</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>Pin a published version, as other projects do (ADR-0011).</strong> We would build with yesterday&#39;s core and learn about a regression only after releasing it (1).</li><li><strong>Render the <code>core/</code> of the same commit into a committed <code>.claude/</code>.</strong> The factory is built with the factory it ships (1, 3), and its <code>.claude/</code> is committed like any project&#39;s (2). A change to <code>core/</code> lands with the change to our own skills and agents, and that diff is part of the review.</li><li><strong>Leave the generated files uncommitted.</strong> A project commits what <code>sync</code> generates (ADR-0003), and a fresh clone would have no review until someone syncs (2).</li><li><strong>Rely on the fixtures alone.</strong> They check the rendering, not whether the skills help on real changes (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. Core it ships</th><th>2. Committed</th><th>3. Real changes</th></tr></thead><tbody><tr><td>Pin a published version</td><td>🔴</td><td>⚪</td><td>⚪</td></tr><tr><td>The same commit&#39;s <code>core/</code>, committed</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>Generated files uncommitted</td><td>⚪</td><td>🔴</td><td>⚪</td></tr><tr><td>The fixtures alone</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 the <code>core/</code> of the same commit into a committed <code>.claude/</code>. The repo&#39;s <code>.factory/</code> overlay targets Claude Code, and <code>bernard sync</code> renders the core into its <code>.claude/</code>, committed like any project&#39;s. It renders the <code>core/</code> of the same commit, not a published version, so the factory is built with the factory it ships.</p><p>CI runs <code>pnpm check:sync</code>, which fails when this repo&#39;s <code>.claude/</code> or a fixture no longer matches what <code>bernard sync</code> renders from <code>core/</code>. A change to <code>core/</code> therefore lands with the change to our own skills and agents, and that diff is part of the review.</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 overlay holds only what the core needs from this project: the harness, the project name, and three slots: the checks to run, the detector table, and the sources a finding may cite. Each reviewer keeps its default criteria until real reviews show which ones miss this repo.</li><li>The overlay sets no pipeline. This repo is not a migration, and no step runs headless until a harness driver exists. Running the ticket flow on this repo comes with that driver.</li><li>A change to the default pipeline that <code>sync</code> cannot render here fails CI like a broken fixture.</li></ul>',16)])])}const m=t(i,[["render",s]]);export{p 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 core the factory repo runs","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0016-which-core-the-factory-repo-runs.md","filePath":"docs/adr/0016-which-core-the-factory-repo-runs.md"}'),i={name:"docs/adr/0016-which-core-the-factory-repo-runs.md"};function s(n,e,c,d,h,l){return o(),r("div",null,[...e[0]||(e[0]=[a("",16)])])}const m=t(i,[["render",s]]);export{p as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as r,c as o,a3 as i}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse(`{"title":"Where a ticket's record lives","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0017-where-a-tickets-record-lives.md","filePath":"docs/adr/0017-where-a-tickets-record-lives.md"}`),s={name:"docs/adr/0017-where-a-tickets-record-lives.md"};function a(n,e,c,d,h,l){return r(),o("div",null,[...e[0]||(e[0]=[i('<h1 id="where-a-ticket-s-record-lives" tabindex="-1">Where a ticket&#39;s record lives <a class="header-anchor" href="#where-a-ticket-s-record-lives" aria-label="Permalink to &quot;Where a ticket&#39;s record lives&quot;">​</a></h1><p>Accepted, amended by ADR-0042 · 2026-10-15</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>ADR-0009 put the pipeline state and the handoffs in the project repo, but said neither where nor who writes them (#31). A run left them scattered: the outcomes in the orchestrator&#39;s trace, on the machine that ran it, and the steps&#39; handoffs under the git-ignored scratch directory of each workspace, gone with it. Nothing could be audited from the repo.</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>Auditable from the repo</strong>: a workspace is disposable and its scratch directory is never committed, so what is left there is gone with it.</li><li><strong>Every attempt recorded</strong>: the attempts after the pull request, such as the merge or a resolved conflict, would otherwise never be recorded.</li><li><strong>No conflict between parallel tickets</strong>: tickets in parallel would conflict on a shared state file.</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 them in the scratch directory.</strong> A workspace is disposable, and the scratch directory is never committed (1).</li><li><strong>A record per ticket, committed on its branch.</strong> After each step, the runner commits the record alone, on the branch the working copy is on, and pushes it (1, 2). Each ticket writes only its own record, so tickets in parallel never conflict over it (3).</li><li><strong>Commit them only with the pull request.</strong> The attempts after it (the merge, a resolved conflict) would never be recorded (2).</li><li><strong>One state file per feature, written by every ticket.</strong> Tickets in parallel would conflict on it (3).</li><li><strong>A database.</strong> ADR-0009 rejected it, and #31 found no reason to revisit it.</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. Auditable</th><th>2. Every attempt</th><th>3. No conflict</th></tr></thead><tbody><tr><td>The scratch directory</td><td>🔴</td><td>⚪</td><td>⚪</td></tr><tr><td>A record per ticket, on its branch</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>Only with the pull request</td><td>⚪</td><td>🔴</td><td>⚪</td></tr><tr><td>One state file per feature</td><td>⚪</td><td>⚪</td><td>🔴</td></tr><tr><td>A database</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 record per ticket, committed on its branch. Each ticket has a <strong>record</strong>: <code>&lt;features&gt;/&lt;feature&gt;/.pipeline/&lt;ticket&gt;/</code>, and <code>feature/</code> for the upstream steps.</p><ul><li><code>log.jsonl</code> is the record&#39;s log: one line per step result, with its outcome (ADR-0012). The orchestrator appends it after each step. ADR-0020 extends it to every event of the run.</li><li><code>&lt;step&gt;-&lt;attempt&gt;/</code> holds what later steps and reviewers read: that step&#39;s handoff, <code>handoff.json</code>, which the runner writes from the step&#39;s answer (ADR-0025), such as a code review&#39;s merged findings or a validation&#39;s report; and the files the step produced for review, such as a pull request&#39;s description. The step receives the directory as <code>handoffDir</code> in its request. Working files, such as the review bundle and each reviewer&#39;s report, stay in the scratch directory.</li><li>After each step, the runner commits the record alone, on the branch the working copy is on, and pushes it: a ticket&#39;s record to the ticket&#39;s branch on the remote, setting it as the upstream, so a ticket that has not reached its pull request is still on the remote; the feature&#39;s record to the base branch. A ticket&#39;s branch holds its whole record; the merge brings it to the base branch.</li><li>Each ticket writes only its own record, so tickets in parallel never conflict over it.</li><li>The record is outside every write scope (ADR-0014): a step adds its files for review there.</li></ul><p>A record that cannot be written is a warning in the run&#39;s trace, not a failure of the step: the step&#39;s outcome stands.</p><p>This settles where the handoffs and the log live. Resuming an interrupted run from the repo stays open (#31): nothing here stores the machines&#39; state.</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 ticket&#39;s branch carries a commit per step, <code>chore(pipeline): record &lt;step&gt; #&lt;attempt&gt;</code>, besides the change itself.</li><li>A project marks <code>.pipeline/</code> as generated in its <code>.gitattributes</code> (<code>linguist-generated</code>), so that its forge folds the record in pull request diffs.</li><li>A step that commits or pushes leaves <code>.pipeline/</code> to the orchestrator.</li><li>The runner needs to write and commit in the working copy, as it already reads and restores it for write scopes.</li></ul><p>Amended by ADR-0042: a secret leak in a record is fatal, not a warning.</p>',19)])])}const f=t(s,[["render",a]]);export{u as __pageData,f as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as r,c as o,a3 as i}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse(`{"title":"Where a ticket's record lives","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0017-where-a-tickets-record-lives.md","filePath":"docs/adr/0017-where-a-tickets-record-lives.md"}`),s={name:"docs/adr/0017-where-a-tickets-record-lives.md"};function a(n,e,c,d,h,l){return r(),o("div",null,[...e[0]||(e[0]=[i("",19)])])}const f=t(s,[["render",a]]);export{u as __pageData,f as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as a,a3 as r}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse(`{"title":"The product's name","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0018-the-products-name.md","filePath":"docs/adr/0018-the-products-name.md"}`),d={name:"docs/adr/0018-the-products-name.md"};function c(n,e,s,i,h,l){return o(),a("div",null,[...e[0]||(e[0]=[r('<h1 id="the-product-s-name" tabindex="-1">The product&#39;s name <a class="header-anchor" href="#the-product-s-name" aria-label="Permalink to &quot;The product&#39;s name&quot;">​</a></h1><p>Accepted · 2026-10-20</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>The product is named <strong>Bernard the taktician</strong>, after the hermit crab already drawn in <code>docs/assets/bernard.svg</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>A name, not a description</strong>: &quot;AI-Mod Factory&quot; is a description, not a name.</li><li><strong>The mascot&#39;s identity</strong>: the mascot already carried the identity.</li><li><strong>A folder names what it holds</strong>: <code>.factory/</code> holds a project&#39;s factory overlay, not the product.</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 &quot;AI-Mod Factory&quot;.</strong> A description, not a name (1), and the mascot already carried the identity (2).</li><li><strong>Bernard the taktician, keeping <code>.factory/</code>.</strong> A name, after the hermit crab already drawn in <code>docs/assets/bernard.svg</code> (1, 2), while <code>.factory/</code> still holds a project&#39;s factory overlay (3).</li><li><strong>Rename <code>.factory/</code> to <code>.bernard/</code> too.</strong> The folder names what it holds, a project&#39;s factory overlay, not the product (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. A name</th><th>2. The mascot</th><th>3. Folder names its content</th></tr></thead><tbody><tr><td>Keep &quot;AI-Mod Factory&quot;</td><td>🔴</td><td>🔴</td><td>⚪</td></tr><tr><td>Bernard, keeping <code>.factory/</code></td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>Rename <code>.factory/</code> too</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>Bernard the taktician, keeping <code>.factory/</code>. Bernard&#39;s CLI is <code>bernard</code>: <code>bernard sync</code>, <code>bernard pipeline</code>, <code>bernard run</code>. Its workspace packages are named under the <code>@bernard/</code> scope and stay private. The package it publishes is <code>@theodo/bernard</code>, under the scope of Theodo&#39;s npm organisation (ADR-0011).</p><p>The name replaces &quot;AI-Mod Factory&quot; everywhere the product names itself: the README, <code>CONTEXT.md</code>, the header of every rendered file, the demo, the CLI&#39;s usage.</p><p>&quot;Factory&quot; stays a word of the vocabulary: the core, the overlays and the pipeline still make up a factory, and a project&#39;s overlay still lives in <code>.factory/</code>, configured by <code>factory.config.ts</code>. Renaming <code>.factory/</code> to <code>.bernard/</code> is ruled 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>The GitHub repo <code>theodo-group/ai-mod-factory</code> keeps its slug until it is renamed on GitHub; <code>docs/github.md</code> follows when it is.</li></ul>',17)])])}const m=t(d,[["render",c]]);export{u as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o,c as a,a3 as r}from"./chunks/framework.hgjl6W6o.js";const u=JSON.parse(`{"title":"The product's name","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0018-the-products-name.md","filePath":"docs/adr/0018-the-products-name.md"}`),d={name:"docs/adr/0018-the-products-name.md"};function c(n,e,s,i,h,l){return o(),a("div",null,[...e[0]||(e[0]=[r("",17)])])}const m=t(d,[["render",c]]);export{u as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as d,c as o,a3 as n}from"./chunks/framework.hgjl6W6o.js";const g=JSON.parse('{"title":"How a finding is answered and judged","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0019-how-a-finding-is-answered-and-judged.md","filePath":"docs/adr/0019-how-a-finding-is-answered-and-judged.md"}'),i={name:"docs/adr/0019-how-a-finding-is-answered-and-judged.md"};function a(r,e,s,c,l,h){return d(),o("div",null,[...e[0]||(e[0]=[n('<h1 id="how-a-finding-is-answered-and-judged" tabindex="-1">How a finding is answered and judged <a class="header-anchor" href="#how-a-finding-is-answered-and-judged" aria-label="Permalink to &quot;How a finding is answered and judged&quot;">​</a></h1><p>Accepted · 2026-10-19</p><h2 id="context" tabindex="-1">Context <a class="header-anchor" href="#context" aria-label="Permalink to &quot;Context&quot;">​</a></h2><p>Two controllers send a ticket back: <code>validate</code> to <code>implement</code>, <code>code-review</code> to <code>apply-review</code> (ADR-0015). Until now the fixing step fixed what it agreed with and said the rest in prose. Nothing recorded which finding it had dropped, or why, and a controller could not tell a finding answered from a finding ignored.</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>Every finding answered</strong>: nothing recorded which finding the fixing step had dropped, or why, and a controller could not tell a finding answered from a finding ignored.</li><li><strong>Controllers judge cold</strong>: a controller that sees the earlier verdicts is biased.</li><li><strong>The ticket keeps moving</strong>: an open question for every contest would stop the ticket at every disagreement.</li><li><strong>Explicit and true</strong>: finding the failed handoff by convention is implicit, and wrong after a <code>BLOCK</code> and a <code>RETRY</code>; calling nits contested is not true.</li><li><strong>Each concern with its owner</strong>: <code>implement</code> already owns the ticket&#39;s code and its tests, <code>feedback</code> is what a human asked at a gate, and whether two findings are the same defect is a rule a schema cannot tell, which lives in the skills.</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 separate <code>apply-validation</code> step after <code>validate</code>.</strong> <code>implement</code> already owns the ticket&#39;s code and its tests; validation failures go back to it (5).</li><li><strong>A verdict for every finding, a contest settled by rediscovery.</strong> The fixing step writes one verdict per finding it received, in its handoff (1). <code>validate</code> and <code>code-review</code> read the code, the ticket and the project&#39;s context, never an earlier verdict or contest (2). A contest nobody finds again stands, and the pull request lists it for the human review (3). A request sent back by a failure carries <code>failure.handoffDir</code>, so the fixing step reads its findings without guessing, and <code>deferred</code> covers a <code>minor</code> or <code>nit</code> review finding (4). The step judges whether two findings are the same defect (5).</li><li><strong>Validation findings in the request&#39;s <code>feedback</code>.</strong> <code>feedback</code> is what a human asked at a gate, with its own clearing rules (5).</li><li><strong>Find the failed handoff by convention, the most recent <code>&lt;step&gt;-&lt;n&gt;/</code>.</strong> Implicit, and wrong after a <code>BLOCK</code> and a <code>RETRY</code> (4).</li><li><strong>A <code>previous</code> field on every request.</strong> It served only to show verdicts to controllers, who judge cold (2).</li><li><strong>Controllers settle contests.</strong> They receive the contested findings and drop or rebut them. A controller that sees the earlier verdicts is biased (2). It would also need finding ids stable across reviews and a <code>rebuttal</code> field.</li><li><strong>Every contest becomes an open question.</strong> It would stop the ticket at every disagreement (3).</li><li><strong>A <code>recontested</code> flag in the handoff.</strong> A schema cannot tell that two findings are the same defect; the rule lives in the skills (5).</li><li><strong>No <code>deferred</code>.</strong> The step would have to fix every nit, or call nits contested, which is not true (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. Every finding answered</th><th>2. Judge cold</th><th>3. Ticket moves</th><th>4. Explicit and true</th><th>5. Concern with its owner</th></tr></thead><tbody><tr><td>A separate <code>apply-validation</code> step</td><td>⚪</td><td>⚪</td><td>⚪</td><td>⚪</td><td>🔴</td></tr><tr><td>A verdict per finding, rediscovery</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td><td>🟢</td></tr><tr><td>Findings in <code>feedback</code></td><td>⚪</td><td>⚪</td><td>⚪</td><td>⚪</td><td>🔴</td></tr><tr><td>Failed handoff by convention</td><td>⚪</td><td>⚪</td><td>⚪</td><td>🔴</td><td>⚪</td></tr><tr><td>A <code>previous</code> field</td><td>⚪</td><td>🔴</td><td>⚪</td><td>⚪</td><td>⚪</td></tr><tr><td>Controllers settle contests</td><td>⚪</td><td>🔴</td><td>⚪</td><td>⚪</td><td>⚪</td></tr><tr><td>Every contest an open question</td><td>⚪</td><td>⚪</td><td>🔴</td><td>⚪</td><td>⚪</td></tr><tr><td>A <code>recontested</code> flag</td><td>⚪</td><td>⚪</td><td>⚪</td><td>⚪</td><td>🔴</td></tr><tr><td>No <code>deferred</code></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 verdict for every finding, a contest settled by rediscovery.</p><ul><li><strong>A verdict for every finding.</strong> The fixing step writes one verdict per finding it received, in its handoff: <code>fixed</code>; <code>contested</code>, with a required proof, a located citation (<code>path § section</code> of the ticket, the plan, an ADR, a standard) or a concrete scenario; <code>deferred</code>, for a <code>minor</code> or <code>nit</code> review finding only; <code>questioned</code>, when the step raised an open question instead (below). A verdict cites its finding by position in the handoff it answers, and that handoff&#39;s path is in the fixing step&#39;s handoff.</li><li><strong>Pass condition.</strong> <code>implement</code> passes when every validation finding has a verdict, every acceptance criterion has a test and the checks are green. <code>apply-review</code> passes when every <code>blocking</code> and <code>major</code> finding is <code>fixed</code>, <code>contested</code> or <code>questioned</code>, and the checks are green.</li><li><strong>Controllers judge cold.</strong> <code>validate</code> and <code>code-review</code> read the code, the ticket and the project&#39;s context, never an earlier verdict or contest.</li><li><strong>A contest is settled by rediscovery.</strong> The fixing step reads its own previous handoff. When an independent controller finds again a defect the step contested (same place, same problem), the contest is rejected: the step fixes it or raises an open question, and may not contest it twice. The step judges whether two findings are the same defect. A contest nobody finds again stands, and the pull request lists it for the human review.</li><li><strong>The failed handoff is named.</strong> A request sent back by a failure carries <code>failure.handoffDir</code>, the failed attempt&#39;s handoff directory in the ticket&#39;s record (ADR-0012, ADR-0017), so the fixing step reads its findings without guessing.</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>A step can only drop a blocking or major finding by proving the code right, and an independent controller rediscovering the defect overrides it.</li><li>The contests that stand reach the human review through the pull request&#39;s description.</li><li>The handoffs&#39; schemas carry the rules one handoff can prove (ADR-0025): verdicts are a union on <code>verdict</code>, so a <code>contested</code> one requires its <code>proof</code> and a <code>questioned</code> one its <code>question</code>, and in <code>apply-review</code> a <code>deferred</code> verdict copies a <code>minor</code> or <code>nit</code> severity. The rules that span handoffs stay in the skills: one verdict per finding of <code>findingsFrom</code>, a contest made only once, and whether <code>passed</code> is deserved.</li></ul>',16)])])}const p=t(i,[["render",a]]);export{g as __pageData,p as default};
@@ -0,0 +1 @@
1
+ import{_ as t,o as d,c as o,a3 as n}from"./chunks/framework.hgjl6W6o.js";const g=JSON.parse('{"title":"How a finding is answered and judged","description":"","frontmatter":{},"headers":[],"relativePath":"docs/adr/0019-how-a-finding-is-answered-and-judged.md","filePath":"docs/adr/0019-how-a-finding-is-answered-and-judged.md"}'),i={name:"docs/adr/0019-how-a-finding-is-answered-and-judged.md"};function a(r,e,s,c,l,h){return d(),o("div",null,[...e[0]||(e[0]=[n("",16)])])}const p=t(i,[["render",a]]);export{g as __pageData,p as default};