@garygentry/feature-forge 0.3.1 → 0.3.4

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 (421) hide show
  1. package/adapters/claude/.feature-forge-bundle.json +1 -1
  2. package/adapters/claude/agents/forge-verifier.md +3 -1
  3. package/adapters/claude/references/decisions/single-writer-threat-model.md +53 -0
  4. package/adapters/claude/references/epic-manifest-schema.json +6 -1
  5. package/adapters/claude/references/epic-state-schema.json +50 -0
  6. package/adapters/claude/references/forge-config-schema.json +19 -1
  7. package/adapters/claude/references/forge-decisions-schema.json +33 -0
  8. package/adapters/claude/references/pipeline-state-schema.json +38 -4
  9. package/adapters/claude/references/ralph-loop-contract.md +6 -3
  10. package/adapters/claude/references/shared-conventions.md +77 -3
  11. package/adapters/claude/references/stage-exit-protocol.md +391 -143
  12. package/adapters/claude/scripts/epic-manifest.py +495 -91
  13. package/adapters/claude/scripts/fix-sweep.py +1180 -0
  14. package/adapters/claude/scripts/forge-bootstrap.py +57 -5
  15. package/adapters/claude/scripts/forge-session.py +4241 -146
  16. package/adapters/claude/scripts/validate-traceability.py +86 -5
  17. package/adapters/claude/skills/forge/SKILL.md +10 -9
  18. package/adapters/claude/skills/forge/references/pipeline-state-schema.json +38 -4
  19. package/adapters/claude/skills/forge/references/shared-conventions.md +77 -3
  20. package/adapters/claude/skills/forge/references/stage-exit-protocol.md +391 -143
  21. package/adapters/claude/skills/forge-0-epic/SKILL.md +7 -2
  22. package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +29 -25
  23. package/adapters/claude/skills/forge-0-epic/references/epic-manifest-subcommands.md +5 -0
  24. package/adapters/claude/skills/forge-0-epic/references/pipeline-state-schema.json +38 -4
  25. package/adapters/claude/skills/forge-0-epic/references/shared-conventions.md +77 -3
  26. package/adapters/claude/skills/forge-0-epic/references/stage-exit-protocol.md +391 -143
  27. package/adapters/claude/skills/forge-1-prd/SKILL.md +17 -4
  28. package/adapters/claude/skills/forge-1-prd/references/shared-conventions.md +77 -3
  29. package/adapters/claude/skills/forge-1-prd/references/stage-exit-protocol.md +391 -143
  30. package/adapters/claude/skills/forge-2-tech/SKILL.md +18 -4
  31. package/adapters/claude/skills/forge-2-tech/references/shared-conventions.md +77 -3
  32. package/adapters/claude/skills/forge-2-tech/references/stage-exit-protocol.md +391 -143
  33. package/adapters/claude/skills/forge-3-specs/SKILL.md +9 -3
  34. package/adapters/claude/skills/forge-3-specs/references/shared-conventions.md +77 -3
  35. package/adapters/claude/skills/forge-3-specs/references/spec-archetypes.md +9 -0
  36. package/adapters/claude/skills/forge-3-specs/references/stage-exit-protocol.md +391 -143
  37. package/adapters/claude/skills/forge-4-backlog/SKILL.md +56 -5
  38. package/adapters/claude/skills/forge-4-backlog/references/shared-conventions.md +77 -3
  39. package/adapters/claude/skills/forge-4-backlog/references/stage-exit-protocol.md +391 -143
  40. package/adapters/claude/skills/forge-5-loop/SKILL.md +67 -69
  41. package/adapters/claude/skills/forge-5-loop/references/agent-selection.md +16 -0
  42. package/adapters/claude/skills/forge-5-loop/references/ralph-loop-contract.md +6 -3
  43. package/adapters/claude/skills/forge-5-loop/references/recovery-procedure.md +349 -0
  44. package/adapters/claude/skills/forge-5-loop/references/result-reporting.md +101 -33
  45. package/adapters/claude/skills/forge-5-loop/references/runner-contract.md +26 -6
  46. package/adapters/claude/skills/forge-5-loop/references/shared-conventions.md +77 -3
  47. package/adapters/claude/skills/forge-5-loop/references/stage-exit-protocol.md +391 -143
  48. package/adapters/claude/skills/forge-6-docs/SKILL.md +75 -8
  49. package/adapters/claude/skills/forge-6-docs/references/shared-conventions.md +77 -3
  50. package/adapters/claude/skills/forge-6-docs/references/stage-exit-protocol.md +516 -0
  51. package/adapters/claude/skills/forge-fix/SKILL.md +119 -33
  52. package/adapters/claude/skills/forge-fix/references/shared-conventions.md +77 -3
  53. package/adapters/claude/skills/forge-fix/references/stage-exit-protocol.md +391 -143
  54. package/adapters/claude/skills/forge-guide/references/forge-config-schema.json +19 -1
  55. package/adapters/claude/skills/forge-guide/references/ralph-loop-contract.md +6 -3
  56. package/adapters/claude/skills/forge-guide/references/shared-conventions.md +77 -3
  57. package/adapters/claude/skills/forge-verify/SKILL.md +82 -51
  58. package/adapters/claude/skills/forge-verify/references/decisions/single-writer-threat-model.md +53 -0
  59. package/adapters/claude/skills/forge-verify/references/findings-template.md +69 -49
  60. package/adapters/claude/skills/forge-verify/references/shared-conventions.md +77 -3
  61. package/adapters/claude/skills/forge-verify/references/stage-exit-protocol.md +516 -0
  62. package/adapters/claude/skills/forge-verify/references/verification-checklists/backlog.md +73 -0
  63. package/adapters/claude/skills/forge-verify/references/verification-checklists/epic.md +4 -3
  64. package/adapters/claude/skills/forge-verify/references/verification-checklists/impl.md +85 -0
  65. package/adapters/claude/skills/forge-verify/references/verification-checklists/specs.md +45 -1
  66. package/adapters/codex/.feature-forge-bundle.json +1 -1
  67. package/adapters/codex/agents/forge-verifier.toml +3 -1
  68. package/adapters/codex/references/decisions/single-writer-threat-model.md +53 -0
  69. package/adapters/codex/references/epic-manifest-schema.json +6 -1
  70. package/adapters/codex/references/epic-state-schema.json +50 -0
  71. package/adapters/codex/references/forge-config-schema.json +19 -1
  72. package/adapters/codex/references/forge-decisions-schema.json +33 -0
  73. package/adapters/codex/references/pipeline-state-schema.json +38 -4
  74. package/adapters/codex/references/process-overview.md +2 -2
  75. package/adapters/codex/references/ralph-loop-contract.md +6 -3
  76. package/adapters/codex/references/shared-conventions.md +102 -28
  77. package/adapters/codex/references/stage-exit-protocol.md +395 -147
  78. package/adapters/codex/scripts/epic-manifest.py +495 -91
  79. package/adapters/codex/scripts/fix-sweep.py +1180 -0
  80. package/adapters/codex/scripts/forge-bootstrap.py +57 -5
  81. package/adapters/codex/scripts/forge-session.py +4241 -146
  82. package/adapters/codex/scripts/validate-traceability.py +86 -5
  83. package/adapters/codex/skills/forge/SKILL.md +13 -12
  84. package/adapters/codex/skills/forge/references/pipeline-state-schema.json +38 -4
  85. package/adapters/codex/skills/forge/references/process-overview.md +2 -2
  86. package/adapters/codex/skills/forge/references/shared-conventions.md +102 -28
  87. package/adapters/codex/skills/forge/references/stage-exit-protocol.md +395 -147
  88. package/adapters/codex/skills/forge-0-epic/SKILL.md +7 -2
  89. package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +37 -33
  90. package/adapters/codex/skills/forge-0-epic/references/epic-manifest-subcommands.md +8 -3
  91. package/adapters/codex/skills/forge-0-epic/references/pipeline-state-schema.json +38 -4
  92. package/adapters/codex/skills/forge-0-epic/references/shared-conventions.md +102 -28
  93. package/adapters/codex/skills/forge-0-epic/references/stage-exit-protocol.md +395 -147
  94. package/adapters/codex/skills/forge-1-prd/SKILL.md +17 -4
  95. package/adapters/codex/skills/forge-1-prd/references/shared-conventions.md +102 -28
  96. package/adapters/codex/skills/forge-1-prd/references/stage-exit-protocol.md +395 -147
  97. package/adapters/codex/skills/forge-2-tech/SKILL.md +18 -4
  98. package/adapters/codex/skills/forge-2-tech/references/shared-conventions.md +102 -28
  99. package/adapters/codex/skills/forge-2-tech/references/stage-exit-protocol.md +395 -147
  100. package/adapters/codex/skills/forge-3-specs/SKILL.md +9 -3
  101. package/adapters/codex/skills/forge-3-specs/references/shared-conventions.md +102 -28
  102. package/adapters/codex/skills/forge-3-specs/references/spec-archetypes.md +9 -0
  103. package/adapters/codex/skills/forge-3-specs/references/stage-exit-protocol.md +395 -147
  104. package/adapters/codex/skills/forge-4-backlog/SKILL.md +56 -5
  105. package/adapters/codex/skills/forge-4-backlog/references/shared-conventions.md +102 -28
  106. package/adapters/codex/skills/forge-4-backlog/references/stage-exit-protocol.md +395 -147
  107. package/adapters/codex/skills/forge-5-loop/SKILL.md +67 -69
  108. package/adapters/codex/skills/forge-5-loop/references/agent-selection.md +17 -1
  109. package/adapters/codex/skills/forge-5-loop/references/ralph-loop-contract.md +6 -3
  110. package/adapters/codex/skills/forge-5-loop/references/recovery-procedure.md +349 -0
  111. package/adapters/codex/skills/forge-5-loop/references/result-reporting.md +101 -33
  112. package/adapters/codex/skills/forge-5-loop/references/runner-contract.md +30 -10
  113. package/adapters/codex/skills/forge-5-loop/references/shared-conventions.md +102 -28
  114. package/adapters/codex/skills/forge-5-loop/references/stage-exit-protocol.md +395 -147
  115. package/adapters/codex/skills/forge-6-docs/SKILL.md +75 -8
  116. package/adapters/codex/skills/forge-6-docs/references/shared-conventions.md +102 -28
  117. package/adapters/codex/skills/forge-6-docs/references/stage-exit-protocol.md +516 -0
  118. package/adapters/codex/skills/forge-fix/SKILL.md +118 -32
  119. package/adapters/codex/skills/forge-fix/references/shared-conventions.md +102 -28
  120. package/adapters/codex/skills/forge-fix/references/stage-exit-protocol.md +395 -147
  121. package/adapters/codex/skills/forge-guide/SKILL.md +1 -1
  122. package/adapters/codex/skills/forge-guide/references/forge-config-schema.json +19 -1
  123. package/adapters/codex/skills/forge-guide/references/process-overview.md +2 -2
  124. package/adapters/codex/skills/forge-guide/references/ralph-loop-contract.md +6 -3
  125. package/adapters/codex/skills/forge-guide/references/shared-conventions.md +102 -28
  126. package/adapters/codex/skills/forge-init/SKILL.md +1 -1
  127. package/adapters/codex/skills/forge-verify/SKILL.md +82 -51
  128. package/adapters/codex/skills/forge-verify/references/decisions/single-writer-threat-model.md +53 -0
  129. package/adapters/codex/skills/forge-verify/references/findings-template.md +70 -50
  130. package/adapters/codex/skills/forge-verify/references/shared-conventions.md +102 -28
  131. package/adapters/codex/skills/forge-verify/references/stage-exit-protocol.md +516 -0
  132. package/adapters/codex/skills/forge-verify/references/verification-checklists/backlog.md +73 -0
  133. package/adapters/codex/skills/forge-verify/references/verification-checklists/epic.md +5 -4
  134. package/adapters/codex/skills/forge-verify/references/verification-checklists/impl.md +85 -0
  135. package/adapters/codex/skills/forge-verify/references/verification-checklists/specs.md +45 -1
  136. package/adapters/copilot/.feature-forge-bundle.json +1 -1
  137. package/adapters/copilot/agents/forge-verifier.md +3 -1
  138. package/adapters/copilot/references/decisions/single-writer-threat-model.md +53 -0
  139. package/adapters/copilot/references/epic-manifest-schema.json +6 -1
  140. package/adapters/copilot/references/epic-state-schema.json +50 -0
  141. package/adapters/copilot/references/forge-config-schema.json +19 -1
  142. package/adapters/copilot/references/forge-decisions-schema.json +33 -0
  143. package/adapters/copilot/references/pipeline-state-schema.json +38 -4
  144. package/adapters/copilot/references/process-overview.md +2 -2
  145. package/adapters/copilot/references/ralph-loop-contract.md +6 -3
  146. package/adapters/copilot/references/shared-conventions.md +102 -28
  147. package/adapters/copilot/references/stage-exit-protocol.md +395 -147
  148. package/adapters/copilot/scripts/epic-manifest.py +495 -91
  149. package/adapters/copilot/scripts/fix-sweep.py +1180 -0
  150. package/adapters/copilot/scripts/forge-bootstrap.py +57 -5
  151. package/adapters/copilot/scripts/forge-session.py +4241 -146
  152. package/adapters/copilot/scripts/validate-traceability.py +86 -5
  153. package/adapters/copilot/skills/forge/forge.md +13 -12
  154. package/adapters/copilot/skills/forge/references/pipeline-state-schema.json +38 -4
  155. package/adapters/copilot/skills/forge/references/process-overview.md +2 -2
  156. package/adapters/copilot/skills/forge/references/shared-conventions.md +102 -28
  157. package/adapters/copilot/skills/forge/references/stage-exit-protocol.md +395 -147
  158. package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +7 -2
  159. package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +37 -33
  160. package/adapters/copilot/skills/forge-0-epic/references/epic-manifest-subcommands.md +8 -3
  161. package/adapters/copilot/skills/forge-0-epic/references/pipeline-state-schema.json +38 -4
  162. package/adapters/copilot/skills/forge-0-epic/references/shared-conventions.md +102 -28
  163. package/adapters/copilot/skills/forge-0-epic/references/stage-exit-protocol.md +395 -147
  164. package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +17 -4
  165. package/adapters/copilot/skills/forge-1-prd/references/shared-conventions.md +102 -28
  166. package/adapters/copilot/skills/forge-1-prd/references/stage-exit-protocol.md +395 -147
  167. package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +18 -4
  168. package/adapters/copilot/skills/forge-2-tech/references/shared-conventions.md +102 -28
  169. package/adapters/copilot/skills/forge-2-tech/references/stage-exit-protocol.md +395 -147
  170. package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +9 -3
  171. package/adapters/copilot/skills/forge-3-specs/references/shared-conventions.md +102 -28
  172. package/adapters/copilot/skills/forge-3-specs/references/spec-archetypes.md +9 -0
  173. package/adapters/copilot/skills/forge-3-specs/references/stage-exit-protocol.md +395 -147
  174. package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +56 -5
  175. package/adapters/copilot/skills/forge-4-backlog/references/shared-conventions.md +102 -28
  176. package/adapters/copilot/skills/forge-4-backlog/references/stage-exit-protocol.md +395 -147
  177. package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +67 -69
  178. package/adapters/copilot/skills/forge-5-loop/references/agent-selection.md +17 -1
  179. package/adapters/copilot/skills/forge-5-loop/references/ralph-loop-contract.md +6 -3
  180. package/adapters/copilot/skills/forge-5-loop/references/recovery-procedure.md +349 -0
  181. package/adapters/copilot/skills/forge-5-loop/references/result-reporting.md +101 -33
  182. package/adapters/copilot/skills/forge-5-loop/references/runner-contract.md +30 -10
  183. package/adapters/copilot/skills/forge-5-loop/references/shared-conventions.md +102 -28
  184. package/adapters/copilot/skills/forge-5-loop/references/stage-exit-protocol.md +395 -147
  185. package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +75 -8
  186. package/adapters/copilot/skills/forge-6-docs/references/shared-conventions.md +102 -28
  187. package/adapters/copilot/skills/forge-6-docs/references/stage-exit-protocol.md +516 -0
  188. package/adapters/copilot/skills/forge-fix/forge-fix.md +118 -32
  189. package/adapters/copilot/skills/forge-fix/references/shared-conventions.md +102 -28
  190. package/adapters/copilot/skills/forge-fix/references/stage-exit-protocol.md +395 -147
  191. package/adapters/copilot/skills/forge-guide/forge-guide.md +1 -1
  192. package/adapters/copilot/skills/forge-guide/references/forge-config-schema.json +19 -1
  193. package/adapters/copilot/skills/forge-guide/references/process-overview.md +2 -2
  194. package/adapters/copilot/skills/forge-guide/references/ralph-loop-contract.md +6 -3
  195. package/adapters/copilot/skills/forge-guide/references/shared-conventions.md +102 -28
  196. package/adapters/copilot/skills/forge-init/forge-init.md +1 -1
  197. package/adapters/copilot/skills/forge-verify/forge-verify.md +82 -51
  198. package/adapters/copilot/skills/forge-verify/references/decisions/single-writer-threat-model.md +53 -0
  199. package/adapters/copilot/skills/forge-verify/references/findings-template.md +70 -50
  200. package/adapters/copilot/skills/forge-verify/references/shared-conventions.md +102 -28
  201. package/adapters/copilot/skills/forge-verify/references/stage-exit-protocol.md +516 -0
  202. package/adapters/copilot/skills/forge-verify/references/verification-checklists/backlog.md +73 -0
  203. package/adapters/copilot/skills/forge-verify/references/verification-checklists/epic.md +5 -4
  204. package/adapters/copilot/skills/forge-verify/references/verification-checklists/impl.md +85 -0
  205. package/adapters/copilot/skills/forge-verify/references/verification-checklists/specs.md +45 -1
  206. package/adapters/cursor/.feature-forge-bundle.json +1 -1
  207. package/adapters/cursor/agents/forge-verifier.mdc +3 -1
  208. package/adapters/cursor/references/decisions/single-writer-threat-model.md +53 -0
  209. package/adapters/cursor/references/epic-manifest-schema.json +6 -1
  210. package/adapters/cursor/references/epic-state-schema.json +50 -0
  211. package/adapters/cursor/references/forge-config-schema.json +19 -1
  212. package/adapters/cursor/references/forge-decisions-schema.json +33 -0
  213. package/adapters/cursor/references/pipeline-state-schema.json +38 -4
  214. package/adapters/cursor/references/process-overview.md +2 -2
  215. package/adapters/cursor/references/ralph-loop-contract.md +6 -3
  216. package/adapters/cursor/references/shared-conventions.md +102 -28
  217. package/adapters/cursor/references/stage-exit-protocol.md +395 -147
  218. package/adapters/cursor/scripts/epic-manifest.py +495 -91
  219. package/adapters/cursor/scripts/fix-sweep.py +1180 -0
  220. package/adapters/cursor/scripts/forge-bootstrap.py +57 -5
  221. package/adapters/cursor/scripts/forge-session.py +4241 -146
  222. package/adapters/cursor/scripts/validate-traceability.py +86 -5
  223. package/adapters/cursor/skills/forge/forge.mdc +13 -12
  224. package/adapters/cursor/skills/forge/references/pipeline-state-schema.json +38 -4
  225. package/adapters/cursor/skills/forge/references/process-overview.md +2 -2
  226. package/adapters/cursor/skills/forge/references/shared-conventions.md +102 -28
  227. package/adapters/cursor/skills/forge/references/stage-exit-protocol.md +395 -147
  228. package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +7 -2
  229. package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +37 -33
  230. package/adapters/cursor/skills/forge-0-epic/references/epic-manifest-subcommands.md +8 -3
  231. package/adapters/cursor/skills/forge-0-epic/references/pipeline-state-schema.json +38 -4
  232. package/adapters/cursor/skills/forge-0-epic/references/shared-conventions.md +102 -28
  233. package/adapters/cursor/skills/forge-0-epic/references/stage-exit-protocol.md +395 -147
  234. package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +17 -4
  235. package/adapters/cursor/skills/forge-1-prd/references/shared-conventions.md +102 -28
  236. package/adapters/cursor/skills/forge-1-prd/references/stage-exit-protocol.md +395 -147
  237. package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +18 -4
  238. package/adapters/cursor/skills/forge-2-tech/references/shared-conventions.md +102 -28
  239. package/adapters/cursor/skills/forge-2-tech/references/stage-exit-protocol.md +395 -147
  240. package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +9 -3
  241. package/adapters/cursor/skills/forge-3-specs/references/shared-conventions.md +102 -28
  242. package/adapters/cursor/skills/forge-3-specs/references/spec-archetypes.md +9 -0
  243. package/adapters/cursor/skills/forge-3-specs/references/stage-exit-protocol.md +395 -147
  244. package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +56 -5
  245. package/adapters/cursor/skills/forge-4-backlog/references/shared-conventions.md +102 -28
  246. package/adapters/cursor/skills/forge-4-backlog/references/stage-exit-protocol.md +395 -147
  247. package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +67 -69
  248. package/adapters/cursor/skills/forge-5-loop/references/agent-selection.md +17 -1
  249. package/adapters/cursor/skills/forge-5-loop/references/ralph-loop-contract.md +6 -3
  250. package/adapters/cursor/skills/forge-5-loop/references/recovery-procedure.md +349 -0
  251. package/adapters/cursor/skills/forge-5-loop/references/result-reporting.md +101 -33
  252. package/adapters/cursor/skills/forge-5-loop/references/runner-contract.md +30 -10
  253. package/adapters/cursor/skills/forge-5-loop/references/shared-conventions.md +102 -28
  254. package/adapters/cursor/skills/forge-5-loop/references/stage-exit-protocol.md +395 -147
  255. package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +75 -8
  256. package/adapters/cursor/skills/forge-6-docs/references/shared-conventions.md +102 -28
  257. package/adapters/cursor/skills/forge-6-docs/references/stage-exit-protocol.md +516 -0
  258. package/adapters/cursor/skills/forge-fix/forge-fix.mdc +118 -32
  259. package/adapters/cursor/skills/forge-fix/references/shared-conventions.md +102 -28
  260. package/adapters/cursor/skills/forge-fix/references/stage-exit-protocol.md +395 -147
  261. package/adapters/cursor/skills/forge-guide/forge-guide.mdc +1 -1
  262. package/adapters/cursor/skills/forge-guide/references/forge-config-schema.json +19 -1
  263. package/adapters/cursor/skills/forge-guide/references/process-overview.md +2 -2
  264. package/adapters/cursor/skills/forge-guide/references/ralph-loop-contract.md +6 -3
  265. package/adapters/cursor/skills/forge-guide/references/shared-conventions.md +102 -28
  266. package/adapters/cursor/skills/forge-init/forge-init.mdc +1 -1
  267. package/adapters/cursor/skills/forge-verify/forge-verify.mdc +82 -51
  268. package/adapters/cursor/skills/forge-verify/references/decisions/single-writer-threat-model.md +53 -0
  269. package/adapters/cursor/skills/forge-verify/references/findings-template.md +70 -50
  270. package/adapters/cursor/skills/forge-verify/references/shared-conventions.md +102 -28
  271. package/adapters/cursor/skills/forge-verify/references/stage-exit-protocol.md +516 -0
  272. package/adapters/cursor/skills/forge-verify/references/verification-checklists/backlog.md +73 -0
  273. package/adapters/cursor/skills/forge-verify/references/verification-checklists/epic.md +5 -4
  274. package/adapters/cursor/skills/forge-verify/references/verification-checklists/impl.md +85 -0
  275. package/adapters/cursor/skills/forge-verify/references/verification-checklists/specs.md +45 -1
  276. package/adapters/gemini/.feature-forge-bundle.json +1 -1
  277. package/adapters/gemini/agents/forge-verifier.md +3 -1
  278. package/adapters/gemini/gemini-extension.json +1 -1
  279. package/adapters/gemini/references/decisions/single-writer-threat-model.md +53 -0
  280. package/adapters/gemini/references/epic-manifest-schema.json +6 -1
  281. package/adapters/gemini/references/epic-state-schema.json +50 -0
  282. package/adapters/gemini/references/forge-config-schema.json +19 -1
  283. package/adapters/gemini/references/forge-decisions-schema.json +33 -0
  284. package/adapters/gemini/references/pipeline-state-schema.json +38 -4
  285. package/adapters/gemini/references/process-overview.md +2 -2
  286. package/adapters/gemini/references/ralph-loop-contract.md +6 -3
  287. package/adapters/gemini/references/shared-conventions.md +102 -28
  288. package/adapters/gemini/references/stage-exit-protocol.md +395 -147
  289. package/adapters/gemini/scripts/epic-manifest.py +495 -91
  290. package/adapters/gemini/scripts/fix-sweep.py +1180 -0
  291. package/adapters/gemini/scripts/forge-bootstrap.py +57 -5
  292. package/adapters/gemini/scripts/forge-session.py +4241 -146
  293. package/adapters/gemini/scripts/validate-traceability.py +86 -5
  294. package/adapters/gemini/skills/forge/forge.md +13 -12
  295. package/adapters/gemini/skills/forge/references/pipeline-state-schema.json +38 -4
  296. package/adapters/gemini/skills/forge/references/process-overview.md +2 -2
  297. package/adapters/gemini/skills/forge/references/shared-conventions.md +102 -28
  298. package/adapters/gemini/skills/forge/references/stage-exit-protocol.md +395 -147
  299. package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +7 -2
  300. package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +37 -33
  301. package/adapters/gemini/skills/forge-0-epic/references/epic-manifest-subcommands.md +8 -3
  302. package/adapters/gemini/skills/forge-0-epic/references/pipeline-state-schema.json +38 -4
  303. package/adapters/gemini/skills/forge-0-epic/references/shared-conventions.md +102 -28
  304. package/adapters/gemini/skills/forge-0-epic/references/stage-exit-protocol.md +395 -147
  305. package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +17 -4
  306. package/adapters/gemini/skills/forge-1-prd/references/shared-conventions.md +102 -28
  307. package/adapters/gemini/skills/forge-1-prd/references/stage-exit-protocol.md +395 -147
  308. package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +18 -4
  309. package/adapters/gemini/skills/forge-2-tech/references/shared-conventions.md +102 -28
  310. package/adapters/gemini/skills/forge-2-tech/references/stage-exit-protocol.md +395 -147
  311. package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +9 -3
  312. package/adapters/gemini/skills/forge-3-specs/references/shared-conventions.md +102 -28
  313. package/adapters/gemini/skills/forge-3-specs/references/spec-archetypes.md +9 -0
  314. package/adapters/gemini/skills/forge-3-specs/references/stage-exit-protocol.md +395 -147
  315. package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +56 -5
  316. package/adapters/gemini/skills/forge-4-backlog/references/shared-conventions.md +102 -28
  317. package/adapters/gemini/skills/forge-4-backlog/references/stage-exit-protocol.md +395 -147
  318. package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +67 -69
  319. package/adapters/gemini/skills/forge-5-loop/references/agent-selection.md +17 -1
  320. package/adapters/gemini/skills/forge-5-loop/references/ralph-loop-contract.md +6 -3
  321. package/adapters/gemini/skills/forge-5-loop/references/recovery-procedure.md +349 -0
  322. package/adapters/gemini/skills/forge-5-loop/references/result-reporting.md +101 -33
  323. package/adapters/gemini/skills/forge-5-loop/references/runner-contract.md +30 -10
  324. package/adapters/gemini/skills/forge-5-loop/references/shared-conventions.md +102 -28
  325. package/adapters/gemini/skills/forge-5-loop/references/stage-exit-protocol.md +395 -147
  326. package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +75 -8
  327. package/adapters/gemini/skills/forge-6-docs/references/shared-conventions.md +102 -28
  328. package/adapters/gemini/skills/forge-6-docs/references/stage-exit-protocol.md +516 -0
  329. package/adapters/gemini/skills/forge-fix/forge-fix.md +118 -32
  330. package/adapters/gemini/skills/forge-fix/references/shared-conventions.md +102 -28
  331. package/adapters/gemini/skills/forge-fix/references/stage-exit-protocol.md +395 -147
  332. package/adapters/gemini/skills/forge-guide/forge-guide.md +1 -1
  333. package/adapters/gemini/skills/forge-guide/references/forge-config-schema.json +19 -1
  334. package/adapters/gemini/skills/forge-guide/references/process-overview.md +2 -2
  335. package/adapters/gemini/skills/forge-guide/references/ralph-loop-contract.md +6 -3
  336. package/adapters/gemini/skills/forge-guide/references/shared-conventions.md +102 -28
  337. package/adapters/gemini/skills/forge-init/forge-init.md +1 -1
  338. package/adapters/gemini/skills/forge-verify/forge-verify.md +82 -51
  339. package/adapters/gemini/skills/forge-verify/references/decisions/single-writer-threat-model.md +53 -0
  340. package/adapters/gemini/skills/forge-verify/references/findings-template.md +70 -50
  341. package/adapters/gemini/skills/forge-verify/references/shared-conventions.md +102 -28
  342. package/adapters/gemini/skills/forge-verify/references/stage-exit-protocol.md +516 -0
  343. package/adapters/gemini/skills/forge-verify/references/verification-checklists/backlog.md +73 -0
  344. package/adapters/gemini/skills/forge-verify/references/verification-checklists/epic.md +5 -4
  345. package/adapters/gemini/skills/forge-verify/references/verification-checklists/impl.md +85 -0
  346. package/adapters/gemini/skills/forge-verify/references/verification-checklists/specs.md +45 -1
  347. package/adapters/pi/.feature-forge-bundle.json +1 -1
  348. package/adapters/pi/agents/forge-verifier.md +3 -1
  349. package/adapters/pi/references/decisions/single-writer-threat-model.md +53 -0
  350. package/adapters/pi/references/epic-manifest-schema.json +6 -1
  351. package/adapters/pi/references/epic-state-schema.json +50 -0
  352. package/adapters/pi/references/forge-config-schema.json +19 -1
  353. package/adapters/pi/references/forge-decisions-schema.json +33 -0
  354. package/adapters/pi/references/pipeline-state-schema.json +38 -4
  355. package/adapters/pi/references/process-overview.md +2 -2
  356. package/adapters/pi/references/ralph-loop-contract.md +6 -3
  357. package/adapters/pi/references/shared-conventions.md +91 -17
  358. package/adapters/pi/references/stage-exit-protocol.md +395 -147
  359. package/adapters/pi/scripts/epic-manifest.py +495 -91
  360. package/adapters/pi/scripts/fix-sweep.py +1180 -0
  361. package/adapters/pi/scripts/forge-bootstrap.py +57 -5
  362. package/adapters/pi/scripts/forge-session.py +4241 -146
  363. package/adapters/pi/scripts/validate-traceability.py +86 -5
  364. package/adapters/pi/skills/forge/SKILL.md +10 -9
  365. package/adapters/pi/skills/forge/references/pipeline-state-schema.json +38 -4
  366. package/adapters/pi/skills/forge/references/process-overview.md +2 -2
  367. package/adapters/pi/skills/forge/references/shared-conventions.md +91 -17
  368. package/adapters/pi/skills/forge/references/stage-exit-protocol.md +395 -147
  369. package/adapters/pi/skills/forge-0-epic/SKILL.md +7 -2
  370. package/adapters/pi/skills/forge-0-epic/references/edit-mode.md +31 -27
  371. package/adapters/pi/skills/forge-0-epic/references/epic-manifest-subcommands.md +6 -1
  372. package/adapters/pi/skills/forge-0-epic/references/pipeline-state-schema.json +38 -4
  373. package/adapters/pi/skills/forge-0-epic/references/shared-conventions.md +91 -17
  374. package/adapters/pi/skills/forge-0-epic/references/stage-exit-protocol.md +395 -147
  375. package/adapters/pi/skills/forge-1-prd/SKILL.md +17 -4
  376. package/adapters/pi/skills/forge-1-prd/references/shared-conventions.md +91 -17
  377. package/adapters/pi/skills/forge-1-prd/references/stage-exit-protocol.md +395 -147
  378. package/adapters/pi/skills/forge-2-tech/SKILL.md +18 -4
  379. package/adapters/pi/skills/forge-2-tech/references/shared-conventions.md +91 -17
  380. package/adapters/pi/skills/forge-2-tech/references/stage-exit-protocol.md +395 -147
  381. package/adapters/pi/skills/forge-3-specs/SKILL.md +9 -3
  382. package/adapters/pi/skills/forge-3-specs/references/shared-conventions.md +91 -17
  383. package/adapters/pi/skills/forge-3-specs/references/spec-archetypes.md +9 -0
  384. package/adapters/pi/skills/forge-3-specs/references/stage-exit-protocol.md +395 -147
  385. package/adapters/pi/skills/forge-4-backlog/SKILL.md +56 -5
  386. package/adapters/pi/skills/forge-4-backlog/references/shared-conventions.md +91 -17
  387. package/adapters/pi/skills/forge-4-backlog/references/stage-exit-protocol.md +395 -147
  388. package/adapters/pi/skills/forge-5-loop/SKILL.md +67 -69
  389. package/adapters/pi/skills/forge-5-loop/references/agent-selection.md +16 -0
  390. package/adapters/pi/skills/forge-5-loop/references/ralph-loop-contract.md +6 -3
  391. package/adapters/pi/skills/forge-5-loop/references/recovery-procedure.md +349 -0
  392. package/adapters/pi/skills/forge-5-loop/references/result-reporting.md +101 -33
  393. package/adapters/pi/skills/forge-5-loop/references/runner-contract.md +29 -9
  394. package/adapters/pi/skills/forge-5-loop/references/shared-conventions.md +91 -17
  395. package/adapters/pi/skills/forge-5-loop/references/stage-exit-protocol.md +395 -147
  396. package/adapters/pi/skills/forge-6-docs/SKILL.md +75 -8
  397. package/adapters/pi/skills/forge-6-docs/references/shared-conventions.md +91 -17
  398. package/adapters/pi/skills/forge-6-docs/references/stage-exit-protocol.md +516 -0
  399. package/adapters/pi/skills/forge-fix/SKILL.md +118 -32
  400. package/adapters/pi/skills/forge-fix/references/shared-conventions.md +91 -17
  401. package/adapters/pi/skills/forge-fix/references/stage-exit-protocol.md +395 -147
  402. package/adapters/pi/skills/forge-guide/references/forge-config-schema.json +19 -1
  403. package/adapters/pi/skills/forge-guide/references/process-overview.md +2 -2
  404. package/adapters/pi/skills/forge-guide/references/ralph-loop-contract.md +6 -3
  405. package/adapters/pi/skills/forge-guide/references/shared-conventions.md +91 -17
  406. package/adapters/pi/skills/forge-verify/SKILL.md +81 -50
  407. package/adapters/pi/skills/forge-verify/references/decisions/single-writer-threat-model.md +53 -0
  408. package/adapters/pi/skills/forge-verify/references/findings-template.md +69 -49
  409. package/adapters/pi/skills/forge-verify/references/shared-conventions.md +91 -17
  410. package/adapters/pi/skills/forge-verify/references/stage-exit-protocol.md +516 -0
  411. package/adapters/pi/skills/forge-verify/references/verification-checklists/backlog.md +73 -0
  412. package/adapters/pi/skills/forge-verify/references/verification-checklists/epic.md +5 -4
  413. package/adapters/pi/skills/forge-verify/references/verification-checklists/impl.md +85 -0
  414. package/adapters/pi/skills/forge-verify/references/verification-checklists/specs.md +45 -1
  415. package/package.json +1 -1
  416. package/adapters/claude/skills/forge-verify/references/pipeline-state-schema.json +0 -191
  417. package/adapters/codex/skills/forge-verify/references/pipeline-state-schema.json +0 -191
  418. package/adapters/copilot/skills/forge-verify/references/pipeline-state-schema.json +0 -191
  419. package/adapters/cursor/skills/forge-verify/references/pipeline-state-schema.json +0 -191
  420. package/adapters/gemini/skills/forge-verify/references/pipeline-state-schema.json +0 -191
  421. package/adapters/pi/skills/forge-verify/references/pipeline-state-schema.json +0 -191
