@garygentry/feature-forge 0.3.5 → 0.3.7

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 (778) hide show
  1. package/README.md +1 -1
  2. package/adapters/GENERATION-REPORT.md +20 -0
  3. package/adapters/claude/.claude-plugin/plugin.json +16 -0
  4. package/adapters/claude/.feature-forge-bundle.json +1 -1
  5. package/adapters/claude/agents/forge-verifier.md +16 -5
  6. package/adapters/claude/references/forge-config-schema.json +5 -5
  7. package/adapters/claude/references/portable-root.md +20 -16
  8. package/adapters/claude/references/preflight-and-self-heal.md +183 -0
  9. package/adapters/claude/references/process-overview.md +1 -1
  10. package/adapters/claude/references/ralph-loop-contract.md +12 -8
  11. package/adapters/claude/references/select-outcome.md +138 -0
  12. package/adapters/claude/references/shared-conventions.md +176 -23
  13. package/adapters/claude/references/skill-frontmatter.schema.json +3 -1
  14. package/adapters/claude/references/stack-resolution.md +1 -1
  15. package/adapters/claude/references/stage-exit-protocol.md +19 -4
  16. package/adapters/claude/references/templates/root-hygiene/AGENTS.md +14 -0
  17. package/adapters/claude/references/templates/root-hygiene/CLAUDE.md +14 -0
  18. package/adapters/claude/references/templates/specs-hygiene/AGENTS.md +3 -1
  19. package/adapters/claude/references/templates/specs-hygiene/CLAUDE.md +2 -1
  20. package/adapters/claude/references/verifier-patterns/MEMORY.md +35 -0
  21. package/adapters/claude/references/verifier-patterns/pattern_absence_claims.md +36 -0
  22. package/adapters/claude/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
  23. package/adapters/claude/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
  24. package/adapters/claude/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
  25. package/adapters/claude/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
  26. package/adapters/claude/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
  27. package/adapters/claude/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
  28. package/adapters/claude/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
  29. package/adapters/claude/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
  30. package/adapters/claude/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
  31. package/adapters/claude/references/verifier-patterns/pattern_self_referential_control.md +26 -0
  32. package/adapters/claude/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
  33. package/adapters/claude/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
  34. package/adapters/claude/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
  35. package/adapters/claude/references/verifier-patterns/pattern_test_count_units.md +20 -0
  36. package/adapters/claude/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
  37. package/adapters/claude/references/verify-state.md +98 -0
  38. package/adapters/claude/scripts/forge-bootstrap.py +23 -2
  39. package/adapters/claude/scripts/forge-root.sh +104 -21
  40. package/adapters/claude/scripts/forge-session.py +620 -6969
  41. package/adapters/claude/scripts/forge_session/__init__.py +11 -0
  42. package/adapters/claude/scripts/forge_session/_common.py +1566 -0
  43. package/adapters/claude/scripts/forge_session/_doctor_util.py +218 -0
  44. package/adapters/claude/scripts/forge_session/cli.py +1067 -0
  45. package/adapters/claude/scripts/forge_session/decisions.py +355 -0
  46. package/adapters/claude/scripts/forge_session/discover.py +472 -0
  47. package/adapters/claude/scripts/forge_session/doctor.py +2144 -0
  48. package/adapters/claude/scripts/forge_session/exit.py +1081 -0
  49. package/adapters/claude/scripts/forge_session/outcomes.py +574 -0
  50. package/adapters/claude/scripts/forge_session/routes.py +1162 -0
  51. package/adapters/claude/scripts/forge_session/state.py +1479 -0
  52. package/adapters/claude/scripts/forge_session/topology.py +443 -0
  53. package/adapters/claude/skills/forge/SKILL.md +7 -7
  54. package/adapters/claude/skills/forge/references/process-overview.md +1 -1
  55. package/adapters/claude/skills/forge/references/shared-conventions.md +176 -23
  56. package/adapters/claude/skills/forge/references/stage-exit-protocol.md +19 -4
  57. package/adapters/claude/skills/forge-0-epic/SKILL.md +7 -7
  58. package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +9 -5
  59. package/adapters/claude/skills/forge-0-epic/references/epic-manifest-subcommands.md +6 -1
  60. package/adapters/claude/skills/forge-0-epic/references/portable-root.md +20 -16
  61. package/adapters/claude/skills/forge-0-epic/references/shared-conventions.md +176 -23
  62. package/adapters/claude/skills/forge-0-epic/references/stage-exit-protocol.md +19 -4
  63. package/adapters/claude/skills/forge-1-prd/SKILL.md +7 -7
  64. package/adapters/claude/skills/forge-1-prd/references/shared-conventions.md +176 -23
  65. package/adapters/claude/skills/forge-1-prd/references/stage-exit-protocol.md +19 -4
  66. package/adapters/claude/skills/forge-2-tech/SKILL.md +7 -7
  67. package/adapters/claude/skills/forge-2-tech/references/shared-conventions.md +176 -23
  68. package/adapters/claude/skills/forge-2-tech/references/stack-resolution.md +1 -1
  69. package/adapters/claude/skills/forge-2-tech/references/stage-exit-protocol.md +19 -4
  70. package/adapters/claude/skills/forge-3-specs/SKILL.md +5 -5
  71. package/adapters/claude/skills/forge-3-specs/references/shared-conventions.md +176 -23
  72. package/adapters/claude/skills/forge-3-specs/references/stage-exit-protocol.md +19 -4
  73. package/adapters/claude/skills/forge-4-backlog/SKILL.md +16 -8
  74. package/adapters/claude/skills/forge-4-backlog/references/shared-conventions.md +176 -23
  75. package/adapters/claude/skills/forge-4-backlog/references/stage-exit-protocol.md +19 -4
  76. package/adapters/claude/skills/forge-4-backlog/references/verify-state.md +98 -0
  77. package/adapters/claude/skills/forge-5-loop/SKILL.md +36 -41
  78. package/adapters/claude/skills/forge-5-loop/references/agent-selection.md +4 -0
  79. package/adapters/claude/skills/forge-5-loop/references/preflight-and-self-heal.md +183 -0
  80. package/adapters/claude/skills/forge-5-loop/references/ralph-loop-contract.md +12 -8
  81. package/adapters/claude/skills/forge-5-loop/references/recovery-procedure.md +10 -0
  82. package/adapters/claude/skills/forge-5-loop/references/runner-contract.md +17 -7
  83. package/adapters/claude/skills/forge-5-loop/references/shared-conventions.md +176 -23
  84. package/adapters/claude/skills/forge-5-loop/references/stage-exit-protocol.md +19 -4
  85. package/adapters/claude/skills/forge-5-loop/references/verify-state.md +98 -0
  86. package/adapters/claude/skills/forge-6-docs/SKILL.md +21 -14
  87. package/adapters/claude/skills/forge-6-docs/references/shared-conventions.md +176 -23
  88. package/adapters/claude/skills/forge-6-docs/references/stage-exit-protocol.md +19 -4
  89. package/adapters/claude/skills/forge-6-docs/references/verify-state.md +98 -0
  90. package/adapters/claude/skills/forge-bootstrap/SKILL.md +19 -10
  91. package/adapters/claude/skills/forge-bootstrap/references/shared-conventions.md +572 -0
  92. package/adapters/claude/skills/forge-fix/SKILL.md +15 -18
  93. package/adapters/claude/skills/forge-fix/references/select-outcome.md +138 -0
  94. package/adapters/claude/skills/forge-fix/references/shared-conventions.md +176 -23
  95. package/adapters/claude/skills/forge-fix/references/stage-exit-protocol.md +19 -4
  96. package/adapters/claude/skills/forge-guide/SKILL.md +87 -1
  97. package/adapters/claude/skills/forge-guide/references/forge-config-schema.json +5 -5
  98. package/adapters/claude/skills/forge-guide/references/preflight-and-self-heal.md +183 -0
  99. package/adapters/claude/skills/forge-guide/references/process-overview.md +1 -1
  100. package/adapters/claude/skills/forge-guide/references/ralph-loop-contract.md +12 -8
  101. package/adapters/claude/skills/forge-guide/references/shared-conventions.md +176 -23
  102. package/adapters/claude/skills/forge-guide/references/stack-resolution.md +1 -1
  103. package/adapters/claude/skills/forge-init/SKILL.md +58 -7
  104. package/adapters/claude/skills/forge-init/references/preflight-and-self-heal.md +183 -0
  105. package/adapters/claude/skills/forge-init/references/shared-conventions.md +572 -0
  106. package/adapters/claude/skills/forge-verify/SKILL.md +25 -26
  107. package/adapters/claude/skills/forge-verify/references/findings-template.md +8 -8
  108. package/adapters/claude/skills/forge-verify/references/select-outcome.md +138 -0
  109. package/adapters/claude/skills/forge-verify/references/shared-conventions.md +176 -23
  110. package/adapters/claude/skills/forge-verify/references/stage-exit-protocol.md +19 -4
  111. package/adapters/claude/skills/forge-verify/references/verification-checklists/epic.md +1 -1
  112. package/adapters/claude/skills/forge-verify/references/verifier-patterns/MEMORY.md +35 -0
  113. package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_absence_claims.md +36 -0
  114. package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
  115. package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
  116. package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
  117. package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
  118. package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
  119. package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
  120. package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
  121. package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
  122. package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
  123. package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_self_referential_control.md +26 -0
  124. package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
  125. package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
  126. package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
  127. package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_test_count_units.md +20 -0
  128. package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
  129. package/adapters/codex/.feature-forge-bundle.json +1 -1
  130. package/adapters/codex/agents/forge-researcher.toml +1 -1
  131. package/adapters/codex/agents/forge-verifier.toml +17 -6
  132. package/adapters/codex/references/forge-config-schema.json +7 -7
  133. package/adapters/codex/references/portable-root.md +20 -16
  134. package/adapters/codex/references/preflight-and-self-heal.md +183 -0
  135. package/adapters/codex/references/process-overview.md +10 -10
  136. package/adapters/codex/references/ralph-loop-contract.md +12 -8
  137. package/adapters/codex/references/select-outcome.md +138 -0
  138. package/adapters/codex/references/shared-conventions.md +189 -36
  139. package/adapters/codex/references/skill-frontmatter.schema.json +3 -1
  140. package/adapters/codex/references/stack-resolution.md +1 -1
  141. package/adapters/codex/references/stage-exit-protocol.md +22 -7
  142. package/adapters/codex/references/templates/root-hygiene/AGENTS.md +14 -0
  143. package/adapters/codex/references/templates/root-hygiene/CLAUDE.md +14 -0
  144. package/adapters/codex/references/templates/specs-hygiene/AGENTS.md +3 -1
  145. package/adapters/codex/references/templates/specs-hygiene/CLAUDE.md +2 -1
  146. package/adapters/codex/references/verifier-patterns/MEMORY.md +35 -0
  147. package/adapters/codex/references/verifier-patterns/pattern_absence_claims.md +36 -0
  148. package/adapters/codex/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
  149. package/adapters/codex/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
  150. package/adapters/codex/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
  151. package/adapters/codex/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
  152. package/adapters/codex/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
  153. package/adapters/codex/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
  154. package/adapters/codex/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
  155. package/adapters/codex/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
  156. package/adapters/codex/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
  157. package/adapters/codex/references/verifier-patterns/pattern_self_referential_control.md +26 -0
  158. package/adapters/codex/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
  159. package/adapters/codex/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
  160. package/adapters/codex/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
  161. package/adapters/codex/references/verifier-patterns/pattern_test_count_units.md +20 -0
  162. package/adapters/codex/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
  163. package/adapters/codex/references/verify-state.md +98 -0
  164. package/adapters/codex/scripts/forge-bootstrap.py +23 -2
  165. package/adapters/codex/scripts/forge-root.sh +104 -21
  166. package/adapters/codex/scripts/forge-session.py +620 -6969
  167. package/adapters/codex/scripts/forge_session/__init__.py +11 -0
  168. package/adapters/codex/scripts/forge_session/_common.py +1566 -0
  169. package/adapters/codex/scripts/forge_session/_doctor_util.py +218 -0
  170. package/adapters/codex/scripts/forge_session/cli.py +1067 -0
  171. package/adapters/codex/scripts/forge_session/decisions.py +355 -0
  172. package/adapters/codex/scripts/forge_session/discover.py +472 -0
  173. package/adapters/codex/scripts/forge_session/doctor.py +2144 -0
  174. package/adapters/codex/scripts/forge_session/exit.py +1081 -0
  175. package/adapters/codex/scripts/forge_session/outcomes.py +574 -0
  176. package/adapters/codex/scripts/forge_session/routes.py +1162 -0
  177. package/adapters/codex/scripts/forge_session/state.py +1479 -0
  178. package/adapters/codex/scripts/forge_session/topology.py +443 -0
  179. package/adapters/codex/skills/forge/SKILL.md +40 -40
  180. package/adapters/codex/skills/forge/references/process-overview.md +10 -10
  181. package/adapters/codex/skills/forge/references/shared-conventions.md +189 -36
  182. package/adapters/codex/skills/forge/references/stage-exit-protocol.md +22 -7
  183. package/adapters/codex/skills/forge-0-epic/SKILL.md +15 -15
  184. package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +14 -10
  185. package/adapters/codex/skills/forge-0-epic/references/epic-manifest-subcommands.md +8 -3
  186. package/adapters/codex/skills/forge-0-epic/references/portable-root.md +20 -16
  187. package/adapters/codex/skills/forge-0-epic/references/shared-conventions.md +189 -36
  188. package/adapters/codex/skills/forge-0-epic/references/stage-exit-protocol.md +22 -7
  189. package/adapters/codex/skills/forge-1-prd/SKILL.md +15 -15
  190. package/adapters/codex/skills/forge-1-prd/references/shared-conventions.md +189 -36
  191. package/adapters/codex/skills/forge-1-prd/references/stage-exit-protocol.md +22 -7
  192. package/adapters/codex/skills/forge-2-tech/SKILL.md +15 -15
  193. package/adapters/codex/skills/forge-2-tech/references/shared-conventions.md +189 -36
  194. package/adapters/codex/skills/forge-2-tech/references/stack-resolution.md +1 -1
  195. package/adapters/codex/skills/forge-2-tech/references/stage-exit-protocol.md +22 -7
  196. package/adapters/codex/skills/forge-3-specs/SKILL.md +10 -10
  197. package/adapters/codex/skills/forge-3-specs/references/shared-conventions.md +189 -36
  198. package/adapters/codex/skills/forge-3-specs/references/stage-exit-protocol.md +22 -7
  199. package/adapters/codex/skills/forge-4-backlog/SKILL.md +19 -11
  200. package/adapters/codex/skills/forge-4-backlog/references/shared-conventions.md +189 -36
  201. package/adapters/codex/skills/forge-4-backlog/references/stage-exit-protocol.md +22 -7
  202. package/adapters/codex/skills/forge-4-backlog/references/verify-state.md +98 -0
  203. package/adapters/codex/skills/forge-5-loop/SKILL.md +48 -53
  204. package/adapters/codex/skills/forge-5-loop/references/agent-selection.md +5 -1
  205. package/adapters/codex/skills/forge-5-loop/references/preflight-and-self-heal.md +183 -0
  206. package/adapters/codex/skills/forge-5-loop/references/ralph-loop-contract.md +12 -8
  207. package/adapters/codex/skills/forge-5-loop/references/recovery-procedure.md +14 -4
  208. package/adapters/codex/skills/forge-5-loop/references/runner-contract.md +20 -10
  209. package/adapters/codex/skills/forge-5-loop/references/shared-conventions.md +189 -36
  210. package/adapters/codex/skills/forge-5-loop/references/stage-exit-protocol.md +22 -7
  211. package/adapters/codex/skills/forge-5-loop/references/verify-state.md +98 -0
  212. package/adapters/codex/skills/forge-6-docs/SKILL.md +27 -20
  213. package/adapters/codex/skills/forge-6-docs/references/shared-conventions.md +189 -36
  214. package/adapters/codex/skills/forge-6-docs/references/stage-exit-protocol.md +22 -7
  215. package/adapters/codex/skills/forge-6-docs/references/verify-state.md +98 -0
  216. package/adapters/codex/skills/forge-bootstrap/SKILL.md +23 -15
  217. package/adapters/codex/skills/forge-bootstrap/references/shared-conventions.md +572 -0
  218. package/adapters/codex/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +2 -2
  219. package/adapters/codex/skills/forge-fix/SKILL.md +27 -30
  220. package/adapters/codex/skills/forge-fix/references/select-outcome.md +138 -0
  221. package/adapters/codex/skills/forge-fix/references/shared-conventions.md +189 -36
  222. package/adapters/codex/skills/forge-fix/references/stage-exit-protocol.md +22 -7
  223. package/adapters/codex/skills/forge-guide/SKILL.md +84 -7
  224. package/adapters/codex/skills/forge-guide/references/forge-config-schema.json +7 -7
  225. package/adapters/codex/skills/forge-guide/references/preflight-and-self-heal.md +183 -0
  226. package/adapters/codex/skills/forge-guide/references/process-overview.md +10 -10
  227. package/adapters/codex/skills/forge-guide/references/ralph-loop-contract.md +12 -8
  228. package/adapters/codex/skills/forge-guide/references/shared-conventions.md +189 -36
  229. package/adapters/codex/skills/forge-guide/references/stack-resolution.md +1 -1
  230. package/adapters/codex/skills/forge-init/SKILL.md +62 -12
  231. package/adapters/codex/skills/forge-init/references/preflight-and-self-heal.md +183 -0
  232. package/adapters/codex/skills/forge-init/references/shared-conventions.md +572 -0
  233. package/adapters/codex/skills/forge-verify/SKILL.md +32 -33
  234. package/adapters/codex/skills/forge-verify/references/findings-template.md +8 -8
  235. package/adapters/codex/skills/forge-verify/references/select-outcome.md +138 -0
  236. package/adapters/codex/skills/forge-verify/references/shared-conventions.md +189 -36
  237. package/adapters/codex/skills/forge-verify/references/stage-exit-protocol.md +22 -7
  238. package/adapters/codex/skills/forge-verify/references/verification-checklists/epic.md +2 -2
  239. package/adapters/codex/skills/forge-verify/references/verifier-patterns/MEMORY.md +35 -0
  240. package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_absence_claims.md +36 -0
  241. package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
  242. package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
  243. package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
  244. package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
  245. package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
  246. package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
  247. package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
  248. package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
  249. package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
  250. package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_self_referential_control.md +26 -0
  251. package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
  252. package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
  253. package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
  254. package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_test_count_units.md +20 -0
  255. package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
  256. package/adapters/copilot/.feature-forge-bundle.json +1 -1
  257. package/adapters/copilot/agents/forge-researcher.md +1 -1
  258. package/adapters/copilot/agents/forge-verifier.md +17 -6
  259. package/adapters/copilot/references/forge-config-schema.json +7 -7
  260. package/adapters/copilot/references/portable-root.md +20 -16
  261. package/adapters/copilot/references/preflight-and-self-heal.md +183 -0
  262. package/adapters/copilot/references/process-overview.md +10 -10
  263. package/adapters/copilot/references/ralph-loop-contract.md +12 -8
  264. package/adapters/copilot/references/select-outcome.md +138 -0
  265. package/adapters/copilot/references/shared-conventions.md +189 -36
  266. package/adapters/copilot/references/skill-frontmatter.schema.json +3 -1
  267. package/adapters/copilot/references/stack-resolution.md +1 -1
  268. package/adapters/copilot/references/stage-exit-protocol.md +22 -7
  269. package/adapters/copilot/references/templates/root-hygiene/AGENTS.md +14 -0
  270. package/adapters/copilot/references/templates/root-hygiene/CLAUDE.md +14 -0
  271. package/adapters/copilot/references/templates/specs-hygiene/AGENTS.md +3 -1
  272. package/adapters/copilot/references/templates/specs-hygiene/CLAUDE.md +2 -1
  273. package/adapters/copilot/references/verifier-patterns/MEMORY.md +35 -0
  274. package/adapters/copilot/references/verifier-patterns/pattern_absence_claims.md +36 -0
  275. package/adapters/copilot/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
  276. package/adapters/copilot/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
  277. package/adapters/copilot/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
  278. package/adapters/copilot/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
  279. package/adapters/copilot/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
  280. package/adapters/copilot/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
  281. package/adapters/copilot/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
  282. package/adapters/copilot/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
  283. package/adapters/copilot/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
  284. package/adapters/copilot/references/verifier-patterns/pattern_self_referential_control.md +26 -0
  285. package/adapters/copilot/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
  286. package/adapters/copilot/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
  287. package/adapters/copilot/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
  288. package/adapters/copilot/references/verifier-patterns/pattern_test_count_units.md +20 -0
  289. package/adapters/copilot/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
  290. package/adapters/copilot/references/verify-state.md +98 -0
  291. package/adapters/copilot/scripts/forge-bootstrap.py +23 -2
  292. package/adapters/copilot/scripts/forge-root.sh +104 -21
  293. package/adapters/copilot/scripts/forge-session.py +620 -6969
  294. package/adapters/copilot/scripts/forge_session/__init__.py +11 -0
  295. package/adapters/copilot/scripts/forge_session/_common.py +1566 -0
  296. package/adapters/copilot/scripts/forge_session/_doctor_util.py +218 -0
  297. package/adapters/copilot/scripts/forge_session/cli.py +1067 -0
  298. package/adapters/copilot/scripts/forge_session/decisions.py +355 -0
  299. package/adapters/copilot/scripts/forge_session/discover.py +472 -0
  300. package/adapters/copilot/scripts/forge_session/doctor.py +2144 -0
  301. package/adapters/copilot/scripts/forge_session/exit.py +1081 -0
  302. package/adapters/copilot/scripts/forge_session/outcomes.py +574 -0
  303. package/adapters/copilot/scripts/forge_session/routes.py +1162 -0
  304. package/adapters/copilot/scripts/forge_session/state.py +1479 -0
  305. package/adapters/copilot/scripts/forge_session/topology.py +443 -0
  306. package/adapters/copilot/skills/forge/forge.md +40 -40
  307. package/adapters/copilot/skills/forge/references/process-overview.md +10 -10
  308. package/adapters/copilot/skills/forge/references/shared-conventions.md +189 -36
  309. package/adapters/copilot/skills/forge/references/stage-exit-protocol.md +22 -7
  310. package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +15 -15
  311. package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +14 -10
  312. package/adapters/copilot/skills/forge-0-epic/references/epic-manifest-subcommands.md +8 -3
  313. package/adapters/copilot/skills/forge-0-epic/references/portable-root.md +20 -16
  314. package/adapters/copilot/skills/forge-0-epic/references/shared-conventions.md +189 -36
  315. package/adapters/copilot/skills/forge-0-epic/references/stage-exit-protocol.md +22 -7
  316. package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +15 -15
  317. package/adapters/copilot/skills/forge-1-prd/references/shared-conventions.md +189 -36
  318. package/adapters/copilot/skills/forge-1-prd/references/stage-exit-protocol.md +22 -7
  319. package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +15 -15
  320. package/adapters/copilot/skills/forge-2-tech/references/shared-conventions.md +189 -36
  321. package/adapters/copilot/skills/forge-2-tech/references/stack-resolution.md +1 -1
  322. package/adapters/copilot/skills/forge-2-tech/references/stage-exit-protocol.md +22 -7
  323. package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +10 -10
  324. package/adapters/copilot/skills/forge-3-specs/references/shared-conventions.md +189 -36
  325. package/adapters/copilot/skills/forge-3-specs/references/stage-exit-protocol.md +22 -7
  326. package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +19 -11
  327. package/adapters/copilot/skills/forge-4-backlog/references/shared-conventions.md +189 -36
  328. package/adapters/copilot/skills/forge-4-backlog/references/stage-exit-protocol.md +22 -7
  329. package/adapters/copilot/skills/forge-4-backlog/references/verify-state.md +98 -0
  330. package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +48 -53
  331. package/adapters/copilot/skills/forge-5-loop/references/agent-selection.md +5 -1
  332. package/adapters/copilot/skills/forge-5-loop/references/preflight-and-self-heal.md +183 -0
  333. package/adapters/copilot/skills/forge-5-loop/references/ralph-loop-contract.md +12 -8
  334. package/adapters/copilot/skills/forge-5-loop/references/recovery-procedure.md +14 -4
  335. package/adapters/copilot/skills/forge-5-loop/references/runner-contract.md +20 -10
  336. package/adapters/copilot/skills/forge-5-loop/references/shared-conventions.md +189 -36
  337. package/adapters/copilot/skills/forge-5-loop/references/stage-exit-protocol.md +22 -7
  338. package/adapters/copilot/skills/forge-5-loop/references/verify-state.md +98 -0
  339. package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +27 -20
  340. package/adapters/copilot/skills/forge-6-docs/references/shared-conventions.md +189 -36
  341. package/adapters/copilot/skills/forge-6-docs/references/stage-exit-protocol.md +22 -7
  342. package/adapters/copilot/skills/forge-6-docs/references/verify-state.md +98 -0
  343. package/adapters/copilot/skills/forge-bootstrap/forge-bootstrap.md +23 -15
  344. package/adapters/copilot/skills/forge-bootstrap/references/shared-conventions.md +572 -0
  345. package/adapters/copilot/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +2 -2
  346. package/adapters/copilot/skills/forge-fix/forge-fix.md +27 -30
  347. package/adapters/copilot/skills/forge-fix/references/select-outcome.md +138 -0
  348. package/adapters/copilot/skills/forge-fix/references/shared-conventions.md +189 -36
  349. package/adapters/copilot/skills/forge-fix/references/stage-exit-protocol.md +22 -7
  350. package/adapters/copilot/skills/forge-guide/forge-guide.md +84 -7
  351. package/adapters/copilot/skills/forge-guide/references/forge-config-schema.json +7 -7
  352. package/adapters/copilot/skills/forge-guide/references/preflight-and-self-heal.md +183 -0
  353. package/adapters/copilot/skills/forge-guide/references/process-overview.md +10 -10
  354. package/adapters/copilot/skills/forge-guide/references/ralph-loop-contract.md +12 -8
  355. package/adapters/copilot/skills/forge-guide/references/shared-conventions.md +189 -36
  356. package/adapters/copilot/skills/forge-guide/references/stack-resolution.md +1 -1
  357. package/adapters/copilot/skills/forge-init/forge-init.md +62 -12
  358. package/adapters/copilot/skills/forge-init/references/preflight-and-self-heal.md +183 -0
  359. package/adapters/copilot/skills/forge-init/references/shared-conventions.md +572 -0
  360. package/adapters/copilot/skills/forge-verify/forge-verify.md +32 -33
  361. package/adapters/copilot/skills/forge-verify/references/findings-template.md +8 -8
  362. package/adapters/copilot/skills/forge-verify/references/select-outcome.md +138 -0
  363. package/adapters/copilot/skills/forge-verify/references/shared-conventions.md +189 -36
  364. package/adapters/copilot/skills/forge-verify/references/stage-exit-protocol.md +22 -7
  365. package/adapters/copilot/skills/forge-verify/references/verification-checklists/epic.md +2 -2
  366. package/adapters/copilot/skills/forge-verify/references/verifier-patterns/MEMORY.md +35 -0
  367. package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_absence_claims.md +36 -0
  368. package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
  369. package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
  370. package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
  371. package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
  372. package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
  373. package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
  374. package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
  375. package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
  376. package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
  377. package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_self_referential_control.md +26 -0
  378. package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
  379. package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
  380. package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
  381. package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_test_count_units.md +20 -0
  382. package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
  383. package/adapters/cursor/.feature-forge-bundle.json +1 -1
  384. package/adapters/cursor/agents/forge-researcher.mdc +1 -1
  385. package/adapters/cursor/agents/forge-verifier.mdc +17 -6
  386. package/adapters/cursor/references/forge-config-schema.json +7 -7
  387. package/adapters/cursor/references/portable-root.md +20 -16
  388. package/adapters/cursor/references/preflight-and-self-heal.md +183 -0
  389. package/adapters/cursor/references/process-overview.md +10 -10
  390. package/adapters/cursor/references/ralph-loop-contract.md +12 -8
  391. package/adapters/cursor/references/select-outcome.md +138 -0
  392. package/adapters/cursor/references/shared-conventions.md +189 -36
  393. package/adapters/cursor/references/skill-frontmatter.schema.json +3 -1
  394. package/adapters/cursor/references/stack-resolution.md +1 -1
  395. package/adapters/cursor/references/stage-exit-protocol.md +22 -7
  396. package/adapters/cursor/references/templates/root-hygiene/AGENTS.md +14 -0
  397. package/adapters/cursor/references/templates/root-hygiene/CLAUDE.md +14 -0
  398. package/adapters/cursor/references/templates/specs-hygiene/AGENTS.md +3 -1
  399. package/adapters/cursor/references/templates/specs-hygiene/CLAUDE.md +2 -1
  400. package/adapters/cursor/references/verifier-patterns/MEMORY.md +35 -0
  401. package/adapters/cursor/references/verifier-patterns/pattern_absence_claims.md +36 -0
  402. package/adapters/cursor/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
  403. package/adapters/cursor/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
  404. package/adapters/cursor/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
  405. package/adapters/cursor/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
  406. package/adapters/cursor/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
  407. package/adapters/cursor/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
  408. package/adapters/cursor/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
  409. package/adapters/cursor/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
  410. package/adapters/cursor/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
  411. package/adapters/cursor/references/verifier-patterns/pattern_self_referential_control.md +26 -0
  412. package/adapters/cursor/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
  413. package/adapters/cursor/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
  414. package/adapters/cursor/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
  415. package/adapters/cursor/references/verifier-patterns/pattern_test_count_units.md +20 -0
  416. package/adapters/cursor/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
  417. package/adapters/cursor/references/verify-state.md +98 -0
  418. package/adapters/cursor/scripts/forge-bootstrap.py +23 -2
  419. package/adapters/cursor/scripts/forge-root.sh +104 -21
  420. package/adapters/cursor/scripts/forge-session.py +620 -6969
  421. package/adapters/cursor/scripts/forge_session/__init__.py +11 -0
  422. package/adapters/cursor/scripts/forge_session/_common.py +1566 -0
  423. package/adapters/cursor/scripts/forge_session/_doctor_util.py +218 -0
  424. package/adapters/cursor/scripts/forge_session/cli.py +1067 -0
  425. package/adapters/cursor/scripts/forge_session/decisions.py +355 -0
  426. package/adapters/cursor/scripts/forge_session/discover.py +472 -0
  427. package/adapters/cursor/scripts/forge_session/doctor.py +2144 -0
  428. package/adapters/cursor/scripts/forge_session/exit.py +1081 -0
  429. package/adapters/cursor/scripts/forge_session/outcomes.py +574 -0
  430. package/adapters/cursor/scripts/forge_session/routes.py +1162 -0
  431. package/adapters/cursor/scripts/forge_session/state.py +1479 -0
  432. package/adapters/cursor/scripts/forge_session/topology.py +443 -0
  433. package/adapters/cursor/skills/forge/forge.mdc +40 -40
  434. package/adapters/cursor/skills/forge/references/process-overview.md +10 -10
  435. package/adapters/cursor/skills/forge/references/shared-conventions.md +189 -36
  436. package/adapters/cursor/skills/forge/references/stage-exit-protocol.md +22 -7
  437. package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +15 -15
  438. package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +14 -10
  439. package/adapters/cursor/skills/forge-0-epic/references/epic-manifest-subcommands.md +8 -3
  440. package/adapters/cursor/skills/forge-0-epic/references/portable-root.md +20 -16
  441. package/adapters/cursor/skills/forge-0-epic/references/shared-conventions.md +189 -36
  442. package/adapters/cursor/skills/forge-0-epic/references/stage-exit-protocol.md +22 -7
  443. package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +15 -15
  444. package/adapters/cursor/skills/forge-1-prd/references/shared-conventions.md +189 -36
  445. package/adapters/cursor/skills/forge-1-prd/references/stage-exit-protocol.md +22 -7
  446. package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +15 -15
  447. package/adapters/cursor/skills/forge-2-tech/references/shared-conventions.md +189 -36
  448. package/adapters/cursor/skills/forge-2-tech/references/stack-resolution.md +1 -1
  449. package/adapters/cursor/skills/forge-2-tech/references/stage-exit-protocol.md +22 -7
  450. package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +10 -10
  451. package/adapters/cursor/skills/forge-3-specs/references/shared-conventions.md +189 -36
  452. package/adapters/cursor/skills/forge-3-specs/references/stage-exit-protocol.md +22 -7
  453. package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +19 -11
  454. package/adapters/cursor/skills/forge-4-backlog/references/shared-conventions.md +189 -36
  455. package/adapters/cursor/skills/forge-4-backlog/references/stage-exit-protocol.md +22 -7
  456. package/adapters/cursor/skills/forge-4-backlog/references/verify-state.md +98 -0
  457. package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +48 -53
  458. package/adapters/cursor/skills/forge-5-loop/references/agent-selection.md +5 -1
  459. package/adapters/cursor/skills/forge-5-loop/references/preflight-and-self-heal.md +183 -0
  460. package/adapters/cursor/skills/forge-5-loop/references/ralph-loop-contract.md +12 -8
  461. package/adapters/cursor/skills/forge-5-loop/references/recovery-procedure.md +14 -4
  462. package/adapters/cursor/skills/forge-5-loop/references/runner-contract.md +20 -10
  463. package/adapters/cursor/skills/forge-5-loop/references/shared-conventions.md +189 -36
  464. package/adapters/cursor/skills/forge-5-loop/references/stage-exit-protocol.md +22 -7
  465. package/adapters/cursor/skills/forge-5-loop/references/verify-state.md +98 -0
  466. package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +27 -20
  467. package/adapters/cursor/skills/forge-6-docs/references/shared-conventions.md +189 -36
  468. package/adapters/cursor/skills/forge-6-docs/references/stage-exit-protocol.md +22 -7
  469. package/adapters/cursor/skills/forge-6-docs/references/verify-state.md +98 -0
  470. package/adapters/cursor/skills/forge-bootstrap/forge-bootstrap.mdc +23 -15
  471. package/adapters/cursor/skills/forge-bootstrap/references/shared-conventions.md +572 -0
  472. package/adapters/cursor/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +2 -2
  473. package/adapters/cursor/skills/forge-fix/forge-fix.mdc +27 -30
  474. package/adapters/cursor/skills/forge-fix/references/select-outcome.md +138 -0
  475. package/adapters/cursor/skills/forge-fix/references/shared-conventions.md +189 -36
  476. package/adapters/cursor/skills/forge-fix/references/stage-exit-protocol.md +22 -7
  477. package/adapters/cursor/skills/forge-guide/forge-guide.mdc +84 -7
  478. package/adapters/cursor/skills/forge-guide/references/forge-config-schema.json +7 -7
  479. package/adapters/cursor/skills/forge-guide/references/preflight-and-self-heal.md +183 -0
  480. package/adapters/cursor/skills/forge-guide/references/process-overview.md +10 -10
  481. package/adapters/cursor/skills/forge-guide/references/ralph-loop-contract.md +12 -8
  482. package/adapters/cursor/skills/forge-guide/references/shared-conventions.md +189 -36
  483. package/adapters/cursor/skills/forge-guide/references/stack-resolution.md +1 -1
  484. package/adapters/cursor/skills/forge-init/forge-init.mdc +62 -12
  485. package/adapters/cursor/skills/forge-init/references/preflight-and-self-heal.md +183 -0
  486. package/adapters/cursor/skills/forge-init/references/shared-conventions.md +572 -0
  487. package/adapters/cursor/skills/forge-verify/forge-verify.mdc +32 -33
  488. package/adapters/cursor/skills/forge-verify/references/findings-template.md +8 -8
  489. package/adapters/cursor/skills/forge-verify/references/select-outcome.md +138 -0
  490. package/adapters/cursor/skills/forge-verify/references/shared-conventions.md +189 -36
  491. package/adapters/cursor/skills/forge-verify/references/stage-exit-protocol.md +22 -7
  492. package/adapters/cursor/skills/forge-verify/references/verification-checklists/epic.md +2 -2
  493. package/adapters/cursor/skills/forge-verify/references/verifier-patterns/MEMORY.md +35 -0
  494. package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_absence_claims.md +36 -0
  495. package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
  496. package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
  497. package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
  498. package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
  499. package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
  500. package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
  501. package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
  502. package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
  503. package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
  504. package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_self_referential_control.md +26 -0
  505. package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
  506. package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
  507. package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
  508. package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_test_count_units.md +20 -0
  509. package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
  510. package/adapters/gemini/.feature-forge-bundle.json +1 -1
  511. package/adapters/gemini/agents/forge-researcher.md +1 -1
  512. package/adapters/gemini/agents/forge-verifier.md +17 -6
  513. package/adapters/gemini/gemini-extension.json +13 -13
  514. package/adapters/gemini/references/forge-config-schema.json +7 -7
  515. package/adapters/gemini/references/portable-root.md +20 -16
  516. package/adapters/gemini/references/preflight-and-self-heal.md +183 -0
  517. package/adapters/gemini/references/process-overview.md +10 -10
  518. package/adapters/gemini/references/ralph-loop-contract.md +12 -8
  519. package/adapters/gemini/references/select-outcome.md +138 -0
  520. package/adapters/gemini/references/shared-conventions.md +189 -36
  521. package/adapters/gemini/references/skill-frontmatter.schema.json +3 -1
  522. package/adapters/gemini/references/stack-resolution.md +1 -1
  523. package/adapters/gemini/references/stage-exit-protocol.md +22 -7
  524. package/adapters/gemini/references/templates/root-hygiene/AGENTS.md +14 -0
  525. package/adapters/gemini/references/templates/root-hygiene/CLAUDE.md +14 -0
  526. package/adapters/gemini/references/templates/specs-hygiene/AGENTS.md +3 -1
  527. package/adapters/gemini/references/templates/specs-hygiene/CLAUDE.md +2 -1
  528. package/adapters/gemini/references/verifier-patterns/MEMORY.md +35 -0
  529. package/adapters/gemini/references/verifier-patterns/pattern_absence_claims.md +36 -0
  530. package/adapters/gemini/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
  531. package/adapters/gemini/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
  532. package/adapters/gemini/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
  533. package/adapters/gemini/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
  534. package/adapters/gemini/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
  535. package/adapters/gemini/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
  536. package/adapters/gemini/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
  537. package/adapters/gemini/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
  538. package/adapters/gemini/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
  539. package/adapters/gemini/references/verifier-patterns/pattern_self_referential_control.md +26 -0
  540. package/adapters/gemini/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
  541. package/adapters/gemini/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
  542. package/adapters/gemini/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
  543. package/adapters/gemini/references/verifier-patterns/pattern_test_count_units.md +20 -0
  544. package/adapters/gemini/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
  545. package/adapters/gemini/references/verify-state.md +98 -0
  546. package/adapters/gemini/scripts/forge-bootstrap.py +23 -2
  547. package/adapters/gemini/scripts/forge-root.sh +104 -21
  548. package/adapters/gemini/scripts/forge-session.py +620 -6969
  549. package/adapters/gemini/scripts/forge_session/__init__.py +11 -0
  550. package/adapters/gemini/scripts/forge_session/_common.py +1566 -0
  551. package/adapters/gemini/scripts/forge_session/_doctor_util.py +218 -0
  552. package/adapters/gemini/scripts/forge_session/cli.py +1067 -0
  553. package/adapters/gemini/scripts/forge_session/decisions.py +355 -0
  554. package/adapters/gemini/scripts/forge_session/discover.py +472 -0
  555. package/adapters/gemini/scripts/forge_session/doctor.py +2144 -0
  556. package/adapters/gemini/scripts/forge_session/exit.py +1081 -0
  557. package/adapters/gemini/scripts/forge_session/outcomes.py +574 -0
  558. package/adapters/gemini/scripts/forge_session/routes.py +1162 -0
  559. package/adapters/gemini/scripts/forge_session/state.py +1479 -0
  560. package/adapters/gemini/scripts/forge_session/topology.py +443 -0
  561. package/adapters/gemini/skills/forge/forge.md +40 -40
  562. package/adapters/gemini/skills/forge/references/process-overview.md +10 -10
  563. package/adapters/gemini/skills/forge/references/shared-conventions.md +189 -36
  564. package/adapters/gemini/skills/forge/references/stage-exit-protocol.md +22 -7
  565. package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +15 -15
  566. package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +14 -10
  567. package/adapters/gemini/skills/forge-0-epic/references/epic-manifest-subcommands.md +8 -3
  568. package/adapters/gemini/skills/forge-0-epic/references/portable-root.md +20 -16
  569. package/adapters/gemini/skills/forge-0-epic/references/shared-conventions.md +189 -36
  570. package/adapters/gemini/skills/forge-0-epic/references/stage-exit-protocol.md +22 -7
  571. package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +15 -15
  572. package/adapters/gemini/skills/forge-1-prd/references/shared-conventions.md +189 -36
  573. package/adapters/gemini/skills/forge-1-prd/references/stage-exit-protocol.md +22 -7
  574. package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +15 -15
  575. package/adapters/gemini/skills/forge-2-tech/references/shared-conventions.md +189 -36
  576. package/adapters/gemini/skills/forge-2-tech/references/stack-resolution.md +1 -1
  577. package/adapters/gemini/skills/forge-2-tech/references/stage-exit-protocol.md +22 -7
  578. package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +10 -10
  579. package/adapters/gemini/skills/forge-3-specs/references/shared-conventions.md +189 -36
  580. package/adapters/gemini/skills/forge-3-specs/references/stage-exit-protocol.md +22 -7
  581. package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +19 -11
  582. package/adapters/gemini/skills/forge-4-backlog/references/shared-conventions.md +189 -36
  583. package/adapters/gemini/skills/forge-4-backlog/references/stage-exit-protocol.md +22 -7
  584. package/adapters/gemini/skills/forge-4-backlog/references/verify-state.md +98 -0
  585. package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +48 -53
  586. package/adapters/gemini/skills/forge-5-loop/references/agent-selection.md +5 -1
  587. package/adapters/gemini/skills/forge-5-loop/references/preflight-and-self-heal.md +183 -0
  588. package/adapters/gemini/skills/forge-5-loop/references/ralph-loop-contract.md +12 -8
  589. package/adapters/gemini/skills/forge-5-loop/references/recovery-procedure.md +14 -4
  590. package/adapters/gemini/skills/forge-5-loop/references/runner-contract.md +20 -10
  591. package/adapters/gemini/skills/forge-5-loop/references/shared-conventions.md +189 -36
  592. package/adapters/gemini/skills/forge-5-loop/references/stage-exit-protocol.md +22 -7
  593. package/adapters/gemini/skills/forge-5-loop/references/verify-state.md +98 -0
  594. package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +27 -20
  595. package/adapters/gemini/skills/forge-6-docs/references/shared-conventions.md +189 -36
  596. package/adapters/gemini/skills/forge-6-docs/references/stage-exit-protocol.md +22 -7
  597. package/adapters/gemini/skills/forge-6-docs/references/verify-state.md +98 -0
  598. package/adapters/gemini/skills/forge-bootstrap/forge-bootstrap.md +23 -15
  599. package/adapters/gemini/skills/forge-bootstrap/references/shared-conventions.md +572 -0
  600. package/adapters/gemini/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +2 -2
  601. package/adapters/gemini/skills/forge-fix/forge-fix.md +27 -30
  602. package/adapters/gemini/skills/forge-fix/references/select-outcome.md +138 -0
  603. package/adapters/gemini/skills/forge-fix/references/shared-conventions.md +189 -36
  604. package/adapters/gemini/skills/forge-fix/references/stage-exit-protocol.md +22 -7
  605. package/adapters/gemini/skills/forge-guide/forge-guide.md +84 -7
  606. package/adapters/gemini/skills/forge-guide/references/forge-config-schema.json +7 -7
  607. package/adapters/gemini/skills/forge-guide/references/preflight-and-self-heal.md +183 -0
  608. package/adapters/gemini/skills/forge-guide/references/process-overview.md +10 -10
  609. package/adapters/gemini/skills/forge-guide/references/ralph-loop-contract.md +12 -8
  610. package/adapters/gemini/skills/forge-guide/references/shared-conventions.md +189 -36
  611. package/adapters/gemini/skills/forge-guide/references/stack-resolution.md +1 -1
  612. package/adapters/gemini/skills/forge-init/forge-init.md +62 -12
  613. package/adapters/gemini/skills/forge-init/references/preflight-and-self-heal.md +183 -0
  614. package/adapters/gemini/skills/forge-init/references/shared-conventions.md +572 -0
  615. package/adapters/gemini/skills/forge-verify/forge-verify.md +32 -33
  616. package/adapters/gemini/skills/forge-verify/references/findings-template.md +8 -8
  617. package/adapters/gemini/skills/forge-verify/references/select-outcome.md +138 -0
  618. package/adapters/gemini/skills/forge-verify/references/shared-conventions.md +189 -36
  619. package/adapters/gemini/skills/forge-verify/references/stage-exit-protocol.md +22 -7
  620. package/adapters/gemini/skills/forge-verify/references/verification-checklists/epic.md +2 -2
  621. package/adapters/gemini/skills/forge-verify/references/verifier-patterns/MEMORY.md +35 -0
  622. package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_absence_claims.md +36 -0
  623. package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
  624. package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
  625. package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
  626. package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
  627. package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
  628. package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
  629. package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
  630. package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
  631. package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
  632. package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_self_referential_control.md +26 -0
  633. package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
  634. package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
  635. package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
  636. package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_test_count_units.md +20 -0
  637. package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
  638. package/adapters/pi/.feature-forge-bundle.json +1 -1
  639. package/adapters/pi/agents/forge-verifier.md +16 -5
  640. package/adapters/pi/extensions/forge-invocation-args/README.md +106 -0
  641. package/adapters/pi/extensions/forge-invocation-args/index.ts +31 -0
  642. package/adapters/pi/extensions/forge-invocation-args/wiring.ts +172 -0
  643. package/adapters/pi/extensions/forge-loop-supervisor/events.ts +90 -0
  644. package/adapters/pi/extensions/forge-loop-supervisor/index.ts +114 -0
  645. package/adapters/pi/extensions/forge-loop-supervisor/registry.ts +137 -0
  646. package/adapters/pi/extensions/forge-loop-supervisor/supervisor.ts +185 -0
  647. package/adapters/pi/extensions/forge-loop-supervisor/tailer.ts +137 -0
  648. package/adapters/pi/extensions/forge-loop-supervisor/types.ts +87 -0
  649. package/adapters/pi/extensions/forge-loop-supervisor/wiring.ts +423 -0
  650. package/adapters/pi/package.json +3 -1
  651. package/adapters/pi/references/forge-config-schema.json +5 -5
  652. package/adapters/pi/references/portable-root.md +20 -16
  653. package/adapters/pi/references/preflight-and-self-heal.md +183 -0
  654. package/adapters/pi/references/ralph-loop-contract.md +12 -8
  655. package/adapters/pi/references/select-outcome.md +138 -0
  656. package/adapters/pi/references/shared-conventions.md +176 -23
  657. package/adapters/pi/references/skill-frontmatter.schema.json +3 -1
  658. package/adapters/pi/references/stack-resolution.md +1 -1
  659. package/adapters/pi/references/stage-exit-protocol.md +19 -4
  660. package/adapters/pi/references/templates/root-hygiene/AGENTS.md +14 -0
  661. package/adapters/pi/references/templates/root-hygiene/CLAUDE.md +14 -0
  662. package/adapters/pi/references/templates/specs-hygiene/AGENTS.md +3 -1
  663. package/adapters/pi/references/templates/specs-hygiene/CLAUDE.md +2 -1
  664. package/adapters/pi/references/verifier-patterns/MEMORY.md +35 -0
  665. package/adapters/pi/references/verifier-patterns/pattern_absence_claims.md +36 -0
  666. package/adapters/pi/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
  667. package/adapters/pi/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
  668. package/adapters/pi/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
  669. package/adapters/pi/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
  670. package/adapters/pi/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
  671. package/adapters/pi/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
  672. package/adapters/pi/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
  673. package/adapters/pi/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
  674. package/adapters/pi/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
  675. package/adapters/pi/references/verifier-patterns/pattern_self_referential_control.md +26 -0
  676. package/adapters/pi/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
  677. package/adapters/pi/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
  678. package/adapters/pi/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
  679. package/adapters/pi/references/verifier-patterns/pattern_test_count_units.md +20 -0
  680. package/adapters/pi/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
  681. package/adapters/pi/references/verify-state.md +98 -0
  682. package/adapters/pi/scripts/forge-bootstrap.py +23 -2
  683. package/adapters/pi/scripts/forge-root.sh +104 -21
  684. package/adapters/pi/scripts/forge-session.py +620 -6969
  685. package/adapters/pi/scripts/forge_session/__init__.py +11 -0
  686. package/adapters/pi/scripts/forge_session/_common.py +1566 -0
  687. package/adapters/pi/scripts/forge_session/_doctor_util.py +218 -0
  688. package/adapters/pi/scripts/forge_session/cli.py +1067 -0
  689. package/adapters/pi/scripts/forge_session/decisions.py +355 -0
  690. package/adapters/pi/scripts/forge_session/discover.py +472 -0
  691. package/adapters/pi/scripts/forge_session/doctor.py +2144 -0
  692. package/adapters/pi/scripts/forge_session/exit.py +1081 -0
  693. package/adapters/pi/scripts/forge_session/outcomes.py +574 -0
  694. package/adapters/pi/scripts/forge_session/routes.py +1162 -0
  695. package/adapters/pi/scripts/forge_session/state.py +1479 -0
  696. package/adapters/pi/scripts/forge_session/topology.py +443 -0
  697. package/adapters/pi/skills/forge/SKILL.md +13 -9
  698. package/adapters/pi/skills/forge/references/shared-conventions.md +176 -23
  699. package/adapters/pi/skills/forge/references/stage-exit-protocol.md +19 -4
  700. package/adapters/pi/skills/forge-0-epic/SKILL.md +13 -9
  701. package/adapters/pi/skills/forge-0-epic/references/edit-mode.md +9 -5
  702. package/adapters/pi/skills/forge-0-epic/references/epic-manifest-subcommands.md +6 -1
  703. package/adapters/pi/skills/forge-0-epic/references/portable-root.md +20 -16
  704. package/adapters/pi/skills/forge-0-epic/references/shared-conventions.md +176 -23
  705. package/adapters/pi/skills/forge-0-epic/references/stage-exit-protocol.md +19 -4
  706. package/adapters/pi/skills/forge-1-prd/SKILL.md +13 -9
  707. package/adapters/pi/skills/forge-1-prd/references/shared-conventions.md +176 -23
  708. package/adapters/pi/skills/forge-1-prd/references/stage-exit-protocol.md +19 -4
  709. package/adapters/pi/skills/forge-2-tech/SKILL.md +12 -8
  710. package/adapters/pi/skills/forge-2-tech/references/shared-conventions.md +176 -23
  711. package/adapters/pi/skills/forge-2-tech/references/stack-resolution.md +1 -1
  712. package/adapters/pi/skills/forge-2-tech/references/stage-exit-protocol.md +19 -4
  713. package/adapters/pi/skills/forge-3-specs/SKILL.md +11 -7
  714. package/adapters/pi/skills/forge-3-specs/references/shared-conventions.md +176 -23
  715. package/adapters/pi/skills/forge-3-specs/references/stage-exit-protocol.md +19 -4
  716. package/adapters/pi/skills/forge-4-backlog/SKILL.md +22 -10
  717. package/adapters/pi/skills/forge-4-backlog/references/shared-conventions.md +176 -23
  718. package/adapters/pi/skills/forge-4-backlog/references/stage-exit-protocol.md +19 -4
  719. package/adapters/pi/skills/forge-4-backlog/references/verify-state.md +98 -0
  720. package/adapters/pi/skills/forge-5-loop/SKILL.md +47 -45
  721. package/adapters/pi/skills/forge-5-loop/references/agent-selection.md +4 -0
  722. package/adapters/pi/skills/forge-5-loop/references/preflight-and-self-heal.md +183 -0
  723. package/adapters/pi/skills/forge-5-loop/references/ralph-loop-contract.md +12 -8
  724. package/adapters/pi/skills/forge-5-loop/references/recovery-procedure.md +10 -0
  725. package/adapters/pi/skills/forge-5-loop/references/runner-contract.md +22 -9
  726. package/adapters/pi/skills/forge-5-loop/references/shared-conventions.md +176 -23
  727. package/adapters/pi/skills/forge-5-loop/references/stage-exit-protocol.md +19 -4
  728. package/adapters/pi/skills/forge-5-loop/references/verify-state.md +98 -0
  729. package/adapters/pi/skills/forge-6-docs/SKILL.md +27 -16
  730. package/adapters/pi/skills/forge-6-docs/references/shared-conventions.md +176 -23
  731. package/adapters/pi/skills/forge-6-docs/references/stage-exit-protocol.md +19 -4
  732. package/adapters/pi/skills/forge-6-docs/references/verify-state.md +98 -0
  733. package/adapters/pi/skills/forge-bootstrap/SKILL.md +24 -12
  734. package/adapters/pi/skills/forge-bootstrap/references/shared-conventions.md +572 -0
  735. package/adapters/pi/skills/forge-fix/SKILL.md +21 -20
  736. package/adapters/pi/skills/forge-fix/references/select-outcome.md +138 -0
  737. package/adapters/pi/skills/forge-fix/references/shared-conventions.md +176 -23
  738. package/adapters/pi/skills/forge-fix/references/stage-exit-protocol.md +19 -4
  739. package/adapters/pi/skills/forge-guide/SKILL.md +83 -2
  740. package/adapters/pi/skills/forge-guide/references/forge-config-schema.json +5 -5
  741. package/adapters/pi/skills/forge-guide/references/preflight-and-self-heal.md +183 -0
  742. package/adapters/pi/skills/forge-guide/references/ralph-loop-contract.md +12 -8
  743. package/adapters/pi/skills/forge-guide/references/shared-conventions.md +176 -23
  744. package/adapters/pi/skills/forge-guide/references/stack-resolution.md +1 -1
  745. package/adapters/pi/skills/forge-init/SKILL.md +63 -9
  746. package/adapters/pi/skills/forge-init/references/preflight-and-self-heal.md +183 -0
  747. package/adapters/pi/skills/forge-init/references/shared-conventions.md +572 -0
  748. package/adapters/pi/skills/forge-verify/SKILL.md +26 -23
  749. package/adapters/pi/skills/forge-verify/references/findings-template.md +7 -7
  750. package/adapters/pi/skills/forge-verify/references/select-outcome.md +138 -0
  751. package/adapters/pi/skills/forge-verify/references/shared-conventions.md +176 -23
  752. package/adapters/pi/skills/forge-verify/references/stage-exit-protocol.md +19 -4
  753. package/adapters/pi/skills/forge-verify/references/verification-checklists/epic.md +1 -1
  754. package/adapters/pi/skills/forge-verify/references/verifier-patterns/MEMORY.md +35 -0
  755. package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_absence_claims.md +36 -0
  756. package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
  757. package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
  758. package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
  759. package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
  760. package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
  761. package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
  762. package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
  763. package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
  764. package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
  765. package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_self_referential_control.md +26 -0
  766. package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
  767. package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
  768. package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
  769. package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_test_count_units.md +20 -0
  770. package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
  771. package/dist/manifest.d.ts +1 -1
  772. package/dist/rauf.d.ts +3 -3
  773. package/dist/rauf.js +2 -2
  774. package/dist/source.d.ts +5 -2
  775. package/dist/source.js +5 -2
  776. package/dist/types.d.ts +1 -1
  777. package/dist/types.js +1 -1
  778. package/package.json +9 -2
