@garygentry/feature-forge 0.3.0 → 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 (404) hide show
  1. package/adapters/claude/.feature-forge-bundle.json +1 -1
  2. package/adapters/claude/agents/forge-verifier.md +1 -1
  3. package/adapters/claude/references/epic-manifest-schema.json +6 -1
  4. package/adapters/claude/references/forge-config-schema.json +1 -1
  5. package/adapters/claude/references/pipeline-state-schema.json +5 -3
  6. package/adapters/claude/references/shared-conventions.md +125 -12
  7. package/adapters/claude/references/stage-exit-protocol.md +358 -144
  8. package/adapters/claude/references/vendor-construct-inventory.md +1 -1
  9. package/adapters/claude/scripts/epic-manifest.py +459 -90
  10. package/adapters/claude/scripts/forge-bootstrap.py +57 -5
  11. package/adapters/claude/scripts/forge-session.py +4346 -201
  12. package/adapters/claude/scripts/validate-traceability.py +92 -6
  13. package/adapters/claude/skills/forge/SKILL.md +18 -11
  14. package/adapters/claude/skills/forge/references/pipeline-state-schema.json +5 -3
  15. package/adapters/claude/skills/forge/references/shared-conventions.md +125 -12
  16. package/adapters/claude/skills/forge/references/stage-exit-protocol.md +358 -144
  17. package/adapters/claude/skills/forge-0-epic/SKILL.md +8 -3
  18. package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +24 -24
  19. package/adapters/claude/skills/forge-0-epic/references/pipeline-state-schema.json +5 -3
  20. package/adapters/claude/skills/forge-0-epic/references/shared-conventions.md +125 -12
  21. package/adapters/claude/skills/forge-0-epic/references/stage-exit-protocol.md +358 -144
  22. package/adapters/claude/skills/forge-1-prd/SKILL.md +39 -11
  23. package/adapters/claude/skills/forge-1-prd/references/shared-conventions.md +125 -12
  24. package/adapters/claude/skills/forge-1-prd/references/stage-exit-protocol.md +358 -144
  25. package/adapters/claude/skills/forge-2-tech/SKILL.md +38 -10
  26. package/adapters/claude/skills/forge-2-tech/references/shared-conventions.md +125 -12
  27. package/adapters/claude/skills/forge-2-tech/references/stage-exit-protocol.md +358 -144
  28. package/adapters/claude/skills/forge-3-specs/SKILL.md +28 -9
  29. package/adapters/claude/skills/forge-3-specs/references/shared-conventions.md +125 -12
  30. package/adapters/claude/skills/forge-3-specs/references/spec-archetypes.md +9 -0
  31. package/adapters/claude/skills/forge-3-specs/references/stage-exit-protocol.md +358 -144
  32. package/adapters/claude/skills/forge-4-backlog/SKILL.md +39 -10
  33. package/adapters/claude/skills/forge-4-backlog/references/shared-conventions.md +125 -12
  34. package/adapters/claude/skills/forge-4-backlog/references/stage-exit-protocol.md +358 -144
  35. package/adapters/claude/skills/forge-5-loop/SKILL.md +65 -67
  36. package/adapters/claude/skills/forge-5-loop/references/agent-selection.md +116 -0
  37. package/adapters/claude/skills/forge-5-loop/references/result-reporting.md +70 -31
  38. package/adapters/claude/skills/forge-5-loop/references/runner-contract.md +18 -109
  39. package/adapters/claude/skills/forge-5-loop/references/shared-conventions.md +125 -12
  40. package/adapters/claude/skills/forge-5-loop/references/stage-exit-protocol.md +358 -144
  41. package/adapters/claude/skills/forge-6-docs/SKILL.md +59 -9
  42. package/adapters/claude/skills/forge-6-docs/references/shared-conventions.md +125 -12
  43. package/adapters/claude/skills/forge-6-docs/references/stage-exit-protocol.md +472 -0
  44. package/adapters/claude/skills/forge-fix/SKILL.md +85 -33
  45. package/adapters/claude/skills/forge-fix/references/shared-conventions.md +125 -12
  46. package/adapters/claude/skills/forge-fix/references/stage-exit-protocol.md +358 -144
  47. package/adapters/claude/skills/forge-guide/references/forge-config-schema.json +1 -1
  48. package/adapters/claude/skills/forge-guide/references/shared-conventions.md +125 -12
  49. package/adapters/claude/skills/forge-verify/SKILL.md +86 -44
  50. package/adapters/claude/skills/forge-verify/references/findings-template.md +147 -0
  51. package/adapters/claude/skills/forge-verify/references/shared-conventions.md +125 -12
  52. package/adapters/claude/skills/forge-verify/references/stage-exit-protocol.md +472 -0
  53. package/adapters/claude/skills/forge-verify/references/verification-checklists/backlog.md +97 -0
  54. package/adapters/claude/skills/forge-verify/references/verification-checklists/epic.md +80 -0
  55. package/adapters/claude/skills/forge-verify/references/verification-checklists/impl.md +48 -0
  56. package/adapters/claude/skills/forge-verify/references/verification-checklists/prd.md +31 -0
  57. package/adapters/claude/skills/forge-verify/references/verification-checklists/specs.md +66 -0
  58. package/adapters/claude/skills/forge-verify/references/verification-checklists/tech.md +35 -0
  59. package/adapters/codex/.feature-forge-bundle.json +1 -1
  60. package/adapters/codex/agents/forge-verifier.toml +1 -1
  61. package/adapters/codex/references/epic-manifest-schema.json +6 -1
  62. package/adapters/codex/references/forge-config-schema.json +1 -1
  63. package/adapters/codex/references/pipeline-state-schema.json +5 -3
  64. package/adapters/codex/references/shared-conventions.md +125 -12
  65. package/adapters/codex/references/stage-exit-protocol.md +358 -144
  66. package/adapters/codex/references/vendor-construct-inventory.md +1 -1
  67. package/adapters/codex/scripts/epic-manifest.py +459 -90
  68. package/adapters/codex/scripts/forge-bootstrap.py +57 -5
  69. package/adapters/codex/scripts/forge-session.py +4346 -201
  70. package/adapters/codex/scripts/validate-traceability.py +92 -6
  71. package/adapters/codex/skills/forge/SKILL.md +18 -11
  72. package/adapters/codex/skills/forge/references/pipeline-state-schema.json +5 -3
  73. package/adapters/codex/skills/forge/references/shared-conventions.md +125 -12
  74. package/adapters/codex/skills/forge/references/stage-exit-protocol.md +358 -144
  75. package/adapters/codex/skills/forge-0-epic/SKILL.md +8 -3
  76. package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +24 -24
  77. package/adapters/codex/skills/forge-0-epic/references/pipeline-state-schema.json +5 -3
  78. package/adapters/codex/skills/forge-0-epic/references/shared-conventions.md +125 -12
  79. package/adapters/codex/skills/forge-0-epic/references/stage-exit-protocol.md +358 -144
  80. package/adapters/codex/skills/forge-1-prd/SKILL.md +39 -11
  81. package/adapters/codex/skills/forge-1-prd/references/shared-conventions.md +125 -12
  82. package/adapters/codex/skills/forge-1-prd/references/stage-exit-protocol.md +358 -144
  83. package/adapters/codex/skills/forge-2-tech/SKILL.md +38 -10
  84. package/adapters/codex/skills/forge-2-tech/references/shared-conventions.md +125 -12
  85. package/adapters/codex/skills/forge-2-tech/references/stage-exit-protocol.md +358 -144
  86. package/adapters/codex/skills/forge-3-specs/SKILL.md +28 -9
  87. package/adapters/codex/skills/forge-3-specs/references/shared-conventions.md +125 -12
  88. package/adapters/codex/skills/forge-3-specs/references/spec-archetypes.md +9 -0
  89. package/adapters/codex/skills/forge-3-specs/references/stage-exit-protocol.md +358 -144
  90. package/adapters/codex/skills/forge-4-backlog/SKILL.md +39 -10
  91. package/adapters/codex/skills/forge-4-backlog/references/shared-conventions.md +125 -12
  92. package/adapters/codex/skills/forge-4-backlog/references/stage-exit-protocol.md +358 -144
  93. package/adapters/codex/skills/forge-5-loop/SKILL.md +65 -67
  94. package/adapters/codex/skills/forge-5-loop/references/agent-selection.md +116 -0
  95. package/adapters/codex/skills/forge-5-loop/references/result-reporting.md +70 -31
  96. package/adapters/codex/skills/forge-5-loop/references/runner-contract.md +18 -109
  97. package/adapters/codex/skills/forge-5-loop/references/shared-conventions.md +125 -12
  98. package/adapters/codex/skills/forge-5-loop/references/stage-exit-protocol.md +358 -144
  99. package/adapters/codex/skills/forge-6-docs/SKILL.md +59 -9
  100. package/adapters/codex/skills/forge-6-docs/references/shared-conventions.md +125 -12
  101. package/adapters/codex/skills/forge-6-docs/references/stage-exit-protocol.md +472 -0
  102. package/adapters/codex/skills/forge-fix/SKILL.md +84 -32
  103. package/adapters/codex/skills/forge-fix/references/shared-conventions.md +125 -12
  104. package/adapters/codex/skills/forge-fix/references/stage-exit-protocol.md +358 -144
  105. package/adapters/codex/skills/forge-guide/references/forge-config-schema.json +1 -1
  106. package/adapters/codex/skills/forge-guide/references/shared-conventions.md +125 -12
  107. package/adapters/codex/skills/forge-verify/SKILL.md +85 -43
  108. package/adapters/codex/skills/forge-verify/references/findings-template.md +147 -0
  109. package/adapters/codex/skills/forge-verify/references/shared-conventions.md +125 -12
  110. package/adapters/codex/skills/forge-verify/references/stage-exit-protocol.md +472 -0
  111. package/adapters/codex/skills/forge-verify/references/verification-checklists/backlog.md +97 -0
  112. package/adapters/codex/skills/forge-verify/references/verification-checklists/epic.md +80 -0
  113. package/adapters/codex/skills/forge-verify/references/verification-checklists/impl.md +48 -0
  114. package/adapters/codex/skills/forge-verify/references/verification-checklists/prd.md +31 -0
  115. package/adapters/codex/skills/forge-verify/references/verification-checklists/specs.md +66 -0
  116. package/adapters/codex/skills/forge-verify/references/verification-checklists/tech.md +35 -0
  117. package/adapters/copilot/.feature-forge-bundle.json +1 -1
  118. package/adapters/copilot/agents/forge-verifier.md +1 -1
  119. package/adapters/copilot/references/epic-manifest-schema.json +6 -1
  120. package/adapters/copilot/references/forge-config-schema.json +1 -1
  121. package/adapters/copilot/references/pipeline-state-schema.json +5 -3
  122. package/adapters/copilot/references/shared-conventions.md +125 -12
  123. package/adapters/copilot/references/stage-exit-protocol.md +358 -144
  124. package/adapters/copilot/references/vendor-construct-inventory.md +1 -1
  125. package/adapters/copilot/scripts/epic-manifest.py +459 -90
  126. package/adapters/copilot/scripts/forge-bootstrap.py +57 -5
  127. package/adapters/copilot/scripts/forge-session.py +4346 -201
  128. package/adapters/copilot/scripts/validate-traceability.py +92 -6
  129. package/adapters/copilot/skills/forge/forge.md +18 -11
  130. package/adapters/copilot/skills/forge/references/pipeline-state-schema.json +5 -3
  131. package/adapters/copilot/skills/forge/references/shared-conventions.md +125 -12
  132. package/adapters/copilot/skills/forge/references/stage-exit-protocol.md +358 -144
  133. package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +8 -3
  134. package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +24 -24
  135. package/adapters/copilot/skills/forge-0-epic/references/pipeline-state-schema.json +5 -3
  136. package/adapters/copilot/skills/forge-0-epic/references/shared-conventions.md +125 -12
  137. package/adapters/copilot/skills/forge-0-epic/references/stage-exit-protocol.md +358 -144
  138. package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +39 -11
  139. package/adapters/copilot/skills/forge-1-prd/references/shared-conventions.md +125 -12
  140. package/adapters/copilot/skills/forge-1-prd/references/stage-exit-protocol.md +358 -144
  141. package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +38 -10
  142. package/adapters/copilot/skills/forge-2-tech/references/shared-conventions.md +125 -12
  143. package/adapters/copilot/skills/forge-2-tech/references/stage-exit-protocol.md +358 -144
  144. package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +28 -9
  145. package/adapters/copilot/skills/forge-3-specs/references/shared-conventions.md +125 -12
  146. package/adapters/copilot/skills/forge-3-specs/references/spec-archetypes.md +9 -0
  147. package/adapters/copilot/skills/forge-3-specs/references/stage-exit-protocol.md +358 -144
  148. package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +39 -10
  149. package/adapters/copilot/skills/forge-4-backlog/references/shared-conventions.md +125 -12
  150. package/adapters/copilot/skills/forge-4-backlog/references/stage-exit-protocol.md +358 -144
  151. package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +65 -67
  152. package/adapters/copilot/skills/forge-5-loop/references/agent-selection.md +116 -0
  153. package/adapters/copilot/skills/forge-5-loop/references/result-reporting.md +70 -31
  154. package/adapters/copilot/skills/forge-5-loop/references/runner-contract.md +18 -109
  155. package/adapters/copilot/skills/forge-5-loop/references/shared-conventions.md +125 -12
  156. package/adapters/copilot/skills/forge-5-loop/references/stage-exit-protocol.md +358 -144
  157. package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +59 -9
  158. package/adapters/copilot/skills/forge-6-docs/references/shared-conventions.md +125 -12
  159. package/adapters/copilot/skills/forge-6-docs/references/stage-exit-protocol.md +472 -0
  160. package/adapters/copilot/skills/forge-fix/forge-fix.md +84 -32
  161. package/adapters/copilot/skills/forge-fix/references/shared-conventions.md +125 -12
  162. package/adapters/copilot/skills/forge-fix/references/stage-exit-protocol.md +358 -144
  163. package/adapters/copilot/skills/forge-guide/references/forge-config-schema.json +1 -1
  164. package/adapters/copilot/skills/forge-guide/references/shared-conventions.md +125 -12
  165. package/adapters/copilot/skills/forge-verify/forge-verify.md +85 -43
  166. package/adapters/copilot/skills/forge-verify/references/findings-template.md +147 -0
  167. package/adapters/copilot/skills/forge-verify/references/shared-conventions.md +125 -12
  168. package/adapters/copilot/skills/forge-verify/references/stage-exit-protocol.md +472 -0
  169. package/adapters/copilot/skills/forge-verify/references/verification-checklists/backlog.md +97 -0
  170. package/adapters/copilot/skills/forge-verify/references/verification-checklists/epic.md +80 -0
  171. package/adapters/copilot/skills/forge-verify/references/verification-checklists/impl.md +48 -0
  172. package/adapters/copilot/skills/forge-verify/references/verification-checklists/prd.md +31 -0
  173. package/adapters/copilot/skills/forge-verify/references/verification-checklists/specs.md +66 -0
  174. package/adapters/copilot/skills/forge-verify/references/verification-checklists/tech.md +35 -0
  175. package/adapters/cursor/.feature-forge-bundle.json +1 -1
  176. package/adapters/cursor/agents/forge-verifier.mdc +1 -1
  177. package/adapters/cursor/references/epic-manifest-schema.json +6 -1
  178. package/adapters/cursor/references/forge-config-schema.json +1 -1
  179. package/adapters/cursor/references/pipeline-state-schema.json +5 -3
  180. package/adapters/cursor/references/shared-conventions.md +125 -12
  181. package/adapters/cursor/references/stage-exit-protocol.md +358 -144
  182. package/adapters/cursor/references/vendor-construct-inventory.md +1 -1
  183. package/adapters/cursor/scripts/epic-manifest.py +459 -90
  184. package/adapters/cursor/scripts/forge-bootstrap.py +57 -5
  185. package/adapters/cursor/scripts/forge-session.py +4346 -201
  186. package/adapters/cursor/scripts/validate-traceability.py +92 -6
  187. package/adapters/cursor/skills/forge/forge.mdc +18 -11
  188. package/adapters/cursor/skills/forge/references/pipeline-state-schema.json +5 -3
  189. package/adapters/cursor/skills/forge/references/shared-conventions.md +125 -12
  190. package/adapters/cursor/skills/forge/references/stage-exit-protocol.md +358 -144
  191. package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +8 -3
  192. package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +24 -24
  193. package/adapters/cursor/skills/forge-0-epic/references/pipeline-state-schema.json +5 -3
  194. package/adapters/cursor/skills/forge-0-epic/references/shared-conventions.md +125 -12
  195. package/adapters/cursor/skills/forge-0-epic/references/stage-exit-protocol.md +358 -144
  196. package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +39 -11
  197. package/adapters/cursor/skills/forge-1-prd/references/shared-conventions.md +125 -12
  198. package/adapters/cursor/skills/forge-1-prd/references/stage-exit-protocol.md +358 -144
  199. package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +38 -10
  200. package/adapters/cursor/skills/forge-2-tech/references/shared-conventions.md +125 -12
  201. package/adapters/cursor/skills/forge-2-tech/references/stage-exit-protocol.md +358 -144
  202. package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +28 -9
  203. package/adapters/cursor/skills/forge-3-specs/references/shared-conventions.md +125 -12
  204. package/adapters/cursor/skills/forge-3-specs/references/spec-archetypes.md +9 -0
  205. package/adapters/cursor/skills/forge-3-specs/references/stage-exit-protocol.md +358 -144
  206. package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +39 -10
  207. package/adapters/cursor/skills/forge-4-backlog/references/shared-conventions.md +125 -12
  208. package/adapters/cursor/skills/forge-4-backlog/references/stage-exit-protocol.md +358 -144
  209. package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +65 -67
  210. package/adapters/cursor/skills/forge-5-loop/references/agent-selection.md +116 -0
  211. package/adapters/cursor/skills/forge-5-loop/references/result-reporting.md +70 -31
  212. package/adapters/cursor/skills/forge-5-loop/references/runner-contract.md +18 -109
  213. package/adapters/cursor/skills/forge-5-loop/references/shared-conventions.md +125 -12
  214. package/adapters/cursor/skills/forge-5-loop/references/stage-exit-protocol.md +358 -144
  215. package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +59 -9
  216. package/adapters/cursor/skills/forge-6-docs/references/shared-conventions.md +125 -12
  217. package/adapters/cursor/skills/forge-6-docs/references/stage-exit-protocol.md +472 -0
  218. package/adapters/cursor/skills/forge-fix/forge-fix.mdc +84 -32
  219. package/adapters/cursor/skills/forge-fix/references/shared-conventions.md +125 -12
  220. package/adapters/cursor/skills/forge-fix/references/stage-exit-protocol.md +358 -144
  221. package/adapters/cursor/skills/forge-guide/references/forge-config-schema.json +1 -1
  222. package/adapters/cursor/skills/forge-guide/references/shared-conventions.md +125 -12
  223. package/adapters/cursor/skills/forge-verify/forge-verify.mdc +85 -43
  224. package/adapters/cursor/skills/forge-verify/references/findings-template.md +147 -0
  225. package/adapters/cursor/skills/forge-verify/references/shared-conventions.md +125 -12
  226. package/adapters/cursor/skills/forge-verify/references/stage-exit-protocol.md +472 -0
  227. package/adapters/cursor/skills/forge-verify/references/verification-checklists/backlog.md +97 -0
  228. package/adapters/cursor/skills/forge-verify/references/verification-checklists/epic.md +80 -0
  229. package/adapters/cursor/skills/forge-verify/references/verification-checklists/impl.md +48 -0
  230. package/adapters/cursor/skills/forge-verify/references/verification-checklists/prd.md +31 -0
  231. package/adapters/cursor/skills/forge-verify/references/verification-checklists/specs.md +66 -0
  232. package/adapters/cursor/skills/forge-verify/references/verification-checklists/tech.md +35 -0
  233. package/adapters/gemini/.feature-forge-bundle.json +1 -1
  234. package/adapters/gemini/agents/forge-verifier.md +1 -1
  235. package/adapters/gemini/gemini-extension.json +1 -1
  236. package/adapters/gemini/references/epic-manifest-schema.json +6 -1
  237. package/adapters/gemini/references/forge-config-schema.json +1 -1
  238. package/adapters/gemini/references/pipeline-state-schema.json +5 -3
  239. package/adapters/gemini/references/shared-conventions.md +125 -12
  240. package/adapters/gemini/references/stage-exit-protocol.md +358 -144
  241. package/adapters/gemini/references/vendor-construct-inventory.md +1 -1
  242. package/adapters/gemini/scripts/epic-manifest.py +459 -90
  243. package/adapters/gemini/scripts/forge-bootstrap.py +57 -5
  244. package/adapters/gemini/scripts/forge-session.py +4346 -201
  245. package/adapters/gemini/scripts/validate-traceability.py +92 -6
  246. package/adapters/gemini/skills/forge/forge.md +18 -11
  247. package/adapters/gemini/skills/forge/references/pipeline-state-schema.json +5 -3
  248. package/adapters/gemini/skills/forge/references/shared-conventions.md +125 -12
  249. package/adapters/gemini/skills/forge/references/stage-exit-protocol.md +358 -144
  250. package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +8 -3
  251. package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +24 -24
  252. package/adapters/gemini/skills/forge-0-epic/references/pipeline-state-schema.json +5 -3
  253. package/adapters/gemini/skills/forge-0-epic/references/shared-conventions.md +125 -12
  254. package/adapters/gemini/skills/forge-0-epic/references/stage-exit-protocol.md +358 -144
  255. package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +39 -11
  256. package/adapters/gemini/skills/forge-1-prd/references/shared-conventions.md +125 -12
  257. package/adapters/gemini/skills/forge-1-prd/references/stage-exit-protocol.md +358 -144
  258. package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +38 -10
  259. package/adapters/gemini/skills/forge-2-tech/references/shared-conventions.md +125 -12
  260. package/adapters/gemini/skills/forge-2-tech/references/stage-exit-protocol.md +358 -144
  261. package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +28 -9
  262. package/adapters/gemini/skills/forge-3-specs/references/shared-conventions.md +125 -12
  263. package/adapters/gemini/skills/forge-3-specs/references/spec-archetypes.md +9 -0
  264. package/adapters/gemini/skills/forge-3-specs/references/stage-exit-protocol.md +358 -144
  265. package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +39 -10
  266. package/adapters/gemini/skills/forge-4-backlog/references/shared-conventions.md +125 -12
  267. package/adapters/gemini/skills/forge-4-backlog/references/stage-exit-protocol.md +358 -144
  268. package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +65 -67
  269. package/adapters/gemini/skills/forge-5-loop/references/agent-selection.md +116 -0
  270. package/adapters/gemini/skills/forge-5-loop/references/result-reporting.md +70 -31
  271. package/adapters/gemini/skills/forge-5-loop/references/runner-contract.md +18 -109
  272. package/adapters/gemini/skills/forge-5-loop/references/shared-conventions.md +125 -12
  273. package/adapters/gemini/skills/forge-5-loop/references/stage-exit-protocol.md +358 -144
  274. package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +59 -9
  275. package/adapters/gemini/skills/forge-6-docs/references/shared-conventions.md +125 -12
  276. package/adapters/gemini/skills/forge-6-docs/references/stage-exit-protocol.md +472 -0
  277. package/adapters/gemini/skills/forge-fix/forge-fix.md +84 -32
  278. package/adapters/gemini/skills/forge-fix/references/shared-conventions.md +125 -12
  279. package/adapters/gemini/skills/forge-fix/references/stage-exit-protocol.md +358 -144
  280. package/adapters/gemini/skills/forge-guide/references/forge-config-schema.json +1 -1
  281. package/adapters/gemini/skills/forge-guide/references/shared-conventions.md +125 -12
  282. package/adapters/gemini/skills/forge-verify/forge-verify.md +85 -43
  283. package/adapters/gemini/skills/forge-verify/references/findings-template.md +147 -0
  284. package/adapters/gemini/skills/forge-verify/references/shared-conventions.md +125 -12
  285. package/adapters/gemini/skills/forge-verify/references/stage-exit-protocol.md +472 -0
  286. package/adapters/gemini/skills/forge-verify/references/verification-checklists/backlog.md +97 -0
  287. package/adapters/gemini/skills/forge-verify/references/verification-checklists/epic.md +80 -0
  288. package/adapters/gemini/skills/forge-verify/references/verification-checklists/impl.md +48 -0
  289. package/adapters/gemini/skills/forge-verify/references/verification-checklists/prd.md +31 -0
  290. package/adapters/gemini/skills/forge-verify/references/verification-checklists/specs.md +66 -0
  291. package/adapters/gemini/skills/forge-verify/references/verification-checklists/tech.md +35 -0
  292. package/adapters/pi/.feature-forge-bundle.json +1 -1
  293. package/adapters/pi/agents/forge-verifier.md +1 -1
  294. package/adapters/pi/references/epic-manifest-schema.json +6 -1
  295. package/adapters/pi/references/forge-config-schema.json +1 -1
  296. package/adapters/pi/references/pipeline-state-schema.json +5 -3
  297. package/adapters/pi/references/shared-conventions.md +125 -12
  298. package/adapters/pi/references/stage-exit-protocol.md +358 -144
  299. package/adapters/pi/references/vendor-construct-inventory.md +1 -1
  300. package/adapters/pi/scripts/epic-manifest.py +459 -90
  301. package/adapters/pi/scripts/forge-bootstrap.py +57 -5
  302. package/adapters/pi/scripts/forge-session.py +4346 -201
  303. package/adapters/pi/scripts/validate-traceability.py +92 -6
  304. package/adapters/pi/skills/forge/SKILL.md +18 -11
  305. package/adapters/pi/skills/forge/references/pipeline-state-schema.json +5 -3
  306. package/adapters/pi/skills/forge/references/shared-conventions.md +125 -12
  307. package/adapters/pi/skills/forge/references/stage-exit-protocol.md +358 -144
  308. package/adapters/pi/skills/forge-0-epic/SKILL.md +8 -3
  309. package/adapters/pi/skills/forge-0-epic/references/edit-mode.md +24 -24
  310. package/adapters/pi/skills/forge-0-epic/references/pipeline-state-schema.json +5 -3
  311. package/adapters/pi/skills/forge-0-epic/references/shared-conventions.md +125 -12
  312. package/adapters/pi/skills/forge-0-epic/references/stage-exit-protocol.md +358 -144
  313. package/adapters/pi/skills/forge-1-prd/SKILL.md +39 -11
  314. package/adapters/pi/skills/forge-1-prd/references/shared-conventions.md +125 -12
  315. package/adapters/pi/skills/forge-1-prd/references/stage-exit-protocol.md +358 -144
  316. package/adapters/pi/skills/forge-2-tech/SKILL.md +38 -10
  317. package/adapters/pi/skills/forge-2-tech/references/shared-conventions.md +125 -12
  318. package/adapters/pi/skills/forge-2-tech/references/stage-exit-protocol.md +358 -144
  319. package/adapters/pi/skills/forge-3-specs/SKILL.md +28 -9
  320. package/adapters/pi/skills/forge-3-specs/references/shared-conventions.md +125 -12
  321. package/adapters/pi/skills/forge-3-specs/references/spec-archetypes.md +9 -0
  322. package/adapters/pi/skills/forge-3-specs/references/stage-exit-protocol.md +358 -144
  323. package/adapters/pi/skills/forge-4-backlog/SKILL.md +39 -10
  324. package/adapters/pi/skills/forge-4-backlog/references/shared-conventions.md +125 -12
  325. package/adapters/pi/skills/forge-4-backlog/references/stage-exit-protocol.md +358 -144
  326. package/adapters/pi/skills/forge-5-loop/SKILL.md +65 -67
  327. package/adapters/pi/skills/forge-5-loop/references/agent-selection.md +116 -0
  328. package/adapters/pi/skills/forge-5-loop/references/result-reporting.md +70 -31
  329. package/adapters/pi/skills/forge-5-loop/references/runner-contract.md +18 -109
  330. package/adapters/pi/skills/forge-5-loop/references/shared-conventions.md +125 -12
  331. package/adapters/pi/skills/forge-5-loop/references/stage-exit-protocol.md +358 -144
  332. package/adapters/pi/skills/forge-6-docs/SKILL.md +59 -9
  333. package/adapters/pi/skills/forge-6-docs/references/shared-conventions.md +125 -12
  334. package/adapters/pi/skills/forge-6-docs/references/stage-exit-protocol.md +472 -0
  335. package/adapters/pi/skills/forge-fix/SKILL.md +84 -32
  336. package/adapters/pi/skills/forge-fix/references/shared-conventions.md +125 -12
  337. package/adapters/pi/skills/forge-fix/references/stage-exit-protocol.md +358 -144
  338. package/adapters/pi/skills/forge-guide/references/forge-config-schema.json +1 -1
  339. package/adapters/pi/skills/forge-guide/references/shared-conventions.md +125 -12
  340. package/adapters/pi/skills/forge-verify/SKILL.md +85 -43
  341. package/adapters/pi/skills/forge-verify/references/findings-template.md +147 -0
  342. package/adapters/pi/skills/forge-verify/references/shared-conventions.md +125 -12
  343. package/adapters/pi/skills/forge-verify/references/stage-exit-protocol.md +472 -0
  344. package/adapters/pi/skills/forge-verify/references/verification-checklists/backlog.md +97 -0
  345. package/adapters/pi/skills/forge-verify/references/verification-checklists/epic.md +80 -0
  346. package/adapters/pi/skills/forge-verify/references/verification-checklists/impl.md +48 -0
  347. package/adapters/pi/skills/forge-verify/references/verification-checklists/prd.md +31 -0
  348. package/adapters/pi/skills/forge-verify/references/verification-checklists/specs.md +66 -0
  349. package/adapters/pi/skills/forge-verify/references/verification-checklists/tech.md +35 -0
  350. package/package.json +1 -1
  351. package/adapters/claude/skills/forge-1-prd/references/pipeline-state-schema.json +0 -191
  352. package/adapters/claude/skills/forge-2-tech/references/pipeline-state-schema.json +0 -191
  353. package/adapters/claude/skills/forge-3-specs/references/pipeline-state-schema.json +0 -191
  354. package/adapters/claude/skills/forge-4-backlog/references/forge-config-schema.json +0 -236
  355. package/adapters/claude/skills/forge-4-backlog/references/pipeline-state-schema.json +0 -191
  356. package/adapters/claude/skills/forge-5-loop/references/forge-config-schema.json +0 -236
  357. package/adapters/claude/skills/forge-6-docs/references/pipeline-state-schema.json +0 -191
  358. package/adapters/claude/skills/forge-verify/references/pipeline-state-schema.json +0 -191
  359. package/adapters/claude/skills/forge-verify/references/verification-checklists.md +0 -477
  360. package/adapters/codex/skills/forge-1-prd/references/pipeline-state-schema.json +0 -191
  361. package/adapters/codex/skills/forge-2-tech/references/pipeline-state-schema.json +0 -191
  362. package/adapters/codex/skills/forge-3-specs/references/pipeline-state-schema.json +0 -191
  363. package/adapters/codex/skills/forge-4-backlog/references/forge-config-schema.json +0 -236
  364. package/adapters/codex/skills/forge-4-backlog/references/pipeline-state-schema.json +0 -191
  365. package/adapters/codex/skills/forge-5-loop/references/forge-config-schema.json +0 -236
  366. package/adapters/codex/skills/forge-6-docs/references/pipeline-state-schema.json +0 -191
  367. package/adapters/codex/skills/forge-verify/references/pipeline-state-schema.json +0 -191
  368. package/adapters/codex/skills/forge-verify/references/verification-checklists.md +0 -477
  369. package/adapters/copilot/skills/forge-1-prd/references/pipeline-state-schema.json +0 -191
  370. package/adapters/copilot/skills/forge-2-tech/references/pipeline-state-schema.json +0 -191
  371. package/adapters/copilot/skills/forge-3-specs/references/pipeline-state-schema.json +0 -191
  372. package/adapters/copilot/skills/forge-4-backlog/references/forge-config-schema.json +0 -236
  373. package/adapters/copilot/skills/forge-4-backlog/references/pipeline-state-schema.json +0 -191
  374. package/adapters/copilot/skills/forge-5-loop/references/forge-config-schema.json +0 -236
  375. package/adapters/copilot/skills/forge-6-docs/references/pipeline-state-schema.json +0 -191
  376. package/adapters/copilot/skills/forge-verify/references/pipeline-state-schema.json +0 -191
  377. package/adapters/copilot/skills/forge-verify/references/verification-checklists.md +0 -477
  378. package/adapters/cursor/skills/forge-1-prd/references/pipeline-state-schema.json +0 -191
  379. package/adapters/cursor/skills/forge-2-tech/references/pipeline-state-schema.json +0 -191
  380. package/adapters/cursor/skills/forge-3-specs/references/pipeline-state-schema.json +0 -191
  381. package/adapters/cursor/skills/forge-4-backlog/references/forge-config-schema.json +0 -236
  382. package/adapters/cursor/skills/forge-4-backlog/references/pipeline-state-schema.json +0 -191
  383. package/adapters/cursor/skills/forge-5-loop/references/forge-config-schema.json +0 -236
  384. package/adapters/cursor/skills/forge-6-docs/references/pipeline-state-schema.json +0 -191
  385. package/adapters/cursor/skills/forge-verify/references/pipeline-state-schema.json +0 -191
  386. package/adapters/cursor/skills/forge-verify/references/verification-checklists.md +0 -477
  387. package/adapters/gemini/skills/forge-1-prd/references/pipeline-state-schema.json +0 -191
  388. package/adapters/gemini/skills/forge-2-tech/references/pipeline-state-schema.json +0 -191
  389. package/adapters/gemini/skills/forge-3-specs/references/pipeline-state-schema.json +0 -191
  390. package/adapters/gemini/skills/forge-4-backlog/references/forge-config-schema.json +0 -236
  391. package/adapters/gemini/skills/forge-4-backlog/references/pipeline-state-schema.json +0 -191
  392. package/adapters/gemini/skills/forge-5-loop/references/forge-config-schema.json +0 -236
  393. package/adapters/gemini/skills/forge-6-docs/references/pipeline-state-schema.json +0 -191
  394. package/adapters/gemini/skills/forge-verify/references/pipeline-state-schema.json +0 -191
  395. package/adapters/gemini/skills/forge-verify/references/verification-checklists.md +0 -477
  396. package/adapters/pi/skills/forge-1-prd/references/pipeline-state-schema.json +0 -191
  397. package/adapters/pi/skills/forge-2-tech/references/pipeline-state-schema.json +0 -191
  398. package/adapters/pi/skills/forge-3-specs/references/pipeline-state-schema.json +0 -191
  399. package/adapters/pi/skills/forge-4-backlog/references/forge-config-schema.json +0 -236
  400. package/adapters/pi/skills/forge-4-backlog/references/pipeline-state-schema.json +0 -191
  401. package/adapters/pi/skills/forge-5-loop/references/forge-config-schema.json +0 -236
  402. package/adapters/pi/skills/forge-6-docs/references/pipeline-state-schema.json +0 -191
  403. package/adapters/pi/skills/forge-verify/references/pipeline-state-schema.json +0 -191
  404. package/adapters/pi/skills/forge-verify/references/verification-checklists.md +0 -477