@@ -12,9 +12,12 @@ Usage:
12
12
  python3 epic-manifest.py render-status <epic> [--specs-dir DIR] [--json]
13
13
  python3 epic-manifest.py add-feature <epic> <name> --charter TEXT \
14
14
  [--depends-on A,B] [--specs-dir DIR] [--json]
15
+ python3 epic-manifest.py adopt-feature <epic> <name> [--charter TEXT] \
16
+ [--depends-on A,B] [--specs-dir DIR] [--json]
15
17
  python3 epic-manifest.py remove-feature <epic> <name> [--specs-dir DIR] [--json]
16
18
  python3 epic-manifest.py reorder <epic> --order A,B,C [--specs-dir DIR] [--json]
17
19
  python3 epic-manifest.py set-dep <epic> <name> --depends-on A,B [--specs-dir DIR] [--json]
20
+ python3 epic-manifest.py set-charter <epic> <name> --charter TEXT [--specs-dir DIR] [--json]
18
21
  python3 epic-manifest.py set-status <epic> --status STATE [--specs-dir DIR] [--json]
19
22
 
20
23
  Exit codes:
@@ -36,10 +39,10 @@ from typing import Final, Literal, TypedDict
36
39
 
37
40
 
38
41
  # --------------------------------------------------------------------------- #
