@garygentry/feature-forge 0.2.14 → 0.3.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 (486) hide show
  1. package/README.md +6 -3
  2. package/adapters/GENERATION-REPORT.md +20 -0
  3. package/adapters/claude/.feature-forge-bundle.json +1 -1
  4. package/adapters/claude/agents/forge-verifier.md +1 -1
  5. package/adapters/claude/references/forge-config-schema.json +2 -2
  6. package/adapters/claude/references/pipeline-state-schema.json +1 -1
  7. package/adapters/claude/references/shared-conventions.md +62 -12
  8. package/adapters/claude/references/stage-exit-protocol.md +14 -4
  9. package/adapters/claude/references/vendor-construct-inventory.md +1 -1
  10. package/adapters/claude/scripts/epic-manifest.py +49 -6
  11. package/adapters/claude/scripts/forge-root.sh +47 -3
  12. package/adapters/claude/scripts/forge-session.py +1179 -9
  13. package/adapters/claude/scripts/validate-traceability.py +6 -1
  14. package/adapters/claude/skills/forge/SKILL.md +11 -5
  15. package/adapters/claude/skills/forge/references/pipeline-state-schema.json +1 -1
  16. package/adapters/claude/skills/forge/references/shared-conventions.md +62 -12
  17. package/adapters/claude/skills/forge/references/stage-exit-protocol.md +14 -4
  18. package/adapters/claude/skills/forge-0-epic/SKILL.md +1 -1
  19. package/adapters/claude/skills/forge-0-epic/references/pipeline-state-schema.json +1 -1
  20. package/adapters/claude/skills/forge-0-epic/references/shared-conventions.md +62 -12
  21. package/adapters/claude/skills/forge-0-epic/references/stage-exit-protocol.md +14 -4
  22. package/adapters/claude/skills/forge-1-prd/SKILL.md +25 -8
  23. package/adapters/claude/skills/forge-1-prd/references/shared-conventions.md +62 -12
  24. package/adapters/claude/skills/forge-1-prd/references/stage-exit-protocol.md +14 -4
  25. package/adapters/claude/skills/forge-2-tech/SKILL.md +25 -7
  26. package/adapters/claude/skills/forge-2-tech/references/shared-conventions.md +62 -12
  27. package/adapters/claude/skills/forge-2-tech/references/stage-exit-protocol.md +14 -4
  28. package/adapters/claude/skills/forge-3-specs/SKILL.md +24 -7
  29. package/adapters/claude/skills/forge-3-specs/references/shared-conventions.md +62 -12
  30. package/adapters/claude/skills/forge-3-specs/references/stage-exit-protocol.md +14 -4
  31. package/adapters/claude/skills/forge-4-backlog/SKILL.md +23 -7
  32. package/adapters/claude/skills/forge-4-backlog/references/shared-conventions.md +62 -12
  33. package/adapters/claude/skills/forge-4-backlog/references/stage-exit-protocol.md +14 -4
  34. package/adapters/claude/skills/forge-5-loop/SKILL.md +25 -25
  35. package/adapters/claude/skills/forge-5-loop/references/agent-selection.md +116 -0
  36. package/adapters/claude/skills/forge-5-loop/references/runner-contract.md +14 -107
  37. package/adapters/claude/skills/forge-5-loop/references/shared-conventions.md +62 -12
  38. package/adapters/claude/skills/forge-5-loop/references/stage-exit-protocol.md +14 -4
  39. package/adapters/claude/skills/forge-6-docs/SKILL.md +11 -5
  40. package/adapters/claude/skills/forge-6-docs/references/shared-conventions.md +62 -12
  41. package/adapters/claude/skills/forge-fix/references/shared-conventions.md +62 -12
  42. package/adapters/claude/skills/forge-fix/references/stage-exit-protocol.md +14 -4
  43. package/adapters/claude/skills/forge-guide/references/forge-config-schema.json +2 -2
  44. package/adapters/claude/skills/forge-guide/references/shared-conventions.md +62 -12
  45. package/adapters/claude/skills/forge-verify/SKILL.md +22 -12
  46. package/adapters/claude/skills/forge-verify/references/findings-template.md +157 -0
  47. package/adapters/claude/skills/forge-verify/references/pipeline-state-schema.json +1 -1
  48. package/adapters/claude/skills/forge-verify/references/shared-conventions.md +62 -12
  49. package/adapters/claude/skills/forge-verify/references/verification-checklists/backlog.md +97 -0
  50. package/adapters/claude/skills/forge-verify/references/verification-checklists/epic.md +79 -0
  51. package/adapters/claude/skills/forge-verify/references/verification-checklists/impl.md +48 -0
  52. package/adapters/claude/skills/forge-verify/references/verification-checklists/prd.md +31 -0
  53. package/adapters/claude/skills/forge-verify/references/verification-checklists/specs.md +64 -0
  54. package/adapters/claude/skills/forge-verify/references/verification-checklists/tech.md +35 -0
  55. package/adapters/codex/.feature-forge-bundle.json +1 -1
  56. package/adapters/codex/agents/forge-verifier.toml +1 -1
  57. package/adapters/codex/references/forge-config-schema.json +2 -2
  58. package/adapters/codex/references/pipeline-state-schema.json +1 -1
  59. package/adapters/codex/references/shared-conventions.md +62 -12
  60. package/adapters/codex/references/stage-exit-protocol.md +14 -4
  61. package/adapters/codex/references/vendor-construct-inventory.md +1 -1
  62. package/adapters/codex/scripts/epic-manifest.py +49 -6
  63. package/adapters/codex/scripts/forge-root.sh +47 -3
  64. package/adapters/codex/scripts/forge-session.py +1179 -9
  65. package/adapters/codex/scripts/validate-traceability.py +6 -1
  66. package/adapters/codex/skills/forge/SKILL.md +11 -5
  67. package/adapters/codex/skills/forge/references/pipeline-state-schema.json +1 -1
  68. package/adapters/codex/skills/forge/references/shared-conventions.md +62 -12
  69. package/adapters/codex/skills/forge/references/stage-exit-protocol.md +14 -4
  70. package/adapters/codex/skills/forge-0-epic/SKILL.md +1 -1
  71. package/adapters/codex/skills/forge-0-epic/references/pipeline-state-schema.json +1 -1
  72. package/adapters/codex/skills/forge-0-epic/references/shared-conventions.md +62 -12
  73. package/adapters/codex/skills/forge-0-epic/references/stage-exit-protocol.md +14 -4
  74. package/adapters/codex/skills/forge-1-prd/SKILL.md +25 -8
  75. package/adapters/codex/skills/forge-1-prd/references/shared-conventions.md +62 -12
  76. package/adapters/codex/skills/forge-1-prd/references/stage-exit-protocol.md +14 -4
  77. package/adapters/codex/skills/forge-2-tech/SKILL.md +25 -7
  78. package/adapters/codex/skills/forge-2-tech/references/shared-conventions.md +62 -12
  79. package/adapters/codex/skills/forge-2-tech/references/stage-exit-protocol.md +14 -4
  80. package/adapters/codex/skills/forge-3-specs/SKILL.md +24 -7
  81. package/adapters/codex/skills/forge-3-specs/references/shared-conventions.md +62 -12
  82. package/adapters/codex/skills/forge-3-specs/references/stage-exit-protocol.md +14 -4
  83. package/adapters/codex/skills/forge-4-backlog/SKILL.md +23 -7
  84. package/adapters/codex/skills/forge-4-backlog/references/shared-conventions.md +62 -12
  85. package/adapters/codex/skills/forge-4-backlog/references/stage-exit-protocol.md +14 -4
  86. package/adapters/codex/skills/forge-5-loop/SKILL.md +25 -25
  87. package/adapters/codex/skills/forge-5-loop/references/agent-selection.md +116 -0
  88. package/adapters/codex/skills/forge-5-loop/references/runner-contract.md +14 -107
  89. package/adapters/codex/skills/forge-5-loop/references/shared-conventions.md +62 -12
  90. package/adapters/codex/skills/forge-5-loop/references/stage-exit-protocol.md +14 -4
  91. package/adapters/codex/skills/forge-6-docs/SKILL.md +11 -5
  92. package/adapters/codex/skills/forge-6-docs/references/shared-conventions.md +62 -12
  93. package/adapters/codex/skills/forge-fix/references/shared-conventions.md +62 -12
  94. package/adapters/codex/skills/forge-fix/references/stage-exit-protocol.md +14 -4
  95. package/adapters/codex/skills/forge-guide/references/forge-config-schema.json +2 -2
  96. package/adapters/codex/skills/forge-guide/references/shared-conventions.md +62 -12
  97. package/adapters/codex/skills/forge-verify/SKILL.md +22 -12
  98. package/adapters/codex/skills/forge-verify/references/findings-template.md +157 -0
  99. package/adapters/codex/skills/forge-verify/references/pipeline-state-schema.json +1 -1
  100. package/adapters/codex/skills/forge-verify/references/shared-conventions.md +62 -12
  101. package/adapters/codex/skills/forge-verify/references/verification-checklists/backlog.md +97 -0
  102. package/adapters/codex/skills/forge-verify/references/verification-checklists/epic.md +79 -0
  103. package/adapters/codex/skills/forge-verify/references/verification-checklists/impl.md +48 -0
  104. package/adapters/codex/skills/forge-verify/references/verification-checklists/prd.md +31 -0
  105. package/adapters/codex/skills/forge-verify/references/verification-checklists/specs.md +64 -0
  106. package/adapters/codex/skills/forge-verify/references/verification-checklists/tech.md +35 -0
  107. package/adapters/copilot/.feature-forge-bundle.json +1 -1
  108. package/adapters/copilot/agents/forge-verifier.md +1 -1
  109. package/adapters/copilot/references/forge-config-schema.json +2 -2
  110. package/adapters/copilot/references/pipeline-state-schema.json +1 -1
  111. package/adapters/copilot/references/shared-conventions.md +62 -12
  112. package/adapters/copilot/references/stage-exit-protocol.md +14 -4
  113. package/adapters/copilot/references/vendor-construct-inventory.md +1 -1
  114. package/adapters/copilot/scripts/epic-manifest.py +49 -6
  115. package/adapters/copilot/scripts/forge-root.sh +47 -3
  116. package/adapters/copilot/scripts/forge-session.py +1179 -9
  117. package/adapters/copilot/scripts/validate-traceability.py +6 -1
  118. package/adapters/copilot/skills/forge/forge.md +11 -5
  119. package/adapters/copilot/skills/forge/references/pipeline-state-schema.json +1 -1
  120. package/adapters/copilot/skills/forge/references/shared-conventions.md +62 -12
  121. package/adapters/copilot/skills/forge/references/stage-exit-protocol.md +14 -4
  122. package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +1 -1
  123. package/adapters/copilot/skills/forge-0-epic/references/pipeline-state-schema.json +1 -1
  124. package/adapters/copilot/skills/forge-0-epic/references/shared-conventions.md +62 -12
  125. package/adapters/copilot/skills/forge-0-epic/references/stage-exit-protocol.md +14 -4
  126. package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +25 -8
  127. package/adapters/copilot/skills/forge-1-prd/references/shared-conventions.md +62 -12
  128. package/adapters/copilot/skills/forge-1-prd/references/stage-exit-protocol.md +14 -4
  129. package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +25 -7
  130. package/adapters/copilot/skills/forge-2-tech/references/shared-conventions.md +62 -12
  131. package/adapters/copilot/skills/forge-2-tech/references/stage-exit-protocol.md +14 -4
  132. package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +24 -7
  133. package/adapters/copilot/skills/forge-3-specs/references/shared-conventions.md +62 -12
  134. package/adapters/copilot/skills/forge-3-specs/references/stage-exit-protocol.md +14 -4
  135. package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +23 -7
  136. package/adapters/copilot/skills/forge-4-backlog/references/shared-conventions.md +62 -12
  137. package/adapters/copilot/skills/forge-4-backlog/references/stage-exit-protocol.md +14 -4
  138. package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +25 -25
  139. package/adapters/copilot/skills/forge-5-loop/references/agent-selection.md +116 -0
  140. package/adapters/copilot/skills/forge-5-loop/references/runner-contract.md +14 -107
  141. package/adapters/copilot/skills/forge-5-loop/references/shared-conventions.md +62 -12
  142. package/adapters/copilot/skills/forge-5-loop/references/stage-exit-protocol.md +14 -4
  143. package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +11 -5
  144. package/adapters/copilot/skills/forge-6-docs/references/shared-conventions.md +62 -12
  145. package/adapters/copilot/skills/forge-fix/references/shared-conventions.md +62 -12
  146. package/adapters/copilot/skills/forge-fix/references/stage-exit-protocol.md +14 -4
  147. package/adapters/copilot/skills/forge-guide/references/forge-config-schema.json +2 -2
  148. package/adapters/copilot/skills/forge-guide/references/shared-conventions.md +62 -12
  149. package/adapters/copilot/skills/forge-verify/forge-verify.md +22 -12
  150. package/adapters/copilot/skills/forge-verify/references/findings-template.md +157 -0
  151. package/adapters/copilot/skills/forge-verify/references/pipeline-state-schema.json +1 -1
  152. package/adapters/copilot/skills/forge-verify/references/shared-conventions.md +62 -12
  153. package/adapters/copilot/skills/forge-verify/references/verification-checklists/backlog.md +97 -0
  154. package/adapters/copilot/skills/forge-verify/references/verification-checklists/epic.md +79 -0
  155. package/adapters/copilot/skills/forge-verify/references/verification-checklists/impl.md +48 -0
  156. package/adapters/copilot/skills/forge-verify/references/verification-checklists/prd.md +31 -0
  157. package/adapters/copilot/skills/forge-verify/references/verification-checklists/specs.md +64 -0
  158. package/adapters/copilot/skills/forge-verify/references/verification-checklists/tech.md +35 -0
  159. package/adapters/cursor/.feature-forge-bundle.json +1 -1
  160. package/adapters/cursor/agents/forge-verifier.mdc +1 -1
  161. package/adapters/cursor/references/forge-config-schema.json +2 -2
  162. package/adapters/cursor/references/pipeline-state-schema.json +1 -1
  163. package/adapters/cursor/references/shared-conventions.md +62 -12
  164. package/adapters/cursor/references/stage-exit-protocol.md +14 -4
  165. package/adapters/cursor/references/vendor-construct-inventory.md +1 -1
  166. package/adapters/cursor/scripts/epic-manifest.py +49 -6
  167. package/adapters/cursor/scripts/forge-root.sh +47 -3
  168. package/adapters/cursor/scripts/forge-session.py +1179 -9
  169. package/adapters/cursor/scripts/validate-traceability.py +6 -1
  170. package/adapters/cursor/skills/forge/forge.mdc +11 -5
  171. package/adapters/cursor/skills/forge/references/pipeline-state-schema.json +1 -1
  172. package/adapters/cursor/skills/forge/references/shared-conventions.md +62 -12
  173. package/adapters/cursor/skills/forge/references/stage-exit-protocol.md +14 -4
  174. package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +1 -1
  175. package/adapters/cursor/skills/forge-0-epic/references/pipeline-state-schema.json +1 -1
  176. package/adapters/cursor/skills/forge-0-epic/references/shared-conventions.md +62 -12
  177. package/adapters/cursor/skills/forge-0-epic/references/stage-exit-protocol.md +14 -4
  178. package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +25 -8
  179. package/adapters/cursor/skills/forge-1-prd/references/shared-conventions.md +62 -12
  180. package/adapters/cursor/skills/forge-1-prd/references/stage-exit-protocol.md +14 -4
  181. package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +25 -7
  182. package/adapters/cursor/skills/forge-2-tech/references/shared-conventions.md +62 -12
  183. package/adapters/cursor/skills/forge-2-tech/references/stage-exit-protocol.md +14 -4
  184. package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +24 -7
  185. package/adapters/cursor/skills/forge-3-specs/references/shared-conventions.md +62 -12
  186. package/adapters/cursor/skills/forge-3-specs/references/stage-exit-protocol.md +14 -4
  187. package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +23 -7
  188. package/adapters/cursor/skills/forge-4-backlog/references/shared-conventions.md +62 -12
  189. package/adapters/cursor/skills/forge-4-backlog/references/stage-exit-protocol.md +14 -4
  190. package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +25 -25
  191. package/adapters/cursor/skills/forge-5-loop/references/agent-selection.md +116 -0
  192. package/adapters/cursor/skills/forge-5-loop/references/runner-contract.md +14 -107
  193. package/adapters/cursor/skills/forge-5-loop/references/shared-conventions.md +62 -12
  194. package/adapters/cursor/skills/forge-5-loop/references/stage-exit-protocol.md +14 -4
  195. package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +11 -5
  196. package/adapters/cursor/skills/forge-6-docs/references/shared-conventions.md +62 -12
  197. package/adapters/cursor/skills/forge-fix/references/shared-conventions.md +62 -12
  198. package/adapters/cursor/skills/forge-fix/references/stage-exit-protocol.md +14 -4
  199. package/adapters/cursor/skills/forge-guide/references/forge-config-schema.json +2 -2
  200. package/adapters/cursor/skills/forge-guide/references/shared-conventions.md +62 -12
  201. package/adapters/cursor/skills/forge-verify/forge-verify.mdc +22 -12
  202. package/adapters/cursor/skills/forge-verify/references/findings-template.md +157 -0
  203. package/adapters/cursor/skills/forge-verify/references/pipeline-state-schema.json +1 -1
  204. package/adapters/cursor/skills/forge-verify/references/shared-conventions.md +62 -12
  205. package/adapters/cursor/skills/forge-verify/references/verification-checklists/backlog.md +97 -0
  206. package/adapters/cursor/skills/forge-verify/references/verification-checklists/epic.md +79 -0
  207. package/adapters/cursor/skills/forge-verify/references/verification-checklists/impl.md +48 -0
  208. package/adapters/cursor/skills/forge-verify/references/verification-checklists/prd.md +31 -0
  209. package/adapters/cursor/skills/forge-verify/references/verification-checklists/specs.md +64 -0
  210. package/adapters/cursor/skills/forge-verify/references/verification-checklists/tech.md +35 -0
  211. package/adapters/gemini/.feature-forge-bundle.json +1 -1
  212. package/adapters/gemini/agents/forge-verifier.md +1 -1
  213. package/adapters/gemini/gemini-extension.json +1 -1
  214. package/adapters/gemini/references/forge-config-schema.json +2 -2
  215. package/adapters/gemini/references/pipeline-state-schema.json +1 -1
  216. package/adapters/gemini/references/shared-conventions.md +62 -12
  217. package/adapters/gemini/references/stage-exit-protocol.md +14 -4
  218. package/adapters/gemini/references/vendor-construct-inventory.md +1 -1
  219. package/adapters/gemini/scripts/epic-manifest.py +49 -6
  220. package/adapters/gemini/scripts/forge-root.sh +47 -3
  221. package/adapters/gemini/scripts/forge-session.py +1179 -9
  222. package/adapters/gemini/scripts/validate-traceability.py +6 -1
  223. package/adapters/gemini/skills/forge/forge.md +11 -5
  224. package/adapters/gemini/skills/forge/references/pipeline-state-schema.json +1 -1
  225. package/adapters/gemini/skills/forge/references/shared-conventions.md +62 -12
  226. package/adapters/gemini/skills/forge/references/stage-exit-protocol.md +14 -4
  227. package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +1 -1
  228. package/adapters/gemini/skills/forge-0-epic/references/pipeline-state-schema.json +1 -1
  229. package/adapters/gemini/skills/forge-0-epic/references/shared-conventions.md +62 -12
  230. package/adapters/gemini/skills/forge-0-epic/references/stage-exit-protocol.md +14 -4
  231. package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +25 -8
  232. package/adapters/gemini/skills/forge-1-prd/references/shared-conventions.md +62 -12
  233. package/adapters/gemini/skills/forge-1-prd/references/stage-exit-protocol.md +14 -4
  234. package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +25 -7
  235. package/adapters/gemini/skills/forge-2-tech/references/shared-conventions.md +62 -12
  236. package/adapters/gemini/skills/forge-2-tech/references/stage-exit-protocol.md +14 -4
  237. package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +24 -7
  238. package/adapters/gemini/skills/forge-3-specs/references/shared-conventions.md +62 -12
  239. package/adapters/gemini/skills/forge-3-specs/references/stage-exit-protocol.md +14 -4
  240. package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +23 -7
  241. package/adapters/gemini/skills/forge-4-backlog/references/shared-conventions.md +62 -12
  242. package/adapters/gemini/skills/forge-4-backlog/references/stage-exit-protocol.md +14 -4
  243. package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +25 -25
  244. package/adapters/gemini/skills/forge-5-loop/references/agent-selection.md +116 -0
  245. package/adapters/gemini/skills/forge-5-loop/references/runner-contract.md +14 -107
  246. package/adapters/gemini/skills/forge-5-loop/references/shared-conventions.md +62 -12
  247. package/adapters/gemini/skills/forge-5-loop/references/stage-exit-protocol.md +14 -4
  248. package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +11 -5
  249. package/adapters/gemini/skills/forge-6-docs/references/shared-conventions.md +62 -12
  250. package/adapters/gemini/skills/forge-fix/references/shared-conventions.md +62 -12
  251. package/adapters/gemini/skills/forge-fix/references/stage-exit-protocol.md +14 -4
  252. package/adapters/gemini/skills/forge-guide/references/forge-config-schema.json +2 -2
  253. package/adapters/gemini/skills/forge-guide/references/shared-conventions.md +62 -12
  254. package/adapters/gemini/skills/forge-verify/forge-verify.md +22 -12
  255. package/adapters/gemini/skills/forge-verify/references/findings-template.md +157 -0
  256. package/adapters/gemini/skills/forge-verify/references/pipeline-state-schema.json +1 -1
  257. package/adapters/gemini/skills/forge-verify/references/shared-conventions.md +62 -12
  258. package/adapters/gemini/skills/forge-verify/references/verification-checklists/backlog.md +97 -0
  259. package/adapters/gemini/skills/forge-verify/references/verification-checklists/epic.md +79 -0
  260. package/adapters/gemini/skills/forge-verify/references/verification-checklists/impl.md +48 -0
  261. package/adapters/gemini/skills/forge-verify/references/verification-checklists/prd.md +31 -0
  262. package/adapters/gemini/skills/forge-verify/references/verification-checklists/specs.md +64 -0
  263. package/adapters/gemini/skills/forge-verify/references/verification-checklists/tech.md +35 -0
  264. package/adapters/pi/.feature-forge-bundle.json +6 -0
  265. package/adapters/pi/agents/forge-researcher.md +139 -0
  266. package/adapters/pi/agents/forge-spec-writer.md +116 -0
  267. package/adapters/pi/agents/forge-verifier.md +126 -0
  268. package/adapters/pi/extensions/ask-user-question/LICENSE +21 -0
  269. package/adapters/pi/extensions/ask-user-question/README.md +91 -0
  270. package/adapters/pi/extensions/ask-user-question/ask-user-question.ts +298 -0
  271. package/adapters/pi/extensions/ask-user-question/config.ts +78 -0
  272. package/adapters/pi/extensions/ask-user-question/events.ts +57 -0
  273. package/adapters/pi/extensions/ask-user-question/index.ts +61 -0
  274. package/adapters/pi/extensions/ask-user-question/locales/de.json +27 -0
  275. package/adapters/pi/extensions/ask-user-question/locales/en.json +27 -0
  276. package/adapters/pi/extensions/ask-user-question/locales/es.json +27 -0
  277. package/adapters/pi/extensions/ask-user-question/locales/fr.json +27 -0
  278. package/adapters/pi/extensions/ask-user-question/locales/pt-BR.json +27 -0
  279. package/adapters/pi/extensions/ask-user-question/locales/pt.json +27 -0
  280. package/adapters/pi/extensions/ask-user-question/locales/ru.json +27 -0
  281. package/adapters/pi/extensions/ask-user-question/locales/uk.json +27 -0
  282. package/adapters/pi/extensions/ask-user-question/locales/zh.json +29 -0
  283. package/adapters/pi/extensions/ask-user-question/reconcile.ts +49 -0
  284. package/adapters/pi/extensions/ask-user-question/rpc-fallback.ts +168 -0
  285. package/adapters/pi/extensions/ask-user-question/state/build-questionnaire.ts +302 -0
  286. package/adapters/pi/extensions/ask-user-question/state/i18n-bridge.ts +53 -0
  287. package/adapters/pi/extensions/ask-user-question/state/key-router.ts +277 -0
  288. package/adapters/pi/extensions/ask-user-question/state/questionnaire-session.ts +234 -0
  289. package/adapters/pi/extensions/ask-user-question/state/row-intent.ts +145 -0
  290. package/adapters/pi/extensions/ask-user-question/state/selectors/contract.ts +26 -0
  291. package/adapters/pi/extensions/ask-user-question/state/selectors/derivations.ts +42 -0
  292. package/adapters/pi/extensions/ask-user-question/state/selectors/focus.ts +19 -0
  293. package/adapters/pi/extensions/ask-user-question/state/selectors/projections.ts +101 -0
  294. package/adapters/pi/extensions/ask-user-question/state/state-reducer.ts +292 -0
  295. package/adapters/pi/extensions/ask-user-question/state/state.ts +55 -0
  296. package/adapters/pi/extensions/ask-user-question/tool/format-answer.ts +31 -0
  297. package/adapters/pi/extensions/ask-user-question/tool/response-envelope.ts +49 -0
  298. package/adapters/pi/extensions/ask-user-question/tool/types.ts +147 -0
  299. package/adapters/pi/extensions/ask-user-question/tool/validate-questionnaire.ts +58 -0
  300. package/adapters/pi/extensions/ask-user-question/vendor-config-shim.ts +65 -0
  301. package/adapters/pi/extensions/ask-user-question/view/component-binding.ts +47 -0
  302. package/adapters/pi/extensions/ask-user-question/view/components/inline-input.ts +98 -0
  303. package/adapters/pi/extensions/ask-user-question/view/components/multi-select-view.ts +193 -0
  304. package/adapters/pi/extensions/ask-user-question/view/components/option-list-view.ts +70 -0
  305. package/adapters/pi/extensions/ask-user-question/view/components/preview/markdown-content-cache.ts +79 -0
  306. package/adapters/pi/extensions/ask-user-question/view/components/preview/preview-block-renderer.ts +111 -0
  307. package/adapters/pi/extensions/ask-user-question/view/components/preview/preview-box-renderer.ts +88 -0
  308. package/adapters/pi/extensions/ask-user-question/view/components/preview/preview-layout-decider.ts +202 -0
  309. package/adapters/pi/extensions/ask-user-question/view/components/preview/preview-pane.ts +228 -0
  310. package/adapters/pi/extensions/ask-user-question/view/components/submit-picker.ts +67 -0
  311. package/adapters/pi/extensions/ask-user-question/view/components/tab-bar.ts +59 -0
  312. package/adapters/pi/extensions/ask-user-question/view/components/wrapping-select.ts +293 -0
  313. package/adapters/pi/extensions/ask-user-question/view/dialog-builder.ts +224 -0
  314. package/adapters/pi/extensions/ask-user-question/view/props-adapter.ts +125 -0
  315. package/adapters/pi/extensions/ask-user-question/view/stateful-view.ts +26 -0
  316. package/adapters/pi/extensions/ask-user-question/view/tab-components.ts +18 -0
  317. package/adapters/pi/extensions/ask-user-question/view/tab-content-strategy.ts +252 -0
  318. package/adapters/pi/package.json +26 -0
  319. package/adapters/pi/references/epic-manifest-schema.json +125 -0
  320. package/adapters/{claude/skills/forge-5-loop → pi}/references/forge-config-schema.json +4 -4
  321. package/adapters/{claude/skills/forge-1-prd → pi}/references/pipeline-state-schema.json +1 -1
  322. package/adapters/pi/references/portable-root.md +71 -0
  323. package/adapters/pi/references/process-overview.md +143 -0
  324. package/adapters/pi/references/ralph-loop-contract.md +221 -0
  325. package/adapters/pi/references/shared-conventions.md +345 -0
  326. package/adapters/pi/references/skill-frontmatter.schema.json +17 -0
  327. package/adapters/pi/references/stack-resolution.md +54 -0
  328. package/adapters/pi/references/stacks/_generic.md +111 -0
  329. package/adapters/pi/references/stacks/go.md +157 -0
  330. package/adapters/pi/references/stacks/python.md +184 -0
  331. package/adapters/pi/references/stacks/rust.md +170 -0
  332. package/adapters/pi/references/stacks/typescript.md +134 -0
  333. package/adapters/pi/references/stage-exit-protocol.md +268 -0
  334. package/adapters/pi/references/templates/specs-hygiene/AGENTS.md +32 -0
  335. package/adapters/pi/references/templates/specs-hygiene/CLAUDE.md +31 -0
  336. package/adapters/pi/references/vendor-construct-inventory.md +50 -0
  337. package/adapters/pi/scripts/epic-manifest.py +1737 -0
  338. package/adapters/pi/scripts/forge-bootstrap.py +1070 -0
  339. package/adapters/pi/scripts/forge-init.sh +58 -0
  340. package/adapters/pi/scripts/forge-root.sh +179 -0
  341. package/adapters/pi/scripts/forge-session.py +3036 -0
  342. package/adapters/pi/scripts/validate-traceability.py +155 -0
  343. package/adapters/pi/skills/forge/SKILL.md +249 -0
  344. package/adapters/{claude/skills/forge-4-backlog → pi/skills/forge}/references/pipeline-state-schema.json +1 -1
  345. package/adapters/pi/skills/forge/references/process-overview.md +143 -0
  346. package/adapters/pi/skills/forge/references/shared-conventions.md +345 -0
  347. package/adapters/pi/skills/forge/references/stage-exit-protocol.md +268 -0
  348. package/adapters/pi/skills/forge-0-epic/SKILL.md +308 -0
  349. package/adapters/pi/skills/forge-0-epic/references/edit-mode.md +266 -0
  350. package/adapters/pi/skills/forge-0-epic/references/epic-manifest-subcommands.md +75 -0
  351. package/adapters/{claude/skills/forge-2-tech → pi/skills/forge-0-epic}/references/pipeline-state-schema.json +1 -1
  352. package/adapters/pi/skills/forge-0-epic/references/portable-root.md +71 -0
  353. package/adapters/pi/skills/forge-0-epic/references/shared-conventions.md +345 -0
  354. package/adapters/pi/skills/forge-0-epic/references/stage-exit-protocol.md +268 -0
  355. package/adapters/pi/skills/forge-1-prd/SKILL.md +181 -0
  356. package/adapters/pi/skills/forge-1-prd/references/prd-template.md +106 -0
  357. package/adapters/pi/skills/forge-1-prd/references/shared-conventions.md +345 -0
  358. package/adapters/pi/skills/forge-1-prd/references/stage-exit-protocol.md +268 -0
  359. package/adapters/pi/skills/forge-2-tech/SKILL.md +243 -0
  360. package/adapters/pi/skills/forge-2-tech/references/shared-conventions.md +345 -0
  361. package/adapters/pi/skills/forge-2-tech/references/stack-discovery-checklist.md +95 -0
  362. package/adapters/pi/skills/forge-2-tech/references/stack-resolution.md +54 -0
  363. package/adapters/pi/skills/forge-2-tech/references/stacks/_generic.md +111 -0
  364. package/adapters/pi/skills/forge-2-tech/references/stacks/go.md +157 -0
  365. package/adapters/pi/skills/forge-2-tech/references/stacks/python.md +184 -0
  366. package/adapters/pi/skills/forge-2-tech/references/stacks/rust.md +170 -0
  367. package/adapters/pi/skills/forge-2-tech/references/stacks/typescript.md +134 -0
  368. package/adapters/pi/skills/forge-2-tech/references/stage-exit-protocol.md +268 -0
  369. package/adapters/pi/skills/forge-3-specs/SKILL.md +195 -0
  370. package/adapters/pi/skills/forge-3-specs/references/shared-conventions.md +345 -0
  371. package/adapters/pi/skills/forge-3-specs/references/spec-archetypes.md +106 -0
  372. package/adapters/pi/skills/forge-3-specs/references/spec-examples.md +71 -0
  373. package/adapters/pi/skills/forge-3-specs/references/stacks/_generic.md +111 -0
  374. package/adapters/pi/skills/forge-3-specs/references/stacks/go.md +157 -0
  375. package/adapters/pi/skills/forge-3-specs/references/stacks/python.md +184 -0
  376. package/adapters/pi/skills/forge-3-specs/references/stacks/rust.md +170 -0
  377. package/adapters/pi/skills/forge-3-specs/references/stacks/typescript.md +134 -0
  378. package/adapters/pi/skills/forge-3-specs/references/stage-exit-protocol.md +268 -0
  379. package/adapters/pi/skills/forge-4-backlog/SKILL.md +191 -0
  380. package/adapters/pi/skills/forge-4-backlog/references/shared-conventions.md +345 -0
  381. package/adapters/pi/skills/forge-4-backlog/references/stage-exit-protocol.md +268 -0
  382. package/adapters/pi/skills/forge-5-loop/SKILL.md +314 -0
  383. package/adapters/pi/skills/forge-5-loop/references/agent-selection.md +116 -0
  384. package/adapters/pi/skills/forge-5-loop/references/ralph-loop-contract.md +221 -0
  385. package/adapters/pi/skills/forge-5-loop/references/result-reporting.md +85 -0
  386. package/adapters/pi/skills/forge-5-loop/references/runner-contract.md +248 -0
  387. package/adapters/pi/skills/forge-5-loop/references/shared-conventions.md +345 -0
  388. package/adapters/pi/skills/forge-5-loop/references/stage-exit-protocol.md +268 -0
  389. package/adapters/pi/skills/forge-6-docs/SKILL.md +208 -0
  390. package/adapters/pi/skills/forge-6-docs/references/doc-conventions.md +126 -0
  391. package/adapters/pi/skills/forge-6-docs/references/shared-conventions.md +345 -0
  392. package/adapters/pi/skills/forge-bootstrap/SKILL.md +250 -0
  393. package/adapters/pi/skills/forge-bootstrap/references/templates/ci/github-actions.yml +12 -0
  394. package/adapters/pi/skills/forge-bootstrap/references/templates/generic/run.sh +3 -0
  395. package/adapters/pi/skills/forge-bootstrap/references/templates/generic/test.sh +13 -0
  396. package/adapters/pi/skills/forge-bootstrap/references/templates/go/go.mod +3 -0
  397. package/adapters/pi/skills/forge-bootstrap/references/templates/go/main.go +12 -0
  398. package/adapters/pi/skills/forge-bootstrap/references/templates/go/main_test.go +11 -0
  399. package/adapters/pi/skills/forge-bootstrap/references/templates/hygiene/AGENTS.md +35 -0
  400. package/adapters/pi/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +36 -0
  401. package/adapters/pi/skills/forge-bootstrap/references/templates/hygiene/README.md +11 -0
  402. package/adapters/pi/skills/forge-bootstrap/references/templates/licenses/Apache-2.0/LICENSE +198 -0
  403. package/adapters/pi/skills/forge-bootstrap/references/templates/licenses/MIT/LICENSE +21 -0
  404. package/adapters/pi/skills/forge-bootstrap/references/templates/python/pyproject.toml +24 -0
  405. package/adapters/pi/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/__init__.py +5 -0
  406. package/adapters/pi/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/main.py +13 -0
  407. package/adapters/pi/skills/forge-bootstrap/references/templates/python/tests/test_smoke.py +8 -0
  408. package/adapters/pi/skills/forge-bootstrap/references/templates/rust/Cargo.toml +15 -0
  409. package/adapters/pi/skills/forge-bootstrap/references/templates/rust/src/lib.rs +7 -0
  410. package/adapters/pi/skills/forge-bootstrap/references/templates/rust/src/main.rs +5 -0
  411. package/adapters/pi/skills/forge-bootstrap/references/templates/rust/tests/smoke.rs +6 -0
  412. package/adapters/pi/skills/forge-bootstrap/references/templates/typescript/package.json +15 -0
  413. package/adapters/pi/skills/forge-bootstrap/references/templates/typescript/src/index.ts +4 -0
  414. package/adapters/pi/skills/forge-bootstrap/references/templates/typescript/test/smoke.test.ts +6 -0
  415. package/adapters/pi/skills/forge-bootstrap/references/templates/typescript/tsconfig.json +14 -0
  416. package/adapters/pi/skills/forge-fix/SKILL.md +98 -0
  417. package/adapters/pi/skills/forge-fix/references/shared-conventions.md +345 -0
  418. package/adapters/pi/skills/forge-fix/references/stage-exit-protocol.md +268 -0
  419. package/adapters/pi/skills/forge-guide/SKILL.md +192 -0
  420. package/adapters/{codex/skills/forge-4-backlog → pi/skills/forge-guide}/references/forge-config-schema.json +4 -4
  421. package/adapters/pi/skills/forge-guide/references/process-overview.md +143 -0
  422. package/adapters/pi/skills/forge-guide/references/ralph-loop-contract.md +221 -0
  423. package/adapters/pi/skills/forge-guide/references/shared-conventions.md +345 -0
  424. package/adapters/pi/skills/forge-guide/references/stack-resolution.md +54 -0
  425. package/adapters/pi/skills/forge-guide/references/stacks/_generic.md +111 -0
  426. package/adapters/pi/skills/forge-guide/references/stacks/go.md +157 -0
  427. package/adapters/pi/skills/forge-guide/references/stacks/python.md +184 -0
  428. package/adapters/pi/skills/forge-guide/references/stacks/rust.md +170 -0
  429. package/adapters/pi/skills/forge-guide/references/stacks/typescript.md +134 -0
  430. package/adapters/pi/skills/forge-init/SKILL.md +72 -0
  431. package/adapters/pi/skills/forge-verify/SKILL.md +283 -0
  432. package/adapters/pi/skills/forge-verify/references/findings-template.md +157 -0
  433. package/adapters/{claude/skills/forge-3-specs → pi/skills/forge-verify}/references/pipeline-state-schema.json +1 -1
  434. package/adapters/pi/skills/forge-verify/references/shared-conventions.md +345 -0
  435. package/adapters/pi/skills/forge-verify/references/verification-checklists/backlog.md +97 -0
  436. package/adapters/pi/skills/forge-verify/references/verification-checklists/epic.md +79 -0
  437. package/adapters/pi/skills/forge-verify/references/verification-checklists/impl.md +48 -0
  438. package/adapters/pi/skills/forge-verify/references/verification-checklists/prd.md +31 -0
  439. package/adapters/pi/skills/forge-verify/references/verification-checklists/specs.md +64 -0
  440. package/adapters/pi/skills/forge-verify/references/verification-checklists/tech.md +35 -0
  441. package/dist/agent-targets.d.ts +1 -1
  442. package/dist/agent-targets.js +23 -3
  443. package/dist/detect.d.ts +1 -1
  444. package/dist/detect.js +2 -1
  445. package/dist/manifest.d.ts +1 -1
  446. package/dist/manifest.js +2 -2
  447. package/dist/placements.js +5 -1
  448. package/dist/rauf.d.ts +4 -4
  449. package/dist/rauf.js +3 -3
  450. package/dist/types.d.ts +31 -6
  451. package/dist/types.js +6 -3
  452. package/package.json +14 -3
  453. package/adapters/claude/skills/forge-4-backlog/references/forge-config-schema.json +0 -236
  454. package/adapters/claude/skills/forge-6-docs/references/pipeline-state-schema.json +0 -191
  455. package/adapters/claude/skills/forge-verify/references/verification-checklists.md +0 -477
  456. package/adapters/codex/skills/forge-1-prd/references/pipeline-state-schema.json +0 -191
  457. package/adapters/codex/skills/forge-2-tech/references/pipeline-state-schema.json +0 -191
  458. package/adapters/codex/skills/forge-3-specs/references/pipeline-state-schema.json +0 -191
  459. package/adapters/codex/skills/forge-4-backlog/references/pipeline-state-schema.json +0 -191
  460. package/adapters/codex/skills/forge-5-loop/references/forge-config-schema.json +0 -236
  461. package/adapters/codex/skills/forge-6-docs/references/pipeline-state-schema.json +0 -191
  462. package/adapters/codex/skills/forge-verify/references/verification-checklists.md +0 -477
  463. package/adapters/copilot/skills/forge-1-prd/references/pipeline-state-schema.json +0 -191
  464. package/adapters/copilot/skills/forge-2-tech/references/pipeline-state-schema.json +0 -191
  465. package/adapters/copilot/skills/forge-3-specs/references/pipeline-state-schema.json +0 -191
  466. package/adapters/copilot/skills/forge-4-backlog/references/forge-config-schema.json +0 -236
  467. package/adapters/copilot/skills/forge-4-backlog/references/pipeline-state-schema.json +0 -191
  468. package/adapters/copilot/skills/forge-5-loop/references/forge-config-schema.json +0 -236
  469. package/adapters/copilot/skills/forge-6-docs/references/pipeline-state-schema.json +0 -191
  470. package/adapters/copilot/skills/forge-verify/references/verification-checklists.md +0 -477
  471. package/adapters/cursor/skills/forge-1-prd/references/pipeline-state-schema.json +0 -191
  472. package/adapters/cursor/skills/forge-2-tech/references/pipeline-state-schema.json +0 -191
  473. package/adapters/cursor/skills/forge-3-specs/references/pipeline-state-schema.json +0 -191
  474. package/adapters/cursor/skills/forge-4-backlog/references/forge-config-schema.json +0 -236
  475. package/adapters/cursor/skills/forge-4-backlog/references/pipeline-state-schema.json +0 -191
  476. package/adapters/cursor/skills/forge-5-loop/references/forge-config-schema.json +0 -236
  477. package/adapters/cursor/skills/forge-6-docs/references/pipeline-state-schema.json +0 -191
  478. package/adapters/cursor/skills/forge-verify/references/verification-checklists.md +0 -477
  479. package/adapters/gemini/skills/forge-1-prd/references/pipeline-state-schema.json +0 -191
  480. package/adapters/gemini/skills/forge-2-tech/references/pipeline-state-schema.json +0 -191
  481. package/adapters/gemini/skills/forge-3-specs/references/pipeline-state-schema.json +0 -191
  482. package/adapters/gemini/skills/forge-4-backlog/references/forge-config-schema.json +0 -236
  483. package/adapters/gemini/skills/forge-4-backlog/references/pipeline-state-schema.json +0 -191
  484. package/adapters/gemini/skills/forge-5-loop/references/forge-config-schema.json +0 -236
  485. package/adapters/gemini/skills/forge-6-docs/references/pipeline-state-schema.json +0 -191
  486. package/adapters/gemini/skills/forge-verify/references/verification-checklists.md +0 -477
