@opengsd/gsd-path 1.0.0 → 1.2.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 +33 -13
- package/CHANGELOG.md +68 -0
- package/DOCS.md +75 -6
- package/FULL.md +12 -15
- package/HOOKS.md +18 -9
- package/QUICK.md +8 -6
- package/README.md +218 -14
- package/RUNTIME.md +16 -5
- package/UPDATE.md +30 -11
- package/WORKFLOW.md +35 -16
- package/package.json +23 -1
- package/platforms/claude/dispatch.md +4 -4
- package/platforms/copilot/dispatch.md +4 -4
- package/platforms/cursor/dispatch.md +5 -10
- package/platforms/grok/dispatch.md +4 -6
- package/platforms/kimi/dispatch.md +4 -4
- package/platforms/kiro/dispatch.md +4 -6
- package/platforms/opencode/dispatch.md +4 -4
- package/platforms/qwen/dispatch.md +4 -4
- package/platforms/shared-agents/dispatch.md +12 -19
- package/scripts/_common.py +5 -0
- package/scripts/archive_milestone.py +30 -124
- package/scripts/bootstrap_repository.py +33 -4
- package/scripts/build_recovery.py +292 -0
- package/scripts/build_state.py +2 -0
- package/scripts/check_docs_audit.py +7 -3
- package/scripts/check_handoffs.py +7 -1
- package/scripts/check_task_briefs.py +3 -0
- package/scripts/detect_project.py +22 -3
- package/scripts/discussion_records.py +3 -0
- package/scripts/discussion_validate.py +8 -3
- package/scripts/dispatch_driver.py +233 -22
- package/scripts/git_guard.py +14 -5
- package/scripts/guard_hook.py +11 -1
- package/scripts/install.mjs +54 -1448
- package/scripts/install.py +155 -188
- package/scripts/integration.py +54 -28
- package/scripts/isolation.py +24 -6
- package/scripts/jev_review.py +138 -0
- package/scripts/lean_verification.py +3 -1
- package/scripts/loop_run.py +2 -0
- package/scripts/model_policy.py +279 -0
- package/scripts/path_config.py +248 -0
- package/scripts/pipeline_diagnose.py +15 -3
- package/scripts/pipeline_git.py +3 -0
- package/scripts/pipeline_state.py +117 -186
- package/scripts/pipeline_undo.py +14 -7
- package/scripts/promote_lookahead.py +33 -162
- package/scripts/review_panel.py +11 -0
- package/scripts/roadmap.py +326 -0
- package/scripts/runtime_store.py +278 -0
- package/scripts/skill-resources.json +182 -4
- package/scripts/state_checkpoint.py +114 -72
- package/scripts/state_promote.py +47 -44
- package/scripts/status_runtime.py +92 -6
- package/scripts/task_context.py +73 -0
- package/scripts/token_budget.py +40 -1
- package/scripts/workflow_run.py +128 -8
- package/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path/BUILD.md +54 -575
- package/skills/gsd-path/DECIDE.md +4 -6
- package/skills/gsd-path/DEFINE.md +23 -21
- package/skills/gsd-path/DOCS-AUDIT.md +2 -0
- package/skills/gsd-path/INSPECT.md +32 -8
- package/skills/gsd-path/PLAN.md +29 -19
- package/skills/gsd-path/RESEARCH.md +5 -6
- package/skills/gsd-path/ROADMAP.md +24 -58
- package/skills/gsd-path/SHIP.md +29 -17
- package/skills/gsd-path/SKILL.md +46 -22
- package/skills/gsd-path/references/build-abandon.md +33 -0
- package/skills/gsd-path/references/build-native.md +502 -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/config.md +57 -0
- package/skills/gsd-path/references/dispatch.md +12 -19
- package/skills/gsd-path/references/docs-auditor.md +14 -0
- package/skills/gsd-path/references/jev-review.md +82 -0
- package/skills/gsd-path/references/model-policy.md +139 -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 -148
- package/skills/gsd-path/references/runtime-selection.md +25 -0
- package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/_common.py +5 -0
- package/skills/gsd-path/scripts/archive_milestone.py +30 -124
- package/skills/gsd-path/scripts/bootstrap_repository.py +33 -4
- package/skills/gsd-path/scripts/build_recovery.py +292 -0
- package/skills/gsd-path/scripts/build_state.py +2 -0
- package/skills/gsd-path/scripts/check_docs_audit.py +7 -3
- package/skills/gsd-path/scripts/check_handoffs.py +7 -1
- package/skills/gsd-path/scripts/check_task_briefs.py +3 -0
- package/skills/gsd-path/scripts/detect_project.py +22 -3
- package/skills/gsd-path/scripts/discussion_records.py +3 -0
- package/skills/gsd-path/scripts/discussion_validate.py +8 -3
- package/skills/gsd-path/scripts/dispatch_driver.py +233 -22
- package/skills/gsd-path/scripts/git_guard.py +768 -0
- package/skills/gsd-path/scripts/integration.py +54 -28
- package/skills/gsd-path/scripts/isolation.py +24 -6
- package/skills/gsd-path/scripts/jev_review.py +138 -0
- package/skills/gsd-path/scripts/lean_verification.py +3 -1
- package/skills/gsd-path/scripts/loop_run.py +2 -0
- package/skills/gsd-path/scripts/model_policy.py +279 -0
- package/skills/gsd-path/scripts/path_config.py +248 -0
- package/skills/gsd-path/scripts/pipeline_git.py +3 -0
- package/skills/gsd-path/scripts/pipeline_state.py +117 -186
- package/skills/gsd-path/scripts/promote_lookahead.py +33 -162
- package/skills/gsd-path/scripts/review_panel.py +11 -0
- package/skills/gsd-path/scripts/roadmap.py +326 -0
- package/skills/gsd-path/scripts/state_checkpoint.py +114 -72
- package/skills/gsd-path/scripts/state_promote.py +47 -44
- package/skills/gsd-path/scripts/status_runtime.py +208 -0
- 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 +128 -8
- package/skills/gsd-path/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path/templates/skeptic.md +1 -1
- package/skills/gsd-path/templates/task.md +2 -0
- package/skills/gsd-path/templates/wave-panel.md +1 -1
- package/skills/gsd-path/templates/wave-review.md +1 -1
- package/skills/gsd-path-build/SKILL.md +54 -575
- package/skills/gsd-path-build/references/build-abandon.md +33 -0
- package/skills/gsd-path-build/references/build-native.md +502 -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 +12 -19
- package/skills/gsd-path-build/references/jev-review.md +82 -0
- package/skills/gsd-path-build/references/model-policy.md +139 -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 -148
- package/skills/gsd-path-build/references/runtime-selection.md +25 -0
- package/skills/gsd-path-build/scripts/__pycache__/_common.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/_common.py +5 -0
- package/skills/gsd-path-build/scripts/archive_milestone.py +30 -124
- package/skills/gsd-path-build/scripts/build_recovery.py +292 -0
- package/skills/gsd-path-build/scripts/build_state.py +2 -0
- package/skills/gsd-path-build/scripts/check_handoffs.py +7 -1
- package/skills/gsd-path-build/scripts/check_task_briefs.py +3 -0
- package/skills/gsd-path-build/scripts/detect_project.py +1536 -0
- package/skills/gsd-path-build/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-build/scripts/discussion_validate.py +8 -3
- package/skills/gsd-path-build/scripts/dispatch_driver.py +233 -22
- package/skills/gsd-path-build/scripts/integration.py +54 -28
- package/skills/gsd-path-build/scripts/isolation.py +24 -6
- package/skills/gsd-path-build/scripts/jev_review.py +138 -0
- package/skills/gsd-path-build/scripts/loop_run.py +2 -0
- package/skills/gsd-path-build/scripts/model_policy.py +279 -0
- package/skills/gsd-path-build/scripts/path_config.py +248 -0
- package/skills/gsd-path-build/scripts/pipeline_git.py +3 -0
- package/skills/gsd-path-build/scripts/pipeline_state.py +117 -186
- package/skills/gsd-path-build/scripts/promote_lookahead.py +286 -0
- package/skills/gsd-path-build/scripts/review_panel.py +11 -0
- package/skills/gsd-path-build/scripts/roadmap.py +326 -0
- package/skills/gsd-path-build/scripts/state_checkpoint.py +114 -72
- package/skills/gsd-path-build/scripts/state_promote.py +47 -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 +128 -8
- package/skills/gsd-path-build/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-build/templates/skeptic.md +1 -1
- package/skills/gsd-path-build/templates/task.md +2 -0
- package/skills/gsd-path-build/templates/wave-panel.md +1 -1
- package/skills/gsd-path-build/templates/wave-review.md +1 -1
- package/skills/gsd-path-decide/SKILL.md +4 -6
- package/skills/gsd-path-decide/references/dispatch.md +12 -19
- package/skills/gsd-path-decide/references/model-policy.md +139 -0
- package/skills/gsd-path-decide/references/runtime-selection.md +25 -0
- package/skills/gsd-path-decide/scripts/_common.py +5 -0
- package/skills/gsd-path-decide/scripts/archive_milestone.py +2171 -0
- package/skills/gsd-path-decide/scripts/build_recovery.py +292 -0
- package/skills/gsd-path-decide/scripts/check_handoffs.py +7 -1
- package/skills/gsd-path-decide/scripts/check_task_briefs.py +3 -0
- package/skills/gsd-path-decide/scripts/detect_project.py +1536 -0
- package/skills/gsd-path-decide/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-decide/scripts/discussion_validate.py +1326 -0
- package/skills/gsd-path-decide/scripts/integration.py +1459 -0
- package/skills/gsd-path-decide/scripts/isolation.py +24 -6
- package/skills/gsd-path-decide/scripts/loop_run.py +839 -0
- package/skills/gsd-path-decide/scripts/model_policy.py +279 -0
- package/skills/gsd-path-decide/scripts/path_config.py +248 -0
- package/skills/gsd-path-decide/scripts/pipeline_git.py +3 -0
- package/skills/gsd-path-decide/scripts/pipeline_state.py +117 -186
- package/skills/gsd-path-decide/scripts/promote_lookahead.py +286 -0
- package/skills/gsd-path-decide/scripts/review_panel.py +564 -0
- package/skills/gsd-path-decide/scripts/roadmap.py +326 -0
- package/skills/gsd-path-decide/scripts/state_checkpoint.py +114 -72
- package/skills/gsd-path-decide/scripts/state_promote.py +47 -44
- package/skills/gsd-path-decide/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-define/SKILL.md +23 -21
- package/skills/gsd-path-define/references/build-recovery.md +78 -0
- package/skills/gsd-path-define/references/runtime-selection.md +25 -0
- package/skills/gsd-path-define/scripts/_common.py +5 -0
- package/skills/gsd-path-define/scripts/archive_milestone.py +2171 -0
- package/skills/gsd-path-define/scripts/build_recovery.py +292 -0
- package/skills/gsd-path-define/scripts/check_handoffs.py +7 -1
- package/skills/gsd-path-define/scripts/check_task_briefs.py +3 -0
- package/skills/gsd-path-define/scripts/detect_project.py +22 -3
- package/skills/gsd-path-define/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-define/scripts/discussion_validate.py +1326 -0
- package/skills/gsd-path-define/scripts/integration.py +1459 -0
- package/skills/gsd-path-define/scripts/isolation.py +24 -6
- package/skills/gsd-path-define/scripts/loop_run.py +839 -0
- package/skills/gsd-path-define/scripts/model_policy.py +279 -0
- package/skills/gsd-path-define/scripts/path_config.py +248 -0
- package/skills/gsd-path-define/scripts/pipeline_git.py +3 -0
- package/skills/gsd-path-define/scripts/pipeline_state.py +117 -186
- package/skills/gsd-path-define/scripts/promote_lookahead.py +286 -0
- package/skills/gsd-path-define/scripts/review_panel.py +564 -0
- package/skills/gsd-path-define/scripts/roadmap.py +326 -0
- package/skills/gsd-path-define/scripts/state_checkpoint.py +114 -72
- package/skills/gsd-path-define/scripts/state_promote.py +47 -44
- package/skills/gsd-path-define/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-discuss/SKILL.md +2 -0
- package/skills/gsd-path-discuss/references/runtime-selection.md +25 -0
- package/skills/gsd-path-discuss/scripts/_common.py +5 -0
- package/skills/gsd-path-discuss/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-discuss/scripts/loop_run.py +839 -0
- package/skills/gsd-path-discuss/scripts/model_policy.py +279 -0
- package/skills/gsd-path-discuss/scripts/path_config.py +248 -0
- package/skills/gsd-path-discuss/scripts/review_panel.py +564 -0
- package/skills/gsd-path-discuss/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-docs-audit/SKILL.md +2 -0
- package/skills/gsd-path-docs-audit/references/dispatch.md +12 -19
- package/skills/gsd-path-docs-audit/references/docs-auditor.md +14 -0
- package/skills/gsd-path-docs-audit/references/model-policy.md +139 -0
- package/skills/gsd-path-docs-audit/references/runtime-selection.md +25 -0
- package/skills/gsd-path-docs-audit/scripts/_common.py +5 -0
- package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +2171 -0
- package/skills/gsd-path-docs-audit/scripts/build_recovery.py +292 -0
- package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +7 -3
- package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +7 -1
- package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +3 -0
- package/skills/gsd-path-docs-audit/scripts/detect_project.py +1536 -0
- package/skills/gsd-path-docs-audit/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +1326 -0
- package/skills/gsd-path-docs-audit/scripts/integration.py +1459 -0
- package/skills/gsd-path-docs-audit/scripts/isolation.py +24 -6
- package/skills/gsd-path-docs-audit/scripts/loop_run.py +839 -0
- package/skills/gsd-path-docs-audit/scripts/model_policy.py +279 -0
- package/skills/gsd-path-docs-audit/scripts/path_config.py +248 -0
- package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +3 -0
- package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +117 -186
- package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +286 -0
- package/skills/gsd-path-docs-audit/scripts/review_panel.py +564 -0
- package/skills/gsd-path-docs-audit/scripts/roadmap.py +326 -0
- package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +114 -72
- package/skills/gsd-path-docs-audit/scripts/state_promote.py +47 -44
- package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-forensics/SKILL.md +9 -5
- package/skills/gsd-path-forensics/references/runtime-selection.md +25 -0
- package/skills/gsd-path-forensics/scripts/_common.py +5 -0
- package/skills/gsd-path-forensics/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-forensics/scripts/loop_run.py +839 -0
- package/skills/gsd-path-forensics/scripts/model_policy.py +279 -0
- package/skills/gsd-path-forensics/scripts/path_config.py +248 -0
- package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +15 -3
- package/skills/gsd-path-forensics/scripts/pipeline_undo.py +14 -7
- package/skills/gsd-path-forensics/scripts/review_panel.py +564 -0
- package/skills/gsd-path-forensics/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-inspect/SKILL.md +32 -8
- package/skills/gsd-path-inspect/references/dispatch.md +12 -19
- package/skills/gsd-path-inspect/references/docs-auditor.md +14 -0
- package/skills/gsd-path-inspect/references/model-policy.md +139 -0
- package/skills/gsd-path-inspect/references/runtime-selection.md +25 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/_common.py +5 -0
- package/skills/gsd-path-inspect/scripts/archive_milestone.py +2171 -0
- package/skills/gsd-path-inspect/scripts/build_recovery.py +292 -0
- package/skills/gsd-path-inspect/scripts/check_docs_audit.py +7 -3
- package/skills/gsd-path-inspect/scripts/check_handoffs.py +7 -1
- package/skills/gsd-path-inspect/scripts/check_task_briefs.py +3 -0
- package/skills/gsd-path-inspect/scripts/detect_project.py +22 -3
- package/skills/gsd-path-inspect/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-inspect/scripts/discussion_validate.py +1326 -0
- package/skills/gsd-path-inspect/scripts/integration.py +1459 -0
- package/skills/gsd-path-inspect/scripts/isolation.py +24 -6
- package/skills/gsd-path-inspect/scripts/loop_run.py +839 -0
- package/skills/gsd-path-inspect/scripts/model_policy.py +279 -0
- package/skills/gsd-path-inspect/scripts/path_config.py +248 -0
- package/skills/gsd-path-inspect/scripts/pipeline_git.py +3 -0
- package/skills/gsd-path-inspect/scripts/pipeline_state.py +117 -186
- package/skills/gsd-path-inspect/scripts/promote_lookahead.py +286 -0
- package/skills/gsd-path-inspect/scripts/review_panel.py +564 -0
- package/skills/gsd-path-inspect/scripts/roadmap.py +326 -0
- package/skills/gsd-path-inspect/scripts/state_checkpoint.py +114 -72
- package/skills/gsd-path-inspect/scripts/state_promote.py +47 -44
- package/skills/gsd-path-inspect/scripts/workflow_run.py +237 -0
- package/skills/gsd-path-inspect/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-loop/SKILL.md +2 -0
- package/skills/gsd-path-loop/references/runtime-selection.md +25 -0
- package/skills/gsd-path-loop/scripts/_common.py +5 -0
- package/skills/gsd-path-loop/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-loop/scripts/loop_run.py +2 -0
- package/skills/gsd-path-loop/scripts/model_policy.py +279 -0
- package/skills/gsd-path-loop/scripts/path_config.py +248 -0
- package/skills/gsd-path-loop/scripts/review_panel.py +564 -0
- package/skills/gsd-path-loop/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-migrate/SKILL.md +2 -0
- package/skills/gsd-path-migrate/references/runtime-selection.md +25 -0
- package/skills/gsd-path-migrate/scripts/_common.py +185 -0
- package/skills/gsd-path-migrate/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-migrate/scripts/loop_run.py +839 -0
- package/skills/gsd-path-migrate/scripts/model_policy.py +279 -0
- package/skills/gsd-path-migrate/scripts/path_config.py +248 -0
- package/skills/gsd-path-migrate/scripts/review_panel.py +564 -0
- package/skills/gsd-path-plan/SKILL.md +29 -19
- package/skills/gsd-path-plan/references/build-recovery.md +78 -0
- package/skills/gsd-path-plan/references/dispatch.md +12 -19
- package/skills/gsd-path-plan/references/model-policy.md +139 -0
- package/skills/gsd-path-plan/references/runtime-selection.md +25 -0
- package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/_common.py +5 -0
- package/skills/gsd-path-plan/scripts/archive_milestone.py +2171 -0
- package/skills/gsd-path-plan/scripts/build_recovery.py +292 -0
- package/skills/gsd-path-plan/scripts/check_handoffs.py +7 -1
- package/skills/gsd-path-plan/scripts/check_task_briefs.py +3 -0
- package/skills/gsd-path-plan/scripts/detect_project.py +1536 -0
- package/skills/gsd-path-plan/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-plan/scripts/discussion_validate.py +1326 -0
- package/skills/gsd-path-plan/scripts/integration.py +1459 -0
- package/skills/gsd-path-plan/scripts/isolation.py +24 -6
- package/skills/gsd-path-plan/scripts/loop_run.py +839 -0
- package/skills/gsd-path-plan/scripts/model_policy.py +279 -0
- package/skills/gsd-path-plan/scripts/path_config.py +248 -0
- package/skills/gsd-path-plan/scripts/pipeline_git.py +3 -0
- package/skills/gsd-path-plan/scripts/pipeline_state.py +117 -186
- package/skills/gsd-path-plan/scripts/promote_lookahead.py +286 -0
- package/skills/gsd-path-plan/scripts/review_panel.py +11 -0
- package/skills/gsd-path-plan/scripts/roadmap.py +326 -0
- package/skills/gsd-path-plan/scripts/state_checkpoint.py +114 -72
- package/skills/gsd-path-plan/scripts/state_promote.py +47 -44
- package/skills/gsd-path-plan/scripts/workflow_run.py +128 -8
- package/skills/gsd-path-plan/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-plan/templates/task.md +2 -0
- package/skills/gsd-path-research/SKILL.md +5 -6
- package/skills/gsd-path-research/references/dispatch.md +12 -19
- package/skills/gsd-path-research/references/model-policy.md +139 -0
- package/skills/gsd-path-research/references/runtime-selection.md +25 -0
- package/skills/gsd-path-research/scripts/_common.py +5 -0
- package/skills/gsd-path-research/scripts/archive_milestone.py +2171 -0
- package/skills/gsd-path-research/scripts/build_recovery.py +292 -0
- package/skills/gsd-path-research/scripts/check_handoffs.py +7 -1
- package/skills/gsd-path-research/scripts/check_task_briefs.py +3 -0
- package/skills/gsd-path-research/scripts/detect_project.py +1536 -0
- package/skills/gsd-path-research/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-research/scripts/discussion_validate.py +1326 -0
- package/skills/gsd-path-research/scripts/integration.py +1459 -0
- package/skills/gsd-path-research/scripts/isolation.py +24 -6
- package/skills/gsd-path-research/scripts/loop_run.py +839 -0
- package/skills/gsd-path-research/scripts/model_policy.py +279 -0
- package/skills/gsd-path-research/scripts/path_config.py +248 -0
- package/skills/gsd-path-research/scripts/pipeline_git.py +3 -0
- package/skills/gsd-path-research/scripts/pipeline_state.py +117 -186
- package/skills/gsd-path-research/scripts/promote_lookahead.py +286 -0
- package/skills/gsd-path-research/scripts/review_panel.py +564 -0
- package/skills/gsd-path-research/scripts/roadmap.py +326 -0
- package/skills/gsd-path-research/scripts/state_checkpoint.py +114 -72
- package/skills/gsd-path-research/scripts/state_promote.py +47 -44
- package/skills/gsd-path-research/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-roadmap/SKILL.md +24 -58
- package/skills/gsd-path-roadmap/references/dispatch.md +12 -19
- package/skills/gsd-path-roadmap/references/model-policy.md +139 -0
- package/skills/gsd-path-roadmap/references/runtime-selection.md +25 -0
- package/skills/gsd-path-roadmap/scripts/_common.py +5 -0
- package/skills/gsd-path-roadmap/scripts/archive_milestone.py +30 -124
- package/skills/gsd-path-roadmap/scripts/build_recovery.py +292 -0
- package/skills/gsd-path-roadmap/scripts/check_handoffs.py +7 -1
- package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +3 -0
- package/skills/gsd-path-roadmap/scripts/detect_project.py +22 -3
- package/skills/gsd-path-roadmap/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-roadmap/scripts/discussion_validate.py +8 -3
- package/skills/gsd-path-roadmap/scripts/integration.py +54 -28
- package/skills/gsd-path-roadmap/scripts/isolation.py +24 -6
- package/skills/gsd-path-roadmap/scripts/loop_run.py +839 -0
- package/skills/gsd-path-roadmap/scripts/model_policy.py +279 -0
- package/skills/gsd-path-roadmap/scripts/path_config.py +248 -0
- package/skills/gsd-path-roadmap/scripts/pipeline_git.py +3 -0
- package/skills/gsd-path-roadmap/scripts/pipeline_state.py +117 -186
- package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +33 -162
- package/skills/gsd-path-roadmap/scripts/review_panel.py +11 -0
- package/skills/gsd-path-roadmap/scripts/roadmap.py +326 -0
- package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +114 -72
- package/skills/gsd-path-roadmap/scripts/state_promote.py +47 -44
- package/skills/gsd-path-roadmap/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-ship/SKILL.md +29 -17
- package/skills/gsd-path-ship/references/dispatch.md +12 -19
- package/skills/gsd-path-ship/references/jev-review.md +82 -0
- package/skills/gsd-path-ship/references/model-policy.md +139 -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 -148
- package/skills/gsd-path-ship/references/runtime-selection.md +25 -0
- package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/_common.py +5 -0
- package/skills/gsd-path-ship/scripts/archive_milestone.py +30 -124
- package/skills/gsd-path-ship/scripts/build_recovery.py +292 -0
- package/skills/gsd-path-ship/scripts/build_state.py +2 -0
- package/skills/gsd-path-ship/scripts/check_handoffs.py +7 -1
- package/skills/gsd-path-ship/scripts/check_task_briefs.py +3 -0
- package/skills/gsd-path-ship/scripts/detect_project.py +1536 -0
- package/skills/gsd-path-ship/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-ship/scripts/discussion_validate.py +8 -3
- package/skills/gsd-path-ship/scripts/integration.py +54 -28
- package/skills/gsd-path-ship/scripts/isolation.py +24 -6
- package/skills/gsd-path-ship/scripts/jev_review.py +138 -0
- package/skills/gsd-path-ship/scripts/lean_verification.py +3 -1
- package/skills/gsd-path-ship/scripts/loop_run.py +839 -0
- package/skills/gsd-path-ship/scripts/model_policy.py +279 -0
- package/skills/gsd-path-ship/scripts/path_config.py +248 -0
- package/skills/gsd-path-ship/scripts/pipeline_git.py +3 -0
- package/skills/gsd-path-ship/scripts/pipeline_state.py +117 -186
- package/skills/gsd-path-ship/scripts/promote_lookahead.py +286 -0
- package/skills/gsd-path-ship/scripts/review_panel.py +11 -0
- package/skills/gsd-path-ship/scripts/roadmap.py +326 -0
- package/skills/gsd-path-ship/scripts/state_checkpoint.py +114 -72
- package/skills/gsd-path-ship/scripts/state_promote.py +47 -44
- package/skills/gsd-path-ship/scripts/workflow_run.py +128 -8
- package/skills/gsd-path-ship/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-ship/templates/wave-review.md +1 -1
- package/skills/gsd-path-undo/SKILL.md +2 -0
- package/skills/gsd-path-undo/references/runtime-selection.md +25 -0
- package/skills/gsd-path-undo/scripts/_common.py +5 -0
- package/skills/gsd-path-undo/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-undo/scripts/loop_run.py +839 -0
- package/skills/gsd-path-undo/scripts/model_policy.py +279 -0
- package/skills/gsd-path-undo/scripts/path_config.py +248 -0
- package/skills/gsd-path-undo/scripts/pipeline_undo.py +14 -7
- package/skills/gsd-path-undo/scripts/review_panel.py +564 -0
- package/skills/gsd-path-undo/scripts/worktree_paths.py +94 -0
- package/skills/path/BUILD.md +54 -575
- package/skills/path/DECIDE.md +4 -6
- package/skills/path/DEFINE.md +23 -21
- package/skills/path/DOCS-AUDIT.md +2 -0
- package/skills/path/INSPECT.md +32 -8
- package/skills/path/PLAN.md +29 -19
- package/skills/path/RESEARCH.md +5 -6
- package/skills/path/ROADMAP.md +24 -58
- package/skills/path/SHIP.md +29 -17
- package/skills/path/SKILL.md +46 -22
- package/skills/path/references/build-abandon.md +33 -0
- package/skills/path/references/build-native.md +502 -0
- package/skills/path/references/build-recovery.md +78 -0
- package/skills/path/references/coder.md +8 -3
- package/skills/path/references/config.md +57 -0
- package/skills/path/references/dispatch.md +12 -19
- package/skills/path/references/docs-auditor.md +14 -0
- package/skills/path/references/jev-review.md +82 -0
- package/skills/path/references/model-policy.md +139 -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 -148
- package/skills/path/references/runtime-selection.md +25 -0
- package/skills/path/scripts/_common.py +5 -0
- package/skills/path/scripts/archive_milestone.py +30 -124
- package/skills/path/scripts/bootstrap_repository.py +33 -4
- package/skills/path/scripts/build_recovery.py +292 -0
- package/skills/path/scripts/build_state.py +2 -0
- package/skills/path/scripts/check_docs_audit.py +7 -3
- package/skills/path/scripts/check_handoffs.py +7 -1
- package/skills/path/scripts/check_task_briefs.py +3 -0
- package/skills/path/scripts/detect_project.py +22 -3
- package/skills/path/scripts/discussion_records.py +3 -0
- package/skills/path/scripts/discussion_validate.py +8 -3
- package/skills/path/scripts/dispatch_driver.py +233 -22
- package/skills/path/scripts/git_guard.py +768 -0
- package/skills/path/scripts/integration.py +54 -28
- package/skills/path/scripts/isolation.py +24 -6
- package/skills/path/scripts/jev_review.py +138 -0
- package/skills/path/scripts/lean_verification.py +3 -1
- package/skills/path/scripts/loop_run.py +2 -0
- package/skills/path/scripts/model_policy.py +279 -0
- package/skills/path/scripts/path_config.py +248 -0
- package/skills/path/scripts/pipeline_git.py +3 -0
- package/skills/path/scripts/pipeline_state.py +117 -186
- package/skills/path/scripts/promote_lookahead.py +33 -162
- package/skills/path/scripts/review_panel.py +11 -0
- package/skills/path/scripts/roadmap.py +326 -0
- package/skills/path/scripts/state_checkpoint.py +114 -72
- package/skills/path/scripts/state_promote.py +47 -44
- package/skills/path/scripts/status_runtime.py +208 -0
- 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 +128 -8
- package/skills/path/scripts/worktree_paths.py +94 -0
- package/skills/path/templates/skeptic.md +1 -1
- package/skills/path/templates/task.md +2 -0
- package/skills/path/templates/wave-panel.md +1 -1
- package/skills/path/templates/wave-review.md +1 -1
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Git pre-commit, commit-msg, and pre-push guard for GSD Path projects.
|
|
3
|
+
|
|
4
|
+
See HOOKS.md for guard rules and scripts/install.py / scripts/install.mjs for
|
|
5
|
+
hook wiring. Inspection failures and violations both exit 1: unreadable Git
|
|
6
|
+
state cannot prove archive immutability or safe publication.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
import subprocess
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
# Installed layout keeps the pipeline runtime in .gsd-path/runtime/ beside this
|
|
16
|
+
# guard; the repository checkout keeps it as sibling modules under scripts/.
|
|
17
|
+
_HERE = Path(__file__).resolve().parent
|
|
18
|
+
sys.path.insert(0, str(_HERE / "runtime" if (_HERE / "runtime" / "isolation.py").is_file() else _HERE))
|
|
19
|
+
sys.dont_write_bytecode = True # a hook must not leave __pycache__ in the worktree
|
|
20
|
+
try:
|
|
21
|
+
from isolation import BOOKKEEPING_PREFIXES, NULL_SHA, _landing_state, task_frontmatter
|
|
22
|
+
from pipeline_git import is_ship_subject, task_commit_body
|
|
23
|
+
from pipeline_state import _completion_status
|
|
24
|
+
except ImportError as error: # pragma: no cover - broken install
|
|
25
|
+
print(
|
|
26
|
+
f"gsd-path guard: pipeline runtime is missing ({error}); commit blocked; "
|
|
27
|
+
"rerun the installer with --hooks-refresh",
|
|
28
|
+
file=sys.stderr,
|
|
29
|
+
)
|
|
30
|
+
raise SystemExit(1)
|
|
31
|
+
|
|
32
|
+
ARCHIVE_PREFIX = ".project/archive/"
|
|
33
|
+
GUARD_MARKER = "gsd-path guard"
|
|
34
|
+
INTEGRATE_SUBJECT = re.compile(
|
|
35
|
+
r"^integrate: (?P<milestone>M\d{3,}) — merge "
|
|
36
|
+
r"(?P<branch>gsd-path/M\d{3,}) into (?P<target>\S+)$"
|
|
37
|
+
)
|
|
38
|
+
ABANDON_SUBJECT = re.compile(
|
|
39
|
+
r"^build: abandon milestone (?P<slug>[a-z0-9][a-z0-9-]*)$"
|
|
40
|
+
)
|
|
41
|
+
ROADMAP_HEADING = re.compile(
|
|
42
|
+
r"^### (?P<milestone>M\d{3,}) — (?P<slug>[a-z0-9][a-z0-9-]*)\s*$"
|
|
43
|
+
)
|
|
44
|
+
ARCHIVE_NAME = re.compile(
|
|
45
|
+
r"^\.project/archive/(?P<number>\d{3,})-(?P<slug>[a-z0-9][a-z0-9-]*)$"
|
|
46
|
+
)
|
|
47
|
+
SLUG = re.compile(r"^[a-z0-9][a-z0-9-]*$")
|
|
48
|
+
BOUND_BRANCH = re.compile(r"^gsd-path/M(?P<number>\d{3,})$")
|
|
49
|
+
TASK_BRANCH_PREFIX = "gsd-path-task/"
|
|
50
|
+
TASK_SUBJECT = re.compile(r"^(?P<id>[A-Za-z][A-Za-z0-9._-]*): \S.*$")
|
|
51
|
+
FULL_SHA = re.compile(r"^[0-9a-f]{40}$")
|
|
52
|
+
LANDING_HINT = (
|
|
53
|
+
"during build, changes outside .project/ land only through isolation.py "
|
|
54
|
+
"land; bookkeeping commits may touch only .project/"
|
|
55
|
+
)
|
|
56
|
+
PRODUCT_HINT = (
|
|
57
|
+
f"while this worktree carries unfinished milestone state, {LANDING_HINT}; "
|
|
58
|
+
"findings during ship reopen through the patch plan"
|
|
59
|
+
)
|
|
60
|
+
PUBLICATION_HINT = (
|
|
61
|
+
"unshipped milestone work reaches a remote only as its ship commit "
|
|
62
|
+
"(STATE shipped/done) through archive_milestone.py integrate; findings during "
|
|
63
|
+
"ship reopen through the patch plan (see SHIP.md)"
|
|
64
|
+
)
|
|
65
|
+
LEGACY_STATE_FIELDS = {
|
|
66
|
+
"pipeline",
|
|
67
|
+
"project",
|
|
68
|
+
"milestone",
|
|
69
|
+
"phase",
|
|
70
|
+
"status",
|
|
71
|
+
"branch",
|
|
72
|
+
"archive",
|
|
73
|
+
}
|
|
74
|
+
INTEGRATION_STATE_FIELDS = LEGACY_STATE_FIELDS | {
|
|
75
|
+
"integration_default",
|
|
76
|
+
"integration",
|
|
77
|
+
}
|
|
78
|
+
PROVENANCE_STATE_FIELDS = INTEGRATION_STATE_FIELDS | {"integration_source"}
|
|
79
|
+
INTEGRATION_MODES = {"direct", "pull-request"}
|
|
80
|
+
INTEGRATION_SOURCES = {"default", "milestone"}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def staged_entries():
|
|
84
|
+
output = subprocess.run(
|
|
85
|
+
["git", "diff", "--cached", "--name-status", "-z", "--find-renames"],
|
|
86
|
+
capture_output=True,
|
|
87
|
+
text=True,
|
|
88
|
+
check=True,
|
|
89
|
+
).stdout
|
|
90
|
+
tokens = output.split("\0")
|
|
91
|
+
entries = []
|
|
92
|
+
index = 0
|
|
93
|
+
while index < len(tokens):
|
|
94
|
+
status = tokens[index]
|
|
95
|
+
if not status:
|
|
96
|
+
index += 1
|
|
97
|
+
continue
|
|
98
|
+
code = status[0]
|
|
99
|
+
if code in "RC":
|
|
100
|
+
entries.append((code, tokens[index + 1], tokens[index + 2]))
|
|
101
|
+
index += 3
|
|
102
|
+
else:
|
|
103
|
+
entries.append((code, tokens[index + 1], None))
|
|
104
|
+
index += 2
|
|
105
|
+
return entries
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def message_of(message_file):
|
|
109
|
+
lines = [
|
|
110
|
+
line.rstrip()
|
|
111
|
+
for line in Path(message_file).read_text(encoding="utf-8").splitlines()
|
|
112
|
+
]
|
|
113
|
+
lines = [line for index, line in enumerate(lines) if line or (index and lines[index - 1])]
|
|
114
|
+
while lines and not lines[0]:
|
|
115
|
+
lines.pop(0)
|
|
116
|
+
if not lines:
|
|
117
|
+
return "", ""
|
|
118
|
+
subject_end = lines.index("") if "" in lines else len(lines)
|
|
119
|
+
return " ".join(line.strip() for line in lines[:subject_end]), "\n".join(lines[subject_end:]).strip("\n")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def archive_root(path):
|
|
123
|
+
if not path.startswith(ARCHIVE_PREFIX):
|
|
124
|
+
return None
|
|
125
|
+
name = path[len(ARCHIVE_PREFIX):].split("/", 1)[0]
|
|
126
|
+
return f"{ARCHIVE_PREFIX}{name}" if name else None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def destination_path(entry):
|
|
130
|
+
code, old, new = entry
|
|
131
|
+
if code == "A":
|
|
132
|
+
return old
|
|
133
|
+
if code in "CR":
|
|
134
|
+
return new
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def committed_archive_roots():
|
|
139
|
+
head = subprocess.run(
|
|
140
|
+
["git", "rev-parse", "--verify", "--quiet", "HEAD^{commit}"],
|
|
141
|
+
capture_output=True,
|
|
142
|
+
text=True,
|
|
143
|
+
check=False,
|
|
144
|
+
)
|
|
145
|
+
if head.returncode == 1:
|
|
146
|
+
return set() # unborn HEAD: the first commit has no committed tree
|
|
147
|
+
head.check_returncode()
|
|
148
|
+
output = subprocess.run(
|
|
149
|
+
["git", "ls-tree", "-r", "--name-only", "HEAD", "--", ARCHIVE_PREFIX],
|
|
150
|
+
capture_output=True,
|
|
151
|
+
text=True,
|
|
152
|
+
check=True,
|
|
153
|
+
).stdout
|
|
154
|
+
return {
|
|
155
|
+
root
|
|
156
|
+
for path in output.splitlines()
|
|
157
|
+
if (root := archive_root(path)) is not None
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def staged_file(path):
|
|
162
|
+
return subprocess.run(
|
|
163
|
+
["git", "show", f":{path}"],
|
|
164
|
+
capture_output=True,
|
|
165
|
+
text=True,
|
|
166
|
+
check=True,
|
|
167
|
+
).stdout
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def staged_frontmatter():
|
|
171
|
+
return frontmatter_of(staged_file(".project/STATE.md"), "staged STATE.md")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def shown_file(revspec):
|
|
175
|
+
"""`git show <revspec>` text, or None when the object does not exist."""
|
|
176
|
+
shown = subprocess.run(
|
|
177
|
+
["git", "show", revspec],
|
|
178
|
+
capture_output=True,
|
|
179
|
+
text=True,
|
|
180
|
+
check=False,
|
|
181
|
+
)
|
|
182
|
+
return shown.stdout if shown.returncode == 0 else None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def state_at(revision):
|
|
186
|
+
"""The STATE.md frontmatter at a revision; None when it has no STATE.md."""
|
|
187
|
+
subprocess.run(
|
|
188
|
+
["git", "cat-file", "-e", revision],
|
|
189
|
+
capture_output=True,
|
|
190
|
+
text=True,
|
|
191
|
+
check=True,
|
|
192
|
+
)
|
|
193
|
+
content = shown_file(f"{revision}:.project/STATE.md")
|
|
194
|
+
return None if content is None else frontmatter_of(content, f"{revision} STATE.md")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def head_frontmatter():
|
|
198
|
+
content = shown_file("HEAD:.project/STATE.md")
|
|
199
|
+
return None if content is None else frontmatter_of(content, "HEAD STATE.md")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def shipped(state):
|
|
203
|
+
return (state.get("phase"), state.get("status")) == ("shipped", "done")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def unshipped_bound_branch(state):
|
|
207
|
+
"""The bound branch a STATE still owes a ship commit to, or None."""
|
|
208
|
+
bound = state.get("branch", "")
|
|
209
|
+
return bound if BOUND_BRANCH.fullmatch(bound) and not shipped(state) else None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def frontmatter_of(content, label):
|
|
213
|
+
lines = content.splitlines()
|
|
214
|
+
if not lines or lines[0] != "---":
|
|
215
|
+
raise ValueError(f"{label} is missing YAML frontmatter")
|
|
216
|
+
values = {}
|
|
217
|
+
closed = False
|
|
218
|
+
for line in lines[1:]:
|
|
219
|
+
if line == "---":
|
|
220
|
+
closed = True
|
|
221
|
+
break
|
|
222
|
+
if not line.strip() or line.lstrip().startswith("#"):
|
|
223
|
+
continue
|
|
224
|
+
match = re.fullmatch(r"([a-z_]+):\s*([^#]*?)(?:\s+#.*)?", line)
|
|
225
|
+
if match is None:
|
|
226
|
+
raise ValueError(f"{label} has malformed frontmatter: {line}")
|
|
227
|
+
key, value = match.groups()
|
|
228
|
+
if key in values:
|
|
229
|
+
raise ValueError(f"{label} repeats field: {key}")
|
|
230
|
+
values[key] = value.strip().strip("\"'")
|
|
231
|
+
if not closed:
|
|
232
|
+
raise ValueError(f"{label} frontmatter is not closed")
|
|
233
|
+
return values
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def strict_ship_state(state, archive):
|
|
237
|
+
fields = frozenset(state)
|
|
238
|
+
if fields not in {
|
|
239
|
+
frozenset(LEGACY_STATE_FIELDS),
|
|
240
|
+
frozenset(INTEGRATION_STATE_FIELDS),
|
|
241
|
+
frozenset(PROVENANCE_STATE_FIELDS),
|
|
242
|
+
}:
|
|
243
|
+
return False
|
|
244
|
+
if fields != frozenset(LEGACY_STATE_FIELDS):
|
|
245
|
+
if state.get("integration_default") not in INTEGRATION_MODES:
|
|
246
|
+
return False
|
|
247
|
+
if state.get("integration") not in INTEGRATION_MODES:
|
|
248
|
+
return False
|
|
249
|
+
if fields == frozenset(PROVENANCE_STATE_FIELDS):
|
|
250
|
+
source = state.get("integration_source")
|
|
251
|
+
if source not in INTEGRATION_SOURCES:
|
|
252
|
+
return False
|
|
253
|
+
if source == "default" and state.get("integration") != state.get(
|
|
254
|
+
"integration_default"
|
|
255
|
+
):
|
|
256
|
+
return False
|
|
257
|
+
archive_match = ARCHIVE_NAME.fullmatch(archive or "")
|
|
258
|
+
branch_match = BOUND_BRANCH.fullmatch(state.get("branch", ""))
|
|
259
|
+
if archive_match is None or branch_match is None:
|
|
260
|
+
return False
|
|
261
|
+
return (
|
|
262
|
+
state.get("pipeline") == "gsd-path/v2"
|
|
263
|
+
and bool(SLUG.fullmatch(state.get("project", "")))
|
|
264
|
+
and state.get("milestone") == archive_match.group("slug")
|
|
265
|
+
and state.get("phase") == "shipped"
|
|
266
|
+
and state.get("status") == "done"
|
|
267
|
+
and state.get("archive") == archive
|
|
268
|
+
and int(branch_match.group("number")) == int(archive_match.group("number"))
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def ship_contract_violations(entries, subject, body, new_archives, state):
|
|
273
|
+
if not is_ship_commit(subject):
|
|
274
|
+
return []
|
|
275
|
+
found = []
|
|
276
|
+
if len(new_archives) != 1:
|
|
277
|
+
return ["a ship commit requires exactly one new current archive"]
|
|
278
|
+
archive = next(iter(new_archives))
|
|
279
|
+
match = ARCHIVE_NAME.fullmatch(archive)
|
|
280
|
+
if match is None or state is None or not strict_ship_state(state, archive):
|
|
281
|
+
found.append("ship commit staged STATE.md is not the strict shipped transaction")
|
|
282
|
+
return found
|
|
283
|
+
expected_subject = f"ship: M{int(match.group('number')):03d} — {match.group('slug')}"
|
|
284
|
+
if subject != expected_subject:
|
|
285
|
+
found.append(f"ship commit subject must be {expected_subject!r}")
|
|
286
|
+
if current_branch() != state["branch"]:
|
|
287
|
+
found.append("ship commit must run on staged STATE.branch")
|
|
288
|
+
reviewed_head = subprocess.run(
|
|
289
|
+
["git", "rev-parse", "--verify", "HEAD"],
|
|
290
|
+
capture_output=True,
|
|
291
|
+
text=True,
|
|
292
|
+
check=True,
|
|
293
|
+
).stdout.strip()
|
|
294
|
+
expected_body = f"Archive: {archive}\nReviewed-HEAD: {reviewed_head}"
|
|
295
|
+
if body != expected_body:
|
|
296
|
+
found.append("ship commit body does not match Archive and Reviewed-HEAD")
|
|
297
|
+
changed_paths = {
|
|
298
|
+
new if code in "CR" else old
|
|
299
|
+
for code, old, new in entries
|
|
300
|
+
}
|
|
301
|
+
if ".project/STATE.md" not in changed_paths:
|
|
302
|
+
found.append("ship commit must stage .project/STATE.md")
|
|
303
|
+
if f"{archive}/MANIFEST.md" not in changed_paths:
|
|
304
|
+
found.append("ship commit must add the current archive MANIFEST.md")
|
|
305
|
+
return found
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def staged_archive_target(values=None):
|
|
309
|
+
values = staged_frontmatter() if values is None else values
|
|
310
|
+
if "archive" not in values:
|
|
311
|
+
raise ValueError("staged STATE.md must contain exactly one archive field")
|
|
312
|
+
value = values["archive"]
|
|
313
|
+
if value in {"", "null"}:
|
|
314
|
+
return None
|
|
315
|
+
root = archive_root(value)
|
|
316
|
+
if root != value.rstrip("/"):
|
|
317
|
+
raise ValueError(f"staged STATE.md archive is invalid: {value}")
|
|
318
|
+
return root
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def staged_abandon_target(new_archives, state):
|
|
322
|
+
"""Return archive, slug, and reason for a complete abandon commit."""
|
|
323
|
+
if len(new_archives) != 1:
|
|
324
|
+
return None
|
|
325
|
+
target = next(iter(new_archives))
|
|
326
|
+
archive_match = ARCHIVE_NAME.fullmatch(target)
|
|
327
|
+
if archive_match is None:
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
required_state = {
|
|
331
|
+
"pipeline": "gsd-path/v2",
|
|
332
|
+
"milestone": "null",
|
|
333
|
+
"phase": "roadmap",
|
|
334
|
+
"status": "active",
|
|
335
|
+
"archive": "null",
|
|
336
|
+
}
|
|
337
|
+
if any(state.get(key) != value for key, value in required_state.items()):
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
sections = []
|
|
341
|
+
lines = staged_file(".project/ROADMAP.md").splitlines()
|
|
342
|
+
for index, line in enumerate(lines):
|
|
343
|
+
heading = ROADMAP_HEADING.fullmatch(line)
|
|
344
|
+
if heading is None:
|
|
345
|
+
continue
|
|
346
|
+
end = next(
|
|
347
|
+
(
|
|
348
|
+
cursor
|
|
349
|
+
for cursor in range(index + 1, len(lines))
|
|
350
|
+
if ROADMAP_HEADING.fullmatch(lines[cursor]) is not None
|
|
351
|
+
),
|
|
352
|
+
len(lines),
|
|
353
|
+
)
|
|
354
|
+
section = lines[index:end]
|
|
355
|
+
if (
|
|
356
|
+
section.count("Status: abandoned") == 1
|
|
357
|
+
and section.count(f"Archive: {target}")
|
|
358
|
+
+ section.count(f"Archive: {target}/")
|
|
359
|
+
== 1
|
|
360
|
+
):
|
|
361
|
+
sections.append(heading)
|
|
362
|
+
if len(sections) != 1:
|
|
363
|
+
return None
|
|
364
|
+
|
|
365
|
+
heading = sections[0]
|
|
366
|
+
slug = heading.group("slug")
|
|
367
|
+
branch = f"gsd-path/{heading.group('milestone')}"
|
|
368
|
+
if (
|
|
369
|
+
slug != archive_match.group("slug")
|
|
370
|
+
or state.get("branch") != branch
|
|
371
|
+
or current_branch() != branch
|
|
372
|
+
):
|
|
373
|
+
return None
|
|
374
|
+
reasons = [
|
|
375
|
+
line[len("Reason:") :].strip()
|
|
376
|
+
for line in staged_file(f"{target}/MANIFEST.md").splitlines()
|
|
377
|
+
if line.startswith("Reason:")
|
|
378
|
+
]
|
|
379
|
+
if len(reasons) != 1 or not reasons[0]:
|
|
380
|
+
return None
|
|
381
|
+
reason = " ".join(reasons[0].split())
|
|
382
|
+
return target, slug, reason
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@lru_cache(maxsize=None)
|
|
386
|
+
def current_branch():
|
|
387
|
+
return subprocess.run(
|
|
388
|
+
["git", "branch", "--show-current"],
|
|
389
|
+
capture_output=True,
|
|
390
|
+
text=True,
|
|
391
|
+
check=True,
|
|
392
|
+
).stdout.strip()
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def closed_milestone_reason():
|
|
396
|
+
branch = current_branch()
|
|
397
|
+
if BOUND_BRANCH.fullmatch(branch) is None:
|
|
398
|
+
return None
|
|
399
|
+
commits = subprocess.run(
|
|
400
|
+
["git", "log", "--format=%H", "--grep=^ship:", "HEAD"],
|
|
401
|
+
capture_output=True, text=True, check=True,
|
|
402
|
+
).stdout.splitlines()
|
|
403
|
+
if any(ship_commit_at(sha, branch) for sha in commits):
|
|
404
|
+
return (f"closed milestone {branch} accepts no new work; finish integration "
|
|
405
|
+
"through ship, then use the router's next-milestone handoff")
|
|
406
|
+
return None
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def head_descends_from(commit):
|
|
410
|
+
return (
|
|
411
|
+
subprocess.run(
|
|
412
|
+
["git", "merge-base", "--is-ancestor", commit, "HEAD"],
|
|
413
|
+
capture_output=True,
|
|
414
|
+
text=True,
|
|
415
|
+
check=False,
|
|
416
|
+
).returncode
|
|
417
|
+
== 0
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def task_landed_at_head(task_file):
|
|
422
|
+
content = shown_file(f"HEAD:{task_file}")
|
|
423
|
+
fields, _ = task_frontmatter(content) if content is not None else (None, None)
|
|
424
|
+
return bool(fields) and fields.get("status") == "done"
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def repo_root():
|
|
428
|
+
return Path(
|
|
429
|
+
subprocess.run(
|
|
430
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
431
|
+
capture_output=True,
|
|
432
|
+
text=True,
|
|
433
|
+
check=True,
|
|
434
|
+
).stdout.strip()
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def product_commit_violations(entries, subject, body):
|
|
439
|
+
"""Hold product changes until build landing or verified integration."""
|
|
440
|
+
if is_ship_commit(subject):
|
|
441
|
+
return [] # ship commits are held to .project/ by ship_contract_violations
|
|
442
|
+
staged = sorted({path for _, old, new in entries for path in (old, new) if path})
|
|
443
|
+
if all(path.startswith(BOOKKEEPING_PREFIXES) for path in staged):
|
|
444
|
+
return []
|
|
445
|
+
# A commit that enters build must stay .project/-only, so the staged STATE
|
|
446
|
+
# counts as much as the committed one.
|
|
447
|
+
state = staged_frontmatter() if ".project/STATE.md" in staged else head_frontmatter()
|
|
448
|
+
if state is None:
|
|
449
|
+
return []
|
|
450
|
+
if shipped(state):
|
|
451
|
+
if is_integration_merge(subject):
|
|
452
|
+
return []
|
|
453
|
+
completion = _completion_status(repo_root(), state, ".project")
|
|
454
|
+
if completion["status"] == "verified":
|
|
455
|
+
return []
|
|
456
|
+
return [f"milestone integration is unverified: {completion.get('reason')}; {PRODUCT_HINT}"]
|
|
457
|
+
phase = state.get("phase")
|
|
458
|
+
if subject is None and phase == "build":
|
|
459
|
+
return [] # pre-commit has no message; commit-msg checks the landing shape
|
|
460
|
+
bound = unshipped_bound_branch(state)
|
|
461
|
+
if bound is None:
|
|
462
|
+
return []
|
|
463
|
+
if phase != "build":
|
|
464
|
+
return [f"product files are staged while STATE is {phase}; {PRODUCT_HINT}"]
|
|
465
|
+
if TASK_SUBJECT.fullmatch(subject) is None:
|
|
466
|
+
return [f"{subject!r} is not a landing commit; {LANDING_HINT}"]
|
|
467
|
+
fields = {
|
|
468
|
+
line[:4]: line[6:].strip()
|
|
469
|
+
for line in body.splitlines()
|
|
470
|
+
if line.startswith(("Task: ", "Base: "))
|
|
471
|
+
}
|
|
472
|
+
task_file, base = fields.get("Task", ""), fields.get("Base", "")
|
|
473
|
+
problems = []
|
|
474
|
+
if FULL_SHA.fullmatch(base) is None or not head_descends_from(base):
|
|
475
|
+
problems.append("Base: must be a full SHA that HEAD descends from")
|
|
476
|
+
elif task_file not in staged or not task_file.startswith(".project/tasks/"):
|
|
477
|
+
problems.append("the Task: file must be a staged .project/tasks/ file")
|
|
478
|
+
elif task_landed_at_head(task_file):
|
|
479
|
+
problems.append("the Task: file is already done at HEAD; a task lands exactly once")
|
|
480
|
+
else:
|
|
481
|
+
# The same proof isolation.py land and recover apply: subject from the
|
|
482
|
+
# base task, allow-list from its files:, and the landed-state transition.
|
|
483
|
+
contract_paths, error = _landing_state(
|
|
484
|
+
repo_root(), base, task_file, staged_file(task_file), subject, None
|
|
485
|
+
)
|
|
486
|
+
if error:
|
|
487
|
+
problems.append(error)
|
|
488
|
+
if contract_paths is not None:
|
|
489
|
+
stray = sorted(set(staged) - contract_paths)
|
|
490
|
+
if stray:
|
|
491
|
+
problems.append("undeclared paths: " + ", ".join(stray))
|
|
492
|
+
if body != task_commit_body(task_file, staged, base).strip():
|
|
493
|
+
problems.append("body must be exactly the Task:/Base:/Files: block for the staged paths")
|
|
494
|
+
if problems:
|
|
495
|
+
return [f"{subject!r} is not a valid landing commit ({'; '.join(problems)}); {LANDING_HINT}"]
|
|
496
|
+
return []
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
@lru_cache(maxsize=None)
|
|
500
|
+
def ship_commit_at(sha, bound):
|
|
501
|
+
"""True when the commit is the strict ship commit its own STATE.md names."""
|
|
502
|
+
state = state_at(sha)
|
|
503
|
+
if state is None or state.get("branch") != bound:
|
|
504
|
+
return False
|
|
505
|
+
shown = subprocess.run(
|
|
506
|
+
["git", "show", "--no-patch", "--format=%s", sha],
|
|
507
|
+
capture_output=True,
|
|
508
|
+
text=True,
|
|
509
|
+
check=True,
|
|
510
|
+
)
|
|
511
|
+
archive = state.get("archive") or ""
|
|
512
|
+
return strict_ship_state(state, archive) and is_ship_subject(
|
|
513
|
+
shown.stdout.strip(), archive.rsplit("/", 1)[-1]
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def pre_push_violations(lines):
|
|
518
|
+
"""Refuse ref updates that publish unshipped milestone work.
|
|
519
|
+
|
|
520
|
+
A bound-named ref moves only to, or away from, its strict ship commit. Any
|
|
521
|
+
other ref may not carry a commit whose STATE.md still owes a ship commit.
|
|
522
|
+
"""
|
|
523
|
+
found = []
|
|
524
|
+
for line in lines:
|
|
525
|
+
parts = line.split()
|
|
526
|
+
if len(parts) != 4:
|
|
527
|
+
raise ValueError(f"unexpected pre-push line: {line!r}")
|
|
528
|
+
local_ref, local_sha, remote_ref, remote_sha = parts
|
|
529
|
+
if any(re.fullmatch(r"[0-9a-fA-F]{40}", sha) is None for sha in (local_sha, remote_sha)):
|
|
530
|
+
raise ValueError(f"unexpected pre-push SHA: {line!r}")
|
|
531
|
+
names = [ref.removeprefix("refs/heads/") for ref in (local_ref, remote_ref)]
|
|
532
|
+
bounds = {name for name in names if BOUND_BRANCH.fullmatch(name)}
|
|
533
|
+
if bounds:
|
|
534
|
+
# A deletion is judged by the commit it removes; an absent ref proves nothing.
|
|
535
|
+
sha = remote_sha if local_sha == NULL_SHA else local_sha
|
|
536
|
+
for bound in sorted(bounds):
|
|
537
|
+
if sha != NULL_SHA and not ship_commit_at(sha, bound):
|
|
538
|
+
found.append(
|
|
539
|
+
f"{remote_ref} <- {sha[:12]} moves {bound} off its ship commit; "
|
|
540
|
+
f"{PUBLICATION_HINT}"
|
|
541
|
+
)
|
|
542
|
+
continue
|
|
543
|
+
if local_sha == NULL_SHA:
|
|
544
|
+
continue
|
|
545
|
+
state = state_at(local_sha)
|
|
546
|
+
owed = unshipped_bound_branch(state) if state is not None else None
|
|
547
|
+
if owed is not None:
|
|
548
|
+
found.append(
|
|
549
|
+
f"{remote_ref} <- {local_sha[:12]} carries unshipped {owed} work "
|
|
550
|
+
f"({state.get('phase')}/{state.get('status')}); {PUBLICATION_HINT}"
|
|
551
|
+
)
|
|
552
|
+
return found
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def is_ship_commit(subject):
|
|
556
|
+
return bool(subject and subject.strip().casefold().startswith("ship:"))
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def is_abandon_commit(subject, abandon):
|
|
560
|
+
match = ABANDON_SUBJECT.fullmatch(subject or "")
|
|
561
|
+
return bool(match and abandon and match.group("slug") == abandon[1])
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def abandon_contract_violations(subject, body, abandon):
|
|
565
|
+
if not is_abandon_commit(subject, abandon):
|
|
566
|
+
return []
|
|
567
|
+
expected = f"Why: {abandon[2]}"
|
|
568
|
+
if body != expected:
|
|
569
|
+
return ["abandon commit body does not match the archived MANIFEST Reason"]
|
|
570
|
+
return []
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def default_branch():
|
|
574
|
+
origin_head = subprocess.run(
|
|
575
|
+
["git", "symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"],
|
|
576
|
+
capture_output=True,
|
|
577
|
+
text=True,
|
|
578
|
+
check=False,
|
|
579
|
+
)
|
|
580
|
+
if origin_head.returncode == 0 and origin_head.stdout.strip().startswith("origin/"):
|
|
581
|
+
return origin_head.stdout.strip()[len("origin/") :]
|
|
582
|
+
configured = subprocess.run(
|
|
583
|
+
["git", "config", "--get", "init.defaultBranch"],
|
|
584
|
+
capture_output=True,
|
|
585
|
+
text=True,
|
|
586
|
+
check=False,
|
|
587
|
+
)
|
|
588
|
+
if configured.returncode == 0 and configured.stdout.strip():
|
|
589
|
+
return configured.stdout.strip()
|
|
590
|
+
return "main"
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def is_integration_merge(subject):
|
|
594
|
+
match = INTEGRATE_SUBJECT.fullmatch(subject or "")
|
|
595
|
+
if not match or match.group("branch") != f"gsd-path/{match.group('milestone')}":
|
|
596
|
+
return False
|
|
597
|
+
target = default_branch()
|
|
598
|
+
if match.group("target") != target:
|
|
599
|
+
return False
|
|
600
|
+
branch = current_branch()
|
|
601
|
+
milestone = match.group("milestone")
|
|
602
|
+
if branch not in {target, f"gsd-path-integrate/{milestone}"}:
|
|
603
|
+
return False
|
|
604
|
+
merge_head = subprocess.run(
|
|
605
|
+
["git", "rev-parse", "--verify", "--quiet", "MERGE_HEAD^{commit}"],
|
|
606
|
+
capture_output=True,
|
|
607
|
+
text=True,
|
|
608
|
+
check=False,
|
|
609
|
+
)
|
|
610
|
+
bound_head = subprocess.run(
|
|
611
|
+
[
|
|
612
|
+
"git",
|
|
613
|
+
"rev-parse",
|
|
614
|
+
"--verify",
|
|
615
|
+
"--quiet",
|
|
616
|
+
f"refs/heads/{match.group('branch')}^{{commit}}",
|
|
617
|
+
],
|
|
618
|
+
capture_output=True,
|
|
619
|
+
text=True,
|
|
620
|
+
check=False,
|
|
621
|
+
)
|
|
622
|
+
return (
|
|
623
|
+
merge_head.returncode == 0
|
|
624
|
+
and bound_head.returncode == 0
|
|
625
|
+
and merge_head.stdout.strip() == bound_head.stdout.strip()
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def violations(
|
|
630
|
+
entries,
|
|
631
|
+
subject,
|
|
632
|
+
existing_archives=(),
|
|
633
|
+
current_archive=None,
|
|
634
|
+
integration_merge=False,
|
|
635
|
+
abandon_commit=False,
|
|
636
|
+
):
|
|
637
|
+
found = []
|
|
638
|
+
existing = set(existing_archives)
|
|
639
|
+
for code, old, new in entries:
|
|
640
|
+
if code in "MDTR" and old.startswith(ARCHIVE_PREFIX):
|
|
641
|
+
label = f"{code} {old}" + (f" -> {new}" if new else "")
|
|
642
|
+
found.append(
|
|
643
|
+
f"{label}: committed archives are read-only; "
|
|
644
|
+
f"unstage it with git restore --staged -- {new or old}"
|
|
645
|
+
)
|
|
646
|
+
for entry in entries:
|
|
647
|
+
destination = destination_path(entry)
|
|
648
|
+
root = archive_root(destination or "")
|
|
649
|
+
if root is None:
|
|
650
|
+
continue
|
|
651
|
+
code, old, new = entry
|
|
652
|
+
label = f"{code} {old}" + (f" -> {new}" if new else "")
|
|
653
|
+
if root in existing:
|
|
654
|
+
found.append(
|
|
655
|
+
f"{label}: committed archives are read-only; "
|
|
656
|
+
f"unstage it with git restore --staged -- {destination}"
|
|
657
|
+
)
|
|
658
|
+
elif root != current_archive:
|
|
659
|
+
found.append(
|
|
660
|
+
f"{label}: only the current STATE.archive "
|
|
661
|
+
f"({current_archive or 'unset'}) may be created; "
|
|
662
|
+
f"unstage it with git restore --staged -- {destination}"
|
|
663
|
+
)
|
|
664
|
+
elif (
|
|
665
|
+
subject is not None
|
|
666
|
+
and not is_ship_commit(subject)
|
|
667
|
+
and not integration_merge
|
|
668
|
+
and not abandon_commit
|
|
669
|
+
):
|
|
670
|
+
milestone, target = root[len(ARCHIVE_PREFIX):], default_branch()
|
|
671
|
+
found.append(
|
|
672
|
+
f"{label}: a new archive requires a ship commit, a "
|
|
673
|
+
"milestone-abandon commit, or an integration merge with the "
|
|
674
|
+
f"subject 'integrate: {milestone} — merge "
|
|
675
|
+
f"gsd-path/{milestone} into {target}'"
|
|
676
|
+
)
|
|
677
|
+
if is_ship_commit(subject):
|
|
678
|
+
for code, old, new in entries:
|
|
679
|
+
for path in (old, new):
|
|
680
|
+
if path and not path.startswith(".project/"):
|
|
681
|
+
found.append(
|
|
682
|
+
f"{code} {path}: a ship commit may only touch .project/; "
|
|
683
|
+
f"unstage it with git restore --staged -- {path}"
|
|
684
|
+
)
|
|
685
|
+
return found
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def commit_message(argv):
|
|
689
|
+
if len(argv) <= 1:
|
|
690
|
+
return "", ""
|
|
691
|
+
if argv[1] == "pre-commit":
|
|
692
|
+
# At pre-commit time .git/COMMIT_EDITMSG still holds the PREVIOUS
|
|
693
|
+
# commit's message, so subject rules cannot be enforced here; the
|
|
694
|
+
# commit-msg hook enforces them with the real message.
|
|
695
|
+
return None, None
|
|
696
|
+
if argv[1] == "commit-msg" and len(argv) > 2:
|
|
697
|
+
return message_of(argv[2])
|
|
698
|
+
return message_of(argv[1])
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def report(found, action):
|
|
702
|
+
if not found:
|
|
703
|
+
return 0
|
|
704
|
+
print(f"gsd-path guard blocked the {action}:", file=sys.stderr)
|
|
705
|
+
for violation in found:
|
|
706
|
+
print(f" {violation}", file=sys.stderr)
|
|
707
|
+
return 1
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def main(argv):
|
|
711
|
+
if len(argv) > 1 and argv[1] == "closed-milestone":
|
|
712
|
+
reason = closed_milestone_reason()
|
|
713
|
+
return report([reason] if reason else [], "mutation")
|
|
714
|
+
if len(argv) > 1 and argv[1] == "pre-push":
|
|
715
|
+
try:
|
|
716
|
+
found = pre_push_violations(sys.stdin.read().splitlines())
|
|
717
|
+
except Exception as error:
|
|
718
|
+
print(f"gsd-path guard: inspection failed; push blocked ({error})", file=sys.stderr)
|
|
719
|
+
return 1
|
|
720
|
+
return report(found, "push")
|
|
721
|
+
try:
|
|
722
|
+
reason = closed_milestone_reason()
|
|
723
|
+
if reason:
|
|
724
|
+
return report([reason], "commit")
|
|
725
|
+
entries = staged_entries()
|
|
726
|
+
subject, body = commit_message(argv)
|
|
727
|
+
existing = committed_archive_roots()
|
|
728
|
+
destinations = {
|
|
729
|
+
root
|
|
730
|
+
for entry in entries
|
|
731
|
+
if (root := archive_root(destination_path(entry) or "")) is not None
|
|
732
|
+
}
|
|
733
|
+
new_archives = destinations - existing
|
|
734
|
+
state = staged_frontmatter() if new_archives else None
|
|
735
|
+
current_archive = staged_archive_target(state) if new_archives else None
|
|
736
|
+
abandon = (
|
|
737
|
+
staged_abandon_target(new_archives, state)
|
|
738
|
+
if new_archives and current_archive is None
|
|
739
|
+
else None
|
|
740
|
+
)
|
|
741
|
+
if abandon is not None:
|
|
742
|
+
current_archive = abandon[0]
|
|
743
|
+
landing = product_commit_violations(entries, subject, body)
|
|
744
|
+
except Exception as error:
|
|
745
|
+
print(
|
|
746
|
+
f"gsd-path guard: inspection failed; commit blocked ({error}); "
|
|
747
|
+
"run git from inside the repository worktree, then retry the commit",
|
|
748
|
+
file=sys.stderr,
|
|
749
|
+
)
|
|
750
|
+
return 1
|
|
751
|
+
integration_merge = is_integration_merge(subject) if new_archives else False
|
|
752
|
+
abandon_commit = is_abandon_commit(subject, abandon) if new_archives else False
|
|
753
|
+
found = violations(
|
|
754
|
+
entries,
|
|
755
|
+
subject,
|
|
756
|
+
existing,
|
|
757
|
+
current_archive,
|
|
758
|
+
integration_merge,
|
|
759
|
+
abandon_commit,
|
|
760
|
+
)
|
|
761
|
+
found.extend(ship_contract_violations(entries, subject, body, new_archives, state))
|
|
762
|
+
found.extend(abandon_contract_violations(subject, body, abandon))
|
|
763
|
+
found.extend(landing)
|
|
764
|
+
return report(found, "commit")
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
if __name__ == "__main__":
|
|
768
|
+
raise SystemExit(main(sys.argv))
|