39
- # Constants (00-core-definitions.md §6)
42
+ # Constants
40
43
  # --------------------------------------------------------------------------- #
41
44
 
42
- #: A safe feature/epic name: one kebab-case token (00 §6).
45
+ #: A safe feature/epic name: one kebab-case token.
43
46
  SAFE_NAME_RE: Final = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
44
47
  #: A directory is "feature-shaped" iff it directly contains this file.
45
48
  PIPELINE_STATE_FILENAME: Final = ".pipeline-state.json"
@@ -55,10 +58,17 @@ NARRATIVE_FILENAME: Final = "EPIC.md"
55
58
  #: NOTE: forge-session.py keeps a byte-identical copy of this constant — flat, self-
56
59
  #: contained scripts have no shared import module (each is copied verbatim into adapters).
57
60
  KNOWN_VERIFY_STATUSES: Final = frozenset(
58
- {"pending", "passed", "findings-reported", "findings-applied", "skipped"}
61
+ {
62
+ "pending",
63
+ "auto-verify-pending",
64
+ "passed",
65
+ "findings-reported",
66
+ "findings-applied",
67
+ "skipped",
68
+ }
59
69
  )
60
70
  #: 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),
71
+ #: complete-for-orchestration. A STRICT subset — 'findings-reported' (unfixed),
62
72
  #: 'skipped', and 'pending' do NOT unblock dependents. Not collapsible into the set above.
