@garygentry/feature-forge 0.3.6 → 0.3.8
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/README.md +1 -1
- package/adapters/GENERATION-REPORT.md +20 -0
- package/adapters/claude/.claude-plugin/plugin.json +16 -0
- package/adapters/claude/.feature-forge-bundle.json +1 -1
- package/adapters/claude/agents/forge-verifier.md +16 -5
- package/adapters/claude/references/forge-config-schema.json +3 -3
- package/adapters/claude/references/portable-root.md +20 -16
- package/adapters/claude/references/preflight-and-self-heal.md +183 -0
- package/adapters/claude/references/process-overview.md +1 -1
- package/adapters/claude/references/select-outcome.md +138 -0
- package/adapters/claude/references/shared-conventions.md +175 -22
- package/adapters/claude/references/skill-frontmatter.schema.json +3 -1
- package/adapters/claude/references/stack-resolution.md +1 -1
- package/adapters/claude/references/stage-exit-protocol.md +19 -4
- package/adapters/claude/references/templates/root-hygiene/AGENTS.md +14 -0
- package/adapters/claude/references/templates/root-hygiene/CLAUDE.md +14 -0
- package/adapters/claude/references/templates/specs-hygiene/AGENTS.md +3 -1
- package/adapters/claude/references/templates/specs-hygiene/CLAUDE.md +2 -1
- package/adapters/claude/references/verifier-patterns/MEMORY.md +35 -0
- package/adapters/claude/references/verifier-patterns/pattern_absence_claims.md +36 -0
- package/adapters/claude/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
- package/adapters/claude/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
- package/adapters/claude/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
- package/adapters/claude/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
- package/adapters/claude/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
- package/adapters/claude/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
- package/adapters/claude/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
- package/adapters/claude/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
- package/adapters/claude/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
- package/adapters/claude/references/verifier-patterns/pattern_self_referential_control.md +26 -0
- package/adapters/claude/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
- package/adapters/claude/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
- package/adapters/claude/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
- package/adapters/claude/references/verifier-patterns/pattern_test_count_units.md +20 -0
- package/adapters/claude/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
- package/adapters/claude/references/verify-state.md +98 -0
- package/adapters/claude/scripts/forge-bootstrap.py +23 -2
- package/adapters/claude/scripts/forge-root.sh +104 -21
- package/adapters/claude/scripts/forge-session.py +620 -6973
- package/adapters/claude/scripts/forge_session/__init__.py +11 -0
- package/adapters/claude/scripts/forge_session/_common.py +1566 -0
- package/adapters/claude/scripts/forge_session/_doctor_util.py +218 -0
- package/adapters/claude/scripts/forge_session/cli.py +1067 -0
- package/adapters/claude/scripts/forge_session/decisions.py +355 -0
- package/adapters/claude/scripts/forge_session/discover.py +472 -0
- package/adapters/claude/scripts/forge_session/doctor.py +2144 -0
- package/adapters/claude/scripts/forge_session/exit.py +1081 -0
- package/adapters/claude/scripts/forge_session/outcomes.py +574 -0
- package/adapters/claude/scripts/forge_session/routes.py +1162 -0
- package/adapters/claude/scripts/forge_session/state.py +1479 -0
- package/adapters/claude/scripts/forge_session/topology.py +443 -0
- package/adapters/claude/skills/forge/SKILL.md +7 -7
- package/adapters/claude/skills/forge/references/process-overview.md +1 -1
- package/adapters/claude/skills/forge/references/shared-conventions.md +175 -22
- package/adapters/claude/skills/forge/references/stage-exit-protocol.md +19 -4
- package/adapters/claude/skills/forge-0-epic/SKILL.md +7 -7
- package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +9 -5
- package/adapters/claude/skills/forge-0-epic/references/epic-manifest-subcommands.md +6 -1
- package/adapters/claude/skills/forge-0-epic/references/portable-root.md +20 -16
- package/adapters/claude/skills/forge-0-epic/references/shared-conventions.md +175 -22
- package/adapters/claude/skills/forge-0-epic/references/stage-exit-protocol.md +19 -4
- package/adapters/claude/skills/forge-1-prd/SKILL.md +7 -7
- package/adapters/claude/skills/forge-1-prd/references/shared-conventions.md +175 -22
- package/adapters/claude/skills/forge-1-prd/references/stage-exit-protocol.md +19 -4
- package/adapters/claude/skills/forge-2-tech/SKILL.md +7 -7
- package/adapters/claude/skills/forge-2-tech/references/shared-conventions.md +175 -22
- package/adapters/claude/skills/forge-2-tech/references/stack-resolution.md +1 -1
- package/adapters/claude/skills/forge-2-tech/references/stage-exit-protocol.md +19 -4
- package/adapters/claude/skills/forge-3-specs/SKILL.md +5 -5
- package/adapters/claude/skills/forge-3-specs/references/shared-conventions.md +175 -22
- package/adapters/claude/skills/forge-3-specs/references/stage-exit-protocol.md +19 -4
- package/adapters/claude/skills/forge-4-backlog/SKILL.md +16 -8
- package/adapters/claude/skills/forge-4-backlog/references/shared-conventions.md +175 -22
- package/adapters/claude/skills/forge-4-backlog/references/stage-exit-protocol.md +19 -4
- package/adapters/claude/skills/forge-4-backlog/references/verify-state.md +98 -0
- package/adapters/claude/skills/forge-5-loop/SKILL.md +36 -41
- package/adapters/claude/skills/forge-5-loop/references/agent-selection.md +4 -0
- package/adapters/claude/skills/forge-5-loop/references/preflight-and-self-heal.md +183 -0
- package/adapters/claude/skills/forge-5-loop/references/recovery-procedure.md +10 -0
- package/adapters/claude/skills/forge-5-loop/references/runner-contract.md +15 -5
- package/adapters/claude/skills/forge-5-loop/references/shared-conventions.md +175 -22
- package/adapters/claude/skills/forge-5-loop/references/stage-exit-protocol.md +19 -4
- package/adapters/claude/skills/forge-5-loop/references/verify-state.md +98 -0
- package/adapters/claude/skills/forge-6-docs/SKILL.md +21 -14
- package/adapters/claude/skills/forge-6-docs/references/shared-conventions.md +175 -22
- package/adapters/claude/skills/forge-6-docs/references/stage-exit-protocol.md +19 -4
- package/adapters/claude/skills/forge-6-docs/references/verify-state.md +98 -0
- package/adapters/claude/skills/forge-bootstrap/SKILL.md +19 -10
- package/adapters/claude/skills/forge-bootstrap/references/shared-conventions.md +572 -0
- package/adapters/claude/skills/forge-fix/SKILL.md +15 -18
- package/adapters/claude/skills/forge-fix/references/select-outcome.md +138 -0
- package/adapters/claude/skills/forge-fix/references/shared-conventions.md +175 -22
- package/adapters/claude/skills/forge-fix/references/stage-exit-protocol.md +19 -4
- package/adapters/claude/skills/forge-guide/SKILL.md +87 -1
- package/adapters/claude/skills/forge-guide/references/forge-config-schema.json +3 -3
- package/adapters/claude/skills/forge-guide/references/preflight-and-self-heal.md +183 -0
- package/adapters/claude/skills/forge-guide/references/process-overview.md +1 -1
- package/adapters/claude/skills/forge-guide/references/shared-conventions.md +175 -22
- package/adapters/claude/skills/forge-guide/references/stack-resolution.md +1 -1
- package/adapters/claude/skills/forge-init/SKILL.md +58 -7
- package/adapters/claude/skills/forge-init/references/preflight-and-self-heal.md +183 -0
- package/adapters/claude/skills/forge-init/references/shared-conventions.md +572 -0
- package/adapters/claude/skills/forge-verify/SKILL.md +20 -23
- package/adapters/claude/skills/forge-verify/references/findings-template.md +2 -2
- package/adapters/claude/skills/forge-verify/references/select-outcome.md +138 -0
- package/adapters/claude/skills/forge-verify/references/shared-conventions.md +175 -22
- package/adapters/claude/skills/forge-verify/references/stage-exit-protocol.md +19 -4
- package/adapters/claude/skills/forge-verify/references/verification-checklists/epic.md +1 -1
- package/adapters/claude/skills/forge-verify/references/verifier-patterns/MEMORY.md +35 -0
- package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_absence_claims.md +36 -0
- package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
- package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
- package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
- package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
- package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
- package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
- package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
- package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
- package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
- package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_self_referential_control.md +26 -0
- package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
- package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
- package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
- package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_test_count_units.md +20 -0
- package/adapters/claude/skills/forge-verify/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
- package/adapters/codex/.feature-forge-bundle.json +1 -1
- package/adapters/codex/agents/forge-researcher.toml +1 -1
- package/adapters/codex/agents/forge-verifier.toml +17 -6
- package/adapters/codex/references/forge-config-schema.json +5 -5
- package/adapters/codex/references/portable-root.md +20 -16
- package/adapters/codex/references/preflight-and-self-heal.md +183 -0
- package/adapters/codex/references/process-overview.md +10 -10
- package/adapters/codex/references/select-outcome.md +138 -0
- package/adapters/codex/references/shared-conventions.md +188 -35
- package/adapters/codex/references/skill-frontmatter.schema.json +3 -1
- package/adapters/codex/references/stack-resolution.md +1 -1
- package/adapters/codex/references/stage-exit-protocol.md +22 -7
- package/adapters/codex/references/templates/root-hygiene/AGENTS.md +14 -0
- package/adapters/codex/references/templates/root-hygiene/CLAUDE.md +14 -0
- package/adapters/codex/references/templates/specs-hygiene/AGENTS.md +3 -1
- package/adapters/codex/references/templates/specs-hygiene/CLAUDE.md +2 -1
- package/adapters/codex/references/verifier-patterns/MEMORY.md +35 -0
- package/adapters/codex/references/verifier-patterns/pattern_absence_claims.md +36 -0
- package/adapters/codex/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
- package/adapters/codex/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
- package/adapters/codex/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
- package/adapters/codex/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
- package/adapters/codex/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
- package/adapters/codex/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
- package/adapters/codex/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
- package/adapters/codex/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
- package/adapters/codex/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
- package/adapters/codex/references/verifier-patterns/pattern_self_referential_control.md +26 -0
- package/adapters/codex/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
- package/adapters/codex/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
- package/adapters/codex/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
- package/adapters/codex/references/verifier-patterns/pattern_test_count_units.md +20 -0
- package/adapters/codex/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
- package/adapters/codex/references/verify-state.md +98 -0
- package/adapters/codex/scripts/forge-bootstrap.py +23 -2
- package/adapters/codex/scripts/forge-root.sh +104 -21
- package/adapters/codex/scripts/forge-session.py +620 -6973
- package/adapters/codex/scripts/forge_session/__init__.py +11 -0
- package/adapters/codex/scripts/forge_session/_common.py +1566 -0
- package/adapters/codex/scripts/forge_session/_doctor_util.py +218 -0
- package/adapters/codex/scripts/forge_session/cli.py +1067 -0
- package/adapters/codex/scripts/forge_session/decisions.py +355 -0
- package/adapters/codex/scripts/forge_session/discover.py +472 -0
- package/adapters/codex/scripts/forge_session/doctor.py +2144 -0
- package/adapters/codex/scripts/forge_session/exit.py +1081 -0
- package/adapters/codex/scripts/forge_session/outcomes.py +574 -0
- package/adapters/codex/scripts/forge_session/routes.py +1162 -0
- package/adapters/codex/scripts/forge_session/state.py +1479 -0
- package/adapters/codex/scripts/forge_session/topology.py +443 -0
- package/adapters/codex/skills/forge/SKILL.md +40 -40
- package/adapters/codex/skills/forge/references/process-overview.md +10 -10
- package/adapters/codex/skills/forge/references/shared-conventions.md +188 -35
- package/adapters/codex/skills/forge/references/stage-exit-protocol.md +22 -7
- package/adapters/codex/skills/forge-0-epic/SKILL.md +15 -15
- package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +14 -10
- package/adapters/codex/skills/forge-0-epic/references/epic-manifest-subcommands.md +8 -3
- package/adapters/codex/skills/forge-0-epic/references/portable-root.md +20 -16
- package/adapters/codex/skills/forge-0-epic/references/shared-conventions.md +188 -35
- package/adapters/codex/skills/forge-0-epic/references/stage-exit-protocol.md +22 -7
- package/adapters/codex/skills/forge-1-prd/SKILL.md +15 -15
- package/adapters/codex/skills/forge-1-prd/references/shared-conventions.md +188 -35
- package/adapters/codex/skills/forge-1-prd/references/stage-exit-protocol.md +22 -7
- package/adapters/codex/skills/forge-2-tech/SKILL.md +15 -15
- package/adapters/codex/skills/forge-2-tech/references/shared-conventions.md +188 -35
- package/adapters/codex/skills/forge-2-tech/references/stack-resolution.md +1 -1
- package/adapters/codex/skills/forge-2-tech/references/stage-exit-protocol.md +22 -7
- package/adapters/codex/skills/forge-3-specs/SKILL.md +10 -10
- package/adapters/codex/skills/forge-3-specs/references/shared-conventions.md +188 -35
- package/adapters/codex/skills/forge-3-specs/references/stage-exit-protocol.md +22 -7
- package/adapters/codex/skills/forge-4-backlog/SKILL.md +19 -11
- package/adapters/codex/skills/forge-4-backlog/references/shared-conventions.md +188 -35
- package/adapters/codex/skills/forge-4-backlog/references/stage-exit-protocol.md +22 -7
- package/adapters/codex/skills/forge-4-backlog/references/verify-state.md +98 -0
- package/adapters/codex/skills/forge-5-loop/SKILL.md +48 -53
- package/adapters/codex/skills/forge-5-loop/references/agent-selection.md +5 -1
- package/adapters/codex/skills/forge-5-loop/references/preflight-and-self-heal.md +183 -0
- package/adapters/codex/skills/forge-5-loop/references/recovery-procedure.md +14 -4
- package/adapters/codex/skills/forge-5-loop/references/runner-contract.md +18 -8
- package/adapters/codex/skills/forge-5-loop/references/shared-conventions.md +188 -35
- package/adapters/codex/skills/forge-5-loop/references/stage-exit-protocol.md +22 -7
- package/adapters/codex/skills/forge-5-loop/references/verify-state.md +98 -0
- package/adapters/codex/skills/forge-6-docs/SKILL.md +27 -20
- package/adapters/codex/skills/forge-6-docs/references/shared-conventions.md +188 -35
- package/adapters/codex/skills/forge-6-docs/references/stage-exit-protocol.md +22 -7
- package/adapters/codex/skills/forge-6-docs/references/verify-state.md +98 -0
- package/adapters/codex/skills/forge-bootstrap/SKILL.md +23 -15
- package/adapters/codex/skills/forge-bootstrap/references/shared-conventions.md +572 -0
- package/adapters/codex/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +2 -2
- package/adapters/codex/skills/forge-fix/SKILL.md +27 -30
- package/adapters/codex/skills/forge-fix/references/select-outcome.md +138 -0
- package/adapters/codex/skills/forge-fix/references/shared-conventions.md +188 -35
- package/adapters/codex/skills/forge-fix/references/stage-exit-protocol.md +22 -7
- package/adapters/codex/skills/forge-guide/SKILL.md +84 -7
- package/adapters/codex/skills/forge-guide/references/forge-config-schema.json +5 -5
- package/adapters/codex/skills/forge-guide/references/preflight-and-self-heal.md +183 -0
- package/adapters/codex/skills/forge-guide/references/process-overview.md +10 -10
- package/adapters/codex/skills/forge-guide/references/shared-conventions.md +188 -35
- package/adapters/codex/skills/forge-guide/references/stack-resolution.md +1 -1
- package/adapters/codex/skills/forge-init/SKILL.md +62 -12
- package/adapters/codex/skills/forge-init/references/preflight-and-self-heal.md +183 -0
- package/adapters/codex/skills/forge-init/references/shared-conventions.md +572 -0
- package/adapters/codex/skills/forge-verify/SKILL.md +27 -30
- package/adapters/codex/skills/forge-verify/references/findings-template.md +2 -2
- package/adapters/codex/skills/forge-verify/references/select-outcome.md +138 -0
- package/adapters/codex/skills/forge-verify/references/shared-conventions.md +188 -35
- package/adapters/codex/skills/forge-verify/references/stage-exit-protocol.md +22 -7
- package/adapters/codex/skills/forge-verify/references/verification-checklists/epic.md +2 -2
- package/adapters/codex/skills/forge-verify/references/verifier-patterns/MEMORY.md +35 -0
- package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_absence_claims.md +36 -0
- package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
- package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
- package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
- package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
- package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
- package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
- package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
- package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
- package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
- package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_self_referential_control.md +26 -0
- package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
- package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
- package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
- package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_test_count_units.md +20 -0
- package/adapters/codex/skills/forge-verify/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
- package/adapters/copilot/.feature-forge-bundle.json +1 -1
- package/adapters/copilot/agents/forge-researcher.md +1 -1
- package/adapters/copilot/agents/forge-verifier.md +17 -6
- package/adapters/copilot/references/forge-config-schema.json +5 -5
- package/adapters/copilot/references/portable-root.md +20 -16
- package/adapters/copilot/references/preflight-and-self-heal.md +183 -0
- package/adapters/copilot/references/process-overview.md +10 -10
- package/adapters/copilot/references/select-outcome.md +138 -0
- package/adapters/copilot/references/shared-conventions.md +188 -35
- package/adapters/copilot/references/skill-frontmatter.schema.json +3 -1
- package/adapters/copilot/references/stack-resolution.md +1 -1
- package/adapters/copilot/references/stage-exit-protocol.md +22 -7
- package/adapters/copilot/references/templates/root-hygiene/AGENTS.md +14 -0
- package/adapters/copilot/references/templates/root-hygiene/CLAUDE.md +14 -0
- package/adapters/copilot/references/templates/specs-hygiene/AGENTS.md +3 -1
- package/adapters/copilot/references/templates/specs-hygiene/CLAUDE.md +2 -1
- package/adapters/copilot/references/verifier-patterns/MEMORY.md +35 -0
- package/adapters/copilot/references/verifier-patterns/pattern_absence_claims.md +36 -0
- package/adapters/copilot/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
- package/adapters/copilot/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
- package/adapters/copilot/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
- package/adapters/copilot/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
- package/adapters/copilot/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
- package/adapters/copilot/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
- package/adapters/copilot/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
- package/adapters/copilot/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
- package/adapters/copilot/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
- package/adapters/copilot/references/verifier-patterns/pattern_self_referential_control.md +26 -0
- package/adapters/copilot/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
- package/adapters/copilot/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
- package/adapters/copilot/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
- package/adapters/copilot/references/verifier-patterns/pattern_test_count_units.md +20 -0
- package/adapters/copilot/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
- package/adapters/copilot/references/verify-state.md +98 -0
- package/adapters/copilot/scripts/forge-bootstrap.py +23 -2
- package/adapters/copilot/scripts/forge-root.sh +104 -21
- package/adapters/copilot/scripts/forge-session.py +620 -6973
- package/adapters/copilot/scripts/forge_session/__init__.py +11 -0
- package/adapters/copilot/scripts/forge_session/_common.py +1566 -0
- package/adapters/copilot/scripts/forge_session/_doctor_util.py +218 -0
- package/adapters/copilot/scripts/forge_session/cli.py +1067 -0
- package/adapters/copilot/scripts/forge_session/decisions.py +355 -0
- package/adapters/copilot/scripts/forge_session/discover.py +472 -0
- package/adapters/copilot/scripts/forge_session/doctor.py +2144 -0
- package/adapters/copilot/scripts/forge_session/exit.py +1081 -0
- package/adapters/copilot/scripts/forge_session/outcomes.py +574 -0
- package/adapters/copilot/scripts/forge_session/routes.py +1162 -0
- package/adapters/copilot/scripts/forge_session/state.py +1479 -0
- package/adapters/copilot/scripts/forge_session/topology.py +443 -0
- package/adapters/copilot/skills/forge/forge.md +40 -40
- package/adapters/copilot/skills/forge/references/process-overview.md +10 -10
- package/adapters/copilot/skills/forge/references/shared-conventions.md +188 -35
- package/adapters/copilot/skills/forge/references/stage-exit-protocol.md +22 -7
- package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +15 -15
- package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +14 -10
- package/adapters/copilot/skills/forge-0-epic/references/epic-manifest-subcommands.md +8 -3
- package/adapters/copilot/skills/forge-0-epic/references/portable-root.md +20 -16
- package/adapters/copilot/skills/forge-0-epic/references/shared-conventions.md +188 -35
- package/adapters/copilot/skills/forge-0-epic/references/stage-exit-protocol.md +22 -7
- package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +15 -15
- package/adapters/copilot/skills/forge-1-prd/references/shared-conventions.md +188 -35
- package/adapters/copilot/skills/forge-1-prd/references/stage-exit-protocol.md +22 -7
- package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +15 -15
- package/adapters/copilot/skills/forge-2-tech/references/shared-conventions.md +188 -35
- package/adapters/copilot/skills/forge-2-tech/references/stack-resolution.md +1 -1
- package/adapters/copilot/skills/forge-2-tech/references/stage-exit-protocol.md +22 -7
- package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +10 -10
- package/adapters/copilot/skills/forge-3-specs/references/shared-conventions.md +188 -35
- package/adapters/copilot/skills/forge-3-specs/references/stage-exit-protocol.md +22 -7
- package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +19 -11
- package/adapters/copilot/skills/forge-4-backlog/references/shared-conventions.md +188 -35
- package/adapters/copilot/skills/forge-4-backlog/references/stage-exit-protocol.md +22 -7
- package/adapters/copilot/skills/forge-4-backlog/references/verify-state.md +98 -0
- package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +48 -53
- package/adapters/copilot/skills/forge-5-loop/references/agent-selection.md +5 -1
- package/adapters/copilot/skills/forge-5-loop/references/preflight-and-self-heal.md +183 -0
- package/adapters/copilot/skills/forge-5-loop/references/recovery-procedure.md +14 -4
- package/adapters/copilot/skills/forge-5-loop/references/runner-contract.md +18 -8
- package/adapters/copilot/skills/forge-5-loop/references/shared-conventions.md +188 -35
- package/adapters/copilot/skills/forge-5-loop/references/stage-exit-protocol.md +22 -7
- package/adapters/copilot/skills/forge-5-loop/references/verify-state.md +98 -0
- package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +27 -20
- package/adapters/copilot/skills/forge-6-docs/references/shared-conventions.md +188 -35
- package/adapters/copilot/skills/forge-6-docs/references/stage-exit-protocol.md +22 -7
- package/adapters/copilot/skills/forge-6-docs/references/verify-state.md +98 -0
- package/adapters/copilot/skills/forge-bootstrap/forge-bootstrap.md +23 -15
- package/adapters/copilot/skills/forge-bootstrap/references/shared-conventions.md +572 -0
- package/adapters/copilot/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +2 -2
- package/adapters/copilot/skills/forge-fix/forge-fix.md +27 -30
- package/adapters/copilot/skills/forge-fix/references/select-outcome.md +138 -0
- package/adapters/copilot/skills/forge-fix/references/shared-conventions.md +188 -35
- package/adapters/copilot/skills/forge-fix/references/stage-exit-protocol.md +22 -7
- package/adapters/copilot/skills/forge-guide/forge-guide.md +84 -7
- package/adapters/copilot/skills/forge-guide/references/forge-config-schema.json +5 -5
- package/adapters/copilot/skills/forge-guide/references/preflight-and-self-heal.md +183 -0
- package/adapters/copilot/skills/forge-guide/references/process-overview.md +10 -10
- package/adapters/copilot/skills/forge-guide/references/shared-conventions.md +188 -35
- package/adapters/copilot/skills/forge-guide/references/stack-resolution.md +1 -1
- package/adapters/copilot/skills/forge-init/forge-init.md +62 -12
- package/adapters/copilot/skills/forge-init/references/preflight-and-self-heal.md +183 -0
- package/adapters/copilot/skills/forge-init/references/shared-conventions.md +572 -0
- package/adapters/copilot/skills/forge-verify/forge-verify.md +27 -30
- package/adapters/copilot/skills/forge-verify/references/findings-template.md +2 -2
- package/adapters/copilot/skills/forge-verify/references/select-outcome.md +138 -0
- package/adapters/copilot/skills/forge-verify/references/shared-conventions.md +188 -35
- package/adapters/copilot/skills/forge-verify/references/stage-exit-protocol.md +22 -7
- package/adapters/copilot/skills/forge-verify/references/verification-checklists/epic.md +2 -2
- package/adapters/copilot/skills/forge-verify/references/verifier-patterns/MEMORY.md +35 -0
- package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_absence_claims.md +36 -0
- package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
- package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
- package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
- package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
- package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
- package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
- package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
- package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
- package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
- package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_self_referential_control.md +26 -0
- package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
- package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
- package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
- package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_test_count_units.md +20 -0
- package/adapters/copilot/skills/forge-verify/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
- package/adapters/cursor/.feature-forge-bundle.json +1 -1
- package/adapters/cursor/agents/forge-researcher.mdc +1 -1
- package/adapters/cursor/agents/forge-verifier.mdc +17 -6
- package/adapters/cursor/references/forge-config-schema.json +5 -5
- package/adapters/cursor/references/portable-root.md +20 -16
- package/adapters/cursor/references/preflight-and-self-heal.md +183 -0
- package/adapters/cursor/references/process-overview.md +10 -10
- package/adapters/cursor/references/select-outcome.md +138 -0
- package/adapters/cursor/references/shared-conventions.md +188 -35
- package/adapters/cursor/references/skill-frontmatter.schema.json +3 -1
- package/adapters/cursor/references/stack-resolution.md +1 -1
- package/adapters/cursor/references/stage-exit-protocol.md +22 -7
- package/adapters/cursor/references/templates/root-hygiene/AGENTS.md +14 -0
- package/adapters/cursor/references/templates/root-hygiene/CLAUDE.md +14 -0
- package/adapters/cursor/references/templates/specs-hygiene/AGENTS.md +3 -1
- package/adapters/cursor/references/templates/specs-hygiene/CLAUDE.md +2 -1
- package/adapters/cursor/references/verifier-patterns/MEMORY.md +35 -0
- package/adapters/cursor/references/verifier-patterns/pattern_absence_claims.md +36 -0
- package/adapters/cursor/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
- package/adapters/cursor/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
- package/adapters/cursor/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
- package/adapters/cursor/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
- package/adapters/cursor/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
- package/adapters/cursor/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
- package/adapters/cursor/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
- package/adapters/cursor/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
- package/adapters/cursor/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
- package/adapters/cursor/references/verifier-patterns/pattern_self_referential_control.md +26 -0
- package/adapters/cursor/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
- package/adapters/cursor/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
- package/adapters/cursor/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
- package/adapters/cursor/references/verifier-patterns/pattern_test_count_units.md +20 -0
- package/adapters/cursor/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
- package/adapters/cursor/references/verify-state.md +98 -0
- package/adapters/cursor/scripts/forge-bootstrap.py +23 -2
- package/adapters/cursor/scripts/forge-root.sh +104 -21
- package/adapters/cursor/scripts/forge-session.py +620 -6973
- package/adapters/cursor/scripts/forge_session/__init__.py +11 -0
- package/adapters/cursor/scripts/forge_session/_common.py +1566 -0
- package/adapters/cursor/scripts/forge_session/_doctor_util.py +218 -0
- package/adapters/cursor/scripts/forge_session/cli.py +1067 -0
- package/adapters/cursor/scripts/forge_session/decisions.py +355 -0
- package/adapters/cursor/scripts/forge_session/discover.py +472 -0
- package/adapters/cursor/scripts/forge_session/doctor.py +2144 -0
- package/adapters/cursor/scripts/forge_session/exit.py +1081 -0
- package/adapters/cursor/scripts/forge_session/outcomes.py +574 -0
- package/adapters/cursor/scripts/forge_session/routes.py +1162 -0
- package/adapters/cursor/scripts/forge_session/state.py +1479 -0
- package/adapters/cursor/scripts/forge_session/topology.py +443 -0
- package/adapters/cursor/skills/forge/forge.mdc +40 -40
- package/adapters/cursor/skills/forge/references/process-overview.md +10 -10
- package/adapters/cursor/skills/forge/references/shared-conventions.md +188 -35
- package/adapters/cursor/skills/forge/references/stage-exit-protocol.md +22 -7
- package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +15 -15
- package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +14 -10
- package/adapters/cursor/skills/forge-0-epic/references/epic-manifest-subcommands.md +8 -3
- package/adapters/cursor/skills/forge-0-epic/references/portable-root.md +20 -16
- package/adapters/cursor/skills/forge-0-epic/references/shared-conventions.md +188 -35
- package/adapters/cursor/skills/forge-0-epic/references/stage-exit-protocol.md +22 -7
- package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +15 -15
- package/adapters/cursor/skills/forge-1-prd/references/shared-conventions.md +188 -35
- package/adapters/cursor/skills/forge-1-prd/references/stage-exit-protocol.md +22 -7
- package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +15 -15
- package/adapters/cursor/skills/forge-2-tech/references/shared-conventions.md +188 -35
- package/adapters/cursor/skills/forge-2-tech/references/stack-resolution.md +1 -1
- package/adapters/cursor/skills/forge-2-tech/references/stage-exit-protocol.md +22 -7
- package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +10 -10
- package/adapters/cursor/skills/forge-3-specs/references/shared-conventions.md +188 -35
- package/adapters/cursor/skills/forge-3-specs/references/stage-exit-protocol.md +22 -7
- package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +19 -11
- package/adapters/cursor/skills/forge-4-backlog/references/shared-conventions.md +188 -35
- package/adapters/cursor/skills/forge-4-backlog/references/stage-exit-protocol.md +22 -7
- package/adapters/cursor/skills/forge-4-backlog/references/verify-state.md +98 -0
- package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +48 -53
- package/adapters/cursor/skills/forge-5-loop/references/agent-selection.md +5 -1
- package/adapters/cursor/skills/forge-5-loop/references/preflight-and-self-heal.md +183 -0
- package/adapters/cursor/skills/forge-5-loop/references/recovery-procedure.md +14 -4
- package/adapters/cursor/skills/forge-5-loop/references/runner-contract.md +18 -8
- package/adapters/cursor/skills/forge-5-loop/references/shared-conventions.md +188 -35
- package/adapters/cursor/skills/forge-5-loop/references/stage-exit-protocol.md +22 -7
- package/adapters/cursor/skills/forge-5-loop/references/verify-state.md +98 -0
- package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +27 -20
- package/adapters/cursor/skills/forge-6-docs/references/shared-conventions.md +188 -35
- package/adapters/cursor/skills/forge-6-docs/references/stage-exit-protocol.md +22 -7
- package/adapters/cursor/skills/forge-6-docs/references/verify-state.md +98 -0
- package/adapters/cursor/skills/forge-bootstrap/forge-bootstrap.mdc +23 -15
- package/adapters/cursor/skills/forge-bootstrap/references/shared-conventions.md +572 -0
- package/adapters/cursor/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +2 -2
- package/adapters/cursor/skills/forge-fix/forge-fix.mdc +27 -30
- package/adapters/cursor/skills/forge-fix/references/select-outcome.md +138 -0
- package/adapters/cursor/skills/forge-fix/references/shared-conventions.md +188 -35
- package/adapters/cursor/skills/forge-fix/references/stage-exit-protocol.md +22 -7
- package/adapters/cursor/skills/forge-guide/forge-guide.mdc +84 -7
- package/adapters/cursor/skills/forge-guide/references/forge-config-schema.json +5 -5
- package/adapters/cursor/skills/forge-guide/references/preflight-and-self-heal.md +183 -0
- package/adapters/cursor/skills/forge-guide/references/process-overview.md +10 -10
- package/adapters/cursor/skills/forge-guide/references/shared-conventions.md +188 -35
- package/adapters/cursor/skills/forge-guide/references/stack-resolution.md +1 -1
- package/adapters/cursor/skills/forge-init/forge-init.mdc +62 -12
- package/adapters/cursor/skills/forge-init/references/preflight-and-self-heal.md +183 -0
- package/adapters/cursor/skills/forge-init/references/shared-conventions.md +572 -0
- package/adapters/cursor/skills/forge-verify/forge-verify.mdc +27 -30
- package/adapters/cursor/skills/forge-verify/references/findings-template.md +2 -2
- package/adapters/cursor/skills/forge-verify/references/select-outcome.md +138 -0
- package/adapters/cursor/skills/forge-verify/references/shared-conventions.md +188 -35
- package/adapters/cursor/skills/forge-verify/references/stage-exit-protocol.md +22 -7
- package/adapters/cursor/skills/forge-verify/references/verification-checklists/epic.md +2 -2
- package/adapters/cursor/skills/forge-verify/references/verifier-patterns/MEMORY.md +35 -0
- package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_absence_claims.md +36 -0
- package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
- package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
- package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
- package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
- package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
- package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
- package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
- package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
- package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
- package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_self_referential_control.md +26 -0
- package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
- package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
- package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
- package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_test_count_units.md +20 -0
- package/adapters/cursor/skills/forge-verify/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
- package/adapters/gemini/.feature-forge-bundle.json +1 -1
- package/adapters/gemini/agents/forge-researcher.md +1 -1
- package/adapters/gemini/agents/forge-verifier.md +17 -6
- package/adapters/gemini/gemini-extension.json +13 -13
- package/adapters/gemini/references/forge-config-schema.json +5 -5
- package/adapters/gemini/references/portable-root.md +20 -16
- package/adapters/gemini/references/preflight-and-self-heal.md +183 -0
- package/adapters/gemini/references/process-overview.md +10 -10
- package/adapters/gemini/references/select-outcome.md +138 -0
- package/adapters/gemini/references/shared-conventions.md +188 -35
- package/adapters/gemini/references/skill-frontmatter.schema.json +3 -1
- package/adapters/gemini/references/stack-resolution.md +1 -1
- package/adapters/gemini/references/stage-exit-protocol.md +22 -7
- package/adapters/gemini/references/templates/root-hygiene/AGENTS.md +14 -0
- package/adapters/gemini/references/templates/root-hygiene/CLAUDE.md +14 -0
- package/adapters/gemini/references/templates/specs-hygiene/AGENTS.md +3 -1
- package/adapters/gemini/references/templates/specs-hygiene/CLAUDE.md +2 -1
- package/adapters/gemini/references/verifier-patterns/MEMORY.md +35 -0
- package/adapters/gemini/references/verifier-patterns/pattern_absence_claims.md +36 -0
- package/adapters/gemini/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
- package/adapters/gemini/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
- package/adapters/gemini/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
- package/adapters/gemini/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
- package/adapters/gemini/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
- package/adapters/gemini/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
- package/adapters/gemini/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
- package/adapters/gemini/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
- package/adapters/gemini/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
- package/adapters/gemini/references/verifier-patterns/pattern_self_referential_control.md +26 -0
- package/adapters/gemini/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
- package/adapters/gemini/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
- package/adapters/gemini/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
- package/adapters/gemini/references/verifier-patterns/pattern_test_count_units.md +20 -0
- package/adapters/gemini/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
- package/adapters/gemini/references/verify-state.md +98 -0
- package/adapters/gemini/scripts/forge-bootstrap.py +23 -2
- package/adapters/gemini/scripts/forge-root.sh +104 -21
- package/adapters/gemini/scripts/forge-session.py +620 -6973
- package/adapters/gemini/scripts/forge_session/__init__.py +11 -0
- package/adapters/gemini/scripts/forge_session/_common.py +1566 -0
- package/adapters/gemini/scripts/forge_session/_doctor_util.py +218 -0
- package/adapters/gemini/scripts/forge_session/cli.py +1067 -0
- package/adapters/gemini/scripts/forge_session/decisions.py +355 -0
- package/adapters/gemini/scripts/forge_session/discover.py +472 -0
- package/adapters/gemini/scripts/forge_session/doctor.py +2144 -0
- package/adapters/gemini/scripts/forge_session/exit.py +1081 -0
- package/adapters/gemini/scripts/forge_session/outcomes.py +574 -0
- package/adapters/gemini/scripts/forge_session/routes.py +1162 -0
- package/adapters/gemini/scripts/forge_session/state.py +1479 -0
- package/adapters/gemini/scripts/forge_session/topology.py +443 -0
- package/adapters/gemini/skills/forge/forge.md +40 -40
- package/adapters/gemini/skills/forge/references/process-overview.md +10 -10
- package/adapters/gemini/skills/forge/references/shared-conventions.md +188 -35
- package/adapters/gemini/skills/forge/references/stage-exit-protocol.md +22 -7
- package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +15 -15
- package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +14 -10
- package/adapters/gemini/skills/forge-0-epic/references/epic-manifest-subcommands.md +8 -3
- package/adapters/gemini/skills/forge-0-epic/references/portable-root.md +20 -16
- package/adapters/gemini/skills/forge-0-epic/references/shared-conventions.md +188 -35
- package/adapters/gemini/skills/forge-0-epic/references/stage-exit-protocol.md +22 -7
- package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +15 -15
- package/adapters/gemini/skills/forge-1-prd/references/shared-conventions.md +188 -35
- package/adapters/gemini/skills/forge-1-prd/references/stage-exit-protocol.md +22 -7
- package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +15 -15
- package/adapters/gemini/skills/forge-2-tech/references/shared-conventions.md +188 -35
- package/adapters/gemini/skills/forge-2-tech/references/stack-resolution.md +1 -1
- package/adapters/gemini/skills/forge-2-tech/references/stage-exit-protocol.md +22 -7
- package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +10 -10
- package/adapters/gemini/skills/forge-3-specs/references/shared-conventions.md +188 -35
- package/adapters/gemini/skills/forge-3-specs/references/stage-exit-protocol.md +22 -7
- package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +19 -11
- package/adapters/gemini/skills/forge-4-backlog/references/shared-conventions.md +188 -35
- package/adapters/gemini/skills/forge-4-backlog/references/stage-exit-protocol.md +22 -7
- package/adapters/gemini/skills/forge-4-backlog/references/verify-state.md +98 -0
- package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +48 -53
- package/adapters/gemini/skills/forge-5-loop/references/agent-selection.md +5 -1
- package/adapters/gemini/skills/forge-5-loop/references/preflight-and-self-heal.md +183 -0
- package/adapters/gemini/skills/forge-5-loop/references/recovery-procedure.md +14 -4
- package/adapters/gemini/skills/forge-5-loop/references/runner-contract.md +18 -8
- package/adapters/gemini/skills/forge-5-loop/references/shared-conventions.md +188 -35
- package/adapters/gemini/skills/forge-5-loop/references/stage-exit-protocol.md +22 -7
- package/adapters/gemini/skills/forge-5-loop/references/verify-state.md +98 -0
- package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +27 -20
- package/adapters/gemini/skills/forge-6-docs/references/shared-conventions.md +188 -35
- package/adapters/gemini/skills/forge-6-docs/references/stage-exit-protocol.md +22 -7
- package/adapters/gemini/skills/forge-6-docs/references/verify-state.md +98 -0
- package/adapters/gemini/skills/forge-bootstrap/forge-bootstrap.md +23 -15
- package/adapters/gemini/skills/forge-bootstrap/references/shared-conventions.md +572 -0
- package/adapters/gemini/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +2 -2
- package/adapters/gemini/skills/forge-fix/forge-fix.md +27 -30
- package/adapters/gemini/skills/forge-fix/references/select-outcome.md +138 -0
- package/adapters/gemini/skills/forge-fix/references/shared-conventions.md +188 -35
- package/adapters/gemini/skills/forge-fix/references/stage-exit-protocol.md +22 -7
- package/adapters/gemini/skills/forge-guide/forge-guide.md +84 -7
- package/adapters/gemini/skills/forge-guide/references/forge-config-schema.json +5 -5
- package/adapters/gemini/skills/forge-guide/references/preflight-and-self-heal.md +183 -0
- package/adapters/gemini/skills/forge-guide/references/process-overview.md +10 -10
- package/adapters/gemini/skills/forge-guide/references/shared-conventions.md +188 -35
- package/adapters/gemini/skills/forge-guide/references/stack-resolution.md +1 -1
- package/adapters/gemini/skills/forge-init/forge-init.md +62 -12
- package/adapters/gemini/skills/forge-init/references/preflight-and-self-heal.md +183 -0
- package/adapters/gemini/skills/forge-init/references/shared-conventions.md +572 -0
- package/adapters/gemini/skills/forge-verify/forge-verify.md +27 -30
- package/adapters/gemini/skills/forge-verify/references/findings-template.md +2 -2
- package/adapters/gemini/skills/forge-verify/references/select-outcome.md +138 -0
- package/adapters/gemini/skills/forge-verify/references/shared-conventions.md +188 -35
- package/adapters/gemini/skills/forge-verify/references/stage-exit-protocol.md +22 -7
- package/adapters/gemini/skills/forge-verify/references/verification-checklists/epic.md +2 -2
- package/adapters/gemini/skills/forge-verify/references/verifier-patterns/MEMORY.md +35 -0
- package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_absence_claims.md +36 -0
- package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
- package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
- package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
- package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
- package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
- package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
- package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
- package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
- package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
- package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_self_referential_control.md +26 -0
- package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
- package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
- package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
- package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_test_count_units.md +20 -0
- package/adapters/gemini/skills/forge-verify/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
- package/adapters/pi/.feature-forge-bundle.json +1 -1
- package/adapters/pi/agents/forge-verifier.md +16 -5
- package/adapters/pi/extensions/forge-invocation-args/README.md +106 -0
- package/adapters/pi/extensions/forge-invocation-args/index.ts +31 -0
- package/adapters/pi/extensions/forge-invocation-args/wiring.ts +172 -0
- package/adapters/pi/package.json +1 -0
- package/adapters/pi/references/forge-config-schema.json +3 -3
- package/adapters/pi/references/portable-root.md +20 -16
- package/adapters/pi/references/preflight-and-self-heal.md +183 -0
- package/adapters/pi/references/select-outcome.md +138 -0
- package/adapters/pi/references/shared-conventions.md +175 -22
- package/adapters/pi/references/skill-frontmatter.schema.json +3 -1
- package/adapters/pi/references/stack-resolution.md +1 -1
- package/adapters/pi/references/stage-exit-protocol.md +19 -4
- package/adapters/pi/references/templates/root-hygiene/AGENTS.md +14 -0
- package/adapters/pi/references/templates/root-hygiene/CLAUDE.md +14 -0
- package/adapters/pi/references/templates/specs-hygiene/AGENTS.md +3 -1
- package/adapters/pi/references/templates/specs-hygiene/CLAUDE.md +2 -1
- package/adapters/pi/references/verifier-patterns/MEMORY.md +35 -0
- package/adapters/pi/references/verifier-patterns/pattern_absence_claims.md +36 -0
- package/adapters/pi/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
- package/adapters/pi/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
- package/adapters/pi/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
- package/adapters/pi/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
- package/adapters/pi/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
- package/adapters/pi/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
- package/adapters/pi/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
- package/adapters/pi/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
- package/adapters/pi/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
- package/adapters/pi/references/verifier-patterns/pattern_self_referential_control.md +26 -0
- package/adapters/pi/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
- package/adapters/pi/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
- package/adapters/pi/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
- package/adapters/pi/references/verifier-patterns/pattern_test_count_units.md +20 -0
- package/adapters/pi/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
- package/adapters/pi/references/verify-state.md +98 -0
- package/adapters/pi/scripts/forge-bootstrap.py +23 -2
- package/adapters/pi/scripts/forge-root.sh +104 -21
- package/adapters/pi/scripts/forge-session.py +620 -6973
- package/adapters/pi/scripts/forge_session/__init__.py +11 -0
- package/adapters/pi/scripts/forge_session/_common.py +1566 -0
- package/adapters/pi/scripts/forge_session/_doctor_util.py +218 -0
- package/adapters/pi/scripts/forge_session/cli.py +1067 -0
- package/adapters/pi/scripts/forge_session/decisions.py +355 -0
- package/adapters/pi/scripts/forge_session/discover.py +472 -0
- package/adapters/pi/scripts/forge_session/doctor.py +2144 -0
- package/adapters/pi/scripts/forge_session/exit.py +1081 -0
- package/adapters/pi/scripts/forge_session/outcomes.py +574 -0
- package/adapters/pi/scripts/forge_session/routes.py +1162 -0
- package/adapters/pi/scripts/forge_session/state.py +1479 -0
- package/adapters/pi/scripts/forge_session/topology.py +443 -0
- package/adapters/pi/skills/forge/SKILL.md +9 -8
- package/adapters/pi/skills/forge/references/shared-conventions.md +175 -22
- package/adapters/pi/skills/forge/references/stage-exit-protocol.md +19 -4
- package/adapters/pi/skills/forge-0-epic/SKILL.md +9 -8
- package/adapters/pi/skills/forge-0-epic/references/edit-mode.md +9 -5
- package/adapters/pi/skills/forge-0-epic/references/epic-manifest-subcommands.md +6 -1
- package/adapters/pi/skills/forge-0-epic/references/portable-root.md +20 -16
- package/adapters/pi/skills/forge-0-epic/references/shared-conventions.md +175 -22
- package/adapters/pi/skills/forge-0-epic/references/stage-exit-protocol.md +19 -4
- package/adapters/pi/skills/forge-1-prd/SKILL.md +9 -8
- package/adapters/pi/skills/forge-1-prd/references/shared-conventions.md +175 -22
- package/adapters/pi/skills/forge-1-prd/references/stage-exit-protocol.md +19 -4
- package/adapters/pi/skills/forge-2-tech/SKILL.md +8 -7
- package/adapters/pi/skills/forge-2-tech/references/shared-conventions.md +175 -22
- package/adapters/pi/skills/forge-2-tech/references/stack-resolution.md +1 -1
- package/adapters/pi/skills/forge-2-tech/references/stage-exit-protocol.md +19 -4
- package/adapters/pi/skills/forge-3-specs/SKILL.md +7 -6
- package/adapters/pi/skills/forge-3-specs/references/shared-conventions.md +175 -22
- package/adapters/pi/skills/forge-3-specs/references/stage-exit-protocol.md +19 -4
- package/adapters/pi/skills/forge-4-backlog/SKILL.md +18 -9
- package/adapters/pi/skills/forge-4-backlog/references/shared-conventions.md +175 -22
- package/adapters/pi/skills/forge-4-backlog/references/stage-exit-protocol.md +19 -4
- package/adapters/pi/skills/forge-4-backlog/references/verify-state.md +98 -0
- package/adapters/pi/skills/forge-5-loop/SKILL.md +38 -42
- package/adapters/pi/skills/forge-5-loop/references/agent-selection.md +4 -0
- package/adapters/pi/skills/forge-5-loop/references/preflight-and-self-heal.md +183 -0
- package/adapters/pi/skills/forge-5-loop/references/recovery-procedure.md +10 -0
- package/adapters/pi/skills/forge-5-loop/references/runner-contract.md +15 -5
- package/adapters/pi/skills/forge-5-loop/references/shared-conventions.md +175 -22
- package/adapters/pi/skills/forge-5-loop/references/stage-exit-protocol.md +19 -4
- package/adapters/pi/skills/forge-5-loop/references/verify-state.md +98 -0
- package/adapters/pi/skills/forge-6-docs/SKILL.md +23 -15
- package/adapters/pi/skills/forge-6-docs/references/shared-conventions.md +175 -22
- package/adapters/pi/skills/forge-6-docs/references/stage-exit-protocol.md +19 -4
- package/adapters/pi/skills/forge-6-docs/references/verify-state.md +98 -0
- package/adapters/pi/skills/forge-bootstrap/SKILL.md +20 -11
- package/adapters/pi/skills/forge-bootstrap/references/shared-conventions.md +572 -0
- package/adapters/pi/skills/forge-fix/SKILL.md +17 -19
- package/adapters/pi/skills/forge-fix/references/select-outcome.md +138 -0
- package/adapters/pi/skills/forge-fix/references/shared-conventions.md +175 -22
- package/adapters/pi/skills/forge-fix/references/stage-exit-protocol.md +19 -4
- package/adapters/pi/skills/forge-guide/SKILL.md +79 -1
- package/adapters/pi/skills/forge-guide/references/forge-config-schema.json +3 -3
- package/adapters/pi/skills/forge-guide/references/preflight-and-self-heal.md +183 -0
- package/adapters/pi/skills/forge-guide/references/shared-conventions.md +175 -22
- package/adapters/pi/skills/forge-guide/references/stack-resolution.md +1 -1
- package/adapters/pi/skills/forge-init/SKILL.md +59 -8
- package/adapters/pi/skills/forge-init/references/preflight-and-self-heal.md +183 -0
- package/adapters/pi/skills/forge-init/references/shared-conventions.md +572 -0
- package/adapters/pi/skills/forge-verify/SKILL.md +17 -19
- package/adapters/pi/skills/forge-verify/references/findings-template.md +1 -1
- package/adapters/pi/skills/forge-verify/references/select-outcome.md +138 -0
- package/adapters/pi/skills/forge-verify/references/shared-conventions.md +175 -22
- package/adapters/pi/skills/forge-verify/references/stage-exit-protocol.md +19 -4
- package/adapters/pi/skills/forge-verify/references/verification-checklists/epic.md +1 -1
- package/adapters/pi/skills/forge-verify/references/verifier-patterns/MEMORY.md +35 -0
- package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_absence_claims.md +36 -0
- package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_carveout_sibling_semantics.md +34 -0
- package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_deviation_judgment.md +36 -0
- package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_enum_vocabulary_ripple.md +43 -0
- package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_extend_the_existing.md +31 -0
- package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_guard_substitution_detection.md +34 -0
- package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_mechanical_rewrite_damage.md +30 -0
- package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_postfix_reverify.md +82 -0
- package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_proximity_window_guards.md +26 -0
- package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_scratch_root_probes.md +40 -0
- package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_self_referential_control.md +26 -0
- package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_sibling_docstring_sweep.md +28 -0
- package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_spec_literals_are_claims.md +45 -0
- package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_stale_counts_after_split.md +36 -0
- package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_test_count_units.md +20 -0
- package/adapters/pi/skills/forge-verify/references/verifier-patterns/pattern_vacuous_self_reading_tests.md +32 -0
- package/dist/manifest.d.ts +1 -1
- package/dist/rauf.d.ts +3 -3
- package/dist/rauf.js +2 -2
- package/dist/source.d.ts +5 -2
- package/dist/source.js +5 -2
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,1479 @@
|
|
|
1
|
+
"""Write-side ``state-*`` verbs and their state-mutation machinery.
|
|
2
|
+
|
|
3
|
+
Every verb that MUTATES a feature's ``.pipeline-state.json`` (or an epic's
|
|
4
|
+
``.epic-state.json``) lives here, carved out of the ``forge-session.py`` monolith
|
|
5
|
+
(#279 P4.1): ``state-enter``, ``state-artifact``, ``state-complete``, ``state-skip``,
|
|
6
|
+
``state-branch``, ``state-note``, ``state-decision``, ``state-ecr`` and
|
|
7
|
+
``state-verify``, plus the fail-closed resolvers, the atomic writer, the verify-entry
|
|
8
|
+
builder and the one-line human printers they share. All verb names, flags, exit
|
|
9
|
+
codes and JSON shapes are FROZEN; ``tests/test_state_verbs.py`` and
|
|
10
|
+
``tests/test_auto_verify.py`` are the oracle.
|
|
11
|
+
|
|
12
|
+
This is a pure move. The write path keeps its atomic-write + commit discipline
|
|
13
|
+
(``_write_state``/``_commit_state`` from ``_common`` — a loop runner may crash
|
|
14
|
+
mid-write, so an interrupted write must never leave a partial state file). Shared
|
|
15
|
+
read-side primitives and the frozen domain constants come from ``_common`` (never
|
|
16
|
+
from the shim, which would be circular); the shim re-exports every symbol below so
|
|
17
|
+
the path-loaded test oracle resolves it unchanged.
|
|
18
|
+
|
|
19
|
+
3.10 baseline, Google-style docstrings, stdlib only — matching the conventions of
|
|
20
|
+
the monolith it was carved out of.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Final
|
|
29
|
+
|
|
30
|
+
from forge_session._common import (
|
|
31
|
+
EPIC_STATE_FILENAME,
|
|
32
|
+
FULL_GIT_HASH_RE,
|
|
33
|
+
MANIFEST_FILENAME,
|
|
34
|
+
PIPELINE_STATE_FILENAME,
|
|
35
|
+
PRODUCTION_STAGES,
|
|
36
|
+
SAFE_NAME_RE,
|
|
37
|
+
VERIFY_RESULT_STATUSES,
|
|
38
|
+
VERIFY_STAGES,
|
|
39
|
+
VERIFY_TOKEN_BY_STAGE,
|
|
40
|
+
UsageError,
|
|
41
|
+
_commit_state,
|
|
42
|
+
_DONE_STATUS,
|
|
43
|
+
_now_iso,
|
|
44
|
+
_SKIP_PROTECTED_PRIOR,
|
|
45
|
+
_stage_version,
|
|
46
|
+
_verify_entry,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def _resolve_feature_dir_for_write(
|
|
50
|
+
specs_dir: Path, feature: str, epic: str | None
|
|
51
|
+
) -> Path:
|
|
52
|
+
"""Fail-closed feature dir for the ``state-*`` WRITERS.
|
|
53
|
+
|
|
54
|
+
``_resolve_feature_dir`` is the reader's best-effort resolver: it returns the
|
|
55
|
+
flat ``{specsDir}/{feature}`` whenever that dir carries a state file, and
|
|
56
|
+
falls back to the flat literal on a multi-match. That tolerance was written
|
|
57
|
+
for ``stage-exit``, which is READ-ONLY — an unresolvable dir there just
|
|
58
|
+
downgrades to ``{}``. For a writer the same tolerance means a bare
|
|
59
|
+
``--feature api`` mutates a standalone ``{specsDir}/api/`` while an epic
|
|
60
|
+
member ``{specsDir}/{epic}/api/`` of the same name is silently left behind:
|
|
61
|
+
cross-feature state corruption at exit 0.
|
|
62
|
+
|
|
63
|
+
So the write path mirrors ``epic-manifest.py resolve`` — the canonical
|
|
64
|
+
resolver that produced ``{resolvedFeatureDir}`` in the first place, and which
|
|
65
|
+
rejects an ambiguous name with a structured ``ambiguous:`` finding. A writer
|
|
66
|
+
must not be more permissive than that resolver: more than one candidate
|
|
67
|
+
carrying a state file, with no explicit ``--epic``, is a hard stop.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
specs_dir: The configured specs directory (``--specs-dir``).
|
|
71
|
+
feature: The feature name (``--feature``).
|
|
72
|
+
epic: The owning epic name for a nested member, else None (``--epic``).
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
The resolved feature directory. With ``--epic`` the nested path is taken
|
|
76
|
+
verbatim; otherwise the single candidate carrying a state file, or the
|
|
77
|
+
flat path when none does (the first-write case).
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
UsageError: The bare name matches more than one directory carrying a
|
|
81
|
+
state file (→ exit 2, nothing written).
|
|
82
|
+
"""
|
|
83
|
+
if epic:
|
|
84
|
+
return specs_dir / epic / feature
|
|
85
|
+
flat = specs_dir / feature
|
|
86
|
+
candidates = [flat] if (flat / PIPELINE_STATE_FILENAME).is_file() else []
|
|
87
|
+
if specs_dir.is_dir():
|
|
88
|
+
candidates.extend(
|
|
89
|
+
sorted(
|
|
90
|
+
p
|
|
91
|
+
for p in specs_dir.glob(f"*/{feature}")
|
|
92
|
+
if (p / PIPELINE_STATE_FILENAME).is_file()
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
if len(candidates) > 1:
|
|
96
|
+
listed = ", ".join(str(p) for p in candidates)
|
|
97
|
+
raise UsageError(
|
|
98
|
+
f"ambiguous feature {feature!r}: {len(candidates)} directories carry a "
|
|
99
|
+
f"state file ({listed}) — pass --epic <epic> to name the one to write. "
|
|
100
|
+
f"Refusing to guess; nothing was written."
|
|
101
|
+
)
|
|
102
|
+
return candidates[0] if candidates else flat
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _load_state_for_write(
|
|
106
|
+
specs_dir: Path, feature: str, epic: str | None
|
|
107
|
+
) -> tuple[Path, dict]:
|
|
108
|
+
"""Resolve a feature's state path and load its current state for mutation.
|
|
109
|
+
|
|
110
|
+
Resolves through the fail-closed `_resolve_feature_dir_for_write`, NOT the
|
|
111
|
+
reader's tolerant `_resolve_feature_dir`. Deliberately does NOT
|
|
112
|
+
reuse `_read_state`: that reader downgrades a *corrupt* file to ``{}`` because
|
|
113
|
+
the navigator's read-only sweep can safely treat it as not-started. A writer
|
|
114
|
+
that inherited it would atomically replace a corrupt-but-recoverable state
|
|
115
|
+
file with a near-empty one at exit 0. So: absent -> ``{}``; present but
|
|
116
|
+
unparseable -> refuse, leaving the file byte-intact.
|
|
117
|
+
|
|
118
|
+
The verbs never create a feature directory; an unknown ``--feature`` is a
|
|
119
|
+
usage error, not a silent create.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
specs_dir: The configured specs directory (``--specs-dir``).
|
|
123
|
+
feature: The feature name (``--feature``).
|
|
124
|
+
epic: The owning epic name for a nested member, else None (``--epic``).
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
A ``(state_path, state)`` tuple. ``state`` is a schema-shaped shell when
|
|
128
|
+
no state file exists yet (see the seeding below).
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
UsageError: The bare ``feature`` name is ambiguous (more than one
|
|
132
|
+
candidate directory carries a state file and no ``--epic`` was
|
|
133
|
+
given), the feature directory does not exist, or the state file
|
|
134
|
+
exists but is not a JSON object (→ exit 2).
|
|
135
|
+
"""
|
|
136
|
+
state_dir = _resolve_feature_dir_for_write(specs_dir, feature, epic)
|
|
137
|
+
if not state_dir.is_dir():
|
|
138
|
+
raise UsageError(
|
|
139
|
+
f"no feature directory at {state_dir} — check --feature "
|
|
140
|
+
f"(and --epic for a nested epic member)"
|
|
141
|
+
)
|
|
142
|
+
state_path = state_dir / PIPELINE_STATE_FILENAME
|
|
143
|
+
if state_path.exists():
|
|
144
|
+
try:
|
|
145
|
+
state = json.loads(state_path.read_text(encoding="utf-8"))
|
|
146
|
+
except json.JSONDecodeError as exc:
|
|
147
|
+
raise UsageError(
|
|
148
|
+
f"{state_path} exists but is not valid JSON ({exc}); refusing to "
|
|
149
|
+
f"overwrite it. Fix or move the file, then re-run."
|
|
150
|
+
) from exc
|
|
151
|
+
if not isinstance(state, dict):
|
|
152
|
+
raise UsageError(
|
|
153
|
+
f"{state_path} is not a JSON object; refusing to overwrite it."
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
state = {}
|
|
157
|
+
|
|
158
|
+
# Seed the schema-required top-level fields for EVERY verb, not just
|
|
159
|
+
# state-enter. Branch Setup fires state-branch before the entry stamp
|
|
160
|
+
# (references/shared-conventions.md), so without this a first-write
|
|
161
|
+
# state-branch would persist {"branch": ..., "updatedAt": ...} — missing
|
|
162
|
+
# every required field — at exit 0. setdefault keeps existing state as-is.
|
|
163
|
+
# (`updatedAt`, the sixth required field, is stamped by _commit_state.)
|
|
164
|
+
state.setdefault("feature", feature)
|
|
165
|
+
state.setdefault("createdAt", _now_iso())
|
|
166
|
+
state.setdefault("pipelineStatus", "active")
|
|
167
|
+
state.setdefault("stages", {})
|
|
168
|
+
state.setdefault("currentStage", PRODUCTION_STAGES[0])
|
|
169
|
+
return state_path, state
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _assert_safe_name(name: str, label: str) -> None:
|
|
173
|
+
"""Reject a name that could steer a write outside ``{specsDir}/{name}``.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
name: The bare name supplied on the command line.
|
|
177
|
+
label: The flag to name in the error (e.g. ``--feature``).
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
UsageError: Empty, absolute, separator-bearing, ``..``, or not a single
|
|
181
|
+
kebab-case token (→ exit 2, nothing read or written).
|
|
182
|
+
"""
|
|
183
|
+
if (
|
|
184
|
+
not name
|
|
185
|
+
or name == ".."
|
|
186
|
+
or "/" in name
|
|
187
|
+
or "\\" in name
|
|
188
|
+
or os.path.isabs(name)
|
|
189
|
+
or not SAFE_NAME_RE.match(name)
|
|
190
|
+
):
|
|
191
|
+
raise UsageError(f"unsafe name {name!r} for {label}")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _load_epic_state_for_write(
|
|
195
|
+
specs_dir: Path, epic_name: str, epic: str | None
|
|
196
|
+
) -> tuple[Path, dict, int]:
|
|
197
|
+
"""Resolve an EPIC's ``.epic-state.json`` and its manifest revision, for mutation.
|
|
198
|
+
|
|
199
|
+
The epic counterpart of ``_load_state_for_write``, and deliberately NOT a
|
|
200
|
+
variant of it: epic verification is epic-scoped and must never resolve, read,
|
|
201
|
+
create, or write a member's ``.pipeline-state.json`` (REQ-SEC-01). There is no
|
|
202
|
+
fallback in either direction — an epic whose manifest is missing or whose
|
|
203
|
+
identity disagrees is an error, not a feature lookup.
|
|
204
|
+
|
|
205
|
+
Resolution is strict where the member resolver is tolerant: the name must be a
|
|
206
|
+
safe single token, the joined path must stay inside ``specs_dir`` after symlink
|
|
207
|
+
resolution, ``epic-manifest.json`` must exist, and the manifest's own ``epic``
|
|
208
|
+
value must equal ``epic_name``. The revision comes from the manifest, which is
|
|
209
|
+
the canonical artifact version for epic freshness — never a member's
|
|
210
|
+
production-stage version. A legacy manifest with no ``revision`` is
|
|
211
|
+
presented as logical ``1`` here, matching ``epic-manifest.py::load_manifest``,
|
|
212
|
+
and its bytes are not rewritten.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
specs_dir: The configured specs directory (``--specs-dir``).
|
|
216
|
+
epic_name: The epic name — what ``--feature`` carries for this stage.
|
|
217
|
+
epic: The ``--epic`` value, which must be absent or equal to ``epic_name``.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
A ``(state_path, state, revision)`` tuple. ``state`` is the lazily created
|
|
221
|
+
minimal shell (``epic`` + ``stages``) when no epic state exists yet.
|
|
222
|
+
|
|
223
|
+
Raises:
|
|
224
|
+
UsageError: Conflicting ``--feature``/``--epic``, unsafe name, containment
|
|
225
|
+
escape, missing/unparseable/non-object/identity-mismatched manifest,
|
|
226
|
+
invalid manifest revision, or an unparseable/non-object epic state or
|
|
227
|
+
``stages`` value (→ exit 2, nothing written).
|
|
228
|
+
"""
|
|
229
|
+
if epic is not None and epic != epic_name:
|
|
230
|
+
raise UsageError(
|
|
231
|
+
f"--stage forge-0-epic writes epic-scoped state, so --feature names the "
|
|
232
|
+
f"epic: --feature {epic_name!r} and --epic {epic!r} disagree. Drop --epic "
|
|
233
|
+
f"or make it match."
|
|
234
|
+
)
|
|
235
|
+
_assert_safe_name(epic_name, "--feature")
|
|
236
|
+
base_real = specs_dir.resolve()
|
|
237
|
+
epic_dir = (base_real / epic_name).resolve()
|
|
238
|
+
if epic_dir != base_real and base_real not in epic_dir.parents:
|
|
239
|
+
raise UsageError(
|
|
240
|
+
f"resolved epic path escapes the specs dir: {specs_dir / epic_name}"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
manifest_path = epic_dir / MANIFEST_FILENAME
|
|
244
|
+
if not manifest_path.is_file():
|
|
245
|
+
raise UsageError(
|
|
246
|
+
f"no epic manifest at {manifest_path} — --stage forge-0-epic verifies an "
|
|
247
|
+
f"epic, and {epic_name!r} is not one. Nothing was written."
|
|
248
|
+
)
|
|
249
|
+
try:
|
|
250
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
251
|
+
except json.JSONDecodeError as exc:
|
|
252
|
+
raise UsageError(f"{manifest_path} is not valid JSON ({exc})") from exc
|
|
253
|
+
if not isinstance(manifest, dict):
|
|
254
|
+
raise UsageError(f"{manifest_path} is not a JSON object")
|
|
255
|
+
if manifest.get("epic") != epic_name:
|
|
256
|
+
raise UsageError(
|
|
257
|
+
f"{manifest_path} declares epic {manifest.get('epic')!r}, not "
|
|
258
|
+
f"{epic_name!r}; refusing to write verification state for a mismatched "
|
|
259
|
+
f"epic identity"
|
|
260
|
+
)
|
|
261
|
+
revision = _require_positive_int(
|
|
262
|
+
manifest.get("revision", 1), f"{epic_name}/{MANIFEST_FILENAME} revision"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
state_path = epic_dir / EPIC_STATE_FILENAME
|
|
266
|
+
if state_path.exists():
|
|
267
|
+
try:
|
|
268
|
+
state = json.loads(state_path.read_text(encoding="utf-8"))
|
|
269
|
+
except json.JSONDecodeError as exc:
|
|
270
|
+
raise UsageError(
|
|
271
|
+
f"{state_path} exists but is not valid JSON ({exc}); refusing to "
|
|
272
|
+
f"overwrite it. Fix or move the file, then re-run."
|
|
273
|
+
) from exc
|
|
274
|
+
if not isinstance(state, dict):
|
|
275
|
+
raise UsageError(
|
|
276
|
+
f"{state_path} is not a JSON object; refusing to overwrite it."
|
|
277
|
+
)
|
|
278
|
+
recorded = state.get("epic")
|
|
279
|
+
if recorded is not None and recorded != epic_name:
|
|
280
|
+
raise UsageError(
|
|
281
|
+
f"{state_path} records epic {recorded!r}, not {epic_name!r}; "
|
|
282
|
+
f"refusing to overwrite it."
|
|
283
|
+
)
|
|
284
|
+
stages = state.get("stages")
|
|
285
|
+
if stages is not None and not isinstance(stages, dict):
|
|
286
|
+
raise UsageError(
|
|
287
|
+
f"{state_path} has a non-object 'stages' value ({type(stages).__name__}); "
|
|
288
|
+
f"refusing to overwrite it."
|
|
289
|
+
)
|
|
290
|
+
else:
|
|
291
|
+
state = {}
|
|
292
|
+
# Seed the minimal state shape in its documented key order. ``updatedAt`` is
|
|
293
|
+
# a placeholder: every caller stamps it through ``_commit_state`` immediately
|
|
294
|
+
# before the single atomic replacement, so the null never reaches disk.
|
|
295
|
+
state.setdefault("epic", epic_name)
|
|
296
|
+
state.setdefault("updatedAt", None)
|
|
297
|
+
state.setdefault("stages", {})
|
|
298
|
+
return state_path, state, revision
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _stage_entry(state: dict, stage: str) -> dict:
|
|
302
|
+
"""Return (creating if absent) the mutable ``stages.{stage}`` sub-object.
|
|
303
|
+
|
|
304
|
+
Bootstraps ``state["stages"]`` and ``state["stages"][stage]`` when missing, so
|
|
305
|
+
a verb can write into a brand-new state (``{}``), and returns the stage dict
|
|
306
|
+
for in-place mutation. The bootstrap seeds ``{"status": "pending"}`` rather
|
|
307
|
+
than ``{}`` because ``stageEntry`` declares ``required: ["status"]`` — an entry
|
|
308
|
+
created by state-artifact (which sets only ``artifacts``) would otherwise be
|
|
309
|
+
schema-invalid at exit 0.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
state: The full state dict (mutated in place).
|
|
313
|
+
stage: A stage id from ``STATE_VERB_STAGES`` (e.g. ``"forge-1-prd"``).
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
The mutable ``stages.{stage}`` dict.
|
|
317
|
+
"""
|
|
318
|
+
stages = state.setdefault("stages", {})
|
|
319
|
+
return stages.setdefault(stage, {"status": "pending"})
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# --------------------------------------------------------------------------- #
|
|
323
|
+
# State-write verbs
|
|
324
|
+
# --------------------------------------------------------------------------- #
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def cmd_state_enter(feature: str, stage: str, specs_dir: Path, epic: str | None) -> dict:
|
|
328
|
+
"""Apply the Entry Stamp: mark ``stage`` in-progress and set ``currentStage``.
|
|
329
|
+
|
|
330
|
+
Idempotent on re-entry within the same run: re-stamping an already
|
|
331
|
+
in-progress stage simply refreshes ``startedAt``/``updatedAt``. The
|
|
332
|
+
interactive resume-vs-restart decision stays the skill's — the verb never
|
|
333
|
+
prompts. The write is left uncommitted; the stage's existing exit commit
|
|
334
|
+
stages it later.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
feature: Feature name.
|
|
338
|
+
stage: The stage being entered (a ``STATE_VERB_STAGES`` id).
|
|
339
|
+
specs_dir: Specs directory.
|
|
340
|
+
epic: Owning epic name, or None.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
The mutated state dict (for the --json echo).
|
|
344
|
+
|
|
345
|
+
Raises:
|
|
346
|
+
UsageError: Unknown feature directory, unparseable state file, or a
|
|
347
|
+
failed atomic write (→ exit 2).
|
|
348
|
+
"""
|
|
349
|
+
state_path, state = _load_state_for_write(specs_dir, feature, epic)
|
|
350
|
+
entry = _stage_entry(state, stage)
|
|
351
|
+
entry["status"] = "in-progress"
|
|
352
|
+
entry["startedAt"] = _now_iso()
|
|
353
|
+
state["currentStage"] = stage
|
|
354
|
+
return _commit_state(state_path, state)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def cmd_state_artifact(
|
|
358
|
+
feature: str, stage: str, paths: list[str], specs_dir: Path, epic: str | None
|
|
359
|
+
) -> dict:
|
|
360
|
+
"""Append each path in ``paths`` to ``stages.{stage}.artifacts``, de-duplicating.
|
|
361
|
+
|
|
362
|
+
Idempotent: an already-tracked path is a no-op (no duplicate append), so a
|
|
363
|
+
resumed run that re-records files it wrote earlier does not bloat the array.
|
|
364
|
+
``updatedAt`` is refreshed even on the all-duplicates branch, keeping "state
|
|
365
|
+
was touched" honest. The verb does NOT stat the file — it records the path
|
|
366
|
+
the skill asserts it wrote.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
feature: Feature name.
|
|
370
|
+
stage: The producing stage id.
|
|
371
|
+
paths: Artifact paths relative to the feature dir (repeatable ``--path``).
|
|
372
|
+
specs_dir: Specs directory.
|
|
373
|
+
epic: Owning epic name, or None.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
The mutated state dict (for the --json echo).
|
|
377
|
+
|
|
378
|
+
Raises:
|
|
379
|
+
UsageError: A ``--path`` that is empty, absolute, ``..``-bearing,
|
|
380
|
+
control-character-bearing, or escaping the feature directory; an
|
|
381
|
+
unknown feature directory, an unparseable state file, or a failed
|
|
382
|
+
atomic write (→ exit 2).
|
|
383
|
+
"""
|
|
384
|
+
state_path, state = _load_state_for_write(specs_dir, feature, epic)
|
|
385
|
+
# Containment is checked against the resolved feature dir, which only the load
|
|
386
|
+
# produces; every path is validated before any of them is appended, so a
|
|
387
|
+
# rejected value in a repeated --path list leaves the file untouched.
|
|
388
|
+
target_dir = state_path.parent
|
|
389
|
+
for path in paths:
|
|
390
|
+
_validated_findings_file(path, target_dir, label="--path")
|
|
391
|
+
entry = _stage_entry(state, stage)
|
|
392
|
+
artifacts = entry.setdefault("artifacts", [])
|
|
393
|
+
for path in paths:
|
|
394
|
+
if path not in artifacts:
|
|
395
|
+
artifacts.append(path)
|
|
396
|
+
return _commit_state(state_path, state)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _parse_based_on(pairs: list[str]) -> dict[str, int]:
|
|
400
|
+
"""Parse ``--based-on STAGE=N`` tokens into a ``{stageId: int}`` map.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
pairs: Raw ``STAGE=N`` strings from repeated ``--based-on`` flags.
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
A ``{stageId: version}`` dict (empty when no pairs were given — the
|
|
407
|
+
forge-1-prd case, which records ``basedOnVersions == {}``).
|
|
408
|
+
|
|
409
|
+
Raises:
|
|
410
|
+
UsageError: If a token lacks ``=`` or its value is not an integer
|
|
411
|
+
(→ exit 2).
|
|
412
|
+
"""
|
|
413
|
+
out: dict[str, int] = {}
|
|
414
|
+
for token in pairs:
|
|
415
|
+
if "=" not in token:
|
|
416
|
+
raise UsageError(f"--based-on expects STAGE=N, got: {token!r}")
|
|
417
|
+
stage_id, _, raw = token.partition("=")
|
|
418
|
+
try:
|
|
419
|
+
out[stage_id] = int(raw)
|
|
420
|
+
except ValueError as exc:
|
|
421
|
+
raise UsageError(f"--based-on version must be an integer: {token!r}") from exc
|
|
422
|
+
return out
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
#: Stages the staleness cascade may mark stale (downstream authored artifacts).
|
|
426
|
+
#: The scope is tech..docs, matching the pre-R4 canon this cascade replaces —
|
|
427
|
+
#: forge-1-prd L134 named `forge-2-tech` FIRST among the stages a PRD revision
|
|
428
|
+
#: invalidates, and the tech spec is a PRD revision's most direct dependent.
|
|
429
|
+
#: forge-1-prd is never marked stale by a later completion (nothing downstream
|
|
430
|
+
#: feeds back into it). Keyed off this map, NOT off PRODUCTION_STAGES ordering —
|
|
431
|
+
#: the two are not interchangeable (a positional slice from the completing stage
|
|
432
|
+
#: would also break on forge-0-epic, which is a valid --stage but not a
|
|
433
|
+
#: PRODUCTION_STAGES member).
|
|
434
|
+
_CASCADE_TARGETS: Final[tuple[str, ...]] = (
|
|
435
|
+
"forge-2-tech",
|
|
436
|
+
"forge-3-specs",
|
|
437
|
+
"forge-4-backlog",
|
|
438
|
+
"forge-5-loop",
|
|
439
|
+
"forge-6-docs",
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _cascade_staleness(state: dict, completed_stage: str, new_version: int) -> list[str]:
|
|
444
|
+
"""Mark downstream stages ``stale`` when they were built on an OLDER version.
|
|
445
|
+
|
|
446
|
+
Deterministic replacement for the model-prose rule in each stage's completion
|
|
447
|
+
step ("if any downstream stage has basedOnVersions referencing an older
|
|
448
|
+
version, set its status to stale"). For every downstream target (tech..docs),
|
|
449
|
+
if its recorded ``basedOnVersions[completed_stage]`` is an integer strictly
|
|
450
|
+
less than ``new_version`` AND the stage is currently ``complete``, flip it to
|
|
451
|
+
``stale``. A downstream stage that never referenced this upstream, or already
|
|
452
|
+
references the new version, is untouched. A ``pending``/``in-progress``/
|
|
453
|
+
already-``stale`` downstream stage is not re-flipped — only a ``complete``
|
|
454
|
+
artifact can go stale.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
state: The full state dict (mutated in place).
|
|
458
|
+
completed_stage: The stage that just completed (e.g. "forge-1-prd").
|
|
459
|
+
new_version: That stage's new version.
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
The list of stage ids newly marked stale (for the --json echo / printer).
|
|
463
|
+
"""
|
|
464
|
+
stages = state.get("stages", {})
|
|
465
|
+
newly_stale: list[str] = []
|
|
466
|
+
for target in _CASCADE_TARGETS:
|
|
467
|
+
if target == completed_stage:
|
|
468
|
+
continue
|
|
469
|
+
entry = stages.get(target)
|
|
470
|
+
if not isinstance(entry, dict) or entry.get("status") != "complete":
|
|
471
|
+
continue
|
|
472
|
+
based_on = entry.get("basedOnVersions")
|
|
473
|
+
if not isinstance(based_on, dict):
|
|
474
|
+
continue
|
|
475
|
+
recorded = based_on.get(completed_stage)
|
|
476
|
+
if isinstance(recorded, int) and not isinstance(recorded, bool) and recorded < new_version:
|
|
477
|
+
entry["status"] = "stale"
|
|
478
|
+
newly_stale.append(target)
|
|
479
|
+
return newly_stale
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def cmd_state_complete(
|
|
483
|
+
feature: str,
|
|
484
|
+
stage: str,
|
|
485
|
+
version: int,
|
|
486
|
+
based_on: dict[str, int],
|
|
487
|
+
artifacts: list[str],
|
|
488
|
+
commit_hash: str | None,
|
|
489
|
+
specs_dir: Path,
|
|
490
|
+
epic: str | None,
|
|
491
|
+
status: str | None = None,
|
|
492
|
+
preserve_commit_hash: bool = False,
|
|
493
|
+
resumable: bool = False,
|
|
494
|
+
) -> dict:
|
|
495
|
+
"""Mark ``stage`` complete, bump version, record provenance, cascade staleness.
|
|
496
|
+
|
|
497
|
+
Three branches, in precedence order:
|
|
498
|
+
|
|
499
|
+
1. ``commit_hash`` given — Commit 2 of the two-commit Git Commit Protocol.
|
|
500
|
+
Sets ONLY ``commitHash``, leaving status/version/artifacts intact. Guarded
|
|
501
|
+
on the stage already being ``complete``, so a typo'd ``--stage`` cannot
|
|
502
|
+
write a lone ``{"commitHash": …}`` entry (which would violate
|
|
503
|
+
``stageEntry``'s ``required: ["status"]``) at exit 0. The value must be a
|
|
504
|
+
full 40-hex object hash (REQ-STATE-01), checked before anything is loaded.
|
|
505
|
+
2. ``resumable`` — the failed-Commit-1 revert (`references/shared-conventions.md`
|
|
506
|
+
L245). Records ONLY ``status = "in-progress"`` plus the ``updatedAt``
|
|
507
|
+
refresh: no completedAt, no version bump, no basedOnVersions/artifacts
|
|
508
|
+
write, no commitHash reset, no cascade. The frozen contract is "leave state
|
|
509
|
+
as in-progress so the stage can be resumed"; stamping a completion, bumping
|
|
510
|
+
the version, or cascading staleness off a commit that never landed are all
|
|
511
|
+
behavioral changes.
|
|
512
|
+
3. Otherwise — the completion write: status, completedAt, version,
|
|
513
|
+
basedOnVersions, artifacts, ``commitHash = None`` (Commit 1) unless
|
|
514
|
+
``preserve_commit_hash``, then the downstream staleness cascade.
|
|
515
|
+
|
|
516
|
+
Branch 2 is gated on ``resumable``, NOT on ``status == "in-progress"``:
|
|
517
|
+
forge-5-loop's PARTIAL completion also passes ``--status in-progress`` but is a
|
|
518
|
+
real completion-with-artifacts, so it takes branch 3 and keeps its
|
|
519
|
+
completedAt/version/basedOnVersions/artifacts. Only ``status`` differs between
|
|
520
|
+
``--status complete`` and a bare ``--status in-progress``. Conflating the two
|
|
521
|
+
would silently discard the ``--based-on`` item 013 passes on that call.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
feature: Feature name.
|
|
525
|
+
stage: The completing stage id.
|
|
526
|
+
version: The stage's new version.
|
|
527
|
+
based_on: Parsed ``{upstreamStage: version}`` provenance map.
|
|
528
|
+
artifacts: Final canonical artifact path list for this stage.
|
|
529
|
+
commit_hash: If given, record it as the stage's commitHash (Commit 2);
|
|
530
|
+
else set commitHash to None (Commit 1). Full 40-hex only on a new
|
|
531
|
+
write — an abbreviation is rejected rather than expanded.
|
|
532
|
+
specs_dir: Specs directory.
|
|
533
|
+
epic: Owning epic name, or None.
|
|
534
|
+
status: Terminal status to record — "complete" (the default when the flag
|
|
535
|
+
is absent) or "in-progress" for a partial forge-5-loop run. ``None``
|
|
536
|
+
means "not passed".
|
|
537
|
+
preserve_commit_hash: Skip the ``commitHash = None`` reset, for the Git
|
|
538
|
+
Commit Protocol's "Nothing to commit" branch (L248).
|
|
539
|
+
resumable: Failed-Commit-1 revert (L245). Record only the status; implies
|
|
540
|
+
``--status in-progress``.
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
The mutated state dict, plus a synthetic ``_cascadedStale`` key that is
|
|
544
|
+
surfaced in the --json echo / printer but NEVER written to disk.
|
|
545
|
+
|
|
546
|
+
Raises:
|
|
547
|
+
UsageError: Contradictory ``--resumable --status complete``, a
|
|
548
|
+
``--version`` below 1, a short or non-hex ``--commit-hash``, a
|
|
549
|
+
``--commit-hash`` follow-up against a stage that is not complete, an
|
|
550
|
+
unknown feature directory, an unparseable state file, or a failed
|
|
551
|
+
atomic write (→ exit 2).
|
|
552
|
+
"""
|
|
553
|
+
if resumable and status == "complete":
|
|
554
|
+
raise UsageError(
|
|
555
|
+
"--resumable implies --status in-progress; do not pass --status complete"
|
|
556
|
+
)
|
|
557
|
+
# The write path must not accept a version the read path refuses; checked before
|
|
558
|
+
# the state file is loaded for mutation, so a rejection touches nothing.
|
|
559
|
+
_require_positive_int(version, "--version")
|
|
560
|
+
if commit_hash is not None:
|
|
561
|
+
# Branch 1's first act: full 40-hex only, validated BEFORE the
|
|
562
|
+
# state file is loaded for mutation and long before _commit_state. Legacy
|
|
563
|
+
# short hashes already recorded in state keep loading unmigrated.
|
|
564
|
+
_assert_full_commit_hash(commit_hash)
|
|
565
|
+
state_path, state = _load_state_for_write(specs_dir, feature, epic)
|
|
566
|
+
entry = _stage_entry(state, stage)
|
|
567
|
+
cascaded: list[str] = []
|
|
568
|
+
if commit_hash is not None:
|
|
569
|
+
# Commit-2 follow-up: record the real hash, leave everything else intact.
|
|
570
|
+
actual = entry.get("status")
|
|
571
|
+
if actual != _DONE_STATUS:
|
|
572
|
+
raise UsageError(
|
|
573
|
+
f"--commit-hash requires {stage} to be complete (status: {actual!r}); "
|
|
574
|
+
"run state-complete without --commit-hash first"
|
|
575
|
+
)
|
|
576
|
+
entry["commitHash"] = commit_hash
|
|
577
|
+
elif resumable:
|
|
578
|
+
# Failed-Commit-1 revert (L245): record ONLY the status. See the note above
|
|
579
|
+
# on why this is gated on --resumable rather than on the status value.
|
|
580
|
+
entry["status"] = "in-progress"
|
|
581
|
+
else:
|
|
582
|
+
entry["status"] = status or _DONE_STATUS # "complete" | "in-progress" (partial)
|
|
583
|
+
entry["completedAt"] = _now_iso()
|
|
584
|
+
entry["version"] = version
|
|
585
|
+
entry["basedOnVersions"] = based_on
|
|
586
|
+
entry["artifacts"] = artifacts
|
|
587
|
+
if not preserve_commit_hash:
|
|
588
|
+
entry["commitHash"] = None # Commit 1 of the Commit Protocol
|
|
589
|
+
cascaded = _cascade_staleness(state, stage, version)
|
|
590
|
+
result = _commit_state(state_path, state)
|
|
591
|
+
# Surface the cascade result for the caller without persisting it in state:
|
|
592
|
+
# _commit_state already wrote the real dict, and `echo` is a copy.
|
|
593
|
+
echo = dict(result)
|
|
594
|
+
echo["_cascadedStale"] = cascaded
|
|
595
|
+
return echo
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def cmd_state_skip(feature: str, stage: str, specs_dir: Path, epic: str | None) -> dict:
|
|
599
|
+
"""Record a deliberate skip of the documentation stage (#197).
|
|
600
|
+
|
|
601
|
+
Writes a REPLACEMENT ``stages.forge-6-docs`` entry ``{"status": "skipped",
|
|
602
|
+
"skippedAt": …, "commitHash": null}`` — the honest terminal for a feature
|
|
603
|
+
that ships without architecture docs. ``skipped`` counts as done for
|
|
604
|
+
next-stage selection (``_DONE_STATUSES``), so the pipeline reads
|
|
605
|
+
``complete: true`` / ``nextStage: null`` without any stage claiming
|
|
606
|
+
artifacts it never produced.
|
|
607
|
+
|
|
608
|
+
Scoped to ``forge-6-docs`` on purpose: a skipped PRD or specs stage is a
|
|
609
|
+
different and much worse proposition, so both the CLI (``choices``) and this
|
|
610
|
+
callable refuse any other stage.
|
|
611
|
+
|
|
612
|
+
The skip must not DESTROY a record of docs that exist: a prior entry whose
|
|
613
|
+
``artifacts`` list is non-empty is refused. A prior ``complete`` entry with
|
|
614
|
+
no recorded artifacts is exactly the dishonest workaround this verb replaces
|
|
615
|
+
(``state-complete`` with no ``--artifact``), so it may be corrected to
|
|
616
|
+
``skipped`` — that is the sanctioned migration path for such state.
|
|
617
|
+
|
|
618
|
+
Args:
|
|
619
|
+
feature: Feature name.
|
|
620
|
+
stage: Must be ``"forge-6-docs"`` (kept explicit so the scoping shows up
|
|
621
|
+
in every call site).
|
|
622
|
+
specs_dir: Specs directory.
|
|
623
|
+
epic: Owning epic name, or None.
|
|
624
|
+
|
|
625
|
+
Returns:
|
|
626
|
+
The mutated state dict (for the --json echo).
|
|
627
|
+
|
|
628
|
+
Raises:
|
|
629
|
+
UsageError: A stage other than forge-6-docs, a prior entry recording
|
|
630
|
+
artifacts, an unknown feature directory, an unparseable state file,
|
|
631
|
+
or a failed atomic write (→ exit 2).
|
|
632
|
+
"""
|
|
633
|
+
if stage != "forge-6-docs":
|
|
634
|
+
raise UsageError(
|
|
635
|
+
f"state-skip is scoped to forge-6-docs; a skipped {stage} is not a "
|
|
636
|
+
"representable pipeline state"
|
|
637
|
+
)
|
|
638
|
+
state_path, state = _load_state_for_write(specs_dir, feature, epic)
|
|
639
|
+
prior = state.get("stages", {}).get(stage)
|
|
640
|
+
if isinstance(prior, dict) and prior.get("artifacts"):
|
|
641
|
+
raise UsageError(
|
|
642
|
+
f"{stage} already records {len(prior['artifacts'])} artifact(s) "
|
|
643
|
+
f"(status: {prior.get('status')!r}); skipping now would erase the "
|
|
644
|
+
"record that docs exist. Re-run forge-6-docs to refresh them instead."
|
|
645
|
+
)
|
|
646
|
+
state.setdefault("stages", {})[stage] = {
|
|
647
|
+
"status": "skipped",
|
|
648
|
+
"skippedAt": _now_iso(),
|
|
649
|
+
"commitHash": None,
|
|
650
|
+
}
|
|
651
|
+
return _commit_state(state_path, state)
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def cmd_state_branch(feature: str, branch: str, specs_dir: Path, epic: str | None) -> dict:
|
|
655
|
+
"""Set the top-level ``branch`` field.
|
|
656
|
+
|
|
657
|
+
Records the branch resolved by Branch Setup / Branch Reconciliation. The verb
|
|
658
|
+
only writes the field; the interactive prompts and the visible one-line
|
|
659
|
+
reconciliation note stay unchanged skill prose.
|
|
660
|
+
|
|
661
|
+
Branch Setup fires before the Entry Stamp, so this verb can legitimately be
|
|
662
|
+
the FIRST thing to touch a feature's state file — `_load_state_for_write`'s
|
|
663
|
+
field seeding is what keeps that first write schema-valid.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
feature: Feature name.
|
|
667
|
+
branch: The branch name to record.
|
|
668
|
+
specs_dir: Specs directory.
|
|
669
|
+
epic: Owning epic name, or None.
|
|
670
|
+
|
|
671
|
+
Returns:
|
|
672
|
+
The mutated state dict (for the --json echo).
|
|
673
|
+
|
|
674
|
+
Raises:
|
|
675
|
+
UsageError: Unknown feature directory, unparseable state file, or a
|
|
676
|
+
failed atomic write (→ exit 2).
|
|
677
|
+
"""
|
|
678
|
+
state_path, state = _load_state_for_write(specs_dir, feature, epic)
|
|
679
|
+
state["branch"] = branch
|
|
680
|
+
return _commit_state(state_path, state)
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def cmd_state_note(feature: str, note: str, specs_dir: Path, epic: str | None) -> dict:
|
|
684
|
+
"""Set the top-level ``notes`` field to ``note``.
|
|
685
|
+
|
|
686
|
+
Overwrites any existing note (the field is a single free-text string, not an
|
|
687
|
+
append log — matching the schema's ``notes: string``). The skill's "offer a
|
|
688
|
+
note — don't force one" statement is unchanged; this verb runs only when the
|
|
689
|
+
user volunteered text.
|
|
690
|
+
|
|
691
|
+
Args:
|
|
692
|
+
feature: Feature name.
|
|
693
|
+
note: The note text.
|
|
694
|
+
specs_dir: Specs directory.
|
|
695
|
+
epic: Owning epic name, or None.
|
|
696
|
+
|
|
697
|
+
Returns:
|
|
698
|
+
The mutated state dict (for the --json echo).
|
|
699
|
+
|
|
700
|
+
Raises:
|
|
701
|
+
UsageError: Unknown feature directory, unparseable state file, or a
|
|
702
|
+
failed atomic write (→ exit 2).
|
|
703
|
+
"""
|
|
704
|
+
state_path, state = _load_state_for_write(specs_dir, feature, epic)
|
|
705
|
+
state["notes"] = note
|
|
706
|
+
return _commit_state(state_path, state)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def cmd_state_decision(
|
|
710
|
+
feature: str,
|
|
711
|
+
question: str,
|
|
712
|
+
raised_by: str,
|
|
713
|
+
rationale: str | None,
|
|
714
|
+
target_stage: str | None,
|
|
715
|
+
specs_dir: Path,
|
|
716
|
+
epic: str | None,
|
|
717
|
+
) -> dict:
|
|
718
|
+
"""Append an open deferred-decision item to ``deferredDecisions[]``.
|
|
719
|
+
|
|
720
|
+
Emits exactly the schema keys — the array item sets
|
|
721
|
+
``additionalProperties: false``, so a convenience field is a hard validation
|
|
722
|
+
failure: required ``question``/``raisedBy``/``raisedAt``/``status``, plus
|
|
723
|
+
``rationale``/``targetStage`` only when provided. ``status`` is always
|
|
724
|
+
``"open"``; the recorder never resolves a decision (the target stage flips it
|
|
725
|
+
to ``"addressed"``).
|
|
726
|
+
|
|
727
|
+
Args:
|
|
728
|
+
feature: Feature name.
|
|
729
|
+
question: The deferred decision, phrased for the target stage.
|
|
730
|
+
raised_by: The deferring stage id.
|
|
731
|
+
rationale: Optional reason for deferring.
|
|
732
|
+
target_stage: Optional resolving stage id.
|
|
733
|
+
specs_dir: Specs directory.
|
|
734
|
+
epic: Owning epic name, or None.
|
|
735
|
+
|
|
736
|
+
Returns:
|
|
737
|
+
The mutated state dict (for the --json echo).
|
|
738
|
+
|
|
739
|
+
Raises:
|
|
740
|
+
UsageError: Unknown feature directory, unparseable state file, or a
|
|
741
|
+
failed atomic write (→ exit 2).
|
|
742
|
+
"""
|
|
743
|
+
state_path, state = _load_state_for_write(specs_dir, feature, epic)
|
|
744
|
+
item: dict = {
|
|
745
|
+
"question": question,
|
|
746
|
+
"raisedBy": raised_by,
|
|
747
|
+
"raisedAt": _now_iso(),
|
|
748
|
+
"status": "open",
|
|
749
|
+
}
|
|
750
|
+
if rationale is not None:
|
|
751
|
+
item["rationale"] = rationale
|
|
752
|
+
if target_stage is not None:
|
|
753
|
+
item["targetStage"] = target_stage
|
|
754
|
+
state.setdefault("deferredDecisions", []).append(item)
|
|
755
|
+
return _commit_state(state_path, state)
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def _parse_bool(raw: str, flag: str) -> bool:
|
|
759
|
+
"""Parse an explicit boolean CLI value; fail closed on anything else.
|
|
760
|
+
|
|
761
|
+
Args:
|
|
762
|
+
raw: The raw flag value (e.g. from ``--blocks-current``).
|
|
763
|
+
flag: The flag name, for the error message.
|
|
764
|
+
|
|
765
|
+
Returns:
|
|
766
|
+
``True`` for ``"true"``, ``False`` for ``"false"`` (case-insensitive,
|
|
767
|
+
surrounding whitespace ignored).
|
|
768
|
+
|
|
769
|
+
Raises:
|
|
770
|
+
UsageError: For any other value (→ exit 2), so a typo like ``"yes"`` is
|
|
771
|
+
rejected rather than silently misrouting the stage exit.
|
|
772
|
+
"""
|
|
773
|
+
normalized = raw.strip().lower()
|
|
774
|
+
if normalized == "true":
|
|
775
|
+
return True
|
|
776
|
+
if normalized == "false":
|
|
777
|
+
return False
|
|
778
|
+
raise UsageError(f"{flag} expects true|false, got: {raw!r}")
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def cmd_state_ecr(
|
|
782
|
+
feature: str,
|
|
783
|
+
kind: str,
|
|
784
|
+
target: str,
|
|
785
|
+
rationale: str,
|
|
786
|
+
raised_by: str,
|
|
787
|
+
blocks_current: bool,
|
|
788
|
+
specs_dir: Path,
|
|
789
|
+
epic: str | None,
|
|
790
|
+
) -> dict:
|
|
791
|
+
"""Append an open epic-change-request item to ``epicChangeRequests[]``.
|
|
792
|
+
|
|
793
|
+
Emits exactly the schema keys — the array item sets
|
|
794
|
+
``additionalProperties: false``, so a convenience field is a hard validation
|
|
795
|
+
failure. All six payload fields are required, and ``status`` is always
|
|
796
|
+
``"open"`` (only forge-0-epic edit mode flips it). ``blocksCurrent`` drives
|
|
797
|
+
stage-exit routing, so it is a strictly-parsed boolean.
|
|
798
|
+
|
|
799
|
+
Args:
|
|
800
|
+
feature: Feature name.
|
|
801
|
+
kind: One of add-feature|redep|move-boundary|split.
|
|
802
|
+
target: The sibling feature to add, or the affected feature/boundary.
|
|
803
|
+
rationale: Why the epic must change.
|
|
804
|
+
raised_by: forge-1-prd or forge-2-tech.
|
|
805
|
+
blocks_current: True → pause-now; False → finish-then-edit.
|
|
806
|
+
specs_dir: Specs directory.
|
|
807
|
+
epic: Owning epic name, or None.
|
|
808
|
+
|
|
809
|
+
Returns:
|
|
810
|
+
The mutated state dict (for the --json echo).
|
|
811
|
+
|
|
812
|
+
Raises:
|
|
813
|
+
UsageError: Unknown feature directory, unparseable state file, or a
|
|
814
|
+
failed atomic write (→ exit 2).
|
|
815
|
+
"""
|
|
816
|
+
state_path, state = _load_state_for_write(specs_dir, feature, epic)
|
|
817
|
+
item = {
|
|
818
|
+
"kind": kind,
|
|
819
|
+
"target": target,
|
|
820
|
+
"rationale": rationale,
|
|
821
|
+
"blocksCurrent": blocks_current,
|
|
822
|
+
"raisedBy": raised_by,
|
|
823
|
+
"raisedAt": _now_iso(),
|
|
824
|
+
"status": "open",
|
|
825
|
+
}
|
|
826
|
+
state.setdefault("epicChangeRequests", []).append(item)
|
|
827
|
+
return _commit_state(state_path, state)
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def _require_positive_int(value: object, label: str) -> int:
|
|
831
|
+
"""Return ``value`` as a positive int, or raise ``UsageError``.
|
|
832
|
+
|
|
833
|
+
``bool`` is rejected explicitly: it is an ``int`` subclass, so ``True`` would
|
|
834
|
+
otherwise sail through as version 1 and record a freshness ledger entry for an
|
|
835
|
+
artifact revision that never existed.
|
|
836
|
+
|
|
837
|
+
Args:
|
|
838
|
+
value: The candidate revision/version.
|
|
839
|
+
label: The flag or field name to name in the error.
|
|
840
|
+
|
|
841
|
+
Returns:
|
|
842
|
+
The validated positive integer.
|
|
843
|
+
|
|
844
|
+
Raises:
|
|
845
|
+
UsageError: Not an int, a bool, or below 1 (→ exit 2).
|
|
846
|
+
"""
|
|
847
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 1:
|
|
848
|
+
raise UsageError(f"{label} must be a positive integer; got {value!r}")
|
|
849
|
+
return value
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def _validated_findings_file(
|
|
853
|
+
value: str, target_dir: Path, label: str = "--findings-file"
|
|
854
|
+
) -> str:
|
|
855
|
+
"""Return ``value`` if it is a safe relative path inside ``target_dir``.
|
|
856
|
+
|
|
857
|
+
``findingsFile`` is defined as relative to the
|
|
858
|
+
feature directory, and downstream consumers (forge-fix selecting the report)
|
|
859
|
+
follow the stored value verbatim. So it gets the same fail-closed containment
|
|
860
|
+
treatment as the write target itself (REQ-SEC-01): an absolute path, a ``..``
|
|
861
|
+
segment, a NUL/control character, or a symlinked escape is rejected BEFORE any
|
|
862
|
+
mutation rather than persisted for a later reader to resolve.
|
|
863
|
+
|
|
864
|
+
The same containment contract governs every stored path a caller asserts is
|
|
865
|
+
inside the feature directory, so the flag being validated is a parameter: the
|
|
866
|
+
diagnostic must name the flag the user actually passed.
|
|
867
|
+
|
|
868
|
+
Args:
|
|
869
|
+
value: The candidate path, as supplied on the command line.
|
|
870
|
+
target_dir: The resolved feature (or epic) directory it must sit inside.
|
|
871
|
+
label: The flag to name in the error.
|
|
872
|
+
|
|
873
|
+
Returns:
|
|
874
|
+
The value unchanged, once validated.
|
|
875
|
+
|
|
876
|
+
Raises:
|
|
877
|
+
UsageError: Empty, absolute, ``..``-bearing, control-character-bearing, or
|
|
878
|
+
escaping the target directory (→ exit 2).
|
|
879
|
+
"""
|
|
880
|
+
if not value:
|
|
881
|
+
raise UsageError(f"{label} must not be empty")
|
|
882
|
+
bad = next((ch for ch in value if ord(ch) < 32 or ord(ch) == 127), None)
|
|
883
|
+
if bad is not None:
|
|
884
|
+
raise UsageError(
|
|
885
|
+
f"{label} contains a control character ({bad!r}); "
|
|
886
|
+
f"expected a plain relative path"
|
|
887
|
+
)
|
|
888
|
+
candidate = Path(value)
|
|
889
|
+
if candidate.is_absolute():
|
|
890
|
+
raise UsageError(
|
|
891
|
+
f"{label} {value!r} is absolute; it must be relative to the "
|
|
892
|
+
f"feature directory ({target_dir})"
|
|
893
|
+
)
|
|
894
|
+
if ".." in candidate.parts:
|
|
895
|
+
raise UsageError(
|
|
896
|
+
f"{label} {value!r} contains a '..' segment; it must stay inside "
|
|
897
|
+
f"the feature directory ({target_dir})"
|
|
898
|
+
)
|
|
899
|
+
root = target_dir.resolve()
|
|
900
|
+
resolved = (target_dir / candidate).resolve()
|
|
901
|
+
if resolved == root or root not in resolved.parents:
|
|
902
|
+
raise UsageError(
|
|
903
|
+
f"{label} {value!r} escapes the feature directory ({target_dir}); "
|
|
904
|
+
f"refusing to record it"
|
|
905
|
+
)
|
|
906
|
+
return value
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def _current_artifact_version(state: dict, stage: str) -> int:
|
|
910
|
+
"""Return the artifact revision a verify result is being recorded against.
|
|
911
|
+
|
|
912
|
+
For a feature target that is the selected production stage's ``version``.
|
|
913
|
+
Only the statuses that consume it resolve it: `passed` and
|
|
914
|
+
`findings-reported` write it into the freshness ledger, and
|
|
915
|
+
`auto-verify-pending` writes it as the revision the debt is owed on.
|
|
916
|
+
`skipped` and `findings-applied` never read it and skip the lookup, so both
|
|
917
|
+
stay recordable on a completed stage with no recorded ``version``.
|
|
918
|
+
|
|
919
|
+
Args:
|
|
920
|
+
state: The loaded state document.
|
|
921
|
+
stage: The production stage the verify entry serves.
|
|
922
|
+
|
|
923
|
+
Returns:
|
|
924
|
+
The stage's current positive-integer version.
|
|
925
|
+
|
|
926
|
+
Raises:
|
|
927
|
+
UsageError: The stage has no recorded (or no valid) ``version`` (→ exit 2).
|
|
928
|
+
"""
|
|
929
|
+
version = _stage_version(state, stage)
|
|
930
|
+
if version is None:
|
|
931
|
+
raise UsageError(
|
|
932
|
+
f"{stage} has no recorded version in this feature's state, so there is no "
|
|
933
|
+
f"artifact revision to verify against; run state-complete for {stage} first"
|
|
934
|
+
)
|
|
935
|
+
return _require_positive_int(version, f"{stage}.version")
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def _assert_full_commit_hash(commit_hash: object) -> None:
|
|
939
|
+
"""Reject a ``--commit-hash`` that is not exactly 40 hexadecimal characters.
|
|
940
|
+
|
|
941
|
+
REQ-STATE-01 constrains WRITES, not reads. New provenance is a
|
|
942
|
+
full ``git rev-parse HEAD`` object hash; an abbreviation is rejected rather than
|
|
943
|
+
expanded, because expanding one would mean shelling out to Git from a script
|
|
944
|
+
whose whole contract is bounded local file reads. Caller case is preserved —
|
|
945
|
+
the regex accepts either case and nothing normalizes it.
|
|
946
|
+
|
|
947
|
+
Nothing constrains the schema, so a legacy short hash already recorded in state
|
|
948
|
+
keeps loading through ``_read_state``, ``_load_state_for_write``, the manifest
|
|
949
|
+
status readers, the navigator, and stage exit unmigrated (REQ-STATE-02).
|
|
950
|
+
|
|
951
|
+
Args:
|
|
952
|
+
commit_hash: The supplied value, typed loosely so a non-string reaching the
|
|
953
|
+
callable in-process is refused here rather than at serialization time.
|
|
954
|
+
|
|
955
|
+
Raises:
|
|
956
|
+
UsageError: The value is not a 40-character hex string (→ exit 2, before
|
|
957
|
+
any load-for-mutation and always before ``_commit_state``).
|
|
958
|
+
"""
|
|
959
|
+
if isinstance(commit_hash, str) and FULL_GIT_HASH_RE.fullmatch(commit_hash):
|
|
960
|
+
return
|
|
961
|
+
raise UsageError(
|
|
962
|
+
f"--commit-hash must be the full 40-character Git object hash "
|
|
963
|
+
f"(`git rev-parse HEAD`); got {commit_hash!r}. An abbreviation is rejected "
|
|
964
|
+
f"rather than expanded. Nothing was written."
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def _load_verify_target(
|
|
969
|
+
specs_dir: Path, feature: str, epic: str | None, is_epic_target: bool
|
|
970
|
+
) -> tuple[Path, dict, int | None]:
|
|
971
|
+
"""Resolve the state document ``state-verify`` will mutate — epic or feature.
|
|
972
|
+
|
|
973
|
+
An epic target NEVER falls back to the member writer, and a member target never
|
|
974
|
+
reaches the epic root: the two resolvers are disjoint (REQ-SEC-01). Both result
|
|
975
|
+
mode and commit-2 mode go through here, so neither can drift onto the other's
|
|
976
|
+
resolver.
|
|
977
|
+
|
|
978
|
+
Args:
|
|
979
|
+
specs_dir: The configured specs directory.
|
|
980
|
+
feature: The feature name, or the epic name for an epic target.
|
|
981
|
+
epic: The owning epic for a member, else None.
|
|
982
|
+
is_epic_target: True when ``--stage forge-0-epic`` selected the epic root.
|
|
983
|
+
|
|
984
|
+
Returns:
|
|
985
|
+
``(state_path, state, revision)``. ``revision`` is the epic's manifest
|
|
986
|
+
revision for an epic target, and None for a feature target (whose artifact
|
|
987
|
+
version is read per-stage out of its own state).
|
|
988
|
+
|
|
989
|
+
Raises:
|
|
990
|
+
UsageError: Any resolution or load failure (→ exit 2, nothing written).
|
|
991
|
+
"""
|
|
992
|
+
if is_epic_target:
|
|
993
|
+
return _load_epic_state_for_write(specs_dir, feature, epic)
|
|
994
|
+
state_path, state = _load_state_for_write(specs_dir, feature, epic)
|
|
995
|
+
return state_path, state, None
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def _verify_result_entry(
|
|
999
|
+
status: str,
|
|
1000
|
+
prior: dict,
|
|
1001
|
+
current: int | None,
|
|
1002
|
+
findings_file: str | None,
|
|
1003
|
+
findings_count: int | None,
|
|
1004
|
+
now: str,
|
|
1005
|
+
) -> dict:
|
|
1006
|
+
"""Build the replacement ``forge-verify-*`` entry for one result transition.
|
|
1007
|
+
|
|
1008
|
+
Each status REPLACES the entry rather than patching it, which is what makes the
|
|
1009
|
+
the "clear …" rules exact: a terminal write cannot leave a stale
|
|
1010
|
+
``scheduledAt``/``scheduledStageVersion`` behind, and the keys are DELETED
|
|
1011
|
+
rather than nulled (``VerifyEntry`` is ``total=False``, so absent means "not
|
|
1012
|
+
scheduled" while present-but-null would be malformed). ``findings-applied`` is
|
|
1013
|
+
the one status that carries prior state forward — the report metadata — and it
|
|
1014
|
+
deliberately writes no ``verifiedStageVersion``: fixes landed, nothing
|
|
1015
|
+
re-verified them, so freshness stays unresolved until a later ``passed``.
|
|
1016
|
+
``passed`` may record NEW attached-report metadata of its own (a clean report,
|
|
1017
|
+
an advisory-only report, or the escalation-acceptance rules in
|
|
1018
|
+
``cmd_state_verify``).
|
|
1019
|
+
|
|
1020
|
+
Args:
|
|
1021
|
+
status: The validated result status.
|
|
1022
|
+
prior: The existing entry (``{}`` when absent).
|
|
1023
|
+
current: The current artifact revision, or None for ``skipped`` and
|
|
1024
|
+
``findings-applied`` (which never consume it).
|
|
1025
|
+
findings_file: Validated relative report path, when supplied.
|
|
1026
|
+
findings_count: Validated non-negative count, when supplied.
|
|
1027
|
+
now: The shared ISO-8601 timestamp for this write.
|
|
1028
|
+
|
|
1029
|
+
Returns:
|
|
1030
|
+
The complete new entry dict.
|
|
1031
|
+
"""
|
|
1032
|
+
if status == "auto-verify-pending":
|
|
1033
|
+
return {
|
|
1034
|
+
"status": status,
|
|
1035
|
+
"scheduledAt": now,
|
|
1036
|
+
"scheduledStageVersion": current,
|
|
1037
|
+
"commitHash": None,
|
|
1038
|
+
}
|
|
1039
|
+
if status == "passed":
|
|
1040
|
+
entry: dict = {"status": status}
|
|
1041
|
+
if findings_file is not None:
|
|
1042
|
+
# An attached clean/advisory report, or residual findings the user
|
|
1043
|
+
# explicitly accepted at the escalation gate, resolves as `passed`
|
|
1044
|
+
# so it never routes to forge-fix while the audit artifact stays
|
|
1045
|
+
# attached. A bare zero count records no report keys, preserving the
|
|
1046
|
+
# legacy plain "verified clean" shape.
|
|
1047
|
+
entry["findingsFile"] = findings_file
|
|
1048
|
+
entry["findingsCount"] = findings_count
|
|
1049
|
+
entry["verifiedAt"] = now
|
|
1050
|
+
entry["verifiedStageVersion"] = current
|
|
1051
|
+
entry["commitHash"] = None
|
|
1052
|
+
return entry
|
|
1053
|
+
if status == "findings-reported":
|
|
1054
|
+
return {
|
|
1055
|
+
"status": status,
|
|
1056
|
+
"findingsFile": findings_file,
|
|
1057
|
+
"findingsCount": findings_count,
|
|
1058
|
+
"verifiedAt": now,
|
|
1059
|
+
"verifiedStageVersion": current,
|
|
1060
|
+
"commitHash": None,
|
|
1061
|
+
}
|
|
1062
|
+
if status == "findings-applied":
|
|
1063
|
+
entry: dict = {"status": status}
|
|
1064
|
+
for key in ("findingsFile", "findingsCount"):
|
|
1065
|
+
if key in prior:
|
|
1066
|
+
entry[key] = prior[key]
|
|
1067
|
+
entry["fixedAt"] = now
|
|
1068
|
+
entry["commitHash"] = None
|
|
1069
|
+
return entry
|
|
1070
|
+
return {"status": status, "commitHash": None} # skipped
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def cmd_state_verify(
|
|
1074
|
+
feature: str,
|
|
1075
|
+
stage: str,
|
|
1076
|
+
specs_dir: Path,
|
|
1077
|
+
epic: str | None,
|
|
1078
|
+
status: str | None = None,
|
|
1079
|
+
findings_file: str | None = None,
|
|
1080
|
+
findings_count: int | None = None,
|
|
1081
|
+
verified_stage_version: int | None = None,
|
|
1082
|
+
commit_hash: str | None = None,
|
|
1083
|
+
) -> dict:
|
|
1084
|
+
"""Write one verify result transition or one provenance follow-up.
|
|
1085
|
+
|
|
1086
|
+
Args:
|
|
1087
|
+
feature: The feature name, or the EPIC name when `stage == "forge-0-epic"`.
|
|
1088
|
+
Resolved through the same path-safety and containment rules as every
|
|
1089
|
+
other state write.
|
|
1090
|
+
stage: The production stage this verify entry serves — one of
|
|
1091
|
+
`VERIFY_MODE_TO_STAGE`'s values, or `"forge-0-epic"` for an epic-target
|
|
1092
|
+
write. Selects `stages["forge-verify-{suffix}"]`.
|
|
1093
|
+
specs_dir: Root of the specs tree, as configured by `specsDir`.
|
|
1094
|
+
epic: Epic name when `feature` is a member, else None. REQUIRED for members
|
|
1095
|
+
so the bare name is never resolved ambiguously. For
|
|
1096
|
+
`stage == "forge-0-epic"` it must be absent or equal to `feature`.
|
|
1097
|
+
status: Result mode. Mutually exclusive with `commit_hash`. Each status
|
|
1098
|
+
admits only the metadata below; everything else is refused before any
|
|
1099
|
+
write, so a contradictory call never lands a partial entry:
|
|
1100
|
+
|
|
1101
|
+
- `passed` — REQUIRES `verified_stage_version`. MAY carry an attached
|
|
1102
|
+
report (`findings_file` + `findings_count` together, count >= 0) in
|
|
1103
|
+
three protocol cases: a CLEAN zero-finding round report (a fix
|
|
1104
|
+
pass's re-verify — valid audit evidence that lets the stage advance
|
|
1105
|
+
instead of stranding at `findings-applied`, #237), an ADVISORY-ONLY
|
|
1106
|
+
report (no blocking `error`/`gap` findings, count >= 1), and
|
|
1107
|
+
residual findings the user explicitly ACCEPTED at the round-ledger
|
|
1108
|
+
escalation (recorded first as a `state-decision`; see "Escalation"
|
|
1109
|
+
in stage-exit-protocol.md). In every case the stage resolves
|
|
1110
|
+
without routing to forge-fix and the report stays attached. Half a
|
|
1111
|
+
pairing is refused: a file without a count, or a positive count
|
|
1112
|
+
without a file. A bare `passed` (neither flag) is also accepted and
|
|
1113
|
+
records the report-free clean shape. Unaccepted blocking findings
|
|
1114
|
+
belong to `findings-reported`.
|
|
1115
|
+
- `findings-reported` — REQUIRES all three of `verified_stage_version`,
|
|
1116
|
+
`findings_file`, and a non-negative `findings_count`.
|
|
1117
|
+
- `findings-applied` — REFUSES `verified_stage_version`. Applying fixes
|
|
1118
|
+
is not verifying them, so this status deliberately CLEARS the recorded
|
|
1119
|
+
freshness and leaves the stage's verification outstanding until a later
|
|
1120
|
+
`passed` records a revision.
|
|
1121
|
+
- `skipped` and `auto-verify-pending` — accept none of the three.
|
|
1122
|
+
|
|
1123
|
+
`passed` and `findings-reported` additionally refuse a
|
|
1124
|
+
`verified_stage_version` that is stale against the served stage's current
|
|
1125
|
+
version. The persisted shape is `references/pipeline-state-schema.json`.
|
|
1126
|
+
findings_file: Path to the findings document, relative to and contained by
|
|
1127
|
+
the resolved feature/epic directory. Required by `findings-reported`,
|
|
1128
|
+
optional on `passed` (the attached-report cases above); rejected when
|
|
1129
|
+
absolute, containing `..`, or carrying NUL/control characters
|
|
1130
|
+
(REQ-SEC-01).
|
|
1131
|
+
findings_count: Number of findings in `findings_file`. Required alongside it.
|
|
1132
|
+
verified_stage_version: The served stage's `version` at verification time,
|
|
1133
|
+
feeding the navigator's freshness ledger. Cleared by
|
|
1134
|
+
`findings-applied`, which deliberately does not claim freshness.
|
|
1135
|
+
commit_hash: Commit-2 mode. Full 40-hex only, validated by
|
|
1136
|
+
`FULL_GIT_HASH_RE.fullmatch`; abbreviations are rejected rather than
|
|
1137
|
+
expanded. Mutually exclusive with `status`.
|
|
1138
|
+
|
|
1139
|
+
Returns:
|
|
1140
|
+
The emitted JSON result: the written verify entry plus the resolved target
|
|
1141
|
+
path, so the caller can report what landed without re-reading state.
|
|
1142
|
+
|
|
1143
|
+
Raises:
|
|
1144
|
+
UsageError: Mixed modes, invalid metadata, invalid hash, missing entry,
|
|
1145
|
+
unsafe/ambiguous target, or atomic write failure.
|
|
1146
|
+
"""
|
|
1147
|
+
# --- Mode exclusivity, before anything is resolved or loaded. -------------
|
|
1148
|
+
if status is None and commit_hash is None:
|
|
1149
|
+
raise UsageError(
|
|
1150
|
+
"state-verify needs exactly one mode: --status <result> to record a "
|
|
1151
|
+
"verification transition, or --commit-hash <40-hex> to record Commit-2 "
|
|
1152
|
+
"provenance for an existing entry"
|
|
1153
|
+
)
|
|
1154
|
+
if status is not None and commit_hash is not None:
|
|
1155
|
+
raise UsageError(
|
|
1156
|
+
"--status and --commit-hash are mutually exclusive: a result write "
|
|
1157
|
+
"records commitHash null (Commit 1), and the hash lands in a separate "
|
|
1158
|
+
"commit-2 call"
|
|
1159
|
+
)
|
|
1160
|
+
if commit_hash is not None:
|
|
1161
|
+
# Commit-2 carries provenance for an entry that ALREADY exists, so every
|
|
1162
|
+
# result field must be absent: a hash arriving next to findings metadata
|
|
1163
|
+
# means the caller conflated the two writes.
|
|
1164
|
+
for label, value in (
|
|
1165
|
+
("--findings-file", findings_file),
|
|
1166
|
+
("--findings-count", findings_count),
|
|
1167
|
+
("--verified-stage-version", verified_stage_version),
|
|
1168
|
+
):
|
|
1169
|
+
if value is not None:
|
|
1170
|
+
raise UsageError(
|
|
1171
|
+
f"--commit-hash records provenance for an existing entry and "
|
|
1172
|
+
f"changes only its commitHash, so it does not accept {label}. "
|
|
1173
|
+
f"Record the result with --status first, commit, then re-run "
|
|
1174
|
+
f"with --commit-hash alone."
|
|
1175
|
+
)
|
|
1176
|
+
_assert_full_commit_hash(commit_hash)
|
|
1177
|
+
elif status not in VERIFY_RESULT_STATUSES:
|
|
1178
|
+
known = ", ".join(VERIFY_RESULT_STATUSES)
|
|
1179
|
+
raise UsageError(f"unknown --status {status!r}; expected one of {known}")
|
|
1180
|
+
|
|
1181
|
+
# --- Target selection: epic before the token map. --------------
|
|
1182
|
+
is_epic_target = stage == "forge-0-epic"
|
|
1183
|
+
if is_epic_target:
|
|
1184
|
+
verify_key = "forge-verify-epic"
|
|
1185
|
+
else:
|
|
1186
|
+
token = VERIFY_TOKEN_BY_STAGE.get(stage)
|
|
1187
|
+
if token is None:
|
|
1188
|
+
raise UsageError(
|
|
1189
|
+
f"{stage} has no verification token, so it has no forge-verify-* entry "
|
|
1190
|
+
f"to write; expected one of {', '.join(VERIFY_STAGES)}"
|
|
1191
|
+
)
|
|
1192
|
+
verify_key = f"forge-verify-{token}"
|
|
1193
|
+
|
|
1194
|
+
# --- Commit-2 provenance mode. ---------------------------------
|
|
1195
|
+
# Commit 1 recorded the result with `commitHash: null`; this second, targeted
|
|
1196
|
+
# write records the hash of THAT commit. Nothing here invokes Git, rewrites
|
|
1197
|
+
# history, or amends — the two commits stay two commits (REQ-STATE-04).
|
|
1198
|
+
if commit_hash is not None:
|
|
1199
|
+
state_path, state, _ = _load_verify_target(
|
|
1200
|
+
specs_dir, feature, epic, is_epic_target
|
|
1201
|
+
)
|
|
1202
|
+
entry = _verify_entry(state, verify_key)
|
|
1203
|
+
if not entry:
|
|
1204
|
+
raise UsageError(
|
|
1205
|
+
f"--commit-hash records provenance for an existing {verify_key} "
|
|
1206
|
+
f"entry, and {feature} has none. Record the verification result "
|
|
1207
|
+
f"with --status first, commit it, then re-run with --commit-hash."
|
|
1208
|
+
)
|
|
1209
|
+
# In place, so status, findings metadata, scheduling metadata, timestamps
|
|
1210
|
+
# and versions are all left exactly as Commit 1 wrote them.
|
|
1211
|
+
entry["commitHash"] = commit_hash
|
|
1212
|
+
written = _commit_state(state_path, state)
|
|
1213
|
+
return {
|
|
1214
|
+
"feature": feature,
|
|
1215
|
+
"stage": stage,
|
|
1216
|
+
"verifyKey": verify_key,
|
|
1217
|
+
"statePath": str(state_path),
|
|
1218
|
+
"entry": entry,
|
|
1219
|
+
"updatedAt": written["updatedAt"],
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
# --- Metadata validation that needs no state. ------------------
|
|
1223
|
+
if verified_stage_version is not None:
|
|
1224
|
+
_require_positive_int(verified_stage_version, "--verified-stage-version")
|
|
1225
|
+
if findings_count is not None and (
|
|
1226
|
+
isinstance(findings_count, bool) or not isinstance(findings_count, int)
|
|
1227
|
+
):
|
|
1228
|
+
raise UsageError(f"--findings-count must be an integer; got {findings_count!r}")
|
|
1229
|
+
|
|
1230
|
+
if status in ("auto-verify-pending", "skipped"):
|
|
1231
|
+
for label, value in (
|
|
1232
|
+
("--findings-file", findings_file),
|
|
1233
|
+
("--findings-count", findings_count),
|
|
1234
|
+
("--verified-stage-version", verified_stage_version),
|
|
1235
|
+
):
|
|
1236
|
+
if value is not None:
|
|
1237
|
+
raise UsageError(f"--status {status} does not accept {label}")
|
|
1238
|
+
elif status == "passed":
|
|
1239
|
+
if findings_file is not None and findings_count is None:
|
|
1240
|
+
raise UsageError(
|
|
1241
|
+
"--status passed with --findings-file requires --findings-count N "
|
|
1242
|
+
"(zero for a clean report, or the number of advisory/residual "
|
|
1243
|
+
"findings it lists)"
|
|
1244
|
+
)
|
|
1245
|
+
if findings_count is not None:
|
|
1246
|
+
if findings_count < 0:
|
|
1247
|
+
raise UsageError(
|
|
1248
|
+
f"--findings-count must not be negative; got {findings_count!r}"
|
|
1249
|
+
)
|
|
1250
|
+
if findings_count > 0 and findings_file is None:
|
|
1251
|
+
raise UsageError(
|
|
1252
|
+
f"--status passed with --findings-count {findings_count} requires "
|
|
1253
|
+
f"--findings-file <advisory report>: a positive count with no "
|
|
1254
|
+
f"report to read is unrecoverable. Blocking findings belong to "
|
|
1255
|
+
f"--status findings-reported instead."
|
|
1256
|
+
)
|
|
1257
|
+
if verified_stage_version is None:
|
|
1258
|
+
raise UsageError(
|
|
1259
|
+
"--status passed requires --verified-stage-version <current version>"
|
|
1260
|
+
)
|
|
1261
|
+
elif status == "findings-reported":
|
|
1262
|
+
if verified_stage_version is None:
|
|
1263
|
+
raise UsageError(
|
|
1264
|
+
"--status findings-reported requires --verified-stage-version "
|
|
1265
|
+
"<current version>"
|
|
1266
|
+
)
|
|
1267
|
+
if findings_file is None:
|
|
1268
|
+
raise UsageError(
|
|
1269
|
+
"--status findings-reported requires --findings-file <path relative "
|
|
1270
|
+
"to the feature directory>"
|
|
1271
|
+
)
|
|
1272
|
+
if findings_count is None:
|
|
1273
|
+
raise UsageError("--status findings-reported requires --findings-count N")
|
|
1274
|
+
if findings_count < 0:
|
|
1275
|
+
raise UsageError(
|
|
1276
|
+
f"--findings-count must not be negative; got {findings_count!r}"
|
|
1277
|
+
)
|
|
1278
|
+
elif verified_stage_version is not None: # findings-applied
|
|
1279
|
+
raise UsageError(
|
|
1280
|
+
"--status findings-applied does not accept --verified-stage-version: "
|
|
1281
|
+
"applying fixes deliberately CLEARS freshness, so only a later "
|
|
1282
|
+
"--status passed may record a verified revision"
|
|
1283
|
+
)
|
|
1284
|
+
|
|
1285
|
+
state_path, state, epic_revision = _load_verify_target(
|
|
1286
|
+
specs_dir, feature, epic, is_epic_target
|
|
1287
|
+
)
|
|
1288
|
+
target_dir = state_path.parent
|
|
1289
|
+
if findings_file is not None:
|
|
1290
|
+
_validated_findings_file(findings_file, target_dir)
|
|
1291
|
+
|
|
1292
|
+
if status in ("skipped", "findings-applied"):
|
|
1293
|
+
# Neither status consumes the artifact revision: `skipped` records no
|
|
1294
|
+
# freshness, and `findings-applied` deliberately clears it (the entry is
|
|
1295
|
+
# built from the prior report plus `fixedAt`). Resolving it anyway would
|
|
1296
|
+
# make both unrecordable on a completed stage whose `version` was never
|
|
1297
|
+
# written — exactly the state that needs the recovery path (#202).
|
|
1298
|
+
current = None
|
|
1299
|
+
elif is_epic_target:
|
|
1300
|
+
# The epic's artifact revision is the manifest revision — never a member's
|
|
1301
|
+
# production-stage version.
|
|
1302
|
+
current = epic_revision
|
|
1303
|
+
else:
|
|
1304
|
+
current = _current_artifact_version(state, stage)
|
|
1305
|
+
if status in ("passed", "findings-reported") and verified_stage_version != current:
|
|
1306
|
+
at = (
|
|
1307
|
+
f"{feature}'s manifest is at revision {current}"
|
|
1308
|
+
if is_epic_target
|
|
1309
|
+
else f"{stage} is at version {current}"
|
|
1310
|
+
)
|
|
1311
|
+
raise UsageError(
|
|
1312
|
+
f"--verified-stage-version {verified_stage_version} is stale: {at}. "
|
|
1313
|
+
f"Re-run verification against the current artifact."
|
|
1314
|
+
)
|
|
1315
|
+
|
|
1316
|
+
prior = _verify_entry(state, verify_key)
|
|
1317
|
+
if status == "skipped" and prior.get("status") in _SKIP_PROTECTED_PRIOR:
|
|
1318
|
+
# The #203 demotion trap: `skipped` over a complete-for-orchestration
|
|
1319
|
+
# status silently dropped the member from its epic rollup and re-blocked
|
|
1320
|
+
# every dependent. Fail closed; a deferral needs no write at all.
|
|
1321
|
+
raise UsageError(
|
|
1322
|
+
f"--status skipped would demote {verify_key} from "
|
|
1323
|
+
f"{prior.get('status')!r}: that status counts as resolved (and, for an "
|
|
1324
|
+
f"epic member, complete-for-orchestration), so replacing it with "
|
|
1325
|
+
f"skipped would drop the member from its epic rollup and re-block its "
|
|
1326
|
+
f"dependents. A deferral needs no write — the recorded result already "
|
|
1327
|
+
f"stands. Re-run verification to refresh it, or record --status passed "
|
|
1328
|
+
f"(with the report attached) to accept residual findings. "
|
|
1329
|
+
f"Nothing was written."
|
|
1330
|
+
)
|
|
1331
|
+
if status == "auto-verify-pending" and prior.get("status") == "findings-reported":
|
|
1332
|
+
# `_verify_result_entry` REPLACES the entry, so scheduling over a report
|
|
1333
|
+
# for the current revision would delete its `findingsFile`/`findingsCount`
|
|
1334
|
+
# and break the later `findings-applied` precondition (REQ-EXIT-04's
|
|
1335
|
+
# forbidden clobber, reached through the CLI instead of a branch exit).
|
|
1336
|
+
# A report against a since-revised artifact is superseded normally.
|
|
1337
|
+
reported = prior.get("verifiedStageVersion")
|
|
1338
|
+
if (
|
|
1339
|
+
isinstance(reported, int)
|
|
1340
|
+
and not isinstance(reported, bool)
|
|
1341
|
+
and current is not None
|
|
1342
|
+
and reported == current
|
|
1343
|
+
):
|
|
1344
|
+
raise UsageError(
|
|
1345
|
+
f"--status auto-verify-pending would replace {verify_key}'s "
|
|
1346
|
+
f"findings-reported entry for the current revision and delete its "
|
|
1347
|
+
f"report metadata ({prior.get('findingsFile')!r}, "
|
|
1348
|
+
f"findingsCount {prior.get('findingsCount')!r}). Apply the report "
|
|
1349
|
+
f"via forge-fix (--status findings-applied) or re-verify to a "
|
|
1350
|
+
f"terminal status; scheduling is valid only after the artifact "
|
|
1351
|
+
f"is revised."
|
|
1352
|
+
)
|
|
1353
|
+
if status == "findings-applied":
|
|
1354
|
+
if prior.get("status") not in ("findings-reported", "findings-applied"):
|
|
1355
|
+
raise UsageError(
|
|
1356
|
+
f"--status findings-applied requires an existing {verify_key} entry "
|
|
1357
|
+
f"with status findings-reported (or findings-applied); found "
|
|
1358
|
+
f"{prior.get('status')!r}"
|
|
1359
|
+
)
|
|
1360
|
+
for label, key, supplied in (
|
|
1361
|
+
("--findings-file", "findingsFile", findings_file),
|
|
1362
|
+
("--findings-count", "findingsCount", findings_count),
|
|
1363
|
+
):
|
|
1364
|
+
if supplied is not None and supplied != prior.get(key):
|
|
1365
|
+
raise UsageError(
|
|
1366
|
+
f"{label} {supplied!r} does not match the recorded report "
|
|
1367
|
+
f"({key}: {prior.get(key)!r}); fix the value or omit the flag"
|
|
1368
|
+
)
|
|
1369
|
+
|
|
1370
|
+
entry = _verify_result_entry(
|
|
1371
|
+
status, prior, current, findings_file, findings_count, _now_iso()
|
|
1372
|
+
)
|
|
1373
|
+
state.setdefault("stages", {})[verify_key] = entry
|
|
1374
|
+
written = _commit_state(state_path, state)
|
|
1375
|
+
return {
|
|
1376
|
+
"feature": feature,
|
|
1377
|
+
"stage": stage,
|
|
1378
|
+
"verifyKey": verify_key,
|
|
1379
|
+
"statePath": str(state_path),
|
|
1380
|
+
"entry": entry,
|
|
1381
|
+
"updatedAt": written["updatedAt"],
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
|
|
1385
|
+
def _print_state_enter(state: dict) -> None:
|
|
1386
|
+
"""Print the one-line human summary for `state-enter`."""
|
|
1387
|
+
print(f"entered {state['currentStage']} (in-progress) for {state['feature']}")
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
def _print_state_artifact(state: dict, stage: str, paths: list[str]) -> None:
|
|
1391
|
+
"""Print the one-line human summary for `state-artifact`."""
|
|
1392
|
+
total = len(state.get("stages", {}).get(stage, {}).get("artifacts", []))
|
|
1393
|
+
print(f"tracked {stage} artifact(s): {', '.join(paths)} ({total} total)")
|
|
1394
|
+
|
|
1395
|
+
|
|
1396
|
+
def _print_state_complete(
|
|
1397
|
+
state: dict, stage: str, commit_hash: str | None, resumable: bool
|
|
1398
|
+
) -> None:
|
|
1399
|
+
"""Print the one-line human summary for `state-complete` (one per branch)."""
|
|
1400
|
+
if commit_hash is not None:
|
|
1401
|
+
print(f"recorded {stage} commitHash: {commit_hash}")
|
|
1402
|
+
return
|
|
1403
|
+
if resumable:
|
|
1404
|
+
print(f"left {stage} in-progress (resumable — no completion recorded)")
|
|
1405
|
+
return
|
|
1406
|
+
entry = state.get("stages", {}).get(stage, {})
|
|
1407
|
+
label = (
|
|
1408
|
+
"completed"
|
|
1409
|
+
if entry.get("status") == _DONE_STATUS
|
|
1410
|
+
else f"partially completed ({entry.get('status')})"
|
|
1411
|
+
)
|
|
1412
|
+
recorded = entry.get("commitHash")
|
|
1413
|
+
cascaded = state.get("_cascadedStale") or []
|
|
1414
|
+
suffix = f"; marked stale: {', '.join(cascaded)}" if cascaded else ""
|
|
1415
|
+
print(
|
|
1416
|
+
f"{label} {stage} v{entry.get('version')} "
|
|
1417
|
+
f"(commitHash: {'null' if recorded is None else recorded}){suffix}"
|
|
1418
|
+
)
|
|
1419
|
+
|
|
1420
|
+
|
|
1421
|
+
def _print_state_skip(state: dict, stage: str) -> None:
|
|
1422
|
+
"""Print the one-line human summary for `state-skip`."""
|
|
1423
|
+
print(
|
|
1424
|
+
f"recorded {stage} as skipped for {state['feature']} "
|
|
1425
|
+
"(deliberate — no docs claimed)"
|
|
1426
|
+
)
|
|
1427
|
+
|
|
1428
|
+
|
|
1429
|
+
def _print_state_branch(state: dict) -> None:
|
|
1430
|
+
"""Print the one-line human summary for `state-branch`."""
|
|
1431
|
+
print(f"recorded branch for {state['feature']}: {state['branch']}")
|
|
1432
|
+
|
|
1433
|
+
|
|
1434
|
+
def _print_state_note(state: dict) -> None:
|
|
1435
|
+
"""Print the one-line human summary for `state-note`."""
|
|
1436
|
+
print(f"note set for {state['feature']} ({len(state['notes'])} chars)")
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
def _print_state_decision(state: dict) -> None:
|
|
1440
|
+
"""Print the one-line human summary for `state-decision` (the item appended)."""
|
|
1441
|
+
item = state["deferredDecisions"][-1]
|
|
1442
|
+
target = item.get("targetStage")
|
|
1443
|
+
routing = f"{item['raisedBy']} → {target}" if target else f"{item['raisedBy']}, no target stage"
|
|
1444
|
+
print(f"deferred decision recorded (raisedBy {routing})")
|
|
1445
|
+
|
|
1446
|
+
|
|
1447
|
+
def _print_state_verify(result: dict, commit_hash: str | None = None) -> None:
|
|
1448
|
+
"""Print the one-line human summary for `state-verify` (one per mode).
|
|
1449
|
+
|
|
1450
|
+
Takes the verb's RESULT dict (entry + resolved path), not a state document —
|
|
1451
|
+
`state-verify` is the one verb whose echo is the written entry rather than the
|
|
1452
|
+
whole file. Commit-2 mode gets its own line: reporting the untouched status
|
|
1453
|
+
would read as if the result had just been re-written.
|
|
1454
|
+
"""
|
|
1455
|
+
entry = result["entry"]
|
|
1456
|
+
if commit_hash is not None:
|
|
1457
|
+
print(f"recorded {result['verifyKey']} commitHash: {commit_hash}")
|
|
1458
|
+
return
|
|
1459
|
+
detail = ""
|
|
1460
|
+
if entry.get("findingsFile"):
|
|
1461
|
+
detail = f" ({entry.get('findingsCount')} in {entry['findingsFile']})"
|
|
1462
|
+
elif entry.get("scheduledStageVersion") is not None:
|
|
1463
|
+
detail = f" (scheduled at v{entry['scheduledStageVersion']})"
|
|
1464
|
+
elif entry.get("verifiedStageVersion") is not None:
|
|
1465
|
+
detail = f" (v{entry['verifiedStageVersion']})"
|
|
1466
|
+
print(
|
|
1467
|
+
f"recorded {result['verifyKey']} = {entry['status']} for "
|
|
1468
|
+
f"{result['feature']}{detail}"
|
|
1469
|
+
)
|
|
1470
|
+
|
|
1471
|
+
|
|
1472
|
+
def _print_state_ecr(state: dict) -> None:
|
|
1473
|
+
"""Print the one-line human summary for `state-ecr` (the item appended)."""
|
|
1474
|
+
item = state["epicChangeRequests"][-1]
|
|
1475
|
+
blocks = "true" if item["blocksCurrent"] else "false"
|
|
1476
|
+
print(
|
|
1477
|
+
f"epic change request recorded ({item['kind']} → {item['target']}, "
|
|
1478
|
+
f"blocksCurrent={blocks})"
|
|
1479
|
+
)
|