@garygentry/feature-forge 0.3.6 → 0.3.7
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,1566 @@
|
|
|
1
|
+
"""Low-level primitives shared across the ``forge_session`` package modules.
|
|
2
|
+
|
|
3
|
+
The foundation layer of the ``forge-session.py`` split (#279): the TypedDicts and
|
|
4
|
+
the ``UsageError`` exception the verbs return/raise, plus the ``VerifyStatus``
|
|
5
|
+
vocabulary alias several verbs share. Every other package module imports what it
|
|
6
|
+
needs FROM here — never from the ``forge-session.py`` shim, which re-exports these
|
|
7
|
+
names back out for the path-loaded test oracle and the CLI. Only genuinely shared
|
|
8
|
+
primitives live here; each cluster module owns its own internals. (The mirrored
|
|
9
|
+
``load_json_with_duplicates``/``warn_duplicate_keys`` pair deliberately stays in the
|
|
10
|
+
shim body, byte-identical to ``scripts/forge-bootstrap.py`` — see
|
|
11
|
+
``tests/test_json_loader_parity.py`` — so it is NOT extracted here.)
|
|
12
|
+
|
|
13
|
+
3.10 baseline, Google-style docstrings, stdlib only — matching the conventions of
|
|
14
|
+
the monolith it was carved out of.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
import tempfile
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Final, Literal, TypedDict, get_args
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class UsageError(Exception):
|
|
31
|
+
"""A usage or I/O failure that must exit 2."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
#: The persisted verify-entry status vocabulary; mirrors KNOWN_VERIFY_STATUSES and
|
|
35
|
+
#: references/pipeline-state-schema.json's verifyEntry.status.enum.
|
|
36
|
+
VerifyStatus = Literal[
|
|
37
|
+
"pending",
|
|
38
|
+
"auto-verify-pending",
|
|
39
|
+
"passed",
|
|
40
|
+
"findings-reported",
|
|
41
|
+
"findings-applied",
|
|
42
|
+
"skipped",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class FeatureRow(TypedDict):
|
|
47
|
+
"""One active feature, ranked by recency, with its next actionable step."""
|
|
48
|
+
|
|
49
|
+
name: str
|
|
50
|
+
epic: str | None
|
|
51
|
+
currentStage: str
|
|
52
|
+
branch: str | None
|
|
53
|
+
updatedAt: str | None
|
|
54
|
+
complete: bool
|
|
55
|
+
nextStage: str | None
|
|
56
|
+
nextCommand: str | None
|
|
57
|
+
verifyPending: bool
|
|
58
|
+
verifyCommand: str | None
|
|
59
|
+
verifyStage: str | None
|
|
60
|
+
verifyState: str
|
|
61
|
+
autoVerify: bool
|
|
62
|
+
autoFix: bool
|
|
63
|
+
verifyGate: str
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class EpicReconcile(TypedDict, total=False):
|
|
67
|
+
"""Existing epic backflow directive retained in expanded exits.
|
|
68
|
+
|
|
69
|
+
Present only for epic members; absent entirely for a standalone feature.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
# True when backflow must run before the member may advance; False when it is
|
|
73
|
+
# merely advisable. Drives whether the exit blocks or only mentions it.
|
|
74
|
+
required: bool
|
|
75
|
+
# True to surface the reminder text in the rendered block. Independent of
|
|
76
|
+
# `required`: a required reconcile with `reminder: False` still blocks silently
|
|
77
|
+
# in `--json` consumers.
|
|
78
|
+
reminder: bool
|
|
79
|
+
# Host-rendered command that performs the reconcile. Already passed through
|
|
80
|
+
# `_host_command`; consumers print it verbatim and never re-translate it.
|
|
81
|
+
command: str
|
|
82
|
+
# Number of member changes awaiting backflow. 0 is meaningful — it means
|
|
83
|
+
# reconcile was evaluated and found nothing, distinct from the key being absent
|
|
84
|
+
# because the feature is not an epic member.
|
|
85
|
+
count: int
|
|
86
|
+
# Canonical (untranslated) production command demoted behind a blocking
|
|
87
|
+
# reconcile — rendered as the unfenced "After reconciling, continue the
|
|
88
|
+
# pipeline with: …" line and passed through `_host_command` at render time.
|
|
89
|
+
# Present only when `required: True`; None/absent otherwise. It is a COMMAND,
|
|
90
|
+
# never a user-supplied reason: the live writer sets it to `next_command`
|
|
91
|
+
# (scripts/forge-session.py) and `_next_steps_block` translates it for the
|
|
92
|
+
# host. Repurposing it to carry prose would send free text through
|
|
93
|
+
# `_host_command` and strip the blocking follow-up line of its source
|
|
94
|
+
# (REQ-COMPAT-01).
|
|
95
|
+
deferred: str | None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class StageExitDirectives(TypedDict, total=False):
|
|
99
|
+
"""Machine-readable decisions emitted by `stage_exit`.
|
|
100
|
+
|
|
101
|
+
`total=False` throughout: a key's ABSENCE means "not applicable to this exit",
|
|
102
|
+
which is never the same as a present-but-null value. `servedStage: None` says
|
|
103
|
+
the exit resolved no served stage; a missing `servedStage` says the concept does
|
|
104
|
+
not apply. Consumers must distinguish the two.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
# The stage whose exit this is — always one of EXIT_STAGES. Always present.
|
|
108
|
+
stage: str
|
|
109
|
+
# Human-readable noun for this stage's artifact, used by
|
|
110
|
+
# references/stage-exit-protocol.md's "{stageNoun}" slots (the auto-verify
|
|
111
|
+
# heading and the "Verify {stageNoun} now" gate label). Always present;
|
|
112
|
+
# STAGE_NOUN.get(stage, stage), so it defaults to the stage id when unmapped.
|
|
113
|
+
# Pre-existing key, retained verbatim for REQ-COMPAT-01.
|
|
114
|
+
stageNoun: str
|
|
115
|
+
# For a verify/fix branch exit, the production stage the diversion served and
|
|
116
|
+
# rejoins. None on a production-stage exit, which serves only itself.
|
|
117
|
+
servedStage: str | None
|
|
118
|
+
# Verify mode in play (`prd`, `tech`, `specs`, `backlog`, `impl`, `epic`), keyed
|
|
119
|
+
# by VERIFY_MODE_TO_STAGE. None when this exit is not a verify/fix exit.
|
|
120
|
+
verifyMode: str | None
|
|
121
|
+
# Terminal outcome for stages with a multi-way result. Must be a member of
|
|
122
|
+
# EXIT_OUTCOMES[stage] — consult that table rather than this comment,
|
|
123
|
+
# which is deliberately not a second copy of the domain. None for stages
|
|
124
|
+
# whose exit has a single outcome.
|
|
125
|
+
outcome: str | None
|
|
126
|
+
# Branch ownership for a verify/fix exit — ExitOwner, i.e. exactly "direct"
|
|
127
|
+
# (this call owns and prints the terminal block) or "nested" (an outer
|
|
128
|
+
# authoring stage owns it). REQUIRED for forge-verify/forge-fix and REJECTED
|
|
129
|
+
# for stages 0–6, which are always direct owners.
|
|
130
|
+
# None only on a production-stage exit, where the concept does not apply.
|
|
131
|
+
owner: str | None
|
|
132
|
+
# Who prints the terminal block. "self" — this caller renders exactly one
|
|
133
|
+
# sentinel-terminated block. "outer" — a nested invocation that must print
|
|
134
|
+
# nothing terminal, leaving ownership with the outermost authoring stage.
|
|
135
|
+
terminalOwnedBy: Literal["self", "outer"]
|
|
136
|
+
# Feature (or epic) name this exit concerns. Always present.
|
|
137
|
+
feature: str
|
|
138
|
+
# Resolved host: "claude", "pi", or "generic". Selects command syntax and
|
|
139
|
+
# fresh-session wording; never inferred downstream, always decided here.
|
|
140
|
+
host: str
|
|
141
|
+
# Whether the host may dispatch a clean-room verifier subagent —
|
|
142
|
+
# VerifyCapability, i.e. exactly "interactive" or "manual". A manual host
|
|
143
|
+
# receives verify-first ordering with copy-paste commands instead of an
|
|
144
|
+
# interactive gate; capable Pi is interactive, not manual (REQ-EXIT-07).
|
|
145
|
+
# "May", not "has the tool": a session that bars unsolicited dispatch but
|
|
146
|
+
# offers a question tool is interactive, since the gate's prompt makes the
|
|
147
|
+
# dispatch solicited. Only no-question-tool-and-no-dispatch is manual.
|
|
148
|
+
verifyCapability: str
|
|
149
|
+
# Current verification state of the served artifact, as classified by
|
|
150
|
+
# `verify_state` — including "auto-pending" for unrun scheduled verification.
|
|
151
|
+
verifyState: str
|
|
152
|
+
# Production stage the outstanding/owed verification belongs to — the value
|
|
153
|
+
# `pending_verify()` returns; mirrors FeatureRow.verifyStage so navigator rows
|
|
154
|
+
# and stage-exit JSON report the same thing. None when nothing is outstanding.
|
|
155
|
+
# DISTINCT from `servedStage`, which is branch-exit-only: on a production-stage
|
|
156
|
+
# exit `servedStage` is None while `verifyStage` names the stage the debt is
|
|
157
|
+
# owed on (REQ-OBS-01, REQ-DEBT-05).
|
|
158
|
+
verifyStage: str | None
|
|
159
|
+
# Which gate form to render, derived from verifyState and verifyCapability.
|
|
160
|
+
verifyGate: str
|
|
161
|
+
# Host-rendered verify command. Present whenever verification is reachable,
|
|
162
|
+
# even if it is not the primary action.
|
|
163
|
+
verifyCommand: str
|
|
164
|
+
# True when the caller must run in-stage verification before returning control.
|
|
165
|
+
# When True, the auto-verify-pending debt write has already been attempted —
|
|
166
|
+
# see `autoVerifyDebtRecorded` for whether it landed.
|
|
167
|
+
runInStageVerify: bool
|
|
168
|
+
# Effective autoVerify for THIS stage after applying autoVerifyStages overrides
|
|
169
|
+
# over the autoVerify default. Not the raw config value.
|
|
170
|
+
autoVerifyEffective: bool
|
|
171
|
+
# True whenever `runInStageVerify` is True — the scheduling boundary
|
|
172
|
+
# persists the auto-verify-pending marker BEFORE this payload exists, and a
|
|
173
|
+
# failed debt write raises UsageError with no payload at all. So
|
|
174
|
+
# `runInStageVerify: True` with `autoVerifyDebtRecorded: False` is UNREACHABLE;
|
|
175
|
+
# the field is carried so tests and downstream tools can assert that invariant
|
|
176
|
+
# rather than infer it. False with `runInStageVerify: False` simply means no
|
|
177
|
+
# debt was owed (REQ-DEBT-01/04, REQ-REL-02).
|
|
178
|
+
autoVerifyDebtRecorded: bool
|
|
179
|
+
# True when an autoFix chain may run unattended: autoFix configured, zero
|
|
180
|
+
# unresolved decision points, and a clean tree at the pre-scheduling snapshot.
|
|
181
|
+
autoFixEligible: bool
|
|
182
|
+
# Next production stage in pipeline order, or None at the end of the pipeline.
|
|
183
|
+
# Routing introspection only — never promote it over `primaryCommand`.
|
|
184
|
+
nextStage: str | None
|
|
185
|
+
# Host-rendered command for `nextStage`. Retained for compatibility; see the
|
|
186
|
+
# promotion rule below. None when `nextStage` is None.
|
|
187
|
+
nextCommand: str | None
|
|
188
|
+
# THE authoritative single action. While verification is unresolved this is the
|
|
189
|
+
# verify command — or the forge-fix command when a findings report is live at
|
|
190
|
+
# the current revision — never the downstream stage. The one fenced command in
|
|
191
|
+
# the rendered block. None only when the pipeline has no further action: a
|
|
192
|
+
# FINISHED EPIC (#248), whose block fences nothing at all because every command
|
|
193
|
+
# such an exit could fence is the dashboard it was just run from. A null here is
|
|
194
|
+
# terminal, not a gap to fill from `nextCommand` (which is null too).
|
|
195
|
+
primaryCommand: str | None
|
|
196
|
+
# Post-verification guidance shown as prose, never fenced, so it cannot be
|
|
197
|
+
# mistaken for the primary action. None when there is nothing deferred.
|
|
198
|
+
deferredCommand: str | None
|
|
199
|
+
# Keys in autoVerifyStages that name no verify-capable stage — a config typo.
|
|
200
|
+
# Empty list means the config was checked and clean; the key is always present
|
|
201
|
+
# when config was read at all, so [] and absent differ. Each key renders as
|
|
202
|
+
# exactly:
|
|
203
|
+
# Warning: autoVerifyStages key "{key}" names no verify-capable stage; it is
|
|
204
|
+
# ignored. Valid keys are forge-1-prd, forge-2-tech, forge-3-specs,
|
|
205
|
+
# forge-4-backlog, forge-5-loop.
|
|
206
|
+
# Keys are rendered in sorted order, per the determinism rule
|
|
207
|
+
# (REQ-OBS-02, REQ-REL-01).
|
|
208
|
+
invalidAutoVerifyKeys: list[str]
|
|
209
|
+
# Whether the working directory is a git repository at all.
|
|
210
|
+
gitRepo: bool
|
|
211
|
+
# Clean-tree snapshot taken BEFORE the pending-debt write, so the sanctioned
|
|
212
|
+
# state mutation does not dirty its own precondition. None when `gitRepo` is
|
|
213
|
+
# False — unknown, not clean.
|
|
214
|
+
cleanTree: bool | None
|
|
215
|
+
# Human-readable non-fatal advisories, in a fixed deterministic order:
|
|
216
|
+
# (1) the epic-member unreadable-state fallback, (2) the legacy/malformed
|
|
217
|
+
# scheduledStageVersion metadata warning, (3) the scheduled-vs-current
|
|
218
|
+
# revision mismatch note. A LIST,
|
|
219
|
+
# not a string, because these are independently triggerable and can co-occur
|
|
220
|
+
# on one call; a single string would force an implementer to drop or
|
|
221
|
+
# concatenate them, and REQ-REL-01's byte-identical-output requirement needs a
|
|
222
|
+
# defined order to assert against. Mirrors RenderStatus.warnings,
|
|
223
|
+
# which is already a list. Empty list means checked and clean; the key is
|
|
224
|
+
# always present, so [] and absent differ. Each entry names its affected
|
|
225
|
+
# feature/stage/key AND the recovery action (REQ-OBS-02).
|
|
226
|
+
warnings: list[str]
|
|
227
|
+
# Epic backflow directive; see EpicReconcile. Absent for standalone features.
|
|
228
|
+
epicReconcile: EpicReconcile
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class StageExitPayload(TypedDict):
|
|
232
|
+
"""Serialized direct or nested exit result.
|
|
233
|
+
|
|
234
|
+
Total (not `total=False`): all three keys are always present, and a nested
|
|
235
|
+
exit carries explicit nulls rather than omitting them.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
# Always populated, for both direct and nested exits.
|
|
239
|
+
directives: StageExitDirectives
|
|
240
|
+
# The rendered terminal block for a direct owner. MUST be None when
|
|
241
|
+
# `terminalOwnedBy == "outer"` — a nested caller has nothing to print.
|
|
242
|
+
nextSteps: str | None
|
|
243
|
+
# NEXT_STEPS_SENTINEL when this payload owns the terminal block, else None.
|
|
244
|
+
# When non-None, `nextSteps` ends with exactly this string and nothing follows
|
|
245
|
+
# it (REQ-EXIT-03). Carried explicitly so a consumer can verify termination
|
|
246
|
+
# without importing the constant.
|
|
247
|
+
sentinel: str | None
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class VerifyEntry(TypedDict, total=False):
|
|
251
|
+
"""Feature or epic verification state persisted by `state-verify`.
|
|
252
|
+
|
|
253
|
+
`total=False` is load-bearing: terminal writes DELETE the scheduling keys rather
|
|
254
|
+
than nulling them, so an absent `scheduledAt` means "not scheduled"
|
|
255
|
+
while a present-but-null one would be a malformed entry. Legacy entries written
|
|
256
|
+
before this feature simply lack the newer keys and load unmigrated
|
|
257
|
+
(REQ-DEBT-06).
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
# The entry's state. Always present on a written entry; a wholly absent entry
|
|
261
|
+
# means never verified, which is distinct from every value here.
|
|
262
|
+
status: VerifyStatus
|
|
263
|
+
# Path to the findings document, relative to the feature directory. Non-empty
|
|
264
|
+
# for `findings-reported`/`findings-applied`; absent otherwise.
|
|
265
|
+
findingsFile: str | None
|
|
266
|
+
# Findings count. 0 is legal and meaningful for `findings-reported` — verified
|
|
267
|
+
# with nothing found — and is not the same as the key being absent.
|
|
268
|
+
findingsCount: int | None
|
|
269
|
+
# UTC ISO-8601 timestamp of the terminal verification result. Absent while
|
|
270
|
+
# scheduling is pending.
|
|
271
|
+
verifiedAt: str | None
|
|
272
|
+
# UTC ISO-8601 timestamp set by `findings-applied`. Its presence alongside a
|
|
273
|
+
# deleted `verifiedStageVersion` is exactly what marks fixes-landed-but-
|
|
274
|
+
# unconfirmed.
|
|
275
|
+
fixedAt: str | None
|
|
276
|
+
# Full 40-character hash of the artifact commit for this entry, or null between
|
|
277
|
+
# commit 1 and commit 2 of the two-commit protocol. Never a short hash on a new
|
|
278
|
+
# write; legacy short hashes still READ (REQ-STATE-01/02).
|
|
279
|
+
commitHash: str | None
|
|
280
|
+
# Artifact revision this result verified — the production stage's `version` for
|
|
281
|
+
# a feature, the manifest `revision` for an epic. Deleted by `findings-applied`
|
|
282
|
+
# on purpose, so freshness stays unresolved until a later `passed` write.
|
|
283
|
+
verifiedStageVersion: int | None
|
|
284
|
+
# UTC ISO-8601 timestamp of the auto-verify schedule. Deleted (not nulled) by
|
|
285
|
+
# any terminal result.
|
|
286
|
+
scheduledAt: str | None
|
|
287
|
+
# Artifact revision current when verification was scheduled. Makes rescheduling
|
|
288
|
+
# idempotent — an identical revision does not rewrite the entry (REQ-REL-01) —
|
|
289
|
+
# and lets a read distinguish debt owed on the current artifact from debt
|
|
290
|
+
# stranded on an older one. Deleted by any terminal result.
|
|
291
|
+
scheduledStageVersion: int | None
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# --------------------------------------------------------------------------- #
|
|
295
|
+
# Config readers and loop-runner resolution
|
|
296
|
+
#
|
|
297
|
+
# These are the shared config/state primitives the package modules need. They are
|
|
298
|
+
# the package-internal home for logic the ``forge-session.py`` shim also carries
|
|
299
|
+
# inline: the shim keeps its own copies for its not-yet-extracted body and for the
|
|
300
|
+
# degraded bare-copy layout (a lone forge-session.py with no sibling package), so
|
|
301
|
+
# the two coexist until later #279 items drain the shim's inline copies. The
|
|
302
|
+
# behaviour is identical either way — a pure move, never a re-derivation.
|
|
303
|
+
#
|
|
304
|
+
# The duplicate-aware JSON loader below is the package layer's copy of the pair
|
|
305
|
+
# mirrored with the flat ``scripts/forge-bootstrap.py`` (byte-identical per
|
|
306
|
+
# tests/test_json_loader_parity.py). The mirror exists because forge-bootstrap.py is
|
|
307
|
+
# a standalone flat script — copied verbatim into per-agent bundles, sharing no import
|
|
308
|
+
# module — so it cannot import this one; the drift guard keeps the two shipped copies
|
|
309
|
+
# in lock-step. This IS the shared import module for the package (the shim and every
|
|
310
|
+
# sibling verb read config through this copy rather than each carrying their own).
|
|
311
|
+
# --------------------------------------------------------------------------- #
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
#: mirrors ``load_json_with_duplicates``/``warn_duplicate_keys`` in scripts/forge-bootstrap.py
|
|
315
|
+
def load_json_with_duplicates(path: Path) -> tuple[object, list[str]]:
|
|
316
|
+
"""Load JSON with last-key-wins values and ordered duplicate key names.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
path: UTF-8 JSON file to read.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
The parsed JSON value and duplicate key names in deterministic decoder-hook
|
|
323
|
+
order. A repeated occurrence is appended whenever its key was already seen
|
|
324
|
+
in that same object. Objects at every nesting depth use the hook.
|
|
325
|
+
|
|
326
|
+
Raises:
|
|
327
|
+
OSError: The path cannot be read as UTF-8 text.
|
|
328
|
+
json.JSONDecodeError: The file is not valid JSON.
|
|
329
|
+
"""
|
|
330
|
+
duplicate_keys: list[str] = []
|
|
331
|
+
|
|
332
|
+
def object_from_pairs(pairs: list[tuple[str, object]]) -> dict[str, object]:
|
|
333
|
+
result: dict[str, object] = {}
|
|
334
|
+
for key, value in pairs:
|
|
335
|
+
if key in result:
|
|
336
|
+
duplicate_keys.append(key)
|
|
337
|
+
result[key] = value
|
|
338
|
+
return result
|
|
339
|
+
|
|
340
|
+
text = path.read_text(encoding="utf-8")
|
|
341
|
+
value = json.loads(text, object_pairs_hook=object_from_pairs)
|
|
342
|
+
return value, duplicate_keys
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def warn_duplicate_keys(path: Path, duplicate_keys: list[str]) -> None:
|
|
346
|
+
"""Write one deterministic warning for each reported duplicate occurrence.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
path: Source file whose duplicate key was accepted.
|
|
350
|
+
duplicate_keys: Ordered names returned by `load_json_with_duplicates`.
|
|
351
|
+
|
|
352
|
+
Raises:
|
|
353
|
+
OSError: The process cannot write to stderr.
|
|
354
|
+
"""
|
|
355
|
+
for key in duplicate_keys:
|
|
356
|
+
rendered_key = json.dumps(key, ensure_ascii=False)
|
|
357
|
+
print(
|
|
358
|
+
f"Warning: duplicate JSON key {rendered_key} in {path}; "
|
|
359
|
+
"using the last value.",
|
|
360
|
+
file=sys.stderr,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _load_config(config_path: Path, *, warn: bool = True) -> dict:
|
|
365
|
+
"""Read config into a dict, warning on duplicates and tolerating bad input.
|
|
366
|
+
|
|
367
|
+
``warn=False`` suppresses the duplicate-key warning for a re-read whose warnings a
|
|
368
|
+
prior load in the same process already emitted — the doctor loads the config once for
|
|
369
|
+
its report, then re-resolves the loopRunner layers, and the warning must fire once (#324).
|
|
370
|
+
"""
|
|
371
|
+
try:
|
|
372
|
+
value, duplicate_keys = load_json_with_duplicates(config_path)
|
|
373
|
+
except (OSError, ValueError, RecursionError): # bad JSON, bad UTF-8, absurd nesting
|
|
374
|
+
return {}
|
|
375
|
+
if warn:
|
|
376
|
+
try:
|
|
377
|
+
warn_duplicate_keys(config_path, duplicate_keys)
|
|
378
|
+
except OSError:
|
|
379
|
+
pass # a diagnostic write failure must not break a total read path
|
|
380
|
+
return value if isinstance(value, dict) else {}
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _local_config_path(config_path: Path) -> Path:
|
|
384
|
+
"""Sibling machine-local overlay path: ``forge.config.json`` -> ``forge.config.local.json``.
|
|
385
|
+
|
|
386
|
+
Inserts a ``.local`` segment before the suffix so the overlay sits next to the committed
|
|
387
|
+
file under any name (#324). The overlay is a gitignored per-machine dev fact — never
|
|
388
|
+
committed — so ``forge-root``/``forge-init`` never write it.
|
|
389
|
+
"""
|
|
390
|
+
return config_path.with_name(f"{config_path.stem}.local{config_path.suffix}")
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _deep_merge(base: dict, overlay: dict) -> dict:
|
|
394
|
+
"""Return a new dict: ``overlay`` deep-merged over ``base`` (overlay wins).
|
|
395
|
+
|
|
396
|
+
Nested dicts merge recursively; scalars and lists replace wholesale (a list is a value,
|
|
397
|
+
not a mergeable structure). Neither input is mutated. Used to layer the machine-local
|
|
398
|
+
config over the committed one (#324) with the same "override the leaf, keep the rest"
|
|
399
|
+
semantics ``resolve_loop_runner`` already applies to schema defaults.
|
|
400
|
+
"""
|
|
401
|
+
result = dict(base)
|
|
402
|
+
for key, value in overlay.items():
|
|
403
|
+
existing = result.get(key)
|
|
404
|
+
if isinstance(existing, dict) and isinstance(value, dict):
|
|
405
|
+
result[key] = _deep_merge(existing, value)
|
|
406
|
+
else:
|
|
407
|
+
result[key] = value
|
|
408
|
+
return result
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def load_effective_config(config_path: Path) -> dict:
|
|
412
|
+
"""Committed ``forge.config.json`` with the machine-local overlay merged over it (#324).
|
|
413
|
+
|
|
414
|
+
Reads the committed config, then deep-merges an optional sibling ``forge.config.local.json``
|
|
415
|
+
(a gitignored per-machine dev override — pick a runner binary, a machine-specific
|
|
416
|
+
``contextWindowTokens``, …) over it, local winning. An absent or empty local file yields the
|
|
417
|
+
committed config unchanged, so this is a drop-in for ``_load_config`` at every *config*
|
|
418
|
+
call site. NOT for manifest reads (``.claude-plugin/plugin.json`` / the bundle sentinel),
|
|
419
|
+
which have no ``.local`` overlay concept — those stay on ``_load_config``.
|
|
420
|
+
"""
|
|
421
|
+
committed = _load_config(config_path)
|
|
422
|
+
local = _load_config(_local_config_path(config_path))
|
|
423
|
+
if not local:
|
|
424
|
+
return committed
|
|
425
|
+
return _deep_merge(committed, local)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _loop_runner_defaults(schema_path: Path) -> dict[str, object]:
|
|
429
|
+
"""Extract every ``loopRunner`` field's schema ``default``.
|
|
430
|
+
|
|
431
|
+
Reads ``properties.loopRunner.properties.<field>.default`` for each field.
|
|
432
|
+
Stdlib-only (``json`` + dict access), mirroring
|
|
433
|
+
``tests/test_config_defaults_parity.py``. The schema is the single source of
|
|
434
|
+
truth; nothing here is hardcoded.
|
|
435
|
+
|
|
436
|
+
Only fields that actually declare a ``default`` keyword are included. Every
|
|
437
|
+
``loopRunner`` field does today; a field losing its default would be a schema
|
|
438
|
+
regression the drift guard catches, not something silently patched here.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
schema_path: Path to ``forge-config-schema.json``.
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
A dict mapping each ``loopRunner`` field name to its declared default
|
|
445
|
+
value (templates such as ``"{bin} loop run …"`` are returned literally).
|
|
446
|
+
|
|
447
|
+
Raises:
|
|
448
|
+
UsageError: If the schema is missing, unreadable, unparseable, or lacks a
|
|
449
|
+
``loopRunner.properties`` object — a deterministic failure that must
|
|
450
|
+
exit 2. Never returns partial/empty defaults silently.
|
|
451
|
+
"""
|
|
452
|
+
try:
|
|
453
|
+
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
454
|
+
except OSError as exc:
|
|
455
|
+
raise UsageError(f"config schema unreadable: {schema_path} ({exc})") from exc
|
|
456
|
+
except json.JSONDecodeError as exc:
|
|
457
|
+
raise UsageError(f"config schema is not valid JSON: {schema_path} ({exc})") from exc
|
|
458
|
+
|
|
459
|
+
props = None
|
|
460
|
+
if isinstance(schema, dict):
|
|
461
|
+
loop_runner = schema.get("properties", {})
|
|
462
|
+
if isinstance(loop_runner, dict):
|
|
463
|
+
loop_runner = loop_runner.get("loopRunner", {})
|
|
464
|
+
if isinstance(loop_runner, dict):
|
|
465
|
+
props = loop_runner.get("properties")
|
|
466
|
+
if not isinstance(props, dict) or not props:
|
|
467
|
+
raise UsageError(f"config schema has no loopRunner.properties object: {schema_path}")
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
field: spec["default"]
|
|
471
|
+
for field, spec in props.items()
|
|
472
|
+
if isinstance(spec, dict) and "default" in spec
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def resolve_loop_runner(config_path: Path, schema_path: Path) -> dict[str, object]:
|
|
477
|
+
"""Resolve the effective ``loopRunner`` block the loop consumes.
|
|
478
|
+
|
|
479
|
+
The flat projection of :func:`resolve_loop_runner_layers`: every field's resolved value,
|
|
480
|
+
with the precedence env > machine-local > committed > schema default applied (#324). A field
|
|
481
|
+
absent from every user layer keeps its schema default; ``FEATURE_FORGE_LOOP_RUNNER_BIN``
|
|
482
|
+
overrides ``bin`` only. Computed deterministically so no model ever merges it by hand; the
|
|
483
|
+
per-field provenance is available via :func:`resolve_loop_runner_layers` for ``doctor``.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
config_path: Path to ``forge.config.json`` (a missing/corrupt file — and a missing
|
|
487
|
+
local overlay — are tolerated, yielding pure defaults).
|
|
488
|
+
schema_path: Path to ``forge-config-schema.json`` (source of the defaults).
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
The resolved ``loopRunner`` object: every schema-defaulted field present, user
|
|
492
|
+
overrides applied.
|
|
493
|
+
|
|
494
|
+
Raises:
|
|
495
|
+
UsageError: If the schema is unreadable/unparseable (propagated from
|
|
496
|
+
``_loop_runner_defaults``) — exit 2, a deterministic failure.
|
|
497
|
+
"""
|
|
498
|
+
return {
|
|
499
|
+
field: info["value"]
|
|
500
|
+
for field, info in resolve_loop_runner_layers(config_path, schema_path).items()
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
#: The environment override for ``loopRunner.bin`` (#324): highest precedence, ``bin`` only.
|
|
505
|
+
#: A one-shot ``rauf-dev`` run or a CI pin sets this without touching any file.
|
|
506
|
+
LOOP_RUNNER_BIN_ENV: Final = "FEATURE_FORGE_LOOP_RUNNER_BIN"
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def resolve_loop_runner_layers(
|
|
510
|
+
config_path: Path, schema_path: Path, *, warn: bool = True
|
|
511
|
+
) -> dict[str, dict[str, object]]:
|
|
512
|
+
"""Resolve ``loopRunner`` with per-field provenance (#324).
|
|
513
|
+
|
|
514
|
+
Returns ``{field: {"value": <value>, "layer": <layer>}}`` where ``layer`` is one of
|
|
515
|
+
``"env"``, ``"local"``, ``"committed"``, ``"default"`` — the precedence order (highest
|
|
516
|
+
first): the ``FEATURE_FORGE_LOOP_RUNNER_BIN`` env var (``bin`` only) over the machine-local
|
|
517
|
+
``forge.config.local.json`` over the committed ``forge.config.json`` over the schema
|
|
518
|
+
default. Fields present in a user file but not in the schema defaults (unknown keys) are
|
|
519
|
+
carried through with their originating layer — the config schema is the authority that
|
|
520
|
+
flags them at author time, exactly as the flat resolver did. ``doctor`` reports these
|
|
521
|
+
layers; ``resolve_loop_runner`` projects just the values.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
config_path: Path to ``forge.config.json``.
|
|
525
|
+
schema_path: Path to ``forge-config-schema.json`` (source of the defaults).
|
|
526
|
+
warn: Emit the duplicate-key warning while reading the config files. ``False`` for a
|
|
527
|
+
re-read whose warnings a prior load already emitted (the doctor path).
|
|
528
|
+
|
|
529
|
+
Raises:
|
|
530
|
+
UsageError: If the schema is unreadable/unparseable (from ``_loop_runner_defaults``).
|
|
531
|
+
"""
|
|
532
|
+
defaults = _loop_runner_defaults(schema_path)
|
|
533
|
+
committed = _load_config(config_path, warn=warn).get("loopRunner")
|
|
534
|
+
committed = committed if isinstance(committed, dict) else {}
|
|
535
|
+
local = _load_config(_local_config_path(config_path), warn=warn).get("loopRunner")
|
|
536
|
+
local = local if isinstance(local, dict) else {}
|
|
537
|
+
env_bin = os.environ.get(LOOP_RUNNER_BIN_ENV)
|
|
538
|
+
|
|
539
|
+
layered: dict[str, dict[str, object]] = {}
|
|
540
|
+
# Every field any layer mentions, defaults first so their order leads; dict.fromkeys
|
|
541
|
+
# dedupes a key that appears in more than one layer (e.g. an unknown key in both files).
|
|
542
|
+
fields = list(dict.fromkeys([*defaults, *committed, *local]))
|
|
543
|
+
for field in fields:
|
|
544
|
+
if field == "bin" and env_bin:
|
|
545
|
+
layered[field] = {"value": env_bin, "layer": "env"}
|
|
546
|
+
elif field in local:
|
|
547
|
+
layered[field] = {"value": local[field], "layer": "local"}
|
|
548
|
+
elif field in committed:
|
|
549
|
+
layered[field] = {"value": committed[field], "layer": "committed"}
|
|
550
|
+
else:
|
|
551
|
+
layered[field] = {"value": defaults[field], "layer": "default"}
|
|
552
|
+
return layered
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
# --------------------------------------------------------------------------- #
|
|
556
|
+
# State writes (shared machinery for the state-* and decision-* writers)
|
|
557
|
+
# --------------------------------------------------------------------------- #
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _now_iso() -> str:
|
|
561
|
+
"""Return the current UTC time as a Z-suffixed, second-precision ISO-8601 string.
|
|
562
|
+
|
|
563
|
+
Matches the `.pipeline-state.json` timestamp convention already on disk (the
|
|
564
|
+
schema's ``format: date-time`` values; the read path normalizes a trailing
|
|
565
|
+
``Z``). Second precision keeps `updatedAt`/`startedAt`/`completedAt` visually
|
|
566
|
+
consistent with the values other pipeline writers produce.
|
|
567
|
+
|
|
568
|
+
Returns:
|
|
569
|
+
A timestamp like ``"2026-07-29T03:30:00Z"``.
|
|
570
|
+
"""
|
|
571
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _write_state(state_path: Path, state: dict) -> None:
|
|
575
|
+
"""Atomically write a `.pipeline-state.json` (temp file + os.replace).
|
|
576
|
+
|
|
577
|
+
Mirrors epic-manifest.py's ``atomic_write``: write to a sibling temp file in
|
|
578
|
+
the same directory as the target, flush + fsync the bytes, then os.replace()
|
|
579
|
+
the temp file onto the target. os.replace is atomic on POSIX within one
|
|
580
|
+
filesystem, so an interrupted write never leaves a partial or corrupt state
|
|
581
|
+
file. Concurrent multi-session mutation is out of scope (single writer
|
|
582
|
+
assumed, matching epic-manifest.py; decision record:
|
|
583
|
+
references/decisions/single-writer-threat-model.md, issue #180).
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
state_path: Destination path, e.g.
|
|
587
|
+
``{specsDir}/{feature}/.pipeline-state.json``.
|
|
588
|
+
state: The fully-formed state dict to serialize.
|
|
589
|
+
|
|
590
|
+
Raises:
|
|
591
|
+
UsageError: If the temp file cannot be created/written or the replace
|
|
592
|
+
fails (→ exit 2). The temp file is removed first, so a failed write
|
|
593
|
+
leaves no debris and the original target untouched.
|
|
594
|
+
"""
|
|
595
|
+
try:
|
|
596
|
+
fd, tmp_name = tempfile.mkstemp(
|
|
597
|
+
prefix=f".{state_path.name}.", suffix=".tmp", dir=state_path.parent
|
|
598
|
+
)
|
|
599
|
+
except OSError as exc:
|
|
600
|
+
raise UsageError(f"atomic write to {state_path} failed: {exc}") from exc
|
|
601
|
+
tmp_path = Path(tmp_name)
|
|
602
|
+
try:
|
|
603
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
604
|
+
json.dump(state, handle, indent=2, ensure_ascii=False)
|
|
605
|
+
handle.write("\n")
|
|
606
|
+
handle.flush()
|
|
607
|
+
os.fsync(handle.fileno())
|
|
608
|
+
os.replace(tmp_path, state_path)
|
|
609
|
+
except OSError as exc:
|
|
610
|
+
tmp_path.unlink(missing_ok=True)
|
|
611
|
+
raise UsageError(f"atomic write to {state_path} failed: {exc}") from exc
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _commit_state(state_path: Path, state: dict) -> dict:
|
|
615
|
+
"""Refresh ``updatedAt`` and write ``state`` atomically; return it for echo.
|
|
616
|
+
|
|
617
|
+
Every verb calls this exactly once, after its mutation, so ``updatedAt`` is
|
|
618
|
+
always refreshed on a successful write and the write is atomic.
|
|
619
|
+
|
|
620
|
+
Args:
|
|
621
|
+
state_path: The resolved state-file path — a feature's
|
|
622
|
+
``.pipeline-state.json``, or an epic's ``.epic-state.json``. The helper
|
|
623
|
+
is target-agnostic: it stamps and writes whatever document it is given,
|
|
624
|
+
so an epic write reuses the same atomic mechanism without
|
|
625
|
+
going anywhere near the member resolver.
|
|
626
|
+
state: The mutated state dict.
|
|
627
|
+
|
|
628
|
+
Returns:
|
|
629
|
+
The same ``state`` dict (now carrying a fresh ``updatedAt``), so the verb
|
|
630
|
+
can echo it under ``--json``.
|
|
631
|
+
|
|
632
|
+
Raises:
|
|
633
|
+
UsageError: If the atomic write fails (→ exit 2).
|
|
634
|
+
"""
|
|
635
|
+
state["updatedAt"] = _now_iso()
|
|
636
|
+
_write_state(state_path, state)
|
|
637
|
+
return state
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
# --------------------------------------------------------------------------- #
|
|
642
|
+
# Shared pipeline-state readers and the recency-ranked feature rows
|
|
643
|
+
#
|
|
644
|
+
# The read-side chain the discover/reconcile/epic-base cluster needs (via
|
|
645
|
+
# ``build_rows``) and that the state/outcome clusters will share too. Copied here
|
|
646
|
+
# from the ``forge-session.py`` shim, which keeps its own inline copies for its
|
|
647
|
+
# not-yet-extracted body and the degraded bare-copy layout (a lone forge-session.py
|
|
648
|
+
# whose stage-exit/doctor paths reach this chain). A pure move, byte-identical to
|
|
649
|
+
# the shim; later #279 items drain the shim's copies once every reader lives here.
|
|
650
|
+
# --------------------------------------------------------------------------- #
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
#: A directory is "feature-shaped" iff it directly contains this file.
|
|
654
|
+
PIPELINE_STATE_FILENAME: Final = ".pipeline-state.json"
|
|
655
|
+
#: Epic roots hold this (and no .pipeline-state.json) — never a feature.
|
|
656
|
+
MANIFEST_FILENAME: Final = "epic-manifest.json"
|
|
657
|
+
#: Epic-scoped verification state, sibling to the manifest. NEVER a member's
|
|
658
|
+
#: .pipeline-state.json: epic verification is epic-scoped (REQ-SEC-01).
|
|
659
|
+
EPIC_STATE_FILENAME: Final = ".epic-state.json"
|
|
660
|
+
|
|
661
|
+
#: A safe bare name: one kebab-case token, no separator, no traversal. Same pattern
|
|
662
|
+
#: epic-manifest.py applies (the flat scripts share no import module), so the epic
|
|
663
|
+
#: target of a state write fails closed exactly where the canonical resolver does
|
|
664
|
+
#: (REQ-SEC-01).
|
|
665
|
+
SAFE_NAME_RE: Final = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
|
666
|
+
|
|
667
|
+
#: New non-null commit hashes are full 40-hex only. Loaded legacy short hashes stay
|
|
668
|
+
#: readable — this validates WRITES, and no schema constrains commitHash.
|
|
669
|
+
FULL_GIT_HASH_RE: Final = re.compile(r"[0-9a-fA-F]{40}")
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
#: The ordered production stages. This is the ONE place stage order lives.
|
|
673
|
+
PRODUCTION_STAGES: Final[tuple[str, ...]] = (
|
|
674
|
+
"forge-1-prd",
|
|
675
|
+
"forge-2-tech",
|
|
676
|
+
"forge-3-specs",
|
|
677
|
+
"forge-4-backlog",
|
|
678
|
+
"forge-5-loop",
|
|
679
|
+
"forge-6-docs",
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
#: Production stage -> the verify token its findings file uses, and the
|
|
684
|
+
#: `forge-verify-<token>` key its state lives under. forge-6-docs has no verify.
|
|
685
|
+
VERIFY_TOKEN_BY_STAGE: Final[dict[str, str]] = {
|
|
686
|
+
"forge-1-prd": "prd",
|
|
687
|
+
"forge-2-tech": "tech",
|
|
688
|
+
"forge-3-specs": "specs",
|
|
689
|
+
"forge-4-backlog": "backlog",
|
|
690
|
+
"forge-5-loop": "impl",
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
#: The `--stage` domain for `state-verify`: forge-0-epic (whose verification lives
|
|
694
|
+
#: in the epic's own `.epic-state.json`) plus the five stages that carry a verify
|
|
695
|
+
#: token. forge-6-docs is excluded on purpose — it has no verification token, so
|
|
696
|
+
#: there is no `forge-verify-*` key for it to write.
|
|
697
|
+
VERIFY_STAGES: Final[tuple[str, ...]] = ("forge-0-epic", *VERIFY_TOKEN_BY_STAGE)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
#: The terminal status the completion writer records (and the commit-hash
|
|
701
|
+
#: follow-up requires) — NOT the whole "done for selection" set below.
|
|
702
|
+
_DONE_STATUS: Final = "complete"
|
|
703
|
+
#: Production stage statuses that count as "done" for next-stage selection.
|
|
704
|
+
#: `skipped` is legal only on forge-6-docs (schema: `docsStageEntry`) — an
|
|
705
|
+
#: explicitly skipped documentation stage ends the pipeline without claiming
|
|
706
|
+
#: artifacts it never produced (#197). Selection treats the status as done
|
|
707
|
+
#: wherever it appears; the schema is what confines it to the docs stage.
|
|
708
|
+
_DONE_STATUSES: Final = frozenset({_DONE_STATUS, "skipped"})
|
|
709
|
+
#: The authoritative forge-verify status vocabulary. SOURCE OF TRUTH:
|
|
710
|
+
#: references/pipeline-state-schema.json (definitions.verifyEntry.properties.status.enum).
|
|
711
|
+
#: A status outside this set is unrecognized and must not be silently interpreted (#148).
|
|
712
|
+
#: NOTE: epic-manifest.py keeps a byte-identical copy — flat, self-contained scripts have
|
|
713
|
+
#: no shared import module (each is copied verbatim into per-agent adapter bundles).
|
|
714
|
+
KNOWN_VERIFY_STATUSES: Final = frozenset(
|
|
715
|
+
{
|
|
716
|
+
"pending",
|
|
717
|
+
"auto-verify-pending",
|
|
718
|
+
"passed",
|
|
719
|
+
"findings-reported",
|
|
720
|
+
"findings-applied",
|
|
721
|
+
"skipped",
|
|
722
|
+
}
|
|
723
|
+
)
|
|
724
|
+
#: Verify statuses that count as "resolved" (no outstanding verify needed). A STRICT
|
|
725
|
+
#: subset of KNOWN_VERIFY_STATUSES — not collapsible into it (different meaning).
|
|
726
|
+
#: `auto-verify-pending` is deliberately ABSENT: owed-but-unrun debt is not resolved.
|
|
727
|
+
_VERIFY_RESOLVED: Final = frozenset({"passed", "findings-applied", "skipped"})
|
|
728
|
+
#: Prior verify statuses a `skipped` result write may NOT replace (#203). Mirrors
|
|
729
|
+
#: epic-manifest.py's `_VERIFY_ORCH_COMPLETE`: these two statuses make an epic
|
|
730
|
+
#: member complete-for-orchestration, so silently replacing one with `skipped`
|
|
731
|
+
#: demoted the member out of the rollup and fabricated unmetDeps on every
|
|
732
|
+
#: dependent — the observed 5/6 → 1/6 collapse. A deferral over one of these
|
|
733
|
+
#: needs NO write: the recorded result already carries the outstanding state.
|
|
734
|
+
#: NOT the same set as `_VERIFY_RESOLVED` (`skipped` re-writing `skipped` is a
|
|
735
|
+
#: harmless idempotent refresh and stays legal).
|
|
736
|
+
_SKIP_PROTECTED_PRIOR: Final = frozenset({"passed", "findings-applied"})
|
|
737
|
+
#: The `state-verify --status` domain: every VerifyStatus a result write may record.
|
|
738
|
+
#: `pending` is excluded — it is the pre-existing generic/manual pending marker, not
|
|
739
|
+
#: a verification RESULT, and `auto-verify-pending` is the value that carries owed
|
|
740
|
+
#: automatic debt. Derived so the two lists cannot drift.
|
|
741
|
+
VERIFY_RESULT_STATUSES: Final[tuple[str, ...]] = tuple(
|
|
742
|
+
status for status in get_args(VerifyStatus) if status != "pending"
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
#: Per-process dedupe for the unknown-verify-status diagnostic (#148) so a single
|
|
747
|
+
#: bogus status is flagged once, not once per verify_state() call in a command.
|
|
748
|
+
_UNKNOWN_VERIFY_WARNED: set[str] = set()
|
|
749
|
+
#: Per-process dedupe for the auto-verify debt-metadata diagnostic, same reason.
|
|
750
|
+
_AUTO_VERIFY_DEBT_WARNED: set[str] = set()
|
|
751
|
+
#: The single normative sentence every read-side emitter uses for owed-but-unrun
|
|
752
|
+
#: automatic verification. One line naming the
|
|
753
|
+
#: subject, the served stage, and the retry command — never a state-file dump.
|
|
754
|
+
AUTO_PENDING_DIAGNOSTIC: Final = (
|
|
755
|
+
"{subject}: automatic verification is still pending for {stage}; "
|
|
756
|
+
"run {command} to resolve it."
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def _read_state(state_path: Path) -> dict:
|
|
761
|
+
"""Read a `.pipeline-state.json`, tolerating missing/corrupt files.
|
|
762
|
+
|
|
763
|
+
A missing, unreadable, or unparseable state downgrades to ``{}`` rather than
|
|
764
|
+
crashing the scan — the navigator simply treats that feature as not-started.
|
|
765
|
+
"""
|
|
766
|
+
try:
|
|
767
|
+
parsed = json.loads(state_path.read_text(encoding="utf-8"))
|
|
768
|
+
except (OSError, ValueError, RecursionError): # bad JSON, bad UTF-8, absurd nesting
|
|
769
|
+
return {}
|
|
770
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def _scan_features(specs_dir: Path) -> list[tuple[str, str | None, dict]]:
|
|
774
|
+
"""Find every feature-shaped dir under the specs tree (flat + nested).
|
|
775
|
+
|
|
776
|
+
Descends exactly one level below each top-level dir (never deeper), matching
|
|
777
|
+
``epic-manifest.py``'s feature-shaped-dir bound.
|
|
778
|
+
|
|
779
|
+
Args:
|
|
780
|
+
specs_dir: The configured specs directory.
|
|
781
|
+
|
|
782
|
+
Returns:
|
|
783
|
+
A list of ``(feature_name, epic_name_or_None, state_dict)`` tuples. The
|
|
784
|
+
epic name is the parent dir name for a nested member, ``None`` for a flat
|
|
785
|
+
feature.
|
|
786
|
+
"""
|
|
787
|
+
if not specs_dir.is_dir():
|
|
788
|
+
return []
|
|
789
|
+
out: list[tuple[str, str | None, dict]] = []
|
|
790
|
+
for top in sorted(p for p in specs_dir.iterdir() if p.is_dir()):
|
|
791
|
+
flat_state = top / PIPELINE_STATE_FILENAME
|
|
792
|
+
if flat_state.is_file():
|
|
793
|
+
out.append((top.name, None, _read_state(flat_state)))
|
|
794
|
+
# Descend one level for nested epic members (skip the epic root itself).
|
|
795
|
+
for child in sorted(p for p in top.iterdir() if p.is_dir()):
|
|
796
|
+
nested_state = child / PIPELINE_STATE_FILENAME
|
|
797
|
+
if nested_state.is_file():
|
|
798
|
+
out.append((child.name, top.name, _read_state(nested_state)))
|
|
799
|
+
return out
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def _stage_status(state: dict, stage: str) -> str | None:
|
|
803
|
+
"""Return the recorded status of a stage, or None if absent."""
|
|
804
|
+
stages = state.get("stages")
|
|
805
|
+
if not isinstance(stages, dict):
|
|
806
|
+
return None
|
|
807
|
+
entry = stages.get(stage)
|
|
808
|
+
if not isinstance(entry, dict):
|
|
809
|
+
return None
|
|
810
|
+
status = entry.get("status")
|
|
811
|
+
return status if isinstance(status, str) else None
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def next_stage(state: dict) -> str | None:
|
|
815
|
+
"""Return the first production stage that is not yet complete (the next step).
|
|
816
|
+
|
|
817
|
+
Walks ``PRODUCTION_STAGES`` in order and returns the first whose recorded
|
|
818
|
+
status is not in ``_DONE_STATUSES`` (a missing/pending/in-progress/stale
|
|
819
|
+
stage all count as "not done"; ``complete`` and a forge-6-docs ``skipped``
|
|
820
|
+
both count as done). Returns ``None`` when every production stage is done
|
|
821
|
+
(nothing left to run).
|
|
822
|
+
|
|
823
|
+
This is the derived "what runs next" value — the single source of truth for
|
|
824
|
+
the next stage. It is intentionally distinct from the stored
|
|
825
|
+
``currentStage`` field ("where the pipeline IS"; see the schema): the next
|
|
826
|
+
stage is computed from ``stages[].status`` here, never read from
|
|
827
|
+
``currentStage``.
|
|
828
|
+
"""
|
|
829
|
+
for stage in PRODUCTION_STAGES:
|
|
830
|
+
if _stage_status(state, stage) not in _DONE_STATUSES:
|
|
831
|
+
return stage
|
|
832
|
+
return None
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def _stage_version(state: dict, stage: str) -> int | None:
|
|
836
|
+
"""Return the recorded ``version`` of a stage entry, or None if absent."""
|
|
837
|
+
stages = state.get("stages")
|
|
838
|
+
if not isinstance(stages, dict):
|
|
839
|
+
return None
|
|
840
|
+
entry = stages.get(stage)
|
|
841
|
+
if not isinstance(entry, dict):
|
|
842
|
+
return None
|
|
843
|
+
version = entry.get("version")
|
|
844
|
+
return version if isinstance(version, int) else None
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def _verify_entry(state: dict, verify_key: str) -> dict:
|
|
848
|
+
"""Return the ``forge-verify-*`` entry dict, or ``{}`` if absent."""
|
|
849
|
+
stages = state.get("stages")
|
|
850
|
+
if not isinstance(stages, dict):
|
|
851
|
+
return {}
|
|
852
|
+
entry = stages.get(verify_key)
|
|
853
|
+
return entry if isinstance(entry, dict) else {}
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def _warn_unknown_verify_status(stage_name: str, status: object) -> None:
|
|
857
|
+
"""Emit a one-time stderr diagnostic for an out-of-vocabulary verify status (#148).
|
|
858
|
+
|
|
859
|
+
The freshness classifier maps an unrecognized status to "never verified" — correct,
|
|
860
|
+
but silent, so a typo poisons the downstream gate (e.g. forge-5-loop's dependency
|
|
861
|
+
check) with no clue. Flagging it here makes the bad value visible where it is read.
|
|
862
|
+
"""
|
|
863
|
+
key = f"{stage_name}={status!r}"
|
|
864
|
+
if key in _UNKNOWN_VERIFY_WARNED:
|
|
865
|
+
return
|
|
866
|
+
_UNKNOWN_VERIFY_WARNED.add(key)
|
|
867
|
+
known = ", ".join(sorted(KNOWN_VERIFY_STATUSES))
|
|
868
|
+
print(
|
|
869
|
+
f"feature-forge: unknown {stage_name} status {status!r} "
|
|
870
|
+
f"(treated as unverified; expected one of {known})",
|
|
871
|
+
file=sys.stderr,
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def _scheduled_stage_version(entry: dict) -> int | None:
|
|
876
|
+
"""Return an ``auto-verify-pending`` entry's usable ``scheduledStageVersion``.
|
|
877
|
+
|
|
878
|
+
``None`` when the field is absent, a bool, a non-integer, or below 1 — i.e.
|
|
879
|
+
legacy state written before the scheduling fields existed, or hand-edited
|
|
880
|
+
state. The caller stays ``auto-pending`` either way: unusable metadata is a
|
|
881
|
+
reason to warn, never a reason to forget the debt.
|
|
882
|
+
"""
|
|
883
|
+
version = entry.get("scheduledStageVersion")
|
|
884
|
+
if isinstance(version, bool) or not isinstance(version, int) or version < 1:
|
|
885
|
+
return None
|
|
886
|
+
return version
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def _warn_auto_verify_debt_metadata(verify_key: str) -> None:
|
|
890
|
+
"""Flag an ``auto-verify-pending`` entry whose scheduled revision is unusable.
|
|
891
|
+
|
|
892
|
+
Without a recorded revision the debt cannot be compared against the current
|
|
893
|
+
artifact, so it can be neither discharged as fresh nor described as advanced.
|
|
894
|
+
It REMAINS outstanding — the alternative (degrading to ``never``) is exactly
|
|
895
|
+
the conflation REQ-DEBT-02 forbids — but the operator needs to know why the
|
|
896
|
+
row carries no revision detail, so say it once per process.
|
|
897
|
+
"""
|
|
898
|
+
if verify_key in _AUTO_VERIFY_DEBT_WARNED:
|
|
899
|
+
return
|
|
900
|
+
_AUTO_VERIFY_DEBT_WARNED.add(verify_key)
|
|
901
|
+
print(
|
|
902
|
+
f"feature-forge: {verify_key} is auto-verify-pending but its "
|
|
903
|
+
"scheduledStageVersion is missing or malformed (legacy or hand-edited "
|
|
904
|
+
"state); the debt stays outstanding — re-run forge-verify to resolve it "
|
|
905
|
+
"and record a usable schedule",
|
|
906
|
+
file=sys.stderr,
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def auto_pending_message(
|
|
911
|
+
subject: str,
|
|
912
|
+
stage: str,
|
|
913
|
+
command: str,
|
|
914
|
+
scheduled_version: int | None = None,
|
|
915
|
+
current_version: int | None = None,
|
|
916
|
+
) -> str:
|
|
917
|
+
"""Render the diagnostic for owed-but-unrun automatic verification.
|
|
918
|
+
|
|
919
|
+
Args:
|
|
920
|
+
subject: The feature or epic the debt belongs to.
|
|
921
|
+
stage: The served production stage the debt is owed on.
|
|
922
|
+
command: The host-translated forge-verify retry command.
|
|
923
|
+
scheduled_version: Revision the debt was recorded against, if usable.
|
|
924
|
+
current_version: The artifact's current revision, if known.
|
|
925
|
+
|
|
926
|
+
Returns:
|
|
927
|
+
One sentence, with both revision numbers appended when the recorded
|
|
928
|
+
schedule predates the current artifact. Never a state-file dump.
|
|
929
|
+
"""
|
|
930
|
+
message = AUTO_PENDING_DIAGNOSTIC.format(
|
|
931
|
+
subject=subject, stage=stage, command=command
|
|
932
|
+
)
|
|
933
|
+
if (
|
|
934
|
+
scheduled_version is not None
|
|
935
|
+
and current_version is not None
|
|
936
|
+
and scheduled_version != current_version
|
|
937
|
+
):
|
|
938
|
+
message += (
|
|
939
|
+
f" The artifact has advanced since it was scheduled "
|
|
940
|
+
f"(scheduled at revision {scheduled_version}, now at revision "
|
|
941
|
+
f"{current_version})."
|
|
942
|
+
)
|
|
943
|
+
return message
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
def verify_state(state: dict) -> tuple[str | None, str]:
|
|
947
|
+
"""Classify verify freshness for the most-recently-completed stage.
|
|
948
|
+
|
|
949
|
+
Returns ``(stage, state_label)`` where ``state_label`` is one of:
|
|
950
|
+
|
|
951
|
+
- ``fresh`` — the entry is ``passed`` AND its ``verifiedStageVersion`` matches
|
|
952
|
+
the stage's current ``version`` (so no re-verify is needed). ``passed`` is the
|
|
953
|
+
ONLY status that reaches ``fresh``: ``findings-applied`` and ``skipped`` are
|
|
954
|
+
resolved but never fresh, for the reasons given below.
|
|
955
|
+
- ``stale`` — verify was resolved once, but the stage version has since moved
|
|
956
|
+
(artifact revised) OR the entry predates the freshness ledger (no
|
|
957
|
+
``verifiedStageVersion``), OR the entry is ``findings-applied``, which never
|
|
958
|
+
classifies ``fresh`` regardless of any version it carries (§4.2 step 4).
|
|
959
|
+
A revised artifact must be re-verified.
|
|
960
|
+
- ``failing`` — verify ran and reported findings that are not yet applied
|
|
961
|
+
(``findings-reported``).
|
|
962
|
+
- ``auto-pending`` — effective configuration scheduled unattended in-stage
|
|
963
|
+
verification and nothing has discharged it: the obligation is RECORDED and
|
|
964
|
+
owed. Deliberately distinct from ``never`` (nobody ever asked for it), from
|
|
965
|
+
manual ``pending`` work, and from every resolved label — a dropped
|
|
966
|
+
``runInStageVerify`` directive is precisely what this makes visible (#163,
|
|
967
|
+
REQ-DEBT-02). Classified BEFORE the generic unresolved handling below, and
|
|
968
|
+
never downgraded when its scheduling metadata is missing or malformed.
|
|
969
|
+
- ``never`` — the stage completed but verify has not run at all.
|
|
970
|
+
- ``skipped`` — the user explicitly chose to proceed without verifying. A
|
|
971
|
+
resolved, non-pending state: it is deliberately NOT re-offered or
|
|
972
|
+
auto-verified, and (unlike a genuine verification result) it does not go
|
|
973
|
+
stale on an artifact revision — skip writers record no version to compare
|
|
974
|
+
against, and re-surfacing would override an explicit human decision.
|
|
975
|
+
- ``none`` — no completed verify-capable stage (nothing to verify), stage
|
|
976
|
+
is ``None``.
|
|
977
|
+
|
|
978
|
+
Only the most-recent completed production stage is considered, matching the
|
|
979
|
+
navigator's "verify before continuing" gate. A ``findings-applied`` entry is
|
|
980
|
+
treated as ``stale`` UNCONDITIONALLY — applying fixes is not verifying them —
|
|
981
|
+
and an absent ``verifiedStageVersion`` on a ``passed`` entry (legacy state) is
|
|
982
|
+
likewise ``stale``: verify rather than skip.
|
|
983
|
+
"""
|
|
984
|
+
for stage in reversed(PRODUCTION_STAGES):
|
|
985
|
+
if _stage_status(state, stage) not in _DONE_STATUSES:
|
|
986
|
+
continue
|
|
987
|
+
token = VERIFY_TOKEN_BY_STAGE.get(stage)
|
|
988
|
+
if token is None:
|
|
989
|
+
continue # forge-6-docs has no verify step
|
|
990
|
+
entry = _verify_entry(state, f"forge-verify-{token}")
|
|
991
|
+
status = entry.get("status")
|
|
992
|
+
if status is not None and not isinstance(status, str):
|
|
993
|
+
# A torn or hand-edited entry can carry any JSON type here; an
|
|
994
|
+
# unhashable one would raise TypeError at the frozenset membership
|
|
995
|
+
# below, crashing the navigator on one bad file. Same answer as an
|
|
996
|
+
# absent entry — and the same #148 diagnostic as an unknown string,
|
|
997
|
+
# so the degradation is never silent.
|
|
998
|
+
_warn_unknown_verify_status(f"forge-verify-{token}", status)
|
|
999
|
+
return stage, "never"
|
|
1000
|
+
if status == "skipped":
|
|
1001
|
+
# An explicit skip is resolved and non-pending — preserve the user's
|
|
1002
|
+
# decision. It never goes stale (no recorded version to compare), so
|
|
1003
|
+
# the freshness check below deliberately does not apply.
|
|
1004
|
+
return stage, "skipped"
|
|
1005
|
+
if status == "auto-verify-pending":
|
|
1006
|
+
# Ordered ahead of the generic unresolved branch so recorded debt can
|
|
1007
|
+
# never fall through to "never". Unusable metadata warns and stays
|
|
1008
|
+
# owed; a superseded revision stays owed too.
|
|
1009
|
+
if _scheduled_stage_version(entry) is None:
|
|
1010
|
+
_warn_auto_verify_debt_metadata(f"forge-verify-{token}")
|
|
1011
|
+
return stage, "auto-pending"
|
|
1012
|
+
if status not in _VERIFY_RESOLVED:
|
|
1013
|
+
if status == "findings-reported":
|
|
1014
|
+
return stage, "failing"
|
|
1015
|
+
# An unrecognized status (outside KNOWN_VERIFY_STATUSES) is treated as
|
|
1016
|
+
# "never verified" — defensible, but flag it once so a typo (e.g. the
|
|
1017
|
+
# eye-slip 'findings-resolved') doesn't silently poison the gate that
|
|
1018
|
+
# reads this label (#148). ``pending``/``None`` are known/absent → quiet.
|
|
1019
|
+
if status is not None and status not in KNOWN_VERIFY_STATUSES:
|
|
1020
|
+
_warn_unknown_verify_status(f"forge-verify-{token}", status)
|
|
1021
|
+
return stage, "never"
|
|
1022
|
+
if status == "findings-applied":
|
|
1023
|
+
# Applying fixes is not verifying them: §4.2 step 4 says `findings-applied`
|
|
1024
|
+
# CLEARS freshness, and only a later `passed` restores it. The writer builds
|
|
1025
|
+
# the entry without `verifiedStageVersion`, but the read side may not rely on
|
|
1026
|
+
# that — REQ-DEBT-06 requires loading legacy state without migration, and a
|
|
1027
|
+
# pre-writer entry can still carry the key. Without this guard such an entry
|
|
1028
|
+
# reads `fresh`, `pending_verify` returns None, and the verification debt for
|
|
1029
|
+
# a fixed-but-never-re-verified stage disappears silently.
|
|
1030
|
+
return stage, "stale"
|
|
1031
|
+
verified_version = entry.get("verifiedStageVersion")
|
|
1032
|
+
stage_version = _stage_version(state, stage)
|
|
1033
|
+
if (
|
|
1034
|
+
isinstance(verified_version, int)
|
|
1035
|
+
and stage_version is not None
|
|
1036
|
+
and verified_version == stage_version
|
|
1037
|
+
):
|
|
1038
|
+
return stage, "fresh"
|
|
1039
|
+
return stage, "stale"
|
|
1040
|
+
return None, "none"
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
def _parse_ts(value: str | None) -> datetime | None:
|
|
1044
|
+
"""Parse an ISO-8601 timestamp (tolerating a trailing 'Z'), else None."""
|
|
1045
|
+
if not isinstance(value, str):
|
|
1046
|
+
return None
|
|
1047
|
+
try:
|
|
1048
|
+
dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
1049
|
+
except ValueError:
|
|
1050
|
+
return None
|
|
1051
|
+
if dt.tzinfo is None:
|
|
1052
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
1053
|
+
return dt
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
def build_rows(specs_dir: Path, config: dict | None = None) -> list[FeatureRow]:
|
|
1057
|
+
"""Build the recency-ranked active-feature rows (the rank-features payload).
|
|
1058
|
+
|
|
1059
|
+
Active features (``pipelineStatus == "active"``, the default when absent) are
|
|
1060
|
+
sorted by ``updatedAt`` descending — most recently touched first — so the
|
|
1061
|
+
navigator's recency default is row 0.
|
|
1062
|
+
|
|
1063
|
+
``config`` is the loaded forge.config.json (or ``{}``); it drives the effective
|
|
1064
|
+
``autoVerify``/``autoFix`` per stage so the navigator can branch without
|
|
1065
|
+
re-reading config.
|
|
1066
|
+
|
|
1067
|
+
A row whose verify classifies ``auto-pending`` carries recorded-but-undischarged
|
|
1068
|
+
automatic verification: ``verifyPending`` is True, ``verifyState`` is
|
|
1069
|
+
``auto-pending``, and ``verifyCommand`` is non-null, so no consumer can read it
|
|
1070
|
+
as verification-complete. The named sentence goes to stderr (this is the
|
|
1071
|
+
one emitter that knows the feature name); stdout keeps the three stable JSON
|
|
1072
|
+
keys — ``verifyState``, ``verifyStage``, ``verifyCommand`` — and no prose.
|
|
1073
|
+
"""
|
|
1074
|
+
config = config or {}
|
|
1075
|
+
# Fail closed: only a literal JSON ``true`` enables artifact-mutating autoFix.
|
|
1076
|
+
global_auto_fix = config.get("autoFix") is True
|
|
1077
|
+
rows: list[FeatureRow] = []
|
|
1078
|
+
for name, epic, state in _scan_features(specs_dir):
|
|
1079
|
+
status = state.get("pipelineStatus", "active")
|
|
1080
|
+
if status != "active":
|
|
1081
|
+
continue
|
|
1082
|
+
nxt = next_stage(state)
|
|
1083
|
+
vstage, vlabel = verify_state(state)
|
|
1084
|
+
verify_pending = vstage is not None and vlabel not in ("fresh", "none", "skipped")
|
|
1085
|
+
effective_auto_verify = auto_verify_for(config, vstage) if vstage else False
|
|
1086
|
+
verify_command = f"/feature-forge:forge-verify {name}" if verify_pending else None
|
|
1087
|
+
if vlabel == "auto-pending" and vstage is not None and verify_command:
|
|
1088
|
+
token = VERIFY_TOKEN_BY_STAGE.get(vstage)
|
|
1089
|
+
entry = _verify_entry(state, f"forge-verify-{token}") if token else {}
|
|
1090
|
+
print(
|
|
1091
|
+
auto_pending_message(
|
|
1092
|
+
name,
|
|
1093
|
+
vstage,
|
|
1094
|
+
verify_command,
|
|
1095
|
+
_scheduled_stage_version(entry),
|
|
1096
|
+
_stage_version(state, vstage),
|
|
1097
|
+
),
|
|
1098
|
+
file=sys.stderr,
|
|
1099
|
+
)
|
|
1100
|
+
branch = state.get("branch")
|
|
1101
|
+
updated = state.get("updatedAt")
|
|
1102
|
+
rows.append({
|
|
1103
|
+
"name": name,
|
|
1104
|
+
"epic": epic,
|
|
1105
|
+
# currentStage = "where the pipeline IS" (the recorded field). When a
|
|
1106
|
+
# legacy/absent state omits it, fall back to the DERIVED next stage
|
|
1107
|
+
# for display only — never conflate the two elsewhere (schema O1).
|
|
1108
|
+
"currentStage": state.get("currentStage") or (nxt or "complete"),
|
|
1109
|
+
"branch": branch if isinstance(branch, str) else None,
|
|
1110
|
+
"updatedAt": updated if isinstance(updated, str) else None,
|
|
1111
|
+
"complete": nxt is None,
|
|
1112
|
+
"nextStage": nxt,
|
|
1113
|
+
"nextCommand": f"/feature-forge:{nxt} {name}" if nxt else None,
|
|
1114
|
+
"verifyPending": verify_pending,
|
|
1115
|
+
"verifyCommand": verify_command,
|
|
1116
|
+
"verifyStage": vstage,
|
|
1117
|
+
"verifyState": vlabel,
|
|
1118
|
+
"autoVerify": effective_auto_verify,
|
|
1119
|
+
"autoFix": global_auto_fix and effective_auto_verify,
|
|
1120
|
+
# Single resolved verify-gate classification (5b — one exit computation,
|
|
1121
|
+
# mirroring stage-exit's `verifyGate`): the navigator reads this instead of
|
|
1122
|
+
# re-deriving from verifyPending + autoVerify in prose. `auto` = the §2b
|
|
1123
|
+
# catch-up runs it unattended; `standard` = the §3 gate (degrades to
|
|
1124
|
+
# manual-print on a non-Claude host); `none` = nothing outstanding.
|
|
1125
|
+
"verifyGate": (
|
|
1126
|
+
"none" if not verify_pending
|
|
1127
|
+
else "auto" if effective_auto_verify
|
|
1128
|
+
else "standard"
|
|
1129
|
+
),
|
|
1130
|
+
})
|
|
1131
|
+
# Sort by updatedAt desc; rows without a parseable timestamp sort last.
|
|
1132
|
+
rows.sort(
|
|
1133
|
+
key=lambda r: (_parse_ts(r["updatedAt"]) or datetime.min.replace(tzinfo=timezone.utc)),
|
|
1134
|
+
reverse=True,
|
|
1135
|
+
)
|
|
1136
|
+
return rows
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
def auto_verify_for(config: dict, stage: str) -> bool:
|
|
1140
|
+
"""Return the effective auto-verify setting for ``stage``.
|
|
1141
|
+
|
|
1142
|
+
Per-stage override in ``autoVerifyStages`` wins over the global ``autoVerify``;
|
|
1143
|
+
both default to off, so a config with neither key means "no auto-verify".
|
|
1144
|
+
|
|
1145
|
+
Parsing is strict and **fails closed**: only a literal JSON ``true`` enables
|
|
1146
|
+
auto-verify. A non-boolean value (e.g. the string ``"false"``, which is truthy
|
|
1147
|
+
in Python) is treated as off, not on. The schema already rejects non-booleans
|
|
1148
|
+
at author time; this guards a hand-edited config from silently enabling
|
|
1149
|
+
automation.
|
|
1150
|
+
"""
|
|
1151
|
+
stages = config.get("autoVerifyStages")
|
|
1152
|
+
if isinstance(stages, dict) and stage in stages:
|
|
1153
|
+
return stages[stage] is True
|
|
1154
|
+
return config.get("autoVerify") is True
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
def _git_output(args: list[str]) -> str | None:
|
|
1158
|
+
"""Run a read-only git command and return stripped stdout, or None.
|
|
1159
|
+
|
|
1160
|
+
Any failure (git missing, not a repo, nonzero exit, timeout) degrades to
|
|
1161
|
+
``None`` — doctor reports absence rather than crashing.
|
|
1162
|
+
"""
|
|
1163
|
+
try:
|
|
1164
|
+
proc = subprocess.run(
|
|
1165
|
+
["git", *args], capture_output=True, text=True, timeout=10,
|
|
1166
|
+
)
|
|
1167
|
+
except (OSError, ValueError, subprocess.TimeoutExpired): # ValueError: undecodable output
|
|
1168
|
+
return None
|
|
1169
|
+
if proc.returncode != 0:
|
|
1170
|
+
return None
|
|
1171
|
+
out = proc.stdout.strip()
|
|
1172
|
+
return out or None
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
def _default_branch() -> str | None:
|
|
1176
|
+
"""The repo's default branch: origin/HEAD target, else `main`/`master` if present."""
|
|
1177
|
+
ref = _git_output(["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"])
|
|
1178
|
+
if ref:
|
|
1179
|
+
return ref.rsplit("/", 1)[-1]
|
|
1180
|
+
for cand in ("main", "master"):
|
|
1181
|
+
if _git_output(["rev-parse", "--verify", "--quiet", f"refs/heads/{cand}"]) is not None:
|
|
1182
|
+
return cand
|
|
1183
|
+
return None
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
def _counts(specs_dir: Path) -> dict[str, int]:
|
|
1187
|
+
"""Tally active/paused/abandoned pipelines across the specs tree."""
|
|
1188
|
+
tally = {"active": 0, "paused": 0, "abandoned": 0}
|
|
1189
|
+
for _name, _epic, state in _scan_features(specs_dir):
|
|
1190
|
+
status = state.get("pipelineStatus", "active")
|
|
1191
|
+
if isinstance(status, str) and status in tally:
|
|
1192
|
+
tally[status] += 1
|
|
1193
|
+
return tally
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
def _config_duplicate_keys(config_path: Path) -> list[str]:
|
|
1197
|
+
"""Duplicate key names in the config file, for doctor's health report.
|
|
1198
|
+
|
|
1199
|
+
Empty on a missing/unreadable/invalid config — those conditions are
|
|
1200
|
+
reported by doctor's ``configExists`` field, not here.
|
|
1201
|
+
"""
|
|
1202
|
+
try:
|
|
1203
|
+
return load_json_with_duplicates(config_path)[1]
|
|
1204
|
+
except (OSError, ValueError, RecursionError): # bad JSON, bad UTF-8, absurd nesting
|
|
1205
|
+
return []
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
def invalid_auto_verify_keys(config: dict) -> list[str]:
|
|
1209
|
+
"""Return ``autoVerifyStages`` keys outside the verify-capable stage ids.
|
|
1210
|
+
|
|
1211
|
+
An unknown/typo key (e.g. ``forge-1-prod``) would silently never take effect,
|
|
1212
|
+
turning an intended off-switch into a no-op. Surfacing it lets the navigator
|
|
1213
|
+
warn instead of failing quietly. Mirrors the schema's ``propertyNames.enum``.
|
|
1214
|
+
|
|
1215
|
+
Sorted, not insertion-ordered: every diagnostic list must be
|
|
1216
|
+
sorted before rendering, so two configs that differ only in key order produce
|
|
1217
|
+
byte-identical output.
|
|
1218
|
+
"""
|
|
1219
|
+
stages = config.get("autoVerifyStages")
|
|
1220
|
+
if not isinstance(stages, dict):
|
|
1221
|
+
return []
|
|
1222
|
+
return sorted(key for key in stages if key not in VERIFY_TOKEN_BY_STAGE)
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
def _default_schema_path() -> Path:
|
|
1226
|
+
"""Return the bundled forge-config-schema.json path (sibling references/ dir).
|
|
1227
|
+
|
|
1228
|
+
Resolved relative to this module so it works from any cwd. This module lives at
|
|
1229
|
+
``<scripts>/forge_session/_common.py`` — one level deeper than the legacy shim —
|
|
1230
|
+
so it walks three parents to the bundle root, matching the shim's target.
|
|
1231
|
+
Overridable via the ``--schema`` flag (chiefly for tests).
|
|
1232
|
+
|
|
1233
|
+
Returns:
|
|
1234
|
+
The Path to ``references/forge-config-schema.json`` at the bundle root.
|
|
1235
|
+
"""
|
|
1236
|
+
return Path(__file__).resolve().parent.parent.parent / "references" / "forge-config-schema.json"
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
#: Verify token per exit stage. Extends the production map with the epic stage,
|
|
1240
|
+
#: whose verify entry is recorded under ``forge-verify-epic``. The shim keeps a
|
|
1241
|
+
#: byte-equal inline copy for its not-yet-extracted stage-exit routing body; this
|
|
1242
|
+
#: is the package-side source the outcomes module reads (#279 P4.1).
|
|
1243
|
+
_EXIT_VERIFY_TOKEN: Final[dict[str, str]] = {
|
|
1244
|
+
**VERIFY_TOKEN_BY_STAGE,
|
|
1245
|
+
"forge-0-epic": "epic",
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
#: The one message table the three upstream-verify gates read instead of each
|
|
1249
|
+
#: phrasing "not verified" its own way — one canonical operator sentence per
|
|
1250
|
+
#: ``VerifyStateCase``. ``{subject}`` is the feature (or epic member), ``{stage}``
|
|
1251
|
+
#: the production stage whose verification this describes, ``{command}`` the
|
|
1252
|
+
#: forge-verify retry invocation. ``auto-verify-pending`` is NOT looked up here at
|
|
1253
|
+
#: runtime: that case routes through ``auto_pending_message()`` (which reads this
|
|
1254
|
+
#: same ``AUTO_PENDING_DIAGNOSTIC`` and appends the version-advance clause when the
|
|
1255
|
+
#: schedule predates the artifact), so the value is a *reference* to that shared
|
|
1256
|
+
#: constant, not a copy that could drift. It stays in the table so the map is
|
|
1257
|
+
#: complete: one entry per case, the invariant ``test_one_message_per_case`` pins.
|
|
1258
|
+
VERIFY_STATE_MESSAGES: Final[dict[str, str]] = {
|
|
1259
|
+
"passed": "{subject}'s {stage} verification passed; proceed.",
|
|
1260
|
+
"findings-reported": (
|
|
1261
|
+
"{subject}'s {stage} verification reported unresolved blocking findings; "
|
|
1262
|
+
"apply and re-verify them — run {command}."
|
|
1263
|
+
),
|
|
1264
|
+
"findings-applied": (
|
|
1265
|
+
"Fixes were applied to {subject}'s {stage} but nothing re-verified them; "
|
|
1266
|
+
"re-verification is still outstanding — run {command}."
|
|
1267
|
+
),
|
|
1268
|
+
"auto-verify-pending": AUTO_PENDING_DIAGNOSTIC,
|
|
1269
|
+
"skipped": (
|
|
1270
|
+
"{subject}'s {stage} verification was explicitly skipped; "
|
|
1271
|
+
"run {command} to verify it after all."
|
|
1272
|
+
),
|
|
1273
|
+
"never": "{subject}'s {stage} hasn't been verified yet — run {command}.",
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
def _resolve_feature_dir(specs_dir: Path, feature: str, epic: str | None) -> Path:
|
|
1278
|
+
"""Best-effort feature dir (flat, else unique nested, else flat literal).
|
|
1279
|
+
|
|
1280
|
+
stage-exit tolerates an unresolvable dir — the state read downgrades to
|
|
1281
|
+
``{}`` and every directive still computes from defaults.
|
|
1282
|
+
"""
|
|
1283
|
+
if epic:
|
|
1284
|
+
return specs_dir / epic / feature
|
|
1285
|
+
flat = specs_dir / feature
|
|
1286
|
+
if (flat / PIPELINE_STATE_FILENAME).is_file():
|
|
1287
|
+
return flat
|
|
1288
|
+
if specs_dir.is_dir():
|
|
1289
|
+
nested = [
|
|
1290
|
+
p for p in specs_dir.glob(f"*/{feature}")
|
|
1291
|
+
if (p / PIPELINE_STATE_FILENAME).is_file()
|
|
1292
|
+
]
|
|
1293
|
+
if len(nested) == 1:
|
|
1294
|
+
return nested[0]
|
|
1295
|
+
return flat
|
|
1296
|
+
|
|
1297
|
+
|
|
1298
|
+
|
|
1299
|
+
|
|
1300
|
+
# --------------------------------------------------------------------------- #
|
|
1301
|
+
# Stage-exit domain (mirror of the shim's copies; item 007). The shim keeps its
|
|
1302
|
+
# own inline definitions (source-grep + parity tests read them there); these
|
|
1303
|
+
# copies exist so the extracted exit/routes modules import them from here
|
|
1304
|
+
# rather than the shim (which would be circular). Item 008 drains the shim copy.
|
|
1305
|
+
# --------------------------------------------------------------------------- #
|
|
1306
|
+
|
|
1307
|
+
#: The directive-facing form of the debt-metadata advisory (`warnings` entry 2).
|
|
1308
|
+
#: The stderr twin lives in `_warn_auto_verify_debt_metadata`; this one also
|
|
1309
|
+
#: names the subject and the host-translated retry command, because a `warnings` entry
|
|
1310
|
+
#: must carry both the affected feature/stage/key AND the recovery action (REQ-OBS-02).
|
|
1311
|
+
AUTO_VERIFY_DEBT_METADATA_DIAGNOSTIC: Final = (
|
|
1312
|
+
"{subject}: {verify_key} is auto-verify-pending but its scheduledStageVersion "
|
|
1313
|
+
"is missing or malformed (legacy or hand-edited state); the debt stays "
|
|
1314
|
+
"outstanding — run {command} to resolve it and record a usable schedule."
|
|
1315
|
+
)
|
|
1316
|
+
|
|
1317
|
+
#: The exact template for an `autoVerifyStages` key that names no
|
|
1318
|
+
#: verify-capable stage. A typo there silently never takes effect, so the exit
|
|
1319
|
+
#: says so — once per offending key, in sorted key order, on stderr, and
|
|
1320
|
+
#: WITHOUT failing the exit: an ignored config key is an advisory, not a usage
|
|
1321
|
+
#: error. `{valid}` is derived from `VERIFY_TOKEN_BY_STAGE` so the sentence cannot
|
|
1322
|
+
#: drift from the domain it describes.
|
|
1323
|
+
INVALID_AUTO_VERIFY_KEY_WARNING: Final = (
|
|
1324
|
+
'Warning: autoVerifyStages key "{key}" names no verify-capable stage; it is '
|
|
1325
|
+
"ignored. Valid keys are {valid}."
|
|
1326
|
+
)
|
|
1327
|
+
|
|
1328
|
+
#: The exact template for an epic edit-mode member whose live pipeline state
|
|
1329
|
+
#: cannot be resolved. It is `warnings` entry 1 and the router's
|
|
1330
|
+
#: ONE tolerant new case: the exit degrades DOWN to `forge-1-prd <member>` rather
|
|
1331
|
+
#: than fabricating progress it could not read (REQ-PROD-06). The trailing sentence
|
|
1332
|
+
#: is what makes the warning name both the affected feature and the recovery action
|
|
1333
|
+
#: (REQ-OBS-02); `{reason}` is one of `EPIC_MEMBER_FALLBACK_REASONS`.
|
|
1334
|
+
EPIC_MEMBER_FALLBACK_WARNING: Final = (
|
|
1335
|
+
"Warning: {member}: pipeline state could not be resolved under epic {epic} "
|
|
1336
|
+
"({reason}); routing to forge-1-prd. Run /feature-forge:forge {member} to "
|
|
1337
|
+
"inspect its state."
|
|
1338
|
+
)
|
|
1339
|
+
#: The closed reason domain for `EPIC_MEMBER_FALLBACK_WARNING`. No other
|
|
1340
|
+
#: value may be substituted — `tests/test_stage_exit.py` asserts the literal.
|
|
1341
|
+
EPIC_MEMBER_FALLBACK_REASONS: Final[tuple[str, ...]] = (
|
|
1342
|
+
"missing",
|
|
1343
|
+
"unreadable",
|
|
1344
|
+
"malformed",
|
|
1345
|
+
"not a member of this epic",
|
|
1346
|
+
)
|
|
1347
|
+
|
|
1348
|
+
#: The seven stages that produce a pipeline artifact. forge-0-epic participates in
|
|
1349
|
+
#: exit and verify routing but not the member production walk (PRODUCTION_STAGES).
|
|
1350
|
+
ProductionStage = Literal[
|
|
1351
|
+
"forge-0-epic",
|
|
1352
|
+
"forge-1-prd",
|
|
1353
|
+
"forge-2-tech",
|
|
1354
|
+
"forge-3-specs",
|
|
1355
|
+
"forge-4-backlog",
|
|
1356
|
+
"forge-5-loop",
|
|
1357
|
+
"forge-6-docs",
|
|
1358
|
+
]
|
|
1359
|
+
|
|
1360
|
+
#: Every skill that closes a stage through `stage-exit` — the seven production
|
|
1361
|
+
#: stages plus the two branch skills.
|
|
1362
|
+
ExitStage = Literal[
|
|
1363
|
+
"forge-0-epic",
|
|
1364
|
+
"forge-1-prd",
|
|
1365
|
+
"forge-2-tech",
|
|
1366
|
+
"forge-3-specs",
|
|
1367
|
+
"forge-4-backlog",
|
|
1368
|
+
"forge-5-loop",
|
|
1369
|
+
"forge-6-docs",
|
|
1370
|
+
"forge-verify",
|
|
1371
|
+
"forge-fix",
|
|
1372
|
+
]
|
|
1373
|
+
|
|
1374
|
+
#: forge-verify's mode, which selects the production stage a diversion served.
|
|
1375
|
+
VerifyMode = Literal["epic", "prd", "tech", "specs", "backlog", "impl"]
|
|
1376
|
+
|
|
1377
|
+
#: Who prints the terminal block for a branch exit.
|
|
1378
|
+
ExitOwner = Literal["direct", "nested"]
|
|
1379
|
+
|
|
1380
|
+
#: Whether the host may run an interactive verify gate + clean-room dispatch.
|
|
1381
|
+
VerifyCapability = Literal["interactive", "manual"]
|
|
1382
|
+
|
|
1383
|
+
#: The navigator/stage-exit freshness label for an artifact's verification.
|
|
1384
|
+
VerifyStateLabel = Literal[
|
|
1385
|
+
"fresh", "stale", "failing", "never", "auto-pending", "skipped", "none"
|
|
1386
|
+
]
|
|
1387
|
+
|
|
1388
|
+
#: Which gate form a stage exit asks the caller to render.
|
|
1389
|
+
VerifyGate = Literal["none", "standard", "manual-print"]
|
|
1390
|
+
|
|
1391
|
+
#: The union of the verification-state cases the three upstream-verify gates branch
|
|
1392
|
+
#: on — the raw `forge-verify-*` statuses they key on, plus `never` for the absent/
|
|
1393
|
+
#: `pending`/unrecognized bucket every gate folds into "not verified". `verify-state`
|
|
1394
|
+
#: maps a served stage's entry to exactly one of these; `test_verify_state.py` pins
|
|
1395
|
+
#: the enum to the three skill bodies so a gate cannot grow a seventh case or spell an
|
|
1396
|
+
#: existing one differently.
|
|
1397
|
+
VerifyStateCase = Literal[
|
|
1398
|
+
"passed",
|
|
1399
|
+
"findings-reported",
|
|
1400
|
+
"findings-applied",
|
|
1401
|
+
"auto-verify-pending",
|
|
1402
|
+
"skipped",
|
|
1403
|
+
"never",
|
|
1404
|
+
]
|
|
1405
|
+
|
|
1406
|
+
LoopOutcome = Literal[
|
|
1407
|
+
"complete", "partial", "blocked", "needs-human", "deferred", "resolved"
|
|
1408
|
+
]
|
|
1409
|
+
|
|
1410
|
+
DocsOutcome = Literal["complete", "blocked", "skipped"]
|
|
1411
|
+
|
|
1412
|
+
VerifyOutcome = Literal["passed", "findings", "skipped", "failed"]
|
|
1413
|
+
|
|
1414
|
+
FixOutcome = Literal[
|
|
1415
|
+
"no-findings",
|
|
1416
|
+
"decisions",
|
|
1417
|
+
"failed",
|
|
1418
|
+
"applied",
|
|
1419
|
+
"reverified",
|
|
1420
|
+
"reverify-findings",
|
|
1421
|
+
"deferred",
|
|
1422
|
+
]
|
|
1423
|
+
|
|
1424
|
+
#: Derived, never hand-listed — see the block comment above.
|
|
1425
|
+
EXIT_STAGES: Final[tuple[str, ...]] = get_args(ExitStage)
|
|
1426
|
+
|
|
1427
|
+
#: The stages whose exit carries a multi-way outcome, and each one's legal values.
|
|
1428
|
+
#: Stages absent from this table take no `--outcome` at all.
|
|
1429
|
+
EXIT_OUTCOMES: Final[dict[str, frozenset[str]]] = {
|
|
1430
|
+
"forge-5-loop": frozenset(get_args(LoopOutcome)),
|
|
1431
|
+
"forge-6-docs": frozenset(get_args(DocsOutcome)),
|
|
1432
|
+
"forge-verify": frozenset(get_args(VerifyOutcome)),
|
|
1433
|
+
"forge-fix": frozenset(get_args(FixOutcome)),
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
#: The one domain still written twice, because neither side is a subset of the
|
|
1437
|
+
#: other: its keys MUST equal set(get_args(VerifyMode)) and its values MUST be a
|
|
1438
|
+
#: subset of get_args(ProductionStage). tests/test_stage_constants_parity.py
|
|
1439
|
+
#: asserts both. NOT collapsible into VERIFY_TOKEN_BY_STAGE's inverse — that map
|
|
1440
|
+
#: has no `epic` mode and exists to name state keys, not to route stages.
|
|
1441
|
+
VERIFY_MODE_TO_STAGE: Final[dict[str, str]] = {
|
|
1442
|
+
"epic": "forge-0-epic",
|
|
1443
|
+
"prd": "forge-1-prd",
|
|
1444
|
+
"tech": "forge-2-tech",
|
|
1445
|
+
"specs": "forge-3-specs",
|
|
1446
|
+
"backlog": "forge-4-backlog",
|
|
1447
|
+
"impl": "forge-5-loop",
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
#: The fixed final line of the NEXT-STEPS block. The stamp instructs the skill
|
|
1451
|
+
#: to print the block verbatim as its absolute last output — nothing after this.
|
|
1452
|
+
NEXT_STEPS_SENTINEL: Final = "─ forge: end of stage ─"
|
|
1453
|
+
|
|
1454
|
+
#: The `--host` domain: command syntax and fresh-session wording only. A host NEVER
|
|
1455
|
+
#: implies a verification capability (REQ-EXIT-07).
|
|
1456
|
+
EXIT_HOSTS: Final[tuple[str, ...]] = ("claude", "generic", "pi")
|
|
1457
|
+
|
|
1458
|
+
#: Stage id -> the noun phrase gate wording uses (the old {stage} stamp slot).
|
|
1459
|
+
STAGE_NOUN: Final[dict[str, str]] = {
|
|
1460
|
+
"forge-0-epic": "the epic decomposition",
|
|
1461
|
+
"forge-1-prd": "the PRD",
|
|
1462
|
+
"forge-2-tech": "the tech spec",
|
|
1463
|
+
"forge-3-specs": "the implementation specs",
|
|
1464
|
+
"forge-4-backlog": "the backlog",
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
def pending_verify(state: dict) -> str | None:
|
|
1468
|
+
"""Return the production stage whose verify is outstanding, if any.
|
|
1469
|
+
|
|
1470
|
+
Outstanding means the most-recently-completed production stage's verify is not
|
|
1471
|
+
``fresh`` (never run, scheduled-but-unrun automatic verification, reported
|
|
1472
|
+
findings, or gone stale after an artifact revision). An ``auto-pending`` stage
|
|
1473
|
+
is returned like any other outstanding one — recorded debt is owed work, and
|
|
1474
|
+
``_VERIFY_RESOLVED`` deliberately excludes it.
|
|
1475
|
+
An explicit ``skipped`` is treated as resolved (never outstanding).
|
|
1476
|
+
Surfaced so the navigator can offer "verify before continuing" as an
|
|
1477
|
+
alternative to advancing. Returns ``None`` when the latest stage is fresh,
|
|
1478
|
+
skipped, or there is nothing to verify.
|
|
1479
|
+
"""
|
|
1480
|
+
stage, label = verify_state(state)
|
|
1481
|
+
return stage if label not in ("fresh", "none", "skipped") else None
|
|
1482
|
+
|
|
1483
|
+
|
|
1484
|
+
__all__ = [
|
|
1485
|
+
"UsageError",
|
|
1486
|
+
"VerifyStatus",
|
|
1487
|
+
"FeatureRow",
|
|
1488
|
+
"EpicReconcile",
|
|
1489
|
+
"StageExitDirectives",
|
|
1490
|
+
"StageExitPayload",
|
|
1491
|
+
"VerifyEntry",
|
|
1492
|
+
"load_json_with_duplicates",
|
|
1493
|
+
"warn_duplicate_keys",
|
|
1494
|
+
"_load_config",
|
|
1495
|
+
"load_effective_config",
|
|
1496
|
+
"_loop_runner_defaults",
|
|
1497
|
+
"resolve_loop_runner",
|
|
1498
|
+
"resolve_loop_runner_layers",
|
|
1499
|
+
"LOOP_RUNNER_BIN_ENV",
|
|
1500
|
+
"_now_iso",
|
|
1501
|
+
"_write_state",
|
|
1502
|
+
"_commit_state",
|
|
1503
|
+
"PIPELINE_STATE_FILENAME",
|
|
1504
|
+
"MANIFEST_FILENAME",
|
|
1505
|
+
"EPIC_STATE_FILENAME",
|
|
1506
|
+
"SAFE_NAME_RE",
|
|
1507
|
+
"FULL_GIT_HASH_RE",
|
|
1508
|
+
"PRODUCTION_STAGES",
|
|
1509
|
+
"VERIFY_TOKEN_BY_STAGE",
|
|
1510
|
+
"VERIFY_STAGES",
|
|
1511
|
+
"KNOWN_VERIFY_STATUSES",
|
|
1512
|
+
"_DONE_STATUS",
|
|
1513
|
+
"_DONE_STATUSES",
|
|
1514
|
+
"_SKIP_PROTECTED_PRIOR",
|
|
1515
|
+
"_VERIFY_RESOLVED",
|
|
1516
|
+
"VERIFY_RESULT_STATUSES",
|
|
1517
|
+
"AUTO_PENDING_DIAGNOSTIC",
|
|
1518
|
+
"_UNKNOWN_VERIFY_WARNED",
|
|
1519
|
+
"_AUTO_VERIFY_DEBT_WARNED",
|
|
1520
|
+
"_read_state",
|
|
1521
|
+
"_scan_features",
|
|
1522
|
+
"_stage_status",
|
|
1523
|
+
"next_stage",
|
|
1524
|
+
"_stage_version",
|
|
1525
|
+
"_verify_entry",
|
|
1526
|
+
"_warn_unknown_verify_status",
|
|
1527
|
+
"_scheduled_stage_version",
|
|
1528
|
+
"_warn_auto_verify_debt_metadata",
|
|
1529
|
+
"auto_pending_message",
|
|
1530
|
+
"verify_state",
|
|
1531
|
+
"auto_verify_for",
|
|
1532
|
+
"_parse_ts",
|
|
1533
|
+
"build_rows",
|
|
1534
|
+
"_git_output",
|
|
1535
|
+
"_default_branch",
|
|
1536
|
+
"_counts",
|
|
1537
|
+
"_config_duplicate_keys",
|
|
1538
|
+
"invalid_auto_verify_keys",
|
|
1539
|
+
"_default_schema_path",
|
|
1540
|
+
"_EXIT_VERIFY_TOKEN",
|
|
1541
|
+
"VERIFY_STATE_MESSAGES",
|
|
1542
|
+
"_resolve_feature_dir",
|
|
1543
|
+
"ProductionStage",
|
|
1544
|
+
"ExitStage",
|
|
1545
|
+
"VerifyMode",
|
|
1546
|
+
"ExitOwner",
|
|
1547
|
+
"VerifyCapability",
|
|
1548
|
+
"VerifyStateLabel",
|
|
1549
|
+
"VerifyGate",
|
|
1550
|
+
"VerifyStateCase",
|
|
1551
|
+
"LoopOutcome",
|
|
1552
|
+
"DocsOutcome",
|
|
1553
|
+
"VerifyOutcome",
|
|
1554
|
+
"FixOutcome",
|
|
1555
|
+
"EXIT_STAGES",
|
|
1556
|
+
"EXIT_OUTCOMES",
|
|
1557
|
+
"VERIFY_MODE_TO_STAGE",
|
|
1558
|
+
"NEXT_STEPS_SENTINEL",
|
|
1559
|
+
"EXIT_HOSTS",
|
|
1560
|
+
"STAGE_NOUN",
|
|
1561
|
+
"AUTO_VERIFY_DEBT_METADATA_DIAGNOSTIC",
|
|
1562
|
+
"INVALID_AUTO_VERIFY_KEY_WARNING",
|
|
1563
|
+
"EPIC_MEMBER_FALLBACK_WARNING",
|
|
1564
|
+
"EPIC_MEMBER_FALLBACK_REASONS",
|
|
1565
|
+
"pending_verify",
|
|
1566
|
+
]
|