@@ -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,16 +55,73 @@ 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
 
72
+ #: The six production stages in pipeline order — mirrors ``PRODUCTION_STAGES`` in
73
+ #: forge-session.py. Used ONLY to DERIVE "what runs next" from ``stages[].status``
74
+ #: (see ``_next_production_stage``), never to read it out of ``currentStage``:
75
+ #: the schema defines ``currentStage`` as "where the pipeline IS" (the most
76
+ #: recently *started* stage), which is one stage behind the next un-run stage for
77
+ #: the whole window between a stage completing and its successor being entered.
78
+ _PRODUCTION_STAGES: Final = (
79
+ "forge-1-prd",
80
+ "forge-2-tech",
81
+ "forge-3-specs",
82
+ "forge-4-backlog",
83
+ "forge-5-loop",
84
+ "forge-6-docs",
85
+ )
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
+
65
122
 
66
123
  # --------------------------------------------------------------------------- #
67
- # Type Definitions (00-core-definitions.md §4, §5; 02 §8.4)
124
+ # Type Definitions
68
125
  # --------------------------------------------------------------------------- #
69
126
 
70
127
  FindingCode = Literal[
@@ -99,12 +156,12 @@ class Finding(TypedDict):
99
156
  DerivedStatus = Literal[
100
157
  "not-started", # no .pipeline-state.json, or all stages pending
101
158
  "in-progress", # at least one stage started, loop not complete-for-orchestration
102
- "complete", # complete-for-orchestration per 00 §7
159
+ "complete", # complete-for-orchestration
103
160
  ]
104
161
 
105
162
 
106
163
  class FeatureStatus(TypedDict):
107
- """Live per-feature status derived from its own pipeline state (00 §5).
164
+ """Live per-feature status derived from its own pipeline state.
108
165
 
109
166
  Attributes:
110
167
  name: Feature name.
@@ -114,7 +171,7 @@ class FeatureStatus(TypedDict):
114
171
  navigator status semantics for display.
115
172
  blocked: True if any entry in unmetDeps is non-empty.
116
173
  unmetDeps: Names of this feature's direct dependencies that are not yet
117
- complete-for-orchestration (00 §7). Empty when actionable or complete.
174
+ complete-for-orchestration. Empty when actionable or complete.
118
175
  openEpicChangeRequests: Count of this member's ``epicChangeRequests``
119
176
  entries with ``status == "open"`` — epic-level change requests raised
120
177
  by a member stage that forge-0-epic edit mode has not yet reconciled.
@@ -134,30 +191,35 @@ class FeatureStatus(TypedDict):
134
191
 
135
192
 
136
193
  class Rollup(TypedDict):
137
- """Aggregate completion counts for the epic dashboard (00 §8)."""
194
+ """Aggregate completion counts for the epic dashboard."""
138
195
 
139
- complete: int #: Number of member features complete-for-orchestration (00 §7).
196
+ complete: int #: Number of member features complete-for-orchestration.
140
197
  total: int #: Total member features in the manifest (0 for an empty epic).
141
198
 
142
199
 
143
200
  class RenderStatus(TypedDict):
144
- """The full live dashboard payload returned by render_status (00 §5, §8).
201
+ """The full live dashboard payload returned by render_status.
145
202
 
146
203
  Attributes:
147
204
  epic: The epic name (manifest `epic`).
148
- status: The epic lifecycle status (00 §2.1).
205
+ status: The epic lifecycle status.
149
206
  features: Per-member status rows, one per manifest feature (may be empty).
150
207
  actionable: Names of features whose dependsOn are all complete and that
151
- are not themselves complete (00 §8).
208
+ are not themselves complete.
152
209
  parallelEligible: Subset of `actionable` with no mutual (transitive)
153
- dependency — surfaced for future parallel execution (00 §8).
210
+ dependency — surfaced for future parallel execution.
154
211
  rollup: Aggregate {complete, total} counts.
155
212
  nextCommand: Recommended next command for the first actionable feature, or
156
213
  None when nothing is actionable (all complete, empty epic, or paused).
157
214
  warnings: Human-readable diagnostics that do NOT invalidate the graph but
158
- need surfacing — currently, members carrying a ``forge-verify-*.status``
159
- outside ``KNOWN_VERIFY_STATUSES`` (treated as incomplete, but silently so
160
- 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.
161
223
  """
162
224
 
163
225
  epic: str
@@ -171,7 +233,7 @@ class RenderStatus(TypedDict):
171
233
 
172
234
 
173
235
  # --------------------------------------------------------------------------- #
174
- # Internal Exceptions (02 §2)
236
+ # Internal Exceptions
175
237
  # --------------------------------------------------------------------------- #
176
238
 
177
239
 
@@ -194,7 +256,7 @@ class UsageError(Exception):
194
256
  class FindingsError(Exception):
195
257
  """A non-fatal validation outcome that must exit 1.
196
258
 
197
- 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
198
260
  gating operation: a cycle, a dangling ref, an ambiguous/not-found name, etc.
199
261
  Maps to exit code 1. Carries the structured findings so the dispatch layer
200
262
  can emit them as JSON or human lines.
@@ -209,7 +271,7 @@ class FindingsError(Exception):
209
271
 
210
272
 
211
273
  # --------------------------------------------------------------------------- #
212
- # Safety & I/O Layer (02 §3)
274
+ # Safety & I/O Layer
213
275
  # --------------------------------------------------------------------------- #
214
276
 
215
277
 
@@ -228,7 +290,7 @@ def assert_safe_name(name: str) -> None:
228
290
  equals '..', or fails SAFE_NAME_RE. The message embeds the
229
291
  offending name (e.g. ``unsafe name '../escape'``) so the caller can
230
292
  surface it verbatim. Corresponds to the 'unsafe-name' Finding code
231
- (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
232
294
  any manifest is read.
233
295
  """
234
296
  if (
@@ -263,7 +325,7 @@ def contained_path(base: Path, *parts: str) -> Path:
263
325
  UsageError: If the resolved path escapes ``base`` (message:
264
326
  ``resolved path escapes specs dir: …``). Containment violations
265
327
  surface only as exit-2 usage errors per the error model in
266
- tech-spec §6 (there is no dedicated Finding code for them).
328
+ the manifest contract (there is no dedicated Finding code for them).
267
329
  """
268
330
  base_real = base.resolve()
269
331
  target = (base_real / Path(*parts)).resolve()
@@ -281,6 +343,12 @@ def load_manifest(epic_dir: Path) -> dict:
281
343
  epic_dir: The epic subtree directory (must already be contained within
282
344
  {specsDir} via contained_path).
283
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
+
284
352
  Returns:
285
353
  The parsed manifest as a plain dict. Structural validation (schema,
286
354
  cycles, dangling refs) is performed separately by ``validate`` — this
@@ -289,7 +357,7 @@ def load_manifest(epic_dir: Path) -> dict:
289
357
  Raises:
290
358
  UsageError: If the manifest file is missing or unreadable (exit 2).
291
359
  FindingsError: If the file exists but is not parseable JSON — emits a
292
- single 'corrupt-json' Finding (00 §4) with the JSON error position,
360
+ single 'corrupt-json' Finding with the JSON error position,
293
361
  so a hand-corrupted manifest fails with an actionable message rather
294
362
  than a traceback (REQ-ROBUST-02). Exit 1.
295
363
  """
@@ -301,7 +369,7 @@ def load_manifest(epic_dir: Path) -> dict:
301
369
  except OSError as exc:
302
370
  raise UsageError(f"cannot read manifest {path}: {exc}")
303
371
  try:
304
- return json.loads(text)
372
+ parsed = json.loads(text)
305
373
  except json.JSONDecodeError as exc:
306
374
  raise FindingsError([
307
375
  {
@@ -310,6 +378,9 @@ def load_manifest(epic_dir: Path) -> dict:
310
378
  "feature": None,
311
379
  }
312
380
  ])
381
+ if isinstance(parsed, dict) and "revision" not in parsed:
382
+ parsed["revision"] = 1 # synthesized only — never written back here.
383
+ return parsed
313
384
 
314
385
 
315
386
  def atomic_write(path: Path, data: dict) -> None:
@@ -358,12 +429,12 @@ def atomic_write(path: Path, data: dict) -> None:
358
429
 
359
430
 
360
431
  # --------------------------------------------------------------------------- #
361
- # Graph Algorithms (02 §4) — implemented in item 004
432
+ # Graph Algorithms — implemented in item 004
362
433
  # --------------------------------------------------------------------------- #
363
434
 
364
435
 
365
436
  def find_cycle(features: list[dict]) -> list[str] | None:
366
- """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.
367
438
 
368
439
  Iterative DFS over the directed graph whose edges are ``feature -> dep``.
369
440
  On the first back-edge into a GRAY node, reconstructs and returns the cycle
@@ -414,7 +485,7 @@ def find_cycle(features: list[dict]) -> list[str] | None:
414
485
  def unmet_deps(
415
486
  name: str, features: list[dict], complete: dict[str, bool]
416
487
  ) -> list[str]:
417
- """Return a feature's direct dependencies that are not complete (02 §4).
488
+ """Return a feature's direct dependencies that are not complete.
418
489
 
419
490
  Names of this feature's direct ``dependsOn`` entries whose value in
420
491
  ``complete`` is False, preserving manifest order. Empty when the feature is
@@ -426,15 +497,15 @@ def unmet_deps(
426
497
 
427
498
 
428
499
  # --------------------------------------------------------------------------- #
429
- # Resolution & Uniqueness (02 §5) — implemented in item 005
500
+ # Resolution & Uniqueness — implemented in item 005
430
501
  # --------------------------------------------------------------------------- #
431
502
 
432
503
 
433
504
  def feature_dirs(specs_dir: Path) -> dict[str, list[Path]]:
434
- """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.
435
506
 
436
507
  Scans both layouts to a fixed depth, treating a directory as a feature iff
437
- it directly contains a ``.pipeline-state.json`` (00 §6, REQ-DIR-03):
508
+ it directly contains a ``.pipeline-state.json`` (REQ-DIR-03):
438
509
  * flat: {specsDir}/{name}/.pipeline-state.json
439
510
  * nested: {specsDir}/{epic}/{name}/.pipeline-state.json
440
511
 
@@ -442,7 +513,7 @@ def feature_dirs(specs_dir: Path) -> dict[str, list[Path]]:
442
513
  entry is a uniqueness violation (REQ-DIR-04) surfaced as 'ambiguous' or
443
514
  'duplicate-name' by the caller. Epic directories themselves (which hold
444
515
  ``epic-manifest.json`` but no ``.pipeline-state.json``) are skipped, so an
445
- epic name never collides with a feature name (01 §4.3).
516
+ epic name never collides with a feature name.
446
517
 
447
518
  Args:
448
519
  specs_dir: The configured specs directory (already verified to exist).
@@ -465,9 +536,9 @@ def feature_dirs(specs_dir: Path) -> dict[str, list[Path]]:
465
536
 
466
537
 
467
538
  def resolve(name: str, specs_dir: Path) -> Path:
468
- """Resolve a bare feature/epic name to its absolute directory (02 §5).
539
+ """Resolve a bare feature/epic name to its absolute directory.
469
540
 
470
- Implements the 5-step algorithm (tech-spec §3.4):
541
+ Implements the 5-step algorithm:
471
542
  1. reject unsafe names (assert_safe_name) — exit 2 before any FS access;
472
543
  2. flat match: {specsDir}/{name}/.pipeline-state.json wins outright;
473
544
  3. exactly one nested match resolves cleanly;
@@ -487,7 +558,7 @@ def resolve(name: str, specs_dir: Path) -> Path:
487
558
  Raises:
488
559
  UsageError: Unsafe name or missing specs dir (exit 2).
489
560
  FindingsError: 'ambiguous' (lists every matching path) or 'not-found'
490
- (exit 1). 00 §4.2 gives the canonical message shapes.
561
+ (exit 1).
491
562
  """
492
563
  assert_safe_name(name)
493
564
  if not specs_dir.is_dir():
@@ -515,10 +586,10 @@ def resolve(name: str, specs_dir: Path) -> Path:
515
586
 
516
587
 
517
588
  def check_name(name: str, specs_dir: Path) -> list[Finding]:
518
- """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.
519
590
 
520
591
  Used by forge-0-epic before creating a new member feature so no NEW global
521
- 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
522
593
  existing occurrence is enough to reject — unlike ``resolve``, which tolerates
523
594
  a uniquely-matching name and only errors on genuine multi-match.
524
595
 
@@ -551,43 +622,44 @@ def check_name(name: str, specs_dir: Path) -> list[Finding]:
551
622
 
552
623
 
553
624
  # --------------------------------------------------------------------------- #
554
- # Validation (02 §6.2, §10) — implemented in item 006
625
+ # Validation — implemented in item 006
555
626
  # --------------------------------------------------------------------------- #
556
627
 
557
628
 
558
- #: 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.
559
631
  _TOP_REQUIRED: Final = (
560
- "schemaVersion", "epic", "description", "status",
632
+ "schemaVersion", "revision", "epic", "description", "status",
561
633
  "narrativeDoc", "createdAt", "updatedAt", "features",
562
634
  )
563
- #: Required keys on each Feature object (00 §2.2).
635
+ #: Required keys on each Feature object.
564
636
  _FEATURE_REQUIRED: Final = ("name", "charter", "dependsOn", "exposes", "consumes")
565
637
  #: Optional keys on each Feature object. `mutatesShared` is the #144 precision
566
638
  #: hint for cross-member coupling (array of project-root-relative path strings);
567
639
  #: schema-legal when present, ignored when absent (mirrors
568
640
  #: epic-manifest-schema.json definitions.feature.properties.mutatesShared).
569
641
  _FEATURE_OPTIONAL: Final = ("mutatesShared",)
570
- #: Required keys on each Contract (exposes[]) object (00 §2.3).
642
+ #: Required keys on each Contract (exposes[]) object.
571
643
  _CONTRACT_REQUIRED: Final = ("name", "kind", "summary")
572
- #: Required keys on each ConsumedContract (consumes[]) object (00 §2.4).
644
+ #: Required keys on each ConsumedContract (consumes[]) object.
573
645
  _CONSUMED_REQUIRED: Final = ("from", "name", "summary")
574
- #: Allowed epic lifecycle states (00 §2.1).
646
+ #: Allowed epic lifecycle states.
575
647
  _EPIC_STATUSES: Final = ("active", "paused", "abandoned", "complete")
576
- #: Allowed Contract kinds (00 §2.3).
648
+ #: Allowed Contract kinds.
577
649
  _CONTRACT_KINDS: Final = ("function", "type", "endpoint", "module", "event")
578
650
 
579
651
 
580
652
  def _schema(message: str, feature: str | None = None) -> Finding:
581
- """Construct a 'schema' Finding (00 §4)."""
653
+ """Construct a 'schema' Finding."""
582
654
  return {"code": "schema", "message": message, "feature": feature}
583
655
 
584
656
 
585
657
  def _schema_findings(manifest: dict) -> list[Finding]:
586
- """Hand-rolled stdlib schema checker over the manifest (02 §6.2, 00 §2.6).
658
+ """Hand-rolled stdlib schema checker over the manifest.
587
659
 
588
- Asserts required keys/types/enums/consts from 00 §2 and explicitly rejects
660
+ Asserts the required keys/types/enums/consts and explicitly rejects
589
661
  any ``features[].status`` key (REQ-STATE-02 -> 'cached-status'). No
590
- third-party ``jsonschema`` (01 §2.1). Returns 'schema' findings plus, for a
662
+ third-party ``jsonschema``. Returns 'schema' findings plus, for a
591
663
  per-feature status key, a 'cached-status' finding.
592
664
  """
593
665
  findings: list[Finding] = []
@@ -600,6 +672,12 @@ def _schema_findings(manifest: dict) -> list[Finding]:
600
672
 
601
673
  if "schemaVersion" in manifest and manifest["schemaVersion"] != 1:
602
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}"))
603
681
  if "narrativeDoc" in manifest and manifest["narrativeDoc"] != NARRATIVE_FILENAME:
604
682
  findings.append(_schema(f"narrativeDoc must be {NARRATIVE_FILENAME!r}, got {manifest['narrativeDoc']!r}")) # noqa: E501
605
683
  for key in ("epic", "description", "createdAt", "updatedAt"):
@@ -683,9 +761,9 @@ def _schema_findings(manifest: dict) -> list[Finding]:
683
761
  def _validate_dict(
684
762
  manifest: dict, epic_dir: Path, specs_dir: Path
685
763
  ) -> list[Finding]:
686
- """Validate an already-parsed manifest dict, returning findings (02 §6.2).
764
+ """Validate an already-parsed manifest dict, returning findings.
687
765
 
688
- 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
689
767
  later check cannot run. Reused by the item-008 mutators on the EDITED dict
690
768
  before writing. Does not parse JSON (that is ``validate``'s job) — operates
691
769
  purely in memory.
@@ -773,7 +851,7 @@ def _validate_dict(
773
851
 
774
852
 
775
853
  def validate(epic_dir: Path, specs_dir: Path) -> list[Finding]:
776
- """Validate a single epic manifest, returning all findings (02 §6.2).
854
+ """Validate a single epic manifest, returning all findings.
777
855
 
778
856
  Parses the manifest (folding any corrupt-json finding from load_manifest
779
857
  into the returned list) then delegates to ``_validate_dict``. Raises
@@ -787,12 +865,12 @@ def validate(epic_dir: Path, specs_dir: Path) -> list[Finding]:
787
865
 
788
866
 
789
867
  # --------------------------------------------------------------------------- #
790
- # Live Status Derivation (02 §8) — implemented in item 007
868
+ # Live Status Derivation — implemented in item 007
791
869
  # --------------------------------------------------------------------------- #
792
870
 
793
871
 
794
872
  def is_complete_for_orchestration(state: dict) -> bool:
795
- """Apply the completion-for-orchestration predicate (00 §7, 02 §8.1).
873
+ """Apply the completion-for-orchestration predicate.
796
874
 
797
875
  A feature is complete-for-orchestration iff::
798
876
 
@@ -802,8 +880,7 @@ def is_complete_for_orchestration(state: dict) -> bool:
802
880
 
803
881
  A feature whose forge-verify-impl is 'findings-reported' (unfixed) is NOT
804
882
  complete and does NOT unblock dependents (REQ-ORCH-01). This is the single
805
- implementation of the predicate, reused by the dependency gate and handoff
806
- (04-pipeline-integration.md).
883
+ implementation of the predicate, reused by the dependency gate and handoff.
807
884
 
808
885
  Args:
809
886
  state: A parsed .pipeline-state.json dict (or {} if the member has none).
@@ -836,6 +913,11 @@ def _verify_status_warnings(name: str, state: dict) -> list[str]:
836
913
 
837
914
  Non-string / malformed status values (a list, an int) are also flagged, and the
838
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.
839
921
  """
840
922
  stages = state.get("stages", {})
841
923
  if not isinstance(stages, dict):
@@ -859,11 +941,209 @@ def _verify_status_warnings(name: str, state: dict) -> list[str]:
859
941
  return warnings
860
942
 
861
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"/skill: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"/skill:forge-verify {epic}",
1137
+ scheduled,
1138
+ revision,
1139
+ )]
1140
+
1141
+
862
1142
  def _read_state_safely(state_path: Path) -> dict:
863
1143
  """Read and parse a member's .pipeline-state.json, tolerating corruption.
864
1144
 
865
1145
  A missing, unreadable, unparseable, or torn (partially-written) member state
866
- downgrades to ``{}`` rather than crashing the dashboard (02 §8.2). Member
1146
+ downgrades to ``{}`` rather than crashing the dashboard. Member
867
1147
  state writes are made by forge-1..5 skills outside the helper's atomicity
868
1148
  scope, so a torn read is expected and simply renders that one feature as
869
1149
  ``not-started``.
@@ -878,7 +1158,7 @@ def _read_state_safely(state_path: Path) -> dict:
878
1158
 
879
1159
 
880
1160
  def derive_status(feature_dir: Path) -> FeatureStatus:
881
- """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.
882
1162
 
883
1163
  Reads ``{feature_dir}/.pipeline-state.json`` and maps it to a FeatureStatus:
884
1164
  missing/unparseable/all-pending -> ``not-started``; complete-for-
@@ -891,7 +1171,7 @@ def derive_status(feature_dir: Path) -> FeatureStatus:
891
1171
  feature_dir: The member feature's directory.
892
1172
 
893
1173
  Returns:
894
- A FeatureStatus (00 §5) with name, stage, coarse status, and placeholder
1174
+ A FeatureStatus with name, stage, coarse status, and placeholder
895
1175
  blocked/unmetDeps.
896
1176
  """
897
1177
  name = feature_dir.name
@@ -934,7 +1214,7 @@ def derive_status(feature_dir: Path) -> FeatureStatus:
934
1214
 
935
1215
 
936
1216
  def _transitive_deps(name: str, adjacency: dict[str, list[str]]) -> set[str]:
937
- """Return all features reachable from ``name`` via dependsOn edges (00 §8)."""
1217
+ """Return all features reachable from ``name`` via dependsOn edges."""
938
1218
  seen: set[str] = set()
939
1219
  stack = list(adjacency.get(name, []))
940
1220
  while stack:
@@ -946,23 +1226,61 @@ def _transitive_deps(name: str, adjacency: dict[str, list[str]]) -> set[str]:
946
1226
  return seen
947
1227
 
948
1228
 
949
- def _next_command(feature_dir: Path, status_row: FeatureStatus) -> str:
950
- """Recommend the next forge command for an actionable feature (02 §8.3).
1229
+ def _next_production_stage(state: dict) -> str | None:
1230
+ """Return the first production stage that is not yet complete, else None.
951
1231
 
952
- ``/skill:forge-1-prd <name>`` when the feature's PRD is absent (or it
953
- has not progressed past epic creation), else the command for its next un-run
954
- stage (its ``currentStage``).
1232
+ The epic-side mirror of ``next_stage()`` in forge-session.py, and for the same
1233
+ reason its docstring gives: "what runs next" is DERIVED from ``stages[].status``,
1234
+ never read from the stored ``currentStage``. A missing, pending, in-progress or
1235
+ stale stage all count as "not done".
1236
+ """
1237
+ stages = state.get("stages")
1238
+ if not isinstance(stages, dict):
1239
+ return _PRODUCTION_STAGES[0]
1240
+ for stage in _PRODUCTION_STAGES:
1241
+ entry = stages.get(stage)
1242
+ if not isinstance(entry, dict) or entry.get("status") != "complete":
1243
+ return stage
1244
+ return None
1245
+
1246
+
1247
+ def _next_command(feature_dir: Path, status_row: FeatureStatus) -> str:
1248
+ """Recommend the next forge command for an actionable feature.
1249
+
1250
+ ``/skill:forge-1-prd <name>`` when the feature's PRD is absent (or the
1251
+ member has not progressed past epic creation), else the command for its next
1252
+ un-run production stage — DERIVED from ``stages[].status``, not read from
1253
+ ``currentStage`` (item 020: ``currentStage`` is "where the pipeline IS", so
1254
+ reading it here recommended re-running the stage the member had just finished
1255
+ for the whole window before the next stage was entered).
1256
+
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).
955
1265
  """
956
1266
  name = status_row["name"]
957
- stage = status_row["stage"]
1267
+ state = _read_state_safely(feature_dir / PIPELINE_STATE_FILENAME)
1268
+ nxt = _next_production_stage(state)
958
1269
  prd_present = (feature_dir / "PRD.md").is_file()
959
- if not prd_present or stage in ("forge-0-epic", "forge-1-prd"):
1270
+ if not prd_present or nxt == "forge-1-prd":
960
1271
  return f"/skill:forge-1-prd {name}"
961
- return f"/skill:{stage} {name}"
1272
+ if nxt is None:
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"/skill:forge-fix {name}"
1278
+ return f"/skill:forge-verify {name}"
1279
+ return f"/skill:{nxt} {name}"
962
1280
 
963
1281
 
964
1282
  def render_status(epic_dir: Path, specs_dir: Path) -> RenderStatus:
965
- """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.
966
1284
 
967
1285
  Validates first (refusing to render over an invalid graph), then derives each
968
1286
  member's live status from its own state file, computes blocked/unmetDeps,
@@ -973,7 +1291,7 @@ def render_status(epic_dir: Path, specs_dir: Path) -> RenderStatus:
973
1291
  specs_dir: The configured specs directory.
974
1292
 
975
1293
  Returns:
976
- The RenderStatus dict (02 §8.4).
1294
+ The RenderStatus dict.
977
1295
 
978
1296
  Raises:
979
1297
  UsageError: Missing/unreadable manifest (exit 2).
@@ -1000,6 +1318,15 @@ def render_status(epic_dir: Path, specs_dir: Path) -> RenderStatus:
1000
1318
  member_state = _read_state_safely(member_dir / PIPELINE_STATE_FILENAME)
1001
1319
  complete[name] = is_complete_for_orchestration(member_state)
1002
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
+ ))
1003
1330
 
