@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
@@ -15,6 +15,27 @@ root navigator:
15
15
  [--config FILE] [--epic E] [--json]
16
16
  python3 forge-session.py stage-exit --feature F --stage S [--specs-dir DIR] \
17
17
  [--config FILE] [--epic E] [--next-feature N] [--host claude|generic] [--json]
18
+ python3 forge-session.py effective-config [--config FILE] [--schema PATH] [--json]
19
+
20
+ Plus the `state-*` write verbs, which author `.pipeline-state.json` so no stage
21
+ has to hand-write the JSON (and therefore no stage has to read the state schema):
22
+
23
+ python3 forge-session.py state-enter --feature F --stage S [--specs-dir DIR] \
24
+ [--epic E] [--json]
25
+ python3 forge-session.py state-artifact --feature F --stage S --path P \
26
+ [--path P ...] [--specs-dir DIR] [--epic E] [--json]
27
+ python3 forge-session.py state-complete --feature F --stage S --version N \
28
+ [--based-on STAGE=N ...] [--artifact P ...] [--commit-hash H] \
29
+ [--status complete|in-progress] [--resumable] [--preserve-commit-hash] \
30
+ [--specs-dir DIR] [--epic E] [--json]
31
+ python3 forge-session.py state-branch --feature F --branch B [--specs-dir DIR] \
32
+ [--epic E] [--json]
33
+ python3 forge-session.py state-note --feature F --note TEXT [--specs-dir DIR] \
34
+ [--epic E] [--json]
35
+ python3 forge-session.py state-decision --feature F --question Q --raised-by S \
36
+ [--rationale R] [--target-stage S] [--specs-dir DIR] [--epic E] [--json]
37
+ python3 forge-session.py state-ecr --feature F --kind K --target T --rationale R \
38
+ --raised-by S --blocks-current true|false [--specs-dir DIR] [--epic E] [--json]
18
39
 
19
40
  `rank-features` scans the specs tree for feature-shaped directories (those that
20
41
  directly contain a `.pipeline-state.json`, in both the flat
@@ -66,6 +87,44 @@ present, autoFix eligibility, the verify and next-stage commands) plus the
66
87
  exact sentinel-terminated NEXT-STEPS block the skill must print verbatim as
67
88
  its absolute last output. Deterministic and read-only; always exits 0.
68
89
 
90
+ `effective-config` resolves the `loopRunner` block deterministically so no
91
+ caller has to read `references/forge-config-schema.json` just to learn the
92
+ defaults: it extracts each field's schema `default` at runtime and merges the
93
+ project's `loopRunner` overrides on top. A missing or corrupt
94
+ `forge.config.json` resolves to pure defaults (exit 0); only an unreadable
95
+ schema is fatal (exit 2), because then there are no defaults to resolve.
96
+
97
+ The `state-*` verbs are the script's only writers. Each follows the same
98
+ resolve -> load -> mutate -> refresh `updatedAt` -> atomic write path, so every
99
+ successful write leaves a schema-conformant state file: `state-enter` stamps a
100
+ stage in-progress and moves `currentStage`, `state-artifact` appends artifact
101
+ paths to a stage (de-duplicating), `state-branch` records the branch resolved by
102
+ Branch Setup / Branch Reconciliation, and `state-note` persists the free-text
103
+ note a user volunteers at a stage exit. They never create a feature directory —
104
+ an unknown `--feature` is a usage error (exit 2) — and they never overwrite a
105
+ state file they could not parse.
106
+
107
+ `state-complete` is the largest of them: it records the completion (status,
108
+ `completedAt`, `version`, `basedOnVersions`, `artifacts`), resets `commitHash` to
109
+ null for Commit 1 of the two-commit Git Commit Protocol, and runs the
110
+ deterministic downstream staleness cascade that each stage used to describe in
111
+ prose. `--commit-hash` is the Commit-2 follow-up, setting only that field (and
112
+ refusing a stage that is not yet complete). The protocol's two recovery branches
113
+ stay executable without hand-authored JSON: `--resumable` is the failed-Commit-1
114
+ revert (status-only, no cascade), and `--preserve-commit-hash` is the "nothing to
115
+ commit" branch. A bare `--status in-progress` is something else again —
116
+ forge-5-loop's partial completion, which keeps every completion field.
117
+
118
+ `state-decision` and `state-ecr` are the two array-appending verbs. The first
119
+ appends a `deferredDecisions[]` item — a same-feature decision deliberately
120
+ postponed to a later stage; the second appends an `epicChangeRequests[]` item —
121
+ a member stage's report that the epic decomposition itself must change, whose
122
+ `blocksCurrent` boolean drives the stage exit's pause-now vs. finish-then-edit
123
+ routing (so it is required and parsed strictly: only `true`/`false`). Both always
124
+ record `status: "open"` — resolving an item is the target stage's job, never the
125
+ recorder's — and both emit exactly the schema keys, because those two array item
126
+ shapes set `additionalProperties: false`.
127
+
69
128
  3.10 baseline, Google-style docstrings, full type annotations, stdlib only —
70
129
  matching the conventions of `scripts/epic-manifest.py`.
71
130
 
@@ -81,9 +140,10 @@ import json
81
140
  import os
82
141
  import subprocess
83
142
  import sys
143
+ import tempfile
84
144
  from datetime import datetime, timezone
85
145
  from pathlib import Path
86
- from typing import Final, TypedDict
146
+ from typing import Callable, Final, TypedDict
87
147
 
88
148
 
89
149
  # --------------------------------------------------------------------------- #
@@ -105,6 +165,34 @@ PRODUCTION_STAGES: Final[tuple[str, ...]] = (
105
165
  "forge-6-docs",
106
166
  )
107
167
 
168
+ #: The --stage domain for the state-write verbs: the six PRODUCTION_STAGES above
169
+ #: (order-sensitive — next_stage/verify_state/stage_exit all walk that tuple, so it
170
+ #: is NEVER redefined) plus forge-0-epic, which also carries a stageEntry but is
171
+ #: excluded from the next-stage walk.
172
+ STATE_VERB_STAGES: Final[tuple[str, ...]] = ("forge-0-epic", *PRODUCTION_STAGES)
173
+
174
+ #: The `--raised-by` / `--target-stage` domains for `state-decision`, and the
175
+ #: `--kind` / `--raised-by` domains for `state-ecr`. SOURCE OF TRUTH:
176
+ #: references/pipeline-state-schema.json (the `deferredDecisions` and
177
+ #: `epicChangeRequests` array item enums). Mirrored here so an out-of-enum value is
178
+ #: rejected at parse time; a drift guard asserts they still match the schema.
179
+ DECISION_RAISED_BY: Final[tuple[str, ...]] = (
180
+ "forge-1-prd",
181
+ "forge-2-tech",
182
+ "forge-3-specs",
183
+ "forge-4-backlog",
184
+ )
185
+ DECISION_TARGET_STAGES: Final[tuple[str, ...]] = (
186
+ "forge-1-prd",
187
+ "forge-2-tech",
188
+ "forge-3-specs",
189
+ "forge-4-backlog",
190
+ "forge-5-loop",
191
+ "forge-6-docs",
192
+ )
193
+ ECR_KINDS: Final[tuple[str, ...]] = ("add-feature", "redep", "move-boundary", "split")
194
+ ECR_RAISED_BY: Final[tuple[str, ...]] = ("forge-1-prd", "forge-2-tech")
195
+
108
196
  #: Production stage -> the verify token its findings file uses, and the
109
197
  #: `forge-verify-<token>` key its state lives under. forge-6-docs has no verify.