63
73
  _VERIFY_ORCH_COMPLETE: Final = frozenset({"passed", "findings-applied"})
64
74
 
@@ -77,9 +87,44 @@ _PRODUCTION_STAGES: Final = (
77
87
  "forge-6-docs",
78
88
  )
79
89
 
90
+ #: The served production stage behind each ``forge-verify-{token}`` member entry, in
91
+ #: pipeline order — the inverse of ``VERIFY_TOKEN_BY_STAGE`` in forge-session.py. Used
92
+ #: to NAME the stage owed automatic verification, and iterated (rather than
93
+ #: iterating the state file's own key order) so the warning order is deterministic
94
+ #: regardless of how a member's state document happens to be serialized.
95
+ _VERIFY_STAGE_BY_TOKEN: Final[dict[str, str]] = {
96
+ "prd": "forge-1-prd",
97
+ "tech": "forge-2-tech",
98
+ "specs": "forge-3-specs",
99
+ "backlog": "forge-4-backlog",
100
+ "impl": "forge-5-loop",
101
+ }
102
+
103
+ #: Epic-scoped verification state lives beside the manifest, NEVER in a member's
104
+ #: .pipeline-state.json (REQ-SEC-01). Mirrors ``EPIC_STATE_FILENAME`` in
105
+ #: forge-session.py.
106
+ EPIC_STATE_FILENAME: Final = ".epic-state.json"
107
+ #: The single verify entry an epic root carries.
108
+ EPIC_VERIFY_KEY: Final = "forge-verify-epic"
109
+ #: The stage identifier epic verification is served for — used in diagnostics only.
110
+ EPIC_VERIFY_STAGE: Final = "forge-0-epic"
111
+
112
+ #: The statuses that count as a RESOLVED verification for freshness classification.
113
+ #: Mirrors ``_VERIFY_RESOLVED`` in forge-session.py. ``auto-verify-pending`` is
114
+ #: deliberately absent: recorded debt is owed, not resolved (REQ-DEBT-02).
115
+ _VERIFY_RESOLVED: Final = frozenset({"passed", "findings-applied", "skipped"})
116
+
117
+ #: The obligation sentence. Mirrors ``AUTO_PENDING_DIAGNOSTIC`` in
118
+ #: forge-session.py so the navigator ledger and the epic dashboard say the same thing
119
+ #: about the same debt; the two scripts share no import module.
120
+ AUTO_PENDING_DIAGNOSTIC: Final = (
121
+ "{subject}: automatic verification is still pending for {stage}; "
122
+ "run {command} to resolve it."
123
+ )
124
+
80
125
 
81
126
  # --------------------------------------------------------------------------- #
82
- # Type Definitions (00-core-definitions.md §4, §5; 02 §8.4)
127
+ # Type Definitions
83
128
  # --------------------------------------------------------------------------- #
84
129
 