1004
1331
  # (4) per-feature unmetDeps + blocked. A feature that is itself complete is
1005
1332
  # never "blocked" — unmet deps only matter for work not yet finished.
@@ -1059,21 +1386,47 @@ def render_status(epic_dir: Path, specs_dir: Path) -> RenderStatus:
1059
1386
 
1060
1387
 
1061
1388
  # --------------------------------------------------------------------------- #
1062
- # Mutators (02 §7) — implemented in item 008
1389
+ # Mutators — implemented in item 008
1063
1390
  # --------------------------------------------------------------------------- #
1064
1391
 
1065
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
+
1066
1404
  def _bump_and_write(
1067
1405
  epic_dir: Path, specs_dir: Path, manifest: dict
1068
1406
  ) -> list[Finding]:
1069
- """Re-validate, bump updatedAt, and atomically persist a manifest (02 §7).
1070
-
1071
- The shared tail of every mutator (REQ-ROBUST-03, REQ-OBS-01, REQ-EPIC-05).
1072
- Re-runs ``_validate_dict`` on the EDITED manifest; if any blocking finding is
1073
- present (cycle, dangling-ref, duplicate-name, schema, ...), the on-disk file
1074
- is left byte-identical and the findings are returned so the caller exits 1.
1075
- Otherwise ``updatedAt`` is set to now (UTC, ISO-8601) and the manifest is
1076
- 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.
1077
1430
 
1078
1431
  Args:
1079
1432
  epic_dir: The epic subtree directory.
@@ -1081,17 +1434,33 @@ def _bump_and_write(
1081
1434
  manifest: The already-edited in-memory manifest dict.
1082
1435
 
1083
1436
  Returns:
1084
- An empty list on success (write performed); the blocking findings on
1085
- 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).
1086
1439
 
1087
1440
  Raises:
1088
1441
  UsageError: If the atomic write itself fails (exit 2).
1089
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
+
1090
1451
  findings = _validate_dict(manifest, epic_dir, specs_dir)
1091
1452
  if findings:
1092
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
1093
1462
  manifest["updatedAt"] = datetime.now(timezone.utc).isoformat()
1094
- atomic_write(epic_dir / MANIFEST_FILENAME, manifest)
1463
+ atomic_write(path, manifest)
1095
1464
  return []
1096
1465
 
1097
1466
 
@@ -1102,7 +1471,7 @@ def add_feature(
1102
1471
  charter: str,
1103
1472
  deps: list[str],
1104
1473
  ) -> list[Finding]:
1105
- """Append a new member feature to the manifest (02 §7.1).
1474
+ """Append a new member feature to the manifest.
1106
1475
 
1107
1476
  Appends a ``Feature`` with the given name/charter/dependsOn and EMPTY
1108
1477
  exposes/consumes. Re-validation surfaces a duplicate name (within the
@@ -1138,7 +1507,7 @@ def add_feature(
1138
1507
 
1139
1508
 
1140
1509
  def remove_feature(epic_dir: Path, specs_dir: Path, name: str) -> list[Finding]:
1141
- """Remove a member feature from the manifest (02 §7.2).
1510
+ """Remove a member feature from the manifest.
1142
1511
 
1143
1512
  Drops the named feature from ``features[]``. After removal, re-validation
1144
1513
  surfaces any now-dangling ``dependsOn`` / ``consumes.from`` that pointed at
@@ -1170,10 +1539,10 @@ def remove_feature(epic_dir: Path, specs_dir: Path, name: str) -> list[Finding]:
1170
1539
 
1171
1540
 
1172
1541
  def reorder(epic_dir: Path, specs_dir: Path, order: list[str]) -> list[Finding]:
1173
- """Reorder the manifest features[] to a given permutation (02 §7.3).
1542
+ """Reorder the manifest features[] to a given permutation.
1174
1543
 
1175
1544
  ``order`` must be an exact permutation of the current member names (purely a
1176
- 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
1177
1546
  ``schema`` finding is returned and the manifest is left unchanged.
1178
1547
 
1179
1548
  Args:
@@ -1203,7 +1572,7 @@ def reorder(epic_dir: Path, specs_dir: Path, order: list[str]) -> list[Finding]:
1203
1572
  def set_dep(
1204
1573
  epic_dir: Path, specs_dir: Path, name: str, deps: list[str]
1205
1574
  ) -> list[Finding]:
1206
- """Replace a member feature's dependsOn list (02 §7.4, §7.6).
1575
+ """Replace a member feature's dependsOn list.
1207
1576
 
1208
1577
  Re-validation enforces every new dependency exists (``dangling-ref``) and the
1209
1578
  resulting graph is acyclic (``cycle``). An empty ``deps`` clears the
@@ -1239,7 +1608,7 @@ def set_dep(
1239
1608
 
1240
1609
 
1241
1610
  def set_status(epic_dir: Path, specs_dir: Path, status: str) -> list[Finding]:
1242
- """Set the epic-level lifecycle status (02 §7.5).
1611
+ """Set the epic-level lifecycle status.
1243
1612
 
1244
1613
  Sets the epic-level ``status`` (the value is constrained to the allowed
1245
1614
  lifecycle states by ``argparse`` ``choices`` before reaching here). Never
@@ -1437,7 +1806,7 @@ def adopt_feature(
1437
1806
 
1438
1807
 
1439
1808
  # --------------------------------------------------------------------------- #
1440
- # CLI Dispatch (02 §9)
1809
+ # CLI Dispatch
1441
1810
  # --------------------------------------------------------------------------- #
1442
1811
 
1443
1812
 
@@ -1466,7 +1835,7 @@ def _emit_findings(findings: list[Finding], as_json: bool) -> None:
1466
1835
 
1467
1836
 
1468
1837
  def _print_status_table(status: RenderStatus) -> None:
1469
- """Print a readable epic dashboard plus the recommended next command (02 §8)."""
1838
+ """Print a readable epic dashboard plus the recommended next command."""
1470
1839
  rollup = status["rollup"]
1471
1840
  print(f"Epic: {status['epic']} [{status['status']}]")
1472
1841
  print(f"Progress: {rollup['complete']}/{rollup['total']} complete")
@@ -1575,7 +1944,7 @@ def _dispatch(args: argparse.Namespace, specs_dir: Path) -> int:
1575
1944
 
1576
1945
 
1577
1946
  def _build_parser() -> argparse.ArgumentParser:
1578
- """Build the argparse parser with one subparser per subcommand (02 §9)."""
1947
+ """Build the argparse parser with one subparser per subcommand."""
1579
1948
  parser = argparse.ArgumentParser(prog="epic-manifest.py", description=__doc__)
1580
1949
  sub = parser.add_subparsers(dest="cmd", required=True)
1581
1950