110
198
  VERIFY_TOKEN_BY_STAGE: Final[dict[str, str]] = {
@@ -1434,6 +1522,17 @@ def _resolve_feature_dir(specs_dir: Path, feature: str, epic: str | None) -> Pat
1434
1522
  return flat
1435
1523
 
1436
1524
 
1525
+ def _host_command(command: str, host: str) -> str:
1526
+ """Rewrite a `/feature-forge:` slash command to the host's surface.
1527
+
1528
+ Pi's slash-command surface is `/skill:` (matching the adapter body's
1529
+ `/feature-forge:` -> `/skill:` translation). The scripted stage-exit output bypasses
1530
+ that body translation, so it rewrites the commands it emits here. No-op for
1531
+ claude/generic, which keep the canonical `/feature-forge:` form.
1532
+ """
1533
+ return command.replace("/feature-forge:", "/skill:") if host == "pi" else command
1534
+
1535
+
1437
1536
  def _next_steps_block(
1438
1537
  next_command: str, host: str, reconcile: dict | None = None
1439
1538
  ) -> str:
@@ -1463,6 +1562,18 @@ def _next_steps_block(
1463
1562
  "2. Then start a fresh session and run the next stage below — or "
1464
1563
  "re-run `/feature-forge:forge` to let the navigator resume from disk."
1465
1564
  )
1565
+ elif host == "pi":
1566
+ # Pi's fresh-session command is `/new` (not `/clear`); its slash-command
1567
+ # surface is `/skill:` (the fenced command below is rewritten to match).
1568
+ clear_line = (
1569
+ "1. `/new` — recommended unconditionally at this stage boundary; every "
1570
+ "artifact is on disk, so the work survives starting a fresh session. "
1571
+ "I can't run `/new` for you — you have to run it yourself."
1572
+ )
1573
+ next_line = (
1574
+ "2. Then, in the new session, run the next stage below — or re-run "
1575
+ "`/skill:forge` to let the navigator resume from disk."
1576
+ )
1466
1577
  else:
1467
1578
  clear_line = (
1468
1579
  "1. Clear your session / start a fresh session — recommended "
@@ -1479,7 +1590,7 @@ def _next_steps_block(
1479
1590
  # epic-change request the primary is the reconcile command; otherwise it is the
1480
1591
  # normal next-stage command. The fence sits before the sentinel, so the
1481
1592
  # sentinel remains the absolute last line.
1482
- fenced_command = reconcile["command"] if blocking else next_command
1593
+ fenced_command = _host_command(reconcile["command"] if blocking else next_command, host)
1483
1594
  lines = ["**Next steps**", clear_line]
1484
1595
  if blocking:
1485
1596
  count = reconcile["count"]
@@ -1495,15 +1606,14 @@ def _next_steps_block(
1495
1606
  lines.append("")
1496
1607
  lines.append(f"```\n{fenced_command}\n```")
1497
1608
  if blocking and reconcile.get("deferred"):
1498
- lines.append(
1499
- f"After reconciling, continue the pipeline with: `{reconcile['deferred']}`"
1500
- )
1609
+ deferred_cmd = _host_command(reconcile["deferred"], host)
1610
+ lines.append(f"After reconciling, continue the pipeline with: `{deferred_cmd}`")
1501
1611
  elif reconcile and reconcile.get("reminder"):
1502
1612
  count = reconcile["count"]
1503
1613
  plural = "s" if count != 1 else ""
1504
1614
  lines.append(
1505
1615
  f"You also flagged {count} epic change{plural} to reconcile when "
1506
- f"convenient: `{reconcile['command']}`"
1616
+ f"convenient: `{_host_command(reconcile['command'], host)}`"
1507
1617
  )
1508
1618
  lines.append(NEXT_STEPS_SENTINEL)
1509
1619
  return "\n".join(lines)
@@ -1630,10 +1740,10 @@ def stage_exit(
1630
1740
  "verifyGate": verify_gate,
1631
1741
  "autoFixEligible": auto_fix_eligible,
1632
1742
  "verifyState": verify_label,
1633
- "verifyCommand": f"/feature-forge:forge-verify {feature}",
1743
+ "verifyCommand": _host_command(f"/feature-forge:forge-verify {feature}", host),
1634
1744
  "autoVerifyEffective": effective_auto_verify,
1635
1745
  "nextStage": next_stage_id,
1636
- "nextCommand": next_command,
1746
+ "nextCommand": _host_command(next_command, host) if next_command else next_command,
1637
1747
  "invalidAutoVerifyKeys": invalid_auto_verify_keys(config),
1638
1748
  "gitRepo": git_repo,
1639
1749
  "cleanTree": clean_tree,
@@ -1661,11 +1771,860 @@ def _print_stage_exit(payload: dict) -> None:
1661
1771
  print(payload["nextSteps"])
1662
1772
 
1663
1773
 
1774
+ # --------------------------------------------------------------------------- #
1775
+ # Effective loopRunner config
1776
+ # --------------------------------------------------------------------------- #
1777
+
1778
+
1779
+ def _default_schema_path() -> Path:
1780
+ """Return the bundled forge-config-schema.json path (sibling references/ dir).
1781
+
1782
+ Resolved relative to this script file so `effective-config` works from any
1783
+ cwd. Overridable via the ``--schema`` flag (chiefly for tests).
1784
+
1785
+ Returns:
1786
+ The Path to ``references/forge-config-schema.json`` next to ``scripts/``.
1787
+ """
1788
+ return Path(__file__).resolve().parent.parent / "references" / "forge-config-schema.json"
1789
+
1790
+
1791
+ def _loop_runner_defaults(schema_path: Path) -> dict[str, object]:
1792
+ """Extract every ``loopRunner`` field's schema ``default``.
1793
+
1794
+ Reads ``properties.loopRunner.properties.<field>.default`` for each field.
1795
+ Stdlib-only (``json`` + dict access), mirroring
1796
+ ``tests/test_config_defaults_parity.py``. The schema is the single source of
1797
+ truth; nothing here is hardcoded.
1798
+
1799
+ Only fields that actually declare a ``default`` keyword are included. Every
1800
+ ``loopRunner`` field does today; a field losing its default would be a schema
1801
+ regression the drift guard catches, not something silently patched here.
1802
+
1803
+ Args:
1804
+ schema_path: Path to ``forge-config-schema.json``.
1805
+
1806
+ Returns:
1807
+ A dict mapping each ``loopRunner`` field name to its declared default
1808
+ value (templates such as ``"{bin} loop run …"`` are returned literally).
1809
+
1810
+ Raises:
1811
+ UsageError: If the schema is missing, unreadable, unparseable, or lacks a
1812
+ ``loopRunner.properties`` object — a deterministic failure that must
1813
+ exit 2. Never returns partial/empty defaults silently.
1814
+ """
1815
+ try:
1816
+ schema = json.loads(schema_path.read_text(encoding="utf-8"))
1817
+ except OSError as exc:
1818
+ raise UsageError(f"config schema unreadable: {schema_path} ({exc})") from exc
1819
+ except json.JSONDecodeError as exc:
1820
+ raise UsageError(f"config schema is not valid JSON: {schema_path} ({exc})") from exc
1821
+
1822
+ props = None
1823
+ if isinstance(schema, dict):
1824
+ loop_runner = schema.get("properties", {})
1825
+ if isinstance(loop_runner, dict):
1826
+ loop_runner = loop_runner.get("loopRunner", {})
1827
+ if isinstance(loop_runner, dict):
1828
+ props = loop_runner.get("properties")
1829
+ if not isinstance(props, dict) or not props:
1830
+ raise UsageError(f"config schema has no loopRunner.properties object: {schema_path}")
1831
+
1832
+ return {
1833
+ field: spec["default"]
1834
+ for field, spec in props.items()
1835
+ if isinstance(spec, dict) and "default" in spec
1836
+ }
1837
+
1838
+
1839
+ def resolve_loop_runner(config_path: Path, schema_path: Path) -> dict[str, object]:
1840
+ """Resolve the effective ``loopRunner`` config: schema defaults + user overrides.
1841
+
1842
+ Reads the schema defaults, then merges the user's ``loopRunner`` block (from
1843
+ ``forge.config.json`` via the existing ``_load_config``) OVER them. A user
1844
+ field replaces the default; an absent field keeps the default. The result is
1845
+ the fully-resolved block the loop consumes — computed deterministically so no
1846
+ model ever merges it by hand.
1847
+
1848
+ Args:
1849
+ config_path: Path to ``forge.config.json`` (``_load_config`` tolerates a
1850
+ missing/corrupt file, yielding pure defaults).
1851
+ schema_path: Path to ``forge-config-schema.json`` (source of the defaults).
1852
+
1853
+ Returns:
1854
+ The resolved ``loopRunner`` object: every schema-defaulted field present,
1855
+ with user overrides applied.
1856
+
1857
+ Raises:
1858
+ UsageError: If the schema is unreadable/unparseable (propagated from
1859
+ ``_loop_runner_defaults``) — exit 2, a deterministic failure.
1860
+ """
1861
+ resolved: dict[str, object] = dict(_loop_runner_defaults(schema_path))
1862
+
1863
+ user_loop_runner = _load_config(config_path).get("loopRunner")
1864
+ if isinstance(user_loop_runner, dict):
1865
+ for key, value in user_loop_runner.items():
1866
+ # Flat override: a user value replaces the default for that field.
1867
+ # (A future nested loopRunner field would recurse here; today every
1868
+ # field is a scalar, so a shallow override is exact.) An unknown key
1869
+ # is carried through — the model would have carried it too, and the
1870
+ # config schema is the authority that flags it at author time.
1871
+ resolved[key] = value
1872
+
1873
+ return resolved
1874
+
1875
+
1876
+ def _print_effective_config(resolved: dict[str, object]) -> None:
1877
+ """Print the resolved loopRunner config as an aligned key: value table.
1878
+
1879
+ Args:
1880
+ resolved: The resolved loopRunner object from ``resolve_loop_runner``.
1881
+ """
1882
+ print("Effective loopRunner config:")
1883
+ width = max((len(k) for k in resolved), default=0)
1884
+ for key in sorted(resolved):
1885
+ print(f" {key.ljust(width)} : {resolved[key]!r}")
1886
+
1887
+
1888
+ # --------------------------------------------------------------------------- #
1889
+ # State writes (shared machinery for the state-* verbs)
1890
+ # --------------------------------------------------------------------------- #
1891
+
1892
+
1893
+ def _now_iso() -> str:
1894
+ """Return the current UTC time as a Z-suffixed, second-precision ISO-8601 string.
1895
+
1896
+ Matches the `.pipeline-state.json` timestamp convention already on disk (the
1897
+ schema's ``format: date-time`` values; the read path normalizes a trailing
1898
+ ``Z``). Second precision keeps `updatedAt`/`startedAt`/`completedAt` visually
1899
+ consistent with the values other pipeline writers produce.
1900
+
1901
+ Returns:
1902
+ A timestamp like ``"2026-07-29T03:30:00Z"``.
1903
+ """
1904
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
1905
+
1906
+
1907
+ def _write_state(state_path: Path, state: dict) -> None:
1908
+ """Atomically write a `.pipeline-state.json` (temp file + os.replace).
1909
+
1910
+ Mirrors epic-manifest.py's ``atomic_write``: write to a sibling temp file in
1911
+ the same directory as the target, flush + fsync the bytes, then os.replace()
1912
+ the temp file onto the target. os.replace is atomic on POSIX within one
1913
+ filesystem, so an interrupted write never leaves a partial or corrupt state
1914
+ file. Concurrent multi-session mutation is out of scope (single writer
1915
+ assumed, matching epic-manifest.py).
1916
+
1917
+ Args:
1918
+ state_path: Destination path, e.g.
1919
+ ``{specsDir}/{feature}/.pipeline-state.json``.
1920
+ state: The fully-formed state dict to serialize.
1921
+
1922
+ Raises:
1923
+ UsageError: If the temp file cannot be created/written or the replace
1924
+ fails (→ exit 2). The temp file is removed first, so a failed write
1925
+ leaves no debris and the original target untouched.
1926
+ """
1927
+ try:
1928
+ fd, tmp_name = tempfile.mkstemp(
1929
+ prefix=f".{state_path.name}.", suffix=".tmp", dir=state_path.parent
1930
+ )
1931
+ except OSError as exc:
1932
+ raise UsageError(f"atomic write to {state_path} failed: {exc}") from exc
1933
+ tmp_path = Path(tmp_name)
1934
+ try:
1935
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
1936
+ json.dump(state, handle, indent=2, ensure_ascii=False)
1937
+ handle.write("\n")
1938
+ handle.flush()
1939
+ os.fsync(handle.fileno())
1940
+ os.replace(tmp_path, state_path)
1941
+ except OSError as exc:
1942
+ tmp_path.unlink(missing_ok=True)
1943
+ raise UsageError(f"atomic write to {state_path} failed: {exc}") from exc
1944
+
1945
+
1946
+ def _resolve_feature_dir_for_write(
1947
+ specs_dir: Path, feature: str, epic: str | None
1948
+ ) -> Path:
1949
+ """Fail-closed feature dir for the ``state-*`` WRITERS.
1950
+
1951
+ ``_resolve_feature_dir`` is the reader's best-effort resolver: it returns the
1952
+ flat ``{specsDir}/{feature}`` whenever that dir carries a state file, and
1953
+ falls back to the flat literal on a multi-match. That tolerance was written
1954
+ for ``stage-exit``, which is READ-ONLY — an unresolvable dir there just
1955
+ downgrades to ``{}``. For a writer the same tolerance means a bare
1956
+ ``--feature api`` mutates a standalone ``{specsDir}/api/`` while an epic
1957
+ member ``{specsDir}/{epic}/api/`` of the same name is silently left behind:
1958
+ cross-feature state corruption at exit 0.
1959
+
1960
+ So the write path mirrors ``epic-manifest.py resolve`` — the canonical
1961
+ resolver that produced ``{resolvedFeatureDir}`` in the first place, and which
1962
+ rejects an ambiguous name with a structured ``ambiguous:`` finding. A writer
1963
+ must not be more permissive than that resolver: more than one candidate
1964
+ carrying a state file, with no explicit ``--epic``, is a hard stop.
1965
+
1966
+ Args:
1967
+ specs_dir: The configured specs directory (``--specs-dir``).
1968
+ feature: The feature name (``--feature``).
1969
+ epic: The owning epic name for a nested member, else None (``--epic``).
1970
+
1971
+ Returns:
1972
+ The resolved feature directory. With ``--epic`` the nested path is taken
1973
+ verbatim; otherwise the single candidate carrying a state file, or the
1974
+ flat path when none does (the first-write case).
1975
+
1976
+ Raises:
1977
+ UsageError: The bare name matches more than one directory carrying a
1978
+ state file (→ exit 2, nothing written).
1979
+ """
1980
+ if epic:
1981
+ return specs_dir / epic / feature
1982
+ flat = specs_dir / feature
1983
+ candidates = [flat] if (flat / PIPELINE_STATE_FILENAME).is_file() else []
1984
+ if specs_dir.is_dir():
1985
+ candidates.extend(
1986
+ sorted(
1987
+ p
1988
+ for p in specs_dir.glob(f"*/{feature}")
1989
+ if (p / PIPELINE_STATE_FILENAME).is_file()
1990
+ )
1991
+ )
1992
+ if len(candidates) > 1:
1993
+ listed = ", ".join(str(p) for p in candidates)
1994
+ raise UsageError(
1995
+ f"ambiguous feature {feature!r}: {len(candidates)} directories carry a "
1996
+ f"state file ({listed}) — pass --epic <epic> to name the one to write. "
1997
+ f"Refusing to guess; nothing was written."
1998
+ )
1999
+ return candidates[0] if candidates else flat
2000
+
2001
+
2002
+ def _load_state_for_write(
2003
+ specs_dir: Path, feature: str, epic: str | None
2004
+ ) -> tuple[Path, dict]:
2005
+ """Resolve a feature's state path and load its current state for mutation.
2006
+
2007
+ Resolves through the fail-closed `_resolve_feature_dir_for_write`, NOT the
2008
+ reader's tolerant `_resolve_feature_dir`. Deliberately does NOT
2009
+ reuse `_read_state`: that reader downgrades a *corrupt* file to ``{}`` because
2010
+ the navigator's read-only sweep can safely treat it as not-started. A writer
2011
+ that inherited it would atomically replace a corrupt-but-recoverable state
2012
+ file with a near-empty one at exit 0. So: absent -> ``{}``; present but
2013
+ unparseable -> refuse, leaving the file byte-intact.
2014
+
2015
+ The verbs never create a feature directory; an unknown ``--feature`` is a
2016
+ usage error, not a silent create.
2017
+
2018
+ Args:
2019
+ specs_dir: The configured specs directory (``--specs-dir``).
2020
+ feature: The feature name (``--feature``).
2021
+ epic: The owning epic name for a nested member, else None (``--epic``).
2022
+
2023
+ Returns:
2024
+ A ``(state_path, state)`` tuple. ``state`` is a schema-shaped shell when
2025
+ no state file exists yet (see the seeding below).
2026
+
2027
+ Raises:
2028
+ UsageError: The bare ``feature`` name is ambiguous (more than one
2029
+ candidate directory carries a state file and no ``--epic`` was
2030
+ given), the feature directory does not exist, or the state file
2031
+ exists but is not a JSON object (→ exit 2).
2032
+ """
2033
+ state_dir = _resolve_feature_dir_for_write(specs_dir, feature, epic)
2034
+ if not state_dir.is_dir():
2035
+ raise UsageError(
2036
+ f"no feature directory at {state_dir} — check --feature "
2037
+ f"(and --epic for a nested epic member)"
2038
+ )
2039
+ state_path = state_dir / PIPELINE_STATE_FILENAME
2040
+ if state_path.exists():
2041
+ try:
2042
+ state = json.loads(state_path.read_text(encoding="utf-8"))
2043
+ except json.JSONDecodeError as exc:
2044
+ raise UsageError(
2045
+ f"{state_path} exists but is not valid JSON ({exc}); refusing to "
2046
+ f"overwrite it. Fix or move the file, then re-run."
2047
+ ) from exc
2048
+ if not isinstance(state, dict):
2049
+ raise UsageError(
2050
+ f"{state_path} is not a JSON object; refusing to overwrite it."
2051
+ )
2052
+ else:
2053
+ state = {}
2054
+
2055
+ # Seed the schema-required top-level fields for EVERY verb, not just
2056
+ # state-enter. Branch Setup fires state-branch before the entry stamp
2057
+ # (references/shared-conventions.md), so without this a first-write
2058
+ # state-branch would persist {"branch": ..., "updatedAt": ...} — missing
2059
+ # every required field — at exit 0. setdefault keeps existing state as-is.
2060
+ # (`updatedAt`, the sixth required field, is stamped by _commit_state.)
2061
+ state.setdefault("feature", feature)
2062
+ state.setdefault("createdAt", _now_iso())
2063
+ state.setdefault("pipelineStatus", "active")
2064
+ state.setdefault("stages", {})
2065
+ state.setdefault("currentStage", PRODUCTION_STAGES[0])
2066
+ return state_path, state
2067
+
2068
+
2069
+ def _commit_state(state_path: Path, state: dict) -> dict:
2070
+ """Refresh ``updatedAt`` and write ``state`` atomically; return it for echo.
2071
+
2072
+ Every verb calls this exactly once, after its mutation, so ``updatedAt`` is
2073
+ always refreshed on a successful write and the write is atomic.
2074
+
2075
+ Args:
2076
+ state_path: The resolved ``.pipeline-state.json`` path.
2077
+ state: The mutated state dict.
2078
+
2079
+ Returns:
2080
+ The same ``state`` dict (now carrying a fresh ``updatedAt``), so the verb
2081
+ can echo it under ``--json``.
2082
+
2083
+ Raises:
2084
+ UsageError: If the atomic write fails (→ exit 2).
2085
+ """
2086
+ state["updatedAt"] = _now_iso()
2087
+ _write_state(state_path, state)
2088
+ return state
2089
+
2090
+
2091
+ def _stage_entry(state: dict, stage: str) -> dict:
2092
+ """Return (creating if absent) the mutable ``stages.{stage}`` sub-object.
2093
+
2094
+ Bootstraps ``state["stages"]`` and ``state["stages"][stage]`` when missing, so
2095
+ a verb can write into a brand-new state (``{}``), and returns the stage dict
2096
+ for in-place mutation. The bootstrap seeds ``{"status": "pending"}`` rather
2097
+ than ``{}`` because ``stageEntry`` declares ``required: ["status"]`` — an entry
2098
+ created by state-artifact (which sets only ``artifacts``) would otherwise be
2099
+ schema-invalid at exit 0.
2100
+
2101
+ Args:
2102
+ state: The full state dict (mutated in place).
2103
+ stage: A stage id from ``STATE_VERB_STAGES`` (e.g. ``"forge-1-prd"``).
2104
+
2105
+ Returns:
2106
+ The mutable ``stages.{stage}`` dict.
2107
+ """
2108
+ stages = state.setdefault("stages", {})
2109
+ return stages.setdefault(stage, {"status": "pending"})
2110
+
2111
+
2112
+ # --------------------------------------------------------------------------- #
2113
+ # State-write verbs
2114
+ # --------------------------------------------------------------------------- #
2115
+
2116
+
2117
+ def cmd_state_enter(feature: str, stage: str, specs_dir: Path, epic: str | None) -> dict:
2118
+ """Apply the Entry Stamp: mark ``stage`` in-progress and set ``currentStage``.
2119
+
2120
+ Idempotent on re-entry within the same run: re-stamping an already
2121
+ in-progress stage simply refreshes ``startedAt``/``updatedAt``. The
2122
+ interactive resume-vs-restart decision stays the skill's — the verb never
2123
+ prompts. The write is left uncommitted; the stage's existing exit commit
2124
+ stages it later.
2125
+
2126
+ Args:
2127
+ feature: Feature name.
2128
+ stage: The stage being entered (a ``STATE_VERB_STAGES`` id).
2129
+ specs_dir: Specs directory.
2130
+ epic: Owning epic name, or None.
2131
+
2132
+ Returns:
2133
+ The mutated state dict (for the --json echo).
2134
+
2135
+ Raises:
2136
+ UsageError: Unknown feature directory, unparseable state file, or a
2137
+ failed atomic write (→ exit 2).
2138
+ """
2139
+ state_path, state = _load_state_for_write(specs_dir, feature, epic)
2140
+ entry = _stage_entry(state, stage)
2141
+ entry["status"] = "in-progress"
2142
+ entry["startedAt"] = _now_iso()
2143
+ state["currentStage"] = stage
2144
+ return _commit_state(state_path, state)
2145
+
2146
+
2147
+ def cmd_state_artifact(
2148
+ feature: str, stage: str, paths: list[str], specs_dir: Path, epic: str | None
2149
+ ) -> dict:
2150
+ """Append each path in ``paths`` to ``stages.{stage}.artifacts``, de-duplicating.
2151
+
2152
+ Idempotent: an already-tracked path is a no-op (no duplicate append), so a
2153
+ resumed run that re-records files it wrote earlier does not bloat the array.
2154
+ ``updatedAt`` is refreshed even on the all-duplicates branch, keeping "state
2155
+ was touched" honest. The verb does NOT stat the file — it records the path
2156
+ the skill asserts it wrote.
2157
+
2158
+ Args:
2159
+ feature: Feature name.
2160
+ stage: The producing stage id.
2161
+ paths: Artifact paths relative to the feature dir (repeatable ``--path``).
2162
+ specs_dir: Specs directory.
2163
+ epic: Owning epic name, or None.
2164
+
2165
+ Returns:
2166
+ The mutated state dict (for the --json echo).
2167
+
2168
+ Raises:
2169
+ UsageError: Unknown feature directory, unparseable state file, or a
2170
+ failed atomic write (→ exit 2).
2171
+ """
2172
+ state_path, state = _load_state_for_write(specs_dir, feature, epic)
2173
+ entry = _stage_entry(state, stage)
2174
+ artifacts = entry.setdefault("artifacts", [])
2175
+ for path in paths:
2176
+ if path not in artifacts:
2177
+ artifacts.append(path)
2178
+ return _commit_state(state_path, state)
2179
+
2180
+
2181
+ def _parse_based_on(pairs: list[str]) -> dict[str, int]:
2182
+ """Parse ``--based-on STAGE=N`` tokens into a ``{stageId: int}`` map.
2183
+
2184
+ Args:
2185
+ pairs: Raw ``STAGE=N`` strings from repeated ``--based-on`` flags.
2186
+
2187
+ Returns:
2188
+ A ``{stageId: version}`` dict (empty when no pairs were given — the
2189
+ forge-1-prd case, which records ``basedOnVersions == {}``).
2190
+
2191
+ Raises:
2192
+ UsageError: If a token lacks ``=`` or its value is not an integer
2193
+ (→ exit 2).
2194
+ """
2195
+ out: dict[str, int] = {}
2196
+ for token in pairs:
2197
+ if "=" not in token:
2198
+ raise UsageError(f"--based-on expects STAGE=N, got: {token!r}")
2199
+ stage_id, _, raw = token.partition("=")
2200
+ try:
2201
+ out[stage_id] = int(raw)
2202
+ except ValueError as exc:
2203
+ raise UsageError(f"--based-on version must be an integer: {token!r}") from exc
2204
+ return out
2205
+
2206
+
2207
+ #: Stages the staleness cascade may mark stale (downstream authored artifacts).
2208
+ #: The scope is tech..docs, matching the pre-R4 canon this cascade replaces —
2209
+ #: forge-1-prd L134 named `forge-2-tech` FIRST among the stages a PRD revision
2210
+ #: invalidates, and the tech spec is a PRD revision's most direct dependent.
2211
+ #: forge-1-prd is never marked stale by a later completion (nothing downstream
2212
+ #: feeds back into it). Keyed off this map, NOT off PRODUCTION_STAGES ordering —
2213
+ #: the two are not interchangeable (a positional slice from the completing stage
2214
+ #: would also break on forge-0-epic, which is a valid --stage but not a
2215
+ #: PRODUCTION_STAGES member).
2216
+ _CASCADE_TARGETS: Final[tuple[str, ...]] = (
2217
+ "forge-2-tech",
2218
+ "forge-3-specs",
2219
+ "forge-4-backlog",
2220
+ "forge-5-loop",
2221
+ "forge-6-docs",
2222
+ )
2223
+
2224
+
2225
+ def _cascade_staleness(state: dict, completed_stage: str, new_version: int) -> list[str]:
2226
+ """Mark downstream stages ``stale`` when they were built on an OLDER version.
2227
+
2228
+ Deterministic replacement for the model-prose rule in each stage's completion
2229
+ step ("if any downstream stage has basedOnVersions referencing an older
2230
+ version, set its status to stale"). For every downstream target (tech..docs),
2231
+ if its recorded ``basedOnVersions[completed_stage]`` is an integer strictly
2232
+ less than ``new_version`` AND the stage is currently ``complete``, flip it to
2233
+ ``stale``. A downstream stage that never referenced this upstream, or already
2234
+ references the new version, is untouched. A ``pending``/``in-progress``/
2235
+ already-``stale`` downstream stage is not re-flipped — only a ``complete``
2236
+ artifact can go stale.
2237
+
2238
+ Args:
2239
+ state: The full state dict (mutated in place).
2240
+ completed_stage: The stage that just completed (e.g. "forge-1-prd").
2241
+ new_version: That stage's new version.
2242
+
2243
+ Returns:
2244
+ The list of stage ids newly marked stale (for the --json echo / printer).
2245
+ """
2246
+ stages = state.get("stages", {})
2247
+ newly_stale: list[str] = []
2248
+ for target in _CASCADE_TARGETS:
2249
+ if target == completed_stage:
2250
+ continue
2251
+ entry = stages.get(target)
2252
+ if not isinstance(entry, dict) or entry.get("status") != "complete":
2253
+ continue
2254
+ based_on = entry.get("basedOnVersions")
2255
+ if not isinstance(based_on, dict):
2256
+ continue
2257
+ recorded = based_on.get(completed_stage)
2258
+ if isinstance(recorded, int) and not isinstance(recorded, bool) and recorded < new_version:
2259
+ entry["status"] = "stale"
2260
+ newly_stale.append(target)
2261
+ return newly_stale
2262
+
2263
+
2264
+ def cmd_state_complete(
2265
+ feature: str,
2266
+ stage: str,
2267
+ version: int,
2268
+ based_on: dict[str, int],
2269
+ artifacts: list[str],
2270
+ commit_hash: str | None,
2271
+ specs_dir: Path,
2272
+ epic: str | None,
2273
+ status: str | None = None,
2274
+ preserve_commit_hash: bool = False,
2275
+ resumable: bool = False,
2276
+ ) -> dict:
2277
+ """Mark ``stage`` complete, bump version, record provenance, cascade staleness.
2278
+
2279
+ Three branches, in precedence order:
2280
+
2281
+ 1. ``commit_hash`` given — Commit 2 of the two-commit Git Commit Protocol.
2282
+ Sets ONLY ``commitHash``, leaving status/version/artifacts intact. Guarded
2283
+ on the stage already being ``complete``, so a typo'd ``--stage`` cannot
2284
+ write a lone ``{"commitHash": …}`` entry (which would violate
2285
+ ``stageEntry``'s ``required: ["status"]``) at exit 0.
2286
+ 2. ``resumable`` — the failed-Commit-1 revert (`references/shared-conventions.md`
2287
+ L245). Records ONLY ``status = "in-progress"`` plus the ``updatedAt``
2288
+ refresh: no completedAt, no version bump, no basedOnVersions/artifacts
2289
+ write, no commitHash reset, no cascade. The frozen contract is "leave state
2290
+ as in-progress so the stage can be resumed"; stamping a completion, bumping
2291
+ the version, or cascading staleness off a commit that never landed are all
2292
+ behavioral changes.
2293
+ 3. Otherwise — the completion write: status, completedAt, version,
2294
+ basedOnVersions, artifacts, ``commitHash = None`` (Commit 1) unless
2295
+ ``preserve_commit_hash``, then the downstream staleness cascade.
2296
+
2297
+ Branch 2 is gated on ``resumable``, NOT on ``status == "in-progress"``:
2298
+ forge-5-loop's PARTIAL completion also passes ``--status in-progress`` but is a
2299
+ real completion-with-artifacts, so it takes branch 3 and keeps its
2300
+ completedAt/version/basedOnVersions/artifacts. Only ``status`` differs between
2301
+ ``--status complete`` and a bare ``--status in-progress``. Conflating the two
2302
+ would silently discard the ``--based-on`` item 013 passes on that call.
2303
+
2304
+ Args:
2305
+ feature: Feature name.
2306
+ stage: The completing stage id.
2307
+ version: The stage's new version.
2308
+ based_on: Parsed ``{upstreamStage: version}`` provenance map.
2309
+ artifacts: Final canonical artifact path list for this stage.
2310
+ commit_hash: If given, record it as the stage's commitHash (Commit 2);
2311
+ else set commitHash to None (Commit 1).
2312
+ specs_dir: Specs directory.
2313
+ epic: Owning epic name, or None.
2314
+ status: Terminal status to record — "complete" (the default when the flag
2315
+ is absent) or "in-progress" for a partial forge-5-loop run. ``None``
2316
+ means "not passed".
2317
+ preserve_commit_hash: Skip the ``commitHash = None`` reset, for the Git
2318
+ Commit Protocol's "Nothing to commit" branch (L248).
2319
+ resumable: Failed-Commit-1 revert (L245). Record only the status; implies
2320
+ ``--status in-progress``.
2321
+
2322
+ Returns:
2323
+ The mutated state dict, plus a synthetic ``_cascadedStale`` key that is
2324
+ surfaced in the --json echo / printer but NEVER written to disk.
2325
+
2326
+ Raises:
2327
+ UsageError: Contradictory ``--resumable --status complete``, a
2328
+ ``--commit-hash`` follow-up against a stage that is not complete, an
2329
+ unknown feature directory, an unparseable state file, or a failed
2330
+ atomic write (→ exit 2).
2331
+ """
2332
+ if resumable and status == "complete":
2333
+ raise UsageError(
2334
+ "--resumable implies --status in-progress; do not pass --status complete"
2335
+ )
2336
+ state_path, state = _load_state_for_write(specs_dir, feature, epic)
2337
+ entry = _stage_entry(state, stage)
2338
+ cascaded: list[str] = []
2339
+ if commit_hash is not None:
2340
+ # Commit-2 follow-up: record the real hash, leave everything else intact.
2341
+ actual = entry.get("status")
2342
+ if actual != _DONE_STATUS:
2343
+ raise UsageError(
2344
+ f"--commit-hash requires {stage} to be complete (status: {actual!r}); "
2345
+ "run state-complete without --commit-hash first"
2346
+ )
2347
+ entry["commitHash"] = commit_hash
2348
+ elif resumable:
2349
+ # Failed-Commit-1 revert (L245): record ONLY the status. See the note above
2350
+ # on why this is gated on --resumable rather than on the status value.
2351
+ entry["status"] = "in-progress"
2352
+ else:
2353
+ entry["status"] = status or _DONE_STATUS # "complete" | "in-progress" (partial)
2354
+ entry["completedAt"] = _now_iso()
2355
+ entry["version"] = version
2356
+ entry["basedOnVersions"] = based_on
2357
+ entry["artifacts"] = artifacts
2358
+ if not preserve_commit_hash:
2359
+ entry["commitHash"] = None # Commit 1 of the Commit Protocol
2360
+ cascaded = _cascade_staleness(state, stage, version)
2361
+ result = _commit_state(state_path, state)
2362
+ # Surface the cascade result for the caller without persisting it in state:
2363
+ # _commit_state already wrote the real dict, and `echo` is a copy.
2364
+ echo = dict(result)
2365
+ echo["_cascadedStale"] = cascaded
2366
+ return echo
2367
+
2368
+
2369
+ def cmd_state_branch(feature: str, branch: str, specs_dir: Path, epic: str | None) -> dict:
2370
+ """Set the top-level ``branch`` field.
2371
+
2372
+ Records the branch resolved by Branch Setup / Branch Reconciliation. The verb
2373
+ only writes the field; the interactive prompts and the visible one-line
2374
+ reconciliation note stay unchanged skill prose.
2375
+
2376
+ Branch Setup fires before the Entry Stamp, so this verb can legitimately be
2377
+ the FIRST thing to touch a feature's state file — `_load_state_for_write`'s
2378
+ field seeding is what keeps that first write schema-valid.
2379
+
2380
+ Args:
2381
+ feature: Feature name.
2382
+ branch: The branch name to record.
2383
+ specs_dir: Specs directory.
2384
+ epic: Owning epic name, or None.
2385
+
2386
+ Returns:
2387
+ The mutated state dict (for the --json echo).
2388
+
2389
+ Raises:
2390
+ UsageError: Unknown feature directory, unparseable state file, or a
2391
+ failed atomic write (→ exit 2).
2392
+ """
2393
+ state_path, state = _load_state_for_write(specs_dir, feature, epic)
2394
+ state["branch"] = branch
2395
+ return _commit_state(state_path, state)
2396
+
2397
+
2398
+ def cmd_state_note(feature: str, note: str, specs_dir: Path, epic: str | None) -> dict:
2399
+ """Set the top-level ``notes`` field to ``note``.
2400
+
2401
+ Overwrites any existing note (the field is a single free-text string, not an
2402
+ append log — matching the schema's ``notes: string``). The skill's "offer a
2403
+ note — don't force one" statement is unchanged; this verb runs only when the
2404
+ user volunteered text.
2405
+
2406
+ Args:
2407
+ feature: Feature name.
2408
+ note: The note text.
2409
+ specs_dir: Specs directory.
2410
+ epic: Owning epic name, or None.
2411
+
2412
+ Returns:
2413
+ The mutated state dict (for the --json echo).
2414
+
2415
+ Raises:
2416
+ UsageError: Unknown feature directory, unparseable state file, or a
2417
+ failed atomic write (→ exit 2).
2418
+ """
2419
+ state_path, state = _load_state_for_write(specs_dir, feature, epic)
2420
+ state["notes"] = note
2421
+ return _commit_state(state_path, state)
2422
+
2423
+
2424
+ def cmd_state_decision(
2425
+ feature: str,
2426
+ question: str,
2427
+ raised_by: str,
2428
+ rationale: str | None,
2429
+ target_stage: str | None,
2430
+ specs_dir: Path,
2431
+ epic: str | None,
2432
+ ) -> dict:
2433
+ """Append an open deferred-decision item to ``deferredDecisions[]``.
2434
+
2435
+ Emits exactly the schema keys — the array item sets
2436
+ ``additionalProperties: false``, so a convenience field is a hard validation
2437
+ failure: required ``question``/``raisedBy``/``raisedAt``/``status``, plus
2438
+ ``rationale``/``targetStage`` only when provided. ``status`` is always
2439
+ ``"open"``; the recorder never resolves a decision (the target stage flips it
2440
+ to ``"addressed"``).
2441
+
2442
+ Args:
2443
+ feature: Feature name.
2444
+ question: The deferred decision, phrased for the target stage.
2445
+ raised_by: The deferring stage id.
2446
+ rationale: Optional reason for deferring.
2447
+ target_stage: Optional resolving stage id.
2448
+ specs_dir: Specs directory.
2449
+ epic: Owning epic name, or None.
2450
+
2451
+ Returns:
2452
+ The mutated state dict (for the --json echo).
2453
+
2454
+ Raises:
2455
+ UsageError: Unknown feature directory, unparseable state file, or a
2456
+ failed atomic write (→ exit 2).
2457
+ """
2458
+ state_path, state = _load_state_for_write(specs_dir, feature, epic)
2459
+ item: dict = {
2460
+ "question": question,
2461
+ "raisedBy": raised_by,
2462
+ "raisedAt": _now_iso(),
2463
+ "status": "open",
2464
+ }
2465
+ if rationale is not None:
2466
+ item["rationale"] = rationale
2467
+ if target_stage is not None:
2468
+ item["targetStage"] = target_stage
2469
+ state.setdefault("deferredDecisions", []).append(item)
2470
+ return _commit_state(state_path, state)
2471
+
2472
+
2473
+ def _parse_bool(raw: str, flag: str) -> bool:
2474
+ """Parse an explicit boolean CLI value; fail closed on anything else.
2475
+
2476
+ Args:
2477
+ raw: The raw flag value (e.g. from ``--blocks-current``).
2478
+ flag: The flag name, for the error message.
2479
+
2480
+ Returns:
2481
+ ``True`` for ``"true"``, ``False`` for ``"false"`` (case-insensitive,
2482
+ surrounding whitespace ignored).
2483
+
2484
+ Raises:
2485
+ UsageError: For any other value (→ exit 2), so a typo like ``"yes"`` is
2486
+ rejected rather than silently misrouting the stage exit.
2487
+ """
2488
+ normalized = raw.strip().lower()
2489
+ if normalized == "true":
2490
+ return True
2491
+ if normalized == "false":
2492
+ return False
2493
+ raise UsageError(f"{flag} expects true|false, got: {raw!r}")
2494
+
2495
+
2496
+ def cmd_state_ecr(
2497
+ feature: str,
2498
+ kind: str,
2499
+ target: str,
2500
+ rationale: str,
2501
+ raised_by: str,
2502
+ blocks_current: bool,
2503
+ specs_dir: Path,
2504
+ epic: str | None,
2505
+ ) -> dict:
2506
+ """Append an open epic-change-request item to ``epicChangeRequests[]``.
2507
+
2508
+ Emits exactly the schema keys — the array item sets
2509
+ ``additionalProperties: false``, so a convenience field is a hard validation
2510
+ failure. All six payload fields are required, and ``status`` is always
2511
+ ``"open"`` (only forge-0-epic edit mode flips it). ``blocksCurrent`` drives
2512
+ stage-exit routing, so it is a strictly-parsed boolean.
2513
+
2514
+ Args:
2515
+ feature: Feature name.
2516
+ kind: One of add-feature|redep|move-boundary|split.
2517
+ target: The sibling feature to add, or the affected feature/boundary.
2518
+ rationale: Why the epic must change.
2519
+ raised_by: forge-1-prd or forge-2-tech.
2520
+ blocks_current: True → pause-now; False → finish-then-edit.
2521
+ specs_dir: Specs directory.
2522
+ epic: Owning epic name, or None.
2523
+
2524
+ Returns:
2525
+ The mutated state dict (for the --json echo).
2526
+
2527
+ Raises:
2528
+ UsageError: Unknown feature directory, unparseable state file, or a
2529
+ failed atomic write (→ exit 2).
2530
+ """
2531
+ state_path, state = _load_state_for_write(specs_dir, feature, epic)
2532
+ item = {
2533
+ "kind": kind,
2534
+ "target": target,
2535
+ "rationale": rationale,
2536
+ "blocksCurrent": blocks_current,
2537
+ "raisedBy": raised_by,
2538
+ "raisedAt": _now_iso(),
2539
+ "status": "open",
2540
+ }
2541
+ state.setdefault("epicChangeRequests", []).append(item)
2542
+ return _commit_state(state_path, state)
2543
+
2544
+
2545
+ def _print_state_enter(state: dict) -> None:
2546
+ """Print the one-line human summary for `state-enter`."""
2547
+ print(f"entered {state['currentStage']} (in-progress) for {state['feature']}")
2548
+
2549
+
2550
+ def _print_state_artifact(state: dict, stage: str, paths: list[str]) -> None:
2551
+ """Print the one-line human summary for `state-artifact`."""
2552
+ total = len(state.get("stages", {}).get(stage, {}).get("artifacts", []))
2553
+ print(f"tracked {stage} artifact(s): {', '.join(paths)} ({total} total)")
2554
+
2555
+
2556
+ def _print_state_complete(
2557
+ state: dict, stage: str, commit_hash: str | None, resumable: bool
2558
+ ) -> None:
2559
+ """Print the one-line human summary for `state-complete` (one per branch)."""
2560
+ if commit_hash is not None:
2561
+ print(f"recorded {stage} commitHash: {commit_hash}")
2562
+ return
2563
+ if resumable:
2564
+ print(f"left {stage} in-progress (resumable — no completion recorded)")
2565
+ return
2566
+ entry = state.get("stages", {}).get(stage, {})
2567
+ label = (
2568
+ "completed"
2569
+ if entry.get("status") == _DONE_STATUS
2570
+ else f"partially completed ({entry.get('status')})"
2571
+ )
2572
+ recorded = entry.get("commitHash")
2573
+ cascaded = state.get("_cascadedStale") or []
2574
+ suffix = f"; marked stale: {', '.join(cascaded)}" if cascaded else ""
2575
+ print(
2576
+ f"{label} {stage} v{entry.get('version')} "
2577
+ f"(commitHash: {'null' if recorded is None else recorded}){suffix}"
2578
+ )
2579
+
2580
+
2581
+ def _print_state_branch(state: dict) -> None:
2582
+ """Print the one-line human summary for `state-branch`."""
2583
+ print(f"recorded branch for {state['feature']}: {state['branch']}")
2584
+
2585
+
2586
+ def _print_state_note(state: dict) -> None:
2587
+ """Print the one-line human summary for `state-note`."""
2588
+ print(f"note set for {state['feature']} ({len(state['notes'])} chars)")
2589
+
2590
+
2591
+ def _print_state_decision(state: dict) -> None:
2592
+ """Print the one-line human summary for `state-decision` (the item appended)."""
2593
+ item = state["deferredDecisions"][-1]
2594
+ target = item.get("targetStage")
2595
+ routing = f"{item['raisedBy']} → {target}" if target else f"{item['raisedBy']}, no target stage"
2596
+ print(f"deferred decision recorded (raisedBy {routing})")
2597
+
2598
+
2599
+ def _print_state_ecr(state: dict) -> None:
2600
+ """Print the one-line human summary for `state-ecr` (the item appended)."""
2601
+ item = state["epicChangeRequests"][-1]
2602
+ blocks = "true" if item["blocksCurrent"] else "false"
2603
+ print(
2604
+ f"epic change request recorded ({item['kind']} → {item['target']}, "
2605
+ f"blocksCurrent={blocks})"
2606
+ )
2607
+
2608
+
1664
2609
  # --------------------------------------------------------------------------- #
1665
2610
  # CLI dispatch
1666
2611
  # --------------------------------------------------------------------------- #
1667
2612
 
1668
2613
 
2614
+ def _emit(payload: dict, json_output: bool, printer: Callable[[dict], None]) -> None:
2615
+ """Emit a state-verb result: the full JSON echo on --json, else the printer.
2616
+
2617
+ Args:
2618
+ payload: The verb's resulting state dict.
2619
+ json_output: The ``--json`` flag.
2620
+ printer: The verb's one-line human-readable printer.
2621
+ """
2622
+ if json_output:
2623
+ print(json.dumps(payload, indent=2, ensure_ascii=False))
2624
+ else:
2625
+ printer(payload)
2626
+
2627
+
1669
2628
  def _print_rank_table(rows: list[FeatureRow], counts: dict[str, int]) -> None:
1670
2629
  """Print a human-readable recency-ranked feature list."""
1671
2630
  print(
@@ -1759,10 +2718,130 @@ def main() -> int:
1759
2718
  p_exit.add_argument("--epic", default=None, help="Epic name for a nested member")
1760
2719
  p_exit.add_argument("--next-feature", default=None, dest="next_feature",
1761
2720
  help="First actionable feature (epic handoff next-command arg)")
1762
- p_exit.add_argument("--host", default="claude", choices=("claude", "generic"),
2721
+ p_exit.add_argument("--host", default="claude", choices=("claude", "generic", "pi"),
1763
2722
  help="Host wording for the NEXT-STEPS block")
1764
2723
  p_exit.add_argument("--json", action="store_true", dest="json_output")
1765
2724
 
2725
+ p_eff = sub.add_parser(
2726
+ "effective-config",
2727
+ help="Resolve the loopRunner config from schema defaults + user overrides",
2728
+ )
2729
+ p_eff.add_argument("--config", default="./forge.config.json", help="forge.config.json path")
2730
+ p_eff.add_argument(
2731
+ "--schema", default=None,
2732
+ help="forge-config-schema.json path (default: bundled references/ copy)",
2733
+ )
2734
+ p_eff.add_argument("--json", action="store_true", dest="json_output")
2735
+
2736
+ p_enter = sub.add_parser(
2737
+ "state-enter", help="Stamp a stage as in-progress (Entry Stamp)"
2738
+ )
2739
+ p_enter.add_argument("--feature", required=True, help="Feature name")
2740
+ p_enter.add_argument("--stage", required=True, choices=STATE_VERB_STAGES,
2741
+ help="The stage being entered")
2742
+ p_enter.add_argument("--specs-dir", default="./specs", help="Specs directory")
2743
+ p_enter.add_argument("--epic", default=None, help="Epic name for a nested member")
2744
+ p_enter.add_argument("--json", action="store_true", dest="json_output")
2745
+
2746
+ p_art = sub.add_parser(
2747
+ "state-artifact", help="Append artifact paths to a stage (de-duplicating)"
2748
+ )
2749
+ p_art.add_argument("--feature", required=True, help="Feature name")
2750
+ p_art.add_argument("--stage", required=True, choices=STATE_VERB_STAGES,
2751
+ help="The stage producing the artifact")
2752
+ p_art.add_argument("--path", required=True, action="append", dest="paths",
2753
+ metavar="PATH",
2754
+ help="Artifact path relative to the feature dir (repeatable)")
2755
+ p_art.add_argument("--specs-dir", default="./specs", help="Specs directory")
2756
+ p_art.add_argument("--epic", default=None, help="Epic name for a nested member")
2757
+ p_art.add_argument("--json", action="store_true", dest="json_output")
2758
+
2759
+ p_comp = sub.add_parser(
2760
+ "state-complete", help="Mark a stage complete; bump version; cascade staleness"
2761
+ )
2762
+ p_comp.add_argument("--feature", required=True, help="Feature name")
2763
+ p_comp.add_argument("--stage", required=True, choices=STATE_VERB_STAGES,
2764
+ help="The stage being completed")
2765
+ p_comp.add_argument("--version", type=int, required=True,
2766
+ help="This stage's new version (integer)")
2767
+ p_comp.add_argument("--based-on", action="append", default=[], dest="based_on",
2768
+ metavar="STAGE=N",
2769
+ help="Upstream version this artifact was built on (repeatable)")
2770
+ p_comp.add_argument("--artifact", action="append", default=[], dest="artifacts",
2771
+ metavar="PATH",
2772
+ help="Artifact path produced by this stage (repeatable)")
2773
+ p_comp.add_argument("--commit-hash", default=None, dest="commit_hash",
2774
+ help="Commit 2 follow-up: record the artifact commit's hash")
2775
+ p_comp.add_argument("--status", default=None,
2776
+ choices=("complete", "in-progress"),
2777
+ help="Terminal status to record (default: complete). "
2778
+ "Use in-progress for a partial forge-5-loop run -- the "
2779
+ "stage still records completedAt/version/basedOnVersions/"
2780
+ "artifacts; only the status differs.")
2781
+ p_comp.add_argument("--resumable", action="store_true",
2782
+ help="Failed-Commit-1 revert (L245): record ONLY status="
2783
+ "in-progress, leaving completedAt/version/basedOnVersions/"
2784
+ "artifacts/commitHash untouched and firing no cascade. "
2785
+ "Implies --status in-progress.")
2786
+ p_comp.add_argument("--preserve-commit-hash", action="store_true",
2787
+ dest="preserve_commit_hash",
2788
+ help="Do not reset commitHash to null on completion "
2789
+ "(the Git Commit Protocol's 'Nothing to commit' branch)")
2790
+ p_comp.add_argument("--specs-dir", default="./specs", help="Specs directory")
2791
+ p_comp.add_argument("--epic", default=None, help="Epic name for a nested member")
2792
+ p_comp.add_argument("--json", action="store_true", dest="json_output")
2793
+
2794
+ p_br = sub.add_parser("state-branch", help="Set the top-level branch field")
2795
+ p_br.add_argument("--feature", required=True, help="Feature name")
2796
+ p_br.add_argument("--branch", required=True, help="Branch name to record")
2797
+ p_br.add_argument("--specs-dir", default="./specs", help="Specs directory")
2798
+ p_br.add_argument("--epic", default=None, help="Epic name for a nested member")
2799
+ p_br.add_argument("--json", action="store_true", dest="json_output")
2800
+
2801
+ p_note = sub.add_parser("state-note", help="Set the top-level notes field")
2802
+ p_note.add_argument("--feature", required=True, help="Feature name")
2803
+ p_note.add_argument("--note", required=True, help="Note text to persist")
2804
+ p_note.add_argument("--specs-dir", default="./specs", help="Specs directory")
2805
+ p_note.add_argument("--epic", default=None, help="Epic name for a nested member")
2806
+ p_note.add_argument("--json", action="store_true", dest="json_output")
2807
+
2808
+ p_dec = sub.add_parser(
2809
+ "state-decision", help="Append a deferred decision (status: open)"
2810
+ )
2811
+ p_dec.add_argument("--feature", required=True, help="Feature name")
2812
+ p_dec.add_argument("--question", required=True,
2813
+ help="The deferred decision, phrased for the target stage")
2814
+ p_dec.add_argument("--raised-by", required=True, dest="raised_by",
2815
+ choices=DECISION_RAISED_BY,
2816
+ help="The stage deferring the decision")
2817
+ p_dec.add_argument("--rationale", default=None, help="Why it is deferred (optional)")
2818
+ p_dec.add_argument("--target-stage", default=None, dest="target_stage",
2819
+ choices=DECISION_TARGET_STAGES,
2820
+ help="The stage that should resolve it (optional)")
2821
+ p_dec.add_argument("--specs-dir", default="./specs", help="Specs directory")
2822
+ p_dec.add_argument("--epic", default=None, help="Epic name for a nested member")
2823
+ p_dec.add_argument("--json", action="store_true", dest="json_output")
2824
+
2825
+ p_ecr = sub.add_parser(
2826
+ "state-ecr", help="Append an epic change request (status: open)"
2827
+ )
2828
+ p_ecr.add_argument("--feature", required=True, help="Feature name")
2829
+ p_ecr.add_argument("--kind", required=True, choices=ECR_KINDS,
2830
+ help="The decomposition change kind")
2831
+ p_ecr.add_argument("--target", required=True,
2832
+ help="The sibling feature to add, or the feature/boundary affected")
2833
+ p_ecr.add_argument("--rationale", required=True, help="Why the epic must change")
2834
+ p_ecr.add_argument("--raised-by", required=True, dest="raised_by",
2835
+ choices=ECR_RAISED_BY,
2836
+ help="The stage that detected the epic-level concern")
2837
+ p_ecr.add_argument("--blocks-current", required=True, dest="blocks_current",
2838
+ metavar="true|false",
2839
+ help="true → pause-now (reconcile before proceeding); "
2840
+ "false → finish-then-edit")
2841
+ p_ecr.add_argument("--specs-dir", default="./specs", help="Specs directory")
2842
+ p_ecr.add_argument("--epic", default=None, help="Epic name for a nested member")
2843
+ p_ecr.add_argument("--json", action="store_true", dest="json_output")
2844
+
1766
2845
  args = parser.parse_args()
1767
2846
 
1768
2847
  try:
@@ -1853,6 +2932,97 @@ def main() -> int:
1853
2932
  _print_stage_exit(payload)
1854
2933
  return 0
1855
2934
 
2935
+ if args.cmd == "effective-config":
2936
+ schema_path = Path(args.schema) if args.schema else _default_schema_path()
2937
+ resolved = resolve_loop_runner(Path(args.config), schema_path)
2938
+ if args.json_output:
2939
+ print(json.dumps(resolved, indent=2, ensure_ascii=False))
2940
+ else:
2941
+ _print_effective_config(resolved)
2942
+ return 0
2943
+
2944
+ if args.cmd == "state-enter":
2945
+ payload = cmd_state_enter(
2946
+ args.feature, args.stage, Path(args.specs_dir), args.epic
2947
+ )
2948
+ _emit(payload, args.json_output, _print_state_enter)
2949
+ return 0
2950
+
2951
+ if args.cmd == "state-artifact":
2952
+ payload = cmd_state_artifact(
2953
+ args.feature, args.stage, args.paths, Path(args.specs_dir), args.epic
2954
+ )
2955
+ _emit(
2956
+ payload,
2957
+ args.json_output,
2958
+ lambda state: _print_state_artifact(state, args.stage, args.paths),
2959
+ )
2960
+ return 0
2961
+
2962
+ if args.cmd == "state-complete":
2963
+ payload = cmd_state_complete(
2964
+ args.feature,
2965
+ args.stage,
2966
+ args.version,
2967
+ _parse_based_on(args.based_on),
2968
+ args.artifacts,
2969
+ args.commit_hash,
2970
+ Path(args.specs_dir),
2971
+ args.epic,
2972
+ status=args.status,
2973
+ preserve_commit_hash=args.preserve_commit_hash,
2974
+ resumable=args.resumable,
2975
+ )
2976
+ _emit(
2977
+ payload,
2978
+ args.json_output,
2979
+ lambda state: _print_state_complete(
2980
+ state, args.stage, args.commit_hash, args.resumable
2981
+ ),
2982
+ )
2983
+ return 0
2984
+
2985
+ if args.cmd == "state-branch":
2986
+ payload = cmd_state_branch(
2987
+ args.feature, args.branch, Path(args.specs_dir), args.epic
2988
+ )
2989
+ _emit(payload, args.json_output, _print_state_branch)
2990
+ return 0
2991
+
2992
+ if args.cmd == "state-note":
2993
+ payload = cmd_state_note(
2994
+ args.feature, args.note, Path(args.specs_dir), args.epic
2995
+ )
2996
+ _emit(payload, args.json_output, _print_state_note)
2997
+ return 0
2998
+
2999
+ if args.cmd == "state-decision":
3000
+ payload = cmd_state_decision(
3001
+ args.feature,
3002
+ args.question,
3003
+ args.raised_by,
3004
+ args.rationale,
3005
+ args.target_stage,
3006
+ Path(args.specs_dir),
3007
+ args.epic,
3008
+ )
3009
+ _emit(payload, args.json_output, _print_state_decision)
3010
+ return 0
3011
+
3012
+ if args.cmd == "state-ecr":
3013
+ payload = cmd_state_ecr(
3014
+ args.feature,
3015
+ args.kind,
3016
+ args.target,
3017
+ args.rationale,
3018
+ args.raised_by,
3019
+ _parse_bool(args.blocks_current, "--blocks-current"),
3020
+ Path(args.specs_dir),
3021
+ args.epic,
3022
+ )
3023
+ _emit(payload, args.json_output, _print_state_ecr)
3024
+ return 0
3025
+
1856
3026
  raise UsageError(f"unknown command: {args.cmd}")
1857
3027
  except UsageError as exc:
1858
3028
  print(f"Error: {exc}", file=sys.stderr)