@garygentry/feature-forge 0.2.14 → 0.3.1
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 +6 -3
- package/adapters/GENERATION-REPORT.md +20 -0
- package/adapters/claude/.feature-forge-bundle.json +1 -1
- package/adapters/claude/agents/forge-verifier.md +1 -1
- package/adapters/claude/references/forge-config-schema.json +2 -2
- package/adapters/claude/references/pipeline-state-schema.json +1 -1
- package/adapters/claude/references/shared-conventions.md +62 -12
- package/adapters/claude/references/stage-exit-protocol.md +14 -4
- package/adapters/claude/references/vendor-construct-inventory.md +1 -1
- package/adapters/claude/scripts/epic-manifest.py +49 -6
- package/adapters/claude/scripts/forge-root.sh +47 -3
- package/adapters/claude/scripts/forge-session.py +1179 -9
- package/adapters/claude/scripts/validate-traceability.py +6 -1
- package/adapters/claude/skills/forge/SKILL.md +11 -5
- package/adapters/claude/skills/forge/references/pipeline-state-schema.json +1 -1
- package/adapters/claude/skills/forge/references/shared-conventions.md +62 -12
- package/adapters/claude/skills/forge/references/stage-exit-protocol.md +14 -4
- package/adapters/claude/skills/forge-0-epic/SKILL.md +1 -1
- package/adapters/claude/skills/forge-0-epic/references/pipeline-state-schema.json +1 -1
- package/adapters/claude/skills/forge-0-epic/references/shared-conventions.md +62 -12
- package/adapters/claude/skills/forge-0-epic/references/stage-exit-protocol.md +14 -4
- package/adapters/claude/skills/forge-1-prd/SKILL.md +25 -8
- package/adapters/claude/skills/forge-1-prd/references/shared-conventions.md +62 -12
- package/adapters/claude/skills/forge-1-prd/references/stage-exit-protocol.md +14 -4
- package/adapters/claude/skills/forge-2-tech/SKILL.md +25 -7
- package/adapters/claude/skills/forge-2-tech/references/shared-conventions.md +62 -12
- package/adapters/claude/skills/forge-2-tech/references/stage-exit-protocol.md +14 -4
- package/adapters/claude/skills/forge-3-specs/SKILL.md +24 -7
- package/adapters/claude/skills/forge-3-specs/references/shared-conventions.md +62 -12
- package/adapters/claude/skills/forge-3-specs/references/stage-exit-protocol.md +14 -4
- package/adapters/claude/skills/forge-4-backlog/SKILL.md +23 -7
- package/adapters/claude/skills/forge-4-backlog/references/shared-conventions.md +62 -12
- package/adapters/claude/skills/forge-4-backlog/references/stage-exit-protocol.md +14 -4
- package/adapters/claude/skills/forge-5-loop/SKILL.md +25 -25
- package/adapters/claude/skills/forge-5-loop/references/agent-selection.md +116 -0
- package/adapters/claude/skills/forge-5-loop/references/runner-contract.md +14 -107
- package/adapters/claude/skills/forge-5-loop/references/shared-conventions.md +62 -12
- package/adapters/claude/skills/forge-5-loop/references/stage-exit-protocol.md +14 -4
- package/adapters/claude/skills/forge-6-docs/SKILL.md +11 -5
- package/adapters/claude/skills/forge-6-docs/references/shared-conventions.md +62 -12
- package/adapters/claude/skills/forge-fix/references/shared-conventions.md +62 -12
- package/adapters/claude/skills/forge-fix/references/stage-exit-protocol.md +14 -4
- package/adapters/claude/skills/forge-guide/references/forge-config-schema.json +2 -2
- package/adapters/claude/skills/forge-guide/references/shared-conventions.md +62 -12
- package/adapters/claude/skills/forge-verify/SKILL.md +22 -12
- package/adapters/claude/skills/forge-verify/references/findings-template.md +157 -0
- package/adapters/claude/skills/forge-verify/references/pipeline-state-schema.json +1 -1
- package/adapters/claude/skills/forge-verify/references/shared-conventions.md +62 -12
- package/adapters/claude/skills/forge-verify/references/verification-checklists/backlog.md +97 -0
- package/adapters/claude/skills/forge-verify/references/verification-checklists/epic.md +79 -0
- package/adapters/claude/skills/forge-verify/references/verification-checklists/impl.md +48 -0
- package/adapters/claude/skills/forge-verify/references/verification-checklists/prd.md +31 -0
- package/adapters/claude/skills/forge-verify/references/verification-checklists/specs.md +64 -0
- package/adapters/claude/skills/forge-verify/references/verification-checklists/tech.md +35 -0
- package/adapters/codex/.feature-forge-bundle.json +1 -1
- package/adapters/codex/agents/forge-verifier.toml +1 -1
- package/adapters/codex/references/forge-config-schema.json +2 -2
- package/adapters/codex/references/pipeline-state-schema.json +1 -1
- package/adapters/codex/references/shared-conventions.md +62 -12
- package/adapters/codex/references/stage-exit-protocol.md +14 -4
- package/adapters/codex/references/vendor-construct-inventory.md +1 -1
- package/adapters/codex/scripts/epic-manifest.py +49 -6
- package/adapters/codex/scripts/forge-root.sh +47 -3
- package/adapters/codex/scripts/forge-session.py +1179 -9
- package/adapters/codex/scripts/validate-traceability.py +6 -1
- package/adapters/codex/skills/forge/SKILL.md +11 -5
- package/adapters/codex/skills/forge/references/pipeline-state-schema.json +1 -1
- package/adapters/codex/skills/forge/references/shared-conventions.md +62 -12
- package/adapters/codex/skills/forge/references/stage-exit-protocol.md +14 -4
- package/adapters/codex/skills/forge-0-epic/SKILL.md +1 -1
- package/adapters/codex/skills/forge-0-epic/references/pipeline-state-schema.json +1 -1
- package/adapters/codex/skills/forge-0-epic/references/shared-conventions.md +62 -12
- package/adapters/codex/skills/forge-0-epic/references/stage-exit-protocol.md +14 -4
- package/adapters/codex/skills/forge-1-prd/SKILL.md +25 -8
- package/adapters/codex/skills/forge-1-prd/references/shared-conventions.md +62 -12
- package/adapters/codex/skills/forge-1-prd/references/stage-exit-protocol.md +14 -4
- package/adapters/codex/skills/forge-2-tech/SKILL.md +25 -7
- package/adapters/codex/skills/forge-2-tech/references/shared-conventions.md +62 -12
- package/adapters/codex/skills/forge-2-tech/references/stage-exit-protocol.md +14 -4
- package/adapters/codex/skills/forge-3-specs/SKILL.md +24 -7
- package/adapters/codex/skills/forge-3-specs/references/shared-conventions.md +62 -12
- package/adapters/codex/skills/forge-3-specs/references/stage-exit-protocol.md +14 -4
- package/adapters/codex/skills/forge-4-backlog/SKILL.md +23 -7
- package/adapters/codex/skills/forge-4-backlog/references/shared-conventions.md +62 -12
- package/adapters/codex/skills/forge-4-backlog/references/stage-exit-protocol.md +14 -4
- package/adapters/codex/skills/forge-5-loop/SKILL.md +25 -25
- package/adapters/codex/skills/forge-5-loop/references/agent-selection.md +116 -0
- package/adapters/codex/skills/forge-5-loop/references/runner-contract.md +14 -107
- package/adapters/codex/skills/forge-5-loop/references/shared-conventions.md +62 -12
- package/adapters/codex/skills/forge-5-loop/references/stage-exit-protocol.md +14 -4
- package/adapters/codex/skills/forge-6-docs/SKILL.md +11 -5
- package/adapters/codex/skills/forge-6-docs/references/shared-conventions.md +62 -12
- package/adapters/codex/skills/forge-fix/references/shared-conventions.md +62 -12
- package/adapters/codex/skills/forge-fix/references/stage-exit-protocol.md +14 -4
- package/adapters/codex/skills/forge-guide/references/forge-config-schema.json +2 -2
- package/adapters/codex/skills/forge-guide/references/shared-conventions.md +62 -12
- package/adapters/codex/skills/forge-verify/SKILL.md +22 -12
- package/adapters/codex/skills/forge-verify/references/findings-template.md +157 -0
- package/adapters/codex/skills/forge-verify/references/pipeline-state-schema.json +1 -1
- package/adapters/codex/skills/forge-verify/references/shared-conventions.md +62 -12
- package/adapters/codex/skills/forge-verify/references/verification-checklists/backlog.md +97 -0
- package/adapters/codex/skills/forge-verify/references/verification-checklists/epic.md +79 -0
- package/adapters/codex/skills/forge-verify/references/verification-checklists/impl.md +48 -0
- package/adapters/codex/skills/forge-verify/references/verification-checklists/prd.md +31 -0
- package/adapters/codex/skills/forge-verify/references/verification-checklists/specs.md +64 -0
- package/adapters/codex/skills/forge-verify/references/verification-checklists/tech.md +35 -0
- package/adapters/copilot/.feature-forge-bundle.json +1 -1
- package/adapters/copilot/agents/forge-verifier.md +1 -1
- package/adapters/copilot/references/forge-config-schema.json +2 -2
- package/adapters/copilot/references/pipeline-state-schema.json +1 -1
- package/adapters/copilot/references/shared-conventions.md +62 -12
- package/adapters/copilot/references/stage-exit-protocol.md +14 -4
- package/adapters/copilot/references/vendor-construct-inventory.md +1 -1
- package/adapters/copilot/scripts/epic-manifest.py +49 -6
- package/adapters/copilot/scripts/forge-root.sh +47 -3
- package/adapters/copilot/scripts/forge-session.py +1179 -9
- package/adapters/copilot/scripts/validate-traceability.py +6 -1
- package/adapters/copilot/skills/forge/forge.md +11 -5
- package/adapters/copilot/skills/forge/references/pipeline-state-schema.json +1 -1
- package/adapters/copilot/skills/forge/references/shared-conventions.md +62 -12
- package/adapters/copilot/skills/forge/references/stage-exit-protocol.md +14 -4
- package/adapters/copilot/skills/forge-0-epic/forge-0-epic.md +1 -1
- package/adapters/copilot/skills/forge-0-epic/references/pipeline-state-schema.json +1 -1
- package/adapters/copilot/skills/forge-0-epic/references/shared-conventions.md +62 -12
- package/adapters/copilot/skills/forge-0-epic/references/stage-exit-protocol.md +14 -4
- package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +25 -8
- package/adapters/copilot/skills/forge-1-prd/references/shared-conventions.md +62 -12
- package/adapters/copilot/skills/forge-1-prd/references/stage-exit-protocol.md +14 -4
- package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +25 -7
- package/adapters/copilot/skills/forge-2-tech/references/shared-conventions.md +62 -12
- package/adapters/copilot/skills/forge-2-tech/references/stage-exit-protocol.md +14 -4
- package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +24 -7
- package/adapters/copilot/skills/forge-3-specs/references/shared-conventions.md +62 -12
- package/adapters/copilot/skills/forge-3-specs/references/stage-exit-protocol.md +14 -4
- package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +23 -7
- package/adapters/copilot/skills/forge-4-backlog/references/shared-conventions.md +62 -12
- package/adapters/copilot/skills/forge-4-backlog/references/stage-exit-protocol.md +14 -4
- package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +25 -25
- package/adapters/copilot/skills/forge-5-loop/references/agent-selection.md +116 -0
- package/adapters/copilot/skills/forge-5-loop/references/runner-contract.md +14 -107
- package/adapters/copilot/skills/forge-5-loop/references/shared-conventions.md +62 -12
- package/adapters/copilot/skills/forge-5-loop/references/stage-exit-protocol.md +14 -4
- package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +11 -5
- package/adapters/copilot/skills/forge-6-docs/references/shared-conventions.md +62 -12
- package/adapters/copilot/skills/forge-fix/references/shared-conventions.md +62 -12
- package/adapters/copilot/skills/forge-fix/references/stage-exit-protocol.md +14 -4
- package/adapters/copilot/skills/forge-guide/references/forge-config-schema.json +2 -2
- package/adapters/copilot/skills/forge-guide/references/shared-conventions.md +62 -12
- package/adapters/copilot/skills/forge-verify/forge-verify.md +22 -12
- package/adapters/copilot/skills/forge-verify/references/findings-template.md +157 -0
- package/adapters/copilot/skills/forge-verify/references/pipeline-state-schema.json +1 -1
- package/adapters/copilot/skills/forge-verify/references/shared-conventions.md +62 -12
- package/adapters/copilot/skills/forge-verify/references/verification-checklists/backlog.md +97 -0
- package/adapters/copilot/skills/forge-verify/references/verification-checklists/epic.md +79 -0
- package/adapters/copilot/skills/forge-verify/references/verification-checklists/impl.md +48 -0
- package/adapters/copilot/skills/forge-verify/references/verification-checklists/prd.md +31 -0
- package/adapters/copilot/skills/forge-verify/references/verification-checklists/specs.md +64 -0
- package/adapters/copilot/skills/forge-verify/references/verification-checklists/tech.md +35 -0
- package/adapters/cursor/.feature-forge-bundle.json +1 -1
- package/adapters/cursor/agents/forge-verifier.mdc +1 -1
- package/adapters/cursor/references/forge-config-schema.json +2 -2
- package/adapters/cursor/references/pipeline-state-schema.json +1 -1
- package/adapters/cursor/references/shared-conventions.md +62 -12
- package/adapters/cursor/references/stage-exit-protocol.md +14 -4
- package/adapters/cursor/references/vendor-construct-inventory.md +1 -1
- package/adapters/cursor/scripts/epic-manifest.py +49 -6
- package/adapters/cursor/scripts/forge-root.sh +47 -3
- package/adapters/cursor/scripts/forge-session.py +1179 -9
- package/adapters/cursor/scripts/validate-traceability.py +6 -1
- package/adapters/cursor/skills/forge/forge.mdc +11 -5
- package/adapters/cursor/skills/forge/references/pipeline-state-schema.json +1 -1
- package/adapters/cursor/skills/forge/references/shared-conventions.md +62 -12
- package/adapters/cursor/skills/forge/references/stage-exit-protocol.md +14 -4
- package/adapters/cursor/skills/forge-0-epic/forge-0-epic.mdc +1 -1
- package/adapters/cursor/skills/forge-0-epic/references/pipeline-state-schema.json +1 -1
- package/adapters/cursor/skills/forge-0-epic/references/shared-conventions.md +62 -12
- package/adapters/cursor/skills/forge-0-epic/references/stage-exit-protocol.md +14 -4
- package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +25 -8
- package/adapters/cursor/skills/forge-1-prd/references/shared-conventions.md +62 -12
- package/adapters/cursor/skills/forge-1-prd/references/stage-exit-protocol.md +14 -4
- package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +25 -7
- package/adapters/cursor/skills/forge-2-tech/references/shared-conventions.md +62 -12
- package/adapters/cursor/skills/forge-2-tech/references/stage-exit-protocol.md +14 -4
- package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +24 -7
- package/adapters/cursor/skills/forge-3-specs/references/shared-conventions.md +62 -12
- package/adapters/cursor/skills/forge-3-specs/references/stage-exit-protocol.md +14 -4
- package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +23 -7
- package/adapters/cursor/skills/forge-4-backlog/references/shared-conventions.md +62 -12
- package/adapters/cursor/skills/forge-4-backlog/references/stage-exit-protocol.md +14 -4
- package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +25 -25
- package/adapters/cursor/skills/forge-5-loop/references/agent-selection.md +116 -0
- package/adapters/cursor/skills/forge-5-loop/references/runner-contract.md +14 -107
- package/adapters/cursor/skills/forge-5-loop/references/shared-conventions.md +62 -12
- package/adapters/cursor/skills/forge-5-loop/references/stage-exit-protocol.md +14 -4
- package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +11 -5
- package/adapters/cursor/skills/forge-6-docs/references/shared-conventions.md +62 -12
- package/adapters/cursor/skills/forge-fix/references/shared-conventions.md +62 -12
- package/adapters/cursor/skills/forge-fix/references/stage-exit-protocol.md +14 -4
- package/adapters/cursor/skills/forge-guide/references/forge-config-schema.json +2 -2
- package/adapters/cursor/skills/forge-guide/references/shared-conventions.md +62 -12
- package/adapters/cursor/skills/forge-verify/forge-verify.mdc +22 -12
- package/adapters/cursor/skills/forge-verify/references/findings-template.md +157 -0
- package/adapters/cursor/skills/forge-verify/references/pipeline-state-schema.json +1 -1
- package/adapters/cursor/skills/forge-verify/references/shared-conventions.md +62 -12
- package/adapters/cursor/skills/forge-verify/references/verification-checklists/backlog.md +97 -0
- package/adapters/cursor/skills/forge-verify/references/verification-checklists/epic.md +79 -0
- package/adapters/cursor/skills/forge-verify/references/verification-checklists/impl.md +48 -0
- package/adapters/cursor/skills/forge-verify/references/verification-checklists/prd.md +31 -0
- package/adapters/cursor/skills/forge-verify/references/verification-checklists/specs.md +64 -0
- package/adapters/cursor/skills/forge-verify/references/verification-checklists/tech.md +35 -0
- package/adapters/gemini/.feature-forge-bundle.json +1 -1
- package/adapters/gemini/agents/forge-verifier.md +1 -1
- package/adapters/gemini/gemini-extension.json +1 -1
- package/adapters/gemini/references/forge-config-schema.json +2 -2
- package/adapters/gemini/references/pipeline-state-schema.json +1 -1
- package/adapters/gemini/references/shared-conventions.md +62 -12
- package/adapters/gemini/references/stage-exit-protocol.md +14 -4
- package/adapters/gemini/references/vendor-construct-inventory.md +1 -1
- package/adapters/gemini/scripts/epic-manifest.py +49 -6
- package/adapters/gemini/scripts/forge-root.sh +47 -3
- package/adapters/gemini/scripts/forge-session.py +1179 -9
- package/adapters/gemini/scripts/validate-traceability.py +6 -1
- package/adapters/gemini/skills/forge/forge.md +11 -5
- package/adapters/gemini/skills/forge/references/pipeline-state-schema.json +1 -1
- package/adapters/gemini/skills/forge/references/shared-conventions.md +62 -12
- package/adapters/gemini/skills/forge/references/stage-exit-protocol.md +14 -4
- package/adapters/gemini/skills/forge-0-epic/forge-0-epic.md +1 -1
- package/adapters/gemini/skills/forge-0-epic/references/pipeline-state-schema.json +1 -1
- package/adapters/gemini/skills/forge-0-epic/references/shared-conventions.md +62 -12
- package/adapters/gemini/skills/forge-0-epic/references/stage-exit-protocol.md +14 -4
- package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +25 -8
- package/adapters/gemini/skills/forge-1-prd/references/shared-conventions.md +62 -12
- package/adapters/gemini/skills/forge-1-prd/references/stage-exit-protocol.md +14 -4
- package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +25 -7
- package/adapters/gemini/skills/forge-2-tech/references/shared-conventions.md +62 -12
- package/adapters/gemini/skills/forge-2-tech/references/stage-exit-protocol.md +14 -4
- package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +24 -7
- package/adapters/gemini/skills/forge-3-specs/references/shared-conventions.md +62 -12
- package/adapters/gemini/skills/forge-3-specs/references/stage-exit-protocol.md +14 -4
- package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +23 -7
- package/adapters/gemini/skills/forge-4-backlog/references/shared-conventions.md +62 -12
- package/adapters/gemini/skills/forge-4-backlog/references/stage-exit-protocol.md +14 -4
- package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +25 -25
- package/adapters/gemini/skills/forge-5-loop/references/agent-selection.md +116 -0
- package/adapters/gemini/skills/forge-5-loop/references/runner-contract.md +14 -107
- package/adapters/gemini/skills/forge-5-loop/references/shared-conventions.md +62 -12
- package/adapters/gemini/skills/forge-5-loop/references/stage-exit-protocol.md +14 -4
- package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +11 -5
- package/adapters/gemini/skills/forge-6-docs/references/shared-conventions.md +62 -12
- package/adapters/gemini/skills/forge-fix/references/shared-conventions.md +62 -12
- package/adapters/gemini/skills/forge-fix/references/stage-exit-protocol.md +14 -4
- package/adapters/gemini/skills/forge-guide/references/forge-config-schema.json +2 -2
- package/adapters/gemini/skills/forge-guide/references/shared-conventions.md +62 -12
- package/adapters/gemini/skills/forge-verify/forge-verify.md +22 -12
- package/adapters/gemini/skills/forge-verify/references/findings-template.md +157 -0
- package/adapters/gemini/skills/forge-verify/references/pipeline-state-schema.json +1 -1
- package/adapters/gemini/skills/forge-verify/references/shared-conventions.md +62 -12
- package/adapters/gemini/skills/forge-verify/references/verification-checklists/backlog.md +97 -0
- package/adapters/gemini/skills/forge-verify/references/verification-checklists/epic.md +79 -0
- package/adapters/gemini/skills/forge-verify/references/verification-checklists/impl.md +48 -0
- package/adapters/gemini/skills/forge-verify/references/verification-checklists/prd.md +31 -0
- package/adapters/gemini/skills/forge-verify/references/verification-checklists/specs.md +64 -0
- package/adapters/gemini/skills/forge-verify/references/verification-checklists/tech.md +35 -0
- package/adapters/pi/.feature-forge-bundle.json +6 -0
- package/adapters/pi/agents/forge-researcher.md +139 -0
- package/adapters/pi/agents/forge-spec-writer.md +116 -0
- package/adapters/pi/agents/forge-verifier.md +126 -0
- package/adapters/pi/extensions/ask-user-question/LICENSE +21 -0
- package/adapters/pi/extensions/ask-user-question/README.md +91 -0
- package/adapters/pi/extensions/ask-user-question/ask-user-question.ts +298 -0
- package/adapters/pi/extensions/ask-user-question/config.ts +78 -0
- package/adapters/pi/extensions/ask-user-question/events.ts +57 -0
- package/adapters/pi/extensions/ask-user-question/index.ts +61 -0
- package/adapters/pi/extensions/ask-user-question/locales/de.json +27 -0
- package/adapters/pi/extensions/ask-user-question/locales/en.json +27 -0
- package/adapters/pi/extensions/ask-user-question/locales/es.json +27 -0
- package/adapters/pi/extensions/ask-user-question/locales/fr.json +27 -0
- package/adapters/pi/extensions/ask-user-question/locales/pt-BR.json +27 -0
- package/adapters/pi/extensions/ask-user-question/locales/pt.json +27 -0
- package/adapters/pi/extensions/ask-user-question/locales/ru.json +27 -0
- package/adapters/pi/extensions/ask-user-question/locales/uk.json +27 -0
- package/adapters/pi/extensions/ask-user-question/locales/zh.json +29 -0
- package/adapters/pi/extensions/ask-user-question/reconcile.ts +49 -0
- package/adapters/pi/extensions/ask-user-question/rpc-fallback.ts +168 -0
- package/adapters/pi/extensions/ask-user-question/state/build-questionnaire.ts +302 -0
- package/adapters/pi/extensions/ask-user-question/state/i18n-bridge.ts +53 -0
- package/adapters/pi/extensions/ask-user-question/state/key-router.ts +277 -0
- package/adapters/pi/extensions/ask-user-question/state/questionnaire-session.ts +234 -0
- package/adapters/pi/extensions/ask-user-question/state/row-intent.ts +145 -0
- package/adapters/pi/extensions/ask-user-question/state/selectors/contract.ts +26 -0
- package/adapters/pi/extensions/ask-user-question/state/selectors/derivations.ts +42 -0
- package/adapters/pi/extensions/ask-user-question/state/selectors/focus.ts +19 -0
- package/adapters/pi/extensions/ask-user-question/state/selectors/projections.ts +101 -0
- package/adapters/pi/extensions/ask-user-question/state/state-reducer.ts +292 -0
- package/adapters/pi/extensions/ask-user-question/state/state.ts +55 -0
- package/adapters/pi/extensions/ask-user-question/tool/format-answer.ts +31 -0
- package/adapters/pi/extensions/ask-user-question/tool/response-envelope.ts +49 -0
- package/adapters/pi/extensions/ask-user-question/tool/types.ts +147 -0
- package/adapters/pi/extensions/ask-user-question/tool/validate-questionnaire.ts +58 -0
- package/adapters/pi/extensions/ask-user-question/vendor-config-shim.ts +65 -0
- package/adapters/pi/extensions/ask-user-question/view/component-binding.ts +47 -0
- package/adapters/pi/extensions/ask-user-question/view/components/inline-input.ts +98 -0
- package/adapters/pi/extensions/ask-user-question/view/components/multi-select-view.ts +193 -0
- package/adapters/pi/extensions/ask-user-question/view/components/option-list-view.ts +70 -0
- package/adapters/pi/extensions/ask-user-question/view/components/preview/markdown-content-cache.ts +79 -0
- package/adapters/pi/extensions/ask-user-question/view/components/preview/preview-block-renderer.ts +111 -0
- package/adapters/pi/extensions/ask-user-question/view/components/preview/preview-box-renderer.ts +88 -0
- package/adapters/pi/extensions/ask-user-question/view/components/preview/preview-layout-decider.ts +202 -0
- package/adapters/pi/extensions/ask-user-question/view/components/preview/preview-pane.ts +228 -0
- package/adapters/pi/extensions/ask-user-question/view/components/submit-picker.ts +67 -0
- package/adapters/pi/extensions/ask-user-question/view/components/tab-bar.ts +59 -0
- package/adapters/pi/extensions/ask-user-question/view/components/wrapping-select.ts +293 -0
- package/adapters/pi/extensions/ask-user-question/view/dialog-builder.ts +224 -0
- package/adapters/pi/extensions/ask-user-question/view/props-adapter.ts +125 -0
- package/adapters/pi/extensions/ask-user-question/view/stateful-view.ts +26 -0
- package/adapters/pi/extensions/ask-user-question/view/tab-components.ts +18 -0
- package/adapters/pi/extensions/ask-user-question/view/tab-content-strategy.ts +252 -0
- package/adapters/pi/package.json +26 -0
- package/adapters/pi/references/epic-manifest-schema.json +125 -0
- package/adapters/{claude/skills/forge-5-loop → pi}/references/forge-config-schema.json +4 -4
- package/adapters/{claude/skills/forge-1-prd → pi}/references/pipeline-state-schema.json +1 -1
- package/adapters/pi/references/portable-root.md +71 -0
- package/adapters/pi/references/process-overview.md +143 -0
- package/adapters/pi/references/ralph-loop-contract.md +221 -0
- package/adapters/pi/references/shared-conventions.md +345 -0
- package/adapters/pi/references/skill-frontmatter.schema.json +17 -0
- package/adapters/pi/references/stack-resolution.md +54 -0
- package/adapters/pi/references/stacks/_generic.md +111 -0
- package/adapters/pi/references/stacks/go.md +157 -0
- package/adapters/pi/references/stacks/python.md +184 -0
- package/adapters/pi/references/stacks/rust.md +170 -0
- package/adapters/pi/references/stacks/typescript.md +134 -0
- package/adapters/pi/references/stage-exit-protocol.md +268 -0
- package/adapters/pi/references/templates/specs-hygiene/AGENTS.md +32 -0
- package/adapters/pi/references/templates/specs-hygiene/CLAUDE.md +31 -0
- package/adapters/pi/references/vendor-construct-inventory.md +50 -0
- package/adapters/pi/scripts/epic-manifest.py +1737 -0
- package/adapters/pi/scripts/forge-bootstrap.py +1070 -0
- package/adapters/pi/scripts/forge-init.sh +58 -0
- package/adapters/pi/scripts/forge-root.sh +179 -0
- package/adapters/pi/scripts/forge-session.py +3036 -0
- package/adapters/pi/scripts/validate-traceability.py +155 -0
- package/adapters/pi/skills/forge/SKILL.md +249 -0
- package/adapters/{claude/skills/forge-4-backlog → pi/skills/forge}/references/pipeline-state-schema.json +1 -1
- package/adapters/pi/skills/forge/references/process-overview.md +143 -0
- package/adapters/pi/skills/forge/references/shared-conventions.md +345 -0
- package/adapters/pi/skills/forge/references/stage-exit-protocol.md +268 -0
- package/adapters/pi/skills/forge-0-epic/SKILL.md +308 -0
- package/adapters/pi/skills/forge-0-epic/references/edit-mode.md +266 -0
- package/adapters/pi/skills/forge-0-epic/references/epic-manifest-subcommands.md +75 -0
- package/adapters/{claude/skills/forge-2-tech → pi/skills/forge-0-epic}/references/pipeline-state-schema.json +1 -1
- package/adapters/pi/skills/forge-0-epic/references/portable-root.md +71 -0
- package/adapters/pi/skills/forge-0-epic/references/shared-conventions.md +345 -0
- package/adapters/pi/skills/forge-0-epic/references/stage-exit-protocol.md +268 -0
- package/adapters/pi/skills/forge-1-prd/SKILL.md +181 -0
- package/adapters/pi/skills/forge-1-prd/references/prd-template.md +106 -0
- package/adapters/pi/skills/forge-1-prd/references/shared-conventions.md +345 -0
- package/adapters/pi/skills/forge-1-prd/references/stage-exit-protocol.md +268 -0
- package/adapters/pi/skills/forge-2-tech/SKILL.md +243 -0
- package/adapters/pi/skills/forge-2-tech/references/shared-conventions.md +345 -0
- package/adapters/pi/skills/forge-2-tech/references/stack-discovery-checklist.md +95 -0
- package/adapters/pi/skills/forge-2-tech/references/stack-resolution.md +54 -0
- package/adapters/pi/skills/forge-2-tech/references/stacks/_generic.md +111 -0
- package/adapters/pi/skills/forge-2-tech/references/stacks/go.md +157 -0
- package/adapters/pi/skills/forge-2-tech/references/stacks/python.md +184 -0
- package/adapters/pi/skills/forge-2-tech/references/stacks/rust.md +170 -0
- package/adapters/pi/skills/forge-2-tech/references/stacks/typescript.md +134 -0
- package/adapters/pi/skills/forge-2-tech/references/stage-exit-protocol.md +268 -0
- package/adapters/pi/skills/forge-3-specs/SKILL.md +195 -0
- package/adapters/pi/skills/forge-3-specs/references/shared-conventions.md +345 -0
- package/adapters/pi/skills/forge-3-specs/references/spec-archetypes.md +106 -0
- package/adapters/pi/skills/forge-3-specs/references/spec-examples.md +71 -0
- package/adapters/pi/skills/forge-3-specs/references/stacks/_generic.md +111 -0
- package/adapters/pi/skills/forge-3-specs/references/stacks/go.md +157 -0
- package/adapters/pi/skills/forge-3-specs/references/stacks/python.md +184 -0
- package/adapters/pi/skills/forge-3-specs/references/stacks/rust.md +170 -0
- package/adapters/pi/skills/forge-3-specs/references/stacks/typescript.md +134 -0
- package/adapters/pi/skills/forge-3-specs/references/stage-exit-protocol.md +268 -0
- package/adapters/pi/skills/forge-4-backlog/SKILL.md +191 -0
- package/adapters/pi/skills/forge-4-backlog/references/shared-conventions.md +345 -0
- package/adapters/pi/skills/forge-4-backlog/references/stage-exit-protocol.md +268 -0
- package/adapters/pi/skills/forge-5-loop/SKILL.md +314 -0
- package/adapters/pi/skills/forge-5-loop/references/agent-selection.md +116 -0
- package/adapters/pi/skills/forge-5-loop/references/ralph-loop-contract.md +221 -0
- package/adapters/pi/skills/forge-5-loop/references/result-reporting.md +85 -0
- package/adapters/pi/skills/forge-5-loop/references/runner-contract.md +248 -0
- package/adapters/pi/skills/forge-5-loop/references/shared-conventions.md +345 -0
- package/adapters/pi/skills/forge-5-loop/references/stage-exit-protocol.md +268 -0
- package/adapters/pi/skills/forge-6-docs/SKILL.md +208 -0
- package/adapters/pi/skills/forge-6-docs/references/doc-conventions.md +126 -0
- package/adapters/pi/skills/forge-6-docs/references/shared-conventions.md +345 -0
- package/adapters/pi/skills/forge-bootstrap/SKILL.md +250 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/ci/github-actions.yml +12 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/generic/run.sh +3 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/generic/test.sh +13 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/go/go.mod +3 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/go/main.go +12 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/go/main_test.go +11 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/hygiene/AGENTS.md +35 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/hygiene/CLAUDE.md +36 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/hygiene/README.md +11 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/licenses/Apache-2.0/LICENSE +198 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/licenses/MIT/LICENSE +21 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/python/pyproject.toml +24 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/__init__.py +5 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/python/src/{{PKG}}/main.py +13 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/python/tests/test_smoke.py +8 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/rust/Cargo.toml +15 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/rust/src/lib.rs +7 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/rust/src/main.rs +5 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/rust/tests/smoke.rs +6 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/typescript/package.json +15 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/typescript/src/index.ts +4 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/typescript/test/smoke.test.ts +6 -0
- package/adapters/pi/skills/forge-bootstrap/references/templates/typescript/tsconfig.json +14 -0
- package/adapters/pi/skills/forge-fix/SKILL.md +98 -0
- package/adapters/pi/skills/forge-fix/references/shared-conventions.md +345 -0
- package/adapters/pi/skills/forge-fix/references/stage-exit-protocol.md +268 -0
- package/adapters/pi/skills/forge-guide/SKILL.md +192 -0
- package/adapters/{codex/skills/forge-4-backlog → pi/skills/forge-guide}/references/forge-config-schema.json +4 -4
- package/adapters/pi/skills/forge-guide/references/process-overview.md +143 -0
- package/adapters/pi/skills/forge-guide/references/ralph-loop-contract.md +221 -0
- package/adapters/pi/skills/forge-guide/references/shared-conventions.md +345 -0
- package/adapters/pi/skills/forge-guide/references/stack-resolution.md +54 -0
- package/adapters/pi/skills/forge-guide/references/stacks/_generic.md +111 -0
- package/adapters/pi/skills/forge-guide/references/stacks/go.md +157 -0
- package/adapters/pi/skills/forge-guide/references/stacks/python.md +184 -0
- package/adapters/pi/skills/forge-guide/references/stacks/rust.md +170 -0
- package/adapters/pi/skills/forge-guide/references/stacks/typescript.md +134 -0
- package/adapters/pi/skills/forge-init/SKILL.md +72 -0
- package/adapters/pi/skills/forge-verify/SKILL.md +283 -0
- package/adapters/pi/skills/forge-verify/references/findings-template.md +157 -0
- package/adapters/{claude/skills/forge-3-specs → pi/skills/forge-verify}/references/pipeline-state-schema.json +1 -1
- package/adapters/pi/skills/forge-verify/references/shared-conventions.md +345 -0
- package/adapters/pi/skills/forge-verify/references/verification-checklists/backlog.md +97 -0
- package/adapters/pi/skills/forge-verify/references/verification-checklists/epic.md +79 -0
- package/adapters/pi/skills/forge-verify/references/verification-checklists/impl.md +48 -0
- package/adapters/pi/skills/forge-verify/references/verification-checklists/prd.md +31 -0
- package/adapters/pi/skills/forge-verify/references/verification-checklists/specs.md +64 -0
- package/adapters/pi/skills/forge-verify/references/verification-checklists/tech.md +35 -0
- package/dist/agent-targets.d.ts +1 -1
- package/dist/agent-targets.js +23 -3
- package/dist/detect.d.ts +1 -1
- package/dist/detect.js +2 -1
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.js +2 -2
- package/dist/placements.js +5 -1
- package/dist/rauf.d.ts +4 -4
- package/dist/rauf.js +3 -3
- package/dist/types.d.ts +31 -6
- package/dist/types.js +6 -3
- package/package.json +14 -3
- package/adapters/claude/skills/forge-4-backlog/references/forge-config-schema.json +0 -236
- package/adapters/claude/skills/forge-6-docs/references/pipeline-state-schema.json +0 -191
- package/adapters/claude/skills/forge-verify/references/verification-checklists.md +0 -477
- package/adapters/codex/skills/forge-1-prd/references/pipeline-state-schema.json +0 -191
- package/adapters/codex/skills/forge-2-tech/references/pipeline-state-schema.json +0 -191
- package/adapters/codex/skills/forge-3-specs/references/pipeline-state-schema.json +0 -191
- package/adapters/codex/skills/forge-4-backlog/references/pipeline-state-schema.json +0 -191
- package/adapters/codex/skills/forge-5-loop/references/forge-config-schema.json +0 -236
- package/adapters/codex/skills/forge-6-docs/references/pipeline-state-schema.json +0 -191
- package/adapters/codex/skills/forge-verify/references/verification-checklists.md +0 -477
- package/adapters/copilot/skills/forge-1-prd/references/pipeline-state-schema.json +0 -191
- package/adapters/copilot/skills/forge-2-tech/references/pipeline-state-schema.json +0 -191
- package/adapters/copilot/skills/forge-3-specs/references/pipeline-state-schema.json +0 -191
- package/adapters/copilot/skills/forge-4-backlog/references/forge-config-schema.json +0 -236
- package/adapters/copilot/skills/forge-4-backlog/references/pipeline-state-schema.json +0 -191
- package/adapters/copilot/skills/forge-5-loop/references/forge-config-schema.json +0 -236
- package/adapters/copilot/skills/forge-6-docs/references/pipeline-state-schema.json +0 -191
- package/adapters/copilot/skills/forge-verify/references/verification-checklists.md +0 -477
- package/adapters/cursor/skills/forge-1-prd/references/pipeline-state-schema.json +0 -191
- package/adapters/cursor/skills/forge-2-tech/references/pipeline-state-schema.json +0 -191
- package/adapters/cursor/skills/forge-3-specs/references/pipeline-state-schema.json +0 -191
- package/adapters/cursor/skills/forge-4-backlog/references/forge-config-schema.json +0 -236
- package/adapters/cursor/skills/forge-4-backlog/references/pipeline-state-schema.json +0 -191
- package/adapters/cursor/skills/forge-5-loop/references/forge-config-schema.json +0 -236
- package/adapters/cursor/skills/forge-6-docs/references/pipeline-state-schema.json +0 -191
- package/adapters/cursor/skills/forge-verify/references/verification-checklists.md +0 -477
- package/adapters/gemini/skills/forge-1-prd/references/pipeline-state-schema.json +0 -191
- package/adapters/gemini/skills/forge-2-tech/references/pipeline-state-schema.json +0 -191
- package/adapters/gemini/skills/forge-3-specs/references/pipeline-state-schema.json +0 -191
- package/adapters/gemini/skills/forge-4-backlog/references/forge-config-schema.json +0 -236
- package/adapters/gemini/skills/forge-4-backlog/references/pipeline-state-schema.json +0 -191
- package/adapters/gemini/skills/forge-5-loop/references/forge-config-schema.json +0 -236
- package/adapters/gemini/skills/forge-6-docs/references/pipeline-state-schema.json +0 -191
- package/adapters/gemini/skills/forge-verify/references/verification-checklists.md +0 -477
|
@@ -15,6 +15,27 @@ root navigator:
|
|
|
15
15
|
[--config FILE] [--epic E] [--json]
|
|
16
16
|
python3 forge-session.py stage-exit --feature F --stage S [--specs-dir DIR] \
|
|
17
17
|
[--config FILE] [--epic E] [--next-feature N] [--host claude|generic] [--json]
|
|
18
|
+
python3 forge-session.py effective-config [--config FILE] [--schema PATH] [--json]
|
|
19
|
+
|
|
20
|
+
Plus the `state-*` write verbs, which author `.pipeline-state.json` so no stage
|
|
21
|
+
has to hand-write the JSON (and therefore no stage has to read the state schema):
|
|
22
|
+
|
|
23
|
+
python3 forge-session.py state-enter --feature F --stage S [--specs-dir DIR] \
|
|
24
|
+
[--epic E] [--json]
|
|
25
|
+
python3 forge-session.py state-artifact --feature F --stage S --path P \
|
|
26
|
+
[--path P ...] [--specs-dir DIR] [--epic E] [--json]
|
|
27
|
+
python3 forge-session.py state-complete --feature F --stage S --version N \
|
|
28
|
+
[--based-on STAGE=N ...] [--artifact P ...] [--commit-hash H] \
|
|
29
|
+
[--status complete|in-progress] [--resumable] [--preserve-commit-hash] \
|
|
30
|
+
[--specs-dir DIR] [--epic E] [--json]
|
|
31
|
+
python3 forge-session.py state-branch --feature F --branch B [--specs-dir DIR] \
|
|
32
|
+
[--epic E] [--json]
|
|
33
|
+
python3 forge-session.py state-note --feature F --note TEXT [--specs-dir DIR] \
|
|
34
|
+
[--epic E] [--json]
|
|
35
|
+
python3 forge-session.py state-decision --feature F --question Q --raised-by S \
|
|
36
|
+
[--rationale R] [--target-stage S] [--specs-dir DIR] [--epic E] [--json]
|
|
37
|
+
python3 forge-session.py state-ecr --feature F --kind K --target T --rationale R \
|
|
38
|
+
--raised-by S --blocks-current true|false [--specs-dir DIR] [--epic E] [--json]
|
|
18
39
|
|
|
19
40
|
`rank-features` scans the specs tree for feature-shaped directories (those that
|
|
20
41
|
directly contain a `.pipeline-state.json`, in both the flat
|
|
@@ -66,6 +87,44 @@ present, autoFix eligibility, the verify and next-stage commands) plus the
|
|
|
66
87
|
exact sentinel-terminated NEXT-STEPS block the skill must print verbatim as
|
|
67
88
|
its absolute last output. Deterministic and read-only; always exits 0.
|
|
68
89
|
|
|
90
|
+
`effective-config` resolves the `loopRunner` block deterministically so no
|
|
91
|
+
caller has to read `references/forge-config-schema.json` just to learn the
|
|
92
|
+
defaults: it extracts each field's schema `default` at runtime and merges the
|
|
93
|
+
project's `loopRunner` overrides on top. A missing or corrupt
|
|
94
|
+
`forge.config.json` resolves to pure defaults (exit 0); only an unreadable
|
|
95
|
+
schema is fatal (exit 2), because then there are no defaults to resolve.
|
|
96
|
+
|
|
97
|
+
The `state-*` verbs are the script's only writers. Each follows the same
|
|
98
|
+
resolve -> load -> mutate -> refresh `updatedAt` -> atomic write path, so every
|
|
99
|
+
successful write leaves a schema-conformant state file: `state-enter` stamps a
|
|
100
|
+
stage in-progress and moves `currentStage`, `state-artifact` appends artifact
|
|
101
|
+
paths to a stage (de-duplicating), `state-branch` records the branch resolved by
|
|
102
|
+
Branch Setup / Branch Reconciliation, and `state-note` persists the free-text
|
|
103
|
+
note a user volunteers at a stage exit. They never create a feature directory —
|
|
104
|
+
an unknown `--feature` is a usage error (exit 2) — and they never overwrite a
|
|
105
|
+
state file they could not parse.
|
|
106
|
+
|
|
107
|
+
`state-complete` is the largest of them: it records the completion (status,
|
|
108
|
+
`completedAt`, `version`, `basedOnVersions`, `artifacts`), resets `commitHash` to
|
|
109
|
+
null for Commit 1 of the two-commit Git Commit Protocol, and runs the
|
|
110
|
+
deterministic downstream staleness cascade that each stage used to describe in
|
|
111
|
+
prose. `--commit-hash` is the Commit-2 follow-up, setting only that field (and
|
|
112
|
+
refusing a stage that is not yet complete). The protocol's two recovery branches
|
|
113
|
+
stay executable without hand-authored JSON: `--resumable` is the failed-Commit-1
|
|
114
|
+
revert (status-only, no cascade), and `--preserve-commit-hash` is the "nothing to
|
|
115
|
+
commit" branch. A bare `--status in-progress` is something else again —
|
|
116
|
+
forge-5-loop's partial completion, which keeps every completion field.
|
|
117
|
+
|
|
118
|
+
`state-decision` and `state-ecr` are the two array-appending verbs. The first
|
|
119
|
+
appends a `deferredDecisions[]` item — a same-feature decision deliberately
|
|
120
|
+
postponed to a later stage; the second appends an `epicChangeRequests[]` item —
|
|
121
|
+
a member stage's report that the epic decomposition itself must change, whose
|
|
122
|
+
`blocksCurrent` boolean drives the stage exit's pause-now vs. finish-then-edit
|
|
123
|
+
routing (so it is required and parsed strictly: only `true`/`false`). Both always
|
|
124
|
+
record `status: "open"` — resolving an item is the target stage's job, never the
|
|
125
|
+
recorder's — and both emit exactly the schema keys, because those two array item
|
|
126
|
+
shapes set `additionalProperties: false`.
|
|
127
|
+
|
|
69
128
|
3.10 baseline, Google-style docstrings, full type annotations, stdlib only —
|
|
70
129
|
matching the conventions of `scripts/epic-manifest.py`.
|
|
71
130
|
|
|
@@ -81,9 +140,10 @@ import json
|
|
|
81
140
|
import os
|
|
82
141
|
import subprocess
|
|
83
142
|
import sys
|
|
143
|
+
import tempfile
|
|
84
144
|
from datetime import datetime, timezone
|
|
85
145
|
from pathlib import Path
|
|
86
|
-
from typing import Final, TypedDict
|
|
146
|
+
from typing import Callable, Final, TypedDict
|
|
87
147
|
|
|
88
148
|
|
|
89
149
|
# --------------------------------------------------------------------------- #
|
|
@@ -105,6 +165,34 @@ PRODUCTION_STAGES: Final[tuple[str, ...]] = (
|
|
|
105
165
|
"forge-6-docs",
|
|
106
166
|
)
|
|
107
167
|
|
|
168
|
+
#: The --stage domain for the state-write verbs: the six PRODUCTION_STAGES above
|
|
169
|
+
#: (order-sensitive — next_stage/verify_state/stage_exit all walk that tuple, so it
|
|
170
|
+
#: is NEVER redefined) plus forge-0-epic, which also carries a stageEntry but is
|
|
171
|
+
#: excluded from the next-stage walk.
|
|
172
|
+
STATE_VERB_STAGES: Final[tuple[str, ...]] = ("forge-0-epic", *PRODUCTION_STAGES)
|
|
173
|
+
|
|
174
|
+
#: The `--raised-by` / `--target-stage` domains for `state-decision`, and the
|
|
175
|
+
#: `--kind` / `--raised-by` domains for `state-ecr`. SOURCE OF TRUTH:
|
|
176
|
+
#: references/pipeline-state-schema.json (the `deferredDecisions` and
|
|
177
|
+
#: `epicChangeRequests` array item enums). Mirrored here so an out-of-enum value is
|
|
178
|
+
#: rejected at parse time; a drift guard asserts they still match the schema.
|
|
179
|
+
DECISION_RAISED_BY: Final[tuple[str, ...]] = (
|
|
180
|
+
"forge-1-prd",
|
|
181
|
+
"forge-2-tech",
|
|
182
|
+
"forge-3-specs",
|
|
183
|
+
"forge-4-backlog",
|
|
184
|
+
)
|
|
185
|
+
DECISION_TARGET_STAGES: Final[tuple[str, ...]] = (
|
|
186
|
+
"forge-1-prd",
|
|
187
|
+
"forge-2-tech",
|
|
188
|
+
"forge-3-specs",
|
|
189
|
+
"forge-4-backlog",
|
|
190
|
+
"forge-5-loop",
|
|
191
|
+
"forge-6-docs",
|
|
192
|
+
)
|
|
193
|
+
ECR_KINDS: Final[tuple[str, ...]] = ("add-feature", "redep", "move-boundary", "split")
|
|
194
|
+
ECR_RAISED_BY: Final[tuple[str, ...]] = ("forge-1-prd", "forge-2-tech")
|
|
195
|
+
|
|
108
196
|
#: Production stage -> the verify token its findings file uses, and the
|
|
109
197
|
#: `forge-verify-<token>` key its state lives under. forge-6-docs has no verify.
|
|
110
198
|
VERIFY_TOKEN_BY_STAGE: Final[dict[str, str]] = {
|
|
@@ -1434,6 +1522,17 @@ def _resolve_feature_dir(specs_dir: Path, feature: str, epic: str | None) -> Pat
|
|
|
1434
1522
|
return flat
|
|
1435
1523
|
|
|
1436
1524
|
|
|
1525
|
+
def _host_command(command: str, host: str) -> str:
|
|
1526
|
+
"""Rewrite a `/feature-forge:` slash command to the host's surface.
|
|
1527
|
+
|
|
1528
|
+
Pi's slash-command surface is `/skill:` (matching the adapter body's
|
|
1529
|
+
`/feature-forge:` -> `/skill:` translation). The scripted stage-exit output bypasses
|
|
1530
|
+
that body translation, so it rewrites the commands it emits here. No-op for
|
|
1531
|
+
claude/generic, which keep the canonical `/feature-forge:` form.
|
|
1532
|
+
"""
|
|
1533
|
+
return command.replace("/feature-forge:", "/skill:") if host == "pi" else command
|
|
1534
|
+
|
|
1535
|
+
|
|
1437
1536
|
def _next_steps_block(
|
|
1438
1537
|
next_command: str, host: str, reconcile: dict | None = None
|
|
1439
1538
|
) -> str:
|
|
@@ -1463,6 +1562,18 @@ def _next_steps_block(
|
|
|
1463
1562
|
"2. Then start a fresh session and run the next stage below — or "
|
|
1464
1563
|
"re-run `/feature-forge:forge` to let the navigator resume from disk."
|
|
1465
1564
|
)
|
|
1565
|
+
elif host == "pi":
|
|
1566
|
+
# Pi's fresh-session command is `/new` (not `/clear`); its slash-command
|
|
1567
|
+
# surface is `/skill:` (the fenced command below is rewritten to match).
|
|
1568
|
+
clear_line = (
|
|
1569
|
+
"1. `/new` — recommended unconditionally at this stage boundary; every "
|
|
1570
|
+
"artifact is on disk, so the work survives starting a fresh session. "
|
|
1571
|
+
"I can't run `/new` for you — you have to run it yourself."
|
|
1572
|
+
)
|
|
1573
|
+
next_line = (
|
|
1574
|
+
"2. Then, in the new session, run the next stage below — or re-run "
|
|
1575
|
+
"`/skill:forge` to let the navigator resume from disk."
|
|
1576
|
+
)
|
|
1466
1577
|
else:
|
|
1467
1578
|
clear_line = (
|
|
1468
1579
|
"1. Clear your session / start a fresh session — recommended "
|
|
@@ -1479,7 +1590,7 @@ def _next_steps_block(
|
|
|
1479
1590
|
# epic-change request the primary is the reconcile command; otherwise it is the
|
|
1480
1591
|
# normal next-stage command. The fence sits before the sentinel, so the
|
|
1481
1592
|
# sentinel remains the absolute last line.
|
|
1482
|
-
fenced_command = reconcile["command"] if blocking else next_command
|
|
1593
|
+
fenced_command = _host_command(reconcile["command"] if blocking else next_command, host)
|
|
1483
1594
|
lines = ["**Next steps**", clear_line]
|
|
1484
1595
|
if blocking:
|
|
1485
1596
|
count = reconcile["count"]
|
|
@@ -1495,15 +1606,14 @@ def _next_steps_block(
|
|
|
1495
1606
|
lines.append("")
|
|
1496
1607
|
lines.append(f"```\n{fenced_command}\n```")
|
|
1497
1608
|
if blocking and reconcile.get("deferred"):
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
)
|
|
1609
|
+
deferred_cmd = _host_command(reconcile["deferred"], host)
|
|
1610
|
+
lines.append(f"After reconciling, continue the pipeline with: `{deferred_cmd}`")
|
|
1501
1611
|
elif reconcile and reconcile.get("reminder"):
|
|
1502
1612
|
count = reconcile["count"]
|
|
1503
1613
|
plural = "s" if count != 1 else ""
|
|
1504
1614
|
lines.append(
|
|
1505
1615
|
f"You also flagged {count} epic change{plural} to reconcile when "
|
|
1506
|
-
f"convenient: `{reconcile['command']}`"
|
|
1616
|
+
f"convenient: `{_host_command(reconcile['command'], host)}`"
|
|
1507
1617
|
)
|
|
1508
1618
|
lines.append(NEXT_STEPS_SENTINEL)
|
|
1509
1619
|
return "\n".join(lines)
|
|
@@ -1630,10 +1740,10 @@ def stage_exit(
|
|
|
1630
1740
|
"verifyGate": verify_gate,
|
|
1631
1741
|
"autoFixEligible": auto_fix_eligible,
|
|
1632
1742
|
"verifyState": verify_label,
|
|
1633
|
-
"verifyCommand": f"/feature-forge:forge-verify {feature}",
|
|
1743
|
+
"verifyCommand": _host_command(f"/feature-forge:forge-verify {feature}", host),
|
|
1634
1744
|
"autoVerifyEffective": effective_auto_verify,
|
|
1635
1745
|
"nextStage": next_stage_id,
|
|
1636
|
-
"nextCommand": next_command,
|
|
1746
|
+
"nextCommand": _host_command(next_command, host) if next_command else next_command,
|
|
1637
1747
|
"invalidAutoVerifyKeys": invalid_auto_verify_keys(config),
|
|
1638
1748
|
"gitRepo": git_repo,
|
|
1639
1749
|
"cleanTree": clean_tree,
|
|
@@ -1661,11 +1771,860 @@ def _print_stage_exit(payload: dict) -> None:
|
|
|
1661
1771
|
print(payload["nextSteps"])
|
|
1662
1772
|
|
|
1663
1773
|
|
|
1774
|
+
# --------------------------------------------------------------------------- #
|
|
1775
|
+
# Effective loopRunner config
|
|
1776
|
+
# --------------------------------------------------------------------------- #
|
|
1777
|
+
|
|
1778
|
+
|
|
1779
|
+
def _default_schema_path() -> Path:
|
|
1780
|
+
"""Return the bundled forge-config-schema.json path (sibling references/ dir).
|
|
1781
|
+
|
|
1782
|
+
Resolved relative to this script file so `effective-config` works from any
|
|
1783
|
+
cwd. Overridable via the ``--schema`` flag (chiefly for tests).
|
|
1784
|
+
|
|
1785
|
+
Returns:
|
|
1786
|
+
The Path to ``references/forge-config-schema.json`` next to ``scripts/``.
|
|
1787
|
+
"""
|
|
1788
|
+
return Path(__file__).resolve().parent.parent / "references" / "forge-config-schema.json"
|
|
1789
|
+
|
|
1790
|
+
|
|
1791
|
+
def _loop_runner_defaults(schema_path: Path) -> dict[str, object]:
|
|
1792
|
+
"""Extract every ``loopRunner`` field's schema ``default``.
|
|
1793
|
+
|
|
1794
|
+
Reads ``properties.loopRunner.properties.<field>.default`` for each field.
|
|
1795
|
+
Stdlib-only (``json`` + dict access), mirroring
|
|
1796
|
+
``tests/test_config_defaults_parity.py``. The schema is the single source of
|
|
1797
|
+
truth; nothing here is hardcoded.
|
|
1798
|
+
|
|
1799
|
+
Only fields that actually declare a ``default`` keyword are included. Every
|
|
1800
|
+
``loopRunner`` field does today; a field losing its default would be a schema
|
|
1801
|
+
regression the drift guard catches, not something silently patched here.
|
|
1802
|
+
|
|
1803
|
+
Args:
|
|
1804
|
+
schema_path: Path to ``forge-config-schema.json``.
|
|
1805
|
+
|
|
1806
|
+
Returns:
|
|
1807
|
+
A dict mapping each ``loopRunner`` field name to its declared default
|
|
1808
|
+
value (templates such as ``"{bin} loop run …"`` are returned literally).
|
|
1809
|
+
|
|
1810
|
+
Raises:
|
|
1811
|
+
UsageError: If the schema is missing, unreadable, unparseable, or lacks a
|
|
1812
|
+
``loopRunner.properties`` object — a deterministic failure that must
|
|
1813
|
+
exit 2. Never returns partial/empty defaults silently.
|
|
1814
|
+
"""
|
|
1815
|
+
try:
|
|
1816
|
+
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
1817
|
+
except OSError as exc:
|
|
1818
|
+
raise UsageError(f"config schema unreadable: {schema_path} ({exc})") from exc
|
|
1819
|
+
except json.JSONDecodeError as exc:
|
|
1820
|
+
raise UsageError(f"config schema is not valid JSON: {schema_path} ({exc})") from exc
|
|
1821
|
+
|
|
1822
|
+
props = None
|
|
1823
|
+
if isinstance(schema, dict):
|
|
1824
|
+
loop_runner = schema.get("properties", {})
|
|
1825
|
+
if isinstance(loop_runner, dict):
|
|
1826
|
+
loop_runner = loop_runner.get("loopRunner", {})
|
|
1827
|
+
if isinstance(loop_runner, dict):
|
|
1828
|
+
props = loop_runner.get("properties")
|
|
1829
|
+
if not isinstance(props, dict) or not props:
|
|
1830
|
+
raise UsageError(f"config schema has no loopRunner.properties object: {schema_path}")
|
|
1831
|
+
|
|
1832
|
+
return {
|
|
1833
|
+
field: spec["default"]
|
|
1834
|
+
for field, spec in props.items()
|
|
1835
|
+
if isinstance(spec, dict) and "default" in spec
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
|
|
1839
|
+
def resolve_loop_runner(config_path: Path, schema_path: Path) -> dict[str, object]:
|
|
1840
|
+
"""Resolve the effective ``loopRunner`` config: schema defaults + user overrides.
|
|
1841
|
+
|
|
1842
|
+
Reads the schema defaults, then merges the user's ``loopRunner`` block (from
|
|
1843
|
+
``forge.config.json`` via the existing ``_load_config``) OVER them. A user
|
|
1844
|
+
field replaces the default; an absent field keeps the default. The result is
|
|
1845
|
+
the fully-resolved block the loop consumes — computed deterministically so no
|
|
1846
|
+
model ever merges it by hand.
|
|
1847
|
+
|
|
1848
|
+
Args:
|
|
1849
|
+
config_path: Path to ``forge.config.json`` (``_load_config`` tolerates a
|
|
1850
|
+
missing/corrupt file, yielding pure defaults).
|
|
1851
|
+
schema_path: Path to ``forge-config-schema.json`` (source of the defaults).
|
|
1852
|
+
|
|
1853
|
+
Returns:
|
|
1854
|
+
The resolved ``loopRunner`` object: every schema-defaulted field present,
|
|
1855
|
+
with user overrides applied.
|
|
1856
|
+
|
|
1857
|
+
Raises:
|
|
1858
|
+
UsageError: If the schema is unreadable/unparseable (propagated from
|
|
1859
|
+
``_loop_runner_defaults``) — exit 2, a deterministic failure.
|
|
1860
|
+
"""
|
|
1861
|
+
resolved: dict[str, object] = dict(_loop_runner_defaults(schema_path))
|
|
1862
|
+
|
|
1863
|
+
user_loop_runner = _load_config(config_path).get("loopRunner")
|
|
1864
|
+
if isinstance(user_loop_runner, dict):
|
|
1865
|
+
for key, value in user_loop_runner.items():
|
|
1866
|
+
# Flat override: a user value replaces the default for that field.
|
|
1867
|
+
# (A future nested loopRunner field would recurse here; today every
|
|
1868
|
+
# field is a scalar, so a shallow override is exact.) An unknown key
|
|
1869
|
+
# is carried through — the model would have carried it too, and the
|
|
1870
|
+
# config schema is the authority that flags it at author time.
|
|
1871
|
+
resolved[key] = value
|
|
1872
|
+
|
|
1873
|
+
return resolved
|
|
1874
|
+
|
|
1875
|
+
|
|
1876
|
+
def _print_effective_config(resolved: dict[str, object]) -> None:
|
|
1877
|
+
"""Print the resolved loopRunner config as an aligned key: value table.
|
|
1878
|
+
|
|
1879
|
+
Args:
|
|
1880
|
+
resolved: The resolved loopRunner object from ``resolve_loop_runner``.
|
|
1881
|
+
"""
|
|
1882
|
+
print("Effective loopRunner config:")
|
|
1883
|
+
width = max((len(k) for k in resolved), default=0)
|
|
1884
|
+
for key in sorted(resolved):
|
|
1885
|
+
print(f" {key.ljust(width)} : {resolved[key]!r}")
|
|
1886
|
+
|
|
1887
|
+
|
|
1888
|
+
# --------------------------------------------------------------------------- #
|
|
1889
|
+
# State writes (shared machinery for the state-* verbs)
|
|
1890
|
+
# --------------------------------------------------------------------------- #
|
|
1891
|
+
|
|
1892
|
+
|
|
1893
|
+
def _now_iso() -> str:
|
|
1894
|
+
"""Return the current UTC time as a Z-suffixed, second-precision ISO-8601 string.
|
|
1895
|
+
|
|
1896
|
+
Matches the `.pipeline-state.json` timestamp convention already on disk (the
|
|
1897
|
+
schema's ``format: date-time`` values; the read path normalizes a trailing
|
|
1898
|
+
``Z``). Second precision keeps `updatedAt`/`startedAt`/`completedAt` visually
|
|
1899
|
+
consistent with the values other pipeline writers produce.
|
|
1900
|
+
|
|
1901
|
+
Returns:
|
|
1902
|
+
A timestamp like ``"2026-07-29T03:30:00Z"``.
|
|
1903
|
+
"""
|
|
1904
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
1905
|
+
|
|
1906
|
+
|
|
1907
|
+
def _write_state(state_path: Path, state: dict) -> None:
|
|
1908
|
+
"""Atomically write a `.pipeline-state.json` (temp file + os.replace).
|
|
1909
|
+
|
|
1910
|
+
Mirrors epic-manifest.py's ``atomic_write``: write to a sibling temp file in
|
|
1911
|
+
the same directory as the target, flush + fsync the bytes, then os.replace()
|
|
1912
|
+
the temp file onto the target. os.replace is atomic on POSIX within one
|
|
1913
|
+
filesystem, so an interrupted write never leaves a partial or corrupt state
|
|
1914
|
+
file. Concurrent multi-session mutation is out of scope (single writer
|
|
1915
|
+
assumed, matching epic-manifest.py).
|
|
1916
|
+
|
|
1917
|
+
Args:
|
|
1918
|
+
state_path: Destination path, e.g.
|
|
1919
|
+
``{specsDir}/{feature}/.pipeline-state.json``.
|
|
1920
|
+
state: The fully-formed state dict to serialize.
|
|
1921
|
+
|
|
1922
|
+
Raises:
|
|
1923
|
+
UsageError: If the temp file cannot be created/written or the replace
|
|
1924
|
+
fails (→ exit 2). The temp file is removed first, so a failed write
|
|
1925
|
+
leaves no debris and the original target untouched.
|
|
1926
|
+
"""
|
|
1927
|
+
try:
|
|
1928
|
+
fd, tmp_name = tempfile.mkstemp(
|
|
1929
|
+
prefix=f".{state_path.name}.", suffix=".tmp", dir=state_path.parent
|
|
1930
|
+
)
|
|
1931
|
+
except OSError as exc:
|
|
1932
|
+
raise UsageError(f"atomic write to {state_path} failed: {exc}") from exc
|
|
1933
|
+
tmp_path = Path(tmp_name)
|
|
1934
|
+
try:
|
|
1935
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
1936
|
+
json.dump(state, handle, indent=2, ensure_ascii=False)
|
|
1937
|
+
handle.write("\n")
|
|
1938
|
+
handle.flush()
|
|
1939
|
+
os.fsync(handle.fileno())
|
|
1940
|
+
os.replace(tmp_path, state_path)
|
|
1941
|
+
except OSError as exc:
|
|
1942
|
+
tmp_path.unlink(missing_ok=True)
|
|
1943
|
+
raise UsageError(f"atomic write to {state_path} failed: {exc}") from exc
|
|
1944
|
+
|
|
1945
|
+
|
|
1946
|
+
def _resolve_feature_dir_for_write(
|
|
1947
|
+
specs_dir: Path, feature: str, epic: str | None
|
|
1948
|
+
) -> Path:
|
|
1949
|
+
"""Fail-closed feature dir for the ``state-*`` WRITERS.
|
|
1950
|
+
|
|
1951
|
+
``_resolve_feature_dir`` is the reader's best-effort resolver: it returns the
|
|
1952
|
+
flat ``{specsDir}/{feature}`` whenever that dir carries a state file, and
|
|
1953
|
+
falls back to the flat literal on a multi-match. That tolerance was written
|
|
1954
|
+
for ``stage-exit``, which is READ-ONLY — an unresolvable dir there just
|
|
1955
|
+
downgrades to ``{}``. For a writer the same tolerance means a bare
|
|
1956
|
+
``--feature api`` mutates a standalone ``{specsDir}/api/`` while an epic
|
|
1957
|
+
member ``{specsDir}/{epic}/api/`` of the same name is silently left behind:
|
|
1958
|
+
cross-feature state corruption at exit 0.
|
|
1959
|
+
|
|
1960
|
+
So the write path mirrors ``epic-manifest.py resolve`` — the canonical
|
|
1961
|
+
resolver that produced ``{resolvedFeatureDir}`` in the first place, and which
|
|
1962
|
+
rejects an ambiguous name with a structured ``ambiguous:`` finding. A writer
|
|
1963
|
+
must not be more permissive than that resolver: more than one candidate
|
|
1964
|
+
carrying a state file, with no explicit ``--epic``, is a hard stop.
|
|
1965
|
+
|
|
1966
|
+
Args:
|
|
1967
|
+
specs_dir: The configured specs directory (``--specs-dir``).
|
|
1968
|
+
feature: The feature name (``--feature``).
|
|
1969
|
+
epic: The owning epic name for a nested member, else None (``--epic``).
|
|
1970
|
+
|
|
1971
|
+
Returns:
|
|
1972
|
+
The resolved feature directory. With ``--epic`` the nested path is taken
|
|
1973
|
+
verbatim; otherwise the single candidate carrying a state file, or the
|
|
1974
|
+
flat path when none does (the first-write case).
|
|
1975
|
+
|
|
1976
|
+
Raises:
|
|
1977
|
+
UsageError: The bare name matches more than one directory carrying a
|
|
1978
|
+
state file (→ exit 2, nothing written).
|
|
1979
|
+
"""
|
|
1980
|
+
if epic:
|
|
1981
|
+
return specs_dir / epic / feature
|
|
1982
|
+
flat = specs_dir / feature
|
|
1983
|
+
candidates = [flat] if (flat / PIPELINE_STATE_FILENAME).is_file() else []
|
|
1984
|
+
if specs_dir.is_dir():
|
|
1985
|
+
candidates.extend(
|
|
1986
|
+
sorted(
|
|
1987
|
+
p
|
|
1988
|
+
for p in specs_dir.glob(f"*/{feature}")
|
|
1989
|
+
if (p / PIPELINE_STATE_FILENAME).is_file()
|
|
1990
|
+
)
|
|
1991
|
+
)
|
|
1992
|
+
if len(candidates) > 1:
|
|
1993
|
+
listed = ", ".join(str(p) for p in candidates)
|
|
1994
|
+
raise UsageError(
|
|
1995
|
+
f"ambiguous feature {feature!r}: {len(candidates)} directories carry a "
|
|
1996
|
+
f"state file ({listed}) — pass --epic <epic> to name the one to write. "
|
|
1997
|
+
f"Refusing to guess; nothing was written."
|
|
1998
|
+
)
|
|
1999
|
+
return candidates[0] if candidates else flat
|
|
2000
|
+
|
|
2001
|
+
|
|
2002
|
+
def _load_state_for_write(
|
|
2003
|
+
specs_dir: Path, feature: str, epic: str | None
|
|
2004
|
+
) -> tuple[Path, dict]:
|
|
2005
|
+
"""Resolve a feature's state path and load its current state for mutation.
|
|
2006
|
+
|
|
2007
|
+
Resolves through the fail-closed `_resolve_feature_dir_for_write`, NOT the
|
|
2008
|
+
reader's tolerant `_resolve_feature_dir`. Deliberately does NOT
|
|
2009
|
+
reuse `_read_state`: that reader downgrades a *corrupt* file to ``{}`` because
|
|
2010
|
+
the navigator's read-only sweep can safely treat it as not-started. A writer
|
|
2011
|
+
that inherited it would atomically replace a corrupt-but-recoverable state
|
|
2012
|
+
file with a near-empty one at exit 0. So: absent -> ``{}``; present but
|
|
2013
|
+
unparseable -> refuse, leaving the file byte-intact.
|
|
2014
|
+
|
|
2015
|
+
The verbs never create a feature directory; an unknown ``--feature`` is a
|
|
2016
|
+
usage error, not a silent create.
|
|
2017
|
+
|
|
2018
|
+
Args:
|
|
2019
|
+
specs_dir: The configured specs directory (``--specs-dir``).
|
|
2020
|
+
feature: The feature name (``--feature``).
|
|
2021
|
+
epic: The owning epic name for a nested member, else None (``--epic``).
|
|
2022
|
+
|
|
2023
|
+
Returns:
|
|
2024
|
+
A ``(state_path, state)`` tuple. ``state`` is a schema-shaped shell when
|
|
2025
|
+
no state file exists yet (see the seeding below).
|
|
2026
|
+
|
|
2027
|
+
Raises:
|
|
2028
|
+
UsageError: The bare ``feature`` name is ambiguous (more than one
|
|
2029
|
+
candidate directory carries a state file and no ``--epic`` was
|
|
2030
|
+
given), the feature directory does not exist, or the state file
|
|
2031
|
+
exists but is not a JSON object (→ exit 2).
|
|
2032
|
+
"""
|
|
2033
|
+
state_dir = _resolve_feature_dir_for_write(specs_dir, feature, epic)
|
|
2034
|
+
if not state_dir.is_dir():
|
|
2035
|
+
raise UsageError(
|
|
2036
|
+
f"no feature directory at {state_dir} — check --feature "
|
|
2037
|
+
f"(and --epic for a nested epic member)"
|
|
2038
|
+
)
|
|
2039
|
+
state_path = state_dir / PIPELINE_STATE_FILENAME
|
|
2040
|
+
if state_path.exists():
|
|
2041
|
+
try:
|
|
2042
|
+
state = json.loads(state_path.read_text(encoding="utf-8"))
|
|
2043
|
+
except json.JSONDecodeError as exc:
|
|
2044
|
+
raise UsageError(
|
|
2045
|
+
f"{state_path} exists but is not valid JSON ({exc}); refusing to "
|
|
2046
|
+
f"overwrite it. Fix or move the file, then re-run."
|
|
2047
|
+
) from exc
|
|
2048
|
+
if not isinstance(state, dict):
|
|
2049
|
+
raise UsageError(
|
|
2050
|
+
f"{state_path} is not a JSON object; refusing to overwrite it."
|
|
2051
|
+
)
|
|
2052
|
+
else:
|
|
2053
|
+
state = {}
|
|
2054
|
+
|
|
2055
|
+
# Seed the schema-required top-level fields for EVERY verb, not just
|
|
2056
|
+
# state-enter. Branch Setup fires state-branch before the entry stamp
|
|
2057
|
+
# (references/shared-conventions.md), so without this a first-write
|
|
2058
|
+
# state-branch would persist {"branch": ..., "updatedAt": ...} — missing
|
|
2059
|
+
# every required field — at exit 0. setdefault keeps existing state as-is.
|
|
2060
|
+
# (`updatedAt`, the sixth required field, is stamped by _commit_state.)
|
|
2061
|
+
state.setdefault("feature", feature)
|
|
2062
|
+
state.setdefault("createdAt", _now_iso())
|
|
2063
|
+
state.setdefault("pipelineStatus", "active")
|
|
2064
|
+
state.setdefault("stages", {})
|
|
2065
|
+
state.setdefault("currentStage", PRODUCTION_STAGES[0])
|
|
2066
|
+
return state_path, state
|
|
2067
|
+
|
|
2068
|
+
|
|
2069
|
+
def _commit_state(state_path: Path, state: dict) -> dict:
|
|
2070
|
+
"""Refresh ``updatedAt`` and write ``state`` atomically; return it for echo.
|
|
2071
|
+
|
|
2072
|
+
Every verb calls this exactly once, after its mutation, so ``updatedAt`` is
|
|
2073
|
+
always refreshed on a successful write and the write is atomic.
|
|
2074
|
+
|
|
2075
|
+
Args:
|
|
2076
|
+
state_path: The resolved ``.pipeline-state.json`` path.
|
|
2077
|
+
state: The mutated state dict.
|
|
2078
|
+
|
|
2079
|
+
Returns:
|
|
2080
|
+
The same ``state`` dict (now carrying a fresh ``updatedAt``), so the verb
|
|
2081
|
+
can echo it under ``--json``.
|
|
2082
|
+
|
|
2083
|
+
Raises:
|
|
2084
|
+
UsageError: If the atomic write fails (→ exit 2).
|
|
2085
|
+
"""
|
|
2086
|
+
state["updatedAt"] = _now_iso()
|
|
2087
|
+
_write_state(state_path, state)
|
|
2088
|
+
return state
|
|
2089
|
+
|
|
2090
|
+
|
|
2091
|
+
def _stage_entry(state: dict, stage: str) -> dict:
|
|
2092
|
+
"""Return (creating if absent) the mutable ``stages.{stage}`` sub-object.
|
|
2093
|
+
|
|
2094
|
+
Bootstraps ``state["stages"]`` and ``state["stages"][stage]`` when missing, so
|
|
2095
|
+
a verb can write into a brand-new state (``{}``), and returns the stage dict
|
|
2096
|
+
for in-place mutation. The bootstrap seeds ``{"status": "pending"}`` rather
|
|
2097
|
+
than ``{}`` because ``stageEntry`` declares ``required: ["status"]`` — an entry
|
|
2098
|
+
created by state-artifact (which sets only ``artifacts``) would otherwise be
|
|
2099
|
+
schema-invalid at exit 0.
|
|
2100
|
+
|
|
2101
|
+
Args:
|
|
2102
|
+
state: The full state dict (mutated in place).
|
|
2103
|
+
stage: A stage id from ``STATE_VERB_STAGES`` (e.g. ``"forge-1-prd"``).
|
|
2104
|
+
|
|
2105
|
+
Returns:
|
|
2106
|
+
The mutable ``stages.{stage}`` dict.
|
|
2107
|
+
"""
|
|
2108
|
+
stages = state.setdefault("stages", {})
|
|
2109
|
+
return stages.setdefault(stage, {"status": "pending"})
|
|
2110
|
+
|
|
2111
|
+
|
|
2112
|
+
# --------------------------------------------------------------------------- #
|
|
2113
|
+
# State-write verbs
|
|
2114
|
+
# --------------------------------------------------------------------------- #
|
|
2115
|
+
|
|
2116
|
+
|
|
2117
|
+
def cmd_state_enter(feature: str, stage: str, specs_dir: Path, epic: str | None) -> dict:
|
|
2118
|
+
"""Apply the Entry Stamp: mark ``stage`` in-progress and set ``currentStage``.
|
|
2119
|
+
|
|
2120
|
+
Idempotent on re-entry within the same run: re-stamping an already
|
|
2121
|
+
in-progress stage simply refreshes ``startedAt``/``updatedAt``. The
|
|
2122
|
+
interactive resume-vs-restart decision stays the skill's — the verb never
|
|
2123
|
+
prompts. The write is left uncommitted; the stage's existing exit commit
|
|
2124
|
+
stages it later.
|
|
2125
|
+
|
|
2126
|
+
Args:
|
|
2127
|
+
feature: Feature name.
|
|
2128
|
+
stage: The stage being entered (a ``STATE_VERB_STAGES`` id).
|
|
2129
|
+
specs_dir: Specs directory.
|
|
2130
|
+
epic: Owning epic name, or None.
|
|
2131
|
+
|
|
2132
|
+
Returns:
|
|
2133
|
+
The mutated state dict (for the --json echo).
|
|
2134
|
+
|
|
2135
|
+
Raises:
|
|
2136
|
+
UsageError: Unknown feature directory, unparseable state file, or a
|
|
2137
|
+
failed atomic write (→ exit 2).
|
|
2138
|
+
"""
|
|
2139
|
+
state_path, state = _load_state_for_write(specs_dir, feature, epic)
|
|
2140
|
+
entry = _stage_entry(state, stage)
|
|
2141
|
+
entry["status"] = "in-progress"
|
|
2142
|
+
entry["startedAt"] = _now_iso()
|
|
2143
|
+
state["currentStage"] = stage
|
|
2144
|
+
return _commit_state(state_path, state)
|
|
2145
|
+
|
|
2146
|
+
|
|
2147
|
+
def cmd_state_artifact(
|
|
2148
|
+
feature: str, stage: str, paths: list[str], specs_dir: Path, epic: str | None
|
|
2149
|
+
) -> dict:
|
|
2150
|
+
"""Append each path in ``paths`` to ``stages.{stage}.artifacts``, de-duplicating.
|
|
2151
|
+
|
|
2152
|
+
Idempotent: an already-tracked path is a no-op (no duplicate append), so a
|
|
2153
|
+
resumed run that re-records files it wrote earlier does not bloat the array.
|
|
2154
|
+
``updatedAt`` is refreshed even on the all-duplicates branch, keeping "state
|
|
2155
|
+
was touched" honest. The verb does NOT stat the file — it records the path
|
|
2156
|
+
the skill asserts it wrote.
|
|
2157
|
+
|
|
2158
|
+
Args:
|
|
2159
|
+
feature: Feature name.
|
|
2160
|
+
stage: The producing stage id.
|
|
2161
|
+
paths: Artifact paths relative to the feature dir (repeatable ``--path``).
|
|
2162
|
+
specs_dir: Specs directory.
|
|
2163
|
+
epic: Owning epic name, or None.
|
|
2164
|
+
|
|
2165
|
+
Returns:
|
|
2166
|
+
The mutated state dict (for the --json echo).
|
|
2167
|
+
|
|
2168
|
+
Raises:
|
|
2169
|
+
UsageError: Unknown feature directory, unparseable state file, or a
|
|
2170
|
+
failed atomic write (→ exit 2).
|
|
2171
|
+
"""
|
|
2172
|
+
state_path, state = _load_state_for_write(specs_dir, feature, epic)
|
|
2173
|
+
entry = _stage_entry(state, stage)
|
|
2174
|
+
artifacts = entry.setdefault("artifacts", [])
|
|
2175
|
+
for path in paths:
|
|
2176
|
+
if path not in artifacts:
|
|
2177
|
+
artifacts.append(path)
|
|
2178
|
+
return _commit_state(state_path, state)
|
|
2179
|
+
|
|
2180
|
+
|
|
2181
|
+
def _parse_based_on(pairs: list[str]) -> dict[str, int]:
|
|
2182
|
+
"""Parse ``--based-on STAGE=N`` tokens into a ``{stageId: int}`` map.
|
|
2183
|
+
|
|
2184
|
+
Args:
|
|
2185
|
+
pairs: Raw ``STAGE=N`` strings from repeated ``--based-on`` flags.
|
|
2186
|
+
|
|
2187
|
+
Returns:
|
|
2188
|
+
A ``{stageId: version}`` dict (empty when no pairs were given — the
|
|
2189
|
+
forge-1-prd case, which records ``basedOnVersions == {}``).
|
|
2190
|
+
|
|
2191
|
+
Raises:
|
|
2192
|
+
UsageError: If a token lacks ``=`` or its value is not an integer
|
|
2193
|
+
(→ exit 2).
|
|
2194
|
+
"""
|
|
2195
|
+
out: dict[str, int] = {}
|
|
2196
|
+
for token in pairs:
|
|
2197
|
+
if "=" not in token:
|
|
2198
|
+
raise UsageError(f"--based-on expects STAGE=N, got: {token!r}")
|
|
2199
|
+
stage_id, _, raw = token.partition("=")
|
|
2200
|
+
try:
|
|
2201
|
+
out[stage_id] = int(raw)
|
|
2202
|
+
except ValueError as exc:
|
|
2203
|
+
raise UsageError(f"--based-on version must be an integer: {token!r}") from exc
|
|
2204
|
+
return out
|
|
2205
|
+
|
|
2206
|
+
|
|
2207
|
+
#: Stages the staleness cascade may mark stale (downstream authored artifacts).
|
|
2208
|
+
#: The scope is tech..docs, matching the pre-R4 canon this cascade replaces —
|
|
2209
|
+
#: forge-1-prd L134 named `forge-2-tech` FIRST among the stages a PRD revision
|
|
2210
|
+
#: invalidates, and the tech spec is a PRD revision's most direct dependent.
|
|
2211
|
+
#: forge-1-prd is never marked stale by a later completion (nothing downstream
|
|
2212
|
+
#: feeds back into it). Keyed off this map, NOT off PRODUCTION_STAGES ordering —
|
|
2213
|
+
#: the two are not interchangeable (a positional slice from the completing stage
|
|
2214
|
+
#: would also break on forge-0-epic, which is a valid --stage but not a
|
|
2215
|
+
#: PRODUCTION_STAGES member).
|
|
2216
|
+
_CASCADE_TARGETS: Final[tuple[str, ...]] = (
|
|
2217
|
+
"forge-2-tech",
|
|
2218
|
+
"forge-3-specs",
|
|
2219
|
+
"forge-4-backlog",
|
|
2220
|
+
"forge-5-loop",
|
|
2221
|
+
"forge-6-docs",
|
|
2222
|
+
)
|
|
2223
|
+
|
|
2224
|
+
|
|
2225
|
+
def _cascade_staleness(state: dict, completed_stage: str, new_version: int) -> list[str]:
|
|
2226
|
+
"""Mark downstream stages ``stale`` when they were built on an OLDER version.
|
|
2227
|
+
|
|
2228
|
+
Deterministic replacement for the model-prose rule in each stage's completion
|
|
2229
|
+
step ("if any downstream stage has basedOnVersions referencing an older
|
|
2230
|
+
version, set its status to stale"). For every downstream target (tech..docs),
|
|
2231
|
+
if its recorded ``basedOnVersions[completed_stage]`` is an integer strictly
|
|
2232
|
+
less than ``new_version`` AND the stage is currently ``complete``, flip it to
|
|
2233
|
+
``stale``. A downstream stage that never referenced this upstream, or already
|
|
2234
|
+
references the new version, is untouched. A ``pending``/``in-progress``/
|
|
2235
|
+
already-``stale`` downstream stage is not re-flipped — only a ``complete``
|
|
2236
|
+
artifact can go stale.
|
|
2237
|
+
|
|
2238
|
+
Args:
|
|
2239
|
+
state: The full state dict (mutated in place).
|
|
2240
|
+
completed_stage: The stage that just completed (e.g. "forge-1-prd").
|
|
2241
|
+
new_version: That stage's new version.
|
|
2242
|
+
|
|
2243
|
+
Returns:
|
|
2244
|
+
The list of stage ids newly marked stale (for the --json echo / printer).
|
|
2245
|
+
"""
|
|
2246
|
+
stages = state.get("stages", {})
|
|
2247
|
+
newly_stale: list[str] = []
|
|
2248
|
+
for target in _CASCADE_TARGETS:
|
|
2249
|
+
if target == completed_stage:
|
|
2250
|
+
continue
|
|
2251
|
+
entry = stages.get(target)
|
|
2252
|
+
if not isinstance(entry, dict) or entry.get("status") != "complete":
|
|
2253
|
+
continue
|
|
2254
|
+
based_on = entry.get("basedOnVersions")
|
|
2255
|
+
if not isinstance(based_on, dict):
|
|
2256
|
+
continue
|
|
2257
|
+
recorded = based_on.get(completed_stage)
|
|
2258
|
+
if isinstance(recorded, int) and not isinstance(recorded, bool) and recorded < new_version:
|
|
2259
|
+
entry["status"] = "stale"
|
|
2260
|
+
newly_stale.append(target)
|
|
2261
|
+
return newly_stale
|
|
2262
|
+
|
|
2263
|
+
|
|
2264
|
+
def cmd_state_complete(
|
|
2265
|
+
feature: str,
|
|
2266
|
+
stage: str,
|
|
2267
|
+
version: int,
|
|
2268
|
+
based_on: dict[str, int],
|
|
2269
|
+
artifacts: list[str],
|
|
2270
|
+
commit_hash: str | None,
|
|
2271
|
+
specs_dir: Path,
|
|
2272
|
+
epic: str | None,
|
|
2273
|
+
status: str | None = None,
|
|
2274
|
+
preserve_commit_hash: bool = False,
|
|
2275
|
+
resumable: bool = False,
|
|
2276
|
+
) -> dict:
|
|
2277
|
+
"""Mark ``stage`` complete, bump version, record provenance, cascade staleness.
|
|
2278
|
+
|
|
2279
|
+
Three branches, in precedence order:
|
|
2280
|
+
|
|
2281
|
+
1. ``commit_hash`` given — Commit 2 of the two-commit Git Commit Protocol.
|
|
2282
|
+
Sets ONLY ``commitHash``, leaving status/version/artifacts intact. Guarded
|
|
2283
|
+
on the stage already being ``complete``, so a typo'd ``--stage`` cannot
|
|
2284
|
+
write a lone ``{"commitHash": …}`` entry (which would violate
|
|
2285
|
+
``stageEntry``'s ``required: ["status"]``) at exit 0.
|
|
2286
|
+
2. ``resumable`` — the failed-Commit-1 revert (`references/shared-conventions.md`
|
|
2287
|
+
L245). Records ONLY ``status = "in-progress"`` plus the ``updatedAt``
|
|
2288
|
+
refresh: no completedAt, no version bump, no basedOnVersions/artifacts
|
|
2289
|
+
write, no commitHash reset, no cascade. The frozen contract is "leave state
|
|
2290
|
+
as in-progress so the stage can be resumed"; stamping a completion, bumping
|
|
2291
|
+
the version, or cascading staleness off a commit that never landed are all
|
|
2292
|
+
behavioral changes.
|
|
2293
|
+
3. Otherwise — the completion write: status, completedAt, version,
|
|
2294
|
+
basedOnVersions, artifacts, ``commitHash = None`` (Commit 1) unless
|
|
2295
|
+
``preserve_commit_hash``, then the downstream staleness cascade.
|
|
2296
|
+
|
|
2297
|
+
Branch 2 is gated on ``resumable``, NOT on ``status == "in-progress"``:
|
|
2298
|
+
forge-5-loop's PARTIAL completion also passes ``--status in-progress`` but is a
|
|
2299
|
+
real completion-with-artifacts, so it takes branch 3 and keeps its
|
|
2300
|
+
completedAt/version/basedOnVersions/artifacts. Only ``status`` differs between
|
|
2301
|
+
``--status complete`` and a bare ``--status in-progress``. Conflating the two
|
|
2302
|
+
would silently discard the ``--based-on`` item 013 passes on that call.
|
|
2303
|
+
|
|
2304
|
+
Args:
|
|
2305
|
+
feature: Feature name.
|
|
2306
|
+
stage: The completing stage id.
|
|
2307
|
+
version: The stage's new version.
|
|
2308
|
+
based_on: Parsed ``{upstreamStage: version}`` provenance map.
|
|
2309
|
+
artifacts: Final canonical artifact path list for this stage.
|
|
2310
|
+
commit_hash: If given, record it as the stage's commitHash (Commit 2);
|
|
2311
|
+
else set commitHash to None (Commit 1).
|
|
2312
|
+
specs_dir: Specs directory.
|
|
2313
|
+
epic: Owning epic name, or None.
|
|
2314
|
+
status: Terminal status to record — "complete" (the default when the flag
|
|
2315
|
+
is absent) or "in-progress" for a partial forge-5-loop run. ``None``
|
|
2316
|
+
means "not passed".
|
|
2317
|
+
preserve_commit_hash: Skip the ``commitHash = None`` reset, for the Git
|
|
2318
|
+
Commit Protocol's "Nothing to commit" branch (L248).
|
|
2319
|
+
resumable: Failed-Commit-1 revert (L245). Record only the status; implies
|
|
2320
|
+
``--status in-progress``.
|
|
2321
|
+
|
|
2322
|
+
Returns:
|
|
2323
|
+
The mutated state dict, plus a synthetic ``_cascadedStale`` key that is
|
|
2324
|
+
surfaced in the --json echo / printer but NEVER written to disk.
|
|
2325
|
+
|
|
2326
|
+
Raises:
|
|
2327
|
+
UsageError: Contradictory ``--resumable --status complete``, a
|
|
2328
|
+
``--commit-hash`` follow-up against a stage that is not complete, an
|
|
2329
|
+
unknown feature directory, an unparseable state file, or a failed
|
|
2330
|
+
atomic write (→ exit 2).
|
|
2331
|
+
"""
|
|
2332
|
+
if resumable and status == "complete":
|
|
2333
|
+
raise UsageError(
|
|
2334
|
+
"--resumable implies --status in-progress; do not pass --status complete"
|
|
2335
|
+
)
|
|
2336
|
+
state_path, state = _load_state_for_write(specs_dir, feature, epic)
|
|
2337
|
+
entry = _stage_entry(state, stage)
|
|
2338
|
+
cascaded: list[str] = []
|
|
2339
|
+
if commit_hash is not None:
|
|
2340
|
+
# Commit-2 follow-up: record the real hash, leave everything else intact.
|
|
2341
|
+
actual = entry.get("status")
|
|
2342
|
+
if actual != _DONE_STATUS:
|
|
2343
|
+
raise UsageError(
|
|
2344
|
+
f"--commit-hash requires {stage} to be complete (status: {actual!r}); "
|
|
2345
|
+
"run state-complete without --commit-hash first"
|
|
2346
|
+
)
|
|
2347
|
+
entry["commitHash"] = commit_hash
|
|
2348
|
+
elif resumable:
|
|
2349
|
+
# Failed-Commit-1 revert (L245): record ONLY the status. See the note above
|
|
2350
|
+
# on why this is gated on --resumable rather than on the status value.
|
|
2351
|
+
entry["status"] = "in-progress"
|
|
2352
|
+
else:
|
|
2353
|
+
entry["status"] = status or _DONE_STATUS # "complete" | "in-progress" (partial)
|
|
2354
|
+
entry["completedAt"] = _now_iso()
|
|
2355
|
+
entry["version"] = version
|
|
2356
|
+
entry["basedOnVersions"] = based_on
|
|
2357
|
+
entry["artifacts"] = artifacts
|
|
2358
|
+
if not preserve_commit_hash:
|
|
2359
|
+
entry["commitHash"] = None # Commit 1 of the Commit Protocol
|
|
2360
|
+
cascaded = _cascade_staleness(state, stage, version)
|
|
2361
|
+
result = _commit_state(state_path, state)
|
|
2362
|
+
# Surface the cascade result for the caller without persisting it in state:
|
|
2363
|
+
# _commit_state already wrote the real dict, and `echo` is a copy.
|
|
2364
|
+
echo = dict(result)
|
|
2365
|
+
echo["_cascadedStale"] = cascaded
|
|
2366
|
+
return echo
|
|
2367
|
+
|
|
2368
|
+
|
|
2369
|
+
def cmd_state_branch(feature: str, branch: str, specs_dir: Path, epic: str | None) -> dict:
|
|
2370
|
+
"""Set the top-level ``branch`` field.
|
|
2371
|
+
|
|
2372
|
+
Records the branch resolved by Branch Setup / Branch Reconciliation. The verb
|
|
2373
|
+
only writes the field; the interactive prompts and the visible one-line
|
|
2374
|
+
reconciliation note stay unchanged skill prose.
|
|
2375
|
+
|
|
2376
|
+
Branch Setup fires before the Entry Stamp, so this verb can legitimately be
|
|
2377
|
+
the FIRST thing to touch a feature's state file — `_load_state_for_write`'s
|
|
2378
|
+
field seeding is what keeps that first write schema-valid.
|
|
2379
|
+
|
|
2380
|
+
Args:
|
|
2381
|
+
feature: Feature name.
|
|
2382
|
+
branch: The branch name to record.
|
|
2383
|
+
specs_dir: Specs directory.
|
|
2384
|
+
epic: Owning epic name, or None.
|
|
2385
|
+
|
|
2386
|
+
Returns:
|
|
2387
|
+
The mutated state dict (for the --json echo).
|
|
2388
|
+
|
|
2389
|
+
Raises:
|
|
2390
|
+
UsageError: Unknown feature directory, unparseable state file, or a
|
|
2391
|
+
failed atomic write (→ exit 2).
|
|
2392
|
+
"""
|
|
2393
|
+
state_path, state = _load_state_for_write(specs_dir, feature, epic)
|
|
2394
|
+
state["branch"] = branch
|
|
2395
|
+
return _commit_state(state_path, state)
|
|
2396
|
+
|
|
2397
|
+
|
|
2398
|
+
def cmd_state_note(feature: str, note: str, specs_dir: Path, epic: str | None) -> dict:
|
|
2399
|
+
"""Set the top-level ``notes`` field to ``note``.
|
|
2400
|
+
|
|
2401
|
+
Overwrites any existing note (the field is a single free-text string, not an
|
|
2402
|
+
append log — matching the schema's ``notes: string``). The skill's "offer a
|
|
2403
|
+
note — don't force one" statement is unchanged; this verb runs only when the
|
|
2404
|
+
user volunteered text.
|
|
2405
|
+
|
|
2406
|
+
Args:
|
|
2407
|
+
feature: Feature name.
|
|
2408
|
+
note: The note text.
|
|
2409
|
+
specs_dir: Specs directory.
|
|
2410
|
+
epic: Owning epic name, or None.
|
|
2411
|
+
|
|
2412
|
+
Returns:
|
|
2413
|
+
The mutated state dict (for the --json echo).
|
|
2414
|
+
|
|
2415
|
+
Raises:
|
|
2416
|
+
UsageError: Unknown feature directory, unparseable state file, or a
|
|
2417
|
+
failed atomic write (→ exit 2).
|
|
2418
|
+
"""
|
|
2419
|
+
state_path, state = _load_state_for_write(specs_dir, feature, epic)
|
|
2420
|
+
state["notes"] = note
|
|
2421
|
+
return _commit_state(state_path, state)
|
|
2422
|
+
|
|
2423
|
+
|
|
2424
|
+
def cmd_state_decision(
|
|
2425
|
+
feature: str,
|
|
2426
|
+
question: str,
|
|
2427
|
+
raised_by: str,
|
|
2428
|
+
rationale: str | None,
|
|
2429
|
+
target_stage: str | None,
|
|
2430
|
+
specs_dir: Path,
|
|
2431
|
+
epic: str | None,
|
|
2432
|
+
) -> dict:
|
|
2433
|
+
"""Append an open deferred-decision item to ``deferredDecisions[]``.
|
|
2434
|
+
|
|
2435
|
+
Emits exactly the schema keys — the array item sets
|
|
2436
|
+
``additionalProperties: false``, so a convenience field is a hard validation
|
|
2437
|
+
failure: required ``question``/``raisedBy``/``raisedAt``/``status``, plus
|
|
2438
|
+
``rationale``/``targetStage`` only when provided. ``status`` is always
|
|
2439
|
+
``"open"``; the recorder never resolves a decision (the target stage flips it
|
|
2440
|
+
to ``"addressed"``).
|
|
2441
|
+
|
|
2442
|
+
Args:
|
|
2443
|
+
feature: Feature name.
|
|
2444
|
+
question: The deferred decision, phrased for the target stage.
|
|
2445
|
+
raised_by: The deferring stage id.
|
|
2446
|
+
rationale: Optional reason for deferring.
|
|
2447
|
+
target_stage: Optional resolving stage id.
|
|
2448
|
+
specs_dir: Specs directory.
|
|
2449
|
+
epic: Owning epic name, or None.
|
|
2450
|
+
|
|
2451
|
+
Returns:
|
|
2452
|
+
The mutated state dict (for the --json echo).
|
|
2453
|
+
|
|
2454
|
+
Raises:
|
|
2455
|
+
UsageError: Unknown feature directory, unparseable state file, or a
|
|
2456
|
+
failed atomic write (→ exit 2).
|
|
2457
|
+
"""
|
|
2458
|
+
state_path, state = _load_state_for_write(specs_dir, feature, epic)
|
|
2459
|
+
item: dict = {
|
|
2460
|
+
"question": question,
|
|
2461
|
+
"raisedBy": raised_by,
|
|
2462
|
+
"raisedAt": _now_iso(),
|
|
2463
|
+
"status": "open",
|
|
2464
|
+
}
|
|
2465
|
+
if rationale is not None:
|
|
2466
|
+
item["rationale"] = rationale
|
|
2467
|
+
if target_stage is not None:
|
|
2468
|
+
item["targetStage"] = target_stage
|
|
2469
|
+
state.setdefault("deferredDecisions", []).append(item)
|
|
2470
|
+
return _commit_state(state_path, state)
|
|
2471
|
+
|
|
2472
|
+
|
|
2473
|
+
def _parse_bool(raw: str, flag: str) -> bool:
|
|
2474
|
+
"""Parse an explicit boolean CLI value; fail closed on anything else.
|
|
2475
|
+
|
|
2476
|
+
Args:
|
|
2477
|
+
raw: The raw flag value (e.g. from ``--blocks-current``).
|
|
2478
|
+
flag: The flag name, for the error message.
|
|
2479
|
+
|
|
2480
|
+
Returns:
|
|
2481
|
+
``True`` for ``"true"``, ``False`` for ``"false"`` (case-insensitive,
|
|
2482
|
+
surrounding whitespace ignored).
|
|
2483
|
+
|
|
2484
|
+
Raises:
|
|
2485
|
+
UsageError: For any other value (→ exit 2), so a typo like ``"yes"`` is
|
|
2486
|
+
rejected rather than silently misrouting the stage exit.
|
|
2487
|
+
"""
|
|
2488
|
+
normalized = raw.strip().lower()
|
|
2489
|
+
if normalized == "true":
|
|
2490
|
+
return True
|
|
2491
|
+
if normalized == "false":
|
|
2492
|
+
return False
|
|
2493
|
+
raise UsageError(f"{flag} expects true|false, got: {raw!r}")
|
|
2494
|
+
|
|
2495
|
+
|
|
2496
|
+
def cmd_state_ecr(
|
|
2497
|
+
feature: str,
|
|
2498
|
+
kind: str,
|
|
2499
|
+
target: str,
|
|
2500
|
+
rationale: str,
|
|
2501
|
+
raised_by: str,
|
|
2502
|
+
blocks_current: bool,
|
|
2503
|
+
specs_dir: Path,
|
|
2504
|
+
epic: str | None,
|
|
2505
|
+
) -> dict:
|
|
2506
|
+
"""Append an open epic-change-request item to ``epicChangeRequests[]``.
|
|
2507
|
+
|
|
2508
|
+
Emits exactly the schema keys — the array item sets
|
|
2509
|
+
``additionalProperties: false``, so a convenience field is a hard validation
|
|
2510
|
+
failure. All six payload fields are required, and ``status`` is always
|
|
2511
|
+
``"open"`` (only forge-0-epic edit mode flips it). ``blocksCurrent`` drives
|
|
2512
|
+
stage-exit routing, so it is a strictly-parsed boolean.
|
|
2513
|
+
|
|
2514
|
+
Args:
|
|
2515
|
+
feature: Feature name.
|
|
2516
|
+
kind: One of add-feature|redep|move-boundary|split.
|
|
2517
|
+
target: The sibling feature to add, or the affected feature/boundary.
|
|
2518
|
+
rationale: Why the epic must change.
|
|
2519
|
+
raised_by: forge-1-prd or forge-2-tech.
|
|
2520
|
+
blocks_current: True → pause-now; False → finish-then-edit.
|
|
2521
|
+
specs_dir: Specs directory.
|
|
2522
|
+
epic: Owning epic name, or None.
|
|
2523
|
+
|
|
2524
|
+
Returns:
|
|
2525
|
+
The mutated state dict (for the --json echo).
|
|
2526
|
+
|
|
2527
|
+
Raises:
|
|
2528
|
+
UsageError: Unknown feature directory, unparseable state file, or a
|
|
2529
|
+
failed atomic write (→ exit 2).
|
|
2530
|
+
"""
|
|
2531
|
+
state_path, state = _load_state_for_write(specs_dir, feature, epic)
|
|
2532
|
+
item = {
|
|
2533
|
+
"kind": kind,
|
|
2534
|
+
"target": target,
|
|
2535
|
+
"rationale": rationale,
|
|
2536
|
+
"blocksCurrent": blocks_current,
|
|
2537
|
+
"raisedBy": raised_by,
|
|
2538
|
+
"raisedAt": _now_iso(),
|
|
2539
|
+
"status": "open",
|
|
2540
|
+
}
|
|
2541
|
+
state.setdefault("epicChangeRequests", []).append(item)
|
|
2542
|
+
return _commit_state(state_path, state)
|
|
2543
|
+
|
|
2544
|
+
|
|
2545
|
+
def _print_state_enter(state: dict) -> None:
|
|
2546
|
+
"""Print the one-line human summary for `state-enter`."""
|
|
2547
|
+
print(f"entered {state['currentStage']} (in-progress) for {state['feature']}")
|
|
2548
|
+
|
|
2549
|
+
|
|
2550
|
+
def _print_state_artifact(state: dict, stage: str, paths: list[str]) -> None:
|
|
2551
|
+
"""Print the one-line human summary for `state-artifact`."""
|
|
2552
|
+
total = len(state.get("stages", {}).get(stage, {}).get("artifacts", []))
|
|
2553
|
+
print(f"tracked {stage} artifact(s): {', '.join(paths)} ({total} total)")
|
|
2554
|
+
|
|
2555
|
+
|
|
2556
|
+
def _print_state_complete(
|
|
2557
|
+
state: dict, stage: str, commit_hash: str | None, resumable: bool
|
|
2558
|
+
) -> None:
|
|
2559
|
+
"""Print the one-line human summary for `state-complete` (one per branch)."""
|
|
2560
|
+
if commit_hash is not None:
|
|
2561
|
+
print(f"recorded {stage} commitHash: {commit_hash}")
|
|
2562
|
+
return
|
|
2563
|
+
if resumable:
|
|
2564
|
+
print(f"left {stage} in-progress (resumable — no completion recorded)")
|
|
2565
|
+
return
|
|
2566
|
+
entry = state.get("stages", {}).get(stage, {})
|
|
2567
|
+
label = (
|
|
2568
|
+
"completed"
|
|
2569
|
+
if entry.get("status") == _DONE_STATUS
|
|
2570
|
+
else f"partially completed ({entry.get('status')})"
|
|
2571
|
+
)
|
|
2572
|
+
recorded = entry.get("commitHash")
|
|
2573
|
+
cascaded = state.get("_cascadedStale") or []
|
|
2574
|
+
suffix = f"; marked stale: {', '.join(cascaded)}" if cascaded else ""
|
|
2575
|
+
print(
|
|
2576
|
+
f"{label} {stage} v{entry.get('version')} "
|
|
2577
|
+
f"(commitHash: {'null' if recorded is None else recorded}){suffix}"
|
|
2578
|
+
)
|
|
2579
|
+
|
|
2580
|
+
|
|
2581
|
+
def _print_state_branch(state: dict) -> None:
|
|
2582
|
+
"""Print the one-line human summary for `state-branch`."""
|
|
2583
|
+
print(f"recorded branch for {state['feature']}: {state['branch']}")
|
|
2584
|
+
|
|
2585
|
+
|
|
2586
|
+
def _print_state_note(state: dict) -> None:
|
|
2587
|
+
"""Print the one-line human summary for `state-note`."""
|
|
2588
|
+
print(f"note set for {state['feature']} ({len(state['notes'])} chars)")
|
|
2589
|
+
|
|
2590
|
+
|
|
2591
|
+
def _print_state_decision(state: dict) -> None:
|
|
2592
|
+
"""Print the one-line human summary for `state-decision` (the item appended)."""
|
|
2593
|
+
item = state["deferredDecisions"][-1]
|
|
2594
|
+
target = item.get("targetStage")
|
|
2595
|
+
routing = f"{item['raisedBy']} → {target}" if target else f"{item['raisedBy']}, no target stage"
|
|
2596
|
+
print(f"deferred decision recorded (raisedBy {routing})")
|
|
2597
|
+
|
|
2598
|
+
|
|
2599
|
+
def _print_state_ecr(state: dict) -> None:
|
|
2600
|
+
"""Print the one-line human summary for `state-ecr` (the item appended)."""
|
|
2601
|
+
item = state["epicChangeRequests"][-1]
|
|
2602
|
+
blocks = "true" if item["blocksCurrent"] else "false"
|
|
2603
|
+
print(
|
|
2604
|
+
f"epic change request recorded ({item['kind']} → {item['target']}, "
|
|
2605
|
+
f"blocksCurrent={blocks})"
|
|
2606
|
+
)
|
|
2607
|
+
|
|
2608
|
+
|
|
1664
2609
|
# --------------------------------------------------------------------------- #
|
|
1665
2610
|
# CLI dispatch
|
|
1666
2611
|
# --------------------------------------------------------------------------- #
|
|
1667
2612
|
|
|
1668
2613
|
|
|
2614
|
+
def _emit(payload: dict, json_output: bool, printer: Callable[[dict], None]) -> None:
|
|
2615
|
+
"""Emit a state-verb result: the full JSON echo on --json, else the printer.
|
|
2616
|
+
|
|
2617
|
+
Args:
|
|
2618
|
+
payload: The verb's resulting state dict.
|
|
2619
|
+
json_output: The ``--json`` flag.
|
|
2620
|
+
printer: The verb's one-line human-readable printer.
|
|
2621
|
+
"""
|
|
2622
|
+
if json_output:
|
|
2623
|
+
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
2624
|
+
else:
|
|
2625
|
+
printer(payload)
|
|
2626
|
+
|
|
2627
|
+
|
|
1669
2628
|
def _print_rank_table(rows: list[FeatureRow], counts: dict[str, int]) -> None:
|
|
1670
2629
|
"""Print a human-readable recency-ranked feature list."""
|
|
1671
2630
|
print(
|
|
@@ -1759,10 +2718,130 @@ def main() -> int:
|
|
|
1759
2718
|
p_exit.add_argument("--epic", default=None, help="Epic name for a nested member")
|
|
1760
2719
|
p_exit.add_argument("--next-feature", default=None, dest="next_feature",
|
|
1761
2720
|
help="First actionable feature (epic handoff next-command arg)")
|
|
1762
|
-
p_exit.add_argument("--host", default="claude", choices=("claude", "generic"),
|
|
2721
|
+
p_exit.add_argument("--host", default="claude", choices=("claude", "generic", "pi"),
|
|
1763
2722
|
help="Host wording for the NEXT-STEPS block")
|
|
1764
2723
|
p_exit.add_argument("--json", action="store_true", dest="json_output")
|
|
1765
2724
|
|
|
2725
|
+
p_eff = sub.add_parser(
|
|
2726
|
+
"effective-config",
|
|
2727
|
+
help="Resolve the loopRunner config from schema defaults + user overrides",
|
|
2728
|
+
)
|
|
2729
|
+
p_eff.add_argument("--config", default="./forge.config.json", help="forge.config.json path")
|
|
2730
|
+
p_eff.add_argument(
|
|
2731
|
+
"--schema", default=None,
|
|
2732
|
+
help="forge-config-schema.json path (default: bundled references/ copy)",
|
|
2733
|
+
)
|
|
2734
|
+
p_eff.add_argument("--json", action="store_true", dest="json_output")
|
|
2735
|
+
|
|
2736
|
+
p_enter = sub.add_parser(
|
|
2737
|
+
"state-enter", help="Stamp a stage as in-progress (Entry Stamp)"
|
|
2738
|
+
)
|
|
2739
|
+
p_enter.add_argument("--feature", required=True, help="Feature name")
|
|
2740
|
+
p_enter.add_argument("--stage", required=True, choices=STATE_VERB_STAGES,
|
|
2741
|
+
help="The stage being entered")
|
|
2742
|
+
p_enter.add_argument("--specs-dir", default="./specs", help="Specs directory")
|
|
2743
|
+
p_enter.add_argument("--epic", default=None, help="Epic name for a nested member")
|
|
2744
|
+
p_enter.add_argument("--json", action="store_true", dest="json_output")
|
|
2745
|
+
|
|
2746
|
+
p_art = sub.add_parser(
|
|
2747
|
+
"state-artifact", help="Append artifact paths to a stage (de-duplicating)"
|
|
2748
|
+
)
|
|
2749
|
+
p_art.add_argument("--feature", required=True, help="Feature name")
|
|
2750
|
+
p_art.add_argument("--stage", required=True, choices=STATE_VERB_STAGES,
|
|
2751
|
+
help="The stage producing the artifact")
|
|
2752
|
+
p_art.add_argument("--path", required=True, action="append", dest="paths",
|
|
2753
|
+
metavar="PATH",
|
|
2754
|
+
help="Artifact path relative to the feature dir (repeatable)")
|
|
2755
|
+
p_art.add_argument("--specs-dir", default="./specs", help="Specs directory")
|
|
2756
|
+
p_art.add_argument("--epic", default=None, help="Epic name for a nested member")
|
|
2757
|
+
p_art.add_argument("--json", action="store_true", dest="json_output")
|
|
2758
|
+
|
|
2759
|
+
p_comp = sub.add_parser(
|
|
2760
|
+
"state-complete", help="Mark a stage complete; bump version; cascade staleness"
|
|
2761
|
+
)
|
|
2762
|
+
p_comp.add_argument("--feature", required=True, help="Feature name")
|
|
2763
|
+
p_comp.add_argument("--stage", required=True, choices=STATE_VERB_STAGES,
|
|
2764
|
+
help="The stage being completed")
|
|
2765
|
+
p_comp.add_argument("--version", type=int, required=True,
|
|
2766
|
+
help="This stage's new version (integer)")
|
|
2767
|
+
p_comp.add_argument("--based-on", action="append", default=[], dest="based_on",
|
|
2768
|
+
metavar="STAGE=N",
|
|
2769
|
+
help="Upstream version this artifact was built on (repeatable)")
|
|
2770
|
+
p_comp.add_argument("--artifact", action="append", default=[], dest="artifacts",
|
|
2771
|
+
metavar="PATH",
|
|
2772
|
+
help="Artifact path produced by this stage (repeatable)")
|
|
2773
|
+
p_comp.add_argument("--commit-hash", default=None, dest="commit_hash",
|
|
2774
|
+
help="Commit 2 follow-up: record the artifact commit's hash")
|
|
2775
|
+
p_comp.add_argument("--status", default=None,
|
|
2776
|
+
choices=("complete", "in-progress"),
|
|
2777
|
+
help="Terminal status to record (default: complete). "
|
|
2778
|
+
"Use in-progress for a partial forge-5-loop run -- the "
|
|
2779
|
+
"stage still records completedAt/version/basedOnVersions/"
|
|
2780
|
+
"artifacts; only the status differs.")
|
|
2781
|
+
p_comp.add_argument("--resumable", action="store_true",
|
|
2782
|
+
help="Failed-Commit-1 revert (L245): record ONLY status="
|
|
2783
|
+
"in-progress, leaving completedAt/version/basedOnVersions/"
|
|
2784
|
+
"artifacts/commitHash untouched and firing no cascade. "
|
|
2785
|
+
"Implies --status in-progress.")
|
|
2786
|
+
p_comp.add_argument("--preserve-commit-hash", action="store_true",
|
|
2787
|
+
dest="preserve_commit_hash",
|
|
2788
|
+
help="Do not reset commitHash to null on completion "
|
|
2789
|
+
"(the Git Commit Protocol's 'Nothing to commit' branch)")
|
|
2790
|
+
p_comp.add_argument("--specs-dir", default="./specs", help="Specs directory")
|
|
2791
|
+
p_comp.add_argument("--epic", default=None, help="Epic name for a nested member")
|
|
2792
|
+
p_comp.add_argument("--json", action="store_true", dest="json_output")
|
|
2793
|
+
|
|
2794
|
+
p_br = sub.add_parser("state-branch", help="Set the top-level branch field")
|
|
2795
|
+
p_br.add_argument("--feature", required=True, help="Feature name")
|
|
2796
|
+
p_br.add_argument("--branch", required=True, help="Branch name to record")
|
|
2797
|
+
p_br.add_argument("--specs-dir", default="./specs", help="Specs directory")
|
|
2798
|
+
p_br.add_argument("--epic", default=None, help="Epic name for a nested member")
|
|
2799
|
+
p_br.add_argument("--json", action="store_true", dest="json_output")
|
|
2800
|
+
|
|
2801
|
+
p_note = sub.add_parser("state-note", help="Set the top-level notes field")
|
|
2802
|
+
p_note.add_argument("--feature", required=True, help="Feature name")
|
|
2803
|
+
p_note.add_argument("--note", required=True, help="Note text to persist")
|
|
2804
|
+
p_note.add_argument("--specs-dir", default="./specs", help="Specs directory")
|
|
2805
|
+
p_note.add_argument("--epic", default=None, help="Epic name for a nested member")
|
|
2806
|
+
p_note.add_argument("--json", action="store_true", dest="json_output")
|
|
2807
|
+
|
|
2808
|
+
p_dec = sub.add_parser(
|
|
2809
|
+
"state-decision", help="Append a deferred decision (status: open)"
|
|
2810
|
+
)
|
|
2811
|
+
p_dec.add_argument("--feature", required=True, help="Feature name")
|
|
2812
|
+
p_dec.add_argument("--question", required=True,
|
|
2813
|
+
help="The deferred decision, phrased for the target stage")
|
|
2814
|
+
p_dec.add_argument("--raised-by", required=True, dest="raised_by",
|
|
2815
|
+
choices=DECISION_RAISED_BY,
|
|
2816
|
+
help="The stage deferring the decision")
|
|
2817
|
+
p_dec.add_argument("--rationale", default=None, help="Why it is deferred (optional)")
|
|
2818
|
+
p_dec.add_argument("--target-stage", default=None, dest="target_stage",
|
|
2819
|
+
choices=DECISION_TARGET_STAGES,
|
|
2820
|
+
help="The stage that should resolve it (optional)")
|
|
2821
|
+
p_dec.add_argument("--specs-dir", default="./specs", help="Specs directory")
|
|
2822
|
+
p_dec.add_argument("--epic", default=None, help="Epic name for a nested member")
|
|
2823
|
+
p_dec.add_argument("--json", action="store_true", dest="json_output")
|
|
2824
|
+
|
|
2825
|
+
p_ecr = sub.add_parser(
|
|
2826
|
+
"state-ecr", help="Append an epic change request (status: open)"
|
|
2827
|
+
)
|
|
2828
|
+
p_ecr.add_argument("--feature", required=True, help="Feature name")
|
|
2829
|
+
p_ecr.add_argument("--kind", required=True, choices=ECR_KINDS,
|
|
2830
|
+
help="The decomposition change kind")
|
|
2831
|
+
p_ecr.add_argument("--target", required=True,
|
|
2832
|
+
help="The sibling feature to add, or the feature/boundary affected")
|
|
2833
|
+
p_ecr.add_argument("--rationale", required=True, help="Why the epic must change")
|
|
2834
|
+
p_ecr.add_argument("--raised-by", required=True, dest="raised_by",
|
|
2835
|
+
choices=ECR_RAISED_BY,
|
|
2836
|
+
help="The stage that detected the epic-level concern")
|
|
2837
|
+
p_ecr.add_argument("--blocks-current", required=True, dest="blocks_current",
|
|
2838
|
+
metavar="true|false",
|
|
2839
|
+
help="true → pause-now (reconcile before proceeding); "
|
|
2840
|
+
"false → finish-then-edit")
|
|
2841
|
+
p_ecr.add_argument("--specs-dir", default="./specs", help="Specs directory")
|
|
2842
|
+
p_ecr.add_argument("--epic", default=None, help="Epic name for a nested member")
|
|
2843
|
+
p_ecr.add_argument("--json", action="store_true", dest="json_output")
|
|
2844
|
+
|
|
1766
2845
|
args = parser.parse_args()
|
|
1767
2846
|
|
|
1768
2847
|
try:
|
|
@@ -1853,6 +2932,97 @@ def main() -> int:
|
|
|
1853
2932
|
_print_stage_exit(payload)
|
|
1854
2933
|
return 0
|
|
1855
2934
|
|
|
2935
|
+
if args.cmd == "effective-config":
|
|
2936
|
+
schema_path = Path(args.schema) if args.schema else _default_schema_path()
|
|
2937
|
+
resolved = resolve_loop_runner(Path(args.config), schema_path)
|
|
2938
|
+
if args.json_output:
|
|
2939
|
+
print(json.dumps(resolved, indent=2, ensure_ascii=False))
|
|
2940
|
+
else:
|
|
2941
|
+
_print_effective_config(resolved)
|
|
2942
|
+
return 0
|
|
2943
|
+
|
|
2944
|
+
if args.cmd == "state-enter":
|
|
2945
|
+
payload = cmd_state_enter(
|
|
2946
|
+
args.feature, args.stage, Path(args.specs_dir), args.epic
|
|
2947
|
+
)
|
|
2948
|
+
_emit(payload, args.json_output, _print_state_enter)
|
|
2949
|
+
return 0
|
|
2950
|
+
|
|
2951
|
+
if args.cmd == "state-artifact":
|
|
2952
|
+
payload = cmd_state_artifact(
|
|
2953
|
+
args.feature, args.stage, args.paths, Path(args.specs_dir), args.epic
|
|
2954
|
+
)
|
|
2955
|
+
_emit(
|
|
2956
|
+
payload,
|
|
2957
|
+
args.json_output,
|
|
2958
|
+
lambda state: _print_state_artifact(state, args.stage, args.paths),
|
|
2959
|
+
)
|
|
2960
|
+
return 0
|
|
2961
|
+
|
|
2962
|
+
if args.cmd == "state-complete":
|
|
2963
|
+
payload = cmd_state_complete(
|
|
2964
|
+
args.feature,
|
|
2965
|
+
args.stage,
|
|
2966
|
+
args.version,
|
|
2967
|
+
_parse_based_on(args.based_on),
|
|
2968
|
+
args.artifacts,
|
|
2969
|
+
args.commit_hash,
|
|
2970
|
+
Path(args.specs_dir),
|
|
2971
|
+
args.epic,
|
|
2972
|
+
status=args.status,
|
|
2973
|
+
preserve_commit_hash=args.preserve_commit_hash,
|
|
2974
|
+
resumable=args.resumable,
|
|
2975
|
+
)
|
|
2976
|
+
_emit(
|
|
2977
|
+
payload,
|
|
2978
|
+
args.json_output,
|
|
2979
|
+
lambda state: _print_state_complete(
|
|
2980
|
+
state, args.stage, args.commit_hash, args.resumable
|
|
2981
|
+
),
|
|
2982
|
+
)
|
|
2983
|
+
return 0
|
|
2984
|
+
|
|
2985
|
+
if args.cmd == "state-branch":
|
|
2986
|
+
payload = cmd_state_branch(
|
|
2987
|
+
args.feature, args.branch, Path(args.specs_dir), args.epic
|
|
2988
|
+
)
|
|
2989
|
+
_emit(payload, args.json_output, _print_state_branch)
|
|
2990
|
+
return 0
|
|
2991
|
+
|
|
2992
|
+
if args.cmd == "state-note":
|
|
2993
|
+
payload = cmd_state_note(
|
|
2994
|
+
args.feature, args.note, Path(args.specs_dir), args.epic
|
|
2995
|
+
)
|
|
2996
|
+
_emit(payload, args.json_output, _print_state_note)
|
|
2997
|
+
return 0
|
|
2998
|
+
|
|
2999
|
+
if args.cmd == "state-decision":
|
|
3000
|
+
payload = cmd_state_decision(
|
|
3001
|
+
args.feature,
|
|
3002
|
+
args.question,
|
|
3003
|
+
args.raised_by,
|
|
3004
|
+
args.rationale,
|
|
3005
|
+
args.target_stage,
|
|
3006
|
+
Path(args.specs_dir),
|
|
3007
|
+
args.epic,
|
|
3008
|
+
)
|
|
3009
|
+
_emit(payload, args.json_output, _print_state_decision)
|
|
3010
|
+
return 0
|
|
3011
|
+
|
|
3012
|
+
if args.cmd == "state-ecr":
|
|
3013
|
+
payload = cmd_state_ecr(
|
|
3014
|
+
args.feature,
|
|
3015
|
+
args.kind,
|
|
3016
|
+
args.target,
|
|
3017
|
+
args.rationale,
|
|
3018
|
+
args.raised_by,
|
|
3019
|
+
_parse_bool(args.blocks_current, "--blocks-current"),
|
|
3020
|
+
Path(args.specs_dir),
|
|
3021
|
+
args.epic,
|
|
3022
|
+
)
|
|
3023
|
+
_emit(payload, args.json_output, _print_state_ecr)
|
|
3024
|
+
return 0
|
|
3025
|
+
|
|
1856
3026
|
raise UsageError(f"unknown command: {args.cmd}")
|
|
1857
3027
|
except UsageError as exc:
|
|
1858
3028
|
print(f"Error: {exc}", file=sys.stderr)
|