@garygentry/feature-forge 0.3.1 → 0.3.2

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 (314) hide show
  1. package/adapters/claude/.feature-forge-bundle.json +1 -1
  2. package/adapters/claude/references/epic-manifest-schema.json +6 -1
  3. package/adapters/claude/references/forge-config-schema.json +1 -1
  4. package/adapters/claude/references/pipeline-state-schema.json +4 -2
  5. package/adapters/claude/references/shared-conventions.md +63 -0
  6. package/adapters/claude/references/stage-exit-protocol.md +344 -140
  7. package/adapters/claude/scripts/epic-manifest.py +413 -87
  8. package/adapters/claude/scripts/forge-bootstrap.py +57 -5
  9. package/adapters/claude/scripts/forge-session.py +3136 -139
  10. package/adapters/claude/scripts/validate-traceability.py +86 -5
  11. package/adapters/claude/skills/forge/SKILL.md +7 -6
  12. package/adapters/claude/skills/forge/references/pipeline-state-schema.json +4 -2
  13. package/adapters/claude/skills/forge/references/shared-conventions.md +63 -0
  14. package/adapters/claude/skills/forge/references/stage-exit-protocol.md +344 -140
  15. package/adapters/claude/skills/forge-0-epic/SKILL.md +7 -2
  16. package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +24 -24
  17. package/adapters/claude/skills/forge-0-epic/references/pipeline-state-schema.json +4 -2
  18. package/adapters/claude/skills/forge-0-epic/references/shared-conventions.md +63 -0
  19. package/adapters/claude/skills/forge-0-epic/references/stage-exit-protocol.md +344 -140
  20. package/adapters/claude/skills/forge-1-prd/SKILL.md +14 -3
  21. package/adapters/claude/skills/forge-1-prd/references/shared-conventions.md +63 -0
  22. package/adapters/claude/skills/forge-1-prd/references/stage-exit-protocol.md +344 -140
  23. package/adapters/claude/skills/forge-2-tech/SKILL.md +13 -3
  24. package/adapters/claude/skills/forge-2-tech/references/shared-conventions.md +63 -0
  25. package/adapters/claude/skills/forge-2-tech/references/stage-exit-protocol.md +344 -140
  26. package/adapters/claude/skills/forge-3-specs/SKILL.md +4 -2
  27. package/adapters/claude/skills/forge-3-specs/references/shared-conventions.md +63 -0
  28. package/adapters/claude/skills/forge-3-specs/references/spec-archetypes.md +9 -0
  29. package/adapters/claude/skills/forge-3-specs/references/stage-exit-protocol.md +344 -140
  30. package/adapters/claude/skills/forge-4-backlog/SKILL.md +16 -3
  31. package/adapters/claude/skills/forge-4-backlog/references/shared-conventions.md +63 -0
  32. package/adapters/claude/skills/forge-4-backlog/references/stage-exit-protocol.md +344 -140
  33. package/adapters/claude/skills/forge-5-loop/SKILL.md +40 -42
  34. package/adapters/claude/skills/forge-5-loop/references/result-reporting.md +70 -31
  35. package/adapters/claude/skills/forge-5-loop/references/runner-contract.md +4 -2
  36. package/adapters/claude/skills/forge-5-loop/references/shared-conventions.md +63 -0
  37. package/adapters/claude/skills/forge-5-loop/references/stage-exit-protocol.md +344 -140
  38. package/adapters/claude/skills/forge-6-docs/SKILL.md +48 -4
  39. package/adapters/claude/skills/forge-6-docs/references/shared-conventions.md +63 -0
  40. package/adapters/claude/skills/forge-6-docs/references/stage-exit-protocol.md +472 -0
  41. package/adapters/claude/skills/forge-fix/SKILL.md +85 -33
  42. package/adapters/claude/skills/forge-fix/references/shared-conventions.md +63 -0
  43. package/adapters/claude/skills/forge-fix/references/stage-exit-protocol.md +344 -140
  44. package/adapters/claude/skills/forge-guide/references/forge-config-schema.json +1 -1
  45. package/adapters/claude/skills/forge-guide/references/shared-conventions.md +63 -0
  46. package/adapters/claude/skills/forge-verify/SKILL.md +73 -41
  47. package/adapters/claude/skills/forge-verify/references/findings-template.md +39 -49
  48. package/adapters/claude/skills/forge-verify/references/shared-conventions.md +63 -0
  49. package/adapters/claude/skills/forge-verify/references/stage-exit-protocol.md +472 -0
  50. package/adapters/claude/skills/forge-verify/references/verification-checklists/epic.md +4 -3
  51. package/adapters/claude/skills/forge-verify/references/verification-checklists/specs.md +2 -0
  52. package/adapters/codex/.feature-forge-bundle.json +1 -1
  53. package/adapters/codex/references/epic-manifest-schema.json +6 -1
  54. package/adapters/codex/references/forge-config-schema.json +1 -1
  55. package/adapters/codex/references/pipeline-state-schema.json +4 -2
  56. package/adapters/codex/references/shared-conventions.md +63 -0
  57. package/adapters/codex/references/stage-exit-protocol.md +344 -140
  58. package/adapters/codex/scripts/epic-manifest.py +413 -87
  59. package/adapters/codex/scripts/forge-bootstrap.py +57 -5
  60. package/adapters/codex/scripts/forge-session.py +3136 -139
  61. package/adapters/codex/scripts/validate-traceability.py +86 -5
  62. package/adapters/codex/skills/forge/SKILL.md +7 -6
  63. package/adapters/codex/skills/forge/references/pipeline-state-schema.json +4 -2
  64. package/adapters/codex/skills/forge/references/shared-conventions.md +63 -0
  65. package/adapters/codex/skills/forge/references/stage-exit-protocol.md +344 -140
  66. package/adapters/codex/skills/forge-0-epic/SKILL.md +7 -2
  67. package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +24 -24
  68. package/adapters/codex/skills/forge-0-epic/references/pipeline-state-schema.json +4 -2
  69. package/adapters/codex/skills/forge-0-epic/references/shared-conventions.md +63 -0
  70. package/adapters/codex/skills/forge-0-epic/references/stage-exit-protocol.md +344 -140
  71. package/adapters/codex/skills/forge-1-prd/SKILL.md +14 -3
  72. package/adapters/codex/skills/forge-1-prd/references/shared-conventions.md +63 -0
  73. package/adapters/codex/skills/forge-1-prd/references/stage-exit-protocol.md +344 -140
  74. package/adapters/codex/skills/forge-2-tech/SKILL.md +13 -3
  75. package/adapters/codex/skills/forge-2-tech/references/shared-conventions.md +63 -0
  76. package/adapters/codex/skills/forge-2-tech/references/stage-exit-protocol.md +344 -140
  77. package/adapters/codex/skills/forge-3-specs/SKILL.md +4 -2
  78. package/adapters/codex/skills/forge-3-specs/references/shared-conventions.md +63 -0
  79. package/adapters/codex/skills/forge-3-specs/references/spec-archetypes.md +9 -0
  80. package/adapters/codex/skills/forge-3-specs/references/stage-exit-protocol.md +344 -140
  81. package/adapters/codex/skills/forge-4-backlog/SKILL.md +16 -3
  82. package/adapters/codex/skills/forge-4-backlog/references/shared-conventions.md +63 -0
  83. package/adapters/codex/skills/forge-4-backlog/references/stage-exit-protocol.md +344 -140
  84. package/adapters/codex/skills/forge-5-loop/SKILL.md +40 -42
  85. package/adapters/codex/skills/forge-5-loop/references/result-reporting.md +70 -31
  86. package/adapters/codex/skills/forge-5-loop/references/runner-contract.md +4 -2
  87. package/adapters/codex/skills/forge-5-loop/references/shared-conventions.md +63 -0
  88. package/adapters/codex/skills/forge-5-loop/references/stage-exit-protocol.md +344 -140
  89. package/adapters/codex/skills/forge-6-docs/SKILL.md +48 -4
  90. package/adapters/codex/skills/forge-6-docs/references/shared-conventions.md +63 -0
  91. package/adapters/codex/skills/forge-6-docs/references/stage-exit-protocol.md +472 -0
  92. package/adapters/codex/skills/forge-fix/SKILL.md +84 -32
  93. package/adapters/codex/skills/forge-fix/references/shared-conventions.md +63 -0
  94. package/adapters/codex/skills/forge-fix/references/stage-exit-protocol.md +344 -140
  95. package/adapters/codex/skills/forge-guide/references/forge-config-schema.json +1 -1
  96. package/adapters/codex/skills/forge-guide/references/shared-conventions.md +63 -0
  97. package/adapters/codex/skills/forge-verify/SKILL.md +72 -40
  98. package/adapters/codex/skills/forge-verify/references/findings-template.md +39 -49
  99. package/adapters/codex/skills/forge-verify/references/shared-conventions.md +63 -0
  100. package/adapters/codex/skills/forge-verify/references/stage-exit-protocol.md +472 -0
  101. package/adapters/codex/skills/forge-verify/references/verification-checklists/epic.md +4 -3
  102. package/adapters/codex/skills/forge-verify/references/verification-checklists/specs.md +2 -0
  103. package/adapters/copilot/.feature-forge-bundle.json +1 -1
  104. package/adapters/copilot/references/epic-manifest-schema.json +6 -1
  105. package/adapters/copilot/references/forge-config-schema.json +1 -1
  106. package/adapters/copilot/references/pipeline-state-schema.json +4 -2
  107. package/adapters/copilot/references/shared-conventions.md +63 -0
  108. package/adapters/copilot/references/stage-exit-protocol.md +344 -140
  109. package/adapters/copilot/scripts/epic-manifest.py +413 -87
  110. package/adapters/copilot/scripts/forge-bootstrap.py +57 -5
  111. package/adapters/copilot/scripts/forge-session.py +3136 -139
  112. package/adapters/copilot/scripts/validate-traceability.py +86 -5
  113. package/adapters/copilot/skills/forge/forge.md +7 -6
  114. package/adapters/copilot/skills/forge/references/pipeline-state-schema.json +4 -2
  115. package/adapters/copilot/skills/forge/references/shared-conventions.md +63 -0
  116. package/adapters/copilot/skills/forge/references/stage-exit-protocol.md +344 -140
  117. package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +7 -2
  118. package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +24 -24
  119. package/adapters/copilot/skills/forge-0-epic/references/pipeline-state-schema.json +4 -2
  120. package/adapters/copilot/skills/forge-0-epic/references/shared-conventions.md +63 -0
  121. package/adapters/copilot/skills/forge-0-epic/references/stage-exit-protocol.md +344 -140
  122. package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +14 -3
  123. package/adapters/copilot/skills/forge-1-prd/references/shared-conventions.md +63 -0
  124. package/adapters/copilot/skills/forge-1-prd/references/stage-exit-protocol.md +344 -140
  125. package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +13 -3
  126. package/adapters/copilot/skills/forge-2-tech/references/shared-conventions.md +63 -0
  127. package/adapters/copilot/skills/forge-2-tech/references/stage-exit-protocol.md +344 -140
  128. package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +4 -2
  129. package/adapters/copilot/skills/forge-3-specs/references/shared-conventions.md +63 -0
  130. package/adapters/copilot/skills/forge-3-specs/references/spec-archetypes.md +9 -0
  131. package/adapters/copilot/skills/forge-3-specs/references/stage-exit-protocol.md +344 -140
  132. package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +16 -3
  133. package/adapters/copilot/skills/forge-4-backlog/references/shared-conventions.md +63 -0
  134. package/adapters/copilot/skills/forge-4-backlog/references/stage-exit-protocol.md +344 -140
  135. package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +40 -42
  136. package/adapters/copilot/skills/forge-5-loop/references/result-reporting.md +70 -31
  137. package/adapters/copilot/skills/forge-5-loop/references/runner-contract.md +4 -2
  138. package/adapters/copilot/skills/forge-5-loop/references/shared-conventions.md +63 -0
  139. package/adapters/copilot/skills/forge-5-loop/references/stage-exit-protocol.md +344 -140
  140. package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +48 -4
  141. package/adapters/copilot/skills/forge-6-docs/references/shared-conventions.md +63 -0
  142. package/adapters/copilot/skills/forge-6-docs/references/stage-exit-protocol.md +472 -0
  143. package/adapters/copilot/skills/forge-fix/forge-fix.md +84 -32
  144. package/adapters/copilot/skills/forge-fix/references/shared-conventions.md +63 -0
  145. package/adapters/copilot/skills/forge-fix/references/stage-exit-protocol.md +344 -140
  146. package/adapters/copilot/skills/forge-guide/references/forge-config-schema.json +1 -1
  147. package/adapters/copilot/skills/forge-guide/references/shared-conventions.md +63 -0
  148. package/adapters/copilot/skills/forge-verify/forge-verify.md +72 -40
  149. package/adapters/copilot/skills/forge-verify/references/findings-template.md +39 -49
  150. package/adapters/copilot/skills/forge-verify/references/shared-conventions.md +63 -0
  151. package/adapters/copilot/skills/forge-verify/references/stage-exit-protocol.md +472 -0
  152. package/adapters/copilot/skills/forge-verify/references/verification-checklists/epic.md +4 -3
  153. package/adapters/copilot/skills/forge-verify/references/verification-checklists/specs.md +2 -0
  154. package/adapters/cursor/.feature-forge-bundle.json +1 -1
  155. package/adapters/cursor/references/epic-manifest-schema.json +6 -1
  156. package/adapters/cursor/references/forge-config-schema.json +1 -1
  157. package/adapters/cursor/references/pipeline-state-schema.json +4 -2
  158. package/adapters/cursor/references/shared-conventions.md +63 -0
  159. package/adapters/cursor/references/stage-exit-protocol.md +344 -140
  160. package/adapters/cursor/scripts/epic-manifest.py +413 -87
  161. package/adapters/cursor/scripts/forge-bootstrap.py +57 -5
  162. package/adapters/cursor/scripts/forge-session.py +3136 -139
  163. package/adapters/cursor/scripts/validate-traceability.py +86 -5
  164. package/adapters/cursor/skills/forge/forge.mdc +7 -6
  165. package/adapters/cursor/skills/forge/references/pipeline-state-schema.json +4 -2
  166. package/adapters/cursor/skills/forge/references/shared-conventions.md +63 -0
  167. package/adapters/cursor/skills/forge/references/stage-exit-protocol.md +344 -140
  168. package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +7 -2
  169. package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +24 -24
  170. package/adapters/cursor/skills/forge-0-epic/references/pipeline-state-schema.json +4 -2
  171. package/adapters/cursor/skills/forge-0-epic/references/shared-conventions.md +63 -0
  172. package/adapters/cursor/skills/forge-0-epic/references/stage-exit-protocol.md +344 -140
  173. package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +14 -3
  174. package/adapters/cursor/skills/forge-1-prd/references/shared-conventions.md +63 -0
  175. package/adapters/cursor/skills/forge-1-prd/references/stage-exit-protocol.md +344 -140
  176. package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +13 -3
  177. package/adapters/cursor/skills/forge-2-tech/references/shared-conventions.md +63 -0
  178. package/adapters/cursor/skills/forge-2-tech/references/stage-exit-protocol.md +344 -140
  179. package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +4 -2
  180. package/adapters/cursor/skills/forge-3-specs/references/shared-conventions.md +63 -0
  181. package/adapters/cursor/skills/forge-3-specs/references/spec-archetypes.md +9 -0
  182. package/adapters/cursor/skills/forge-3-specs/references/stage-exit-protocol.md +344 -140
  183. package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +16 -3
  184. package/adapters/cursor/skills/forge-4-backlog/references/shared-conventions.md +63 -0
  185. package/adapters/cursor/skills/forge-4-backlog/references/stage-exit-protocol.md +344 -140
  186. package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +40 -42
  187. package/adapters/cursor/skills/forge-5-loop/references/result-reporting.md +70 -31
  188. package/adapters/cursor/skills/forge-5-loop/references/runner-contract.md +4 -2
  189. package/adapters/cursor/skills/forge-5-loop/references/shared-conventions.md +63 -0
  190. package/adapters/cursor/skills/forge-5-loop/references/stage-exit-protocol.md +344 -140
  191. package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +48 -4
  192. package/adapters/cursor/skills/forge-6-docs/references/shared-conventions.md +63 -0
  193. package/adapters/cursor/skills/forge-6-docs/references/stage-exit-protocol.md +472 -0
  194. package/adapters/cursor/skills/forge-fix/forge-fix.mdc +84 -32
  195. package/adapters/cursor/skills/forge-fix/references/shared-conventions.md +63 -0
  196. package/adapters/cursor/skills/forge-fix/references/stage-exit-protocol.md +344 -140
  197. package/adapters/cursor/skills/forge-guide/references/forge-config-schema.json +1 -1
  198. package/adapters/cursor/skills/forge-guide/references/shared-conventions.md +63 -0
  199. package/adapters/cursor/skills/forge-verify/forge-verify.mdc +72 -40
  200. package/adapters/cursor/skills/forge-verify/references/findings-template.md +39 -49
  201. package/adapters/cursor/skills/forge-verify/references/shared-conventions.md +63 -0
  202. package/adapters/cursor/skills/forge-verify/references/stage-exit-protocol.md +472 -0
  203. package/adapters/cursor/skills/forge-verify/references/verification-checklists/epic.md +4 -3
  204. package/adapters/cursor/skills/forge-verify/references/verification-checklists/specs.md +2 -0
  205. package/adapters/gemini/.feature-forge-bundle.json +1 -1
  206. package/adapters/gemini/gemini-extension.json +1 -1
  207. package/adapters/gemini/references/epic-manifest-schema.json +6 -1
  208. package/adapters/gemini/references/forge-config-schema.json +1 -1
  209. package/adapters/gemini/references/pipeline-state-schema.json +4 -2
  210. package/adapters/gemini/references/shared-conventions.md +63 -0
  211. package/adapters/gemini/references/stage-exit-protocol.md +344 -140
  212. package/adapters/gemini/scripts/epic-manifest.py +413 -87
  213. package/adapters/gemini/scripts/forge-bootstrap.py +57 -5
  214. package/adapters/gemini/scripts/forge-session.py +3136 -139
  215. package/adapters/gemini/scripts/validate-traceability.py +86 -5
  216. package/adapters/gemini/skills/forge/forge.md +7 -6
  217. package/adapters/gemini/skills/forge/references/pipeline-state-schema.json +4 -2
  218. package/adapters/gemini/skills/forge/references/shared-conventions.md +63 -0
  219. package/adapters/gemini/skills/forge/references/stage-exit-protocol.md +344 -140
  220. package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +7 -2
  221. package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +24 -24
  222. package/adapters/gemini/skills/forge-0-epic/references/pipeline-state-schema.json +4 -2
  223. package/adapters/gemini/skills/forge-0-epic/references/shared-conventions.md +63 -0
  224. package/adapters/gemini/skills/forge-0-epic/references/stage-exit-protocol.md +344 -140
  225. package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +14 -3
  226. package/adapters/gemini/skills/forge-1-prd/references/shared-conventions.md +63 -0
  227. package/adapters/gemini/skills/forge-1-prd/references/stage-exit-protocol.md +344 -140
  228. package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +13 -3
  229. package/adapters/gemini/skills/forge-2-tech/references/shared-conventions.md +63 -0
  230. package/adapters/gemini/skills/forge-2-tech/references/stage-exit-protocol.md +344 -140
  231. package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +4 -2
  232. package/adapters/gemini/skills/forge-3-specs/references/shared-conventions.md +63 -0
  233. package/adapters/gemini/skills/forge-3-specs/references/spec-archetypes.md +9 -0
  234. package/adapters/gemini/skills/forge-3-specs/references/stage-exit-protocol.md +344 -140
  235. package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +16 -3
  236. package/adapters/gemini/skills/forge-4-backlog/references/shared-conventions.md +63 -0
  237. package/adapters/gemini/skills/forge-4-backlog/references/stage-exit-protocol.md +344 -140
  238. package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +40 -42
  239. package/adapters/gemini/skills/forge-5-loop/references/result-reporting.md +70 -31
  240. package/adapters/gemini/skills/forge-5-loop/references/runner-contract.md +4 -2
  241. package/adapters/gemini/skills/forge-5-loop/references/shared-conventions.md +63 -0
  242. package/adapters/gemini/skills/forge-5-loop/references/stage-exit-protocol.md +344 -140
  243. package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +48 -4
  244. package/adapters/gemini/skills/forge-6-docs/references/shared-conventions.md +63 -0
  245. package/adapters/gemini/skills/forge-6-docs/references/stage-exit-protocol.md +472 -0
  246. package/adapters/gemini/skills/forge-fix/forge-fix.md +84 -32
  247. package/adapters/gemini/skills/forge-fix/references/shared-conventions.md +63 -0
  248. package/adapters/gemini/skills/forge-fix/references/stage-exit-protocol.md +344 -140
  249. package/adapters/gemini/skills/forge-guide/references/forge-config-schema.json +1 -1
  250. package/adapters/gemini/skills/forge-guide/references/shared-conventions.md +63 -0
  251. package/adapters/gemini/skills/forge-verify/forge-verify.md +72 -40
  252. package/adapters/gemini/skills/forge-verify/references/findings-template.md +39 -49
  253. package/adapters/gemini/skills/forge-verify/references/shared-conventions.md +63 -0
  254. package/adapters/gemini/skills/forge-verify/references/stage-exit-protocol.md +472 -0
  255. package/adapters/gemini/skills/forge-verify/references/verification-checklists/epic.md +4 -3
  256. package/adapters/gemini/skills/forge-verify/references/verification-checklists/specs.md +2 -0
  257. package/adapters/pi/.feature-forge-bundle.json +1 -1
  258. package/adapters/pi/references/epic-manifest-schema.json +6 -1
  259. package/adapters/pi/references/forge-config-schema.json +1 -1
  260. package/adapters/pi/references/pipeline-state-schema.json +4 -2
  261. package/adapters/pi/references/shared-conventions.md +63 -0
  262. package/adapters/pi/references/stage-exit-protocol.md +344 -140
  263. package/adapters/pi/scripts/epic-manifest.py +413 -87
  264. package/adapters/pi/scripts/forge-bootstrap.py +57 -5
  265. package/adapters/pi/scripts/forge-session.py +3136 -139
  266. package/adapters/pi/scripts/validate-traceability.py +86 -5
  267. package/adapters/pi/skills/forge/SKILL.md +7 -6
  268. package/adapters/pi/skills/forge/references/pipeline-state-schema.json +4 -2
  269. package/adapters/pi/skills/forge/references/shared-conventions.md +63 -0
  270. package/adapters/pi/skills/forge/references/stage-exit-protocol.md +344 -140
  271. package/adapters/pi/skills/forge-0-epic/SKILL.md +7 -2
  272. package/adapters/pi/skills/forge-0-epic/references/edit-mode.md +24 -24
  273. package/adapters/pi/skills/forge-0-epic/references/pipeline-state-schema.json +4 -2
  274. package/adapters/pi/skills/forge-0-epic/references/shared-conventions.md +63 -0
  275. package/adapters/pi/skills/forge-0-epic/references/stage-exit-protocol.md +344 -140
  276. package/adapters/pi/skills/forge-1-prd/SKILL.md +14 -3
  277. package/adapters/pi/skills/forge-1-prd/references/shared-conventions.md +63 -0
  278. package/adapters/pi/skills/forge-1-prd/references/stage-exit-protocol.md +344 -140
  279. package/adapters/pi/skills/forge-2-tech/SKILL.md +13 -3
  280. package/adapters/pi/skills/forge-2-tech/references/shared-conventions.md +63 -0
  281. package/adapters/pi/skills/forge-2-tech/references/stage-exit-protocol.md +344 -140
  282. package/adapters/pi/skills/forge-3-specs/SKILL.md +4 -2
  283. package/adapters/pi/skills/forge-3-specs/references/shared-conventions.md +63 -0
  284. package/adapters/pi/skills/forge-3-specs/references/spec-archetypes.md +9 -0
  285. package/adapters/pi/skills/forge-3-specs/references/stage-exit-protocol.md +344 -140
  286. package/adapters/pi/skills/forge-4-backlog/SKILL.md +16 -3
  287. package/adapters/pi/skills/forge-4-backlog/references/shared-conventions.md +63 -0
  288. package/adapters/pi/skills/forge-4-backlog/references/stage-exit-protocol.md +344 -140
  289. package/adapters/pi/skills/forge-5-loop/SKILL.md +40 -42
  290. package/adapters/pi/skills/forge-5-loop/references/result-reporting.md +70 -31
  291. package/adapters/pi/skills/forge-5-loop/references/runner-contract.md +4 -2
  292. package/adapters/pi/skills/forge-5-loop/references/shared-conventions.md +63 -0
  293. package/adapters/pi/skills/forge-5-loop/references/stage-exit-protocol.md +344 -140
  294. package/adapters/pi/skills/forge-6-docs/SKILL.md +48 -4
  295. package/adapters/pi/skills/forge-6-docs/references/shared-conventions.md +63 -0
  296. package/adapters/pi/skills/forge-6-docs/references/stage-exit-protocol.md +472 -0
  297. package/adapters/pi/skills/forge-fix/SKILL.md +84 -32
  298. package/adapters/pi/skills/forge-fix/references/shared-conventions.md +63 -0
  299. package/adapters/pi/skills/forge-fix/references/stage-exit-protocol.md +344 -140
  300. package/adapters/pi/skills/forge-guide/references/forge-config-schema.json +1 -1
  301. package/adapters/pi/skills/forge-guide/references/shared-conventions.md +63 -0
  302. package/adapters/pi/skills/forge-verify/SKILL.md +72 -40
  303. package/adapters/pi/skills/forge-verify/references/findings-template.md +39 -49
  304. package/adapters/pi/skills/forge-verify/references/shared-conventions.md +63 -0
  305. package/adapters/pi/skills/forge-verify/references/stage-exit-protocol.md +472 -0
  306. package/adapters/pi/skills/forge-verify/references/verification-checklists/epic.md +4 -3
  307. package/adapters/pi/skills/forge-verify/references/verification-checklists/specs.md +2 -0
  308. package/package.json +1 -1
  309. package/adapters/claude/skills/forge-verify/references/pipeline-state-schema.json +0 -191
  310. package/adapters/codex/skills/forge-verify/references/pipeline-state-schema.json +0 -191
  311. package/adapters/copilot/skills/forge-verify/references/pipeline-state-schema.json +0 -191
  312. package/adapters/cursor/skills/forge-verify/references/pipeline-state-schema.json +0 -191
  313. package/adapters/gemini/skills/forge-verify/references/pipeline-state-schema.json +0 -191
  314. package/adapters/pi/skills/forge-verify/references/pipeline-state-schema.json +0 -191
