@opengsd/gsd-path 1.0.0 → 1.1.0
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/AGENTS.md +28 -11
- package/DOCS.md +6 -5
- package/FULL.md +9 -13
- package/QUICK.md +3 -3
- package/README.md +151 -14
- package/RUNTIME.md +7 -1
- package/WORKFLOW.md +30 -14
- package/package.json +15 -1
- package/platforms/shared-agents/dispatch.md +6 -0
- package/scripts/archive_milestone.py +10 -118
- package/scripts/bootstrap_repository.py +25 -3
- package/scripts/build_recovery.py +287 -0
- package/scripts/check_docs_audit.py +5 -3
- package/scripts/check_handoffs.py +1 -1
- package/scripts/detect_project.py +11 -2
- package/scripts/dispatch_driver.py +4 -2
- package/scripts/git_guard.py +2 -1
- package/scripts/install.mjs +2 -0
- package/scripts/install.py +2 -0
- package/scripts/integration.py +28 -22
- package/scripts/isolation.py +9 -1
- package/scripts/lean_verification.py +1 -1
- package/scripts/pipeline_diagnose.py +12 -3
- package/scripts/pipeline_state.py +114 -186
- package/scripts/pipeline_undo.py +11 -7
- package/scripts/promote_lookahead.py +30 -162
- package/scripts/roadmap.py +321 -0
- package/scripts/skill-resources.json +79 -1
- package/scripts/state_checkpoint.py +109 -72
- package/scripts/state_promote.py +42 -44
- package/scripts/task_context.py +73 -0
- package/scripts/token_budget.py +40 -1
- package/scripts/workflow_run.py +116 -7
- package/skills/gsd-path/BUILD.md +47 -575
- package/skills/gsd-path/DECIDE.md +2 -6
- package/skills/gsd-path/DEFINE.md +10 -14
- package/skills/gsd-path/INSPECT.md +30 -8
- package/skills/gsd-path/PLAN.md +25 -17
- package/skills/gsd-path/RESEARCH.md +3 -6
- package/skills/gsd-path/ROADMAP.md +22 -58
- package/skills/gsd-path/SHIP.md +5 -4
- package/skills/gsd-path/SKILL.md +24 -14
- package/skills/gsd-path/references/build-abandon.md +33 -0
- package/skills/gsd-path/references/build-native.md +494 -0
- package/skills/gsd-path/references/build-recovery.md +78 -0
- package/skills/gsd-path/references/coder.md +8 -3
- package/skills/gsd-path/references/dispatch.md +6 -0
- package/skills/gsd-path/references/docs-auditor.md +14 -0
- package/skills/gsd-path/references/reviewer-final.md +21 -0
- package/skills/gsd-path/references/reviewer-panel.md +17 -0
- package/skills/gsd-path/references/reviewer-skeptic.md +20 -0
- package/skills/gsd-path/references/reviewer-wave.md +89 -0
- package/skills/gsd-path/references/reviewer.md +12 -150
- package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/build_state.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/check_task_briefs.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/discussion_records.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/integration.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/loop_run.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/review_findings.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/task_context.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/workflow_run.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/archive_milestone.py +10 -118
- package/skills/gsd-path/scripts/bootstrap_repository.py +25 -3
- package/skills/gsd-path/scripts/build_recovery.py +287 -0
- package/skills/gsd-path/scripts/check_docs_audit.py +5 -3
- package/skills/gsd-path/scripts/check_handoffs.py +1 -1
- package/skills/gsd-path/scripts/detect_project.py +11 -2
- package/skills/gsd-path/scripts/dispatch_driver.py +4 -2
- package/skills/gsd-path/scripts/integration.py +28 -22
- package/skills/gsd-path/scripts/isolation.py +9 -1
- package/skills/gsd-path/scripts/lean_verification.py +1 -1
- package/skills/gsd-path/scripts/pipeline_state.py +114 -186
- package/skills/gsd-path/scripts/promote_lookahead.py +30 -162
- package/skills/gsd-path/scripts/roadmap.py +321 -0
- package/skills/gsd-path/scripts/state_checkpoint.py +109 -72
- package/skills/gsd-path/scripts/state_promote.py +42 -44
- package/skills/gsd-path/scripts/task_context.py +73 -0
- package/skills/gsd-path/scripts/token_budget.py +40 -1
- package/skills/gsd-path/scripts/workflow_run.py +116 -7
- package/skills/gsd-path/templates/wave-review.md +1 -1
- package/skills/gsd-path-build/SKILL.md +47 -575
- package/skills/gsd-path-build/references/build-abandon.md +33 -0
- package/skills/gsd-path-build/references/build-native.md +494 -0
- package/skills/gsd-path-build/references/build-recovery.md +78 -0
- package/skills/gsd-path-build/references/coder.md +8 -3
- package/skills/gsd-path-build/references/dispatch.md +6 -0
- package/skills/gsd-path-build/references/reviewer-final.md +21 -0
- package/skills/gsd-path-build/references/reviewer-panel.md +17 -0
- package/skills/gsd-path-build/references/reviewer-skeptic.md +20 -0
- package/skills/gsd-path-build/references/reviewer-wave.md +89 -0
- package/skills/gsd-path-build/references/reviewer.md +12 -150
- package/skills/gsd-path-build/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/build_state.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/check_task_briefs.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/loop_run.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/review_findings.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/task_context.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/workflow_run.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/archive_milestone.py +10 -118
- package/skills/gsd-path-build/scripts/build_recovery.py +287 -0
- package/skills/gsd-path-build/scripts/check_handoffs.py +1 -1
- package/skills/gsd-path-build/scripts/detect_project.py +1526 -0
- package/skills/gsd-path-build/scripts/dispatch_driver.py +4 -2
- package/skills/gsd-path-build/scripts/integration.py +28 -22
- package/skills/gsd-path-build/scripts/isolation.py +9 -1
- package/skills/gsd-path-build/scripts/pipeline_state.py +114 -186
- package/skills/gsd-path-build/scripts/promote_lookahead.py +283 -0
- package/skills/gsd-path-build/scripts/roadmap.py +321 -0
- package/skills/gsd-path-build/scripts/state_checkpoint.py +109 -72
- package/skills/gsd-path-build/scripts/state_promote.py +42 -44
- package/skills/gsd-path-build/scripts/task_context.py +73 -0
- package/skills/gsd-path-build/scripts/token_budget.py +40 -1
- package/skills/gsd-path-build/scripts/workflow_run.py +116 -7
- package/skills/gsd-path-build/templates/wave-review.md +1 -1
- package/skills/gsd-path-decide/SKILL.md +2 -6
- package/skills/gsd-path-decide/references/dispatch.md +6 -0
- package/skills/gsd-path-decide/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-decide/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
- package/skills/gsd-path-decide/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
- package/skills/gsd-path-decide/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
- package/skills/gsd-path-decide/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
- package/skills/gsd-path-decide/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
- package/skills/gsd-path-decide/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
- package/skills/gsd-path-decide/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
- package/skills/gsd-path-decide/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
- package/skills/gsd-path-decide/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
- package/skills/gsd-path-decide/scripts/archive_milestone.py +2157 -0
- package/skills/gsd-path-decide/scripts/build_recovery.py +287 -0
- package/skills/gsd-path-decide/scripts/check_handoffs.py +1 -1
- package/skills/gsd-path-decide/scripts/detect_project.py +1526 -0
- package/skills/gsd-path-decide/scripts/discussion_validate.py +1321 -0
- package/skills/gsd-path-decide/scripts/integration.py +1439 -0
- package/skills/gsd-path-decide/scripts/isolation.py +9 -1
- package/skills/gsd-path-decide/scripts/pipeline_state.py +114 -186
- package/skills/gsd-path-decide/scripts/promote_lookahead.py +283 -0
- package/skills/gsd-path-decide/scripts/review_panel.py +553 -0
- package/skills/gsd-path-decide/scripts/roadmap.py +321 -0
- package/skills/gsd-path-decide/scripts/state_checkpoint.py +109 -72
- package/skills/gsd-path-decide/scripts/state_promote.py +42 -44
- package/skills/gsd-path-define/SKILL.md +10 -14
- package/skills/gsd-path-define/references/build-recovery.md +78 -0
- package/skills/gsd-path-define/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-define/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
- package/skills/gsd-path-define/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
- package/skills/gsd-path-define/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
- package/skills/gsd-path-define/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
- package/skills/gsd-path-define/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
- package/skills/gsd-path-define/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
- package/skills/gsd-path-define/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
- package/skills/gsd-path-define/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
- package/skills/gsd-path-define/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
- package/skills/gsd-path-define/scripts/archive_milestone.py +2157 -0
- package/skills/gsd-path-define/scripts/build_recovery.py +287 -0
- package/skills/gsd-path-define/scripts/check_handoffs.py +1 -1
- package/skills/gsd-path-define/scripts/detect_project.py +11 -2
- package/skills/gsd-path-define/scripts/discussion_validate.py +1321 -0
- package/skills/gsd-path-define/scripts/integration.py +1439 -0
- package/skills/gsd-path-define/scripts/isolation.py +9 -1
- package/skills/gsd-path-define/scripts/pipeline_state.py +114 -186
- package/skills/gsd-path-define/scripts/promote_lookahead.py +283 -0
- package/skills/gsd-path-define/scripts/review_panel.py +553 -0
- package/skills/gsd-path-define/scripts/roadmap.py +321 -0
- package/skills/gsd-path-define/scripts/state_checkpoint.py +109 -72
- package/skills/gsd-path-define/scripts/state_promote.py +42 -44
- package/skills/gsd-path-docs-audit/references/dispatch.md +6 -0
- package/skills/gsd-path-docs-audit/references/docs-auditor.md +14 -0
- package/skills/gsd-path-docs-audit/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-docs-audit/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
- package/skills/gsd-path-docs-audit/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
- package/skills/gsd-path-docs-audit/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
- package/skills/gsd-path-docs-audit/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
- package/skills/gsd-path-docs-audit/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
- package/skills/gsd-path-docs-audit/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
- package/skills/gsd-path-docs-audit/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
- package/skills/gsd-path-docs-audit/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
- package/skills/gsd-path-docs-audit/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
- package/skills/gsd-path-docs-audit/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
- package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +2157 -0
- package/skills/gsd-path-docs-audit/scripts/build_recovery.py +287 -0
- package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +5 -3
- package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +1 -1
- package/skills/gsd-path-docs-audit/scripts/detect_project.py +1526 -0
- package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +1321 -0
- package/skills/gsd-path-docs-audit/scripts/integration.py +1439 -0
- package/skills/gsd-path-docs-audit/scripts/isolation.py +9 -1
- package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +114 -186
- package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +283 -0
- package/skills/gsd-path-docs-audit/scripts/review_panel.py +553 -0
- package/skills/gsd-path-docs-audit/scripts/roadmap.py +321 -0
- package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +109 -72
- package/skills/gsd-path-docs-audit/scripts/state_promote.py +42 -44
- package/skills/gsd-path-forensics/scripts/__pycache__/pipeline_undo.cpython-312.pyc +0 -0
- package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +12 -3
- package/skills/gsd-path-forensics/scripts/pipeline_undo.py +11 -7
- package/skills/gsd-path-inspect/SKILL.md +30 -8
- package/skills/gsd-path-inspect/references/dispatch.md +6 -0
- package/skills/gsd-path-inspect/references/docs-auditor.md +14 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/archive_milestone.py +2157 -0
- package/skills/gsd-path-inspect/scripts/build_recovery.py +287 -0
- package/skills/gsd-path-inspect/scripts/check_docs_audit.py +5 -3
- package/skills/gsd-path-inspect/scripts/check_handoffs.py +1 -1
- package/skills/gsd-path-inspect/scripts/detect_project.py +11 -2
- package/skills/gsd-path-inspect/scripts/discussion_validate.py +1321 -0
- package/skills/gsd-path-inspect/scripts/integration.py +1439 -0
- package/skills/gsd-path-inspect/scripts/isolation.py +9 -1
- package/skills/gsd-path-inspect/scripts/pipeline_state.py +114 -186
- package/skills/gsd-path-inspect/scripts/promote_lookahead.py +283 -0
- package/skills/gsd-path-inspect/scripts/review_panel.py +553 -0
- package/skills/gsd-path-inspect/scripts/roadmap.py +321 -0
- package/skills/gsd-path-inspect/scripts/state_checkpoint.py +109 -72
- package/skills/gsd-path-inspect/scripts/state_promote.py +42 -44
- package/skills/gsd-path-inspect/scripts/workflow_run.py +226 -0
- package/skills/gsd-path-plan/SKILL.md +25 -17
- package/skills/gsd-path-plan/references/build-recovery.md +78 -0
- package/skills/gsd-path-plan/references/dispatch.md +6 -0
- package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/archive_milestone.py +2157 -0
- package/skills/gsd-path-plan/scripts/build_recovery.py +287 -0
- package/skills/gsd-path-plan/scripts/check_handoffs.py +1 -1
- package/skills/gsd-path-plan/scripts/detect_project.py +1526 -0
- package/skills/gsd-path-plan/scripts/discussion_validate.py +1321 -0
- package/skills/gsd-path-plan/scripts/integration.py +1439 -0
- package/skills/gsd-path-plan/scripts/isolation.py +9 -1
- package/skills/gsd-path-plan/scripts/pipeline_state.py +114 -186
- package/skills/gsd-path-plan/scripts/promote_lookahead.py +283 -0
- package/skills/gsd-path-plan/scripts/roadmap.py +321 -0
- package/skills/gsd-path-plan/scripts/state_checkpoint.py +109 -72
- package/skills/gsd-path-plan/scripts/state_promote.py +42 -44
- package/skills/gsd-path-plan/scripts/workflow_run.py +116 -7
- package/skills/gsd-path-research/SKILL.md +3 -6
- package/skills/gsd-path-research/references/dispatch.md +6 -0
- package/skills/gsd-path-research/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-research/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
- package/skills/gsd-path-research/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
- package/skills/gsd-path-research/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
- package/skills/gsd-path-research/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
- package/skills/gsd-path-research/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
- package/skills/gsd-path-research/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
- package/skills/gsd-path-research/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
- package/skills/gsd-path-research/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
- package/skills/gsd-path-research/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
- package/skills/gsd-path-research/scripts/archive_milestone.py +2157 -0
- package/skills/gsd-path-research/scripts/build_recovery.py +287 -0
- package/skills/gsd-path-research/scripts/check_handoffs.py +1 -1
- package/skills/gsd-path-research/scripts/detect_project.py +1526 -0
- package/skills/gsd-path-research/scripts/discussion_validate.py +1321 -0
- package/skills/gsd-path-research/scripts/integration.py +1439 -0
- package/skills/gsd-path-research/scripts/isolation.py +9 -1
- package/skills/gsd-path-research/scripts/pipeline_state.py +114 -186
- package/skills/gsd-path-research/scripts/promote_lookahead.py +283 -0
- package/skills/gsd-path-research/scripts/review_panel.py +553 -0
- package/skills/gsd-path-research/scripts/roadmap.py +321 -0
- package/skills/gsd-path-research/scripts/state_checkpoint.py +109 -72
- package/skills/gsd-path-research/scripts/state_promote.py +42 -44
- package/skills/gsd-path-roadmap/SKILL.md +22 -58
- package/skills/gsd-path-roadmap/references/dispatch.md +6 -0
- package/skills/gsd-path-roadmap/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-roadmap/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
- package/skills/gsd-path-roadmap/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
- package/skills/gsd-path-roadmap/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
- package/skills/gsd-path-roadmap/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
- package/skills/gsd-path-roadmap/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
- package/skills/gsd-path-roadmap/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
- package/skills/gsd-path-roadmap/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
- package/skills/gsd-path-roadmap/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
- package/skills/gsd-path-roadmap/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
- package/skills/gsd-path-roadmap/scripts/archive_milestone.py +10 -118
- package/skills/gsd-path-roadmap/scripts/build_recovery.py +287 -0
- package/skills/gsd-path-roadmap/scripts/check_handoffs.py +1 -1
- package/skills/gsd-path-roadmap/scripts/detect_project.py +11 -2
- package/skills/gsd-path-roadmap/scripts/integration.py +28 -22
- package/skills/gsd-path-roadmap/scripts/isolation.py +9 -1
- package/skills/gsd-path-roadmap/scripts/pipeline_state.py +114 -186
- package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +30 -162
- package/skills/gsd-path-roadmap/scripts/roadmap.py +321 -0
- package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +109 -72
- package/skills/gsd-path-roadmap/scripts/state_promote.py +42 -44
- package/skills/gsd-path-ship/SKILL.md +5 -4
- package/skills/gsd-path-ship/references/dispatch.md +6 -0
- package/skills/gsd-path-ship/references/reviewer-final.md +21 -0
- package/skills/gsd-path-ship/references/reviewer-panel.md +17 -0
- package/skills/gsd-path-ship/references/reviewer-skeptic.md +20 -0
- package/skills/gsd-path-ship/references/reviewer-wave.md +89 -0
- package/skills/gsd-path-ship/references/reviewer.md +12 -150
- package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/build_state.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/check_task_briefs.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/archive_milestone.py +10 -118
- package/skills/gsd-path-ship/scripts/build_recovery.py +287 -0
- package/skills/gsd-path-ship/scripts/check_handoffs.py +1 -1
- package/skills/gsd-path-ship/scripts/detect_project.py +1526 -0
- package/skills/gsd-path-ship/scripts/integration.py +28 -22
- package/skills/gsd-path-ship/scripts/isolation.py +9 -1
- package/skills/gsd-path-ship/scripts/lean_verification.py +1 -1
- package/skills/gsd-path-ship/scripts/pipeline_state.py +114 -186
- package/skills/gsd-path-ship/scripts/promote_lookahead.py +283 -0
- package/skills/gsd-path-ship/scripts/roadmap.py +321 -0
- package/skills/gsd-path-ship/scripts/state_checkpoint.py +109 -72
- package/skills/gsd-path-ship/scripts/state_promote.py +42 -44
- package/skills/gsd-path-ship/scripts/workflow_run.py +116 -7
- package/skills/gsd-path-ship/templates/wave-review.md +1 -1
- package/skills/gsd-path-undo/scripts/pipeline_undo.py +11 -7
- package/skills/path/BUILD.md +47 -575
- package/skills/path/DECIDE.md +2 -6
- package/skills/path/DEFINE.md +10 -14
- package/skills/path/INSPECT.md +30 -8
- package/skills/path/PLAN.md +25 -17
- package/skills/path/RESEARCH.md +3 -6
- package/skills/path/ROADMAP.md +22 -58
- package/skills/path/SHIP.md +5 -4
- package/skills/path/SKILL.md +24 -14
- package/skills/path/references/build-abandon.md +33 -0
- package/skills/path/references/build-native.md +494 -0
- package/skills/path/references/build-recovery.md +78 -0
- package/skills/path/references/coder.md +8 -3
- package/skills/path/references/dispatch.md +6 -0
- package/skills/path/references/docs-auditor.md +14 -0
- package/skills/path/references/reviewer-final.md +21 -0
- package/skills/path/references/reviewer-panel.md +17 -0
- package/skills/path/references/reviewer-skeptic.md +20 -0
- package/skills/path/references/reviewer-wave.md +89 -0
- package/skills/path/references/reviewer.md +12 -150
- package/skills/path/scripts/archive_milestone.py +10 -118
- package/skills/path/scripts/bootstrap_repository.py +25 -3
- package/skills/path/scripts/build_recovery.py +287 -0
- package/skills/path/scripts/check_docs_audit.py +5 -3
- package/skills/path/scripts/check_handoffs.py +1 -1
- package/skills/path/scripts/detect_project.py +11 -2
- package/skills/path/scripts/dispatch_driver.py +4 -2
- package/skills/path/scripts/integration.py +28 -22
- package/skills/path/scripts/isolation.py +9 -1
- package/skills/path/scripts/lean_verification.py +1 -1
- package/skills/path/scripts/pipeline_state.py +114 -186
- package/skills/path/scripts/promote_lookahead.py +30 -162
- package/skills/path/scripts/roadmap.py +321 -0
- package/skills/path/scripts/state_checkpoint.py +109 -72
- package/skills/path/scripts/state_promote.py +42 -44
- package/skills/path/scripts/task_context.py +73 -0
- package/skills/path/scripts/token_budget.py +40 -1
- package/skills/path/scripts/workflow_run.py +116 -7
- package/skills/path/templates/wave-review.md +1 -1
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# gsd-path project runtime
|
|
3
|
+
"""Select and compare GSD Path lookahead tracks."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import stat
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional, Sequence
|
|
17
|
+
|
|
18
|
+
if __package__:
|
|
19
|
+
from . import _common, archive_milestone, detect_project
|
|
20
|
+
from .roadmap import (
|
|
21
|
+
RoadmapError as LookaheadError,
|
|
22
|
+
NoEligibleMilestone as NoEligibleLookahead,
|
|
23
|
+
roadmap_blocks,
|
|
24
|
+
milestone_block,
|
|
25
|
+
dependency_ids,
|
|
26
|
+
next_eligible_pending,
|
|
27
|
+
selection_payload,
|
|
28
|
+
select_lookahead,
|
|
29
|
+
roadmap_contract,
|
|
30
|
+
compare_roadmap_entry,
|
|
31
|
+
)
|
|
32
|
+
else:
|
|
33
|
+
from roadmap import (
|
|
34
|
+
RoadmapError as LookaheadError,
|
|
35
|
+
NoEligibleMilestone as NoEligibleLookahead,
|
|
36
|
+
roadmap_blocks,
|
|
37
|
+
milestone_block,
|
|
38
|
+
dependency_ids,
|
|
39
|
+
next_eligible_pending,
|
|
40
|
+
selection_payload,
|
|
41
|
+
select_lookahead,
|
|
42
|
+
roadmap_contract,
|
|
43
|
+
compare_roadmap_entry,
|
|
44
|
+
)
|
|
45
|
+
import _common
|
|
46
|
+
import archive_milestone
|
|
47
|
+
import detect_project
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
PHASES = {"inspect", "define", "research", "decide", "plan"}
|
|
51
|
+
STATUSES = {"active", "blocked", "done"}
|
|
52
|
+
SHA_RE = re.compile(r"^[0-9a-f]{40}$")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def read_text(path: Path, description: str) -> str:
|
|
56
|
+
try:
|
|
57
|
+
return path.read_text(encoding="utf-8")
|
|
58
|
+
except (OSError, UnicodeDecodeError) as error:
|
|
59
|
+
raise LookaheadError(f"cannot read {description} {path}: {error}") from error
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def path_mode(path: Path) -> Optional[int]:
|
|
63
|
+
try:
|
|
64
|
+
return path.lstat().st_mode
|
|
65
|
+
except (FileNotFoundError, NotADirectoryError):
|
|
66
|
+
return None
|
|
67
|
+
except OSError as error:
|
|
68
|
+
raise LookaheadError(f"cannot inspect path {path}: {error}") from error
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def require_directory(path: Path, description: str) -> None:
|
|
72
|
+
mode = path_mode(path)
|
|
73
|
+
if mode is None or not stat.S_ISDIR(mode):
|
|
74
|
+
raise LookaheadError(f"{description} must be a real directory: {path}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def require_file(path: Path, description: str) -> None:
|
|
78
|
+
mode = path_mode(path)
|
|
79
|
+
if mode is None or not stat.S_ISREG(mode):
|
|
80
|
+
raise LookaheadError(f"{description} must be a real file: {path}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
atomic_write = _common.atomic_write
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def state_fields(content: str) -> dict[str, str]:
|
|
87
|
+
try:
|
|
88
|
+
fields = detect_project.state_frontmatter(content)
|
|
89
|
+
except detect_project.DetectError as error:
|
|
90
|
+
raise LookaheadError(str(error)) from error
|
|
91
|
+
if fields is None:
|
|
92
|
+
raise LookaheadError("STATE.md is missing YAML frontmatter")
|
|
93
|
+
return fields
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def select_track_branch(content: str, track_state: str) -> dict:
|
|
97
|
+
fields = state_fields(track_state)
|
|
98
|
+
if fields.get("pipeline", "") != archive_milestone.PIPELINE_MARKER:
|
|
99
|
+
raise LookaheadError("lookahead STATE.md is not owned by gsd-path/v2")
|
|
100
|
+
phase = fields.get("phase", "")
|
|
101
|
+
status = fields.get("status", "")
|
|
102
|
+
milestone = fields.get("milestone", "")
|
|
103
|
+
if phase not in PHASES or status not in STATUSES:
|
|
104
|
+
raise LookaheadError(f"invalid lookahead state: {phase}/{status}")
|
|
105
|
+
if archive_milestone.is_unset(milestone):
|
|
106
|
+
raise LookaheadError("lookahead STATE.md does not name a milestone")
|
|
107
|
+
if not archive_milestone.is_unset(fields.get("branch")):
|
|
108
|
+
raise LookaheadError("lookahead STATE.md must not bind a branch")
|
|
109
|
+
if not archive_milestone.is_unset(fields.get("archive")):
|
|
110
|
+
raise LookaheadError("lookahead STATE.md must not own an archive")
|
|
111
|
+
selected = milestone_block(content, milestone)
|
|
112
|
+
if selected["fields"].get("Status", (-1, ""))[1] != "pending":
|
|
113
|
+
raise LookaheadError(f"lookahead milestone {milestone} is not pending")
|
|
114
|
+
eligible = next_eligible_pending(content)
|
|
115
|
+
if selected["id"] != eligible["id"]:
|
|
116
|
+
raise LookaheadError(
|
|
117
|
+
f"lookahead milestone {milestone} is not the next eligible milestone "
|
|
118
|
+
f"{eligible['id']}"
|
|
119
|
+
)
|
|
120
|
+
return selection_payload(selected)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def snapshot_roadmap(source: Path, destination: Path) -> dict:
|
|
124
|
+
require_file(source, "ROADMAP.md")
|
|
125
|
+
require_directory(destination.parent, "roadmap snapshot parent")
|
|
126
|
+
existing_mode = path_mode(destination)
|
|
127
|
+
if existing_mode is not None:
|
|
128
|
+
require_file(destination, "roadmap baseline snapshot")
|
|
129
|
+
content = read_text(destination, "roadmap baseline snapshot")
|
|
130
|
+
return {
|
|
131
|
+
"status": "existing",
|
|
132
|
+
"sha256": hashlib.sha256(content.encode("utf-8")).hexdigest(),
|
|
133
|
+
}
|
|
134
|
+
content = read_text(source, "ROADMAP.md")
|
|
135
|
+
temporary = destination.parent / f".{destination.name}.gsd-path-tmp"
|
|
136
|
+
try:
|
|
137
|
+
atomic_write(temporary, content)
|
|
138
|
+
try:
|
|
139
|
+
os.link(temporary, destination)
|
|
140
|
+
status = "created"
|
|
141
|
+
except FileExistsError:
|
|
142
|
+
require_file(destination, "roadmap baseline snapshot")
|
|
143
|
+
content = read_text(destination, "roadmap baseline snapshot")
|
|
144
|
+
status = "existing"
|
|
145
|
+
finally:
|
|
146
|
+
if path_mode(temporary) is not None:
|
|
147
|
+
temporary.unlink()
|
|
148
|
+
return {
|
|
149
|
+
"status": status,
|
|
150
|
+
"sha256": hashlib.sha256(content.encode("utf-8")).hexdigest(),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def git_commit_file(root: Path, commit: str, relative: str) -> str:
|
|
155
|
+
require_directory(root, "repository")
|
|
156
|
+
if not SHA_RE.fullmatch(commit):
|
|
157
|
+
raise LookaheadError("base must be a full lowercase commit SHA")
|
|
158
|
+
entry = subprocess.run(
|
|
159
|
+
["git", "-C", str(root), "ls-tree", "-z", commit, "--", relative],
|
|
160
|
+
capture_output=True,
|
|
161
|
+
check=False,
|
|
162
|
+
)
|
|
163
|
+
metadata, separator, path = entry.stdout.rstrip(b"\0").partition(b"\t")
|
|
164
|
+
fields = metadata.split()
|
|
165
|
+
if (
|
|
166
|
+
entry.returncode != 0
|
|
167
|
+
or not separator
|
|
168
|
+
or path.decode("utf-8", "replace") != relative
|
|
169
|
+
or len(fields) != 3
|
|
170
|
+
or fields[0] not in {b"100644", b"100755"}
|
|
171
|
+
or fields[1] != b"blob"
|
|
172
|
+
):
|
|
173
|
+
raise LookaheadError(f"{relative} must be a regular file at base {commit}")
|
|
174
|
+
result = subprocess.run(
|
|
175
|
+
["git", "-C", str(root), "show", f"{commit}:{relative}"],
|
|
176
|
+
capture_output=True,
|
|
177
|
+
check=False,
|
|
178
|
+
)
|
|
179
|
+
if result.returncode != 0:
|
|
180
|
+
detail = result.stderr.decode("utf-8", "replace").strip()
|
|
181
|
+
suffix = f": {detail}" if detail else ""
|
|
182
|
+
raise LookaheadError(f"cannot read {relative} at base {commit}{suffix}")
|
|
183
|
+
try:
|
|
184
|
+
return result.stdout.decode("utf-8")
|
|
185
|
+
except UnicodeDecodeError as error:
|
|
186
|
+
raise LookaheadError(
|
|
187
|
+
f"cannot decode {relative} at base {commit}: {error}"
|
|
188
|
+
) from error
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def select_fetched_base(
|
|
192
|
+
repo: Path,
|
|
193
|
+
base: str,
|
|
194
|
+
remote_default: str,
|
|
195
|
+
use_lookahead: bool,
|
|
196
|
+
) -> dict:
|
|
197
|
+
root = repo.resolve()
|
|
198
|
+
if not re.fullmatch(r"origin/[A-Za-z0-9][A-Za-z0-9._/-]*", remote_default):
|
|
199
|
+
raise LookaheadError("remote default must be an origin branch")
|
|
200
|
+
if ".." in remote_default or "//" in remote_default:
|
|
201
|
+
raise LookaheadError("remote default is invalid")
|
|
202
|
+
resolved = subprocess.run(
|
|
203
|
+
["git", "-C", str(root), "rev-parse", "--verify", f"{remote_default}^{{commit}}"],
|
|
204
|
+
capture_output=True,
|
|
205
|
+
text=True,
|
|
206
|
+
check=False,
|
|
207
|
+
)
|
|
208
|
+
if resolved.returncode != 0 or resolved.stdout.strip() != base:
|
|
209
|
+
raise LookaheadError("base does not match the fetched remote default")
|
|
210
|
+
roadmap = git_commit_file(root, base, ".project/ROADMAP.md")
|
|
211
|
+
if use_lookahead:
|
|
212
|
+
track_state = git_commit_file(root, base, ".project/next/STATE.md")
|
|
213
|
+
result = select_track_branch(roadmap, track_state)
|
|
214
|
+
else:
|
|
215
|
+
result = select_lookahead(roadmap)
|
|
216
|
+
return {**result, "base": base}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def parser() -> argparse.ArgumentParser:
|
|
221
|
+
result = argparse.ArgumentParser(description=__doc__)
|
|
222
|
+
subparsers = result.add_subparsers(dest="command", required=True)
|
|
223
|
+
compare_parser = subparsers.add_parser("compare-entry")
|
|
224
|
+
compare_parser.add_argument("--before", required=True, type=Path)
|
|
225
|
+
compare_parser.add_argument("--after", required=True, type=Path)
|
|
226
|
+
compare_parser.add_argument("--milestone", required=True)
|
|
227
|
+
compare_parser.add_argument("--active-milestone")
|
|
228
|
+
select_parser = subparsers.add_parser("select-next")
|
|
229
|
+
select_parser.add_argument("--roadmap", required=True, type=Path)
|
|
230
|
+
select_parser.add_argument("--active-milestone")
|
|
231
|
+
branch_parser = subparsers.add_parser("select-branch")
|
|
232
|
+
branch_parser.add_argument("--roadmap", required=True, type=Path)
|
|
233
|
+
branch_parser.add_argument("--state", required=True, type=Path)
|
|
234
|
+
base_parser = subparsers.add_parser("select-base")
|
|
235
|
+
base_parser.add_argument("--repo", required=True, type=Path)
|
|
236
|
+
base_parser.add_argument("--base", required=True)
|
|
237
|
+
base_parser.add_argument("--remote-default", required=True)
|
|
238
|
+
base_parser.add_argument("--lookahead", action="store_true")
|
|
239
|
+
snapshot_parser = subparsers.add_parser("snapshot-roadmap")
|
|
240
|
+
snapshot_parser.add_argument("--roadmap", required=True, type=Path)
|
|
241
|
+
snapshot_parser.add_argument("--snapshot", required=True, type=Path)
|
|
242
|
+
return result
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
246
|
+
arguments = parser().parse_args(argv)
|
|
247
|
+
try:
|
|
248
|
+
if arguments.command == "compare-entry":
|
|
249
|
+
result = compare_roadmap_entry(
|
|
250
|
+
read_text(arguments.before, "previous ROADMAP.md"),
|
|
251
|
+
read_text(arguments.after, "proposed ROADMAP.md"),
|
|
252
|
+
arguments.milestone,
|
|
253
|
+
arguments.active_milestone,
|
|
254
|
+
)
|
|
255
|
+
elif arguments.command == "select-next":
|
|
256
|
+
result = select_lookahead(
|
|
257
|
+
read_text(arguments.roadmap, "ROADMAP.md"),
|
|
258
|
+
arguments.active_milestone,
|
|
259
|
+
)
|
|
260
|
+
elif arguments.command == "select-branch":
|
|
261
|
+
require_file(arguments.state, "lookahead STATE.md")
|
|
262
|
+
result = select_track_branch(
|
|
263
|
+
read_text(arguments.roadmap, "ROADMAP.md"),
|
|
264
|
+
read_text(arguments.state, "lookahead STATE.md"),
|
|
265
|
+
)
|
|
266
|
+
elif arguments.command == "select-base":
|
|
267
|
+
result = select_fetched_base(
|
|
268
|
+
arguments.repo,
|
|
269
|
+
arguments.base,
|
|
270
|
+
arguments.remote_default,
|
|
271
|
+
arguments.lookahead,
|
|
272
|
+
)
|
|
273
|
+
elif arguments.command == "snapshot-roadmap":
|
|
274
|
+
result = snapshot_roadmap(arguments.roadmap, arguments.snapshot)
|
|
275
|
+
except (LookaheadError, OSError) as error:
|
|
276
|
+
print(f"error: {error}", file=sys.stderr)
|
|
277
|
+
return 1
|
|
278
|
+
print(json.dumps(result, sort_keys=True))
|
|
279
|
+
return 0
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
if __name__ == "__main__":
|
|
283
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# gsd-path project runtime
|
|
3
|
+
"""Interpret Roadmap text for selection, approval, promotion and milestone close."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RoadmapError(RuntimeError):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NoEligibleMilestone(RoadmapError):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
ROADMAP_HEADING_RE = re.compile(r"^### (M\d{3,}) — (.+)$")
|
|
19
|
+
STRICT_HEADING_RE = re.compile(r"^### (M\d{3,}) — ([a-z0-9][a-z0-9-]*)\s*$")
|
|
20
|
+
MUTABLE_ROADMAP_FIELDS_RE = re.compile(r"^(Status|Archive|Integrated):")
|
|
21
|
+
|
|
22
|
+
def normalized_slug(value: str) -> str:
|
|
23
|
+
slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
|
|
24
|
+
return slug or "project"
|
|
25
|
+
|
|
26
|
+
def roadmap_blocks(content: str) -> list[dict]:
|
|
27
|
+
lines = content.splitlines(keepends=True)
|
|
28
|
+
headings = []
|
|
29
|
+
for index, line in enumerate(lines):
|
|
30
|
+
match = ROADMAP_HEADING_RE.fullmatch(line.rstrip("\r\n"))
|
|
31
|
+
if match:
|
|
32
|
+
headings.append((index, match.group(1), match.group(2).strip()))
|
|
33
|
+
blocks = []
|
|
34
|
+
for position, (start, milestone_id, title) in enumerate(headings):
|
|
35
|
+
end = headings[position + 1][0] if position + 1 < len(headings) else len(lines)
|
|
36
|
+
fields = {}
|
|
37
|
+
for index in range(start + 1, end):
|
|
38
|
+
match = re.match(
|
|
39
|
+
r"^(Status|Archive|Integrated|Depends on):\s*([^#]*?)\s*(?:#.*)?$",
|
|
40
|
+
lines[index].rstrip("\r\n"),
|
|
41
|
+
)
|
|
42
|
+
if match:
|
|
43
|
+
if match.group(1) in fields:
|
|
44
|
+
raise RoadmapError(
|
|
45
|
+
f"roadmap milestone {milestone_id} has duplicate "
|
|
46
|
+
f"{match.group(1)} field"
|
|
47
|
+
)
|
|
48
|
+
fields[match.group(1)] = (index, match.group(2).strip())
|
|
49
|
+
blocks.append(
|
|
50
|
+
{
|
|
51
|
+
"id": milestone_id,
|
|
52
|
+
"title": title,
|
|
53
|
+
"start": start,
|
|
54
|
+
"end": end,
|
|
55
|
+
"fields": fields,
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
return blocks
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def milestone_block(content: str, milestone: str) -> dict:
|
|
62
|
+
normalized = normalized_slug(milestone)
|
|
63
|
+
matches = [
|
|
64
|
+
block
|
|
65
|
+
for block in roadmap_blocks(content)
|
|
66
|
+
if normalized_slug(block["title"]) == normalized
|
|
67
|
+
]
|
|
68
|
+
if len(matches) != 1:
|
|
69
|
+
raise RoadmapError(f"ROADMAP.md must contain exactly one entry for {milestone}")
|
|
70
|
+
return matches[0]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def dependency_ids(block: dict) -> list[str]:
|
|
74
|
+
dependencies = block["fields"].get("Depends on")
|
|
75
|
+
if dependencies is None:
|
|
76
|
+
raise RoadmapError(f"roadmap milestone {block['id']} lacks dependencies")
|
|
77
|
+
dependency_list = re.fullmatch(
|
|
78
|
+
r"\[\s*(M\d{3,}(?:\s*,\s*M\d{3,})*)?\s*\]",
|
|
79
|
+
dependencies[1],
|
|
80
|
+
)
|
|
81
|
+
if dependency_list is None:
|
|
82
|
+
raise RoadmapError(f"roadmap milestone {block['id']} has invalid dependencies")
|
|
83
|
+
return re.findall(r"M\d{3,}", dependency_list.group(1) or "")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def next_eligible_pending(content: str, active_milestone: Optional[str] = None) -> dict:
|
|
87
|
+
blocks = roadmap_blocks(content)
|
|
88
|
+
blocks_by_id = {block["id"]: block for block in blocks}
|
|
89
|
+
if len(blocks_by_id) != len(blocks):
|
|
90
|
+
raise RoadmapError("ROADMAP.md milestone ids must be unique")
|
|
91
|
+
active_id = None
|
|
92
|
+
if active_milestone is not None:
|
|
93
|
+
active = milestone_block(content, active_milestone)
|
|
94
|
+
if active["fields"].get("Status", (-1, ""))[1] != "active":
|
|
95
|
+
raise RoadmapError(f"roadmap milestone {active_milestone} is not active")
|
|
96
|
+
active_id = active["id"]
|
|
97
|
+
for block in blocks:
|
|
98
|
+
if block["fields"].get("Status", (-1, ""))[1] != "pending":
|
|
99
|
+
continue
|
|
100
|
+
dependencies = dependency_ids(block)
|
|
101
|
+
if all(
|
|
102
|
+
dependency in blocks_by_id
|
|
103
|
+
and (
|
|
104
|
+
blocks_by_id[dependency]["fields"].get("Status", (-1, ""))[1]
|
|
105
|
+
== "shipped"
|
|
106
|
+
or dependency == active_id
|
|
107
|
+
)
|
|
108
|
+
for dependency in dependencies
|
|
109
|
+
):
|
|
110
|
+
return block
|
|
111
|
+
raise NoEligibleMilestone("ROADMAP.md has no dependency-ready pending milestone")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def selection_payload(selected: dict) -> dict:
|
|
115
|
+
milestone = normalized_slug(selected["title"])
|
|
116
|
+
return {
|
|
117
|
+
"status": "selected",
|
|
118
|
+
"id": selected["id"],
|
|
119
|
+
"milestone": milestone,
|
|
120
|
+
"branch": f"gsd-path/{selected['id']}",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def select_lookahead(content: str, active_milestone: Optional[str] = None) -> dict:
|
|
125
|
+
try:
|
|
126
|
+
selected = next_eligible_pending(content, active_milestone)
|
|
127
|
+
except NoEligibleMilestone:
|
|
128
|
+
blocks = roadmap_blocks(content)
|
|
129
|
+
if blocks and all(
|
|
130
|
+
block["fields"].get("Status", (-1, ""))[1]
|
|
131
|
+
in {"shipped", "abandoned"}
|
|
132
|
+
for block in blocks
|
|
133
|
+
):
|
|
134
|
+
return {"status": "complete"}
|
|
135
|
+
return {"status": "none"}
|
|
136
|
+
return selection_payload(selected)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def roadmap_contract(content: str, milestone: str) -> tuple[str, ...]:
|
|
140
|
+
block = milestone_block(content, milestone)
|
|
141
|
+
lines = content.splitlines()
|
|
142
|
+
return tuple(
|
|
143
|
+
line.rstrip()
|
|
144
|
+
for line in lines[block["start"] : block["end"]]
|
|
145
|
+
if not MUTABLE_ROADMAP_FIELDS_RE.match(line)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def compare_roadmap_entry(
|
|
150
|
+
before: str,
|
|
151
|
+
after: str,
|
|
152
|
+
milestone: str,
|
|
153
|
+
active_milestone: Optional[str] = None,
|
|
154
|
+
) -> dict:
|
|
155
|
+
before_contract = roadmap_contract(before, milestone)
|
|
156
|
+
try:
|
|
157
|
+
after_contract = roadmap_contract(after, milestone)
|
|
158
|
+
selected = milestone_block(after, milestone)
|
|
159
|
+
eligible = next_eligible_pending(after, active_milestone)
|
|
160
|
+
except RoadmapError:
|
|
161
|
+
after_contract = ()
|
|
162
|
+
selected = None
|
|
163
|
+
eligible = None
|
|
164
|
+
status = (
|
|
165
|
+
"unchanged"
|
|
166
|
+
if before_contract == after_contract
|
|
167
|
+
and selected is not None
|
|
168
|
+
and eligible is not None
|
|
169
|
+
and selected["id"] == eligible["id"]
|
|
170
|
+
else "changed"
|
|
171
|
+
)
|
|
172
|
+
return {"status": status, "milestone": milestone}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def strict_sections(text: str) -> dict[str, tuple[str, int, int]]:
|
|
176
|
+
lines = text.splitlines(keepends=True)
|
|
177
|
+
headings: list[tuple[str, str, int]] = []
|
|
178
|
+
for index, line in enumerate(lines):
|
|
179
|
+
match = STRICT_HEADING_RE.fullmatch(line.rstrip("\r\n"))
|
|
180
|
+
if match:
|
|
181
|
+
if int(match.group(1).removeprefix("M")) < 1:
|
|
182
|
+
raise RoadmapError(f"ROADMAP.md has invalid milestone id: {match.group(1)}")
|
|
183
|
+
headings.append((match.group(1), match.group(2), index))
|
|
184
|
+
sections: dict[str, tuple[str, int, int]] = {}
|
|
185
|
+
for position, (milestone_id, slug, start) in enumerate(headings):
|
|
186
|
+
end = headings[position + 1][2] if position + 1 < len(headings) else len(lines)
|
|
187
|
+
if slug in sections:
|
|
188
|
+
raise RoadmapError(f"ROADMAP.md repeats milestone slug: {slug}")
|
|
189
|
+
sections[slug] = (milestone_id, start, end)
|
|
190
|
+
return sections
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def replace_field(
|
|
194
|
+
lines: list[str],
|
|
195
|
+
start: int,
|
|
196
|
+
end: int,
|
|
197
|
+
field: str,
|
|
198
|
+
expected: set[str],
|
|
199
|
+
value: str,
|
|
200
|
+
) -> None:
|
|
201
|
+
found: Optional[int] = None
|
|
202
|
+
for index in range(start, end):
|
|
203
|
+
raw = lines[index].rstrip("\r\n")
|
|
204
|
+
ending = lines[index][len(raw) :]
|
|
205
|
+
match = re.match(rf"^({re.escape(field)}:\s*)(\S+)(.*)$", raw)
|
|
206
|
+
if match is None:
|
|
207
|
+
continue
|
|
208
|
+
if found is not None:
|
|
209
|
+
raise RoadmapError(f"ROADMAP.md repeats {field} in one milestone")
|
|
210
|
+
if match.group(2) not in expected:
|
|
211
|
+
raise RoadmapError(
|
|
212
|
+
f"ROADMAP.md {field} is {match.group(2)}, expected {sorted(expected)}"
|
|
213
|
+
)
|
|
214
|
+
lines[index] = f"{match.group(1)}{value}{match.group(3)}{ending}"
|
|
215
|
+
found = index
|
|
216
|
+
if found is None:
|
|
217
|
+
raise RoadmapError(f"ROADMAP.md milestone is missing {field}")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def milestone_status(lines: list[str], start: int, end: int) -> str:
|
|
221
|
+
values = []
|
|
222
|
+
for line in lines[start:end]:
|
|
223
|
+
match = re.fullmatch(r"Status:\s*(\S+)\s*", line.rstrip("\r\n"))
|
|
224
|
+
if match:
|
|
225
|
+
values.append(match.group(1))
|
|
226
|
+
if len(values) != 1:
|
|
227
|
+
raise RoadmapError("ROADMAP.md milestone must have one Status field")
|
|
228
|
+
return values[0]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def activate_milestone(text: str, milestone: str) -> str:
|
|
232
|
+
sections = strict_sections(text)
|
|
233
|
+
if milestone not in sections:
|
|
234
|
+
raise RoadmapError(f"ROADMAP.md is missing selected milestone: {milestone}")
|
|
235
|
+
lines = text.splitlines(keepends=True)
|
|
236
|
+
active = [
|
|
237
|
+
slug
|
|
238
|
+
for slug, (_, start, end) in sections.items()
|
|
239
|
+
if milestone_status(lines, start, end) == "active"
|
|
240
|
+
]
|
|
241
|
+
if active:
|
|
242
|
+
raise RoadmapError(
|
|
243
|
+
"ROADMAP.md already has an active milestone: " + ", ".join(active)
|
|
244
|
+
)
|
|
245
|
+
_, start, end = sections[milestone]
|
|
246
|
+
replace_field(lines, start, end, "Status", {"pending"}, "active")
|
|
247
|
+
return "".join(lines)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def reslice(before: str, candidate: str, phase: str, selected: str) -> str:
|
|
251
|
+
old_blocks = roadmap_blocks(before)
|
|
252
|
+
before_lines = before.splitlines(keepends=True)
|
|
253
|
+
candidate_lines = candidate.splitlines(keepends=True)
|
|
254
|
+
for block in old_blocks:
|
|
255
|
+
status = block["fields"].get("Status", (-1, ""))[1]
|
|
256
|
+
if status not in {"active", "shipped", "abandoned"}:
|
|
257
|
+
continue
|
|
258
|
+
slug = normalized_slug(block["title"])
|
|
259
|
+
new = milestone_block(candidate, slug)
|
|
260
|
+
if status == "shipped":
|
|
261
|
+
changed = roadmap_contract(before, slug) != roadmap_contract(candidate, slug)
|
|
262
|
+
else:
|
|
263
|
+
changed = before_lines[block["start"]:block["end"]] != candidate_lines[new["start"]:new["end"]]
|
|
264
|
+
if changed:
|
|
265
|
+
raise RoadmapError(f"{status} roadmap entry changed: {slug}")
|
|
266
|
+
if phase in {"inspect", "define"}:
|
|
267
|
+
active = [b for b in roadmap_blocks(candidate) if b["fields"].get("Status", (-1, ""))[1] == "active"]
|
|
268
|
+
if len(active) != 1 or selection_payload(active[0])["milestone"] != selected:
|
|
269
|
+
raise RoadmapError("roadmap re-slice must retain the active entry")
|
|
270
|
+
return candidate
|
|
271
|
+
if not any(b["fields"].get("Status", (-1, ""))[1] == "abandoned" for b in old_blocks):
|
|
272
|
+
raise RoadmapError("post-abandon re-slice requires an abandoned milestone")
|
|
273
|
+
selection = select_lookahead(candidate)
|
|
274
|
+
if selection.get("milestone") != selected:
|
|
275
|
+
raise RoadmapError("roadmap re-slice must use the selected dependency-ready milestone")
|
|
276
|
+
return activate_milestone(candidate, selected)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def has_questions(text: str, milestone: Optional[str] = None) -> bool:
|
|
280
|
+
"""Report open questions for the active entry, or for ``milestone`` when given."""
|
|
281
|
+
lines = text.splitlines()
|
|
282
|
+
active_section: Optional[list[str]] = None
|
|
283
|
+
for index, line in enumerate(lines):
|
|
284
|
+
heading = STRICT_HEADING_RE.fullmatch(line)
|
|
285
|
+
if heading is None:
|
|
286
|
+
continue
|
|
287
|
+
if int(heading.group(1).removeprefix("M")) < 1:
|
|
288
|
+
raise RoadmapError(
|
|
289
|
+
f"ROADMAP.md has invalid milestone id: {heading.group(1)}"
|
|
290
|
+
)
|
|
291
|
+
end = next(
|
|
292
|
+
(
|
|
293
|
+
cursor
|
|
294
|
+
for cursor in range(index + 1, len(lines))
|
|
295
|
+
if STRICT_HEADING_RE.fullmatch(lines[cursor]) is not None
|
|
296
|
+
),
|
|
297
|
+
len(lines),
|
|
298
|
+
)
|
|
299
|
+
section = lines[index:end]
|
|
300
|
+
if milestone is not None:
|
|
301
|
+
if heading.group(2) == milestone:
|
|
302
|
+
active_section = section
|
|
303
|
+
continue
|
|
304
|
+
if any(re.fullmatch(r"Status:\s*active(?:\s+#.*)?", item) for item in section):
|
|
305
|
+
if active_section is not None:
|
|
306
|
+
raise RoadmapError("ROADMAP.md has more than one active milestone")
|
|
307
|
+
active_section = section
|
|
308
|
+
if active_section is None:
|
|
309
|
+
if milestone is not None:
|
|
310
|
+
raise RoadmapError(f"ROADMAP.md is missing lookahead milestone: {milestone}")
|
|
311
|
+
raise RoadmapError("ROADMAP.md has no active milestone")
|
|
312
|
+
try:
|
|
313
|
+
start = active_section.index("Open questions") + 1
|
|
314
|
+
except ValueError as error:
|
|
315
|
+
raise RoadmapError("active roadmap milestone has no Open questions") from error
|
|
316
|
+
questions = [
|
|
317
|
+
line[2:].strip()
|
|
318
|
+
for line in active_section[start:]
|
|
319
|
+
if line.startswith("- ")
|
|
320
|
+
]
|
|
321
|
+
return any(question.lower() != "none" for question in questions)
|