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