@@ -36,10 +36,10 @@ from typing import Final, Literal, TypedDict
36
36
 
37
37
 
38
38
  # --------------------------------------------------------------------------- #
39
- # Constants (00-core-definitions.md §6)
39
+ # Constants
40
40
  # --------------------------------------------------------------------------- #
41
41
 
42
- #: A safe feature/epic name: one kebab-case token (00 §6).
42
+ #: A safe feature/epic name: one kebab-case token.
43
43
  SAFE_NAME_RE: Final = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
44
44
  #: A directory is "feature-shaped" iff it directly contains this file.
45
45
  PIPELINE_STATE_FILENAME: Final = ".pipeline-state.json"
@@ -55,10 +55,17 @@ NARRATIVE_FILENAME: Final = "EPIC.md"
55
55
  #: NOTE: forge-session.py keeps a byte-identical copy of this constant — flat, self-
56
56
  #: contained scripts have no shared import module (each is copied verbatim into adapters).
57
57
  KNOWN_VERIFY_STATUSES: Final = frozenset(
58
- {"pending", "passed", "findings-reported", "findings-applied", "skipped"}
58
+ {
59
+ "pending",
60
+ "auto-verify-pending",
61
+ "passed",
62
+ "findings-reported",
63
+ "findings-applied",
64
+ "skipped",
65
+ }
59
66
  )