@@ -0,0 +1,1070 @@
1
+ #!/usr/bin/env python3
2
+ """Scaffold a brand-new empty repository to a pipeline-ready, green baseline.
3
+
4
+ The deterministic core of forge-bootstrap: the greenfield gate + recovery
5
+ detection, git init, template-driven scaffold emission, forge.config.json write,
6
+ the transient .forge-bootstrap.json resume sentinel, toolchain detection +
7
+ lint/test verification, and the exact-list baseline commit. The interview and all
8
+ human-facing output live in skills/forge-bootstrap/SKILL.md; this helper only
9
+ emits structured JSON + exit codes.
10
+
11
+ Usage:
12
+ python3 forge-bootstrap.py check <target-dir> [--specs-dir DIR] [--json]
13
+ python3 forge-bootstrap.py scaffold <target-dir> --answers JSON [--json]
14
+ python3 forge-bootstrap.py verify <target-dir> --answers JSON [--json]
15
+ python3 forge-bootstrap.py commit <target-dir> --answers JSON \
16
+ [--stage-only] [--json]
17
+ python3 forge-bootstrap.py status <target-dir> [--json]
18
+
19
+ Exit codes:
20
+ 0 = success: eligible / green / committed-or-staged
21
+ 1 = findings: gate refusal (eligible:false) or verify not-green
22
+ 2 = usage or IO error -- including verify toolchain-missing
23
+ """
24
+
25
+ import argparse
26
+ import json
27
+ import os
28
+ import re
29
+ import shlex
30
+ import shutil
31
+ import subprocess
32
+ import sys
33
+ import tempfile
34
+ from datetime import datetime, timezone
35
+ from pathlib import Path
36
+ from typing import Final, Literal, TypedDict
37
+
38
+
39
+ # --------------------------------------------------------------------------- #
40
+ # Constants (00-core-definitions.md §2, §3, §6)
41
+ # --------------------------------------------------------------------------- #
42
+
43
+ #: The five built-in stack profiles, parity with references/stacks/*.md (REQ-STACK-01).
44
+ Stack = Literal["typescript", "python", "go", "rust", "generic"]
45
+
46
+ #: Stacks that have a meaningful package-manager choice (drives REQ-INPUT-04).
47
+ #: A stack absent from this map skips the package-manager question entirely.
48
+ PACKAGE_MANAGERS: Final[dict[str, list[str]]] = {
49
+ "typescript": ["npm", "pnpm", "yarn"],
50
+ "python": ["uv", "poetry", "pip"],
51
+ # go, rust, generic: no package-manager question (tech-spec §3.9 row 4).
52
+ }
53
+
54
+ #: Exact directory entries always permitted at the target repo root (00 §3).
55
+ ALLOWED_META_DIRS: Final[frozenset[str]] = frozenset({".git"})
56
+
57
+ #: The transient resume sentinel (§8) is allow-listed so a re-run over a partial
58
+ #: scaffold is routed to recovery, not refused (REQ-LIFE-02, tech-spec §3.4).
59
+ SENTINEL_FILENAME: Final = ".forge-bootstrap.json"
60
+
61
+ #: Case-insensitive filename patterns for allowed repo-meta files (REQ-GATE-01/04, OQ-02).
62
+ #: A fresh remote's auto-generated README + LICENSE must pass (REQ-GATE-04).
63
+ ALLOWED_META_FILE_RE: Final = re.compile(
64
+ r"""^(
65
+ README(\.md|\.txt|\.rst)? # README, README.md, README.txt, README.rst
66
+ | LICENSE(\.md|\.txt)? # LICENSE, LICENSE.md, LICENSE.txt
67
+ | \.gitignore
68
+ | \.gitattributes
69
+ )$""",
70
+ re.IGNORECASE | re.VERBOSE,
71
+ )
72
+
73
+ #: Resolved verification + toolchain-probe commands per stack (00 §6). "{pm}" is
74
+ #: substituted with the member's packageManager; "{member}" with its path.
75
+ #: STACK_COMMANDS[stack] -> (typeCheckCommand-template, testCommand-template,
76
+ #: tuple-of-toolchain-probe-binaries). The single source of truth shared by
77
+ #: write_config (§4.3) and verify (§5), so config and the run agree exactly.
78
+ STACK_COMMANDS: Final[dict[str, tuple[str, str, tuple[str, ...]]]] = {
79
+ "typescript": ("npx tsc --noEmit", "{pm} test", ("node", "{pm}")),
80
+ "python": ("mypy .", "pytest", ("python3", "{pm}")),
81
+ "go": ("go vet ./...", "go test ./...", ("go",)),
82
+ "rust": ("cargo clippy", "cargo test", ("cargo",)),
83
+ "generic": ("sh -n run.sh test.sh", "./test.sh", ("sh",)),
84
+ }
85
+
86
+
87
+ # --------------------------------------------------------------------------- #
88
+ # Type Definitions (00-core-definitions.md §4, §5, §8)
89
+ # --------------------------------------------------------------------------- #
90
+
91
+
92
+ class Member(TypedDict):
93
+ """One package to scaffold. A single-package project has exactly one implicit member.
94
+
95
+ Attributes:
96
+ name: Package name. For a single package this equals the project name; for a
97
+ monorepo member it is the user-supplied member name (REQ-MONO-01).
98
+ path: Repo-relative directory for this member ("." for a single package;
99
+ e.g. "packages/api" for a monorepo member). Becomes workspaces[].path.
100
+ stack: The member's stack profile (REQ-MONO-02 allows mixed-language members).
101
+ packageManager: The chosen package manager when the stack has a choice
102
+ (PACKAGE_MANAGERS), else None (go/rust/generic).
103
+ """
104
+
105
+ name: str
106
+ path: str
107
+ stack: Stack
108
+ packageManager: str | None
109
+
110
+
111
+ class Answers(TypedDict):
112
+ """The resolved interview payload (skill → helper), mirrored into the sentinel.
113
+
114
+ Attributes:
115
+ projectName: Project name (REQ-INPUT-01; default inferred from the target dir).
116
+ purpose: One-line project purpose, seeds README + config metadata (REQ-INPUT-02).
117
+ layout: "single" or "monorepo" (REQ-INPUT-06; default "single").
118
+ license: SPDX-ish identifier (e.g. "MIT", "Apache-2.0") or "none" (REQ-INPUT-05).
119
+ members: One Member for a single package, ≥1 for a monorepo (REQ-MONO-01).
120
+ modeB: True iff the user opted into pipeline hand-off (REQ-MODEB-01; default False).
121
+ modeBTarget: "feature" or "epic" when modeB is True, else None (REQ-INPUT-07).
122
+ ci: True iff a CI workflow should be emitted (REQ-SCAF-07, REQ-MONO-04).
123
+ commitStyle: "commit" (single baseline commit) or "stage-only" (REQ-LIFE-05).
124
+ author: Copyright holder for the generated LICENSE ({{AUTHOR}} token, §6.2);
125
+ seeded from git user.name when available, else the project name (REQ-SCAF-06).
126
+ host: The running agent host ("claude", "codex", or "other"/None). Drives the
127
+ host-conditional agent-instruction file: AGENTS.md is always emitted; CLAUDE.md
128
+ is additionally emitted when host == "claude" (REQ-SCAF-06).
129
+ """
130
+
131
+ projectName: str
132
+ purpose: str
133
+ layout: Literal["single", "monorepo"]
134
+ license: str
135
+ members: list[Member]
136
+ modeB: bool
137
+ modeBTarget: Literal["feature", "epic"] | None
138
+ ci: bool
139
+ commitStyle: Literal["commit", "stage-only"]
140
+ author: str
141
+ host: Literal["claude", "codex", "other"] | None
142
+
143
+
144
+ class Sentinel(TypedDict):
145
+ """The transient `.forge-bootstrap.json` resume marker (target repo root).
146
+
147
+ Attributes:
148
+ version: Schema guard (const 1).
149
+ status: "in-progress" while scaffolding; "complete" only in the instant
150
+ between a successful verify/commit decision and sentinel removal.
151
+ startedAt: ISO-8601 timestamp set once when the sentinel is first written.
152
+ answers: The full interview Answers, mirrored so a resume reconstructs
153
+ prior answers with no re-interview (REQ-LIFE-02, OQ-03).
154
+ artifactsWritten: Repo-relative paths the helper has written so far. `scaffold`
155
+ is idempotent over this list (skips files already recorded), enabling resume.
156
+ """
157
+
158
+ version: Literal[1]
159
+ status: Literal["in-progress", "complete"]
160
+ startedAt: str
161
+ answers: Answers
162
+ artifactsWritten: list[str]
163
+
164
+
165
+ class CheckResult(TypedDict):
166
+ """Output of `check` — greenfield gate + recovery detection (REQ-GATE-01/02, REQ-LIFE-02).
167
+
168
+ Attributes:
169
+ eligible: True iff the target is a permitted greenfield OR a resume of
170
+ this tool's own partial scaffold. False ⇒ greenfield refusal.
171
+ disqualifying: Repo-relative paths that fail the allow-list. Empty when
172
+ eligible. Drives the REQ-GATE-02 refusal message.
173
+ hasGit: True iff the target already contains a `.git/` repository (REQ-GATE-03
174
+ decides whether `scaffold` runs `git init`).
175
+ resumeMarker: The parsed sentinel when one is present, else None. When
176
+ non-None the skill routes to resume / restart / cancel (REQ-LIFE-02) rather
177
+ than treating `eligible` as a fresh-start signal.
178
+ """
179
+
180
+ eligible: bool
181
+ disqualifying: list[str]
182
+ hasGit: bool
183
+ resumeMarker: "Sentinel | None"
184
+
185
+
186
+ class CommandOutcome(TypedDict):
187
+ """Result of one resolved lint or test command (REQ-SCAF-05, REQ-STACK-02).
188
+
189
+ Attributes:
190
+ command: The exact command string that was run (from the §6 table).
191
+ ok: True iff the command exited 0.
192
+ member: The member's repo-relative path the command ran for ("." for a
193
+ single package; e.g. "packages/api" for a monorepo member).
194
+ """
195
+
196
+ command: str
197
+ ok: bool
198
+ member: str
199
+
200
+
201
+ class VerifyResult(TypedDict):
202
+ """Output of `verify` — toolchain detection + lint/test (REQ-SCAF-05, REQ-LIFE-03/04).
203
+
204
+ Attributes:
205
+ toolchainPresent: True iff every required tool for the resolved stack(s) was
206
+ found via `command -v`. False ⇒ missing-toolchain outcome (exit 2).
207
+ lint: One CommandOutcome per resolved lint command (per member for a monorepo).
208
+ test: One CommandOutcome per resolved test command (per member for a monorepo).
209
+ green: True iff toolchainPresent AND every lint/test outcome is ok. The single
210
+ predicate Mode B gates on (REQ-MODEB-04).
211
+ """
212
+
213
+ toolchainPresent: bool
214
+ lint: list[CommandOutcome]
215
+ test: list[CommandOutcome]
216
+ green: bool
217
+
218
+
219
+ class CommitResult(TypedDict):
220
+ """Output of `commit` — staged-or-committed baseline (REQ-LIFE-05/06, REQ-SCAF-08).
221
+
222
+ Attributes:
223
+ committed: True iff a baseline commit was made; False when `--stage-only` left
224
+ the scaffold staged with no commit (REQ-LIFE-05).
225
+ commitHash: The new commit's hash when committed, else None.
226
+ staged: The exact list of repo-relative paths staged (the tracked artifact set;
227
+ never via `git add -A` — REQ-SEC-02).
228
+ sentinelRemoved: True once the sentinel was deleted before staging so it never
229
+ enters history (REQ-SCAF-08, OQ-T3).
230
+ """
231
+
232
+ committed: bool
233
+ commitHash: str | None
234
+ staged: list[str]
235
+ sentinelRemoved: bool
236
+
237
+
238
+ # --------------------------------------------------------------------------- #
239
+ # Internal Exceptions (02 §2)
240
+ # --------------------------------------------------------------------------- #
241
+
242
+
243
+ class UsageError(Exception):
244
+ """A usage or I/O failure that must exit 2.
245
+
246
+ Raised for malformed CLI arguments, unreadable/unwritable paths, a malformed
247
+ --answers payload, git/subprocess invocation failures, and the distinct
248
+ verify toolchain-missing outcome (00 §9). Maps to exit code 2.
249
+
250
+ Attributes:
251
+ message: Human-readable description printed to stderr.
252
+ """
253
+
254
+ def __init__(self, message: str) -> None:
255
+ self.message = message
256
+ super().__init__(message)
257
+
258
+
259
+ class FindingsError(Exception):
260
+ """A non-fatal outcome that must exit 1.
261
+
262
+ Raised for an actionable, non-error finding: a greenfield refusal
263
+ (CheckResult.eligible == False) or a not-green verify result. Maps to exit
264
+ code 1. Carries the structured result so the dispatch layer can emit it as
265
+ JSON on stdout.
266
+
267
+ Attributes:
268
+ result: The CheckResult / VerifyResult to surface verbatim.
269
+ """
270
+
271
+ def __init__(self, result: dict) -> None:
272
+ self.result = result
273
+ super().__init__("findings")
274
+
275
+
276
+ # --------------------------------------------------------------------------- #
277
+ # Safety & I/O Layer (02 §8.1)
278
+ # --------------------------------------------------------------------------- #
279
+
280
+
281
+ def _json_text(obj: object) -> str:
282
+ """Serialize ``obj`` to canonical JSON text with a trailing newline."""
283
+ return json.dumps(obj, indent=2, ensure_ascii=False) + "\n"
284
+
285
+
286
+ def contained_path(base: Path, *parts: str) -> Path:
287
+ """Join ``parts`` onto ``base`` and assert the result stays within ``base``.
288
+
289
+ Ports epic-manifest.py's ``contained_path`` guard: canonicalizes (symlink-
290
+ resolves) both ends and verifies containment so no scaffold write or verify
291
+ cwd can escape the target repo — defense in depth behind ``_validate_answers``
292
+ should a crafted member path slip through. Containment violations surface only
293
+ as exit-2 usage errors.
294
+
295
+ Args:
296
+ base: The containing directory (the target repo root), already known to exist.
297
+ *parts: Repo-relative segments to append (member path, artifact rel path).
298
+
299
+ Returns:
300
+ The resolved, contained absolute path.
301
+
302
+ Raises:
303
+ UsageError: If the resolved path escapes ``base`` (exit 2).
304
+ """
305
+ base_real = base.resolve()
306
+ resolved = (base_real / Path(*parts)).resolve()
307
+ if resolved != base_real and base_real not in resolved.parents:
308
+ raise UsageError(f"path escapes target dir: {os.path.join(*parts)}")
309
+ return resolved
310
+
311
+
312
+ def _atomic_write_text(path: Path, text: str) -> None:
313
+ """Write ``text`` to ``path`` atomically via a same-dir temp file + os.replace.
314
+
315
+ Mirrors epic-manifest.py's atomic_write, generalized to text: writes to a
316
+ temporary file in the destination's directory, flushes + fsyncs it, then
317
+ ``os.replace`` swaps it into place so an interrupted write never leaves a torn
318
+ file.
319
+
320
+ Args:
321
+ path: The destination path (parent must already exist).
322
+ text: The full file content to write.
323
+
324
+ Raises:
325
+ UsageError: If the temp file cannot be created/written or the replace
326
+ fails (exit 2). On failure the temp file is removed.
327
+ """
328
+ parent = path.parent
329
+ fd, tmp_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=parent)
330
+ tmp_path = Path(tmp_name)
331
+ try:
332
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
333
+ handle.write(text)
334
+ handle.flush()
335
+ os.fsync(handle.fileno())
336
+ os.replace(tmp_path, path)
337
+ except OSError as exc:
338
+ tmp_path.unlink(missing_ok=True)
339
+ raise UsageError(f"atomic write to {path} failed: {exc}")
340
+
341
+
342
+ def read_sentinel(target: Path) -> "Sentinel | None":
343
+ """Read and parse the transient resume sentinel, or None when absent (00 §8).
344
+
345
+ Args:
346
+ target: The project root being inspected.
347
+
348
+ Returns:
349
+ The parsed Sentinel when present, else None.
350
+
351
+ Raises:
352
+ UsageError: If the file exists but is unreadable or not valid JSON (exit 2).
353
+ """
354
+ path = target / SENTINEL_FILENAME
355
+ if not path.is_file():
356
+ return None
357
+ try:
358
+ return json.loads(path.read_text(encoding="utf-8"))
359
+ except (OSError, json.JSONDecodeError) as exc:
360
+ raise UsageError(f"corrupt sentinel {path}: {exc}")
361
+
362
+
363
+ def write_sentinel(target: Path, sentinel: Sentinel) -> None:
364
+ """Atomically write the resume sentinel to the target root (00 §8).
365
+
366
+ Uses a same-dir temp file + os.replace so an interrupted write never leaves a
367
+ torn marker (mirroring epic-manifest.py's atomic_write).
368
+
369
+ Args:
370
+ target: The project root.
371
+ sentinel: The sentinel dict to persist.
372
+
373
+ Raises:
374
+ UsageError: If the write fails (exit 2).
375
+ """
376
+ _atomic_write_text(target / SENTINEL_FILENAME, _json_text(sentinel))
377
+
378
+
379
+ def run(
380
+ cmd: list[str], cwd: Path, check: bool = True
381
+ ) -> subprocess.CompletedProcess:
382
+ """Run a subprocess (git / toolchain probe / lint / test) and capture output.
383
+
384
+ A thin wrapper that captures stdout/stderr as text. With ``check=True`` a
385
+ non-zero exit raises UsageError (exit 2) — used for git operations that must
386
+ succeed. With ``check=False`` the CompletedProcess is returned for the caller
387
+ to inspect ``returncode`` — used for ``command -v`` probes and lint/test, whose
388
+ non-zero exits are data (toolchain-missing / not-green), not IO errors.
389
+
390
+ Args:
391
+ cmd: The argv list (never shell-joined except the explicit ``sh -c`` forms).
392
+ cwd: The working directory (the target root or a member dir).
393
+ check: Raise on non-zero when True; return the process when False.
394
+
395
+ Returns:
396
+ The CompletedProcess.
397
+
398
+ Raises:
399
+ UsageError: ``check`` is True and the command exited non-zero, or the
400
+ command could not be launched (exit 2).
401
+ """
402
+ try:
403
+ proc = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
404
+ except OSError as exc:
405
+ raise UsageError(f"failed to run {cmd[0]!r}: {exc}")
406
+ if check and proc.returncode != 0:
407
+ raise UsageError(f"command {' '.join(cmd)!r} failed: {proc.stderr.strip()}")
408
+ return proc
409
+
410
+
411
+ # --------------------------------------------------------------------------- #
412
+ # Subcommands: check / scaffold / verify / commit / status (02 §8.2)
413
+ # --------------------------------------------------------------------------- #
414
+
415
+
416
+ def check(target: Path, specs_dir: Path) -> CheckResult:
417
+ """Run the greenfield gate + recovery detection over a target repo (00 §3, §4).
418
+
419
+ Reads only — lists the target's immediate entries and parses any sentinel; it
420
+ never creates, modifies, or deletes a file (REQ-SEC-01). An entry is permitted
421
+ iff it is ``.git``, the configured specs dir, the SENTINEL_FILENAME, or a
422
+ regular file matching ALLOWED_META_FILE_RE (00 §3). Any other entry — a source
423
+ file, a package manifest, a build/tooling config — is disqualifying
424
+ (REQ-GATE-01) and its repo-relative path is recorded (REQ-GATE-02). A fresh
425
+ remote's auto-generated README + LICENSE pass (REQ-GATE-04). When the sentinel
426
+ is present the target is treated as eligible regardless of disqualifying
427
+ entries, so a re-run over bootstrap's own partial scaffold routes to recovery
428
+ rather than refusal (REQ-LIFE-02).
429
+
430
+ Args:
431
+ target: The project root being bootstrapped (the cwd, not the plugin root).
432
+ specs_dir: The configured specs directory; its basename is the one extra
433
+ entry permitted at the target root.
434
+
435
+ Returns:
436
+ A CheckResult (00 §4) with eligible / disqualifying / hasGit / resumeMarker.
437
+
438
+ Raises:
439
+ UsageError: If ``target`` exists but is not a directory, or its entries
440
+ cannot be listed (exit 2).
441
+ """
442
+ if target.exists() and not target.is_dir():
443
+ raise UsageError(f"target is not a directory: {target}")
444
+ marker = read_sentinel(target)
445
+ has_git = (target / ".git").is_dir()
446
+ allowed_dir_names = {*ALLOWED_META_DIRS, specs_dir.name}
447
+ disqualifying: list[str] = []
448
+ try:
449
+ entries = sorted(target.iterdir()) if target.is_dir() else []
450
+ except OSError as exc:
451
+ raise UsageError(f"cannot list target {target}: {exc}")
452
+ for entry in entries:
453
+ name = entry.name
454
+ if name in allowed_dir_names and entry.is_dir():
455
+ continue
456
+ if name == SENTINEL_FILENAME:
457
+ continue
458
+ if entry.is_file() and ALLOWED_META_FILE_RE.match(name):
459
+ continue
460
+ disqualifying.append(name)
461
+ eligible = (not disqualifying) or marker is not None
462
+ return {
463
+ "eligible": eligible,
464
+ "disqualifying": disqualifying,
465
+ "hasGit": has_git,
466
+ "resumeMarker": marker,
467
+ }
468
+
469
+
470
+ #: Repo-relative location of the bundled scaffold templates (00 §1.1). The helper
471
+ #: lives at scripts/forge-bootstrap.py, so the repo root is two levels up.
472
+ TEMPLATE_ROOT: Final = (
473
+ Path(__file__).resolve().parent.parent
474
+ / "skills" / "forge-bootstrap" / "references" / "templates"
475
+ )
476
+
477
+
478
+ def _sanitize_pkg(name: str) -> str:
479
+ """Map a member name to a language-safe package identifier ({{PKG}} token).
480
+
481
+ Lowercases, replaces any run of non-alphanumeric characters with a single
482
+ underscore, and strips leading/trailing underscores. Kept identical across
483
+ stacks for determinism (03 §1).
484
+ """
485
+ pkg = re.sub(r"[^0-9a-zA-Z]+", "_", name).strip("_").lower()
486
+ return pkg or "pkg"
487
+
488
+
489
+ def _write_artifact(
490
+ target: Path,
491
+ rel_path: str,
492
+ content: str,
493
+ sentinel: Sentinel,
494
+ executable: bool = False,
495
+ ) -> None:
496
+ """Write one scaffold artifact, idempotently and never overwriting (02 §4.1).
497
+
498
+ Skips the write when ``rel_path`` is already recorded in artifactsWritten[]
499
+ (resume idempotency, REQ-LIFE-02) OR when the destination already exists and was
500
+ not written by this run — a pre-existing allowed-meta file kept verbatim
501
+ (REQ-SCAF-09, REQ-GATE-05, REQ-SEC-01). A kept file is NOT recorded. Otherwise
502
+ it creates parent dirs, writes atomically, appends the path, and persists the
503
+ sentinel so an interrupt leaves a consistent resume list.
504
+ """
505
+ if rel_path in sentinel["artifactsWritten"]:
506
+ return
507
+ contained_path(target, rel_path) # never write outside the target repo (exit 2)
508
+ dest = target / rel_path
509
+ if dest.exists():
510
+ return
511
+ dest.parent.mkdir(parents=True, exist_ok=True)
512
+ _atomic_write_text(dest, content)
513
+ if executable:
514
+ # Shell-script templates carry a shebang and are resolved as ./run.sh /
515
+ # ./test.sh — they must be executable for verify to pass with no manual
516
+ # chmod (REQ-STACK-03). Apply 0755 (owner+group+other execute).
517
+ dest.chmod(0o755)
518
+ sentinel["artifactsWritten"].append(rel_path)
519
+ write_sentinel(target, sentinel)
520
+
521
+
522
+ def compose_member(
523
+ member: Member, answers: Answers, target: Path, sentinel: Sentinel
524
+ ) -> None:
525
+ """Compose one member's scaffold from its stack template dir (02 §4.2, 00 §6.2)."""
526
+ template_root = TEMPLATE_ROOT / member["stack"]
527
+ if not template_root.is_dir():
528
+ raise UsageError(
529
+ f"template dir not found for stack {member['stack']!r}: {template_root}"
530
+ )
531
+ pkg = _sanitize_pkg(member["name"])
532
+ tokens = {
533
+ "{{PROJECT_NAME}}": answers["projectName"],
534
+ "{{PKG}}": pkg,
535
+ "{{PM}}": member["packageManager"] or "",
536
+ "{{PURPOSE}}": answers["purpose"],
537
+ }
538
+ member_base = "" if member["path"] == "." else member["path"]
539
+ for src in sorted(p for p in template_root.rglob("*") if p.is_file()):
540
+ rel = src.relative_to(template_root).as_posix()
541
+ for tok, val in tokens.items():
542
+ rel = rel.replace(tok, val)
543
+ rel_path = rel if not member_base else f"{member_base}/{rel}"
544
+ text = src.read_text(encoding="utf-8")
545
+ for tok, val in tokens.items():
546
+ text = text.replace(tok, val)
547
+ # Preserve/apply +x for shell-script templates (those carrying a shebang
548
+ # or named *.sh) so the scaffolded baseline passes verify (REQ-STACK-03).
549
+ executable = rel.endswith(".sh") or text.startswith("#!")
550
+ _write_artifact(target, rel_path, text, sentinel, executable=executable)
551
+
552
+
553
+ def _resolve_commands(member: Member) -> tuple[str, str]:
554
+ """Resolve a member's (typeCheckCommand, testCommand) from STACK_COMMANDS (00 §6)."""
555
+ lint_t, test_t, _ = STACK_COMMANDS[member["stack"]]
556
+ pm = member["packageManager"] or ""
557
+ return lint_t.replace("{pm}", pm), test_t.replace("{pm}", pm)
558
+
559
+
560
+ def write_config(answers: Answers, target: Path, sentinel: Sentinel) -> None:
561
+ """Write forge.config.json == forge-init's field set + loopRunner (02 §4.3, 00 §7)."""
562
+ config: dict = {
563
+ "specsDir": "./specs",
564
+ "docsDir": "./docs/architecture",
565
+ "backlogDir": None,
566
+ "gitCommitAfterStage": True,
567
+ "commitPrefix": "forge",
568
+ "stack": None,
569
+ "typeCheckCommand": None,
570
+ "testCommand": None,
571
+ "smokeCommand": None,
572
+ "loopIterationMultiplier": 1.5,
573
+ # Navigator keys — kept in lockstep with forge-init.sh's emitted field set
574
+ # (00 §7, REQ-CFG-02) so a bootstrapped config == forge-init's + loopRunner.
575
+ "autoInvokeNextStage": True,
576
+ "contextWindowTokens": None,
577
+ "contextWarnThreshold": 0.7,
578
+ "autoVerify": False,
579
+ "autoVerifyStages": {},
580
+ "autoFix": False,
581
+ "loopRunner": {"name": "rauf", "bin": "rauf"},
582
+ }
583
+ if answers["layout"] == "single":
584
+ member = answers["members"][0]
585
+ lint, test = _resolve_commands(member)
586
+ config["stack"] = member["stack"]
587
+ config["typeCheckCommand"] = lint
588
+ config["testCommand"] = test
589
+ else:
590
+ workspaces: list[dict] = []
591
+ for member in answers["members"]:
592
+ lint, test = _resolve_commands(member)
593
+ workspaces.append({
594
+ "name": member["name"],
595
+ "path": member["path"],
596
+ "stack": member["stack"],
597
+ "typeCheckCommand": lint,
598
+ "testCommand": test,
599
+ })
600
+ config["workspaces"] = workspaces
601
+ _write_artifact(target, "forge.config.json", _json_text(config), sentinel)
602
+
603
+
604
+ def _compose_readme(answers: Answers) -> str:
605
+ """Compose README.md from the hygiene template with token substitution (02 §4.5)."""
606
+ text = (TEMPLATE_ROOT / "hygiene" / "README.md").read_text(encoding="utf-8")
607
+ license_label = answers["license"] if answers["license"] != "none" else "no license"
608
+ for tok, val in (
609
+ ("{{PROJECT_NAME}}", answers["projectName"]),
610
+ ("{{PURPOSE}}", answers["purpose"]),
611
+ ("{{LICENSE}}", license_label),
612
+ ):
613
+ text = text.replace(tok, val)
614
+ return text
615
+
616
+
617
+ def _compose_license(answers: Answers) -> str:
618
+ """Compose the LICENSE text from templates/licenses/<id>/LICENSE (02 §4.5)."""
619
+ src = TEMPLATE_ROOT / "licenses" / answers["license"] / "LICENSE"
620
+ if not src.is_file():
621
+ raise UsageError(f"no license template for {answers['license']!r}: {src}")
622
+ text = src.read_text(encoding="utf-8")
623
+ year = str(datetime.now(timezone.utc).year)
624
+ for tok, val in (
625
+ ("{{YEAR}}", year),
626
+ ("{{AUTHOR}}", answers["author"]),
627
+ ("{{PROJECT_NAME}}", answers["projectName"]),
628
+ ):
629
+ text = text.replace(tok, val)
630
+ return text
631
+
632
+
633
+ def _compose_agent_file(answers: Answers, filename: str) -> str:
634
+ """Compose AGENTS.md / CLAUDE.md from the hygiene template (02 §4.5)."""
635
+ text = (TEMPLATE_ROOT / "hygiene" / filename).read_text(encoding="utf-8")
636
+ for tok, val in (
637
+ ("{{PROJECT_NAME}}", answers["projectName"]),
638
+ ("{{PURPOSE}}", answers["purpose"]),
639
+ ):
640
+ text = text.replace(tok, val)
641
+ return text
642
+
643
+
644
+ def write_hygiene(answers: Answers, target: Path, sentinel: Sentinel) -> None:
645
+ """Emit README, LICENSE, and the host agent-instruction file(s) (02 §4.5)."""
646
+ _write_artifact(target, "README.md", _compose_readme(answers), sentinel)
647
+ if answers["license"] != "none":
648
+ _write_artifact(target, "LICENSE", _compose_license(answers), sentinel)
649
+ _write_artifact(
650
+ target, "AGENTS.md", _compose_agent_file(answers, "AGENTS.md"), sentinel
651
+ )
652
+ if answers["host"] == "claude":
653
+ _write_artifact(
654
+ target, "CLAUDE.md", _compose_agent_file(answers, "CLAUDE.md"), sentinel
655
+ )
656
+
657
+
658
+ def _compose_ci_workflow(answers: Answers) -> str:
659
+ """Compose the CI workflow, injecting per-member lint+test steps (03 §9).
660
+
661
+ Reads templates/ci/github-actions.yml and replaces the ``# <<MEMBER_STEPS>>``
662
+ marker with one lint step + one test step per member. For a single package
663
+ (one implicit member at '.') the steps carry no ``working-directory``; for a
664
+ monorepo every member is pinned to its ``path`` so CI exercises EVERY member
665
+ (REQ-MONO-04).
666
+ """
667
+ src = TEMPLATE_ROOT / "ci" / "github-actions.yml"
668
+ if not src.is_file():
669
+ raise UsageError(f"CI workflow template not found: {src}")
670
+ template = src.read_text(encoding="utf-8")
671
+ marker = "# <<MEMBER_STEPS>>"
672
+ if marker not in template:
673
+ raise UsageError(f"CI template missing {marker!r} marker: {src}")
674
+ indent = template[: template.index(marker)].rsplit("\n", 1)[-1]
675
+ single = answers["layout"] == "single"
676
+ lines: list[str] = []
677
+ for member in answers["members"]:
678
+ lint, test = _resolve_commands(member)
679
+ wd = None if single else member["path"]
680
+ label = member["name"]
681
+ for kind, cmd in (("lint", lint), ("test", test)):
682
+ name = kind if single else f"{label} — {kind}"
683
+ lines.append(f"{indent}- name: {name}")
684
+ if wd is not None:
685
+ lines.append(f"{indent} working-directory: {wd}")
686
+ lines.append(f"{indent} run: {cmd}")
687
+ block = "\n".join(lines)
688
+ return template.replace(f"{indent}{marker}", block)
689
+
690
+
691
+ def maybe_write_ci(answers: Answers, target: Path, sentinel: Sentinel) -> None:
692
+ """Emit a CI workflow when answers.ci is true (02 §4.4).
693
+
694
+ No-op when answers.ci is false (REQ-SCAF-07). When enabled, composes
695
+ .github/workflows/ci.yml from templates/ci/github-actions.yml with one
696
+ lint+test step per member (03 §9, REQ-MONO-04) and records it for staging.
697
+ """
698
+ if not answers["ci"]:
699
+ return
700
+ content = _compose_ci_workflow(answers)
701
+ _write_artifact(target, ".github/workflows/ci.yml", content, sentinel)
702
+
703
+
704
+ def scaffold(target: Path, answers: Answers) -> list[str]:
705
+ """Emit the pipeline-ready baseline for every member, idempotently (02 §4).
706
+
707
+ Ordering is load-bearing: the sentinel is written FIRST (REQ-LIFE-01) so a crash
708
+ leaves a recoverable partial scaffold; ``git init`` runs only when no ``.git/``
709
+ exists (REQ-GATE-03); then each member is composed (REQ-MONO-01/02), the
710
+ repo-hygiene files are emitted (REQ-SCAF-06/09), ``forge.config.json`` is written
711
+ (REQ-CFG-01/02/03), and a CI workflow is emitted when requested. Every written
712
+ path is recorded in the sentinel's artifactsWritten[]; a recorded path or a
713
+ pre-existing allowed-meta file is skipped, making the run idempotent.
714
+ """
715
+ sentinel = read_sentinel(target)
716
+ if sentinel is None:
717
+ sentinel = {
718
+ "version": 1,
719
+ "status": "in-progress",
720
+ "startedAt": datetime.now(timezone.utc).isoformat(),
721
+ "answers": answers,
722
+ "artifactsWritten": [],
723
+ }
724
+ write_sentinel(target, sentinel)
725
+ if not (target / ".git").is_dir():
726
+ run(["git", "init"], cwd=target)
727
+ for member in answers["members"]:
728
+ compose_member(member, answers, target, sentinel)
729
+ write_hygiene(answers, target, sentinel)
730
+ write_config(answers, target, sentinel)
731
+ maybe_write_ci(answers, target, sentinel)
732
+ return sentinel["artifactsWritten"]
733
+
734
+
735
+ def toolchain_present(required: list[str]) -> bool:
736
+ """Return True iff every required tool is on PATH (REQ-LIFE-03).
737
+
738
+ Probes each binary with :func:`shutil.which` (a pure PATH lookup, no shell
739
+ spawned). A single missing tool yields False, driving the distinct
740
+ missing-toolchain outcome (exit 2, 00 §9): the skill then offers
741
+ scaffold-anyway-unverified vs abort and marks the baseline unverified
742
+ (REQ-LIFE-04). Bootstrap NEVER installs a toolchain (tech-spec §9).
743
+
744
+ Args:
745
+ required: Distinct probe binaries for the resolved stack(s) (00 §6),
746
+ already {pm}-substituted.
747
+
748
+ Returns:
749
+ True iff every entry resolves on PATH.
750
+ """
751
+ for tool in required:
752
+ if shutil.which(tool) is None:
753
+ return False
754
+ return True
755
+
756
+
757
+ def verify(target: Path, answers: Answers) -> VerifyResult:
758
+ """Detect the toolchain and run resolved lint/test per member (02 §5, 00 §6).
759
+
760
+ First probes every required tool (toolchain_present). If any is missing,
761
+ returns immediately with toolchainPresent=False, empty lint/test, green=False —
762
+ the caller maps this to exit 2 (the distinct missing-toolchain outcome, 00 §9,
763
+ REQ-LIFE-03/04). When the toolchain is present, runs each member's resolved
764
+ typeCheckCommand then testCommand (00 §6) in that member's directory, collecting
765
+ one CommandOutcome per command (per member for a monorepo — REQ-MONO-03). The
766
+ ``green`` predicate — toolchainPresent AND every outcome ok — is the single gate
767
+ Mode B checks before launching the next stage (REQ-MODEB-04). Commands resolve
768
+ from STACK_COMMANDS so they match references/stacks/*.md exactly (REQ-STACK-02).
769
+
770
+ Args:
771
+ target: The project root being verified.
772
+ answers: The resolved interview payload (members + commands).
773
+
774
+ Returns:
775
+ A VerifyResult (00 §4): toolchainPresent / lint[] / test[] / green.
776
+
777
+ Raises:
778
+ UsageError: If a member directory is missing or a command cannot be
779
+ launched (exit 2).
780
+ """
781
+ required: list[str] = []
782
+ for member in answers["members"]:
783
+ _, _, probes = STACK_COMMANDS[member["stack"]]
784
+ pm = member["packageManager"] or ""
785
+ for probe in probes:
786
+ tool = probe.replace("{pm}", pm)
787
+ if tool and tool not in required:
788
+ required.append(tool)
789
+ present = toolchain_present(required)
790
+ if not present:
791
+ return {"toolchainPresent": False, "lint": [], "test": [], "green": False}
792
+
793
+ lint: list[CommandOutcome] = []
794
+ test: list[CommandOutcome] = []
795
+ for member in answers["members"]:
796
+ lint_cmd, test_cmd = _resolve_commands(member)
797
+ cwd = contained_path(target, member["path"]) # never run outside the target
798
+ for bucket, cmd in ((lint, lint_cmd), (test, test_cmd)):
799
+ # STACK_COMMANDS templates with an allow-listed {pm} (see _validate_answers)
800
+ # — split into an argv token list and run without a shell (no `sh -c`).
801
+ proc = run(shlex.split(cmd), cwd=cwd, check=False)
802
+ bucket.append(
803
+ {"command": cmd, "ok": proc.returncode == 0, "member": member["path"]}
804
+ )
805
+ green = all(o["ok"] for o in (*lint, *test))
806
+ return {"toolchainPresent": True, "lint": lint, "test": test, "green": green}
807
+
808
+
809
+ def commit(target: Path, answers: Answers, stage_only: bool) -> CommitResult:
810
+ """Stage the exact artifact list and commit or stop at staged (02 §6, 00 §4).
811
+
812
+ Ordering is load-bearing (OQ-T3): the sentinel file is deleted BEFORE any
813
+ ``git add`` so it can never be staged or enter history (REQ-SCAF-08). Staging
814
+ uses the exact artifactsWritten[] list with ``git add -- <paths>`` — never
815
+ ``git add -A`` (REQ-SEC-02) — and follows the shared Git Commit Protocol (never
816
+ --force / --no-verify). When stage_only (or commitStyle == "stage-only") the
817
+ scaffold is left staged with no commit (REQ-LIFE-05); otherwise a single
818
+ baseline commit captures the whole scaffold plus forge.config.json (REQ-LIFE-06).
819
+ The commit prefix is read from the just-written forge.config.json's
820
+ ``commitPrefix`` field (default "forge"), NOT from answers (00 §5/§7).
821
+
822
+ Args:
823
+ target: The project root being committed.
824
+ answers: The resolved interview payload (for commitStyle; the commit
825
+ prefix is read from forge.config.json, not from answers).
826
+ stage_only: True to stop at staged with no commit (the --stage-only flag).
827
+
828
+ Returns:
829
+ A CommitResult (00 §4): committed / commitHash / staged / sentinelRemoved.
830
+
831
+ Raises:
832
+ UsageError: No sentinel to commit, or a git/IO failure (exit 2). On a git
833
+ failure the sentinel is already removed; the run is re-stageable.
834
+ """
835
+ sentinel = read_sentinel(target)
836
+ if sentinel is None:
837
+ raise UsageError(
838
+ "no .forge-bootstrap.json sentinel to commit; run scaffold first"
839
+ )
840
+ staged = list(sentinel["artifactsWritten"])
841
+
842
+ # OQ-T3: remove the sentinel BEFORE staging so it never enters history.
843
+ (target / SENTINEL_FILENAME).unlink(missing_ok=True)
844
+
845
+ run(["git", "add", "--", *staged], cwd=target)
846
+
847
+ if stage_only or answers["commitStyle"] == "stage-only":
848
+ return {
849
+ "committed": False,
850
+ "commitHash": None,
851
+ "staged": staged,
852
+ "sentinelRemoved": True,
853
+ }
854
+
855
+ # commitPrefix is a forge.config.json field (00 §7), not an interview answer.
856
+ # Read it back from the config bootstrap just wrote; default "forge" if absent.
857
+ try:
858
+ cfg = json.loads(
859
+ (target / "forge.config.json").read_text(encoding="utf-8")
860
+ )
861
+ except (OSError, json.JSONDecodeError) as exc:
862
+ raise UsageError(f"cannot read forge.config.json: {exc}")
863
+ prefix = cfg.get("commitPrefix") or "forge"
864
+ message = f"{prefix}: bootstrap baseline"
865
+ run(["git", "commit", "-m", message], cwd=target)
866
+ rev = run(["git", "rev-parse", "HEAD"], cwd=target)
867
+ return {
868
+ "committed": True,
869
+ "commitHash": rev.stdout.strip(),
870
+ "staged": staged,
871
+ "sentinelRemoved": True,
872
+ }
873
+
874
+
875
+ def status(target: Path) -> "Sentinel | None":
876
+ """Return the parsed resume sentinel, or None when absent (02 §7)."""
877
+ return read_sentinel(target)
878
+
879
+
880
+ # --------------------------------------------------------------------------- #
881
+ # CLI Dispatch (02 §8.2)
882
+ # --------------------------------------------------------------------------- #
883
+
884
+
885
+ def _parse_answers(raw: str) -> Answers:
886
+ """Parse the --answers JSON payload into an Answers dict (02 §8.2).
887
+
888
+ Args:
889
+ raw: The raw JSON string passed via --answers.
890
+
891
+ Returns:
892
+ The parsed Answers dict.
893
+
894
+ Raises:
895
+ UsageError: If the payload is not valid JSON or not an object (exit 2).
896
+ """
897
+ try:
898
+ parsed = json.loads(raw)
899
+ except json.JSONDecodeError as exc:
900
+ raise UsageError(f"malformed --answers JSON: {exc}")
901
+ if not isinstance(parsed, dict):
902
+ raise UsageError("--answers must be a JSON object")
903
+ _validate_answers(parsed)
904
+ return parsed
905
+
906
+
907
+ def _validate_answers(answers: Answers) -> None:
908
+ """Reject non-allowlisted stack/packageManager and unsafe member paths (exit 2).
909
+
910
+ Runs before any ``{pm}`` substitution, ``STACK_COMMANDS[...]`` subscript, or
911
+ artifact write so a malformed ``--answers`` payload fails with a stderr
912
+ diagnostic, never a KeyError traceback or a path escape. Mirrors the
913
+ input-validation rigor epic-manifest.py applies via ``assert_safe_name`` /
914
+ ``contained_path``:
915
+
916
+ - ``stack`` must be a known :data:`Stack` (a ``STACK_COMMANDS`` key).
917
+ - ``packageManager`` must be in ``PACKAGE_MANAGERS[stack]`` for a stack that
918
+ has a choice; stacks with no choice (go/rust/generic) must leave it ``None``.
919
+ - ``path`` must be a repo-relative string — no absolute path, no ``..``
920
+ component, no backslash — so it can never resolve outside the target.
921
+ """
922
+ members = answers.get("members")
923
+ if not isinstance(members, list) or not members:
924
+ raise UsageError("--answers must include a non-empty members[] array")
925
+ for member in members:
926
+ stack = member.get("stack")
927
+ if stack not in STACK_COMMANDS:
928
+ raise UsageError(
929
+ f"unknown stack {stack!r}: expected one of {sorted(STACK_COMMANDS)}"
930
+ )
931
+ pm = member.get("packageManager")
932
+ choices = PACKAGE_MANAGERS.get(stack)
933
+ if choices is not None:
934
+ if pm not in choices:
935
+ raise UsageError(
936
+ f"invalid packageManager {pm!r} for stack {stack!r}: "
937
+ f"expected one of {choices}"
938
+ )
939
+ elif pm is not None:
940
+ raise UsageError(
941
+ f"stack {stack!r} takes no packageManager, got {pm!r}"
942
+ )
943
+ path = member.get("path")
944
+ if not isinstance(path, str) or not path:
945
+ raise UsageError(f"member path must be a non-empty string, got {path!r}")
946
+ if os.path.isabs(path) or "\\" in path or ".." in Path(path).parts:
947
+ raise UsageError(
948
+ f"unsafe member path {path!r}: must be repo-relative with no '..'"
949
+ )
950
+
951
+
952
+ def _dispatch(args: argparse.Namespace, target: Path) -> int:
953
+ """Route a parsed command to its handler, translating outcomes into exit codes.
954
+
955
+ Read-only / write commands print their result as JSON under --json. ``check``
956
+ raises FindingsError on a greenfield refusal; ``verify`` raises UsageError on a
957
+ missing toolchain (exit 2) or FindingsError when not green (exit 1).
958
+ """
959
+ cmd: str = args.cmd
960
+
961
+ if cmd == "check":
962
+ specs_dir = Path(args.specs_dir)
963
+ result = check(target, specs_dir)
964
+ if not result["eligible"] and result["resumeMarker"] is None:
965
+ raise FindingsError(result)
966
+ if args.json_output:
967
+ print(_json_text(result), end="")
968
+ return 0
969
+
970
+ if cmd == "scaffold":
971
+ answers = _parse_answers(args.answers)
972
+ written = scaffold(target, answers)
973
+ if args.json_output:
974
+ print(_json_text({"artifactsWritten": written}), end="")
975
+ return 0
976
+
977
+ if cmd == "verify":
978
+ answers = _parse_answers(args.answers)
979
+ result = verify(target, answers)
980
+ if not result["toolchainPresent"]:
981
+ if args.json_output:
982
+ print(_json_text(result), end="")
983
+ raise UsageError("toolchain missing")
984
+ if not result["green"]:
985
+ raise FindingsError(result)
986
+ if args.json_output:
987
+ print(_json_text(result), end="")
988
+ return 0
989
+
990
+ if cmd == "commit":
991
+ answers = _parse_answers(args.answers)
992
+ result = commit(target, answers, args.stage_only)
993
+ if args.json_output:
994
+ print(_json_text(result), end="")
995
+ return 0
996
+
997
+ if cmd == "status":
998
+ result = status(target)
999
+ if args.json_output:
1000
+ print(_json_text(result), end="")
1001
+ return 0
1002
+
1003
+ raise UsageError(f"unknown command: {cmd}")
1004
+
1005
+
1006
+ def _build_parser() -> argparse.ArgumentParser:
1007
+ """Build the argparse parser with one subparser per subcommand (02 §8.2)."""
1008
+ parser = argparse.ArgumentParser(prog="forge-bootstrap.py", description=__doc__)
1009
+ sub = parser.add_subparsers(dest="cmd", required=True)
1010
+
1011
+ def add_json(p: argparse.ArgumentParser) -> None:
1012
+ p.add_argument(
1013
+ "--json", action="store_true", dest="json_output", help="Output as JSON"
1014
+ )
1015
+
1016
+ # check ----------------------------------------------------------------- #
1017
+ p_check = sub.add_parser("check", help="Greenfield gate + recovery detection")
1018
+ p_check.add_argument("target", help="Target repo directory to bootstrap")
1019
+ p_check.add_argument("--specs-dir", default="./specs", help="Specs directory")
1020
+ add_json(p_check)
1021
+
1022
+ # scaffold -------------------------------------------------------------- #
1023
+ p_scaffold = sub.add_parser("scaffold", help="Emit the pipeline-ready baseline")
1024
+ p_scaffold.add_argument("target", help="Target repo directory to bootstrap")
1025
+ p_scaffold.add_argument("--answers", required=True, help="Resolved Answers JSON")
1026
+ add_json(p_scaffold)
1027
+
1028
+ # verify ---------------------------------------------------------------- #
1029
+ p_verify = sub.add_parser("verify", help="Toolchain detection + lint/test")
1030
+ p_verify.add_argument("target", help="Target repo directory to verify")
1031
+ p_verify.add_argument("--answers", required=True, help="Resolved Answers JSON")
1032
+ add_json(p_verify)
1033
+
1034
+ # commit ---------------------------------------------------------------- #
1035
+ p_commit = sub.add_parser("commit", help="Exact-list baseline commit")
1036
+ p_commit.add_argument("target", help="Target repo directory to commit")
1037
+ p_commit.add_argument("--answers", required=True, help="Resolved Answers JSON")
1038
+ p_commit.add_argument(
1039
+ "--stage-only", action="store_true", dest="stage_only",
1040
+ help="Stage the scaffold without committing",
1041
+ )
1042
+ add_json(p_commit)
1043
+
1044
+ # status ---------------------------------------------------------------- #
1045
+ p_status = sub.add_parser("status", help="Inspect the resume sentinel")
1046
+ p_status.add_argument("target", help="Target repo directory to inspect")
1047
+ add_json(p_status)
1048
+
1049
+ return parser
1050
+
1051
+
1052
+ def main() -> int:
1053
+ parser = _build_parser()
1054
+ args = parser.parse_args()
1055
+ target = Path(args.target)
1056
+ try:
1057
+ return _dispatch(args, target)
1058
+ except UsageError as exc:
1059
+ print(f"Error: {exc.message}", file=sys.stderr)
1060
+ return 2
1061
+ except FindingsError as exc:
1062
+ print(json.dumps(exc.result, indent=2, ensure_ascii=False))
1063
+ return 1
1064
+ except OSError as exc:
1065
+ print(f"Error: {exc}", file=sys.stderr)
1066
+ return 2
1067
+
1068
+
1069
+ if __name__ == "__main__":
1070
+ sys.exit(main())