@@ -0,0 +1,1479 @@
1
+ """Write-side ``state-*`` verbs and their state-mutation machinery.
2
+
3
+ Every verb that MUTATES a feature's ``.pipeline-state.json`` (or an epic's
4
+ ``.epic-state.json``) lives here, carved out of the ``forge-session.py`` monolith
5
+ (#279 P4.1): ``state-enter``, ``state-artifact``, ``state-complete``, ``state-skip``,
6
+ ``state-branch``, ``state-note``, ``state-decision``, ``state-ecr`` and
7
+ ``state-verify``, plus the fail-closed resolvers, the atomic writer, the verify-entry
8
+ builder and the one-line human printers they share. All verb names, flags, exit
9
+ codes and JSON shapes are FROZEN; ``tests/test_state_verbs.py`` and
10
+ ``tests/test_auto_verify.py`` are the oracle.
11
+
12
+ This is a pure move. The write path keeps its atomic-write + commit discipline
13
+ (``_write_state``/``_commit_state`` from ``_common`` — a loop runner may crash
14
+ mid-write, so an interrupted write must never leave a partial state file). Shared
15
+ read-side primitives and the frozen domain constants come from ``_common`` (never
16
+ from the shim, which would be circular); the shim re-exports every symbol below so
17
+ the path-loaded test oracle resolves it unchanged.
18
+
19
+ 3.10 baseline, Google-style docstrings, stdlib only — matching the conventions of
20
+ the monolith it was carved out of.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import os
27
+ from pathlib import Path
28
+ from typing import Final
29
+
30
+ from forge_session._common import (
31
+ EPIC_STATE_FILENAME,
32
+ FULL_GIT_HASH_RE,
33
+ MANIFEST_FILENAME,
34
+ PIPELINE_STATE_FILENAME,
35
+ PRODUCTION_STAGES,
36
+ SAFE_NAME_RE,
37
+ VERIFY_RESULT_STATUSES,
38
+ VERIFY_STAGES,
39
+ VERIFY_TOKEN_BY_STAGE,
40
+ UsageError,
41
+ _commit_state,
42
+ _DONE_STATUS,
43
+ _now_iso,
44
+ _SKIP_PROTECTED_PRIOR,
45
+ _stage_version,
46
+ _verify_entry,
47
+ )
48
+
49
+ def _resolve_feature_dir_for_write(
50
+ specs_dir: Path, feature: str, epic: str | None
51
+ ) -> Path:
52
+ """Fail-closed feature dir for the ``state-*`` WRITERS.
53
+
54
+ ``_resolve_feature_dir`` is the reader's best-effort resolver: it returns the
55
+ flat ``{specsDir}/{feature}`` whenever that dir carries a state file, and
56
+ falls back to the flat literal on a multi-match. That tolerance was written
57
+ for ``stage-exit``, which is READ-ONLY — an unresolvable dir there just
58
+ downgrades to ``{}``. For a writer the same tolerance means a bare
59
+ ``--feature api`` mutates a standalone ``{specsDir}/api/`` while an epic
60
+ member ``{specsDir}/{epic}/api/`` of the same name is silently left behind:
61
+ cross-feature state corruption at exit 0.
62
+
63
+ So the write path mirrors ``epic-manifest.py resolve`` — the canonical
64
+ resolver that produced ``{resolvedFeatureDir}`` in the first place, and which
65
+ rejects an ambiguous name with a structured ``ambiguous:`` finding. A writer
66
+ must not be more permissive than that resolver: more than one candidate
67
+ carrying a state file, with no explicit ``--epic``, is a hard stop.
68
+
69
+ Args:
70
+ specs_dir: The configured specs directory (``--specs-dir``).
71
+ feature: The feature name (``--feature``).
72
+ epic: The owning epic name for a nested member, else None (``--epic``).
73
+
74
+ Returns:
75
+ The resolved feature directory. With ``--epic`` the nested path is taken
76
+ verbatim; otherwise the single candidate carrying a state file, or the
77
+ flat path when none does (the first-write case).
78
+
79
+ Raises:
80
+ UsageError: The bare name matches more than one directory carrying a
81
+ state file (→ exit 2, nothing written).
82
+ """
83
+ if epic:
84
+ return specs_dir / epic / feature
85
+ flat = specs_dir / feature
86
+ candidates = [flat] if (flat / PIPELINE_STATE_FILENAME).is_file() else []
87
+ if specs_dir.is_dir():
88
+ candidates.extend(
89
+ sorted(
90
+ p
91
+ for p in specs_dir.glob(f"*/{feature}")
92
+ if (p / PIPELINE_STATE_FILENAME).is_file()
93
+ )
94
+ )
95
+ if len(candidates) > 1:
96
+ listed = ", ".join(str(p) for p in candidates)
97
+ raise UsageError(
98
+ f"ambiguous feature {feature!r}: {len(candidates)} directories carry a "
99
+ f"state file ({listed}) — pass --epic <epic> to name the one to write. "
100
+ f"Refusing to guess; nothing was written."
101
+ )
102
+ return candidates[0] if candidates else flat
103
+
104
+
105
+ def _load_state_for_write(
106
+ specs_dir: Path, feature: str, epic: str | None
107
+ ) -> tuple[Path, dict]:
108
+ """Resolve a feature's state path and load its current state for mutation.
109
+
110
+ Resolves through the fail-closed `_resolve_feature_dir_for_write`, NOT the
111
+ reader's tolerant `_resolve_feature_dir`. Deliberately does NOT
112
+ reuse `_read_state`: that reader downgrades a *corrupt* file to ``{}`` because
113
+ the navigator's read-only sweep can safely treat it as not-started. A writer
114
+ that inherited it would atomically replace a corrupt-but-recoverable state
115
+ file with a near-empty one at exit 0. So: absent -> ``{}``; present but
116
+ unparseable -> refuse, leaving the file byte-intact.
117
+
118
+ The verbs never create a feature directory; an unknown ``--feature`` is a
119
+ usage error, not a silent create.
120
+
121
+ Args:
122
+ specs_dir: The configured specs directory (``--specs-dir``).
123
+ feature: The feature name (``--feature``).
124
+ epic: The owning epic name for a nested member, else None (``--epic``).
125
+
126
+ Returns:
127
+ A ``(state_path, state)`` tuple. ``state`` is a schema-shaped shell when
128
+ no state file exists yet (see the seeding below).
129
+
130
+ Raises:
131
+ UsageError: The bare ``feature`` name is ambiguous (more than one
132
+ candidate directory carries a state file and no ``--epic`` was
133
+ given), the feature directory does not exist, or the state file
134
+ exists but is not a JSON object (→ exit 2).
135
+ """
136
+ state_dir = _resolve_feature_dir_for_write(specs_dir, feature, epic)
137
+ if not state_dir.is_dir():
138
+ raise UsageError(
139
+ f"no feature directory at {state_dir} — check --feature "
140
+ f"(and --epic for a nested epic member)"
141
+ )
142
+ state_path = state_dir / PIPELINE_STATE_FILENAME
143
+ if state_path.exists():
144
+ try:
145
+ state = json.loads(state_path.read_text(encoding="utf-8"))
146
+ except json.JSONDecodeError as exc:
147
+ raise UsageError(
148
+ f"{state_path} exists but is not valid JSON ({exc}); refusing to "
149
+ f"overwrite it. Fix or move the file, then re-run."
150
+ ) from exc
151
+ if not isinstance(state, dict):
152
+ raise UsageError(
153
+ f"{state_path} is not a JSON object; refusing to overwrite it."
154
+ )
155
+ else:
156
+ state = {}
157
+
158
+ # Seed the schema-required top-level fields for EVERY verb, not just
159
+ # state-enter. Branch Setup fires state-branch before the entry stamp
160
+ # (references/shared-conventions.md), so without this a first-write
161
+ # state-branch would persist {"branch": ..., "updatedAt": ...} — missing
162
+ # every required field — at exit 0. setdefault keeps existing state as-is.
163
+ # (`updatedAt`, the sixth required field, is stamped by _commit_state.)
164
+ state.setdefault("feature", feature)
165
+ state.setdefault("createdAt", _now_iso())
166
+ state.setdefault("pipelineStatus", "active")
167
+ state.setdefault("stages", {})
168
+ state.setdefault("currentStage", PRODUCTION_STAGES[0])
169
+ return state_path, state
170
+
171
+
172
+ def _assert_safe_name(name: str, label: str) -> None:
173
+ """Reject a name that could steer a write outside ``{specsDir}/{name}``.
174
+
175
+ Args:
176
+ name: The bare name supplied on the command line.
177
+ label: The flag to name in the error (e.g. ``--feature``).
178
+
179
+ Raises:
180
+ UsageError: Empty, absolute, separator-bearing, ``..``, or not a single
181
+ kebab-case token (→ exit 2, nothing read or written).
182
+ """
183
+ if (
184
+ not name
185
+ or name == ".."
186
+ or "/" in name
187
+ or "\\" in name
188
+ or os.path.isabs(name)
189
+ or not SAFE_NAME_RE.match(name)
190
+ ):
191
+ raise UsageError(f"unsafe name {name!r} for {label}")
192
+
193
+
194
+ def _load_epic_state_for_write(
195
+ specs_dir: Path, epic_name: str, epic: str | None
196
+ ) -> tuple[Path, dict, int]:
197
+ """Resolve an EPIC's ``.epic-state.json`` and its manifest revision, for mutation.
198
+
199
+ The epic counterpart of ``_load_state_for_write``, and deliberately NOT a
200
+ variant of it: epic verification is epic-scoped and must never resolve, read,
201
+ create, or write a member's ``.pipeline-state.json`` (REQ-SEC-01). There is no
202
+ fallback in either direction — an epic whose manifest is missing or whose
203
+ identity disagrees is an error, not a feature lookup.
204
+
205
+ Resolution is strict where the member resolver is tolerant: the name must be a
206
+ safe single token, the joined path must stay inside ``specs_dir`` after symlink
207
+ resolution, ``epic-manifest.json`` must exist, and the manifest's own ``epic``
208
+ value must equal ``epic_name``. The revision comes from the manifest, which is
209
+ the canonical artifact version for epic freshness — never a member's
210
+ production-stage version. A legacy manifest with no ``revision`` is
211
+ presented as logical ``1`` here, matching ``epic-manifest.py::load_manifest``,
212
+ and its bytes are not rewritten.
213
+
214
+ Args:
215
+ specs_dir: The configured specs directory (``--specs-dir``).
216
+ epic_name: The epic name — what ``--feature`` carries for this stage.
217
+ epic: The ``--epic`` value, which must be absent or equal to ``epic_name``.
218
+
219
+ Returns:
220
+ A ``(state_path, state, revision)`` tuple. ``state`` is the lazily created
221
+ minimal shell (``epic`` + ``stages``) when no epic state exists yet.
222
+
223
+ Raises:
224
+ UsageError: Conflicting ``--feature``/``--epic``, unsafe name, containment
225
+ escape, missing/unparseable/non-object/identity-mismatched manifest,
226
+ invalid manifest revision, or an unparseable/non-object epic state or
227
+ ``stages`` value (→ exit 2, nothing written).
228
+ """
229
+ if epic is not None and epic != epic_name:
230
+ raise UsageError(
231
+ f"--stage forge-0-epic writes epic-scoped state, so --feature names the "
232
+ f"epic: --feature {epic_name!r} and --epic {epic!r} disagree. Drop --epic "
233
+ f"or make it match."
234
+ )
235
+ _assert_safe_name(epic_name, "--feature")
236
+ base_real = specs_dir.resolve()
237
+ epic_dir = (base_real / epic_name).resolve()
238
+ if epic_dir != base_real and base_real not in epic_dir.parents:
239
+ raise UsageError(
240
+ f"resolved epic path escapes the specs dir: {specs_dir / epic_name}"
241
+ )
242
+
243
+ manifest_path = epic_dir / MANIFEST_FILENAME
244
+ if not manifest_path.is_file():
245
+ raise UsageError(
246
+ f"no epic manifest at {manifest_path} — --stage forge-0-epic verifies an "
247
+ f"epic, and {epic_name!r} is not one. Nothing was written."
248
+ )
249
+ try:
250
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
251
+ except json.JSONDecodeError as exc:
252
+ raise UsageError(f"{manifest_path} is not valid JSON ({exc})") from exc
253
+ if not isinstance(manifest, dict):
254
+ raise UsageError(f"{manifest_path} is not a JSON object")
255
+ if manifest.get("epic") != epic_name:
256
+ raise UsageError(
257
+ f"{manifest_path} declares epic {manifest.get('epic')!r}, not "
258
+ f"{epic_name!r}; refusing to write verification state for a mismatched "
259
+ f"epic identity"
260
+ )
261
+ revision = _require_positive_int(
262
+ manifest.get("revision", 1), f"{epic_name}/{MANIFEST_FILENAME} revision"
263
+ )
264
+
265
+ state_path = epic_dir / EPIC_STATE_FILENAME
266
+ if state_path.exists():
267
+ try:
268
+ state = json.loads(state_path.read_text(encoding="utf-8"))
269
+ except json.JSONDecodeError as exc:
270
+ raise UsageError(
271
+ f"{state_path} exists but is not valid JSON ({exc}); refusing to "
272
+ f"overwrite it. Fix or move the file, then re-run."
273
+ ) from exc
274
+ if not isinstance(state, dict):
275
+ raise UsageError(
276
+ f"{state_path} is not a JSON object; refusing to overwrite it."
277
+ )
278
+ recorded = state.get("epic")
279
+ if recorded is not None and recorded != epic_name:
280
+ raise UsageError(
281
+ f"{state_path} records epic {recorded!r}, not {epic_name!r}; "
282
+ f"refusing to overwrite it."
283
+ )
284
+ stages = state.get("stages")
285
+ if stages is not None and not isinstance(stages, dict):
286
+ raise UsageError(
287
+ f"{state_path} has a non-object 'stages' value ({type(stages).__name__}); "
288
+ f"refusing to overwrite it."
289
+ )
290
+ else:
291
+ state = {}
292
+ # Seed the minimal state shape in its documented key order. ``updatedAt`` is
293
+ # a placeholder: every caller stamps it through ``_commit_state`` immediately
294
+ # before the single atomic replacement, so the null never reaches disk.
295
+ state.setdefault("epic", epic_name)
296
+ state.setdefault("updatedAt", None)
297
+ state.setdefault("stages", {})
298
+ return state_path, state, revision
299
+
300
+
301
+ def _stage_entry(state: dict, stage: str) -> dict:
302
+ """Return (creating if absent) the mutable ``stages.{stage}`` sub-object.
303
+
304
+ Bootstraps ``state["stages"]`` and ``state["stages"][stage]`` when missing, so
305
+ a verb can write into a brand-new state (``{}``), and returns the stage dict
306
+ for in-place mutation. The bootstrap seeds ``{"status": "pending"}`` rather
307
+ than ``{}`` because ``stageEntry`` declares ``required: ["status"]`` — an entry
308
+ created by state-artifact (which sets only ``artifacts``) would otherwise be
309
+ schema-invalid at exit 0.
310
+
311
+ Args:
312
+ state: The full state dict (mutated in place).
313
+ stage: A stage id from ``STATE_VERB_STAGES`` (e.g. ``"forge-1-prd"``).
314
+
315
+ Returns:
316
+ The mutable ``stages.{stage}`` dict.
317
+ """
318
+ stages = state.setdefault("stages", {})
319
+ return stages.setdefault(stage, {"status": "pending"})
320
+
321
+
322
+ # --------------------------------------------------------------------------- #
323
+ # State-write verbs
324
+ # --------------------------------------------------------------------------- #
325
+
326
+
327
+ def cmd_state_enter(feature: str, stage: str, specs_dir: Path, epic: str | None) -> dict:
328
+ """Apply the Entry Stamp: mark ``stage`` in-progress and set ``currentStage``.
329
+
330
+ Idempotent on re-entry within the same run: re-stamping an already
331
+ in-progress stage simply refreshes ``startedAt``/``updatedAt``. The
332
+ interactive resume-vs-restart decision stays the skill's — the verb never
333
+ prompts. The write is left uncommitted; the stage's existing exit commit
334
+ stages it later.
335
+
336
+ Args:
337
+ feature: Feature name.
338
+ stage: The stage being entered (a ``STATE_VERB_STAGES`` id).
339
+ specs_dir: Specs directory.
340
+ epic: Owning epic name, or None.
341
+
342
+ Returns:
343
+ The mutated state dict (for the --json echo).
344
+
345
+ Raises:
346
+ UsageError: Unknown feature directory, unparseable state file, or a
347
+ failed atomic write (→ exit 2).
348
+ """
349
+ state_path, state = _load_state_for_write(specs_dir, feature, epic)
350
+ entry = _stage_entry(state, stage)
351
+ entry["status"] = "in-progress"
352
+ entry["startedAt"] = _now_iso()
353
+ state["currentStage"] = stage
354
+ return _commit_state(state_path, state)
355
+
356
+
357
+ def cmd_state_artifact(
358
+ feature: str, stage: str, paths: list[str], specs_dir: Path, epic: str | None
359
+ ) -> dict:
360
+ """Append each path in ``paths`` to ``stages.{stage}.artifacts``, de-duplicating.
361
+
362
+ Idempotent: an already-tracked path is a no-op (no duplicate append), so a
363
+ resumed run that re-records files it wrote earlier does not bloat the array.
364
+ ``updatedAt`` is refreshed even on the all-duplicates branch, keeping "state
365
+ was touched" honest. The verb does NOT stat the file — it records the path
366
+ the skill asserts it wrote.
367
+
368
+ Args:
369
+ feature: Feature name.
370
+ stage: The producing stage id.
371
+ paths: Artifact paths relative to the feature dir (repeatable ``--path``).
372
+ specs_dir: Specs directory.
373
+ epic: Owning epic name, or None.
374
+
375
+ Returns:
376
+ The mutated state dict (for the --json echo).
377
+
378
+ Raises:
379
+ UsageError: A ``--path`` that is empty, absolute, ``..``-bearing,
380
+ control-character-bearing, or escaping the feature directory; an
381
+ unknown feature directory, an unparseable state file, or a failed
382
+ atomic write (→ exit 2).
383
+ """
384
+ state_path, state = _load_state_for_write(specs_dir, feature, epic)
385
+ # Containment is checked against the resolved feature dir, which only the load
386
+ # produces; every path is validated before any of them is appended, so a
387
+ # rejected value in a repeated --path list leaves the file untouched.
388
+ target_dir = state_path.parent
389
+ for path in paths:
390
+ _validated_findings_file(path, target_dir, label="--path")
391
+ entry = _stage_entry(state, stage)
392
+ artifacts = entry.setdefault("artifacts", [])
393
+ for path in paths:
394
+ if path not in artifacts:
395
+ artifacts.append(path)
396
+ return _commit_state(state_path, state)
397
+
398
+
399
+ def _parse_based_on(pairs: list[str]) -> dict[str, int]:
400
+ """Parse ``--based-on STAGE=N`` tokens into a ``{stageId: int}`` map.
401
+
402
+ Args:
403
+ pairs: Raw ``STAGE=N`` strings from repeated ``--based-on`` flags.
404
+
405
+ Returns:
406
+ A ``{stageId: version}`` dict (empty when no pairs were given — the
407
+ forge-1-prd case, which records ``basedOnVersions == {}``).
408
+
409
+ Raises:
410
+ UsageError: If a token lacks ``=`` or its value is not an integer
411
+ (→ exit 2).
412
+ """
413
+ out: dict[str, int] = {}
414
+ for token in pairs:
415
+ if "=" not in token:
416
+ raise UsageError(f"--based-on expects STAGE=N, got: {token!r}")
417
+ stage_id, _, raw = token.partition("=")
418
+ try:
419
+ out[stage_id] = int(raw)
420
+ except ValueError as exc:
421
+ raise UsageError(f"--based-on version must be an integer: {token!r}") from exc
422
+ return out
423
+
424
+
425
+ #: Stages the staleness cascade may mark stale (downstream authored artifacts).
426
+ #: The scope is tech..docs, matching the pre-R4 canon this cascade replaces —
427
+ #: forge-1-prd L134 named `forge-2-tech` FIRST among the stages a PRD revision
428
+ #: invalidates, and the tech spec is a PRD revision's most direct dependent.
429
+ #: forge-1-prd is never marked stale by a later completion (nothing downstream
430
+ #: feeds back into it). Keyed off this map, NOT off PRODUCTION_STAGES ordering —
431
+ #: the two are not interchangeable (a positional slice from the completing stage
432
+ #: would also break on forge-0-epic, which is a valid --stage but not a
433
+ #: PRODUCTION_STAGES member).
434
+ _CASCADE_TARGETS: Final[tuple[str, ...]] = (
435
+ "forge-2-tech",
436
+ "forge-3-specs",
437
+ "forge-4-backlog",
438
+ "forge-5-loop",
439
+ "forge-6-docs",
440
+ )
441
+
442
+
443
+ def _cascade_staleness(state: dict, completed_stage: str, new_version: int) -> list[str]:
444
+ """Mark downstream stages ``stale`` when they were built on an OLDER version.
445
+
446
+ Deterministic replacement for the model-prose rule in each stage's completion
447
+ step ("if any downstream stage has basedOnVersions referencing an older
448
+ version, set its status to stale"). For every downstream target (tech..docs),
449
+ if its recorded ``basedOnVersions[completed_stage]`` is an integer strictly
450
+ less than ``new_version`` AND the stage is currently ``complete``, flip it to
451
+ ``stale``. A downstream stage that never referenced this upstream, or already
452
+ references the new version, is untouched. A ``pending``/``in-progress``/
453
+ already-``stale`` downstream stage is not re-flipped — only a ``complete``
454
+ artifact can go stale.
455
+
456
+ Args:
457
+ state: The full state dict (mutated in place).
458
+ completed_stage: The stage that just completed (e.g. "forge-1-prd").
459
+ new_version: That stage's new version.
460
+
461
+ Returns:
462
+ The list of stage ids newly marked stale (for the --json echo / printer).
463
+ """
464
+ stages = state.get("stages", {})
465
+ newly_stale: list[str] = []
466
+ for target in _CASCADE_TARGETS:
467
+ if target == completed_stage:
468
+ continue
469
+ entry = stages.get(target)
470
+ if not isinstance(entry, dict) or entry.get("status") != "complete":
471
+ continue
472
+ based_on = entry.get("basedOnVersions")
473
+ if not isinstance(based_on, dict):
474
+ continue
475
+ recorded = based_on.get(completed_stage)
476
+ if isinstance(recorded, int) and not isinstance(recorded, bool) and recorded < new_version:
477
+ entry["status"] = "stale"
478
+ newly_stale.append(target)
479
+ return newly_stale
480
+
481
+
482
+ def cmd_state_complete(
483
+ feature: str,
484
+ stage: str,
485
+ version: int,
486
+ based_on: dict[str, int],
487
+ artifacts: list[str],
488
+ commit_hash: str | None,
489
+ specs_dir: Path,
490
+ epic: str | None,
491
+ status: str | None = None,
492
+ preserve_commit_hash: bool = False,
493
+ resumable: bool = False,
494
+ ) -> dict:
495
+ """Mark ``stage`` complete, bump version, record provenance, cascade staleness.
496
+
497
+ Three branches, in precedence order:
498
+
499
+ 1. ``commit_hash`` given — Commit 2 of the two-commit Git Commit Protocol.
500
+ Sets ONLY ``commitHash``, leaving status/version/artifacts intact. Guarded
501
+ on the stage already being ``complete``, so a typo'd ``--stage`` cannot
502
+ write a lone ``{"commitHash": …}`` entry (which would violate
503
+ ``stageEntry``'s ``required: ["status"]``) at exit 0. The value must be a
504
+ full 40-hex object hash (REQ-STATE-01), checked before anything is loaded.
505
+ 2. ``resumable`` — the failed-Commit-1 revert (`references/shared-conventions.md`
506
+ L245). Records ONLY ``status = "in-progress"`` plus the ``updatedAt``
507
+ refresh: no completedAt, no version bump, no basedOnVersions/artifacts
508
+ write, no commitHash reset, no cascade. The frozen contract is "leave state
509
+ as in-progress so the stage can be resumed"; stamping a completion, bumping
510
+ the version, or cascading staleness off a commit that never landed are all
511
+ behavioral changes.
512
+ 3. Otherwise — the completion write: status, completedAt, version,
513
+ basedOnVersions, artifacts, ``commitHash = None`` (Commit 1) unless
514
+ ``preserve_commit_hash``, then the downstream staleness cascade.
515
+
516
+ Branch 2 is gated on ``resumable``, NOT on ``status == "in-progress"``:
517
+ forge-5-loop's PARTIAL completion also passes ``--status in-progress`` but is a
518
+ real completion-with-artifacts, so it takes branch 3 and keeps its
519
+ completedAt/version/basedOnVersions/artifacts. Only ``status`` differs between
520
+ ``--status complete`` and a bare ``--status in-progress``. Conflating the two
521
+ would silently discard the ``--based-on`` item 013 passes on that call.
522
+
523
+ Args:
524
+ feature: Feature name.
525
+ stage: The completing stage id.
526
+ version: The stage's new version.
527
+ based_on: Parsed ``{upstreamStage: version}`` provenance map.
528
+ artifacts: Final canonical artifact path list for this stage.
529
+ commit_hash: If given, record it as the stage's commitHash (Commit 2);
530
+ else set commitHash to None (Commit 1). Full 40-hex only on a new
531
+ write — an abbreviation is rejected rather than expanded.
532
+ specs_dir: Specs directory.
533
+ epic: Owning epic name, or None.
534
+ status: Terminal status to record — "complete" (the default when the flag
535
+ is absent) or "in-progress" for a partial forge-5-loop run. ``None``
536
+ means "not passed".
537
+ preserve_commit_hash: Skip the ``commitHash = None`` reset, for the Git
538
+ Commit Protocol's "Nothing to commit" branch (L248).
539
+ resumable: Failed-Commit-1 revert (L245). Record only the status; implies
540
+ ``--status in-progress``.
541
+
542
+ Returns:
543
+ The mutated state dict, plus a synthetic ``_cascadedStale`` key that is
544
+ surfaced in the --json echo / printer but NEVER written to disk.
545
+
546
+ Raises:
547
+ UsageError: Contradictory ``--resumable --status complete``, a
548
+ ``--version`` below 1, a short or non-hex ``--commit-hash``, a
549
+ ``--commit-hash`` follow-up against a stage that is not complete, an
550
+ unknown feature directory, an unparseable state file, or a failed
551
+ atomic write (→ exit 2).
552
+ """
553
+ if resumable and status == "complete":
554
+ raise UsageError(
555
+ "--resumable implies --status in-progress; do not pass --status complete"
556
+ )
557
+ # The write path must not accept a version the read path refuses; checked before
558
+ # the state file is loaded for mutation, so a rejection touches nothing.
559
+ _require_positive_int(version, "--version")
560
+ if commit_hash is not None:
561
+ # Branch 1's first act: full 40-hex only, validated BEFORE the
562
+ # state file is loaded for mutation and long before _commit_state. Legacy
563
+ # short hashes already recorded in state keep loading unmigrated.
564
+ _assert_full_commit_hash(commit_hash)
565
+ state_path, state = _load_state_for_write(specs_dir, feature, epic)
566
+ entry = _stage_entry(state, stage)
567
+ cascaded: list[str] = []
568
+ if commit_hash is not None:
569
+ # Commit-2 follow-up: record the real hash, leave everything else intact.
570
+ actual = entry.get("status")
571
+ if actual != _DONE_STATUS:
572
+ raise UsageError(
573
+ f"--commit-hash requires {stage} to be complete (status: {actual!r}); "
574
+ "run state-complete without --commit-hash first"
575
+ )
576
+ entry["commitHash"] = commit_hash
577
+ elif resumable:
578
+ # Failed-Commit-1 revert (L245): record ONLY the status. See the note above
579
+ # on why this is gated on --resumable rather than on the status value.
580
+ entry["status"] = "in-progress"
581
+ else:
582
+ entry["status"] = status or _DONE_STATUS # "complete" | "in-progress" (partial)
583
+ entry["completedAt"] = _now_iso()
584
+ entry["version"] = version
585
+ entry["basedOnVersions"] = based_on
586
+ entry["artifacts"] = artifacts
587
+ if not preserve_commit_hash:
588
+ entry["commitHash"] = None # Commit 1 of the Commit Protocol
589
+ cascaded = _cascade_staleness(state, stage, version)
590
+ result = _commit_state(state_path, state)
591
+ # Surface the cascade result for the caller without persisting it in state:
592
+ # _commit_state already wrote the real dict, and `echo` is a copy.
593
+ echo = dict(result)
594
+ echo["_cascadedStale"] = cascaded
595
+ return echo
596
+
597
+
598
+ def cmd_state_skip(feature: str, stage: str, specs_dir: Path, epic: str | None) -> dict:
599
+ """Record a deliberate skip of the documentation stage (#197).
600
+
601
+ Writes a REPLACEMENT ``stages.forge-6-docs`` entry ``{"status": "skipped",
602
+ "skippedAt": …, "commitHash": null}`` — the honest terminal for a feature
603
+ that ships without architecture docs. ``skipped`` counts as done for
604
+ next-stage selection (``_DONE_STATUSES``), so the pipeline reads
605
+ ``complete: true`` / ``nextStage: null`` without any stage claiming
606
+ artifacts it never produced.
607
+
608
+ Scoped to ``forge-6-docs`` on purpose: a skipped PRD or specs stage is a
609
+ different and much worse proposition, so both the CLI (``choices``) and this
610
+ callable refuse any other stage.
611
+
612
+ The skip must not DESTROY a record of docs that exist: a prior entry whose
613
+ ``artifacts`` list is non-empty is refused. A prior ``complete`` entry with
614
+ no recorded artifacts is exactly the dishonest workaround this verb replaces
615
+ (``state-complete`` with no ``--artifact``), so it may be corrected to
616
+ ``skipped`` — that is the sanctioned migration path for such state.
617
+
618
+ Args:
619
+ feature: Feature name.
620
+ stage: Must be ``"forge-6-docs"`` (kept explicit so the scoping shows up
621
+ in every call site).
622
+ specs_dir: Specs directory.
623
+ epic: Owning epic name, or None.
624
+
625
+ Returns:
626
+ The mutated state dict (for the --json echo).
627
+
628
+ Raises:
629
+ UsageError: A stage other than forge-6-docs, a prior entry recording
630
+ artifacts, an unknown feature directory, an unparseable state file,
631
+ or a failed atomic write (→ exit 2).
632
+ """
633
+ if stage != "forge-6-docs":
634
+ raise UsageError(
635
+ f"state-skip is scoped to forge-6-docs; a skipped {stage} is not a "
636
+ "representable pipeline state"
637
+ )
638
+ state_path, state = _load_state_for_write(specs_dir, feature, epic)
639
+ prior = state.get("stages", {}).get(stage)
640
+ if isinstance(prior, dict) and prior.get("artifacts"):
641
+ raise UsageError(
642
+ f"{stage} already records {len(prior['artifacts'])} artifact(s) "
643
+ f"(status: {prior.get('status')!r}); skipping now would erase the "
644
+ "record that docs exist. Re-run forge-6-docs to refresh them instead."
645
+ )
646
+ state.setdefault("stages", {})[stage] = {
647
+ "status": "skipped",
648
+ "skippedAt": _now_iso(),
649
+ "commitHash": None,
650
+ }
651
+ return _commit_state(state_path, state)
652
+
653
+
654
+ def cmd_state_branch(feature: str, branch: str, specs_dir: Path, epic: str | None) -> dict:
655
+ """Set the top-level ``branch`` field.
656
+
657
+ Records the branch resolved by Branch Setup / Branch Reconciliation. The verb
658
+ only writes the field; the interactive prompts and the visible one-line
659
+ reconciliation note stay unchanged skill prose.
660
+
661
+ Branch Setup fires before the Entry Stamp, so this verb can legitimately be
662
+ the FIRST thing to touch a feature's state file — `_load_state_for_write`'s
663
+ field seeding is what keeps that first write schema-valid.
664
+
665
+ Args:
666
+ feature: Feature name.
667
+ branch: The branch name to record.
668
+ specs_dir: Specs directory.
669
+ epic: Owning epic name, or None.
670
+
671
+ Returns:
672
+ The mutated state dict (for the --json echo).
673
+
674
+ Raises:
675
+ UsageError: Unknown feature directory, unparseable state file, or a
676
+ failed atomic write (→ exit 2).
677
+ """
678
+ state_path, state = _load_state_for_write(specs_dir, feature, epic)
679
+ state["branch"] = branch
680
+ return _commit_state(state_path, state)
681
+
682
+
683
+ def cmd_state_note(feature: str, note: str, specs_dir: Path, epic: str | None) -> dict:
684
+ """Set the top-level ``notes`` field to ``note``.
685
+
686
+ Overwrites any existing note (the field is a single free-text string, not an
687
+ append log — matching the schema's ``notes: string``). The skill's "offer a
688
+ note — don't force one" statement is unchanged; this verb runs only when the
689
+ user volunteered text.
690
+
691
+ Args:
692
+ feature: Feature name.
693
+ note: The note text.
694
+ specs_dir: Specs directory.
695
+ epic: Owning epic name, or None.
696
+
697
+ Returns:
698
+ The mutated state dict (for the --json echo).
699
+
700
+ Raises:
701
+ UsageError: Unknown feature directory, unparseable state file, or a
702
+ failed atomic write (→ exit 2).
703
+ """
704
+ state_path, state = _load_state_for_write(specs_dir, feature, epic)
705
+ state["notes"] = note
706
+ return _commit_state(state_path, state)
707
+
708
+
709
+ def cmd_state_decision(
710
+ feature: str,
711
+ question: str,
712
+ raised_by: str,
713
+ rationale: str | None,
714
+ target_stage: str | None,
715
+ specs_dir: Path,
716
+ epic: str | None,
717
+ ) -> dict:
718
+ """Append an open deferred-decision item to ``deferredDecisions[]``.
719
+
720
+ Emits exactly the schema keys — the array item sets
721
+ ``additionalProperties: false``, so a convenience field is a hard validation
722
+ failure: required ``question``/``raisedBy``/``raisedAt``/``status``, plus
723
+ ``rationale``/``targetStage`` only when provided. ``status`` is always
724
+ ``"open"``; the recorder never resolves a decision (the target stage flips it
725
+ to ``"addressed"``).
726
+
727
+ Args:
728
+ feature: Feature name.
729
+ question: The deferred decision, phrased for the target stage.
730
+ raised_by: The deferring stage id.
731
+ rationale: Optional reason for deferring.
732
+ target_stage: Optional resolving stage id.
733
+ specs_dir: Specs directory.
734
+ epic: Owning epic name, or None.
735
+
736
+ Returns:
737
+ The mutated state dict (for the --json echo).
738
+
739
+ Raises:
740
+ UsageError: Unknown feature directory, unparseable state file, or a
741
+ failed atomic write (→ exit 2).
742
+ """
743
+ state_path, state = _load_state_for_write(specs_dir, feature, epic)
744
+ item: dict = {
745
+ "question": question,
746
+ "raisedBy": raised_by,
747
+ "raisedAt": _now_iso(),
748
+ "status": "open",
749
+ }
750
+ if rationale is not None:
751
+ item["rationale"] = rationale
752
+ if target_stage is not None:
753
+ item["targetStage"] = target_stage
754
+ state.setdefault("deferredDecisions", []).append(item)
755
+ return _commit_state(state_path, state)
756
+
757
+
758
+ def _parse_bool(raw: str, flag: str) -> bool:
759
+ """Parse an explicit boolean CLI value; fail closed on anything else.
760
+
761
+ Args:
762
+ raw: The raw flag value (e.g. from ``--blocks-current``).
763
+ flag: The flag name, for the error message.
764
+
765
+ Returns:
766
+ ``True`` for ``"true"``, ``False`` for ``"false"`` (case-insensitive,
767
+ surrounding whitespace ignored).
768
+
769
+ Raises:
770
+ UsageError: For any other value (→ exit 2), so a typo like ``"yes"`` is
771
+ rejected rather than silently misrouting the stage exit.
772
+ """
773
+ normalized = raw.strip().lower()
774
+ if normalized == "true":
775
+ return True
776
+ if normalized == "false":
777
+ return False
778
+ raise UsageError(f"{flag} expects true|false, got: {raw!r}")
779
+
780
+
781
+ def cmd_state_ecr(
782
+ feature: str,
783
+ kind: str,
784
+ target: str,
785
+ rationale: str,
786
+ raised_by: str,
787
+ blocks_current: bool,
788
+ specs_dir: Path,
789
+ epic: str | None,
790
+ ) -> dict:
791
+ """Append an open epic-change-request item to ``epicChangeRequests[]``.
792
+
793
+ Emits exactly the schema keys — the array item sets
794
+ ``additionalProperties: false``, so a convenience field is a hard validation
795
+ failure. All six payload fields are required, and ``status`` is always
796
+ ``"open"`` (only forge-0-epic edit mode flips it). ``blocksCurrent`` drives
797
+ stage-exit routing, so it is a strictly-parsed boolean.
798
+
799
+ Args:
800
+ feature: Feature name.
801
+ kind: One of add-feature|redep|move-boundary|split.
802
+ target: The sibling feature to add, or the affected feature/boundary.
803
+ rationale: Why the epic must change.
804
+ raised_by: forge-1-prd or forge-2-tech.
805
+ blocks_current: True → pause-now; False → finish-then-edit.
806
+ specs_dir: Specs directory.
807
+ epic: Owning epic name, or None.
808
+
809
+ Returns:
810
+ The mutated state dict (for the --json echo).
811
+
812
+ Raises:
813
+ UsageError: Unknown feature directory, unparseable state file, or a
814
+ failed atomic write (→ exit 2).
815
+ """
816
+ state_path, state = _load_state_for_write(specs_dir, feature, epic)
817
+ item = {
818
+ "kind": kind,
819
+ "target": target,
820
+ "rationale": rationale,
821
+ "blocksCurrent": blocks_current,
822
+ "raisedBy": raised_by,
823
+ "raisedAt": _now_iso(),
824
+ "status": "open",
825
+ }
826
+ state.setdefault("epicChangeRequests", []).append(item)
827
+ return _commit_state(state_path, state)
828
+
829
+
830
+ def _require_positive_int(value: object, label: str) -> int:
831
+ """Return ``value`` as a positive int, or raise ``UsageError``.
832
+
833
+ ``bool`` is rejected explicitly: it is an ``int`` subclass, so ``True`` would
834
+ otherwise sail through as version 1 and record a freshness ledger entry for an
835
+ artifact revision that never existed.
836
+
837
+ Args:
838
+ value: The candidate revision/version.
839
+ label: The flag or field name to name in the error.
840
+
841
+ Returns:
842
+ The validated positive integer.
843
+
844
+ Raises:
845
+ UsageError: Not an int, a bool, or below 1 (→ exit 2).
846
+ """
847
+ if isinstance(value, bool) or not isinstance(value, int) or value < 1:
848
+ raise UsageError(f"{label} must be a positive integer; got {value!r}")
849
+ return value
850
+
851
+
852
+ def _validated_findings_file(
853
+ value: str, target_dir: Path, label: str = "--findings-file"
854
+ ) -> str:
855
+ """Return ``value`` if it is a safe relative path inside ``target_dir``.
856
+
857
+ ``findingsFile`` is defined as relative to the
858
+ feature directory, and downstream consumers (forge-fix selecting the report)
859
+ follow the stored value verbatim. So it gets the same fail-closed containment
860
+ treatment as the write target itself (REQ-SEC-01): an absolute path, a ``..``
861
+ segment, a NUL/control character, or a symlinked escape is rejected BEFORE any
862
+ mutation rather than persisted for a later reader to resolve.
863
+
864
+ The same containment contract governs every stored path a caller asserts is
865
+ inside the feature directory, so the flag being validated is a parameter: the
866
+ diagnostic must name the flag the user actually passed.
867
+
868
+ Args:
869
+ value: The candidate path, as supplied on the command line.
870
+ target_dir: The resolved feature (or epic) directory it must sit inside.
871
+ label: The flag to name in the error.
872
+
873
+ Returns:
874
+ The value unchanged, once validated.
875
+
876
+ Raises:
877
+ UsageError: Empty, absolute, ``..``-bearing, control-character-bearing, or
878
+ escaping the target directory (→ exit 2).
879
+ """
880
+ if not value:
881
+ raise UsageError(f"{label} must not be empty")
882
+ bad = next((ch for ch in value if ord(ch) < 32 or ord(ch) == 127), None)
883
+ if bad is not None:
884
+ raise UsageError(
885
+ f"{label} contains a control character ({bad!r}); "
886
+ f"expected a plain relative path"
887
+ )
888
+ candidate = Path(value)
889
+ if candidate.is_absolute():
890
+ raise UsageError(
891
+ f"{label} {value!r} is absolute; it must be relative to the "
892
+ f"feature directory ({target_dir})"
893
+ )
894
+ if ".." in candidate.parts:
895
+ raise UsageError(
896
+ f"{label} {value!r} contains a '..' segment; it must stay inside "
897
+ f"the feature directory ({target_dir})"
898
+ )
899
+ root = target_dir.resolve()
900
+ resolved = (target_dir / candidate).resolve()
901
+ if resolved == root or root not in resolved.parents:
902
+ raise UsageError(
903
+ f"{label} {value!r} escapes the feature directory ({target_dir}); "
904
+ f"refusing to record it"
905
+ )
906
+ return value
907
+
908
+
909
+ def _current_artifact_version(state: dict, stage: str) -> int:
910
+ """Return the artifact revision a verify result is being recorded against.
911
+
912
+ For a feature target that is the selected production stage's ``version``.
913
+ Only the statuses that consume it resolve it: `passed` and
914
+ `findings-reported` write it into the freshness ledger, and
915
+ `auto-verify-pending` writes it as the revision the debt is owed on.
916
+ `skipped` and `findings-applied` never read it and skip the lookup, so both
917
+ stay recordable on a completed stage with no recorded ``version``.
918
+
919
+ Args:
920
+ state: The loaded state document.
921
+ stage: The production stage the verify entry serves.
922
+
923
+ Returns:
924
+ The stage's current positive-integer version.
925
+
926
+ Raises:
927
+ UsageError: The stage has no recorded (or no valid) ``version`` (→ exit 2).
928
+ """
929
+ version = _stage_version(state, stage)
930
+ if version is None:
931
+ raise UsageError(
932
+ f"{stage} has no recorded version in this feature's state, so there is no "
933
+ f"artifact revision to verify against; run state-complete for {stage} first"
934
+ )
935
+ return _require_positive_int(version, f"{stage}.version")
936
+
937
+
938
+ def _assert_full_commit_hash(commit_hash: object) -> None:
939
+ """Reject a ``--commit-hash`` that is not exactly 40 hexadecimal characters.
940
+
941
+ REQ-STATE-01 constrains WRITES, not reads. New provenance is a
942
+ full ``git rev-parse HEAD`` object hash; an abbreviation is rejected rather than
943
+ expanded, because expanding one would mean shelling out to Git from a script
944
+ whose whole contract is bounded local file reads. Caller case is preserved —
945
+ the regex accepts either case and nothing normalizes it.
946
+
947
+ Nothing constrains the schema, so a legacy short hash already recorded in state
948
+ keeps loading through ``_read_state``, ``_load_state_for_write``, the manifest
949
+ status readers, the navigator, and stage exit unmigrated (REQ-STATE-02).
950
+
951
+ Args:
952
+ commit_hash: The supplied value, typed loosely so a non-string reaching the
953
+ callable in-process is refused here rather than at serialization time.
954
+
955
+ Raises:
956
+ UsageError: The value is not a 40-character hex string (→ exit 2, before
957
+ any load-for-mutation and always before ``_commit_state``).
958
+ """
959
+ if isinstance(commit_hash, str) and FULL_GIT_HASH_RE.fullmatch(commit_hash):
960
+ return
961
+ raise UsageError(
962
+ f"--commit-hash must be the full 40-character Git object hash "
963
+ f"(`git rev-parse HEAD`); got {commit_hash!r}. An abbreviation is rejected "
964
+ f"rather than expanded. Nothing was written."
965
+ )
966
+
967
+
968
+ def _load_verify_target(
969
+ specs_dir: Path, feature: str, epic: str | None, is_epic_target: bool
970
+ ) -> tuple[Path, dict, int | None]:
971
+ """Resolve the state document ``state-verify`` will mutate — epic or feature.
972
+
973
+ An epic target NEVER falls back to the member writer, and a member target never
974
+ reaches the epic root: the two resolvers are disjoint (REQ-SEC-01). Both result
975
+ mode and commit-2 mode go through here, so neither can drift onto the other's
976
+ resolver.
977
+
978
+ Args:
979
+ specs_dir: The configured specs directory.
980
+ feature: The feature name, or the epic name for an epic target.
981
+ epic: The owning epic for a member, else None.
982
+ is_epic_target: True when ``--stage forge-0-epic`` selected the epic root.
983
+
984
+ Returns:
985
+ ``(state_path, state, revision)``. ``revision`` is the epic's manifest
986
+ revision for an epic target, and None for a feature target (whose artifact
987
+ version is read per-stage out of its own state).
988
+
989
+ Raises:
990
+ UsageError: Any resolution or load failure (→ exit 2, nothing written).
991
+ """
992
+ if is_epic_target:
993
+ return _load_epic_state_for_write(specs_dir, feature, epic)
994
+ state_path, state = _load_state_for_write(specs_dir, feature, epic)
995
+ return state_path, state, None
996
+
997
+
998
+ def _verify_result_entry(
999
+ status: str,
1000
+ prior: dict,
1001
+ current: int | None,
1002
+ findings_file: str | None,
1003
+ findings_count: int | None,
1004
+ now: str,
1005
+ ) -> dict:
1006
+ """Build the replacement ``forge-verify-*`` entry for one result transition.
1007
+
1008
+ Each status REPLACES the entry rather than patching it, which is what makes the
1009
+ the "clear …" rules exact: a terminal write cannot leave a stale
1010
+ ``scheduledAt``/``scheduledStageVersion`` behind, and the keys are DELETED
1011
+ rather than nulled (``VerifyEntry`` is ``total=False``, so absent means "not
1012
+ scheduled" while present-but-null would be malformed). ``findings-applied`` is
1013
+ the one status that carries prior state forward — the report metadata — and it
1014
+ deliberately writes no ``verifiedStageVersion``: fixes landed, nothing
1015
+ re-verified them, so freshness stays unresolved until a later ``passed``.
1016
+ ``passed`` may record NEW attached-report metadata of its own (a clean report,
1017
+ an advisory-only report, or the escalation-acceptance rules in
1018
+ ``cmd_state_verify``).
1019
+
1020
+ Args:
1021
+ status: The validated result status.
1022
+ prior: The existing entry (``{}`` when absent).
1023
+ current: The current artifact revision, or None for ``skipped`` and
1024
+ ``findings-applied`` (which never consume it).
1025
+ findings_file: Validated relative report path, when supplied.
1026
+ findings_count: Validated non-negative count, when supplied.
1027
+ now: The shared ISO-8601 timestamp for this write.
1028
+
1029
+ Returns:
1030
+ The complete new entry dict.
1031
+ """
1032
+ if status == "auto-verify-pending":
1033
+ return {
1034
+ "status": status,
1035
+ "scheduledAt": now,
1036
+ "scheduledStageVersion": current,
1037
+ "commitHash": None,
1038
+ }
1039
+ if status == "passed":
1040
+ entry: dict = {"status": status}
1041
+ if findings_file is not None:
1042
+ # An attached clean/advisory report, or residual findings the user
1043
+ # explicitly accepted at the escalation gate, resolves as `passed`
1044
+ # so it never routes to forge-fix while the audit artifact stays
1045
+ # attached. A bare zero count records no report keys, preserving the
1046
+ # legacy plain "verified clean" shape.
1047
+ entry["findingsFile"] = findings_file
1048
+ entry["findingsCount"] = findings_count
1049
+ entry["verifiedAt"] = now
1050
+ entry["verifiedStageVersion"] = current
1051
+ entry["commitHash"] = None
1052
+ return entry
1053
+ if status == "findings-reported":
1054
+ return {
1055
+ "status": status,
1056
+ "findingsFile": findings_file,
1057
+ "findingsCount": findings_count,
1058
+ "verifiedAt": now,
1059
+ "verifiedStageVersion": current,
1060
+ "commitHash": None,
1061
+ }
1062
+ if status == "findings-applied":
1063
+ entry: dict = {"status": status}
1064
+ for key in ("findingsFile", "findingsCount"):
1065
+ if key in prior:
1066
+ entry[key] = prior[key]
1067
+ entry["fixedAt"] = now
1068
+ entry["commitHash"] = None
1069
+ return entry
1070
+ return {"status": status, "commitHash": None} # skipped
1071
+
1072
+
1073
+ def cmd_state_verify(
1074
+ feature: str,
1075
+ stage: str,
1076
+ specs_dir: Path,
1077
+ epic: str | None,
1078
+ status: str | None = None,
1079
+ findings_file: str | None = None,
1080
+ findings_count: int | None = None,
1081
+ verified_stage_version: int | None = None,
1082
+ commit_hash: str | None = None,
1083
+ ) -> dict:
1084
+ """Write one verify result transition or one provenance follow-up.
1085
+
1086
+ Args:
1087
+ feature: The feature name, or the EPIC name when `stage == "forge-0-epic"`.
1088
+ Resolved through the same path-safety and containment rules as every
1089
+ other state write.
1090
+ stage: The production stage this verify entry serves — one of
1091
+ `VERIFY_MODE_TO_STAGE`'s values, or `"forge-0-epic"` for an epic-target
1092
+ write. Selects `stages["forge-verify-{suffix}"]`.
1093
+ specs_dir: Root of the specs tree, as configured by `specsDir`.
1094
+ epic: Epic name when `feature` is a member, else None. REQUIRED for members
1095
+ so the bare name is never resolved ambiguously. For
1096
+ `stage == "forge-0-epic"` it must be absent or equal to `feature`.
1097
+ status: Result mode. Mutually exclusive with `commit_hash`. Each status
1098
+ admits only the metadata below; everything else is refused before any
1099
+ write, so a contradictory call never lands a partial entry:
1100
+
1101
+ - `passed` — REQUIRES `verified_stage_version`. MAY carry an attached
1102
+ report (`findings_file` + `findings_count` together, count >= 0) in
1103
+ three protocol cases: a CLEAN zero-finding round report (a fix
1104
+ pass's re-verify — valid audit evidence that lets the stage advance
1105
+ instead of stranding at `findings-applied`, #237), an ADVISORY-ONLY
1106
+ report (no blocking `error`/`gap` findings, count >= 1), and
1107
+ residual findings the user explicitly ACCEPTED at the round-ledger
1108
+ escalation (recorded first as a `state-decision`; see "Escalation"
1109
+ in stage-exit-protocol.md). In every case the stage resolves
1110
+ without routing to forge-fix and the report stays attached. Half a
1111
+ pairing is refused: a file without a count, or a positive count
1112
+ without a file. A bare `passed` (neither flag) is also accepted and
1113
+ records the report-free clean shape. Unaccepted blocking findings
1114
+ belong to `findings-reported`.
1115
+ - `findings-reported` — REQUIRES all three of `verified_stage_version`,
1116
+ `findings_file`, and a non-negative `findings_count`.
1117
+ - `findings-applied` — REFUSES `verified_stage_version`. Applying fixes
1118
+ is not verifying them, so this status deliberately CLEARS the recorded
1119
+ freshness and leaves the stage's verification outstanding until a later
1120
+ `passed` records a revision.
1121
+ - `skipped` and `auto-verify-pending` — accept none of the three.
1122
+
1123
+ `passed` and `findings-reported` additionally refuse a
1124
+ `verified_stage_version` that is stale against the served stage's current
1125
+ version. The persisted shape is `references/pipeline-state-schema.json`.
1126
+ findings_file: Path to the findings document, relative to and contained by
1127
+ the resolved feature/epic directory. Required by `findings-reported`,
1128
+ optional on `passed` (the attached-report cases above); rejected when
1129
+ absolute, containing `..`, or carrying NUL/control characters
1130
+ (REQ-SEC-01).
1131
+ findings_count: Number of findings in `findings_file`. Required alongside it.
1132
+ verified_stage_version: The served stage's `version` at verification time,
1133
+ feeding the navigator's freshness ledger. Cleared by
1134
+ `findings-applied`, which deliberately does not claim freshness.
1135
+ commit_hash: Commit-2 mode. Full 40-hex only, validated by
1136
+ `FULL_GIT_HASH_RE.fullmatch`; abbreviations are rejected rather than
1137
+ expanded. Mutually exclusive with `status`.
1138
+
1139
+ Returns:
1140
+ The emitted JSON result: the written verify entry plus the resolved target
1141
+ path, so the caller can report what landed without re-reading state.
1142
+
1143
+ Raises:
1144
+ UsageError: Mixed modes, invalid metadata, invalid hash, missing entry,
1145
+ unsafe/ambiguous target, or atomic write failure.
1146
+ """
1147
+ # --- Mode exclusivity, before anything is resolved or loaded. -------------
1148
+ if status is None and commit_hash is None:
1149
+ raise UsageError(
1150
+ "state-verify needs exactly one mode: --status <result> to record a "
1151
+ "verification transition, or --commit-hash <40-hex> to record Commit-2 "
1152
+ "provenance for an existing entry"
1153
+ )
1154
+ if status is not None and commit_hash is not None:
1155
+ raise UsageError(
1156
+ "--status and --commit-hash are mutually exclusive: a result write "
1157
+ "records commitHash null (Commit 1), and the hash lands in a separate "
1158
+ "commit-2 call"
1159
+ )
1160
+ if commit_hash is not None:
1161
+ # Commit-2 carries provenance for an entry that ALREADY exists, so every
1162
+ # result field must be absent: a hash arriving next to findings metadata
1163
+ # means the caller conflated the two writes.
1164
+ for label, value in (
1165
+ ("--findings-file", findings_file),
1166
+ ("--findings-count", findings_count),
1167
+ ("--verified-stage-version", verified_stage_version),
1168
+ ):
1169
+ if value is not None:
1170
+ raise UsageError(
1171
+ f"--commit-hash records provenance for an existing entry and "
1172
+ f"changes only its commitHash, so it does not accept {label}. "
1173
+ f"Record the result with --status first, commit, then re-run "
1174
+ f"with --commit-hash alone."
1175
+ )
1176
+ _assert_full_commit_hash(commit_hash)
1177
+ elif status not in VERIFY_RESULT_STATUSES:
1178
+ known = ", ".join(VERIFY_RESULT_STATUSES)
1179
+ raise UsageError(f"unknown --status {status!r}; expected one of {known}")
1180
+
1181
+ # --- Target selection: epic before the token map. --------------
1182
+ is_epic_target = stage == "forge-0-epic"
1183
+ if is_epic_target:
1184
+ verify_key = "forge-verify-epic"
1185
+ else:
1186
+ token = VERIFY_TOKEN_BY_STAGE.get(stage)
1187
+ if token is None:
1188
+ raise UsageError(
1189
+ f"{stage} has no verification token, so it has no forge-verify-* entry "
1190
+ f"to write; expected one of {', '.join(VERIFY_STAGES)}"
1191
+ )
1192
+ verify_key = f"forge-verify-{token}"
1193
+
1194
+ # --- Commit-2 provenance mode. ---------------------------------
1195
+ # Commit 1 recorded the result with `commitHash: null`; this second, targeted
1196
+ # write records the hash of THAT commit. Nothing here invokes Git, rewrites
1197
+ # history, or amends — the two commits stay two commits (REQ-STATE-04).
1198
+ if commit_hash is not None:
1199
+ state_path, state, _ = _load_verify_target(
1200
+ specs_dir, feature, epic, is_epic_target
1201
+ )
1202
+ entry = _verify_entry(state, verify_key)
1203
+ if not entry:
1204
+ raise UsageError(
1205
+ f"--commit-hash records provenance for an existing {verify_key} "
1206
+ f"entry, and {feature} has none. Record the verification result "
1207
+ f"with --status first, commit it, then re-run with --commit-hash."
1208
+ )
1209
+ # In place, so status, findings metadata, scheduling metadata, timestamps
1210
+ # and versions are all left exactly as Commit 1 wrote them.
1211
+ entry["commitHash"] = commit_hash
1212
+ written = _commit_state(state_path, state)
1213
+ return {
1214
+ "feature": feature,
1215
+ "stage": stage,
1216
+ "verifyKey": verify_key,
1217
+ "statePath": str(state_path),
1218
+ "entry": entry,
1219
+ "updatedAt": written["updatedAt"],
1220
+ }
1221
+
1222
+ # --- Metadata validation that needs no state. ------------------
1223
+ if verified_stage_version is not None:
1224
+ _require_positive_int(verified_stage_version, "--verified-stage-version")
1225
+ if findings_count is not None and (
1226
+ isinstance(findings_count, bool) or not isinstance(findings_count, int)
1227
+ ):
1228
+ raise UsageError(f"--findings-count must be an integer; got {findings_count!r}")
1229
+
1230
+ if status in ("auto-verify-pending", "skipped"):
1231
+ for label, value in (
1232
+ ("--findings-file", findings_file),
1233
+ ("--findings-count", findings_count),
1234
+ ("--verified-stage-version", verified_stage_version),
1235
+ ):
1236
+ if value is not None:
1237
+ raise UsageError(f"--status {status} does not accept {label}")
1238
+ elif status == "passed":
1239
+ if findings_file is not None and findings_count is None:
1240
+ raise UsageError(
1241
+ "--status passed with --findings-file requires --findings-count N "
1242
+ "(zero for a clean report, or the number of advisory/residual "
1243
+ "findings it lists)"
1244
+ )
1245
+ if findings_count is not None:
1246
+ if findings_count < 0:
1247
+ raise UsageError(
1248
+ f"--findings-count must not be negative; got {findings_count!r}"
1249
+ )
1250
+ if findings_count > 0 and findings_file is None:
1251
+ raise UsageError(
1252
+ f"--status passed with --findings-count {findings_count} requires "
1253
+ f"--findings-file <advisory report>: a positive count with no "
1254
+ f"report to read is unrecoverable. Blocking findings belong to "
1255
+ f"--status findings-reported instead."
1256
+ )
1257
+ if verified_stage_version is None:
1258
+ raise UsageError(
1259
+ "--status passed requires --verified-stage-version <current version>"
1260
+ )
1261
+ elif status == "findings-reported":
1262
+ if verified_stage_version is None:
1263
+ raise UsageError(
1264
+ "--status findings-reported requires --verified-stage-version "
1265
+ "<current version>"
1266
+ )
1267
+ if findings_file is None:
1268
+ raise UsageError(
1269
+ "--status findings-reported requires --findings-file <path relative "
1270
+ "to the feature directory>"
1271
+ )
1272
+ if findings_count is None:
1273
+ raise UsageError("--status findings-reported requires --findings-count N")
1274
+ if findings_count < 0:
1275
+ raise UsageError(
1276
+ f"--findings-count must not be negative; got {findings_count!r}"
1277
+ )
1278
+ elif verified_stage_version is not None: # findings-applied
1279
+ raise UsageError(
1280
+ "--status findings-applied does not accept --verified-stage-version: "
1281
+ "applying fixes deliberately CLEARS freshness, so only a later "
1282
+ "--status passed may record a verified revision"
1283
+ )
1284
+
1285
+ state_path, state, epic_revision = _load_verify_target(
1286
+ specs_dir, feature, epic, is_epic_target
1287
+ )
1288
+ target_dir = state_path.parent
1289
+ if findings_file is not None:
1290
+ _validated_findings_file(findings_file, target_dir)
1291
+
1292
+ if status in ("skipped", "findings-applied"):
1293
+ # Neither status consumes the artifact revision: `skipped` records no
1294
+ # freshness, and `findings-applied` deliberately clears it (the entry is
1295
+ # built from the prior report plus `fixedAt`). Resolving it anyway would
1296
+ # make both unrecordable on a completed stage whose `version` was never
1297
+ # written — exactly the state that needs the recovery path (#202).
1298
+ current = None
1299
+ elif is_epic_target:
1300
+ # The epic's artifact revision is the manifest revision — never a member's
1301
+ # production-stage version.
1302
+ current = epic_revision
1303
+ else:
1304
+ current = _current_artifact_version(state, stage)
1305
+ if status in ("passed", "findings-reported") and verified_stage_version != current:
1306
+ at = (
1307
+ f"{feature}'s manifest is at revision {current}"
1308
+ if is_epic_target
1309
+ else f"{stage} is at version {current}"
1310
+ )
1311
+ raise UsageError(
1312
+ f"--verified-stage-version {verified_stage_version} is stale: {at}. "
1313
+ f"Re-run verification against the current artifact."
1314
+ )
1315
+
1316
+ prior = _verify_entry(state, verify_key)
1317
+ if status == "skipped" and prior.get("status") in _SKIP_PROTECTED_PRIOR:
1318
+ # The #203 demotion trap: `skipped` over a complete-for-orchestration
1319
+ # status silently dropped the member from its epic rollup and re-blocked
1320
+ # every dependent. Fail closed; a deferral needs no write at all.
1321
+ raise UsageError(
1322
+ f"--status skipped would demote {verify_key} from "
1323
+ f"{prior.get('status')!r}: that status counts as resolved (and, for an "
1324
+ f"epic member, complete-for-orchestration), so replacing it with "
1325
+ f"skipped would drop the member from its epic rollup and re-block its "
1326
+ f"dependents. A deferral needs no write — the recorded result already "
1327
+ f"stands. Re-run verification to refresh it, or record --status passed "
1328
+ f"(with the report attached) to accept residual findings. "
1329
+ f"Nothing was written."
1330
+ )
1331
+ if status == "auto-verify-pending" and prior.get("status") == "findings-reported":
1332
+ # `_verify_result_entry` REPLACES the entry, so scheduling over a report
1333
+ # for the current revision would delete its `findingsFile`/`findingsCount`
1334
+ # and break the later `findings-applied` precondition (REQ-EXIT-04's
1335
+ # forbidden clobber, reached through the CLI instead of a branch exit).
1336
+ # A report against a since-revised artifact is superseded normally.
1337
+ reported = prior.get("verifiedStageVersion")
1338
+ if (
1339
+ isinstance(reported, int)
1340
+ and not isinstance(reported, bool)
1341
+ and current is not None
1342
+ and reported == current
1343
+ ):
1344
+ raise UsageError(
1345
+ f"--status auto-verify-pending would replace {verify_key}'s "
1346
+ f"findings-reported entry for the current revision and delete its "
1347
+ f"report metadata ({prior.get('findingsFile')!r}, "
1348
+ f"findingsCount {prior.get('findingsCount')!r}). Apply the report "
1349
+ f"via forge-fix (--status findings-applied) or re-verify to a "
1350
+ f"terminal status; scheduling is valid only after the artifact "
1351
+ f"is revised."
1352
+ )
1353
+ if status == "findings-applied":
1354
+ if prior.get("status") not in ("findings-reported", "findings-applied"):
1355
+ raise UsageError(
1356
+ f"--status findings-applied requires an existing {verify_key} entry "
1357
+ f"with status findings-reported (or findings-applied); found "
1358
+ f"{prior.get('status')!r}"
1359
+ )
1360
+ for label, key, supplied in (
1361
+ ("--findings-file", "findingsFile", findings_file),
1362
+ ("--findings-count", "findingsCount", findings_count),
1363
+ ):
1364
+ if supplied is not None and supplied != prior.get(key):
1365
+ raise UsageError(
1366
+ f"{label} {supplied!r} does not match the recorded report "
1367
+ f"({key}: {prior.get(key)!r}); fix the value or omit the flag"
1368
+ )
1369
+
1370
+ entry = _verify_result_entry(
1371
+ status, prior, current, findings_file, findings_count, _now_iso()
1372
+ )
1373
+ state.setdefault("stages", {})[verify_key] = entry
1374
+ written = _commit_state(state_path, state)
1375
+ return {
1376
+ "feature": feature,
1377
+ "stage": stage,
1378
+ "verifyKey": verify_key,
1379
+ "statePath": str(state_path),
1380
+ "entry": entry,
1381
+ "updatedAt": written["updatedAt"],
1382
+ }
1383
+
1384
+
1385
+ def _print_state_enter(state: dict) -> None:
1386
+ """Print the one-line human summary for `state-enter`."""
1387
+ print(f"entered {state['currentStage']} (in-progress) for {state['feature']}")
1388
+
1389
+
1390
+ def _print_state_artifact(state: dict, stage: str, paths: list[str]) -> None:
1391
+ """Print the one-line human summary for `state-artifact`."""
1392
+ total = len(state.get("stages", {}).get(stage, {}).get("artifacts", []))
1393
+ print(f"tracked {stage} artifact(s): {', '.join(paths)} ({total} total)")
1394
+
1395
+
1396
+ def _print_state_complete(
1397
+ state: dict, stage: str, commit_hash: str | None, resumable: bool
1398
+ ) -> None:
1399
+ """Print the one-line human summary for `state-complete` (one per branch)."""
1400
+ if commit_hash is not None:
1401
+ print(f"recorded {stage} commitHash: {commit_hash}")
1402
+ return
1403
+ if resumable:
1404
+ print(f"left {stage} in-progress (resumable — no completion recorded)")
1405
+ return
1406
+ entry = state.get("stages", {}).get(stage, {})
1407
+ label = (
1408
+ "completed"
1409
+ if entry.get("status") == _DONE_STATUS
1410
+ else f"partially completed ({entry.get('status')})"
1411
+ )
1412
+ recorded = entry.get("commitHash")
1413
+ cascaded = state.get("_cascadedStale") or []
1414
+ suffix = f"; marked stale: {', '.join(cascaded)}" if cascaded else ""
1415
+ print(
1416
+ f"{label} {stage} v{entry.get('version')} "
1417
+ f"(commitHash: {'null' if recorded is None else recorded}){suffix}"
1418
+ )
1419
+
1420
+
1421
+ def _print_state_skip(state: dict, stage: str) -> None:
1422
+ """Print the one-line human summary for `state-skip`."""
1423
+ print(
1424
+ f"recorded {stage} as skipped for {state['feature']} "
1425
+ "(deliberate — no docs claimed)"
1426
+ )
1427
+
1428
+
1429
+ def _print_state_branch(state: dict) -> None:
1430
+ """Print the one-line human summary for `state-branch`."""
1431
+ print(f"recorded branch for {state['feature']}: {state['branch']}")
1432
+
1433
+
1434
+ def _print_state_note(state: dict) -> None:
1435
+ """Print the one-line human summary for `state-note`."""
1436
+ print(f"note set for {state['feature']} ({len(state['notes'])} chars)")
1437
+
1438
+
1439
+ def _print_state_decision(state: dict) -> None:
1440
+ """Print the one-line human summary for `state-decision` (the item appended)."""
1441
+ item = state["deferredDecisions"][-1]
1442
+ target = item.get("targetStage")
1443
+ routing = f"{item['raisedBy']} → {target}" if target else f"{item['raisedBy']}, no target stage"
1444
+ print(f"deferred decision recorded (raisedBy {routing})")
1445
+
1446
+
1447
+ def _print_state_verify(result: dict, commit_hash: str | None = None) -> None:
1448
+ """Print the one-line human summary for `state-verify` (one per mode).
1449
+
1450
+ Takes the verb's RESULT dict (entry + resolved path), not a state document —
1451
+ `state-verify` is the one verb whose echo is the written entry rather than the
1452
+ whole file. Commit-2 mode gets its own line: reporting the untouched status
1453
+ would read as if the result had just been re-written.
1454
+ """
1455
+ entry = result["entry"]
1456
+ if commit_hash is not None:
1457
+ print(f"recorded {result['verifyKey']} commitHash: {commit_hash}")
1458
+ return
1459
+ detail = ""
1460
+ if entry.get("findingsFile"):
1461
+ detail = f" ({entry.get('findingsCount')} in {entry['findingsFile']})"
1462
+ elif entry.get("scheduledStageVersion") is not None:
1463
+ detail = f" (scheduled at v{entry['scheduledStageVersion']})"
1464
+ elif entry.get("verifiedStageVersion") is not None:
1465
+ detail = f" (v{entry['verifiedStageVersion']})"
1466
+ print(
1467
+ f"recorded {result['verifyKey']} = {entry['status']} for "
1468
+ f"{result['feature']}{detail}"
1469
+ )
1470
+
1471
+
1472
+ def _print_state_ecr(state: dict) -> None:
1473
+ """Print the one-line human summary for `state-ecr` (the item appended)."""
1474
+ item = state["epicChangeRequests"][-1]
1475
+ blocks = "true" if item["blocksCurrent"] else "false"
1476
+ print(
1477
+ f"epic change request recorded ({item['kind']} → {item['target']}, "
1478
+ f"blocksCurrent={blocks})"
1479
+ )