@opengsd/gsd-path 1.4.0 → 1.6.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/CHANGELOG.md +460 -341
- package/DOCS.md +16 -5
- package/FULL.md +67 -6
- package/HOOKS.md +158 -10
- package/MIGRATE.md +12 -0
- package/QUICK.md +4 -2
- package/README.md +136 -116
- package/RUNTIME.md +24 -6
- package/UPDATE.md +39 -3
- package/WORKFLOW.md +80 -4
- package/package.json +5 -2
- package/platforms/shared-agents/dispatch.md +13 -0
- package/scripts/_common.py +256 -2
- package/scripts/archive_milestone.py +14 -13
- package/scripts/build_recovery.py +155 -58
- package/scripts/build_state.py +274 -8
- package/scripts/check_docs_audit.py +77 -4
- package/scripts/check_handoffs.py +635 -57
- package/scripts/check_task_briefs.py +293 -32
- package/scripts/detect_project.py +260 -20
- package/scripts/discussion_records.py +9 -1
- package/scripts/discussion_validate.py +93 -35
- package/scripts/dispatch_driver.py +549 -53
- package/scripts/git_guard.py +27 -8
- package/scripts/guard_hook.py +2630 -210
- package/scripts/install.mjs +99 -11
- package/scripts/install.py +231 -46
- package/scripts/integration.py +244 -65
- package/scripts/isolation.py +539 -70
- package/scripts/loop_run.py +16 -4
- package/scripts/members.py +77 -32
- package/scripts/migrate_core.py +2 -0
- package/scripts/model_policy.py +5 -1
- package/scripts/path_config.py +3 -3
- package/scripts/pipeline_diagnose.py +17 -2
- package/scripts/pipeline_git.py +131 -42
- package/scripts/pipeline_state.py +139 -39
- package/scripts/pipeline_undo.py +118 -16
- package/scripts/promote_lookahead.py +3 -3
- package/scripts/review_findings.py +180 -44
- package/scripts/roadmap.py +9 -1
- package/scripts/runtime_store.py +21 -2
- package/scripts/skill-resources.json +9 -2
- package/scripts/state_checkpoint.py +39 -36
- package/scripts/state_promote.py +15 -4
- package/scripts/status_runtime.py +22 -4
- package/scripts/task_context.py +14 -3
- package/scripts/wizard.mjs +1 -0
- package/scripts/workflow_run.py +14 -4
- package/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path/DEFINE.md +34 -5
- package/skills/gsd-path/PLAN.md +14 -3
- package/skills/gsd-path/RESEARCH.md +11 -0
- package/skills/gsd-path/ROADMAP.md +16 -6
- package/skills/gsd-path/SHIP.md +32 -5
- package/skills/gsd-path/SKILL.md +52 -24
- package/skills/gsd-path/references/build-native.md +112 -10
- package/skills/gsd-path/references/build-recovery.md +13 -0
- package/skills/gsd-path/references/coder.md +7 -3
- package/skills/gsd-path/references/dispatch.md +13 -0
- package/skills/gsd-path/references/docs-auditor.md +5 -0
- package/skills/gsd-path/references/operating-rules.md +11 -3
- package/skills/gsd-path/references/planner.md +26 -4
- package/skills/gsd-path/references/reviewer-final.md +20 -2
- package/skills/gsd-path/references/reviewer-wave.md +56 -12
- package/skills/gsd-path/references/reviewer.md +3 -1
- package/skills/gsd-path/references/runtime-selection.md +9 -0
- package/skills/gsd-path/references/spec-probes.md +150 -0
- package/skills/gsd-path/scripts/_common.py +256 -2
- package/skills/gsd-path/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path/scripts/build_recovery.py +155 -58
- package/skills/gsd-path/scripts/build_state.py +274 -8
- package/skills/gsd-path/scripts/check_docs_audit.py +77 -4
- package/skills/gsd-path/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path/scripts/detect_project.py +260 -20
- package/skills/gsd-path/scripts/discussion_records.py +9 -1
- package/skills/gsd-path/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path/scripts/dispatch_driver.py +549 -53
- package/skills/gsd-path/scripts/git_guard.py +27 -8
- package/skills/gsd-path/scripts/integration.py +244 -65
- package/skills/gsd-path/scripts/isolation.py +539 -70
- package/skills/gsd-path/scripts/loop_run.py +16 -4
- package/skills/gsd-path/scripts/members.py +77 -32
- package/skills/gsd-path/scripts/model_policy.py +5 -1
- package/skills/gsd-path/scripts/path_config.py +3 -3
- package/skills/gsd-path/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path/scripts/review_findings.py +180 -44
- package/skills/gsd-path/scripts/roadmap.py +9 -1
- package/skills/gsd-path/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path/scripts/state_promote.py +15 -4
- package/skills/gsd-path/scripts/status_runtime.py +22 -4
- package/skills/gsd-path/scripts/task_context.py +14 -3
- package/skills/gsd-path/scripts/workflow_run.py +14 -4
- package/skills/gsd-path/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path/templates/docs-audit.md +5 -1
- package/skills/gsd-path/templates/final-review.md +11 -2
- package/skills/gsd-path/templates/intent.md +30 -0
- package/skills/gsd-path/templates/patch-findings.md +3 -3
- package/skills/gsd-path/templates/plan.md +17 -2
- package/skills/gsd-path/templates/research-handoff.md +14 -3
- package/skills/gsd-path/templates/state.md +2 -1
- package/skills/gsd-path/templates/task.md +1 -0
- package/skills/gsd-path/templates/wave-review.md +28 -9
- package/skills/gsd-path-build/references/build-native.md +112 -10
- package/skills/gsd-path-build/references/build-recovery.md +13 -0
- package/skills/gsd-path-build/references/coder.md +7 -3
- package/skills/gsd-path-build/references/dispatch.md +13 -0
- package/skills/gsd-path-build/references/operating-rules.md +11 -3
- package/skills/gsd-path-build/references/reviewer-final.md +20 -2
- package/skills/gsd-path-build/references/reviewer-wave.md +56 -12
- package/skills/gsd-path-build/references/reviewer.md +3 -1
- package/skills/gsd-path-build/references/runtime-selection.md +9 -0
- package/skills/gsd-path-build/references/spec-probes.md +150 -0
- package/skills/gsd-path-build/scripts/_common.py +256 -2
- package/skills/gsd-path-build/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-build/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-build/scripts/build_state.py +274 -8
- package/skills/gsd-path-build/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-build/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-build/scripts/detect_project.py +260 -20
- package/skills/gsd-path-build/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-build/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-build/scripts/dispatch_driver.py +549 -53
- package/skills/gsd-path-build/scripts/integration.py +244 -65
- package/skills/gsd-path-build/scripts/isolation.py +539 -70
- package/skills/gsd-path-build/scripts/loop_run.py +16 -4
- package/skills/gsd-path-build/scripts/members.py +77 -32
- package/skills/gsd-path-build/scripts/model_policy.py +5 -1
- package/skills/gsd-path-build/scripts/path_config.py +3 -3
- package/skills/gsd-path-build/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-build/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-build/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-build/scripts/review_findings.py +180 -44
- package/skills/gsd-path-build/scripts/roadmap.py +9 -1
- package/skills/gsd-path-build/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-build/scripts/state_promote.py +15 -4
- package/skills/gsd-path-build/scripts/task_context.py +14 -3
- package/skills/gsd-path-build/scripts/workflow_run.py +14 -4
- package/skills/gsd-path-build/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-build/templates/task.md +1 -0
- package/skills/gsd-path-build/templates/wave-review.md +28 -9
- package/skills/gsd-path-decide/references/dispatch.md +13 -0
- package/skills/gsd-path-decide/references/operating-rules.md +11 -3
- package/skills/gsd-path-decide/references/runtime-selection.md +9 -0
- package/skills/gsd-path-decide/scripts/_common.py +256 -2
- package/skills/gsd-path-decide/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-decide/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-decide/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-decide/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-decide/scripts/detect_project.py +260 -20
- package/skills/gsd-path-decide/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-decide/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-decide/scripts/integration.py +244 -65
- package/skills/gsd-path-decide/scripts/isolation.py +539 -70
- package/skills/gsd-path-decide/scripts/loop_run.py +16 -4
- package/skills/gsd-path-decide/scripts/members.py +77 -32
- package/skills/gsd-path-decide/scripts/model_policy.py +5 -1
- package/skills/gsd-path-decide/scripts/path_config.py +3 -3
- package/skills/gsd-path-decide/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-decide/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-decide/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-decide/scripts/roadmap.py +9 -1
- package/skills/gsd-path-decide/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-decide/scripts/state_promote.py +15 -4
- package/skills/gsd-path-decide/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-define/SKILL.md +34 -5
- package/skills/gsd-path-define/references/build-recovery.md +13 -0
- package/skills/gsd-path-define/references/operating-rules.md +11 -3
- package/skills/gsd-path-define/references/runtime-selection.md +9 -0
- package/skills/gsd-path-define/references/spec-probes.md +150 -0
- package/skills/gsd-path-define/scripts/_common.py +256 -2
- package/skills/gsd-path-define/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-define/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-define/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-define/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-define/scripts/detect_project.py +260 -20
- package/skills/gsd-path-define/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-define/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-define/scripts/integration.py +244 -65
- package/skills/gsd-path-define/scripts/isolation.py +539 -70
- package/skills/gsd-path-define/scripts/loop_run.py +16 -4
- package/skills/gsd-path-define/scripts/members.py +77 -32
- package/skills/gsd-path-define/scripts/model_policy.py +5 -1
- package/skills/gsd-path-define/scripts/path_config.py +3 -3
- package/skills/gsd-path-define/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-define/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-define/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-define/scripts/roadmap.py +9 -1
- package/skills/gsd-path-define/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-define/scripts/state_promote.py +15 -4
- package/skills/gsd-path-define/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-define/templates/intent.md +30 -0
- package/skills/gsd-path-define/templates/state.md +2 -1
- package/skills/gsd-path-discuss/references/operating-rules.md +11 -3
- package/skills/gsd-path-discuss/references/runtime-selection.md +9 -0
- package/skills/gsd-path-discuss/scripts/_common.py +256 -2
- package/skills/gsd-path-discuss/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-discuss/scripts/loop_run.py +16 -4
- package/skills/gsd-path-discuss/scripts/model_policy.py +5 -1
- package/skills/gsd-path-discuss/scripts/path_config.py +3 -3
- package/skills/gsd-path-discuss/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-docs-audit/references/dispatch.md +13 -0
- package/skills/gsd-path-docs-audit/references/docs-auditor.md +5 -0
- package/skills/gsd-path-docs-audit/references/operating-rules.md +11 -3
- package/skills/gsd-path-docs-audit/references/runtime-selection.md +9 -0
- package/skills/gsd-path-docs-audit/scripts/_common.py +256 -2
- package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-docs-audit/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +77 -4
- package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-docs-audit/scripts/detect_project.py +260 -20
- package/skills/gsd-path-docs-audit/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-docs-audit/scripts/integration.py +244 -65
- package/skills/gsd-path-docs-audit/scripts/isolation.py +539 -70
- package/skills/gsd-path-docs-audit/scripts/loop_run.py +16 -4
- package/skills/gsd-path-docs-audit/scripts/members.py +77 -32
- package/skills/gsd-path-docs-audit/scripts/model_policy.py +5 -1
- package/skills/gsd-path-docs-audit/scripts/path_config.py +3 -3
- package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-docs-audit/scripts/roadmap.py +9 -1
- package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-docs-audit/scripts/state_promote.py +15 -4
- package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-docs-audit/templates/docs-audit.md +5 -1
- package/skills/gsd-path-forensics/references/operating-rules.md +11 -3
- package/skills/gsd-path-forensics/references/runtime-selection.md +9 -0
- package/skills/gsd-path-forensics/scripts/_common.py +256 -2
- package/skills/gsd-path-forensics/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-forensics/scripts/loop_run.py +16 -4
- package/skills/gsd-path-forensics/scripts/model_policy.py +5 -1
- package/skills/gsd-path-forensics/scripts/path_config.py +3 -3
- package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +17 -2
- package/skills/gsd-path-forensics/scripts/pipeline_undo.py +118 -16
- package/skills/gsd-path-forensics/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-inspect/references/dispatch.md +13 -0
- package/skills/gsd-path-inspect/references/docs-auditor.md +5 -0
- package/skills/gsd-path-inspect/references/operating-rules.md +11 -3
- package/skills/gsd-path-inspect/references/runtime-selection.md +9 -0
- package/skills/gsd-path-inspect/scripts/_common.py +256 -2
- package/skills/gsd-path-inspect/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-inspect/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-inspect/scripts/check_docs_audit.py +77 -4
- package/skills/gsd-path-inspect/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-inspect/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-inspect/scripts/detect_project.py +260 -20
- package/skills/gsd-path-inspect/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-inspect/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-inspect/scripts/integration.py +244 -65
- package/skills/gsd-path-inspect/scripts/isolation.py +539 -70
- package/skills/gsd-path-inspect/scripts/loop_run.py +16 -4
- package/skills/gsd-path-inspect/scripts/members.py +77 -32
- package/skills/gsd-path-inspect/scripts/model_policy.py +5 -1
- package/skills/gsd-path-inspect/scripts/path_config.py +3 -3
- package/skills/gsd-path-inspect/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-inspect/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-inspect/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-inspect/scripts/roadmap.py +9 -1
- package/skills/gsd-path-inspect/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-inspect/scripts/state_promote.py +15 -4
- package/skills/gsd-path-inspect/scripts/workflow_run.py +14 -4
- package/skills/gsd-path-inspect/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-inspect/templates/docs-audit.md +5 -1
- package/skills/gsd-path-inspect/templates/state.md +2 -1
- package/skills/gsd-path-loop/references/operating-rules.md +11 -3
- package/skills/gsd-path-loop/references/runtime-selection.md +9 -0
- package/skills/gsd-path-loop/scripts/_common.py +256 -2
- package/skills/gsd-path-loop/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-loop/scripts/loop_run.py +16 -4
- package/skills/gsd-path-loop/scripts/model_policy.py +5 -1
- package/skills/gsd-path-loop/scripts/path_config.py +3 -3
- package/skills/gsd-path-loop/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-migrate/SKILL.md +8 -0
- package/skills/gsd-path-migrate/references/operating-rules.md +11 -3
- package/skills/gsd-path-migrate/references/runtime-selection.md +9 -0
- package/skills/gsd-path-migrate/scripts/_common.py +256 -2
- package/skills/gsd-path-migrate/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-migrate/scripts/loop_run.py +16 -4
- package/skills/gsd-path-migrate/scripts/migrate_core.py +2 -0
- package/skills/gsd-path-migrate/scripts/model_policy.py +5 -1
- package/skills/gsd-path-migrate/scripts/path_config.py +3 -3
- package/skills/gsd-path-plan/SKILL.md +14 -3
- package/skills/gsd-path-plan/references/build-recovery.md +13 -0
- package/skills/gsd-path-plan/references/dispatch.md +13 -0
- package/skills/gsd-path-plan/references/operating-rules.md +11 -3
- package/skills/gsd-path-plan/references/planner.md +26 -4
- package/skills/gsd-path-plan/references/runtime-selection.md +9 -0
- package/skills/gsd-path-plan/references/spec-probes.md +150 -0
- package/skills/gsd-path-plan/scripts/_common.py +256 -2
- package/skills/gsd-path-plan/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-plan/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-plan/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-plan/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-plan/scripts/detect_project.py +260 -20
- package/skills/gsd-path-plan/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-plan/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-plan/scripts/integration.py +244 -65
- package/skills/gsd-path-plan/scripts/isolation.py +539 -70
- package/skills/gsd-path-plan/scripts/loop_run.py +16 -4
- package/skills/gsd-path-plan/scripts/members.py +77 -32
- package/skills/gsd-path-plan/scripts/model_policy.py +5 -1
- package/skills/gsd-path-plan/scripts/path_config.py +3 -3
- package/skills/gsd-path-plan/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-plan/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-plan/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-plan/scripts/roadmap.py +9 -1
- package/skills/gsd-path-plan/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-plan/scripts/state_promote.py +15 -4
- package/skills/gsd-path-plan/scripts/workflow_run.py +14 -4
- package/skills/gsd-path-plan/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-plan/templates/patch-findings.md +3 -3
- package/skills/gsd-path-plan/templates/plan.md +17 -2
- package/skills/gsd-path-plan/templates/task.md +1 -0
- package/skills/gsd-path-research/SKILL.md +11 -0
- package/skills/gsd-path-research/references/dispatch.md +13 -0
- package/skills/gsd-path-research/references/operating-rules.md +11 -3
- package/skills/gsd-path-research/references/runtime-selection.md +9 -0
- package/skills/gsd-path-research/scripts/_common.py +256 -2
- package/skills/gsd-path-research/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-research/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-research/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-research/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-research/scripts/detect_project.py +260 -20
- package/skills/gsd-path-research/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-research/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-research/scripts/integration.py +244 -65
- package/skills/gsd-path-research/scripts/isolation.py +539 -70
- package/skills/gsd-path-research/scripts/loop_run.py +16 -4
- package/skills/gsd-path-research/scripts/members.py +77 -32
- package/skills/gsd-path-research/scripts/model_policy.py +5 -1
- package/skills/gsd-path-research/scripts/path_config.py +3 -3
- package/skills/gsd-path-research/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-research/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-research/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-research/scripts/roadmap.py +9 -1
- package/skills/gsd-path-research/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-research/scripts/state_promote.py +15 -4
- package/skills/gsd-path-research/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-research/templates/research-handoff.md +14 -3
- package/skills/gsd-path-roadmap/SKILL.md +16 -6
- package/skills/gsd-path-roadmap/references/dispatch.md +13 -0
- package/skills/gsd-path-roadmap/references/operating-rules.md +11 -3
- package/skills/gsd-path-roadmap/references/runtime-selection.md +9 -0
- package/skills/gsd-path-roadmap/scripts/_common.py +256 -2
- package/skills/gsd-path-roadmap/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-roadmap/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-roadmap/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-roadmap/scripts/detect_project.py +260 -20
- package/skills/gsd-path-roadmap/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-roadmap/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-roadmap/scripts/integration.py +244 -65
- package/skills/gsd-path-roadmap/scripts/isolation.py +539 -70
- package/skills/gsd-path-roadmap/scripts/loop_run.py +16 -4
- package/skills/gsd-path-roadmap/scripts/members.py +77 -32
- package/skills/gsd-path-roadmap/scripts/model_policy.py +5 -1
- package/skills/gsd-path-roadmap/scripts/path_config.py +3 -3
- package/skills/gsd-path-roadmap/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-roadmap/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-roadmap/scripts/roadmap.py +9 -1
- package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-roadmap/scripts/state_promote.py +15 -4
- package/skills/gsd-path-roadmap/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-ship/SKILL.md +32 -5
- package/skills/gsd-path-ship/references/dispatch.md +13 -0
- package/skills/gsd-path-ship/references/operating-rules.md +11 -3
- package/skills/gsd-path-ship/references/reviewer-final.md +20 -2
- package/skills/gsd-path-ship/references/reviewer-wave.md +56 -12
- package/skills/gsd-path-ship/references/reviewer.md +3 -1
- package/skills/gsd-path-ship/references/runtime-selection.md +9 -0
- package/skills/gsd-path-ship/references/spec-probes.md +150 -0
- package/skills/gsd-path-ship/scripts/_common.py +256 -2
- package/skills/gsd-path-ship/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-ship/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-ship/scripts/build_state.py +274 -8
- package/skills/gsd-path-ship/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-ship/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-ship/scripts/detect_project.py +260 -20
- package/skills/gsd-path-ship/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-ship/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-ship/scripts/integration.py +244 -65
- package/skills/gsd-path-ship/scripts/isolation.py +539 -70
- package/skills/gsd-path-ship/scripts/loop_run.py +16 -4
- package/skills/gsd-path-ship/scripts/members.py +77 -32
- package/skills/gsd-path-ship/scripts/model_policy.py +5 -1
- package/skills/gsd-path-ship/scripts/path_config.py +3 -3
- package/skills/gsd-path-ship/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-ship/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-ship/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-ship/scripts/roadmap.py +9 -1
- package/skills/gsd-path-ship/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-ship/scripts/state_promote.py +15 -4
- package/skills/gsd-path-ship/scripts/workflow_run.py +14 -4
- package/skills/gsd-path-ship/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-ship/templates/final-review.md +11 -2
- package/skills/gsd-path-ship/templates/patch-findings.md +3 -3
- package/skills/gsd-path-ship/templates/wave-review.md +28 -9
- package/skills/gsd-path-undo/references/operating-rules.md +11 -3
- package/skills/gsd-path-undo/references/runtime-selection.md +9 -0
- package/skills/gsd-path-undo/scripts/_common.py +256 -2
- package/skills/gsd-path-undo/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-undo/scripts/loop_run.py +16 -4
- package/skills/gsd-path-undo/scripts/model_policy.py +5 -1
- package/skills/gsd-path-undo/scripts/path_config.py +3 -3
- package/skills/gsd-path-undo/scripts/pipeline_undo.py +118 -16
- package/skills/gsd-path-undo/scripts/worktree_paths.py +54 -2
- package/skills/path/DEFINE.md +34 -5
- package/skills/path/PLAN.md +14 -3
- package/skills/path/RESEARCH.md +11 -0
- package/skills/path/ROADMAP.md +16 -6
- package/skills/path/SHIP.md +32 -5
- package/skills/path/SKILL.md +52 -24
- package/skills/path/references/build-native.md +112 -10
- package/skills/path/references/build-recovery.md +13 -0
- package/skills/path/references/coder.md +7 -3
- package/skills/path/references/dispatch.md +13 -0
- package/skills/path/references/docs-auditor.md +5 -0
- package/skills/path/references/operating-rules.md +11 -3
- package/skills/path/references/planner.md +26 -4
- package/skills/path/references/reviewer-final.md +20 -2
- package/skills/path/references/reviewer-wave.md +56 -12
- package/skills/path/references/reviewer.md +3 -1
- package/skills/path/references/runtime-selection.md +9 -0
- package/skills/path/references/spec-probes.md +150 -0
- package/skills/path/scripts/_common.py +256 -2
- package/skills/path/scripts/archive_milestone.py +14 -13
- package/skills/path/scripts/build_recovery.py +155 -58
- package/skills/path/scripts/build_state.py +274 -8
- package/skills/path/scripts/check_docs_audit.py +77 -4
- package/skills/path/scripts/check_handoffs.py +635 -57
- package/skills/path/scripts/check_task_briefs.py +293 -32
- package/skills/path/scripts/detect_project.py +260 -20
- package/skills/path/scripts/discussion_records.py +9 -1
- package/skills/path/scripts/discussion_validate.py +93 -35
- package/skills/path/scripts/dispatch_driver.py +549 -53
- package/skills/path/scripts/git_guard.py +27 -8
- package/skills/path/scripts/integration.py +244 -65
- package/skills/path/scripts/isolation.py +539 -70
- package/skills/path/scripts/loop_run.py +16 -4
- package/skills/path/scripts/members.py +77 -32
- package/skills/path/scripts/model_policy.py +5 -1
- package/skills/path/scripts/path_config.py +3 -3
- package/skills/path/scripts/pipeline_git.py +131 -42
- package/skills/path/scripts/pipeline_state.py +139 -39
- package/skills/path/scripts/promote_lookahead.py +3 -3
- package/skills/path/scripts/review_findings.py +180 -44
- package/skills/path/scripts/roadmap.py +9 -1
- package/skills/path/scripts/state_checkpoint.py +39 -36
- package/skills/path/scripts/state_promote.py +15 -4
- package/skills/path/scripts/status_runtime.py +22 -4
- package/skills/path/scripts/task_context.py +14 -3
- package/skills/path/scripts/workflow_run.py +14 -4
- package/skills/path/scripts/worktree_paths.py +54 -2
- package/skills/path/templates/docs-audit.md +5 -1
- package/skills/path/templates/final-review.md +11 -2
- package/skills/path/templates/intent.md +30 -0
- package/skills/path/templates/patch-findings.md +3 -3
- package/skills/path/templates/plan.md +17 -2
- package/skills/path/templates/research-handoff.md +14 -3
- package/skills/path/templates/state.md +2 -1
- package/skills/path/templates/task.md +1 -0
- package/skills/path/templates/wave-review.md +28 -9
- 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-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-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
|
@@ -514,16 +514,28 @@ def run_shell(command: str, timeout: float) -> tuple[Optional[int], str]:
|
|
|
514
514
|
"""Run one spec command; a hung command counts as failed (exit None).
|
|
515
515
|
|
|
516
516
|
POSIX runs /bin/sh as shell=True did; Windows runs Git for Windows bash, not
|
|
517
|
-
cmd.exe. A timeout kills the whole process group
|
|
518
|
-
|
|
517
|
+
cmd.exe. A timeout kills the whole process group when possible; if output
|
|
518
|
+
pipes stay open, drain them with a short grace timeout and then close them.
|
|
519
519
|
"""
|
|
520
520
|
argv = _common.bash_argv(command) if os.name == "nt" else ["/bin/sh", "-c", command]
|
|
521
521
|
process = _common.popen_group(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
522
|
+
stdout = b""
|
|
523
|
+
stderr = b""
|
|
522
524
|
try:
|
|
523
525
|
stdout, stderr = process.communicate(timeout=timeout)
|
|
524
|
-
except subprocess.TimeoutExpired:
|
|
526
|
+
except subprocess.TimeoutExpired as expired:
|
|
525
527
|
_common.kill_tree(process)
|
|
526
|
-
|
|
528
|
+
try:
|
|
529
|
+
stdout, stderr = process.communicate(timeout=5)
|
|
530
|
+
except subprocess.TimeoutExpired as drained:
|
|
531
|
+
stdout = drained.stdout or expired.stdout or b""
|
|
532
|
+
stderr = drained.stderr or expired.stderr or b""
|
|
533
|
+
if process.stdout:
|
|
534
|
+
process.stdout.close()
|
|
535
|
+
if process.stderr:
|
|
536
|
+
process.stderr.close()
|
|
537
|
+
process.kill()
|
|
538
|
+
process.wait(timeout=5)
|
|
527
539
|
output = stdout.decode("utf-8", errors="replace") + stderr.decode("utf-8", errors="replace")
|
|
528
540
|
return None, f"{output}\ntimed out at the admitted wall-clock deadline"
|
|
529
541
|
return process.returncode, (stdout + stderr).decode("utf-8", errors="replace")
|
|
@@ -35,6 +35,8 @@ MARKER_SCHEMA = "gsd-path/member/v1"
|
|
|
35
35
|
MARKER_KEYS = {"schema", "coordinator", "project", "name"}
|
|
36
36
|
NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$")
|
|
37
37
|
FIELDS = ("Checkout", "Remote", "Integration")
|
|
38
|
+
# Optional: `add` records it one time; a row without it means main.
|
|
39
|
+
DEFAULT_BRANCH_FIELD = "Default branch"
|
|
38
40
|
INTEGRATIONS = ("default", "direct", "pull-request")
|
|
39
41
|
GITHUB_REMOTE_RE = re.compile(
|
|
40
42
|
r"(?:https://github\.com/|git@github\.com:|ssh://git@github\.com/)([^/\s]+)/([^/\s]+)"
|
|
@@ -221,9 +223,10 @@ def read_members(coordinator: Path) -> list[dict[str, str]]:
|
|
|
221
223
|
if not line.strip():
|
|
222
224
|
continue
|
|
223
225
|
key, separator, value = line.partition(":")
|
|
224
|
-
|
|
226
|
+
field = key.lower().replace(" ", "_")
|
|
227
|
+
if not separator or key not in (*FIELDS, DEFAULT_BRANCH_FIELD) or field in current:
|
|
225
228
|
raise MembersError(f"MEMBERS.md line {number}: unexpected line {line!r}")
|
|
226
|
-
current[
|
|
229
|
+
current[field] = value.strip()
|
|
227
230
|
if not preamble or preamble[0] != "# Members":
|
|
228
231
|
raise MembersError("MEMBERS.md format error: expected # Members")
|
|
229
232
|
rest = preamble[1:]
|
|
@@ -247,16 +250,37 @@ def read_members(coordinator: Path) -> list[dict[str, str]]:
|
|
|
247
250
|
)
|
|
248
251
|
if not Path(member["checkout"]).is_absolute():
|
|
249
252
|
raise MembersError(f"MEMBERS.md member {member['name']} Checkout must be absolute")
|
|
253
|
+
if not _common.valid_default_branch(default_branch(member)):
|
|
254
|
+
raise MembersError(
|
|
255
|
+
f"MEMBERS.md member {member['name']} has invalid Default branch: {member['default_branch']}"
|
|
256
|
+
)
|
|
250
257
|
return members
|
|
251
258
|
|
|
252
259
|
|
|
260
|
+
def default_branch(member: dict[str, str]) -> str:
|
|
261
|
+
"""The default branch a MEMBERS.md row records; a row without the field means main."""
|
|
262
|
+
return member.get("default_branch", "main")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def member_default_branch(coordinator: Path, name: str) -> str:
|
|
266
|
+
"""The default branch MEMBERS.md records for the member `name`."""
|
|
267
|
+
for member in read_members(coordinator):
|
|
268
|
+
if member["name"] == name:
|
|
269
|
+
return default_branch(member)
|
|
270
|
+
raise MembersError(f"member {name} is not in MEMBERS.md")
|
|
271
|
+
|
|
272
|
+
|
|
253
273
|
def check_member(
|
|
254
274
|
coordinator: Path,
|
|
255
275
|
coordinator_name: str,
|
|
256
276
|
checkout: Path,
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
277
|
+
recorded: Optional[dict[str, str]] = None,
|
|
278
|
+
refuse_reserved_refs: bool = True,
|
|
279
|
+
) -> tuple[str, str]:
|
|
280
|
+
"""Refuse a checkout that cannot join; return its origin URL and default branch.
|
|
281
|
+
|
|
282
|
+
`recorded` is the member's MEMBERS.md row. Without it (`add`), the default
|
|
283
|
+
branch is the local origin/HEAD, read this one time; an unset ref means main."""
|
|
260
284
|
if checkout.is_symlink() or not checkout.is_dir():
|
|
261
285
|
raise MembersError(f"member checkout is not a real directory: {checkout}")
|
|
262
286
|
checkout = checkout.resolve()
|
|
@@ -278,12 +302,21 @@ def check_member(
|
|
|
278
302
|
remote = _git(checkout, "config", "--get", "remote.origin.url")
|
|
279
303
|
if not GITHUB_REMOTE_RE.fullmatch(remote):
|
|
280
304
|
raise MembersError(f"member requires a GitHub.com origin: {remote}")
|
|
281
|
-
if
|
|
282
|
-
raise MembersError(f"member origin changed: {
|
|
283
|
-
|
|
284
|
-
if
|
|
285
|
-
|
|
286
|
-
|
|
305
|
+
if recorded is not None and remote != recorded["remote"]:
|
|
306
|
+
raise MembersError(f"member origin changed: {recorded['remote']} -> {remote}")
|
|
307
|
+
live = _common.origin_default_branch(checkout)
|
|
308
|
+
default = live if recorded is None else default_branch(recorded)
|
|
309
|
+
if live != default:
|
|
310
|
+
raise MembersError(
|
|
311
|
+
f"member remote default must be {default}, got {live}: {checkout}; "
|
|
312
|
+
"run `git remote set-head origin --auto` when the local origin/HEAD is stale"
|
|
313
|
+
)
|
|
314
|
+
if not _common.valid_default_branch(default):
|
|
315
|
+
raise MembersError(
|
|
316
|
+
f"member default branch {default!r} cannot be recorded in MEMBERS.md: it is a "
|
|
317
|
+
"GSD Path reserved name or it has a character that the file cannot hold"
|
|
318
|
+
)
|
|
319
|
+
require_origin_branch(checkout, default)
|
|
287
320
|
member_state = checkout / ".project" / "STATE.md"
|
|
288
321
|
if member_state.exists() or member_state.is_symlink():
|
|
289
322
|
if member_state.is_symlink() or not member_state.is_file():
|
|
@@ -294,14 +327,15 @@ def check_member(
|
|
|
294
327
|
raise MembersError(f"member STATE.md is unreadable: {error}") from error
|
|
295
328
|
if state.milestone is not None and (state.phase, state.status) != ("shipped", "done"):
|
|
296
329
|
raise MembersError(f"member has an active milestone: {state.milestone}")
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
330
|
+
if refuse_reserved_refs:
|
|
331
|
+
prefixes = member_ref_prefixes(coordinator_name) + tuple(
|
|
332
|
+
"refs/remotes/origin/" + branch.format(coordinator_name) for branch in MEMBER_BRANCH_FORMATS
|
|
333
|
+
)
|
|
334
|
+
refs = _git(checkout, "for-each-ref", "--format=%(refname)").splitlines()
|
|
335
|
+
colliding = [ref for ref in refs if ref.startswith(prefixes)]
|
|
336
|
+
if colliding:
|
|
337
|
+
raise MembersError("member refs collide with coordinator names: " + ", ".join(colliding))
|
|
338
|
+
return remote, default
|
|
305
339
|
|
|
306
340
|
|
|
307
341
|
MEMBER_CLOSE_SCHEMA = "gsd-path/member-close/v1"
|
|
@@ -336,12 +370,14 @@ def write_member_close(common: Path, archive_name: str, rows: list[dict]) -> Non
|
|
|
336
370
|
_common.atomic_write(path, json.dumps({"schema": MEMBER_CLOSE_SCHEMA, "members": rows}, indent=2) + "\n")
|
|
337
371
|
|
|
338
372
|
|
|
339
|
-
def
|
|
373
|
+
def require_origin_branch(checkout: Path, branch: str) -> str:
|
|
374
|
+
"""The commit of the member's fetched default branch `branch`."""
|
|
340
375
|
baseline = _common.run_git(
|
|
341
|
-
checkout, "rev-parse", "--verify", "--quiet", "refs/remotes/origin/
|
|
376
|
+
checkout, "rev-parse", "--verify", "--quiet", f"refs/remotes/origin/{branch}^{{commit}}"
|
|
342
377
|
)
|
|
343
378
|
if baseline.returncode != 0:
|
|
344
|
-
raise MembersError("member requires refs/remotes/origin/
|
|
379
|
+
raise MembersError(f"member requires refs/remotes/origin/{branch}; run git fetch origin")
|
|
380
|
+
return baseline.stdout.strip()
|
|
345
381
|
|
|
346
382
|
|
|
347
383
|
def member_ref_prefixes(project: str) -> tuple[str, ...]:
|
|
@@ -403,7 +439,8 @@ def _task_members(coordinator: Path) -> set[str]:
|
|
|
403
439
|
|
|
404
440
|
def lock_build_members(coordinator: Path) -> Optional[list[dict[str, str]]]:
|
|
405
441
|
"""At build start, lock the members tasks name (MEMBERS.md order) and create
|
|
406
|
-
each member's bound branch at its origin
|
|
442
|
+
each member's bound branch at its recorded default branch on origin. None when
|
|
443
|
+
no task names a member."""
|
|
407
444
|
root, state = _coordinator(coordinator)
|
|
408
445
|
named = _task_members(root)
|
|
409
446
|
lock_path = root / LOCK_PATH
|
|
@@ -442,8 +479,8 @@ def lock_build_members(coordinator: Path) -> Optional[list[dict[str, str]]]:
|
|
|
442
479
|
remote = _git(checkout, "config", "--get", "remote.origin.url")
|
|
443
480
|
if remote != member["remote"]:
|
|
444
481
|
raise MembersError(f"member origin changed: {member['remote']} -> {remote}")
|
|
445
|
-
|
|
446
|
-
base =
|
|
482
|
+
default = default_branch(member)
|
|
483
|
+
base = require_origin_branch(checkout, default)
|
|
447
484
|
existing = _common.run_git(checkout, "rev-parse", "--verify", "--quiet", f"refs/heads/{branch}")
|
|
448
485
|
if existing.returncode == 0:
|
|
449
486
|
tip = existing.stdout.strip()
|
|
@@ -456,7 +493,7 @@ def lock_build_members(coordinator: Path) -> Optional[list[dict[str, str]]]:
|
|
|
456
493
|
elif _common.run_git(checkout, "merge-base", "--is-ancestor", tip, base).returncode == 0:
|
|
457
494
|
base = tip
|
|
458
495
|
else:
|
|
459
|
-
raise MembersError(f"member {member['name']} branch {branch} has commits not on origin/
|
|
496
|
+
raise MembersError(f"member {member['name']} branch {branch} has commits not on origin/{default}")
|
|
460
497
|
else:
|
|
461
498
|
missing.append((checkout, base))
|
|
462
499
|
entries.append({"name": member["name"], "branch": branch, "base": base})
|
|
@@ -473,6 +510,8 @@ def render(members: Sequence[dict[str, str]]) -> str:
|
|
|
473
510
|
f"Checkout: {member['checkout']}\n"
|
|
474
511
|
f"Remote: {member['remote']}\n"
|
|
475
512
|
f"Integration: {member['integration']}\n"
|
|
513
|
+
# A row that never had the field keeps none, so old rows are not rewritten.
|
|
514
|
+
+ (f"{DEFAULT_BRANCH_FIELD}: {member['default_branch']}\n" if "default_branch" in member else "")
|
|
476
515
|
for member in members
|
|
477
516
|
]
|
|
478
517
|
return "\n".join([HEADER, *sections])
|
|
@@ -486,7 +525,7 @@ def add_member(repo: Path, name: str, checkout: Path, integration: str) -> list[
|
|
|
486
525
|
raise MembersError(f"invalid member name: {name!r}")
|
|
487
526
|
members = read_members(root)
|
|
488
527
|
resolved = checkout.resolve()
|
|
489
|
-
remote = check_member(root, state.project, checkout)
|
|
528
|
+
remote, default = check_member(root, state.project, checkout)
|
|
490
529
|
common = Path(_git(resolved, "rev-parse", "--path-format=absolute", "--git-common-dir")).resolve()
|
|
491
530
|
identity = _remote_identity(remote)
|
|
492
531
|
for member in members:
|
|
@@ -497,7 +536,8 @@ def add_member(repo: Path, name: str, checkout: Path, integration: str) -> list[
|
|
|
497
536
|
raise MembersError(f"member already recorded: {member['name']}")
|
|
498
537
|
_refuse_foreign_marker(resolved, root, state.project, name)
|
|
499
538
|
members.append(
|
|
500
|
-
{"name": name, "checkout": str(resolved), "remote": remote, "integration": integration
|
|
539
|
+
{"name": name, "checkout": str(resolved), "remote": remote, "integration": integration,
|
|
540
|
+
"default_branch": default}
|
|
501
541
|
)
|
|
502
542
|
_write_marker(resolved, root, state.project, name)
|
|
503
543
|
_common.atomic_write(root / ".project" / MEMBERS_FILE, render(members))
|
|
@@ -558,8 +598,9 @@ def create_member(repo: Path, name: str, checkout: Path, integration: str, githu
|
|
|
558
598
|
_common.atomic_write(journal, json.dumps(recorded, indent=2, sort_keys=True) + "\n")
|
|
559
599
|
listed = [member for member in read_members(root) if member["name"] == name]
|
|
560
600
|
if listed:
|
|
561
|
-
if (len(listed) != 1 or listed[0]
|
|
562
|
-
|
|
601
|
+
if (len(listed) != 1 or {**listed[0], "default_branch": default_branch(listed[0])} != {
|
|
602
|
+
"name": name, "checkout": str(resolved), "remote": url,
|
|
603
|
+
"integration": integration, "default_branch": "main"}
|
|
563
604
|
or member_role(resolved) != {"coordinator": root, "project": state.project, "name": name}):
|
|
564
605
|
raise MembersError(f"member already recorded with a different checkout or marker: {name}")
|
|
565
606
|
journal.unlink()
|
|
@@ -608,6 +649,10 @@ def create_member(repo: Path, name: str, checkout: Path, integration: str, githu
|
|
|
608
649
|
cloned = _common.run_git(resolved.parent, "clone", "-q", url, str(resolved))
|
|
609
650
|
if cloned.returncode != 0:
|
|
610
651
|
raise MembersError(f"could not clone {url}: {(cloned.stderr or cloned.stdout).strip()}")
|
|
652
|
+
# ponytail: --create stays main-only, because a new repository uses main;
|
|
653
|
+
# `add` joins an existing repository with any default branch.
|
|
654
|
+
if _common.origin_default_branch(resolved) != "main":
|
|
655
|
+
raise MembersError(f"member remote default must be main: {resolved}")
|
|
611
656
|
recorded["step"] = "join"
|
|
612
657
|
_common.atomic_write(journal, json.dumps(recorded, indent=2, sort_keys=True) + "\n")
|
|
613
658
|
joined = add_member(root, name, resolved, integration)
|
|
@@ -620,7 +665,7 @@ def validate_members(repo: Path) -> list[dict[str, str]]:
|
|
|
620
665
|
members = read_members(root)
|
|
621
666
|
for member in members:
|
|
622
667
|
checkout = Path(member["checkout"])
|
|
623
|
-
check_member(root, state.project, checkout, member
|
|
668
|
+
check_member(root, state.project, checkout, member, refuse_reserved_refs=False)
|
|
624
669
|
try:
|
|
625
670
|
role = member_role(checkout)
|
|
626
671
|
except MembersError as error:
|
|
@@ -639,7 +684,7 @@ def repair_members(repo: Path) -> list[dict[str, str]]:
|
|
|
639
684
|
members = read_members(root)
|
|
640
685
|
for member in members:
|
|
641
686
|
checkout = Path(member["checkout"])
|
|
642
|
-
check_member(root, state.project, checkout, member
|
|
687
|
+
check_member(root, state.project, checkout, member, refuse_reserved_refs=False)
|
|
643
688
|
_refuse_foreign_marker(checkout.resolve(), root, state.project, member["name"])
|
|
644
689
|
for member in members:
|
|
645
690
|
checkout = Path(member["checkout"]).resolve()
|
|
@@ -202,7 +202,11 @@ def native_record(args):
|
|
|
202
202
|
identity = {'repo': str(args.repo.resolve()), 'project_dir': args.project_dir,
|
|
203
203
|
'scope': args.scope, 'assignment': args.assignment, 'role': args.role}
|
|
204
204
|
if data is not None and data.get('identity') != identity:
|
|
205
|
-
|
|
205
|
+
recorded = data.get('identity')
|
|
206
|
+
recorded = recorded if isinstance(recorded, dict) else {}
|
|
207
|
+
differing = ', '.join(f'{field}: {value}, record has {recorded.get(field)}'
|
|
208
|
+
for field, value in identity.items() if recorded.get(field) != value)
|
|
209
|
+
raise PolicyError(f'record belongs to another assignment or milestone/track/cycle ({differing})')
|
|
206
210
|
if args.action == 'release':
|
|
207
211
|
if data is None or not args.terminal_result:
|
|
208
212
|
raise PolicyError('release requires an existing record and terminal-result evidence')
|
|
@@ -13,7 +13,7 @@ sys.dont_write_bytecode = True
|
|
|
13
13
|
ROLES = ('coder', 'reviewer', 'review_panel', 'skeptic', 'inspect_codebase',
|
|
14
14
|
'inspect_docs', 'docs_audit', 'research', 'decide', 'roadmap', 'plan', 'plan_patch')
|
|
15
15
|
HOSTS = ('codex', 'claude', 'grok', 'opencode', 'copilot', 'qwen', 'antigravity',
|
|
16
|
-
'cursor', 'zed', 'kiro', 'kimi')
|
|
16
|
+
'cursor', 'zed', 'kiro', 'kimi', 'muse')
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def sibling(name):
|
|
@@ -73,8 +73,8 @@ def validate_value(key, value):
|
|
|
73
73
|
if not isinstance(value, str) or not value or value != value.strip():
|
|
74
74
|
raise ValueError(f'{key} requires a nonempty string')
|
|
75
75
|
if key == 'integration':
|
|
76
|
-
if value not in ('direct', 'pull-request'):
|
|
77
|
-
raise ValueError('integration must be direct or
|
|
76
|
+
if value not in ('direct', 'pull-request', 'external-landing'):
|
|
77
|
+
raise ValueError('integration must be direct, pull-request, or external-landing')
|
|
78
78
|
elif key == 'review_panel':
|
|
79
79
|
if any(char.isspace() for char in value):
|
|
80
80
|
raise ValueError('use off, detected, or comma-separated families without spaces')
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
# gsd-path project runtime
|
|
3
3
|
"""Deterministic GSD Path branch names and pipeline commit messages.
|
|
4
4
|
|
|
5
|
-
Bound work lives on `gsd-path/M00N` for that milestone. The
|
|
6
|
-
|
|
5
|
+
Bound work lives on `gsd-path/M00N` for that milestone. The remote default
|
|
6
|
+
that STATE.md records (`main` when unrecorded) stays a separate trunk. Ship
|
|
7
|
+
merges the bound branch onto it;
|
|
7
8
|
the next milestone binds a new unused `gsd-path/M00N` and retires the
|
|
8
9
|
integrated previous branch, locally and on origin.
|
|
9
10
|
"""
|
|
@@ -98,7 +99,8 @@ def ship_subject(archive_name: str) -> str:
|
|
|
98
99
|
|
|
99
100
|
|
|
100
101
|
def integrate_subject(archive_name: str, default_branch: str) -> str:
|
|
101
|
-
|
|
102
|
+
# The bare name, under the rule that records it; `origin/trunk` is a legal bare name.
|
|
103
|
+
if not _common.valid_default_branch(default_branch):
|
|
102
104
|
raise PipelineGitError(f"default branch is invalid: {default_branch}")
|
|
103
105
|
number = milestone_number(archive_name)
|
|
104
106
|
return (
|
|
@@ -146,6 +148,29 @@ def ship_commit_body(archive_path: str, reviewed_head: str, members: Sequence[di
|
|
|
146
148
|
)
|
|
147
149
|
|
|
148
150
|
|
|
151
|
+
_TRAILER_LINE = re.compile(r"^[A-Za-z-]+:\s")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def ship_body_matches(body: str, expected_body: str) -> bool:
|
|
155
|
+
"""True when body equals the contract or adds only optional git trailers after it."""
|
|
156
|
+
normalized = body.rstrip("\n")
|
|
157
|
+
expected = expected_body.rstrip("\n")
|
|
158
|
+
if normalized == expected:
|
|
159
|
+
return True
|
|
160
|
+
prefix = expected + "\n"
|
|
161
|
+
if not normalized.startswith(prefix):
|
|
162
|
+
return False
|
|
163
|
+
remainder = normalized[len(prefix):]
|
|
164
|
+
if not remainder:
|
|
165
|
+
return True
|
|
166
|
+
for line in remainder.splitlines():
|
|
167
|
+
if not line.strip():
|
|
168
|
+
return False
|
|
169
|
+
if not _TRAILER_LINE.match(line):
|
|
170
|
+
return False
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
|
|
149
174
|
def integrate_commit_body(
|
|
150
175
|
archive_path: str,
|
|
151
176
|
ship_commit: str,
|
|
@@ -270,22 +295,50 @@ def _symbolic_branch(repo: Path) -> str:
|
|
|
270
295
|
return result.stdout.strip()
|
|
271
296
|
|
|
272
297
|
|
|
273
|
-
def
|
|
274
|
-
"""Return
|
|
275
|
-
|
|
298
|
+
def worktree_list_porcelain(repo: Path) -> tuple[str, bool]:
|
|
299
|
+
"""Return worktree list output and whether it is NUL-separated."""
|
|
300
|
+
result = _run_git(repo, "worktree", "list", "--porcelain", "-z", check=False)
|
|
301
|
+
if result.returncode == 0:
|
|
302
|
+
return result.stdout, True
|
|
303
|
+
result = _run_git(repo, "worktree", "list", "--porcelain", check=False)
|
|
304
|
+
if result.returncode != 0:
|
|
305
|
+
message = (result.stderr or result.stdout).strip() or "could not list worktrees"
|
|
306
|
+
raise PipelineGitError(message)
|
|
307
|
+
return result.stdout, False
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def branch_worktrees_from_porcelain(output: str, *, nul_separated: bool) -> dict[str, Path]:
|
|
276
311
|
worktrees: dict[str, Path] = {}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
312
|
+
if nul_separated:
|
|
313
|
+
current_path: Optional[Path] = None
|
|
314
|
+
for field in output.split("\0"):
|
|
315
|
+
if not field:
|
|
316
|
+
continue
|
|
317
|
+
if field.startswith("worktree "):
|
|
318
|
+
current_path = Path(field.removeprefix("worktree ")).resolve()
|
|
319
|
+
elif field.startswith("branch ") and current_path is not None:
|
|
320
|
+
ref = field.removeprefix("branch ")
|
|
321
|
+
prefix = "refs/heads/"
|
|
322
|
+
if ref.startswith(prefix):
|
|
323
|
+
worktrees[ref.removeprefix(prefix)] = current_path
|
|
324
|
+
return worktrees
|
|
325
|
+
for block in output.strip().split("\n\n"):
|
|
326
|
+
fields = block.splitlines()
|
|
327
|
+
if not fields or not fields[0].startswith("worktree "):
|
|
328
|
+
continue
|
|
329
|
+
current_path = Path(fields[0].removeprefix("worktree ")).resolve()
|
|
330
|
+
for line in fields[1:]:
|
|
331
|
+
if line.startswith("branch refs/heads/"):
|
|
332
|
+
worktrees[line.removeprefix("branch refs/heads/")] = current_path
|
|
286
333
|
return worktrees
|
|
287
334
|
|
|
288
335
|
|
|
336
|
+
def _branch_worktrees(repo: Path) -> dict[str, Path]:
|
|
337
|
+
"""Return every checked-out local branch from Git's worktree registry."""
|
|
338
|
+
output, nul_separated = worktree_list_porcelain(repo)
|
|
339
|
+
return branch_worktrees_from_porcelain(output, nul_separated=nul_separated)
|
|
340
|
+
|
|
341
|
+
|
|
289
342
|
def _remote_ref_sha(repo: Path, ref: str) -> Optional[str]:
|
|
290
343
|
if not _origin_remote_exists(repo):
|
|
291
344
|
raise PipelineGitError("origin remote is not configured")
|
|
@@ -312,7 +365,7 @@ def _remote_branch_exists(repo: Path, branch: str) -> bool:
|
|
|
312
365
|
return _remote_ref_sha(repo, f"refs/heads/{branch}") is not None
|
|
313
366
|
|
|
314
367
|
|
|
315
|
-
def
|
|
368
|
+
def _milestone_tag_fields(repo: Path, ref: str) -> dict[str, str]:
|
|
316
369
|
tag_type = _run_git(repo, "cat-file", "-t", ref, check=False)
|
|
317
370
|
if tag_type.returncode != 0 or tag_type.stdout.strip() != "tag":
|
|
318
371
|
return {}
|
|
@@ -322,7 +375,7 @@ def _pull_request_tag_fields(repo: Path, ref: str) -> dict[str, str]:
|
|
|
322
375
|
match = re.fullmatch(r"(Mode|Pull-Request|Ship|Landing): (.+)", line)
|
|
323
376
|
if match:
|
|
324
377
|
if match.group(1) in fields:
|
|
325
|
-
raise PipelineGitError("
|
|
378
|
+
raise PipelineGitError("milestone tag repeats metadata")
|
|
326
379
|
fields[match.group(1)] = match.group(2)
|
|
327
380
|
return fields
|
|
328
381
|
|
|
@@ -353,7 +406,15 @@ def _validated_shipped_state(repo: Path) -> dict[str, object]:
|
|
|
353
406
|
return state
|
|
354
407
|
|
|
355
408
|
|
|
356
|
-
|
|
409
|
+
# Metadata a published milestone tag must carry, per integration mode, before
|
|
410
|
+
# bind-next accepts a previous bound branch that the forge deleted on merge.
|
|
411
|
+
TAG_PROOF_FIELDS = {
|
|
412
|
+
"pull-request": {"Mode", "Pull-Request", "Ship", "Landing"},
|
|
413
|
+
"external-landing": {"Mode", "Ship", "Landing"},
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _require_tag_integration_proof(
|
|
357
418
|
repo: Path,
|
|
358
419
|
ship: str,
|
|
359
420
|
base: str,
|
|
@@ -364,8 +425,11 @@ def _require_pull_request_integration_proof(
|
|
|
364
425
|
raise PipelineGitError("bind-next requires shipped/done state")
|
|
365
426
|
if state.get("branch") != previous_branch:
|
|
366
427
|
raise PipelineGitError("shipped state does not name the previous branch")
|
|
367
|
-
|
|
368
|
-
|
|
428
|
+
mode = state.get("integration")
|
|
429
|
+
if mode not in TAG_PROOF_FIELDS:
|
|
430
|
+
raise PipelineGitError(
|
|
431
|
+
"shipped integration mode is not pull-request or external-landing"
|
|
432
|
+
)
|
|
369
433
|
archive = state.get("archive")
|
|
370
434
|
if not isinstance(archive, str):
|
|
371
435
|
raise PipelineGitError("shipped state does not name an archive")
|
|
@@ -376,24 +440,23 @@ def _require_pull_request_integration_proof(
|
|
|
376
440
|
):
|
|
377
441
|
raise PipelineGitError("shipped archive does not match the previous branch")
|
|
378
442
|
ref = f"refs/tags/milestone/{match.group(1)}"
|
|
379
|
-
fields =
|
|
380
|
-
if set(fields) !=
|
|
381
|
-
raise PipelineGitError("
|
|
382
|
-
if fields["Mode"] !=
|
|
383
|
-
raise PipelineGitError("
|
|
384
|
-
|
|
385
|
-
if re.fullmatch(
|
|
443
|
+
fields = _milestone_tag_fields(repo, ref)
|
|
444
|
+
if set(fields) != TAG_PROOF_FIELDS[mode]:
|
|
445
|
+
raise PipelineGitError(f"{mode} integration proof is incomplete")
|
|
446
|
+
if fields["Mode"] != mode or fields["Ship"] != ship:
|
|
447
|
+
raise PipelineGitError(f"{mode} integration proof does not match shipped state")
|
|
448
|
+
if mode == "pull-request" and re.fullmatch(
|
|
386
449
|
r"https://github\.com/[^/]+/[^/]+/pull/[1-9][0-9]*",
|
|
387
|
-
|
|
450
|
+
fields["Pull-Request"],
|
|
388
451
|
) is None:
|
|
389
452
|
raise PipelineGitError("pull-request integration proof has an invalid PR URL")
|
|
390
453
|
landing = fields["Landing"]
|
|
391
454
|
if re.fullmatch(r"[0-9a-f]{40}", landing) is None:
|
|
392
|
-
raise PipelineGitError("
|
|
455
|
+
raise PipelineGitError(f"{mode} integration proof has an invalid landing")
|
|
393
456
|
local_object = _run_git(repo, "rev-parse", ref).stdout.strip()
|
|
394
457
|
local_target = _run_git(repo, "rev-parse", f"{ref}^{{commit}}").stdout.strip()
|
|
395
458
|
if local_target != landing:
|
|
396
|
-
raise PipelineGitError("
|
|
459
|
+
raise PipelineGitError(f"{mode} integration tag does not point at landing")
|
|
397
460
|
remote = _run_git(
|
|
398
461
|
repo,
|
|
399
462
|
"ls-remote",
|
|
@@ -405,14 +468,14 @@ def _require_pull_request_integration_proof(
|
|
|
405
468
|
check=False,
|
|
406
469
|
)
|
|
407
470
|
if remote.returncode != 0:
|
|
408
|
-
raise PipelineGitError("
|
|
471
|
+
raise PipelineGitError(f"{mode} integration tag is not published")
|
|
409
472
|
rows = dict(
|
|
410
473
|
line.split("\t", 1)[::-1]
|
|
411
474
|
for line in remote.stdout.splitlines()
|
|
412
475
|
if "\t" in line
|
|
413
476
|
)
|
|
414
477
|
if rows != {ref: local_object, f"{ref}^{{}}": landing}:
|
|
415
|
-
raise PipelineGitError("published
|
|
478
|
+
raise PipelineGitError(f"published {mode} integration tag differs")
|
|
416
479
|
parents = _run_git(
|
|
417
480
|
repo,
|
|
418
481
|
"rev-list",
|
|
@@ -422,11 +485,11 @@ def _require_pull_request_integration_proof(
|
|
|
422
485
|
landing,
|
|
423
486
|
).stdout.split()
|
|
424
487
|
if len(parents) != 3 or parents[2] != ship:
|
|
425
|
-
raise PipelineGitError("
|
|
488
|
+
raise PipelineGitError(f"{mode} integration proof has invalid merge topology")
|
|
426
489
|
first_parent = _run_git(repo, "rev-list", "--first-parent", base).stdout.splitlines()
|
|
427
490
|
if landing not in first_parent:
|
|
428
491
|
raise PipelineGitError(
|
|
429
|
-
"
|
|
492
|
+
f"{mode} integration landing is not on the default branch first-parent history"
|
|
430
493
|
)
|
|
431
494
|
return landing
|
|
432
495
|
|
|
@@ -482,6 +545,34 @@ def _write_bind_next_journal(path: Path, value: dict[str, object]) -> None:
|
|
|
482
545
|
temporary.unlink()
|
|
483
546
|
|
|
484
547
|
|
|
548
|
+
def _require_state_remote_default(repo: Path, remote_default: str) -> str:
|
|
549
|
+
"""Return the default branch STATE.md records; `remote_default` must name it.
|
|
550
|
+
|
|
551
|
+
No STATE.md yet (bind-initial before initialize) or no field means main.
|
|
552
|
+
An unreadable STATE.md also means main, as before the field existed: the
|
|
553
|
+
callers that need a valid state check it themselves.
|
|
554
|
+
"""
|
|
555
|
+
if __package__:
|
|
556
|
+
from . import pipeline_state
|
|
557
|
+
else:
|
|
558
|
+
try:
|
|
559
|
+
import pipeline_state
|
|
560
|
+
except ImportError: # pragma: no cover - package import used by tests
|
|
561
|
+
from scripts import pipeline_state
|
|
562
|
+
|
|
563
|
+
default_name, unreadable = "main", ""
|
|
564
|
+
if (repo / ".project" / "STATE.md").exists():
|
|
565
|
+
try:
|
|
566
|
+
default_name = pipeline_state.load_state(repo)[0].default_branch
|
|
567
|
+
except (OSError, pipeline_state.PipelineStateError) as error:
|
|
568
|
+
unreadable = f" (STATE.md is unreadable: {error})"
|
|
569
|
+
if remote_default != f"origin/{default_name}":
|
|
570
|
+
raise PipelineGitError(
|
|
571
|
+
f"remote default must be origin/{default_name}, got {remote_default}{unreadable}"
|
|
572
|
+
)
|
|
573
|
+
return default_name
|
|
574
|
+
|
|
575
|
+
|
|
485
576
|
def _initial_binding_snapshot(repo: Path) -> Optional[tuple]:
|
|
486
577
|
"""Admit only clean work or the validated, untracked initializer state."""
|
|
487
578
|
dirty = _run_git(repo, "status", "--porcelain=v1", "-z", "--untracked-files=all").stdout
|
|
@@ -528,8 +619,7 @@ def bind_initial_milestone_branch(
|
|
|
528
619
|
resolved = _require_worktree_root(repo)
|
|
529
620
|
if not is_bound_branch(branch):
|
|
530
621
|
raise PipelineGitError(f"invalid bound branch: {branch}")
|
|
531
|
-
|
|
532
|
-
raise PipelineGitError(f"remote default must be origin/main, got {remote_default}")
|
|
622
|
+
default_name = _require_state_remote_default(resolved, remote_default)
|
|
533
623
|
|
|
534
624
|
default_sha = _run_git(
|
|
535
625
|
resolved,
|
|
@@ -549,7 +639,7 @@ def bind_initial_milestone_branch(
|
|
|
549
639
|
raise PipelineGitError(
|
|
550
640
|
f"validated base is stale: {base_sha} != {remote_default} {default_sha}"
|
|
551
641
|
)
|
|
552
|
-
remote_sha = _remote_ref_sha(resolved, "refs/heads/
|
|
642
|
+
remote_sha = _remote_ref_sha(resolved, f"refs/heads/{default_name}")
|
|
553
643
|
if remote_sha != default_sha:
|
|
554
644
|
raise PipelineGitError(
|
|
555
645
|
f"fetched {remote_default} is stale relative to origin"
|
|
@@ -691,8 +781,7 @@ def bind_next_milestone_branch(
|
|
|
691
781
|
raise PipelineGitError(f"invalid previous bound branch: {previous_branch}")
|
|
692
782
|
if milestone_number(branch) <= milestone_number(previous_branch):
|
|
693
783
|
raise PipelineGitError(f"next branch {branch} must follow {previous_branch}")
|
|
694
|
-
|
|
695
|
-
raise PipelineGitError(f"remote default must be origin/main, got {remote_default}")
|
|
784
|
+
default_name = _require_state_remote_default(repo, remote_default)
|
|
696
785
|
|
|
697
786
|
default_sha = _run_git(
|
|
698
787
|
repo,
|
|
@@ -712,7 +801,7 @@ def bind_next_milestone_branch(
|
|
|
712
801
|
raise PipelineGitError(
|
|
713
802
|
f"validated base is stale: {base_sha} != {remote_default} {default_sha}"
|
|
714
803
|
)
|
|
715
|
-
live_default_sha = _remote_ref_sha(repo, "refs/heads/
|
|
804
|
+
live_default_sha = _remote_ref_sha(repo, f"refs/heads/{default_name}")
|
|
716
805
|
if live_default_sha != default_sha:
|
|
717
806
|
raise PipelineGitError(f"fetched {remote_default} is stale relative to origin")
|
|
718
807
|
ship_sha = _run_git(
|
|
@@ -770,7 +859,7 @@ def bind_next_milestone_branch(
|
|
|
770
859
|
detail = landing_integrated.stderr.strip() or landing_integrated.stdout.strip()
|
|
771
860
|
raise PipelineGitError(f"could not verify integrated branch: {detail}")
|
|
772
861
|
if allow_remote_absent:
|
|
773
|
-
proven_landing =
|
|
862
|
+
proven_landing = _require_tag_integration_proof(
|
|
774
863
|
repo,
|
|
775
864
|
ship_sha,
|
|
776
865
|
base_sha,
|
|
@@ -778,7 +867,7 @@ def bind_next_milestone_branch(
|
|
|
778
867
|
)
|
|
779
868
|
if proven_landing != landing_sha:
|
|
780
869
|
raise PipelineGitError(
|
|
781
|
-
"validated landing does not match
|
|
870
|
+
"validated landing does not match the milestone tag integration proof"
|
|
782
871
|
)
|
|
783
872
|
|
|
784
873
|
symbolic_branch = _run_git(
|