@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.
- package/adapters/claude/.feature-forge-bundle.json +1 -1
- package/adapters/claude/agents/forge-verifier.md +3 -1
- package/adapters/claude/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/claude/references/epic-manifest-schema.json +6 -1
- package/adapters/claude/references/epic-state-schema.json +50 -0
- package/adapters/claude/references/forge-config-schema.json +19 -1
- package/adapters/claude/references/forge-decisions-schema.json +33 -0
- package/adapters/claude/references/pipeline-state-schema.json +38 -4
- package/adapters/claude/references/ralph-loop-contract.md +6 -3
- package/adapters/claude/references/shared-conventions.md +77 -3
- package/adapters/claude/references/stage-exit-protocol.md +391 -143
- package/adapters/claude/scripts/epic-manifest.py +495 -91
- package/adapters/claude/scripts/fix-sweep.py +1180 -0
- package/adapters/claude/scripts/forge-bootstrap.py +57 -5
- package/adapters/claude/scripts/forge-session.py +4241 -146
- package/adapters/claude/scripts/validate-traceability.py +86 -5
- package/adapters/claude/skills/forge/SKILL.md +10 -9
- package/adapters/claude/skills/forge/references/pipeline-state-schema.json +38 -4
- package/adapters/claude/skills/forge/references/shared-conventions.md +77 -3
- package/adapters/claude/skills/forge/references/stage-exit-protocol.md +391 -143
- package/adapters/claude/skills/forge-0-epic/SKILL.md +7 -2
- package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +29 -25
- package/adapters/claude/skills/forge-0-epic/references/epic-manifest-subcommands.md +5 -0
- package/adapters/claude/skills/forge-0-epic/references/pipeline-state-schema.json +38 -4
- package/adapters/claude/skills/forge-0-epic/references/shared-conventions.md +77 -3
- package/adapters/claude/skills/forge-0-epic/references/stage-exit-protocol.md +391 -143
- package/adapters/claude/skills/forge-1-prd/SKILL.md +17 -4
- package/adapters/claude/skills/forge-1-prd/references/shared-conventions.md +77 -3
- package/adapters/claude/skills/forge-1-prd/references/stage-exit-protocol.md +391 -143
- package/adapters/claude/skills/forge-2-tech/SKILL.md +18 -4
- package/adapters/claude/skills/forge-2-tech/references/shared-conventions.md +77 -3
- package/adapters/claude/skills/forge-2-tech/references/stage-exit-protocol.md +391 -143
- package/adapters/claude/skills/forge-3-specs/SKILL.md +9 -3
- package/adapters/claude/skills/forge-3-specs/references/shared-conventions.md +77 -3
- 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 +391 -143
- package/adapters/claude/skills/forge-4-backlog/SKILL.md +56 -5
- package/adapters/claude/skills/forge-4-backlog/references/shared-conventions.md +77 -3
- package/adapters/claude/skills/forge-4-backlog/references/stage-exit-protocol.md +391 -143
- package/adapters/claude/skills/forge-5-loop/SKILL.md +67 -69
- package/adapters/claude/skills/forge-5-loop/references/agent-selection.md +16 -0
- package/adapters/claude/skills/forge-5-loop/references/ralph-loop-contract.md +6 -3
- package/adapters/claude/skills/forge-5-loop/references/recovery-procedure.md +349 -0
- package/adapters/claude/skills/forge-5-loop/references/result-reporting.md +101 -33
- package/adapters/claude/skills/forge-5-loop/references/runner-contract.md +26 -6
- package/adapters/claude/skills/forge-5-loop/references/shared-conventions.md +77 -3
- package/adapters/claude/skills/forge-5-loop/references/stage-exit-protocol.md +391 -143
- package/adapters/claude/skills/forge-6-docs/SKILL.md +75 -8
- package/adapters/claude/skills/forge-6-docs/references/shared-conventions.md +77 -3
- package/adapters/claude/skills/forge-6-docs/references/stage-exit-protocol.md +516 -0
- package/adapters/claude/skills/forge-fix/SKILL.md +119 -33
- package/adapters/claude/skills/forge-fix/references/shared-conventions.md +77 -3
- package/adapters/claude/skills/forge-fix/references/stage-exit-protocol.md +391 -143
- package/adapters/claude/skills/forge-guide/references/forge-config-schema.json +19 -1
- package/adapters/claude/skills/forge-guide/references/ralph-loop-contract.md +6 -3
- package/adapters/claude/skills/forge-guide/references/shared-conventions.md +77 -3
- package/adapters/claude/skills/forge-verify/SKILL.md +82 -51
- package/adapters/claude/skills/forge-verify/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/claude/skills/forge-verify/references/findings-template.md +69 -49
- package/adapters/claude/skills/forge-verify/references/shared-conventions.md +77 -3
- package/adapters/claude/skills/forge-verify/references/stage-exit-protocol.md +516 -0
- package/adapters/claude/skills/forge-verify/references/verification-checklists/backlog.md +73 -0
- package/adapters/claude/skills/forge-verify/references/verification-checklists/epic.md +4 -3
- package/adapters/claude/skills/forge-verify/references/verification-checklists/impl.md +85 -0
- package/adapters/claude/skills/forge-verify/references/verification-checklists/specs.md +45 -1
- package/adapters/codex/.feature-forge-bundle.json +1 -1
- package/adapters/codex/agents/forge-verifier.toml +3 -1
- package/adapters/codex/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/codex/references/epic-manifest-schema.json +6 -1
- package/adapters/codex/references/epic-state-schema.json +50 -0
- package/adapters/codex/references/forge-config-schema.json +19 -1
- package/adapters/codex/references/forge-decisions-schema.json +33 -0
- package/adapters/codex/references/pipeline-state-schema.json +38 -4
- package/adapters/codex/references/process-overview.md +2 -2
- package/adapters/codex/references/ralph-loop-contract.md +6 -3
- package/adapters/codex/references/shared-conventions.md +102 -28
- package/adapters/codex/references/stage-exit-protocol.md +395 -147
- package/adapters/codex/scripts/epic-manifest.py +495 -91
- package/adapters/codex/scripts/fix-sweep.py +1180 -0
- package/adapters/codex/scripts/forge-bootstrap.py +57 -5
- package/adapters/codex/scripts/forge-session.py +4241 -146
- package/adapters/codex/scripts/validate-traceability.py +86 -5
- package/adapters/codex/skills/forge/SKILL.md +13 -12
- package/adapters/codex/skills/forge/references/pipeline-state-schema.json +38 -4
- package/adapters/codex/skills/forge/references/process-overview.md +2 -2
- package/adapters/codex/skills/forge/references/shared-conventions.md +102 -28
- package/adapters/codex/skills/forge/references/stage-exit-protocol.md +395 -147
- package/adapters/codex/skills/forge-0-epic/SKILL.md +7 -2
- package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +37 -33
- package/adapters/codex/skills/forge-0-epic/references/epic-manifest-subcommands.md +8 -3
- package/adapters/codex/skills/forge-0-epic/references/pipeline-state-schema.json +38 -4
- package/adapters/codex/skills/forge-0-epic/references/shared-conventions.md +102 -28
- package/adapters/codex/skills/forge-0-epic/references/stage-exit-protocol.md +395 -147
- package/adapters/codex/skills/forge-1-prd/SKILL.md +17 -4
- package/adapters/codex/skills/forge-1-prd/references/shared-conventions.md +102 -28
- package/adapters/codex/skills/forge-1-prd/references/stage-exit-protocol.md +395 -147
- package/adapters/codex/skills/forge-2-tech/SKILL.md +18 -4
- package/adapters/codex/skills/forge-2-tech/references/shared-conventions.md +102 -28
- package/adapters/codex/skills/forge-2-tech/references/stage-exit-protocol.md +395 -147
- package/adapters/codex/skills/forge-3-specs/SKILL.md +9 -3
- package/adapters/codex/skills/forge-3-specs/references/shared-conventions.md +102 -28
- 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 +395 -147
- package/adapters/codex/skills/forge-4-backlog/SKILL.md +56 -5
- package/adapters/codex/skills/forge-4-backlog/references/shared-conventions.md +102 -28
- package/adapters/codex/skills/forge-4-backlog/references/stage-exit-protocol.md +395 -147
- package/adapters/codex/skills/forge-5-loop/SKILL.md +67 -69
- package/adapters/codex/skills/forge-5-loop/references/agent-selection.md +17 -1
- package/adapters/codex/skills/forge-5-loop/references/ralph-loop-contract.md +6 -3
- package/adapters/codex/skills/forge-5-loop/references/recovery-procedure.md +349 -0
- package/adapters/codex/skills/forge-5-loop/references/result-reporting.md +101 -33
- package/adapters/codex/skills/forge-5-loop/references/runner-contract.md +30 -10
- package/adapters/codex/skills/forge-5-loop/references/shared-conventions.md +102 -28
- package/adapters/codex/skills/forge-5-loop/references/stage-exit-protocol.md +395 -147
- package/adapters/codex/skills/forge-6-docs/SKILL.md +75 -8
- package/adapters/codex/skills/forge-6-docs/references/shared-conventions.md +102 -28
- package/adapters/codex/skills/forge-6-docs/references/stage-exit-protocol.md +516 -0
- package/adapters/codex/skills/forge-fix/SKILL.md +118 -32
- package/adapters/codex/skills/forge-fix/references/shared-conventions.md +102 -28
- package/adapters/codex/skills/forge-fix/references/stage-exit-protocol.md +395 -147
- package/adapters/codex/skills/forge-guide/SKILL.md +1 -1
- package/adapters/codex/skills/forge-guide/references/forge-config-schema.json +19 -1
- package/adapters/codex/skills/forge-guide/references/process-overview.md +2 -2
- package/adapters/codex/skills/forge-guide/references/ralph-loop-contract.md +6 -3
- package/adapters/codex/skills/forge-guide/references/shared-conventions.md +102 -28
- package/adapters/codex/skills/forge-init/SKILL.md +1 -1
- package/adapters/codex/skills/forge-verify/SKILL.md +82 -51
- package/adapters/codex/skills/forge-verify/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/codex/skills/forge-verify/references/findings-template.md +70 -50
- package/adapters/codex/skills/forge-verify/references/shared-conventions.md +102 -28
- package/adapters/codex/skills/forge-verify/references/stage-exit-protocol.md +516 -0
- package/adapters/codex/skills/forge-verify/references/verification-checklists/backlog.md +73 -0
- package/adapters/codex/skills/forge-verify/references/verification-checklists/epic.md +5 -4
- package/adapters/codex/skills/forge-verify/references/verification-checklists/impl.md +85 -0
- package/adapters/codex/skills/forge-verify/references/verification-checklists/specs.md +45 -1
- package/adapters/copilot/.feature-forge-bundle.json +1 -1
- package/adapters/copilot/agents/forge-verifier.md +3 -1
- package/adapters/copilot/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/copilot/references/epic-manifest-schema.json +6 -1
- package/adapters/copilot/references/epic-state-schema.json +50 -0
- package/adapters/copilot/references/forge-config-schema.json +19 -1
- package/adapters/copilot/references/forge-decisions-schema.json +33 -0
- package/adapters/copilot/references/pipeline-state-schema.json +38 -4
- package/adapters/copilot/references/process-overview.md +2 -2
- package/adapters/copilot/references/ralph-loop-contract.md +6 -3
- package/adapters/copilot/references/shared-conventions.md +102 -28
- package/adapters/copilot/references/stage-exit-protocol.md +395 -147
- package/adapters/copilot/scripts/epic-manifest.py +495 -91
- package/adapters/copilot/scripts/fix-sweep.py +1180 -0
- package/adapters/copilot/scripts/forge-bootstrap.py +57 -5
- package/adapters/copilot/scripts/forge-session.py +4241 -146
- package/adapters/copilot/scripts/validate-traceability.py +86 -5
- package/adapters/copilot/skills/forge/forge.md +13 -12
- package/adapters/copilot/skills/forge/references/pipeline-state-schema.json +38 -4
- package/adapters/copilot/skills/forge/references/process-overview.md +2 -2
- package/adapters/copilot/skills/forge/references/shared-conventions.md +102 -28
- package/adapters/copilot/skills/forge/references/stage-exit-protocol.md +395 -147
- package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +7 -2
- package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +37 -33
- package/adapters/copilot/skills/forge-0-epic/references/epic-manifest-subcommands.md +8 -3
- package/adapters/copilot/skills/forge-0-epic/references/pipeline-state-schema.json +38 -4
- package/adapters/copilot/skills/forge-0-epic/references/shared-conventions.md +102 -28
- package/adapters/copilot/skills/forge-0-epic/references/stage-exit-protocol.md +395 -147
- package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +17 -4
- package/adapters/copilot/skills/forge-1-prd/references/shared-conventions.md +102 -28
- package/adapters/copilot/skills/forge-1-prd/references/stage-exit-protocol.md +395 -147
- package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +18 -4
- package/adapters/copilot/skills/forge-2-tech/references/shared-conventions.md +102 -28
- package/adapters/copilot/skills/forge-2-tech/references/stage-exit-protocol.md +395 -147
- package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +9 -3
- package/adapters/copilot/skills/forge-3-specs/references/shared-conventions.md +102 -28
- 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 +395 -147
- package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +56 -5
- package/adapters/copilot/skills/forge-4-backlog/references/shared-conventions.md +102 -28
- package/adapters/copilot/skills/forge-4-backlog/references/stage-exit-protocol.md +395 -147
- package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +67 -69
- package/adapters/copilot/skills/forge-5-loop/references/agent-selection.md +17 -1
- package/adapters/copilot/skills/forge-5-loop/references/ralph-loop-contract.md +6 -3
- package/adapters/copilot/skills/forge-5-loop/references/recovery-procedure.md +349 -0
- package/adapters/copilot/skills/forge-5-loop/references/result-reporting.md +101 -33
- package/adapters/copilot/skills/forge-5-loop/references/runner-contract.md +30 -10
- package/adapters/copilot/skills/forge-5-loop/references/shared-conventions.md +102 -28
- package/adapters/copilot/skills/forge-5-loop/references/stage-exit-protocol.md +395 -147
- package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +75 -8
- package/adapters/copilot/skills/forge-6-docs/references/shared-conventions.md +102 -28
- package/adapters/copilot/skills/forge-6-docs/references/stage-exit-protocol.md +516 -0
- package/adapters/copilot/skills/forge-fix/forge-fix.md +118 -32
- package/adapters/copilot/skills/forge-fix/references/shared-conventions.md +102 -28
- package/adapters/copilot/skills/forge-fix/references/stage-exit-protocol.md +395 -147
- package/adapters/copilot/skills/forge-guide/forge-guide.md +1 -1
- package/adapters/copilot/skills/forge-guide/references/forge-config-schema.json +19 -1
- package/adapters/copilot/skills/forge-guide/references/process-overview.md +2 -2
- package/adapters/copilot/skills/forge-guide/references/ralph-loop-contract.md +6 -3
- package/adapters/copilot/skills/forge-guide/references/shared-conventions.md +102 -28
- package/adapters/copilot/skills/forge-init/forge-init.md +1 -1
- package/adapters/copilot/skills/forge-verify/forge-verify.md +82 -51
- package/adapters/copilot/skills/forge-verify/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/copilot/skills/forge-verify/references/findings-template.md +70 -50
- package/adapters/copilot/skills/forge-verify/references/shared-conventions.md +102 -28
- package/adapters/copilot/skills/forge-verify/references/stage-exit-protocol.md +516 -0
- package/adapters/copilot/skills/forge-verify/references/verification-checklists/backlog.md +73 -0
- package/adapters/copilot/skills/forge-verify/references/verification-checklists/epic.md +5 -4
- package/adapters/copilot/skills/forge-verify/references/verification-checklists/impl.md +85 -0
- package/adapters/copilot/skills/forge-verify/references/verification-checklists/specs.md +45 -1
- package/adapters/cursor/.feature-forge-bundle.json +1 -1
- package/adapters/cursor/agents/forge-verifier.mdc +3 -1
- package/adapters/cursor/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/cursor/references/epic-manifest-schema.json +6 -1
- package/adapters/cursor/references/epic-state-schema.json +50 -0
- package/adapters/cursor/references/forge-config-schema.json +19 -1
- package/adapters/cursor/references/forge-decisions-schema.json +33 -0
- package/adapters/cursor/references/pipeline-state-schema.json +38 -4
- package/adapters/cursor/references/process-overview.md +2 -2
- package/adapters/cursor/references/ralph-loop-contract.md +6 -3
- package/adapters/cursor/references/shared-conventions.md +102 -28
- package/adapters/cursor/references/stage-exit-protocol.md +395 -147
- package/adapters/cursor/scripts/epic-manifest.py +495 -91
- package/adapters/cursor/scripts/fix-sweep.py +1180 -0
- package/adapters/cursor/scripts/forge-bootstrap.py +57 -5
- package/adapters/cursor/scripts/forge-session.py +4241 -146
- package/adapters/cursor/scripts/validate-traceability.py +86 -5
- package/adapters/cursor/skills/forge/forge.mdc +13 -12
- package/adapters/cursor/skills/forge/references/pipeline-state-schema.json +38 -4
- package/adapters/cursor/skills/forge/references/process-overview.md +2 -2
- package/adapters/cursor/skills/forge/references/shared-conventions.md +102 -28
- package/adapters/cursor/skills/forge/references/stage-exit-protocol.md +395 -147
- package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +7 -2
- package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +37 -33
- package/adapters/cursor/skills/forge-0-epic/references/epic-manifest-subcommands.md +8 -3
- package/adapters/cursor/skills/forge-0-epic/references/pipeline-state-schema.json +38 -4
- package/adapters/cursor/skills/forge-0-epic/references/shared-conventions.md +102 -28
- package/adapters/cursor/skills/forge-0-epic/references/stage-exit-protocol.md +395 -147
- package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +17 -4
- package/adapters/cursor/skills/forge-1-prd/references/shared-conventions.md +102 -28
- package/adapters/cursor/skills/forge-1-prd/references/stage-exit-protocol.md +395 -147
- package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +18 -4
- package/adapters/cursor/skills/forge-2-tech/references/shared-conventions.md +102 -28
- package/adapters/cursor/skills/forge-2-tech/references/stage-exit-protocol.md +395 -147
- package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +9 -3
- package/adapters/cursor/skills/forge-3-specs/references/shared-conventions.md +102 -28
- 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 +395 -147
- package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +56 -5
- package/adapters/cursor/skills/forge-4-backlog/references/shared-conventions.md +102 -28
- package/adapters/cursor/skills/forge-4-backlog/references/stage-exit-protocol.md +395 -147
- package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +67 -69
- package/adapters/cursor/skills/forge-5-loop/references/agent-selection.md +17 -1
- package/adapters/cursor/skills/forge-5-loop/references/ralph-loop-contract.md +6 -3
- package/adapters/cursor/skills/forge-5-loop/references/recovery-procedure.md +349 -0
- package/adapters/cursor/skills/forge-5-loop/references/result-reporting.md +101 -33
- package/adapters/cursor/skills/forge-5-loop/references/runner-contract.md +30 -10
- package/adapters/cursor/skills/forge-5-loop/references/shared-conventions.md +102 -28
- package/adapters/cursor/skills/forge-5-loop/references/stage-exit-protocol.md +395 -147
- package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +75 -8
- package/adapters/cursor/skills/forge-6-docs/references/shared-conventions.md +102 -28
- package/adapters/cursor/skills/forge-6-docs/references/stage-exit-protocol.md +516 -0
- package/adapters/cursor/skills/forge-fix/forge-fix.mdc +118 -32
- package/adapters/cursor/skills/forge-fix/references/shared-conventions.md +102 -28
- package/adapters/cursor/skills/forge-fix/references/stage-exit-protocol.md +395 -147
- package/adapters/cursor/skills/forge-guide/forge-guide.mdc +1 -1
- package/adapters/cursor/skills/forge-guide/references/forge-config-schema.json +19 -1
- package/adapters/cursor/skills/forge-guide/references/process-overview.md +2 -2
- package/adapters/cursor/skills/forge-guide/references/ralph-loop-contract.md +6 -3
- package/adapters/cursor/skills/forge-guide/references/shared-conventions.md +102 -28
- package/adapters/cursor/skills/forge-init/forge-init.mdc +1 -1
- package/adapters/cursor/skills/forge-verify/forge-verify.mdc +82 -51
- package/adapters/cursor/skills/forge-verify/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/cursor/skills/forge-verify/references/findings-template.md +70 -50
- package/adapters/cursor/skills/forge-verify/references/shared-conventions.md +102 -28
- package/adapters/cursor/skills/forge-verify/references/stage-exit-protocol.md +516 -0
- package/adapters/cursor/skills/forge-verify/references/verification-checklists/backlog.md +73 -0
- package/adapters/cursor/skills/forge-verify/references/verification-checklists/epic.md +5 -4
- package/adapters/cursor/skills/forge-verify/references/verification-checklists/impl.md +85 -0
- package/adapters/cursor/skills/forge-verify/references/verification-checklists/specs.md +45 -1
- package/adapters/gemini/.feature-forge-bundle.json +1 -1
- package/adapters/gemini/agents/forge-verifier.md +3 -1
- package/adapters/gemini/gemini-extension.json +1 -1
- package/adapters/gemini/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/gemini/references/epic-manifest-schema.json +6 -1
- package/adapters/gemini/references/epic-state-schema.json +50 -0
- package/adapters/gemini/references/forge-config-schema.json +19 -1
- package/adapters/gemini/references/forge-decisions-schema.json +33 -0
- package/adapters/gemini/references/pipeline-state-schema.json +38 -4
- package/adapters/gemini/references/process-overview.md +2 -2
- package/adapters/gemini/references/ralph-loop-contract.md +6 -3
- package/adapters/gemini/references/shared-conventions.md +102 -28
- package/adapters/gemini/references/stage-exit-protocol.md +395 -147
- package/adapters/gemini/scripts/epic-manifest.py +495 -91
- package/adapters/gemini/scripts/fix-sweep.py +1180 -0
- package/adapters/gemini/scripts/forge-bootstrap.py +57 -5
- package/adapters/gemini/scripts/forge-session.py +4241 -146
- package/adapters/gemini/scripts/validate-traceability.py +86 -5
- package/adapters/gemini/skills/forge/forge.md +13 -12
- package/adapters/gemini/skills/forge/references/pipeline-state-schema.json +38 -4
- package/adapters/gemini/skills/forge/references/process-overview.md +2 -2
- package/adapters/gemini/skills/forge/references/shared-conventions.md +102 -28
- package/adapters/gemini/skills/forge/references/stage-exit-protocol.md +395 -147
- package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +7 -2
- package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +37 -33
- package/adapters/gemini/skills/forge-0-epic/references/epic-manifest-subcommands.md +8 -3
- package/adapters/gemini/skills/forge-0-epic/references/pipeline-state-schema.json +38 -4
- package/adapters/gemini/skills/forge-0-epic/references/shared-conventions.md +102 -28
- package/adapters/gemini/skills/forge-0-epic/references/stage-exit-protocol.md +395 -147
- package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +17 -4
- package/adapters/gemini/skills/forge-1-prd/references/shared-conventions.md +102 -28
- package/adapters/gemini/skills/forge-1-prd/references/stage-exit-protocol.md +395 -147
- package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +18 -4
- package/adapters/gemini/skills/forge-2-tech/references/shared-conventions.md +102 -28
- package/adapters/gemini/skills/forge-2-tech/references/stage-exit-protocol.md +395 -147
- package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +9 -3
- package/adapters/gemini/skills/forge-3-specs/references/shared-conventions.md +102 -28
- 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 +395 -147
- package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +56 -5
- package/adapters/gemini/skills/forge-4-backlog/references/shared-conventions.md +102 -28
- package/adapters/gemini/skills/forge-4-backlog/references/stage-exit-protocol.md +395 -147
- package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +67 -69
- package/adapters/gemini/skills/forge-5-loop/references/agent-selection.md +17 -1
- package/adapters/gemini/skills/forge-5-loop/references/ralph-loop-contract.md +6 -3
- package/adapters/gemini/skills/forge-5-loop/references/recovery-procedure.md +349 -0
- package/adapters/gemini/skills/forge-5-loop/references/result-reporting.md +101 -33
- package/adapters/gemini/skills/forge-5-loop/references/runner-contract.md +30 -10
- package/adapters/gemini/skills/forge-5-loop/references/shared-conventions.md +102 -28
- package/adapters/gemini/skills/forge-5-loop/references/stage-exit-protocol.md +395 -147
- package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +75 -8
- package/adapters/gemini/skills/forge-6-docs/references/shared-conventions.md +102 -28
- package/adapters/gemini/skills/forge-6-docs/references/stage-exit-protocol.md +516 -0
- package/adapters/gemini/skills/forge-fix/forge-fix.md +118 -32
- package/adapters/gemini/skills/forge-fix/references/shared-conventions.md +102 -28
- package/adapters/gemini/skills/forge-fix/references/stage-exit-protocol.md +395 -147
- package/adapters/gemini/skills/forge-guide/forge-guide.md +1 -1
- package/adapters/gemini/skills/forge-guide/references/forge-config-schema.json +19 -1
- package/adapters/gemini/skills/forge-guide/references/process-overview.md +2 -2
- package/adapters/gemini/skills/forge-guide/references/ralph-loop-contract.md +6 -3
- package/adapters/gemini/skills/forge-guide/references/shared-conventions.md +102 -28
- package/adapters/gemini/skills/forge-init/forge-init.md +1 -1
- package/adapters/gemini/skills/forge-verify/forge-verify.md +82 -51
- package/adapters/gemini/skills/forge-verify/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/gemini/skills/forge-verify/references/findings-template.md +70 -50
- package/adapters/gemini/skills/forge-verify/references/shared-conventions.md +102 -28
- package/adapters/gemini/skills/forge-verify/references/stage-exit-protocol.md +516 -0
- package/adapters/gemini/skills/forge-verify/references/verification-checklists/backlog.md +73 -0
- package/adapters/gemini/skills/forge-verify/references/verification-checklists/epic.md +5 -4
- package/adapters/gemini/skills/forge-verify/references/verification-checklists/impl.md +85 -0
- package/adapters/gemini/skills/forge-verify/references/verification-checklists/specs.md +45 -1
- package/adapters/pi/.feature-forge-bundle.json +1 -1
- package/adapters/pi/agents/forge-verifier.md +3 -1
- package/adapters/pi/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/pi/references/epic-manifest-schema.json +6 -1
- package/adapters/pi/references/epic-state-schema.json +50 -0
- package/adapters/pi/references/forge-config-schema.json +19 -1
- package/adapters/pi/references/forge-decisions-schema.json +33 -0
- package/adapters/pi/references/pipeline-state-schema.json +38 -4
- package/adapters/pi/references/process-overview.md +2 -2
- package/adapters/pi/references/ralph-loop-contract.md +6 -3
- package/adapters/pi/references/shared-conventions.md +91 -17
- package/adapters/pi/references/stage-exit-protocol.md +395 -147
- package/adapters/pi/scripts/epic-manifest.py +495 -91
- package/adapters/pi/scripts/fix-sweep.py +1180 -0
- package/adapters/pi/scripts/forge-bootstrap.py +57 -5
- package/adapters/pi/scripts/forge-session.py +4241 -146
- package/adapters/pi/scripts/validate-traceability.py +86 -5
- package/adapters/pi/skills/forge/SKILL.md +10 -9
- package/adapters/pi/skills/forge/references/pipeline-state-schema.json +38 -4
- package/adapters/pi/skills/forge/references/process-overview.md +2 -2
- package/adapters/pi/skills/forge/references/shared-conventions.md +91 -17
- package/adapters/pi/skills/forge/references/stage-exit-protocol.md +395 -147
- package/adapters/pi/skills/forge-0-epic/SKILL.md +7 -2
- package/adapters/pi/skills/forge-0-epic/references/edit-mode.md +31 -27
- package/adapters/pi/skills/forge-0-epic/references/epic-manifest-subcommands.md +6 -1
- package/adapters/pi/skills/forge-0-epic/references/pipeline-state-schema.json +38 -4
- package/adapters/pi/skills/forge-0-epic/references/shared-conventions.md +91 -17
- package/adapters/pi/skills/forge-0-epic/references/stage-exit-protocol.md +395 -147
- package/adapters/pi/skills/forge-1-prd/SKILL.md +17 -4
- package/adapters/pi/skills/forge-1-prd/references/shared-conventions.md +91 -17
- package/adapters/pi/skills/forge-1-prd/references/stage-exit-protocol.md +395 -147
- package/adapters/pi/skills/forge-2-tech/SKILL.md +18 -4
- package/adapters/pi/skills/forge-2-tech/references/shared-conventions.md +91 -17
- package/adapters/pi/skills/forge-2-tech/references/stage-exit-protocol.md +395 -147
- package/adapters/pi/skills/forge-3-specs/SKILL.md +9 -3
- package/adapters/pi/skills/forge-3-specs/references/shared-conventions.md +91 -17
- 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 +395 -147
- package/adapters/pi/skills/forge-4-backlog/SKILL.md +56 -5
- package/adapters/pi/skills/forge-4-backlog/references/shared-conventions.md +91 -17
- package/adapters/pi/skills/forge-4-backlog/references/stage-exit-protocol.md +395 -147
- package/adapters/pi/skills/forge-5-loop/SKILL.md +67 -69
- package/adapters/pi/skills/forge-5-loop/references/agent-selection.md +16 -0
- package/adapters/pi/skills/forge-5-loop/references/ralph-loop-contract.md +6 -3
- package/adapters/pi/skills/forge-5-loop/references/recovery-procedure.md +349 -0
- package/adapters/pi/skills/forge-5-loop/references/result-reporting.md +101 -33
- package/adapters/pi/skills/forge-5-loop/references/runner-contract.md +29 -9
- package/adapters/pi/skills/forge-5-loop/references/shared-conventions.md +91 -17
- package/adapters/pi/skills/forge-5-loop/references/stage-exit-protocol.md +395 -147
- package/adapters/pi/skills/forge-6-docs/SKILL.md +75 -8
- package/adapters/pi/skills/forge-6-docs/references/shared-conventions.md +91 -17
- package/adapters/pi/skills/forge-6-docs/references/stage-exit-protocol.md +516 -0
- package/adapters/pi/skills/forge-fix/SKILL.md +118 -32
- package/adapters/pi/skills/forge-fix/references/shared-conventions.md +91 -17
- package/adapters/pi/skills/forge-fix/references/stage-exit-protocol.md +395 -147
- package/adapters/pi/skills/forge-guide/references/forge-config-schema.json +19 -1
- package/adapters/pi/skills/forge-guide/references/process-overview.md +2 -2
- package/adapters/pi/skills/forge-guide/references/ralph-loop-contract.md +6 -3
- package/adapters/pi/skills/forge-guide/references/shared-conventions.md +91 -17
- package/adapters/pi/skills/forge-verify/SKILL.md +81 -50
- package/adapters/pi/skills/forge-verify/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/pi/skills/forge-verify/references/findings-template.md +69 -49
- package/adapters/pi/skills/forge-verify/references/shared-conventions.md +91 -17
- package/adapters/pi/skills/forge-verify/references/stage-exit-protocol.md +516 -0
- package/adapters/pi/skills/forge-verify/references/verification-checklists/backlog.md +73 -0
- package/adapters/pi/skills/forge-verify/references/verification-checklists/epic.md +5 -4
- package/adapters/pi/skills/forge-verify/references/verification-checklists/impl.md +85 -0
- package/adapters/pi/skills/forge-verify/references/verification-checklists/specs.md +45 -1
- 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
|
@@ -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
|
|
42
|
+
# Constants
|
|
40
43
|
# --------------------------------------------------------------------------- #
|
|
41
44
|
|
|
42
|
-
#: A safe feature/epic name: one kebab-case token
|
|
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
|
-
{
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
204
|
+
"""Aggregate completion counts for the epic dashboard."""
|
|
153
205
|
|
|
154
|
-
complete: int #: Number of member features complete-for-orchestration
|
|
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
|
|
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
|
|
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
|
|
218
|
+
are not themselves complete.
|
|
167
219
|
parallelEligible: Subset of `actionable` with no mutual (transitive)
|
|
168
|
-
dependency — surfaced for future parallel execution
|
|
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
|
|
174
|
-
outside ``KNOWN_VERIFY_STATUSES`` (treated as
|
|
175
|
-
would poison the rollup + dependency gates,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`` (
|
|
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
|
|
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
|
|
550
|
+
"""Resolve a bare feature/epic name to its absolute directory.
|
|
484
551
|
|
|
485
|
-
Implements the 5-step algorithm
|
|
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).
|
|
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
|
|
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
|
|
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
|
|
636
|
+
# Validation — implemented in item 006
|
|
570
637
|
# --------------------------------------------------------------------------- #
|
|
571
638
|
|
|
572
639
|
|
|
573
|
-
#: Top-level required keys (
|
|
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
|
|
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
|
|
653
|
+
#: Required keys on each Contract (exposes[]) object.
|
|
586
654
|
_CONTRACT_REQUIRED: Final = ("name", "kind", "summary")
|
|
587
|
-
#: Required keys on each ConsumedContract (consumes[]) object
|
|
655
|
+
#: Required keys on each ConsumedContract (consumes[]) object.
|
|
588
656
|
_CONSUMED_REQUIRED: Final = ("from", "name", "summary")
|
|
589
|
-
#: Allowed epic lifecycle states
|
|
657
|
+
#: Allowed epic lifecycle states.
|
|
590
658
|
_EPIC_STATUSES: Final = ("active", "paused", "abandoned", "complete")
|
|
591
|
-
#: Allowed Contract kinds
|
|
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
|
|
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
|
|
669
|
+
"""Hand-rolled stdlib schema checker over the manifest.
|
|
602
670
|
|
|
603
|
-
Asserts required keys/types/enums/consts
|
|
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
|
|
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
|
|
775
|
+
"""Validate an already-parsed manifest dict, returning findings.
|
|
702
776
|
|
|
703
|
-
Runs the invariant checks
|
|
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
|
|
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
|
|
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
|
|
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"/skill: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"/skill: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
|
|
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
|
|
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
|
|
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
|
|
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")
|
|
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
|
|
1269
|
+
"""Recommend the next forge command for an actionable feature.
|
|
984
1270
|
|
|
985
1271
|
``/skill: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,
|
|
993
|
-
|
|
994
|
-
|
|
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"/skill:forge-1-prd {name}"
|
|
1002
1293
|
if nxt is None:
|
|
1003
|
-
|
|
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"/skill:forge-fix {name}"
|
|
1299
|
+
return f"/skill:forge-verify {name}"
|
|
1004
1300
|
return f"/skill:{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
|
|
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
|
|
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
|
|
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
|
|
1113
|
-
|
|
1114
|
-
The shared tail of every mutator (REQ-ROBUST-03, REQ-OBS-01, REQ-EPIC-05
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
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)
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
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")
|