60
67
  #: The subset of KNOWN_VERIFY_STATUSES that makes a member's forge-verify-impl count as
61
- #: complete-for-orchestration (00 §7). A STRICT subset — 'findings-reported' (unfixed),
68
+ #: complete-for-orchestration. A STRICT subset — 'findings-reported' (unfixed),
62
69
  #: 'skipped', and 'pending' do NOT unblock dependents. Not collapsible into the set above.
63
70
  _VERIFY_ORCH_COMPLETE: Final = frozenset({"passed", "findings-applied"})
64
71
 
@@ -77,9 +84,44 @@ _PRODUCTION_STAGES: Final = (
77
84
  "forge-6-docs",
78
85
  )
79
86
 
87
+ #: The served production stage behind each ``forge-verify-{token}`` member entry, in
88
+ #: pipeline order — the inverse of ``VERIFY_TOKEN_BY_STAGE`` in forge-session.py. Used
89
+ #: to NAME the stage owed automatic verification, and iterated (rather than
90
+ #: iterating the state file's own key order) so the warning order is deterministic
91
+ #: regardless of how a member's state document happens to be serialized.
92
+ _VERIFY_STAGE_BY_TOKEN: Final[dict[str, str]] = {
93
+ "prd": "forge-1-prd",
94
+ "tech": "forge-2-tech",
95
+ "specs": "forge-3-specs",
96
+ "backlog": "forge-4-backlog",
97
+ "impl": "forge-5-loop",
98
+ }
99
+
100
+ #: Epic-scoped verification state lives beside the manifest, NEVER in a member's
101
+ #: .pipeline-state.json (REQ-SEC-01). Mirrors ``EPIC_STATE_FILENAME`` in
102
+ #: forge-session.py.
103
+ EPIC_STATE_FILENAME: Final = ".epic-state.json"
104
+ #: The single verify entry an epic root carries.
105
+ EPIC_VERIFY_KEY: Final = "forge-verify-epic"
106
+ #: The stage identifier epic verification is served for — used in diagnostics only.
107
+ EPIC_VERIFY_STAGE: Final = "forge-0-epic"
108
+
109
+ #: The statuses that count as a RESOLVED verification for freshness classification.
110
+ #: Mirrors ``_VERIFY_RESOLVED`` in forge-session.py. ``auto-verify-pending`` is
111
+ #: deliberately absent: recorded debt is owed, not resolved (REQ-DEBT-02).
112
+ _VERIFY_RESOLVED: Final = frozenset({"passed", "findings-applied", "skipped"})
113
+
114
+ #: The obligation sentence. Mirrors ``AUTO_PENDING_DIAGNOSTIC`` in
115
+ #: forge-session.py so the navigator ledger and the epic dashboard say the same thing
116
+ #: about the same debt; the two scripts share no import module.
117
+ AUTO_PENDING_DIAGNOSTIC: Final = (
118
+ "{subject}: automatic verification is still pending for {stage}; "
119
+ "run {command} to resolve it."
120
+ )
121
+
80
122
 