85
130
  FindingCode = Literal[
@@ -114,12 +159,12 @@ class Finding(TypedDict):
114
159
  DerivedStatus = Literal[
115
160
  "not-started", # no .pipeline-state.json, or all stages pending
116
161
  "in-progress", # at least one stage started, loop not complete-for-orchestration
117
- "complete", # complete-for-orchestration per 00 §7
162
+ "complete", # complete-for-orchestration
118
163
  ]
119
164
 
120
165
 
121
166
  class FeatureStatus(TypedDict):
122
- """Live per-feature status derived from its own pipeline state (00 §5).
167
+ """Live per-feature status derived from its own pipeline state.
123
168
 
124
169
  Attributes:
125
170
  name: Feature name.
@@ -129,7 +174,7 @@ class FeatureStatus(TypedDict):
129
174
  navigator status semantics for display.
130
175
  blocked: True if any entry in unmetDeps is non-empty.
131
176
  unmetDeps: Names of this feature's direct dependencies that are not yet
132
- complete-for-orchestration (00 §7). Empty when actionable or complete.
177
+ complete-for-orchestration. Empty when actionable or complete.
133
178
  openEpicChangeRequests: Count of this member's ``epicChangeRequests``
134
179
  entries with ``status == "open"`` — epic-level change requests raised
135
180
  by a member stage that forge-0-epic edit mode has not yet reconciled.
@@ -137,6 +182,12 @@ class FeatureStatus(TypedDict):
137
182
  blockingEpicChangeRequests: The subset of ``openEpicChangeRequests`` with
138
183
  ``blocksCurrent == true`` (pause-now, reconcile-before-specs). Always
139
184
  ``<= openEpicChangeRequests``.
185
+ docsStatus: The member's recorded ``stages["forge-6-docs"].status``, or
186
+ None when absent/malformed. DISTINCT from the orchestration rollup
187
+ (which reads forge-5-loop + impl verify and deliberately ignores
188
+ docs): consumers gating a DOCUMENTATION decision — the epic-level
189
+ doc offer (#173) — read this, treating ``complete`` and ``skipped``
190
+ (#197's deliberate skip) both as docs-settled.
140
191
  """
141
192
 
142
193
  name: str
@@ -146,33 +197,39 @@ class FeatureStatus(TypedDict):
146
197
  unmetDeps: list[str]
147
198
  openEpicChangeRequests: int
148
199
  blockingEpicChangeRequests: int
200
+ docsStatus: str | None
149
201
 
150
202
 
151
203
  class Rollup(TypedDict):
152
- """Aggregate completion counts for the epic dashboard (00 §8)."""
204
+ """Aggregate completion counts for the epic dashboard."""
153
205
 
154
- complete: int #: Number of member features complete-for-orchestration (00 §7).
206
+ complete: int #: Number of member features complete-for-orchestration.
155
207
  total: int #: Total member features in the manifest (0 for an empty epic).
156
208
 
157
209
 
158
210
  class RenderStatus(TypedDict):
159
- """The full live dashboard payload returned by render_status (00 §5, §8).
211
+ """The full live dashboard payload returned by render_status.
160
212
 
161
213
  Attributes:
162
214
  epic: The epic name (manifest `epic`).
163
- status: The epic lifecycle status (00 §2.1).
215
+ status: The epic lifecycle status.
164
216
  features: Per-member status rows, one per manifest feature (may be empty).
165
217
  actionable: Names of features whose dependsOn are all complete and that
166
- are not themselves complete (00 §8).
218
+ are not themselves complete.
167
219
  parallelEligible: Subset of `actionable` with no mutual (transitive)
168
- dependency — surfaced for future parallel execution (00 §8).
220
+ dependency — surfaced for future parallel execution.
169
221
  rollup: Aggregate {complete, total} counts.
170
222
  nextCommand: Recommended next command for the first actionable feature, or
171
223
  None when nothing is actionable (all complete, empty epic, or paused).
172
224
  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.
225
+ need surfacing. Two kinds, in this order: members carrying a
226
+ ``forge-verify-*.status`` outside ``KNOWN_VERIFY_STATUSES`` (treated as
227
+ incomplete, but silently so would poison the rollup + dependency gates,
228
+ #148), then OBLIGATION warnings for owed automatic verification — first
229
+ per member in pipeline-stage order, then the epic root.
230
+ The two are deliberately distinct: an unknown status is a corrupt value,
231
+ owed debt is a valid value naming work that has not happened. Empty in
232
+ the common case.
176
233
  """
177
234
 
178
235
  epic: str
@@ -186,7 +243,7 @@ class RenderStatus(TypedDict):
186
243
 
187
244
 
188
245
  # --------------------------------------------------------------------------- #
189
- # Internal Exceptions (02 §2)
246
+ # Internal Exceptions
190
247
  # --------------------------------------------------------------------------- #
191
248
 
192
249
 
@@ -209,7 +266,7 @@ class UsageError(Exception):
209
266
  class FindingsError(Exception):
210
267
  """A non-fatal validation outcome that must exit 1.
211
268
 
212
- Raised when an operation produces one or more Findings (00 §4) that block a
269
+ Raised when an operation produces one or more Findings that block a
213
270
  gating operation: a cycle, a dangling ref, an ambiguous/not-found name, etc.
214
271
  Maps to exit code 1. Carries the structured findings so the dispatch layer
215
272
  can emit them as JSON or human lines.
@@ -224,7 +281,7 @@ class FindingsError(Exception):
224
281
 
225
282
 
226
283
  # --------------------------------------------------------------------------- #
227
- # Safety & I/O Layer (02 §3)
284
+ # Safety & I/O Layer
228
285
  # --------------------------------------------------------------------------- #
229
286
 
230
287
 
@@ -243,7 +300,7 @@ def assert_safe_name(name: str) -> None:
243
300
  equals '..', or fails SAFE_NAME_RE. The message embeds the
244
301
  offending name (e.g. ``unsafe name '../escape'``) so the caller can
245
302
  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
303
+ but is raised as a usage error because it is detected before
247
304
  any manifest is read.
248
305
  """
249
306
  if (
@@ -278,7 +335,7 @@ def contained_path(base: Path, *parts: str) -> Path:
278
335
  UsageError: If the resolved path escapes ``base`` (message:
279
336
  ``resolved path escapes specs dir: …``). Containment violations
280
337
  surface only as exit-2 usage errors per the error model in
281
- tech-spec §6 (there is no dedicated Finding code for them).
338
+ the manifest contract (there is no dedicated Finding code for them).
282
339
  """
283
340
  base_real = base.resolve()
284
341
  target = (base_real / Path(*parts)).resolve()
@@ -296,6 +353,12 @@ def load_manifest(epic_dir: Path) -> dict:
296
353
  epic_dir: The epic subtree directory (must already be contained within
297
354
  {specsDir} via contained_path).
298
355
 
356
+ A legacy manifest written before the canonical ``revision`` field existed is
357
+ presented as logical ``revision: 1`` in the returned dict WITHOUT rewriting the
358
+ file. Legacy validation and rendering therefore keep working, and the
359
+ file's bytes only change on its first genuine semantic mutation — which writes
360
+ ``revision: 2`` (REQ-DEBT-06, REQ-COMPAT-02).
361
+
299
362
  Returns:
300
363
  The parsed manifest as a plain dict. Structural validation (schema,
301
364
  cycles, dangling refs) is performed separately by ``validate`` — this
@@ -304,7 +367,7 @@ def load_manifest(epic_dir: Path) -> dict:
304
367
  Raises:
305
368
  UsageError: If the manifest file is missing or unreadable (exit 2).
306
369
  FindingsError: If the file exists but is not parseable JSON — emits a
307
- single 'corrupt-json' Finding (00 §4) with the JSON error position,
370
+ single 'corrupt-json' Finding with the JSON error position,
308
371
  so a hand-corrupted manifest fails with an actionable message rather
309
372
  than a traceback (REQ-ROBUST-02). Exit 1.
310
373
  """
@@ -316,7 +379,7 @@ def load_manifest(epic_dir: Path) -> dict:
316
379
  except OSError as exc:
317
380
  raise UsageError(f"cannot read manifest {path}: {exc}")
318
381
  try:
319
- return json.loads(text)
382
+ parsed = json.loads(text)
320
383
  except json.JSONDecodeError as exc:
321
384
  raise FindingsError([
322
385
  {
@@ -325,6 +388,9 @@ def load_manifest(epic_dir: Path) -> dict:
325
388
  "feature": None,
326
389
  }
327
390
  ])
391
+ if isinstance(parsed, dict) and "revision" not in parsed:
392
+ parsed["revision"] = 1 # synthesized only — never written back here.
393
+ return parsed
328
394
 
329
395
 
330
396
  def atomic_write(path: Path, data: dict) -> None:
@@ -334,7 +400,8 @@ def atomic_write(path: Path, data: dict) -> None:
334
400
  and fsyncs it, then ``os.replace`` swaps it into place. ``os.replace`` is
335
401
  atomic on POSIX within a single filesystem, so an interrupted write never
336
402
  leaves a partial or corrupt manifest (REQ-ROBUST-03). Concurrent multi-
337
- session mutation is out of scope (single-writer assumed, PRD REQ-ROBUST-03).
403
+ session mutation is out of scope (single-writer assumed, PRD REQ-ROBUST-03;
404
+ decision record: references/decisions/single-writer-threat-model.md, #180).
338
405
 
339
406
  Args:
340
407
  path: The destination manifest path (e.g. {epic}/epic-manifest.json).
@@ -373,12 +440,12 @@ def atomic_write(path: Path, data: dict) -> None:
373
440
 
374
441
 
375
442
  # --------------------------------------------------------------------------- #
376
- # Graph Algorithms (02 §4) — implemented in item 004
443
+ # Graph Algorithms — implemented in item 004
377
444
  # --------------------------------------------------------------------------- #
378
445
 
379
446
 
380
447
  def find_cycle(features: list[dict]) -> list[str] | None:
381
- """Return a cycle in the dependsOn graph, or None if acyclic (02 §4).
448
+ """Return a cycle in the dependsOn graph, or None if acyclic.
382
449
 
383
450
  Iterative DFS over the directed graph whose edges are ``feature -> dep``.
384
451
  On the first back-edge into a GRAY node, reconstructs and returns the cycle
@@ -429,7 +496,7 @@ def find_cycle(features: list[dict]) -> list[str] | None:
429
496
  def unmet_deps(
430
497
  name: str, features: list[dict], complete: dict[str, bool]
431
498
  ) -> list[str]:
432
- """Return a feature's direct dependencies that are not complete (02 §4).
499
+ """Return a feature's direct dependencies that are not complete.
433
500
 
434
501
  Names of this feature's direct ``dependsOn`` entries whose value in
435
502
  ``complete`` is False, preserving manifest order. Empty when the feature is
@@ -441,15 +508,15 @@ def unmet_deps(
441
508
 
442
509
 
443
510
  # --------------------------------------------------------------------------- #
444
- # Resolution & Uniqueness (02 §5) — implemented in item 005
511
+ # Resolution & Uniqueness — implemented in item 005
445
512
  # --------------------------------------------------------------------------- #
446
513
 
447
514
 
448
515
  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).
516
+ """Map every feature name in the specs tree to the dirs that bear it.
450
517
 
451
518
  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):
519
+ it directly contains a ``.pipeline-state.json`` (REQ-DIR-03):
453
520
  * flat: {specsDir}/{name}/.pipeline-state.json
454
521
  * nested: {specsDir}/{epic}/{name}/.pipeline-state.json
455
522
 
@@ -457,7 +524,7 @@ def feature_dirs(specs_dir: Path) -> dict[str, list[Path]]:
457
524
  entry is a uniqueness violation (REQ-DIR-04) surfaced as 'ambiguous' or
458
525
  'duplicate-name' by the caller. Epic directories themselves (which hold
459
526
  ``epic-manifest.json`` but no ``.pipeline-state.json``) are skipped, so an
460
- epic name never collides with a feature name (01 §4.3).
527
+ epic name never collides with a feature name.
461
528
 
462
529
  Args:
463
530
  specs_dir: The configured specs directory (already verified to exist).
@@ -480,9 +547,9 @@ def feature_dirs(specs_dir: Path) -> dict[str, list[Path]]:
480
547
 
481
548
 
482
549
  def resolve(name: str, specs_dir: Path) -> Path:
483
- """Resolve a bare feature/epic name to its absolute directory (02 §5).
550
+ """Resolve a bare feature/epic name to its absolute directory.
484
551
 
485
- Implements the 5-step algorithm (tech-spec §3.4):
552
+ Implements the 5-step algorithm:
486
553
  1. reject unsafe names (assert_safe_name) — exit 2 before any FS access;
487
554
  2. flat match: {specsDir}/{name}/.pipeline-state.json wins outright;
488
555
  3. exactly one nested match resolves cleanly;
@@ -502,7 +569,7 @@ def resolve(name: str, specs_dir: Path) -> Path:
502
569
  Raises:
503
570
  UsageError: Unsafe name or missing specs dir (exit 2).
504
571
  FindingsError: 'ambiguous' (lists every matching path) or 'not-found'
505
- (exit 1). 00 §4.2 gives the canonical message shapes.
572
+ (exit 1).
506
573
  """
507
574
  assert_safe_name(name)
508
575
  if not specs_dir.is_dir():
@@ -530,10 +597,10 @@ def resolve(name: str, specs_dir: Path) -> Path:
530
597
 
531
598
 
532
599
  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).
600
+ """Return a duplicate-name finding if the name is already taken.
534
601
 
535
602
  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
603
+ name collision can be introduced (REQ-DIR-04). Any single
537
604
  existing occurrence is enough to reject — unlike ``resolve``, which tolerates
538
605
  a uniquely-matching name and only errors on genuine multi-match.
539
606
 
@@ -566,43 +633,44 @@ def check_name(name: str, specs_dir: Path) -> list[Finding]:
566
633
 
567
634
 
568
635
  # --------------------------------------------------------------------------- #
569
- # Validation (02 §6.2, §10) — implemented in item 006
636
+ # Validation — implemented in item 006
570
637
  # --------------------------------------------------------------------------- #
571
638
 
572
639
 
573
- #: Top-level required keys (00 §2.1, mirrors epic-manifest-schema.json).
640
+ #: Top-level required keys (mirrors epic-manifest-schema.json). Doubles as
641
+ #: the allow-list for the unknown-top-level-key check below.
574
642
  _TOP_REQUIRED: Final = (
575
- "schemaVersion", "epic", "description", "status",
643
+ "schemaVersion", "revision", "epic", "description", "status",
576
644
  "narrativeDoc", "createdAt", "updatedAt", "features",
577
645
  )
578
- #: Required keys on each Feature object (00 §2.2).
646
+ #: Required keys on each Feature object.
579
647
  _FEATURE_REQUIRED: Final = ("name", "charter", "dependsOn", "exposes", "consumes")
580
648
  #: Optional keys on each Feature object. `mutatesShared` is the #144 precision
581
649
  #: hint for cross-member coupling (array of project-root-relative path strings);
582
650
  #: schema-legal when present, ignored when absent (mirrors
583
651
  #: epic-manifest-schema.json definitions.feature.properties.mutatesShared).
584
652
  _FEATURE_OPTIONAL: Final = ("mutatesShared",)
585
- #: Required keys on each Contract (exposes[]) object (00 §2.3).
653
+ #: Required keys on each Contract (exposes[]) object.
586
654
  _CONTRACT_REQUIRED: Final = ("name", "kind", "summary")
587
- #: Required keys on each ConsumedContract (consumes[]) object (00 §2.4).
655
+ #: Required keys on each ConsumedContract (consumes[]) object.
588
656
  _CONSUMED_REQUIRED: Final = ("from", "name", "summary")
589
- #: Allowed epic lifecycle states (00 §2.1).
657
+ #: Allowed epic lifecycle states.
590
658
  _EPIC_STATUSES: Final = ("active", "paused", "abandoned", "complete")
591
- #: Allowed Contract kinds (00 §2.3).
659
+ #: Allowed Contract kinds.
592
660
  _CONTRACT_KINDS: Final = ("function", "type", "endpoint", "module", "event")
593
661
 
594
662
 
595
663
  def _schema(message: str, feature: str | None = None) -> Finding:
596
- """Construct a 'schema' Finding (00 §4)."""
664
+ """Construct a 'schema' Finding."""
597
665
  return {"code": "schema", "message": message, "feature": feature}
598
666
 
599
667
 
600
668
  def _schema_findings(manifest: dict) -> list[Finding]:
601
- """Hand-rolled stdlib schema checker over the manifest (02 §6.2, 00 §2.6).
669
+ """Hand-rolled stdlib schema checker over the manifest.
602
670
 
603
- Asserts required keys/types/enums/consts from 00 §2 and explicitly rejects
671
+ Asserts the required keys/types/enums/consts and explicitly rejects
604
672
  any ``features[].status`` key (REQ-STATE-02 -> 'cached-status'). No
605
- third-party ``jsonschema`` (01 §2.1). Returns 'schema' findings plus, for a
673
+ third-party ``jsonschema``. Returns 'schema' findings plus, for a
606
674
  per-feature status key, a 'cached-status' finding.
607
675
  """
608
676
  findings: list[Finding] = []
@@ -615,6 +683,12 @@ def _schema_findings(manifest: dict) -> list[Finding]:
615
683
 
616
684
  if "schemaVersion" in manifest and manifest["schemaVersion"] != 1:
617
685
  findings.append(_schema(f"schemaVersion must be 1, got {manifest['schemaVersion']!r}"))
686
+ if "revision" in manifest:
687
+ revision = manifest["revision"]
688
+ # `bool` is a subclass of `int`, so `True` would otherwise pass as revision 1
689
+ # and then silently arithmetic-increment to 2.
690
+ if isinstance(revision, bool) or not isinstance(revision, int) or revision < 1:
691
+ findings.append(_schema(f"revision must be an integer >= 1, got {revision!r}"))
618
692
  if "narrativeDoc" in manifest and manifest["narrativeDoc"] != NARRATIVE_FILENAME:
619
693
  findings.append(_schema(f"narrativeDoc must be {NARRATIVE_FILENAME!r}, got {manifest['narrativeDoc']!r}")) # noqa: E501
620
694
  for key in ("epic", "description", "createdAt", "updatedAt"):
@@ -698,9 +772,9 @@ def _schema_findings(manifest: dict) -> list[Finding]:
698
772
  def _validate_dict(
699
773
  manifest: dict, epic_dir: Path, specs_dir: Path
700
774
  ) -> list[Finding]:
701
- """Validate an already-parsed manifest dict, returning findings (02 §6.2).
775
+ """Validate an already-parsed manifest dict, returning findings.
702
776
 
703
- Runs the invariant checks of 00 §2.6 in order, short-circuiting only where a
777
+ Runs the invariant checks in order, short-circuiting only where a
704
778
  later check cannot run. Reused by the item-008 mutators on the EDITED dict
705
779
  before writing. Does not parse JSON (that is ``validate``'s job) — operates
706
780
  purely in memory.
@@ -788,7 +862,7 @@ def _validate_dict(
788
862
 
789
863
 
790
864
  def validate(epic_dir: Path, specs_dir: Path) -> list[Finding]:
791
- """Validate a single epic manifest, returning all findings (02 §6.2).
865
+ """Validate a single epic manifest, returning all findings.
792
866
 
793
867
  Parses the manifest (folding any corrupt-json finding from load_manifest
794
868
  into the returned list) then delegates to ``_validate_dict``. Raises
@@ -802,12 +876,12 @@ def validate(epic_dir: Path, specs_dir: Path) -> list[Finding]:
802
876
 
803
877
 
804
878
  # --------------------------------------------------------------------------- #
805
- # Live Status Derivation (02 §8) — implemented in item 007
879
+ # Live Status Derivation — implemented in item 007
806
880
  # --------------------------------------------------------------------------- #
807
881
 
808
882
 
809
883
  def is_complete_for_orchestration(state: dict) -> bool:
810
- """Apply the completion-for-orchestration predicate (00 §7, 02 §8.1).
884
+ """Apply the completion-for-orchestration predicate.
811
885
 
812
886
  A feature is complete-for-orchestration iff::
813
887
 
@@ -817,8 +891,7 @@ def is_complete_for_orchestration(state: dict) -> bool:
817
891
 
818
892
  A feature whose forge-verify-impl is 'findings-reported' (unfixed) is NOT
819
893
  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).
894
+ implementation of the predicate, reused by the dependency gate and handoff.
822
895
 
823
896
  Args:
824
897
  state: A parsed .pipeline-state.json dict (or {} if the member has none).
@@ -851,6 +924,11 @@ def _verify_status_warnings(name: str, state: dict) -> list[str]:
851
924
 
852
925
  Non-string / malformed status values (a list, an int) are also flagged, and the
853
926
  membership test is guarded so an unhashable value never raises.
927
+
928
+ ``auto-verify-pending`` is a KNOWN status and is therefore silent here.
929
+ Owed automatic verification is an OBLIGATION, not a corrupt value, so it is
930
+ surfaced separately by ``_auto_verify_debt_warnings`` with actionable wording —
931
+ reporting it as an unknown status would be actively misleading.
854
932
  """
855
933
  stages = state.get("stages", {})
856
934
  if not isinstance(stages, dict):
@@ -874,11 +952,209 @@ def _verify_status_warnings(name: str, state: dict) -> list[str]:
874
952
  return warnings
875
953
 
876
954
 
955
+ def _positive_int(value: object) -> int | None:
956
+ """Return ``value`` when it is a usable artifact revision, else None.
957
+
958
+ A bool is rejected before the int test on purpose: ``True`` is an ``int`` and
959
+ would otherwise compare equal to revision 1 (the same trap the manifest
960
+ ``revision`` validator guards). Absent, non-integer, and sub-1 values are
961
+ unusable — the caller keeps the debt owed rather than guessing.
962
+ """
963
+ if isinstance(value, bool) or not isinstance(value, int) or value < 1:
964
+ return None
965
+ return value
966
+
967
+
968
+ def _auto_pending_message(
969
+ subject: str,
970
+ stage: str,
971
+ command: str,
972
+ scheduled_version: int | None = None,
973
+ current_version: int | None = None,
974
+ ) -> str:
975
+ """Render the obligation sentence for owed automatic verification.
976
+
977
+ Mirrors ``auto_pending_message`` in forge-session.py — one sentence naming the
978
+ subject, the served stage, and the retry command, with both revision numbers
979
+ appended when the recorded schedule predates the current artifact. Never a
980
+ state-file dump (REQ-OBS-02).
981
+ """
982
+ message = AUTO_PENDING_DIAGNOSTIC.format(
983
+ subject=subject, stage=stage, command=command
984
+ )
985
+ if (
986
+ scheduled_version is not None
987
+ and current_version is not None
988
+ and scheduled_version != current_version
989
+ ):
990
+ message += (
991
+ f" The artifact has advanced since it was scheduled "
992
+ f"(scheduled at revision {scheduled_version}, now at revision "
993
+ f"{current_version})."
994
+ )
995
+ return message
996
+
997
+
998
+ def _auto_verify_debt_warnings(name: str, state: dict) -> list[str]:
999
+ """Surface every ``auto-verify-pending`` member entry as an obligation.
1000
+
1001
+ A member carrying recorded-but-undischarged automatic verification is neither
1002
+ "never verified" nor done: the directive that should have run it was dropped,
1003
+ crashed, or was interrupted, and the epic dashboard is where an operator notices
1004
+ (#163, REQ-DEBT-02/05). Emitted in pipeline-stage order — not the state file's key
1005
+ order — so repeated renders are byte-identical.
1006
+
1007
+ Args:
1008
+ name: The member feature name.
1009
+ state: The member's parsed .pipeline-state.json (or {}).
1010
+
1011
+ Returns:
1012
+ Zero or more sentences naming the member, the served production
1013
+ stage, and the ``forge-verify`` retry command.
1014
+ """
1015
+ stages = state.get("stages", {})
1016
+ if not isinstance(stages, dict):
1017
+ return []
1018
+ warnings: list[str] = []
1019
+ for token, served_stage in _VERIFY_STAGE_BY_TOKEN.items():
1020
+ entry = stages.get(f"forge-verify-{token}")
1021
+ if not isinstance(entry, dict) or entry.get("status") != "auto-verify-pending":
1022
+ continue
1023
+ production = stages.get(served_stage)
1024
+ warnings.append(_auto_pending_message(
1025
+ name,
1026
+ served_stage,
1027
+ f"/feature-forge:forge-verify {name}",
1028
+ _positive_int(entry.get("scheduledStageVersion")),
1029
+ _positive_int(production.get("version")) if isinstance(production, dict) else None,
1030
+ ))
1031
+ return warnings
1032
+
1033
+
1034
+ def _read_epic_state_safely(epic_dir: Path) -> dict:
1035
+ """Read an epic's own ``.epic-state.json``, tolerating absence and corruption.
1036
+
1037
+ Epic verification state is epic-scoped: this reads the sibling of the manifest
1038
+ and NEVER a member's ``.pipeline-state.json`` (REQ-SEC-01). A missing, unreadable,
1039
+ unparseable, or non-object file downgrades to ``{}``, which classifies as
1040
+ ``never`` — the dashboard must not crash on one torn file.
1041
+ """
1042
+ path = epic_dir / EPIC_STATE_FILENAME
1043
+ if not path.is_file():
1044
+ return {}
1045
+ try:
1046
+ parsed = json.loads(path.read_text(encoding="utf-8"))
1047
+ except (OSError, json.JSONDecodeError):
1048
+ return {}
1049
+ return parsed if isinstance(parsed, dict) else {}
1050
+
1051
+
1052
+ def epic_verify_state(epic_dir: Path, revision: int | None) -> str:
1053
+ """Classify epic-root verification freshness against the manifest revision.
1054
+
1055
+ The epic mirror of ``verify_state`` in forge-session.py, with the manifest
1056
+ ``revision`` standing in for a production stage's ``version``. It reads
1057
+ ``.epic-state.json`` and the supplied revision ONLY — no member state participates
1058
+ in epic verification (REQ-SEC-01).
1059
+
1060
+ Args:
1061
+ epic_dir: The epic subtree directory.
1062
+ revision: The manifest's canonical revision, or None when unusable.
1063
+
1064
+ Returns:
1065
+ One of:
1066
+
1067
+ - ``never`` — no epic state, no ``forge-verify-epic`` entry, or a
1068
+ status outside the known vocabulary. Verification was never scheduled.
1069
+ - ``auto-pending``— ``auto-verify-pending``: scheduled and owed. Classified
1070
+ ahead of every other rule and never downgraded to ``never``, even when its
1071
+ ``scheduledStageVersion`` is missing or names an older revision — a later
1072
+ manifest edit does not erase owed work (REQ-DEBT-02).
1073
+ - ``failing`` — ``findings-reported``: verification ran and its findings
1074
+ are not yet applied.
1075
+ - ``skipped`` — an explicit human decision, resolved under the existing
1076
+ compatibility rule: it records no version and so never goes stale.
1077
+ - ``fresh`` — ``passed`` whose ``verifiedStageVersion`` equals the
1078
+ current manifest revision.
1079
+ - ``stale`` — a resolved terminal entry whose recorded revision is
1080
+ absent or does not match. ``findings-applied`` is classified here
1081
+ UNCONDITIONALLY, not merely because the writer deletes
1082
+ ``verifiedStageVersion``: applying fixes is not verifying them, and legacy
1083
+ state loaded without migration (REQ-DEBT-06) may still carry the key.
1084
+ """
1085
+ return _classify_epic_verify_entry(_read_epic_verify_entry(epic_dir), revision)
1086
+
1087
+
1088
+ def _read_epic_verify_entry(epic_dir: Path) -> dict:
1089
+ """The ``forge-verify-epic`` entry out of ``.epic-state.json``, or ``{}``.
1090
+
1091
+ The single read both the classifier and the warning renderer share: a caller
1092
+ that needs the label AND the entry's metadata reads once and passes the same
1093
+ dict to both, so a file rewritten between two reads can never classify against
1094
+ one snapshot and index into another.
1095
+ """
1096
+ stages = _read_epic_state_safely(epic_dir).get("stages", {})
1097
+ entry = stages.get(EPIC_VERIFY_KEY) if isinstance(stages, dict) else None
1098
+ return entry if isinstance(entry, dict) else {}
1099
+
1100
+
1101
+ def _classify_epic_verify_entry(entry: dict, revision: int | None) -> str:
1102
+ """The classification half of ``epic_verify_state``, over an already-read entry."""
1103
+ if not entry:
1104
+ return "never"
1105
+ status = entry.get("status")
1106
+ if not isinstance(status, str):
1107
+ # A torn or hand-edited entry can carry any JSON type here; an unhashable
1108
+ # one would raise TypeError at the frozenset membership below, crashing
1109
+ # the whole dashboard on one bad file.
1110
+ return "never"
1111
+ if status == "auto-verify-pending":
1112
+ return "auto-pending"
1113
+ if status == "findings-reported":
1114
+ return "failing"
1115
+ if status == "skipped":
1116
+ return "skipped"
1117
+ if status not in _VERIFY_RESOLVED:
1118
+ return "never"
1119
+ if status == "findings-applied":
1120
+ # §4.2 step 4: applying fixes CLEARS freshness; only a later `passed` restores
1121
+ # it. Mirrors the identical guards in forge-session.py's `verify_state` and
1122
+ # `_classify_verify_entry` — §5.1 requires identical labels across all three,
1123
+ # and §5.2 requires manifest parity, so a partial fix is itself the drift
1124
+ # `test_stage_constants_parity.py` exists to catch.
1125
+ return "stale"
1126
+ verified = _positive_int(entry.get("verifiedStageVersion"))
1127
+ if verified is not None and revision is not None and verified == revision:
1128
+ return "fresh"
1129
+ return "stale"
1130
+
1131
+
1132
+ def _epic_verify_warnings(epic: str, epic_dir: Path, revision: int | None) -> list[str]:
1133
+ """Surface owed epic-root automatic verification as an obligation.
1134
+
1135
+ Only ``auto-pending`` warrants a dashboard warning: ``never`` is the ordinary
1136
+ state of an epic nobody has verified, and ``stale``/``failing`` are already
1137
+ reachable through the epic's own verify run. Owed-and-dropped debt is the case
1138
+ that is otherwise invisible.
1139
+ """
1140
+ entry = _read_epic_verify_entry(epic_dir)
1141
+ if _classify_epic_verify_entry(entry, revision) != "auto-pending":
1142
+ return []
1143
+ scheduled = _positive_int(entry.get("scheduledStageVersion"))
1144
+ return [_auto_pending_message(
1145
+ epic,
1146
+ EPIC_VERIFY_STAGE,
1147
+ f"/feature-forge:forge-verify {epic}",
1148
+ scheduled,
1149
+ revision,
1150
+ )]
1151
+
1152
+
877
1153
  def _read_state_safely(state_path: Path) -> dict:
878
1154
  """Read and parse a member's .pipeline-state.json, tolerating corruption.
879
1155
 
880
1156
  A missing, unreadable, unparseable, or torn (partially-written) member state
881
- downgrades to ``{}`` rather than crashing the dashboard (02 §8.2). Member
1157
+ downgrades to ``{}`` rather than crashing the dashboard. Member
882
1158
  state writes are made by forge-1..5 skills outside the helper's atomicity
883
1159
  scope, so a torn read is expected and simply renders that one feature as
884
1160
  ``not-started``.
@@ -893,7 +1169,7 @@ def _read_state_safely(state_path: Path) -> dict:
893
1169
 
894
1170
 
895
1171
  def derive_status(feature_dir: Path) -> FeatureStatus:
896
- """Derive a feature's live status from its own pipeline state (00 §5, 02 §8).
1172
+ """Derive a feature's live status from its own pipeline state.
897
1173
 
898
1174
  Reads ``{feature_dir}/.pipeline-state.json`` and maps it to a FeatureStatus:
899
1175
  missing/unparseable/all-pending -> ``not-started``; complete-for-
@@ -906,7 +1182,7 @@ def derive_status(feature_dir: Path) -> FeatureStatus:
906
1182
  feature_dir: The member feature's directory.
907
1183
 
908
1184
  Returns:
909
- A FeatureStatus (00 §5) with name, stage, coarse status, and placeholder
1185
+ A FeatureStatus with name, stage, coarse status, and placeholder
910
1186
  blocked/unmetDeps.
911
1187
  """
912
1188
  name = feature_dir.name
@@ -937,6 +1213,13 @@ def derive_status(feature_dir: Path) -> FeatureStatus:
937
1213
  open_count = len(open_reqs)
938
1214
  blocking_count = sum(1 for r in open_reqs if r.get("blocksCurrent") is True)
939
1215
 
1216
+ # Docs state for the epic-level doc offer gate (#173). Read directly rather
1217
+ # than folded into `derived`: docs completeness is deliberately invisible to
1218
+ # the orchestration rollup, and conflating them is the bug this field fixes.
1219
+ stages = state.get("stages", {})
1220
+ docs = stages.get("forge-6-docs") if isinstance(stages, dict) else None
1221
+ docs_status = docs.get("status") if isinstance(docs, dict) else None
1222
+
940
1223
  return {
941
1224
  "name": name,
942
1225
  "stage": stage,
@@ -945,11 +1228,12 @@ def derive_status(feature_dir: Path) -> FeatureStatus:
945
1228
  "unmetDeps": [],
946
1229
  "openEpicChangeRequests": open_count,
947
1230
  "blockingEpicChangeRequests": blocking_count,
1231
+ "docsStatus": docs_status if isinstance(docs_status, str) else None,
948
1232
  }
949
1233
 
950
1234
 
951
1235
  def _transitive_deps(name: str, adjacency: dict[str, list[str]]) -> set[str]:
952
- """Return all features reachable from ``name`` via dependsOn edges (00 §8)."""
1236
+ """Return all features reachable from ``name`` via dependsOn edges."""
953
1237
  seen: set[str] = set()
954
1238
  stack = list(adjacency.get(name, []))
955
1239
  while stack:
@@ -967,20 +1251,22 @@ def _next_production_stage(state: dict) -> str | None:
967
1251
  The epic-side mirror of ``next_stage()`` in forge-session.py, and for the same
968
1252
  reason its docstring gives: "what runs next" is DERIVED from ``stages[].status``,
969
1253
  never read from the stored ``currentStage``. A missing, pending, in-progress or
970
- stale stage all count as "not done".
1254
+ stale stage all count as "not done"; ``complete`` and ``skipped`` both count as
1255
+ done (``skipped`` is legal only on forge-6-docs — schema ``docsStageEntry`` —
1256
+ so a deliberately docs-skipped member is never re-offered forge-6-docs, #197).
971
1257
  """
972
1258
  stages = state.get("stages")
973
1259
  if not isinstance(stages, dict):
974
1260
  return _PRODUCTION_STAGES[0]
975
1261
  for stage in _PRODUCTION_STAGES:
976
1262
  entry = stages.get(stage)
977
- if not isinstance(entry, dict) or entry.get("status") != "complete":
1263
+ if not isinstance(entry, dict) or entry.get("status") not in ("complete", "skipped"):
978
1264
  return stage
979
1265
  return None
980
1266
 
981
1267
 
982
1268
  def _next_command(feature_dir: Path, status_row: FeatureStatus) -> str:
983
- """Recommend the next forge command for an actionable feature (02 §8.3).
1269
+ """Recommend the next forge command for an actionable feature.
984
1270
 
985
1271
  ``/feature-forge:forge-1-prd <name>`` when the feature's PRD is absent (or the
986
1272
  member has not progressed past epic creation), else the command for its next
@@ -989,9 +1275,14 @@ def _next_command(feature_dir: Path, status_row: FeatureStatus) -> str:
989
1275
  reading it here recommended re-running the stage the member had just finished
990
1276
  for the whole window before the next stage was entered).
991
1277
 
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.
1278
+ When every production stage is complete but the member is still actionable, its
1279
+ ``forge-verify-impl`` entry is what holds it back, and the two cases need
1280
+ different commands: ``findings-reported`` means a report exists and is
1281
+ unapplied, so ``forge-fix`` is accurate; anything else outstanding — notably
1282
+ ``auto-verify-pending``, where the scheduled run never happened — has nothing to
1283
+ fix and needs the verification itself, so ``forge-verify`` is. Recommending
1284
+ ``forge-fix`` for owed automatic debt sends the operator looking for a findings
1285
+ document that was never written (REQ-DEBT-02/05).
995
1286
  """
996
1287
  name = status_row["name"]
997
1288
  state = _read_state_safely(feature_dir / PIPELINE_STATE_FILENAME)
@@ -1000,12 +1291,17 @@ def _next_command(feature_dir: Path, status_row: FeatureStatus) -> str:
1000
1291
  if not prd_present or nxt == "forge-1-prd":
1001
1292
  return f"/feature-forge:forge-1-prd {name}"
1002
1293
  if nxt is None:
1003
- return f"/feature-forge:forge-fix {name}"
1294
+ stages = state.get("stages")
1295
+ impl = stages.get("forge-verify-impl") if isinstance(stages, dict) else None
1296
+ status = impl.get("status") if isinstance(impl, dict) else None
1297
+ if status == "findings-reported":
1298
+ return f"/feature-forge:forge-fix {name}"
1299
+ return f"/feature-forge:forge-verify {name}"
1004
1300
  return f"/feature-forge:{nxt} {name}"
1005
1301
 
1006
1302
 
1007
1303
  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).
1304
+ """Build the full live dashboard payload for an epic.
1009
1305
 
1010
1306
  Validates first (refusing to render over an invalid graph), then derives each
1011
1307
  member's live status from its own state file, computes blocked/unmetDeps,
@@ -1016,7 +1312,7 @@ def render_status(epic_dir: Path, specs_dir: Path) -> RenderStatus:
1016
1312
  specs_dir: The configured specs directory.
1017
1313
 
1018
1314
  Returns:
1019
- The RenderStatus dict (02 §8.4).
1315
+ The RenderStatus dict.
1020
1316
 
1021
1317
  Raises:
1022
1318
  UsageError: Missing/unreadable manifest (exit 2).
@@ -1043,6 +1339,15 @@ def render_status(epic_dir: Path, specs_dir: Path) -> RenderStatus:
1043
1339
  member_state = _read_state_safely(member_dir / PIPELINE_STATE_FILENAME)
1044
1340
  complete[name] = is_complete_for_orchestration(member_state)
1045
1341
  warnings.extend(_verify_status_warnings(name, member_state))
1342
+ warnings.extend(_auto_verify_debt_warnings(name, member_state))
1343
+
1344
+ # (3b) epic-root verification debt, classified against the manifest revision and
1345
+ # read from .epic-state.json alone — no member state participates.
1346
+ warnings.extend(_epic_verify_warnings(
1347
+ manifest.get("epic", epic_dir.name),
1348
+ epic_dir,
1349
+ _positive_int(manifest.get("revision")),
1350
+ ))
1046
1351
 
1047
1352
  # (4) per-feature unmetDeps + blocked. A feature that is itself complete is
1048
1353
  # never "blocked" — unmet deps only matter for work not yet finished.
@@ -1102,21 +1407,47 @@ def render_status(epic_dir: Path, specs_dir: Path) -> RenderStatus:
1102
1407
 
1103
1408
 
1104
1409
  # --------------------------------------------------------------------------- #
1105
- # Mutators (02 §7) — implemented in item 008
1410
+ # Mutators — implemented in item 008
1106
1411
  # --------------------------------------------------------------------------- #
1107
1412
 
1108
1413
 
1414
+ def _semantic_manifest(manifest: dict) -> dict:
1415
+ """Return a manifest copy carrying only its semantic fields.
1416
+
1417
+ Drops exactly the two bookkeeping fields a mutation is allowed to change on its
1418
+ own: ``updatedAt`` (a timestamp) and ``revision`` (the counter this function's
1419
+ caller maintains, and which ``load_manifest`` synthesizes for legacy files). What
1420
+ remains is the content an epic's verification freshness is actually about.
1421
+ """
1422
+ return {k: v for k, v in manifest.items() if k not in ("updatedAt", "revision")}
1423
+
1424
+
1109
1425
  def _bump_and_write(
1110
1426
  epic_dir: Path, specs_dir: Path, manifest: dict
1111
1427
  ) -> 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``.
1428
+ """Re-validate, bump revision + updatedAt, and atomically persist.
1429
+
1430
+ The shared tail of every mutator (REQ-ROBUST-03, REQ-OBS-01, REQ-EPIC-05,
1431
+ REQ-REL-01) and the SINGLE place ``revision`` is incremented — no mutator bumps
1432
+ it itself, so every successful mutation advances it exactly once.
1433
+
1434
+ Order of operations:
1435
+
1436
+ 1. Re-run ``_validate_dict`` on the EDITED manifest; if any blocking finding is
1437
+ present (cycle, dangling-ref, duplicate-name, schema, ...), the on-disk file
1438
+ is left byte-identical and the findings are returned so the caller exits 1.
1439
+ This runs FIRST, ahead of the no-op comparison, so that EVERY mutator
1440
+ re-validates (REQ-ROBUST-03): a semantically idempotent edit against a
1441
+ manifest that is already invalid on disk must report the same blocking
1442
+ findings a non-idempotent edit would, instead of exiting 0 in silence and
1443
+ letting a caller read that as "the epic is well-formed".
1444
+ 2. Compare the proposed manifest with the on-disk one, ignoring only
1445
+ ``updatedAt`` and the (possibly synthesized) ``revision``. If every semantic
1446
+ field matches this is a no-op: return ``[]`` WITHOUT writing, so an edit that
1447
+ changes nothing leaves the file byte-identical — including ``updatedAt``.
1448
+ 3. Set ``revision`` to ``current + 1`` and ``updatedAt`` to now (UTC, ISO-8601),
1449
+ then write once via ``atomic_write``. A failed write raises, so a torn
1450
+ mutation leaves the previous revision and bytes intact.
1120
1451
 
1121
1452
  Args:
1122
1453
  epic_dir: The epic subtree directory.
@@ -1124,17 +1455,33 @@ def _bump_and_write(
1124
1455
  manifest: The already-edited in-memory manifest dict.
1125
1456
 
1126
1457
  Returns:
1127
- An empty list on success (write performed); the blocking findings on
1128
- refusal (no write performed).
1458
+ An empty list on success (write performed) OR on a semantic no-op (no write);
1459
+ the blocking findings on refusal (no write performed).
1129
1460
 
1130
1461
  Raises:
1131
1462
  UsageError: If the atomic write itself fails (exit 2).
1132
1463
  """
1464
+ path = epic_dir / MANIFEST_FILENAME
1465
+ try:
1466
+ on_disk: dict | None = load_manifest(epic_dir)
1467
+ except (UsageError, FindingsError):
1468
+ # No readable predecessor (first write, or a corrupt file the caller is
1469
+ # replacing wholesale) — treat every field as changed.
1470
+ on_disk = None
1471
+
1133
1472
  findings = _validate_dict(manifest, epic_dir, specs_dir)
1134
1473
  if findings:
1135
1474
  return findings
1475
+
1476
+ if on_disk is not None and _semantic_manifest(on_disk) == _semantic_manifest(manifest):
1477
+ return []
1478
+
1479
+ current = on_disk.get("revision") if isinstance(on_disk, dict) else None
1480
+ if isinstance(current, bool) or not isinstance(current, int) or current < 1:
1481
+ current = 1 # legacy / malformed predecessor: logical revision 1.
1482
+ manifest["revision"] = current + 1
1136
1483
  manifest["updatedAt"] = datetime.now(timezone.utc).isoformat()
1137
- atomic_write(epic_dir / MANIFEST_FILENAME, manifest)
1484
+ atomic_write(path, manifest)
1138
1485
  return []
1139
1486
 
1140
1487
 
@@ -1145,7 +1492,7 @@ def add_feature(
1145
1492
  charter: str,
1146
1493
  deps: list[str],
1147
1494
  ) -> list[Finding]:
1148
- """Append a new member feature to the manifest (02 §7.1).
1495
+ """Append a new member feature to the manifest.
1149
1496
 
1150
1497
  Appends a ``Feature`` with the given name/charter/dependsOn and EMPTY
1151
1498
  exposes/consumes. Re-validation surfaces a duplicate name (within the
@@ -1181,7 +1528,7 @@ def add_feature(
1181
1528
 
1182
1529
 
1183
1530
  def remove_feature(epic_dir: Path, specs_dir: Path, name: str) -> list[Finding]:
1184
- """Remove a member feature from the manifest (02 §7.2).
1531
+ """Remove a member feature from the manifest.
1185
1532
 
1186
1533
  Drops the named feature from ``features[]``. After removal, re-validation
1187
1534
  surfaces any now-dangling ``dependsOn`` / ``consumes.from`` that pointed at
@@ -1213,10 +1560,10 @@ def remove_feature(epic_dir: Path, specs_dir: Path, name: str) -> list[Finding]:
1213
1560
 
1214
1561
 
1215
1562
  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).
1563
+ """Reorder the manifest features[] to a given permutation.
1217
1564
 
1218
1565
  ``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
1566
+ display sequence, not a dependency ordering). If it is not, a
1220
1567
  ``schema`` finding is returned and the manifest is left unchanged.
1221
1568
 
1222
1569
  Args:
@@ -1246,7 +1593,7 @@ def reorder(epic_dir: Path, specs_dir: Path, order: list[str]) -> list[Finding]:
1246
1593
  def set_dep(
1247
1594
  epic_dir: Path, specs_dir: Path, name: str, deps: list[str]
1248
1595
  ) -> list[Finding]:
1249
- """Replace a member feature's dependsOn list (02 §7.4, §7.6).
1596
+ """Replace a member feature's dependsOn list.
1250
1597
 
1251
1598
  Re-validation enforces every new dependency exists (``dangling-ref``) and the
1252
1599
  resulting graph is acyclic (``cycle``). An empty ``deps`` clears the
@@ -1281,8 +1628,54 @@ def set_dep(
1281
1628
  return _bump_and_write(epic_dir, specs_dir, manifest)
1282
1629
 
1283
1630
 
1631
+ def set_charter(
1632
+ epic_dir: Path, specs_dir: Path, name: str, charter: str
1633
+ ) -> list[Finding]:
1634
+ """Replace a member feature's charter text (#166).
1635
+
1636
+ The charter is the member field most likely to need revision after creation
1637
+ (an ADR lands, a boundary moves, a member turns out to own more than
1638
+ expected) — and it previously had no mutator, so edit mode had no sanctioned
1639
+ path to rewrite one. Same shape as ``set_dep``: an atomic, re-validated,
1640
+ revision-bumping replacement. EPIC.md prose is NOT touched — the agent
1641
+ re-syncs the mirror afterward (edit-mode Step E5), exactly as for every
1642
+ other mutation.
1643
+
1644
+ Args:
1645
+ epic_dir: The epic subtree directory.
1646
+ specs_dir: The configured specs directory.
1647
+ name: The member feature to edit.
1648
+ charter: The replacement charter text (argparse requires the flag; an
1649
+ empty string is refused below — a charter is required prose, and
1650
+ blanking it would only manufacture a schema-shaped hole).
1651
+
1652
+ Returns:
1653
+ Empty list on success; blocking findings (not-found / empty-charter, or
1654
+ anything re-validation raises) on refusal — manifest left unchanged.
1655
+
1656
+ Raises:
1657
+ UsageError: Unsafe name, corrupt/missing manifest, or write failure.
1658
+ """
1659
+ if not charter.strip():
1660
+ return [{"code": "empty-charter",
1661
+ "message": "refusing to blank a charter: pass non-empty --charter text",
1662
+ "feature": name}]
1663
+ manifest = load_manifest(epic_dir)
1664
+ by_name = {
1665
+ f["name"]: f
1666
+ for f in manifest.get("features", [])
1667
+ if isinstance(f, dict) and isinstance(f.get("name"), str)
1668
+ }
1669
+ if name not in by_name:
1670
+ return [{"code": "not-found",
1671
+ "message": f"feature {name!r} is not a member of epic {epic_dir.name!r}",
1672
+ "feature": name}]
1673
+ by_name[name]["charter"] = charter
1674
+ return _bump_and_write(epic_dir, specs_dir, manifest)
1675
+
1676
+
1284
1677
  def set_status(epic_dir: Path, specs_dir: Path, status: str) -> list[Finding]:
1285
- """Set the epic-level lifecycle status (02 §7.5).
1678
+ """Set the epic-level lifecycle status.
1286
1679
 
1287
1680
  Sets the epic-level ``status`` (the value is constrained to the allowed
1288
1681
  lifecycle states by ``argparse`` ``choices`` before reaching here). Never
@@ -1480,7 +1873,7 @@ def adopt_feature(
1480
1873
 
1481
1874
 
1482
1875
  # --------------------------------------------------------------------------- #
1483
- # CLI Dispatch (02 §9)
1876
+ # CLI Dispatch
1484
1877
  # --------------------------------------------------------------------------- #
1485
1878
 
1486
1879
 
@@ -1509,7 +1902,7 @@ def _emit_findings(findings: list[Finding], as_json: bool) -> None:
1509
1902
 
1510
1903
 
1511
1904
  def _print_status_table(status: RenderStatus) -> None:
1512
- """Print a readable epic dashboard plus the recommended next command (02 §8)."""
1905
+ """Print a readable epic dashboard plus the recommended next command."""
1513
1906
  rollup = status["rollup"]
1514
1907
  print(f"Epic: {status['epic']} [{status['status']}]")
1515
1908
  print(f"Progress: {rollup['complete']}/{rollup['total']} complete")
@@ -1593,7 +1986,8 @@ def _dispatch(args: argparse.Namespace, specs_dir: Path) -> int:
1593
1986
  return 0
1594
1987
 
1595
1988
  # Mutators ---------------------------------------------------------------
1596
- if cmd in {"add-feature", "remove-feature", "reorder", "set-dep", "set-status"}:
1989
+ if cmd in {"add-feature", "remove-feature", "reorder", "set-dep", "set-charter",
1990
+ "set-status"}:
1597
1991
  epic_dir = contained_path(specs_dir, args.epic)
1598
1992
  if cmd == "add-feature":
1599
1993
  findings = add_feature(
@@ -1608,6 +2002,8 @@ def _dispatch(args: argparse.Namespace, specs_dir: Path) -> int:
1608
2002
  findings = set_dep(
1609
2003
  epic_dir, specs_dir, args.name, _split_list(args.depends_on)
1610
2004
  )
2005
+ elif cmd == "set-charter":
2006
+ findings = set_charter(epic_dir, specs_dir, args.name, args.charter)
1611
2007
  else: # set-status
1612
2008
  findings = set_status(epic_dir, specs_dir, args.status)
1613
2009
  if findings:
@@ -1618,7 +2014,7 @@ def _dispatch(args: argparse.Namespace, specs_dir: Path) -> int:
1618
2014
 
1619
2015
 
1620
2016
  def _build_parser() -> argparse.ArgumentParser:
1621
- """Build the argparse parser with one subparser per subcommand (02 §9)."""
2017
+ """Build the argparse parser with one subparser per subcommand."""
1622
2018
  parser = argparse.ArgumentParser(prog="epic-manifest.py", description=__doc__)
1623
2019
  sub = parser.add_subparsers(dest="cmd", required=True)
1624
2020
 
@@ -1702,6 +2098,14 @@ def _build_parser() -> argparse.ArgumentParser:
1702
2098
  add_specs_dir(p_setdep)
1703
2099
  add_json(p_setdep)
1704
2100
 
2101
+ # set-charter ----------------------------------------------------------- #
2102
+ p_setcharter = sub.add_parser("set-charter", help="Replace a feature's charter")
2103
+ p_setcharter.add_argument("epic")
2104
+ p_setcharter.add_argument("name")
2105
+ p_setcharter.add_argument("--charter", required=True, help="Replacement charter text")
2106
+ add_specs_dir(p_setcharter)
2107
+ add_json(p_setcharter)
2108
+
1705
2109
  # set-status ------------------------------------------------------------ #
1706
2110
  p_setstatus = sub.add_parser("set-status", help="Set the epic lifecycle status")
1707
2111
  p_setstatus.add_argument("epic")