@garygentry/feature-forge 0.1.4 → 0.2.0

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