81
123
  # --------------------------------------------------------------------------- #
82
- # Type Definitions (00-core-definitions.md §4, §5; 02 §8.4)
124
+ # Type Definitions
83
125
  # --------------------------------------------------------------------------- #
84
126
 
85
127
  FindingCode = Literal[
@@ -114,12 +156,12 @@ class Finding(TypedDict):
114
156
  DerivedStatus = Literal[
115
157
  "not-started", # no .pipeline-state.json, or all stages pending
116
158
  "in-progress", # at least one stage started, loop not complete-for-orchestration
117
- "complete", # complete-for-orchestration per 00 §7
159
+ "complete", # complete-for-orchestration
118
160
  ]
119
161
 
120
162
 
121
163
  class FeatureStatus(TypedDict):
122
- """Live per-feature status derived from its own pipeline state (00 §5).
164
+ """Live per-feature status derived from its own pipeline state.
123
165
 
124
166
  Attributes:
125
167
  name: Feature name.
@@ -129,7 +171,7 @@ class FeatureStatus(TypedDict):
129
171
  navigator status semantics for display.
130
172
  blocked: True if any entry in unmetDeps is non-empty.
131
173
  unmetDeps: Names of this feature's direct dependencies that are not yet
132
- complete-for-orchestration (00 §7). Empty when actionable or complete.
174
+ complete-for-orchestration. Empty when actionable or complete.
133
175
  openEpicChangeRequests: Count of this member's ``epicChangeRequests``
134
176
  entries with ``status == "open"`` — epic-level change requests raised
135
177
  by a member stage that forge-0-epic edit mode has not yet reconciled.
@@ -149,30 +191,35 @@ class FeatureStatus(TypedDict):
149
191
 
150
192
 
151
193
  class Rollup(TypedDict):
152
- """Aggregate completion counts for the epic dashboard (00 §8)."""
194
+ """Aggregate completion counts for the epic dashboard."""
153
195
 
154
- complete: int #: Number of member features complete-for-orchestration (00 §7).
196
+ complete: int #: Number of member features complete-for-orchestration.
155
197
  total: int #: Total member features in the manifest (0 for an empty epic).
156
198
 
157
199
 
158
200
  class RenderStatus(TypedDict):
159
- """The full live dashboard payload returned by render_status (00 §5, §8).
201
+ """The full live dashboard payload returned by render_status.
160
202
 
161
203
  Attributes:
162
204
  epic: The epic name (manifest `epic`).
163
- status: The epic lifecycle status (00 §2.1).
205
+ status: The epic lifecycle status.
164
206
  features: Per-member status rows, one per manifest feature (may be empty).
165
207
  actionable: Names of features whose dependsOn are all complete and that
166
- are not themselves complete (00 §8).
208
+ are not themselves complete.
167
209
  parallelEligible: Subset of `actionable` with no mutual (transitive)
168
- dependency — surfaced for future parallel execution (00 §8).
210
+ dependency — surfaced for future parallel execution.
169
211
  rollup: Aggregate {complete, total} counts.
170
212
  nextCommand: Recommended next command for the first actionable feature, or
171
213
  None when nothing is actionable (all complete, empty epic, or paused).
172
214
  warnings: Human-readable diagnostics that do NOT invalidate the graph but
173
- need surfacing — currently, members carrying a ``forge-verify-*.status``
174
- outside ``KNOWN_VERIFY_STATUSES`` (treated as incomplete, but silently so
175
- would poison the rollup + dependency gates, #148). Empty in the common case.
215
+ need surfacing. Two kinds, in this order: members carrying a
216
+ ``forge-verify-*.status`` outside ``KNOWN_VERIFY_STATUSES`` (treated as
217
+ incomplete, but silently so would poison the rollup + dependency gates,
218
+ #148), then OBLIGATION warnings for owed automatic verification — first
219
+ per member in pipeline-stage order, then the epic root.
220
+ The two are deliberately distinct: an unknown status is a corrupt value,
221
+ owed debt is a valid value naming work that has not happened. Empty in
222
+ the common case.
176
223
  """
177
224
 
178
225
  epic: str
@@ -186,7 +233,7 @@ class RenderStatus(TypedDict):
186
233
 
187
234
 
188
235
  # --------------------------------------------------------------------------- #
189
- # Internal Exceptions (02 §2)
236
+ # Internal Exceptions
190
237
  # --------------------------------------------------------------------------- #
191
238
 
192
239
 
@@ -209,7 +256,7 @@ class UsageError(Exception):
209
256
  class FindingsError(Exception):
210
257
  """A non-fatal validation outcome that must exit 1.
211
258
 
212
- Raised when an operation produces one or more Findings (00 §4) that block a
259
+ Raised when an operation produces one or more Findings that block a
213
260
  gating operation: a cycle, a dangling ref, an ambiguous/not-found name, etc.
214
261
  Maps to exit code 1. Carries the structured findings so the dispatch layer
215
262
  can emit them as JSON or human lines.
@@ -224,7 +271,7 @@ class FindingsError(Exception):
224
271
 
225
272
 
226
273
  # --------------------------------------------------------------------------- #
227
- # Safety & I/O Layer (02 §3)
274
+ # Safety & I/O Layer
228
275
  # --------------------------------------------------------------------------- #
229
276
 
230
277
 
@@ -243,7 +290,7 @@ def assert_safe_name(name: str) -> None:
243
290
  equals '..', or fails SAFE_NAME_RE. The message embeds the
244
291
  offending name (e.g. ``unsafe name '../escape'``) so the caller can
245
292
  surface it verbatim. Corresponds to the 'unsafe-name' Finding code
246
- (00 §4) but is raised as a usage error because it is detected before
293
+ but is raised as a usage error because it is detected before
247
294
  any manifest is read.
248
295
  """
249
296
  if (
@@ -278,7 +325,7 @@ def contained_path(base: Path, *parts: str) -> Path:
278
325
  UsageError: If the resolved path escapes ``base`` (message:
279
326
  ``resolved path escapes specs dir: …``). Containment violations
280
327
  surface only as exit-2 usage errors per the error model in
281
- tech-spec §6 (there is no dedicated Finding code for them).
328
+ the manifest contract (there is no dedicated Finding code for them).
282
329
  """
283
330
  base_real = base.resolve()
284
331
  target = (base_real / Path(*parts)).resolve()
@@ -296,6 +343,12 @@ def load_manifest(epic_dir: Path) -> dict:
296
343
  epic_dir: The epic subtree directory (must already be contained within
297
344
  {specsDir} via contained_path).
298
345
 
346
+ A legacy manifest written before the canonical ``revision`` field existed is
347
+ presented as logical ``revision: 1`` in the returned dict WITHOUT rewriting the
348
+ file. Legacy validation and rendering therefore keep working, and the
349
+ file's bytes only change on its first genuine semantic mutation — which writes
350
+ ``revision: 2`` (REQ-DEBT-06, REQ-COMPAT-02).
351
+
299
352
  Returns:
300
353
  The parsed manifest as a plain dict. Structural validation (schema,
301
354
  cycles, dangling refs) is performed separately by ``validate`` — this
@@ -304,7 +357,7 @@ def load_manifest(epic_dir: Path) -> dict:
304
357
  Raises:
305
358
  UsageError: If the manifest file is missing or unreadable (exit 2).
306
359
  FindingsError: If the file exists but is not parseable JSON — emits a
307
- single 'corrupt-json' Finding (00 §4) with the JSON error position,
360
+ single 'corrupt-json' Finding with the JSON error position,
308
361
  so a hand-corrupted manifest fails with an actionable message rather
309
362
  than a traceback (REQ-ROBUST-02). Exit 1.
310
363
  """
@@ -316,7 +369,7 @@ def load_manifest(epic_dir: Path) -> dict:
316
369
  except OSError as exc:
317
370
  raise UsageError(f"cannot read manifest {path}: {exc}")
318
371
  try:
319
- return json.loads(text)
372
+ parsed = json.loads(text)
320
373
  except json.JSONDecodeError as exc:
321
374
  raise FindingsError([
322
375
  {
@@ -325,6 +378,9 @@ def load_manifest(epic_dir: Path) -> dict:
325
378
  "feature": None,
326
379
  }
327
380
  ])
381
+ if isinstance(parsed, dict) and "revision" not in parsed:
382
+ parsed["revision"] = 1 # synthesized only — never written back here.
383
+ return parsed
328
384
 
329
385
 
330
386
  def atomic_write(path: Path, data: dict) -> None:
@@ -373,12 +429,12 @@ def atomic_write(path: Path, data: dict) -> None:
373
429
 
374
430
 
375
431
  # --------------------------------------------------------------------------- #
376
- # Graph Algorithms (02 §4) — implemented in item 004
432
+ # Graph Algorithms — implemented in item 004
377
433
  # --------------------------------------------------------------------------- #
378
434
 
379
435
 
380
436
  def find_cycle(features: list[dict]) -> list[str] | None:
381
- """Return a cycle in the dependsOn graph, or None if acyclic (02 §4).
437
+ """Return a cycle in the dependsOn graph, or None if acyclic.
382
438
 
383
439
  Iterative DFS over the directed graph whose edges are ``feature -> dep``.
384
440
  On the first back-edge into a GRAY node, reconstructs and returns the cycle
@@ -429,7 +485,7 @@ def find_cycle(features: list[dict]) -> list[str] | None:
429
485
  def unmet_deps(
430
486
  name: str, features: list[dict], complete: dict[str, bool]
431
487
  ) -> list[str]:
432
- """Return a feature's direct dependencies that are not complete (02 §4).
488
+ """Return a feature's direct dependencies that are not complete.
433
489
 
434
490
  Names of this feature's direct ``dependsOn`` entries whose value in
435
491
  ``complete`` is False, preserving manifest order. Empty when the feature is
@@ -441,15 +497,15 @@ def unmet_deps(
441
497
 
442
498
 
443
499
  # --------------------------------------------------------------------------- #
444
- # Resolution & Uniqueness (02 §5) — implemented in item 005
500
+ # Resolution & Uniqueness — implemented in item 005
445
501
  # --------------------------------------------------------------------------- #
446
502
 
447
503
 
448
504
  def feature_dirs(specs_dir: Path) -> dict[str, list[Path]]:
449
- """Map every feature name in the specs tree to the dirs that bear it (02 §5).
505
+ """Map every feature name in the specs tree to the dirs that bear it.
450
506
 
451
507
  Scans both layouts to a fixed depth, treating a directory as a feature iff
452
- it directly contains a ``.pipeline-state.json`` (00 §6, REQ-DIR-03):
508
+ it directly contains a ``.pipeline-state.json`` (REQ-DIR-03):
453
509
  * flat: {specsDir}/{name}/.pipeline-state.json
454
510
  * nested: {specsDir}/{epic}/{name}/.pipeline-state.json
455
511
 
@@ -457,7 +513,7 @@ def feature_dirs(specs_dir: Path) -> dict[str, list[Path]]:
457
513
  entry is a uniqueness violation (REQ-DIR-04) surfaced as 'ambiguous' or
458
514
  'duplicate-name' by the caller. Epic directories themselves (which hold
459
515
  ``epic-manifest.json`` but no ``.pipeline-state.json``) are skipped, so an
460
- epic name never collides with a feature name (01 §4.3).
516
+ epic name never collides with a feature name.
461
517
 
462
518
  Args:
463
519
  specs_dir: The configured specs directory (already verified to exist).
@@ -480,9 +536,9 @@ def feature_dirs(specs_dir: Path) -> dict[str, list[Path]]:
480
536
 
481
537
 
482
538
  def resolve(name: str, specs_dir: Path) -> Path:
483
- """Resolve a bare feature/epic name to its absolute directory (02 §5).
539
+ """Resolve a bare feature/epic name to its absolute directory.
484
540
 
485
- Implements the 5-step algorithm (tech-spec §3.4):
541
+ Implements the 5-step algorithm:
486
542
  1. reject unsafe names (assert_safe_name) — exit 2 before any FS access;
487
543
  2. flat match: {specsDir}/{name}/.pipeline-state.json wins outright;
488
544
  3. exactly one nested match resolves cleanly;
@@ -502,7 +558,7 @@ def resolve(name: str, specs_dir: Path) -> Path:
502
558
  Raises:
503
559
  UsageError: Unsafe name or missing specs dir (exit 2).
504
560
  FindingsError: 'ambiguous' (lists every matching path) or 'not-found'
505
- (exit 1). 00 §4.2 gives the canonical message shapes.
561
+ (exit 1).
506
562
  """
507
563
  assert_safe_name(name)
508
564
  if not specs_dir.is_dir():
@@ -530,10 +586,10 @@ def resolve(name: str, specs_dir: Path) -> Path:
530
586
 
531
587
 
532
588
  def check_name(name: str, specs_dir: Path) -> list[Finding]:
533
- """Return a duplicate-name finding if the name is already taken (02 §6.3).
589
+ """Return a duplicate-name finding if the name is already taken.
534
590
 
535
591
  Used by forge-0-epic before creating a new member feature so no NEW global
536
- name collision can be introduced (REQ-DIR-04, tech-spec §3.4). Any single
592
+ name collision can be introduced (REQ-DIR-04). Any single
537
593
  existing occurrence is enough to reject — unlike ``resolve``, which tolerates
538
594
  a uniquely-matching name and only errors on genuine multi-match.
539
595
 
@@ -566,43 +622,44 @@ def check_name(name: str, specs_dir: Path) -> list[Finding]:
566
622
 
567
623
 
568
624
  # --------------------------------------------------------------------------- #
569
- # Validation (02 §6.2, §10) — implemented in item 006
625
+ # Validation — implemented in item 006
570
626
  # --------------------------------------------------------------------------- #
571
627
 
572
628
 
573
- #: Top-level required keys (00 §2.1, mirrors epic-manifest-schema.json).
629
+ #: Top-level required keys (mirrors epic-manifest-schema.json). Doubles as
630
+ #: the allow-list for the unknown-top-level-key check below.
574
631
  _TOP_REQUIRED: Final = (
575
- "schemaVersion", "epic", "description", "status",
632
+ "schemaVersion", "revision", "epic", "description", "status",
576
633
  "narrativeDoc", "createdAt", "updatedAt", "features",
577
634
  )
578
- #: Required keys on each Feature object (00 §2.2).
635
+ #: Required keys on each Feature object.
579
636
  _FEATURE_REQUIRED: Final = ("name", "charter", "dependsOn", "exposes", "consumes")
580
637
  #: Optional keys on each Feature object. `mutatesShared` is the #144 precision
581
638
  #: hint for cross-member coupling (array of project-root-relative path strings);
582
639
  #: schema-legal when present, ignored when absent (mirrors
583
640
  #: epic-manifest-schema.json definitions.feature.properties.mutatesShared).
584
641
  _FEATURE_OPTIONAL: Final = ("mutatesShared",)
585
- #: Required keys on each Contract (exposes[]) object (00 §2.3).
642
+ #: Required keys on each Contract (exposes[]) object.
586
643
  _CONTRACT_REQUIRED: Final = ("name", "kind", "summary")
587
- #: Required keys on each ConsumedContract (consumes[]) object (00 §2.4).
644
+ #: Required keys on each ConsumedContract (consumes[]) object.
588
645
  _CONSUMED_REQUIRED: Final = ("from", "name", "summary")
589
- #: Allowed epic lifecycle states (00 §2.1).
646
+ #: Allowed epic lifecycle states.
590
647
  _EPIC_STATUSES: Final = ("active", "paused", "abandoned", "complete")
591
- #: Allowed Contract kinds (00 §2.3).
648
+ #: Allowed Contract kinds.
592
649
  _CONTRACT_KINDS: Final = ("function", "type", "endpoint", "module", "event")
593
650
 
594
651
 
595
652
  def _schema(message: str, feature: str | None = None) -> Finding:
596
- """Construct a 'schema' Finding (00 §4)."""
653
+ """Construct a 'schema' Finding."""
597
654
  return {"code": "schema", "message": message, "feature": feature}
598
655
 
599
656
 
600
657
  def _schema_findings(manifest: dict) -> list[Finding]:
601
- """Hand-rolled stdlib schema checker over the manifest (02 §6.2, 00 §2.6).
658
+ """Hand-rolled stdlib schema checker over the manifest.
602
659
 
603
- Asserts required keys/types/enums/consts from 00 §2 and explicitly rejects
660
+ Asserts the required keys/types/enums/consts and explicitly rejects
604
661
  any ``features[].status`` key (REQ-STATE-02 -> 'cached-status'). No
605
- third-party ``jsonschema`` (01 §2.1). Returns 'schema' findings plus, for a
662
+ third-party ``jsonschema``. Returns 'schema' findings plus, for a
606
663
  per-feature status key, a 'cached-status' finding.
607
664
  """
608
665
  findings: list[Finding] = []
@@ -615,6 +672,12 @@ def _schema_findings(manifest: dict) -> list[Finding]:
615
672
 
616
673
  if "schemaVersion" in manifest and manifest["schemaVersion"] != 1:
617
674
  findings.append(_schema(f"schemaVersion must be 1, got {manifest['schemaVersion']!r}"))
675
+ if "revision" in manifest:
676
+ revision = manifest["revision"]
677
+ # `bool` is a subclass of `int`, so `True` would otherwise pass as revision 1
678
+ # and then silently arithmetic-increment to 2.
679
+ if isinstance(revision, bool) or not isinstance(revision, int) or revision < 1:
680
+ findings.append(_schema(f"revision must be an integer >= 1, got {revision!r}"))
618
681
  if "narrativeDoc" in manifest and manifest["narrativeDoc"] != NARRATIVE_FILENAME:
619
682
  findings.append(_schema(f"narrativeDoc must be {NARRATIVE_FILENAME!r}, got {manifest['narrativeDoc']!r}")) # noqa: E501
620
683
  for key in ("epic", "description", "createdAt", "updatedAt"):
@@ -698,9 +761,9 @@ def _schema_findings(manifest: dict) -> list[Finding]:
698
761
  def _validate_dict(
699
762
  manifest: dict, epic_dir: Path, specs_dir: Path
700
763
  ) -> list[Finding]:
701
- """Validate an already-parsed manifest dict, returning findings (02 §6.2).
764
+ """Validate an already-parsed manifest dict, returning findings.
702
765
 
703
- Runs the invariant checks of 00 §2.6 in order, short-circuiting only where a
766
+ Runs the invariant checks in order, short-circuiting only where a
704
767
  later check cannot run. Reused by the item-008 mutators on the EDITED dict
705
768
  before writing. Does not parse JSON (that is ``validate``'s job) — operates
706
769
  purely in memory.
@@ -788,7 +851,7 @@ def _validate_dict(
788
851
 
789
852
 
790
853
  def validate(epic_dir: Path, specs_dir: Path) -> list[Finding]:
791
- """Validate a single epic manifest, returning all findings (02 §6.2).
854
+ """Validate a single epic manifest, returning all findings.
792
855
 
793
856
  Parses the manifest (folding any corrupt-json finding from load_manifest
794
857
  into the returned list) then delegates to ``_validate_dict``. Raises
@@ -802,12 +865,12 @@ def validate(epic_dir: Path, specs_dir: Path) -> list[Finding]:
802
865
 
803
866
 
804
867
  # --------------------------------------------------------------------------- #
805
- # Live Status Derivation (02 §8) — implemented in item 007
868
+ # Live Status Derivation — implemented in item 007
806
869
  # --------------------------------------------------------------------------- #
807
870
 
808
871
 
809
872
  def is_complete_for_orchestration(state: dict) -> bool:
810
- """Apply the completion-for-orchestration predicate (00 §7, 02 §8.1).
873
+ """Apply the completion-for-orchestration predicate.
811
874
 
812
875
  A feature is complete-for-orchestration iff::
813
876
 
@@ -817,8 +880,7 @@ def is_complete_for_orchestration(state: dict) -> bool:
817
880
 
818
881
  A feature whose forge-verify-impl is 'findings-reported' (unfixed) is NOT
819
882
  complete and does NOT unblock dependents (REQ-ORCH-01). This is the single
820
- implementation of the predicate, reused by the dependency gate and handoff
821
- (04-pipeline-integration.md).
883
+ implementation of the predicate, reused by the dependency gate and handoff.
822
884
 
823
885
  Args:
824
886
  state: A parsed .pipeline-state.json dict (or {} if the member has none).
@@ -851,6 +913,11 @@ def _verify_status_warnings(name: str, state: dict) -> list[str]:
851
913
 
852
914
  Non-string / malformed status values (a list, an int) are also flagged, and the
853
915
  membership test is guarded so an unhashable value never raises.
916
+
917
+ ``auto-verify-pending`` is a KNOWN status and is therefore silent here.
918
+ Owed automatic verification is an OBLIGATION, not a corrupt value, so it is
919
+ surfaced separately by ``_auto_verify_debt_warnings`` with actionable wording —
920
+ reporting it as an unknown status would be actively misleading.
854
921
  """
855
922
  stages = state.get("stages", {})
856
923
  if not isinstance(stages, dict):
@@ -874,11 +941,209 @@ def _verify_status_warnings(name: str, state: dict) -> list[str]:
874
941
  return warnings
875
942
 
876
943
 
944
+ def _positive_int(value: object) -> int | None:
945
+ """Return ``value`` when it is a usable artifact revision, else None.
946
+
947
+ A bool is rejected before the int test on purpose: ``True`` is an ``int`` and
948
+ would otherwise compare equal to revision 1 (the same trap the manifest
949
+ ``revision`` validator guards). Absent, non-integer, and sub-1 values are
950
+ unusable — the caller keeps the debt owed rather than guessing.
951
+ """
952
+ if isinstance(value, bool) or not isinstance(value, int) or value < 1:
953
+ return None
954
+ return value
955
+
956
+
957
+ def _auto_pending_message(
958
+ subject: str,
959
+ stage: str,
960
+ command: str,
961
+ scheduled_version: int | None = None,
962
+ current_version: int | None = None,
963
+ ) -> str:
964
+ """Render the obligation sentence for owed automatic verification.
965
+
966
+ Mirrors ``auto_pending_message`` in forge-session.py — one sentence naming the
967
+ subject, the served stage, and the retry command, with both revision numbers
968
+ appended when the recorded schedule predates the current artifact. Never a
969
+ state-file dump (REQ-OBS-02).
970
+ """
971
+ message = AUTO_PENDING_DIAGNOSTIC.format(
972
+ subject=subject, stage=stage, command=command
973
+ )
974
+ if (
975
+ scheduled_version is not None
976
+ and current_version is not None
977
+ and scheduled_version != current_version
978
+ ):
979
+ message += (
980
+ f" The artifact has advanced since it was scheduled "
981
+ f"(scheduled at revision {scheduled_version}, now at revision "
982
+ f"{current_version})."
983
+ )
984
+ return message
985
+
986
+
987
+ def _auto_verify_debt_warnings(name: str, state: dict) -> list[str]:
988
+ """Surface every ``auto-verify-pending`` member entry as an obligation.
989
+
990
+ A member carrying recorded-but-undischarged automatic verification is neither
991
+ "never verified" nor done: the directive that should have run it was dropped,
992
+ crashed, or was interrupted, and the epic dashboard is where an operator notices
993
+ (#163, REQ-DEBT-02/05). Emitted in pipeline-stage order — not the state file's key
994
+ order — so repeated renders are byte-identical.
995
+
996
+ Args:
997
+ name: The member feature name.
998
+ state: The member's parsed .pipeline-state.json (or {}).
999
+
1000
+ Returns:
1001
+ Zero or more sentences naming the member, the served production
1002
+ stage, and the ``forge-verify`` retry command.
1003
+ """
1004
+ stages = state.get("stages", {})
1005
+ if not isinstance(stages, dict):
1006
+ return []
1007
+ warnings: list[str] = []
1008
+ for token, served_stage in _VERIFY_STAGE_BY_TOKEN.items():
1009
+ entry = stages.get(f"forge-verify-{token}")
1010
+ if not isinstance(entry, dict) or entry.get("status") != "auto-verify-pending":
1011
+ continue
1012
+ production = stages.get(served_stage)
1013
+ warnings.append(_auto_pending_message(
1014
+ name,
1015
+ served_stage,
1016
+ f"/feature-forge:forge-verify {name}",
1017
+ _positive_int(entry.get("scheduledStageVersion")),
1018
+ _positive_int(production.get("version")) if isinstance(production, dict) else None,
1019
+ ))
1020
+ return warnings
1021
+
1022
+
1023
+ def _read_epic_state_safely(epic_dir: Path) -> dict:
1024
+ """Read an epic's own ``.epic-state.json``, tolerating absence and corruption.
1025
+
1026
+ Epic verification state is epic-scoped: this reads the sibling of the manifest
1027
+ and NEVER a member's ``.pipeline-state.json`` (REQ-SEC-01). A missing, unreadable,
1028
+ unparseable, or non-object file downgrades to ``{}``, which classifies as
1029
+ ``never`` — the dashboard must not crash on one torn file.
1030
+ """
1031
+ path = epic_dir / EPIC_STATE_FILENAME
1032
+ if not path.is_file():
1033
+ return {}
1034
+ try:
1035
+ parsed = json.loads(path.read_text(encoding="utf-8"))
1036
+ except (OSError, json.JSONDecodeError):
1037
+ return {}
1038
+ return parsed if isinstance(parsed, dict) else {}
1039
+
1040
+
1041
+ def epic_verify_state(epic_dir: Path, revision: int | None) -> str:
1042
+ """Classify epic-root verification freshness against the manifest revision.
1043
+
1044
+ The epic mirror of ``verify_state`` in forge-session.py, with the manifest
1045
+ ``revision`` standing in for a production stage's ``version``. It reads
1046
+ ``.epic-state.json`` and the supplied revision ONLY — no member state participates
1047
+ in epic verification (REQ-SEC-01).
1048
+
1049
+ Args:
1050
+ epic_dir: The epic subtree directory.
1051
+ revision: The manifest's canonical revision, or None when unusable.
1052
+
1053
+ Returns:
1054
+ One of:
1055
+
1056
+ - ``never`` — no epic state, no ``forge-verify-epic`` entry, or a
1057
+ status outside the known vocabulary. Verification was never scheduled.
1058
+ - ``auto-pending``— ``auto-verify-pending``: scheduled and owed. Classified
1059
+ ahead of every other rule and never downgraded to ``never``, even when its
1060
+ ``scheduledStageVersion`` is missing or names an older revision — a later
1061
+ manifest edit does not erase owed work (REQ-DEBT-02).
1062
+ - ``failing`` — ``findings-reported``: verification ran and its findings
1063
+ are not yet applied.
1064
+ - ``skipped`` — an explicit human decision, resolved under the existing
1065
+ compatibility rule: it records no version and so never goes stale.
1066
+ - ``fresh`` — ``passed`` whose ``verifiedStageVersion`` equals the
1067
+ current manifest revision.
1068
+ - ``stale`` — a resolved terminal entry whose recorded revision is
1069
+ absent or does not match. ``findings-applied`` is classified here
1070
+ UNCONDITIONALLY, not merely because the writer deletes
1071
+ ``verifiedStageVersion``: applying fixes is not verifying them, and legacy
1072
+ state loaded without migration (REQ-DEBT-06) may still carry the key.
1073
+ """
1074
+ return _classify_epic_verify_entry(_read_epic_verify_entry(epic_dir), revision)
1075
+
1076
+
1077
+ def _read_epic_verify_entry(epic_dir: Path) -> dict:
1078
+ """The ``forge-verify-epic`` entry out of ``.epic-state.json``, or ``{}``.
1079
+
1080
+ The single read both the classifier and the warning renderer share: a caller
1081
+ that needs the label AND the entry's metadata reads once and passes the same
1082
+ dict to both, so a file rewritten between two reads can never classify against
1083
+ one snapshot and index into another.
1084
+ """
1085
+ stages = _read_epic_state_safely(epic_dir).get("stages", {})
1086
+ entry = stages.get(EPIC_VERIFY_KEY) if isinstance(stages, dict) else None
1087
+ return entry if isinstance(entry, dict) else {}
1088
+
1089
+
1090
+ def _classify_epic_verify_entry(entry: dict, revision: int | None) -> str:
1091
+ """The classification half of ``epic_verify_state``, over an already-read entry."""
1092
+ if not entry:
1093
+ return "never"
1094
+ status = entry.get("status")
1095
+ if not isinstance(status, str):
1096
+ # A torn or hand-edited entry can carry any JSON type here; an unhashable
1097
+ # one would raise TypeError at the frozenset membership below, crashing
1098
+ # the whole dashboard on one bad file.
1099
+ return "never"
1100
+ if status == "auto-verify-pending":
1101
+ return "auto-pending"
1102
+ if status == "findings-reported":
1103
+ return "failing"
1104
+ if status == "skipped":
1105
+ return "skipped"
1106
+ if status not in _VERIFY_RESOLVED:
1107
+ return "never"
1108
+ if status == "findings-applied":
1109
+ # §4.2 step 4: applying fixes CLEARS freshness; only a later `passed` restores
1110
+ # it. Mirrors the identical guards in forge-session.py's `verify_state` and
1111
+ # `_classify_verify_entry` — §5.1 requires identical labels across all three,
1112
+ # and §5.2 requires manifest parity, so a partial fix is itself the drift
1113
+ # `test_stage_constants_parity.py` exists to catch.
1114
+ return "stale"
1115
+ verified = _positive_int(entry.get("verifiedStageVersion"))
1116
+ if verified is not None and revision is not None and verified == revision:
1117
+ return "fresh"
1118
+ return "stale"
1119
+
1120
+
1121
+ def _epic_verify_warnings(epic: str, epic_dir: Path, revision: int | None) -> list[str]:
1122
+ """Surface owed epic-root automatic verification as an obligation.
1123
+
1124
+ Only ``auto-pending`` warrants a dashboard warning: ``never`` is the ordinary
1125
+ state of an epic nobody has verified, and ``stale``/``failing`` are already
1126
+ reachable through the epic's own verify run. Owed-and-dropped debt is the case
1127
+ that is otherwise invisible.
1128
+ """
1129
+ entry = _read_epic_verify_entry(epic_dir)
1130
+ if _classify_epic_verify_entry(entry, revision) != "auto-pending":
1131
+ return []
1132
+ scheduled = _positive_int(entry.get("scheduledStageVersion"))
1133
+ return [_auto_pending_message(
1134
+ epic,
1135
+ EPIC_VERIFY_STAGE,
1136
+ f"/feature-forge:forge-verify {epic}",
1137
+ scheduled,
1138
+ revision,
1139
+ )]
1140
+
1141
+
877
1142
  def _read_state_safely(state_path: Path) -> dict:
878
1143
  """Read and parse a member's .pipeline-state.json, tolerating corruption.
879
1144
 
880
1145
  A missing, unreadable, unparseable, or torn (partially-written) member state
881
- downgrades to ``{}`` rather than crashing the dashboard (02 §8.2). Member
1146
+ downgrades to ``{}`` rather than crashing the dashboard. Member
882
1147
  state writes are made by forge-1..5 skills outside the helper's atomicity
883
1148
  scope, so a torn read is expected and simply renders that one feature as
884
1149
  ``not-started``.
@@ -893,7 +1158,7 @@ def _read_state_safely(state_path: Path) -> dict:
893
1158
 
894
1159
 
895
1160
  def derive_status(feature_dir: Path) -> FeatureStatus:
896
- """Derive a feature's live status from its own pipeline state (00 §5, 02 §8).
1161
+ """Derive a feature's live status from its own pipeline state.
897
1162
 
898
1163
  Reads ``{feature_dir}/.pipeline-state.json`` and maps it to a FeatureStatus:
899
1164
  missing/unparseable/all-pending -> ``not-started``; complete-for-
@@ -906,7 +1171,7 @@ def derive_status(feature_dir: Path) -> FeatureStatus:
906
1171
  feature_dir: The member feature's directory.
907
1172
 
908
1173
  Returns:
909
- A FeatureStatus (00 §5) with name, stage, coarse status, and placeholder
1174
+ A FeatureStatus with name, stage, coarse status, and placeholder
910
1175
  blocked/unmetDeps.
911
1176
  """
912
1177
  name = feature_dir.name
@@ -949,7 +1214,7 @@ def derive_status(feature_dir: Path) -> FeatureStatus:
949
1214
 
950
1215
 
951
1216
  def _transitive_deps(name: str, adjacency: dict[str, list[str]]) -> set[str]:
952
- """Return all features reachable from ``name`` via dependsOn edges (00 §8)."""
1217
+ """Return all features reachable from ``name`` via dependsOn edges."""
953
1218
  seen: set[str] = set()
954
1219
  stack = list(adjacency.get(name, []))
955
1220
  while stack:
@@ -980,7 +1245,7 @@ def _next_production_stage(state: dict) -> str | None:
980
1245
 
981
1246
 
982
1247
  def _next_command(feature_dir: Path, status_row: FeatureStatus) -> str:
983
- """Recommend the next forge command for an actionable feature (02 §8.3).
1248
+ """Recommend the next forge command for an actionable feature.
984
1249
 
985
1250
  ``/feature-forge:forge-1-prd <name>`` when the feature's PRD is absent (or the
986
1251
  member has not progressed past epic creation), else the command for its next
@@ -989,9 +1254,14 @@ def _next_command(feature_dir: Path, status_row: FeatureStatus) -> str:
989
1254
  reading it here recommended re-running the stage the member had just finished
990
1255
  for the whole window before the next stage was entered).
991
1256
 
992
- When every production stage is complete but the member is still actionable, the
993
- only thing holding it back is unapplied verify findings (``forge-verify-impl``
994
- is ``findings-reported``), so ``forge-fix`` is the accurate recommendation.
1257
+ When every production stage is complete but the member is still actionable, its
1258
+ ``forge-verify-impl`` entry is what holds it back, and the two cases need
1259
+ different commands: ``findings-reported`` means a report exists and is
1260
+ unapplied, so ``forge-fix`` is accurate; anything else outstanding — notably
1261
+ ``auto-verify-pending``, where the scheduled run never happened — has nothing to
1262
+ fix and needs the verification itself, so ``forge-verify`` is. Recommending
1263
+ ``forge-fix`` for owed automatic debt sends the operator looking for a findings
1264
+ document that was never written (REQ-DEBT-02/05).
995
1265
  """
996
1266
  name = status_row["name"]
997
1267
  state = _read_state_safely(feature_dir / PIPELINE_STATE_FILENAME)
@@ -1000,12 +1270,17 @@ def _next_command(feature_dir: Path, status_row: FeatureStatus) -> str:
1000
1270
  if not prd_present or nxt == "forge-1-prd":
1001
1271
  return f"/feature-forge:forge-1-prd {name}"
1002
1272
  if nxt is None:
1003
- return f"/feature-forge:forge-fix {name}"
1273
+ stages = state.get("stages")
1274
+ impl = stages.get("forge-verify-impl") if isinstance(stages, dict) else None
1275
+ status = impl.get("status") if isinstance(impl, dict) else None
1276
+ if status == "findings-reported":
1277
+ return f"/feature-forge:forge-fix {name}"
1278
+ return f"/feature-forge:forge-verify {name}"
1004
1279
  return f"/feature-forge:{nxt} {name}"
1005
1280
 
1006
1281
 
1007
1282
  def render_status(epic_dir: Path, specs_dir: Path) -> RenderStatus:
1008
- """Build the full live dashboard payload for an epic (00 §5, §8; 02 §8.3).
1283
+ """Build the full live dashboard payload for an epic.
1009
1284
 
1010
1285
  Validates first (refusing to render over an invalid graph), then derives each
1011
1286
  member's live status from its own state file, computes blocked/unmetDeps,
@@ -1016,7 +1291,7 @@ def render_status(epic_dir: Path, specs_dir: Path) -> RenderStatus:
1016
1291
  specs_dir: The configured specs directory.
1017
1292
 
1018
1293
  Returns:
1019
- The RenderStatus dict (02 §8.4).
1294
+ The RenderStatus dict.
1020
1295
 
1021
1296
  Raises:
1022
1297
  UsageError: Missing/unreadable manifest (exit 2).
@@ -1043,6 +1318,15 @@ def render_status(epic_dir: Path, specs_dir: Path) -> RenderStatus:
1043
1318
  member_state = _read_state_safely(member_dir / PIPELINE_STATE_FILENAME)
1044
1319
  complete[name] = is_complete_for_orchestration(member_state)
1045
1320
  warnings.extend(_verify_status_warnings(name, member_state))
1321
+ warnings.extend(_auto_verify_debt_warnings(name, member_state))
1322
+
1323
+ # (3b) epic-root verification debt, classified against the manifest revision and
1324
+ # read from .epic-state.json alone — no member state participates.
1325
+ warnings.extend(_epic_verify_warnings(
1326
+ manifest.get("epic", epic_dir.name),
1327
+ epic_dir,
1328
+ _positive_int(manifest.get("revision")),
1329
+ ))
1046
1330
 
1047
1331
  # (4) per-feature unmetDeps + blocked. A feature that is itself complete is
1048
1332
  # never "blocked" — unmet deps only matter for work not yet finished.
@@ -1102,21 +1386,47 @@ def render_status(epic_dir: Path, specs_dir: Path) -> RenderStatus:
1102
1386
 
1103
1387
 
1104
1388
  # --------------------------------------------------------------------------- #
1105
- # Mutators (02 §7) — implemented in item 008
1389
+ # Mutators — implemented in item 008
1106
1390
  # --------------------------------------------------------------------------- #
1107
1391
 
1108
1392
 
1393
+ def _semantic_manifest(manifest: dict) -> dict:
1394
+ """Return a manifest copy carrying only its semantic fields.
1395
+
1396
+ Drops exactly the two bookkeeping fields a mutation is allowed to change on its
1397
+ own: ``updatedAt`` (a timestamp) and ``revision`` (the counter this function's
1398
+ caller maintains, and which ``load_manifest`` synthesizes for legacy files). What
1399
+ remains is the content an epic's verification freshness is actually about.
1400
+ """
1401
+ return {k: v for k, v in manifest.items() if k not in ("updatedAt", "revision")}
1402
+
1403
+
1109
1404
  def _bump_and_write(
1110
1405
  epic_dir: Path, specs_dir: Path, manifest: dict
1111
1406
  ) -> list[Finding]:
1112
- """Re-validate, bump updatedAt, and atomically persist a manifest (02 §7).
1113
-
1114
- The shared tail of every mutator (REQ-ROBUST-03, REQ-OBS-01, REQ-EPIC-05).
1115
- Re-runs ``_validate_dict`` on the EDITED manifest; if any blocking finding is
1116
- present (cycle, dangling-ref, duplicate-name, schema, ...), the on-disk file
1117
- is left byte-identical and the findings are returned so the caller exits 1.
1118
- Otherwise ``updatedAt`` is set to now (UTC, ISO-8601) and the manifest is
1119
- written via ``atomic_write``.
1407
+ """Re-validate, bump revision + updatedAt, and atomically persist.
1408
+
1409
+ The shared tail of every mutator (REQ-ROBUST-03, REQ-OBS-01, REQ-EPIC-05,
1410
+ REQ-REL-01) and the SINGLE place ``revision`` is incremented — no mutator bumps
1411
+ it itself, so every successful mutation advances it exactly once.
1412
+
1413
+ Order of operations:
1414
+
1415
+ 1. Re-run ``_validate_dict`` on the EDITED manifest; if any blocking finding is
1416
+ present (cycle, dangling-ref, duplicate-name, schema, ...), the on-disk file
1417
+ is left byte-identical and the findings are returned so the caller exits 1.
1418
+ This runs FIRST, ahead of the no-op comparison, so that EVERY mutator
1419
+ re-validates (REQ-ROBUST-03): a semantically idempotent edit against a
1420
+ manifest that is already invalid on disk must report the same blocking
1421
+ findings a non-idempotent edit would, instead of exiting 0 in silence and
1422
+ letting a caller read that as "the epic is well-formed".
1423
+ 2. Compare the proposed manifest with the on-disk one, ignoring only
1424
+ ``updatedAt`` and the (possibly synthesized) ``revision``. If every semantic
1425
+ field matches this is a no-op: return ``[]`` WITHOUT writing, so an edit that
1426
+ changes nothing leaves the file byte-identical — including ``updatedAt``.
1427
+ 3. Set ``revision`` to ``current + 1`` and ``updatedAt`` to now (UTC, ISO-8601),
1428
+ then write once via ``atomic_write``. A failed write raises, so a torn
1429
+ mutation leaves the previous revision and bytes intact.
1120
1430
 
1121
1431
  Args:
1122
1432
  epic_dir: The epic subtree directory.
@@ -1124,17 +1434,33 @@ def _bump_and_write(
1124
1434
  manifest: The already-edited in-memory manifest dict.
1125
1435
 
1126
1436
  Returns:
1127
- An empty list on success (write performed); the blocking findings on
1128
- refusal (no write performed).
1437
+ An empty list on success (write performed) OR on a semantic no-op (no write);
1438
+ the blocking findings on refusal (no write performed).
1129
1439
 
1130
1440
  Raises:
1131
1441
  UsageError: If the atomic write itself fails (exit 2).
1132
1442
  """
1443
+ path = epic_dir / MANIFEST_FILENAME
1444
+ try:
1445
+ on_disk: dict | None = load_manifest(epic_dir)
1446
+ except (UsageError, FindingsError):
1447
+ # No readable predecessor (first write, or a corrupt file the caller is
1448
+ # replacing wholesale) — treat every field as changed.
1449
+ on_disk = None
1450
+
1133
1451
  findings = _validate_dict(manifest, epic_dir, specs_dir)
1134
1452
  if findings:
1135
1453
  return findings
1454
+
1455
+ if on_disk is not None and _semantic_manifest(on_disk) == _semantic_manifest(manifest):
1456
+ return []
1457
+
1458
+ current = on_disk.get("revision") if isinstance(on_disk, dict) else None
1459
+ if isinstance(current, bool) or not isinstance(current, int) or current < 1:
1460
+ current = 1 # legacy / malformed predecessor: logical revision 1.
1461
+ manifest["revision"] = current + 1
1136
1462
  manifest["updatedAt"] = datetime.now(timezone.utc).isoformat()
1137
- atomic_write(epic_dir / MANIFEST_FILENAME, manifest)
1463
+ atomic_write(path, manifest)
1138
1464
  return []
1139
1465
 
1140
1466
 
@@ -1145,7 +1471,7 @@ def add_feature(
1145
1471
  charter: str,
1146
1472
  deps: list[str],
1147
1473
  ) -> list[Finding]:
1148
- """Append a new member feature to the manifest (02 §7.1).
1474
+ """Append a new member feature to the manifest.
1149
1475
 
1150
1476
  Appends a ``Feature`` with the given name/charter/dependsOn and EMPTY
1151
1477
  exposes/consumes. Re-validation surfaces a duplicate name (within the
@@ -1181,7 +1507,7 @@ def add_feature(
1181
1507
 
1182
1508
 
1183
1509
  def remove_feature(epic_dir: Path, specs_dir: Path, name: str) -> list[Finding]:
1184
- """Remove a member feature from the manifest (02 §7.2).
1510
+ """Remove a member feature from the manifest.
1185
1511
 
1186
1512
  Drops the named feature from ``features[]``. After removal, re-validation
1187
1513
  surfaces any now-dangling ``dependsOn`` / ``consumes.from`` that pointed at
@@ -1213,10 +1539,10 @@ def remove_feature(epic_dir: Path, specs_dir: Path, name: str) -> list[Finding]:
1213
1539
 
1214
1540
 
1215
1541
  def reorder(epic_dir: Path, specs_dir: Path, order: list[str]) -> list[Finding]:
1216
- """Reorder the manifest features[] to a given permutation (02 §7.3).
1542
+ """Reorder the manifest features[] to a given permutation.
1217
1543
 
1218
1544
  ``order`` must be an exact permutation of the current member names (purely a
1219
- display sequence, not a dependency ordering — 00 §2.1). If it is not, a
1545
+ display sequence, not a dependency ordering). If it is not, a
1220
1546
  ``schema`` finding is returned and the manifest is left unchanged.
1221
1547
 
1222
1548
  Args:
@@ -1246,7 +1572,7 @@ def reorder(epic_dir: Path, specs_dir: Path, order: list[str]) -> list[Finding]:
1246
1572
  def set_dep(
1247
1573
  epic_dir: Path, specs_dir: Path, name: str, deps: list[str]
1248
1574
  ) -> list[Finding]:
1249
- """Replace a member feature's dependsOn list (02 §7.4, §7.6).
1575
+ """Replace a member feature's dependsOn list.
1250
1576
 
1251
1577
  Re-validation enforces every new dependency exists (``dangling-ref``) and the
1252
1578
  resulting graph is acyclic (``cycle``). An empty ``deps`` clears the
@@ -1282,7 +1608,7 @@ def set_dep(
1282
1608
 
1283
1609
 
1284
1610
  def set_status(epic_dir: Path, specs_dir: Path, status: str) -> list[Finding]:
1285
- """Set the epic-level lifecycle status (02 §7.5).
1611
+ """Set the epic-level lifecycle status.
1286
1612
 
1287
1613
  Sets the epic-level ``status`` (the value is constrained to the allowed
1288
1614
  lifecycle states by ``argparse`` ``choices`` before reaching here). Never
@@ -1480,7 +1806,7 @@ def adopt_feature(
1480
1806
 
1481
1807
 
1482
1808
  # --------------------------------------------------------------------------- #
1483
- # CLI Dispatch (02 §9)
1809
+ # CLI Dispatch
1484
1810
  # --------------------------------------------------------------------------- #
1485
1811
 
1486
1812
 
@@ -1509,7 +1835,7 @@ def _emit_findings(findings: list[Finding], as_json: bool) -> None:
1509
1835
 
1510
1836
 
1511
1837
  def _print_status_table(status: RenderStatus) -> None:
1512
- """Print a readable epic dashboard plus the recommended next command (02 §8)."""
1838
+ """Print a readable epic dashboard plus the recommended next command."""
1513
1839
  rollup = status["rollup"]
1514
1840
  print(f"Epic: {status['epic']} [{status['status']}]")
1515
1841
  print(f"Progress: {rollup['complete']}/{rollup['total']} complete")
@@ -1618,7 +1944,7 @@ def _dispatch(args: argparse.Namespace, specs_dir: Path) -> int:
1618
1944
 
1619
1945
 
1620
1946
  def _build_parser() -> argparse.ArgumentParser:
1621
- """Build the argparse parser with one subparser per subcommand (02 §9)."""
1947
+ """Build the argparse parser with one subparser per subcommand."""
1622
1948
  parser = argparse.ArgumentParser(prog="epic-manifest.py", description=__doc__)
1623
1949
  sub = parser.add_subparsers(dest="cmd", required=True)
1624
1950